diff --git a/client/src/BattleRoom.lua b/client/src/BattleRoom.lua index 30661273..790b6d20 100644 --- a/client/src/BattleRoom.lua +++ b/client/src/BattleRoom.lua @@ -448,6 +448,7 @@ function BattleRoom:restoreInputConfigurations() end -- Gets all local human players in the battle room +---@return Player[] localHumanPlayers function BattleRoom:getLocalHumanPlayers() local localPlayers = {} for _, player in ipairs(self.players) do diff --git a/client/src/Game.lua b/client/src/Game.lua index 3f05bb2c..e9b40a53 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -376,6 +376,10 @@ function Game:onJoystickAdded(joystick) self.input:onJoystickAdded(joystick) end +function Game:onJoystickRemoved(joystick) + self.input:onJoystickRemoved(joystick) +end + -- Setup signal listener for unconfigured joysticks function Game:setupInputSignals() self.input:connectSignal("unconfiguredJoystickAdded", SceneCoordinator, SceneCoordinator.onUnconfiguredJoystickAdded) diff --git a/client/src/config.lua b/client/src/config.lua index cafb3ab7..6b6a6f2d 100644 --- a/client/src/config.lua +++ b/client/src/config.lua @@ -140,7 +140,7 @@ config = { function() local encoded = json.encode(config) ---@cast encoded string - love.filesystem.write("conf.json", json.encode(config)) + love.filesystem.write("conf.json", encoded) end ) end diff --git a/client/src/input/InputConfiguration.lua b/client/src/input/InputConfiguration.lua index 63130e61..726fb4e0 100644 --- a/client/src/input/InputConfiguration.lua +++ b/client/src/input/InputConfiguration.lua @@ -4,6 +4,8 @@ local util = require("common.lib.util") local joystickManager = require("common.lib.joystickManager") require("client.src.input.JoystickProvider") +---@alias InputDeviceType ("keyboard" | "controller" | "touch" | nil) + -- Represents a single input configuration slot with key bindings ---@class InputConfiguration ---@field index number Configuration slot number (1-8) @@ -15,7 +17,7 @@ require("client.src.input.JoystickProvider") ---@field isPressedWithRepeat function ---@field joystickProvider JoystickProvider ---@field id string Unique identifier (e.g., "config_1") ----@field deviceType string? Device type ("keyboard", "controller", "touch", or nil if empty) +---@field deviceType InputDeviceType Device type ("keyboard", "controller", "touch", or nil if empty) ---@field deviceName string? Human-readable device name ---@field controllerImageVariant string? Controller icon variant ---@field deviceNumber number? Device count of this type (e.g., 2nd keyboard) @@ -107,7 +109,7 @@ function InputConfiguration:parseControllerBinding(keyName) end -- Determine device type based on the first available binding ----@return "keyboard"|"controller"|"touch"|nil deviceType Type of device or nil if no bindings +---@return InputDeviceType deviceType Type of device or nil if no bindings function InputConfiguration:getDeviceType() if self:isEmpty() then return nil diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index b450c89f..2a979358 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -25,6 +25,7 @@ local inputManager = { isUp = {}, allKeys = {isDown = {}, isPressed = {}, isUp = {}}, mouse = {isDown = {}, isPressed = {}, isUp = {}, x = 0, y = 0}, + ---@type InputConfiguration[] inputConfigurations = {}, maxConfigurations = 8, hasUnsavedChanges = false, @@ -82,7 +83,8 @@ end function inputManager:onJoystickAdded(joystick) joystickManager:registerJoystick(joystick) - local unconfiguredJoysticks = self:updateUnconfiguredJoysticksCache() + self:updateUnconfiguredJoysticksCache() + local unconfiguredJoysticks = self:getUnconfiguredJoysticks() -- Check if the newly added joystick is unconfigured for _, unconfiguredJoystick in ipairs(unconfiguredJoysticks) do @@ -94,36 +96,26 @@ function inputManager:onJoystickAdded(joystick) end function inputManager:onJoystickRemoved(joystick) - -- GUID identifies the device type, 2 controllers of the same type will have a matching GUID - -- the GUID is consistent across sessions - local guid = joystick:getGUID() - -- ID is a per-session identifier for each controller regardless of type - local id = joystick:getID() - - local vendorID, productID, productVersion = joystick:getDeviceInfo() - - logger.info("Disconnecting device " .. vendorID .. ";" .. productID .. ";" .. productVersion .. ";" .. joystick:getName() .. ";" .. guid .. ";" .. id) - - if joystickManager.guidsToJoysticks[guid] then - joystickManager.guidsToJoysticks[guid][id] = nil - - if tableUtils.length(joystickManager.guidsToJoysticks[guid]) == 0 then - joystickManager.guidsToJoysticks[guid] = nil - end - end - - joystickManager.devices[id] = nil + joystickManager:unregisterJoystick(joystick) self:updateUnconfiguredJoysticksCache() end function inputManager:joystickPressed(joystick, button) - joystickManager:registerJoystick(joystick) + if not joystickManager:isRegistered(joystick) then + -- always check and register to be sure, in rare cases joystickadded is not called or not called early enough + joystickManager:registerJoystick(joystick) + end + local key = joystickManager:getJoystickButtonName(joystick, button) self.allKeys.isDown[key] = KEY_CHANGE.DETECTED end function inputManager:joystickReleased(joystick, button) - joystickManager:registerJoystick(joystick) + if not joystickManager:isRegistered(joystick) then + -- always check and register to be sure, in rare cases joystickadded is not called or not called early enough + joystickManager:registerJoystick(joystick) + end + local key = joystickManager:getJoystickButtonName(joystick, button) self.allKeys.isUp[key] = KEY_CHANGE.DETECTED end @@ -464,14 +456,14 @@ function inputManager:getSaveKeyMap() return result end -function inputManager:write_key_file() +function inputManager:writeKeyConfigurationToFile() FileUtils.writeJson("", "keysV3.json", self:getSaveKeyMap()) self.hasUnsavedChanges = false end -- Saves input configuration mappings to disk function inputManager:saveInputConfigurationMappings() - self:write_key_file() + self:writeKeyConfigurationToFile() end @@ -529,12 +521,14 @@ function inputManager:importConfigurations(configurations) end end -- Update all cached properties after importing - for _, config in ipairs(self.inputConfigurations) do - config:updateCachedProperties() + for _, inputConfig in ipairs(self.inputConfigurations) do + inputConfig:updateCachedProperties() end self:updateAllDeviceNumbers() end +---@param player Player +---@param inputConfiguration InputConfiguration function inputManager:claimConfiguration(player, inputConfiguration) if inputConfiguration.claimed and inputConfiguration.player ~= player then error("Trying to assign input configuration to player " .. player.playerNumber .. @@ -549,6 +543,8 @@ function inputManager:claimConfiguration(player, inputConfiguration) return inputConfiguration end +---@param player Player +---@param inputConfiguration InputConfiguration function inputManager:releaseConfiguration(player, inputConfiguration) if not inputConfiguration.claimed then error("Trying to release an unclaimed inputConfiguration") @@ -567,21 +563,21 @@ end function inputManager:updateAllDeviceNumbers() local deviceTypeCounters = {} - for _, config in ipairs(self.inputConfigurations) do + for _, inputConfig in ipairs(self.inputConfigurations) do -- Only count non-empty configurations with bindings - if not config:isEmpty() and config.deviceType then - deviceTypeCounters[config.deviceType] = (deviceTypeCounters[config.deviceType] or 0) + 1 - config.deviceNumber = deviceTypeCounters[config.deviceType] + if not inputConfig:isEmpty() and inputConfig.deviceType then + deviceTypeCounters[inputConfig.deviceType] = (deviceTypeCounters[inputConfig.deviceType] or 0) + 1 + inputConfig.deviceNumber = deviceTypeCounters[inputConfig.deviceType] else - config.deviceNumber = nil + inputConfig.deviceNumber = nil end end end -- Updates a specific InputConfiguration when its bindings change ----@param config InputConfiguration Configuration to update -function inputManager:updateInputConfiguration(config) - config:update() +---@param inputConfig InputConfiguration Configuration to update +function inputManager:updateInputConfiguration(inputConfig) + inputConfig:update() self:updateAllDeviceNumbers() end @@ -592,10 +588,10 @@ function inputManager:clearButtonFromAllConfigs(buttonBinding) return end - for _, config in ipairs(self.inputConfigurations) do + for _, inputConfig in ipairs(self.inputConfigurations) do for _, keyName in ipairs(consts.KEY_NAMES) do - if config[keyName] == buttonBinding then - config[keyName] = nil + if inputConfig[keyName] == buttonBinding then + inputConfig[keyName] = nil end end end @@ -619,7 +615,7 @@ function inputManager:changeKeyBindingOnInputConfiguration(inputConfiguration, k self:updateInputConfiguration(inputConfiguration) self:updateUnconfiguredJoysticksCache() if not skipSave then - self:write_key_file() + self:writeKeyConfigurationToFile() end end @@ -632,7 +628,7 @@ function inputManager:clearKeyBindingsOnInputConfiguration(inputConfiguration) self.hasUnsavedChanges = true self:updateInputConfiguration(inputConfiguration) self:updateUnconfiguredJoysticksCache() - self:write_key_file() + self:writeKeyConfigurationToFile() end function inputManager:setupDefaultKeyConfigurations() @@ -682,19 +678,19 @@ function inputManager:setupDefaultKeyConfigurations() end -- Update all cached properties after setting defaults - for _, config in ipairs(self.inputConfigurations) do - config:updateCachedProperties() + for _, inputConfig in ipairs(self.inputConfigurations) do + inputConfig:updateCachedProperties() end self:updateAllDeviceNumbers() end ----@return table? Input configuration with active input, or nil +---@return InputConfiguration? Input configuration with active input, or nil function inputManager:detectActiveInputConfiguration() for i = 1, #self.inputConfigurations do - local config = self.inputConfigurations[i] + local inputConfig = self.inputConfigurations[i] for _, keyName in ipairs(consts.KEY_NAMES) do - if config.isDown and config.isDown[keyName] then - return config + if inputConfig.isDown and inputConfig.isDown[keyName] then + return inputConfig end end end @@ -702,10 +698,10 @@ function inputManager:detectActiveInputConfiguration() return nil end ----@param battleRoom BattleRoom? +---@param localHumanPlayers Player[] ---@return boolean True if an unassigned configuration has active input -function inputManager:checkForUnassignedConfigurationInputs(battleRoom) - if not battleRoom then +function inputManager:checkForUnassignedConfigurationInputs(localHumanPlayers) + if #localHumanPlayers == 0 then return false end @@ -715,7 +711,7 @@ function inputManager:checkForUnassignedConfigurationInputs(battleRoom) end local assignedConfigs = {} - for _, player in ipairs(battleRoom:getLocalHumanPlayers()) do + for _, player in ipairs(localHumanPlayers) do if player.inputConfiguration then assignedConfigs[player.inputConfiguration] = true end @@ -729,10 +725,10 @@ end function inputManager:getConfiguredJoystickGuids() local configuredGuids = {} for i = 1, self.maxConfigurations do - local config = self.inputConfigurations[i] - if config then + local inputConfig = self.inputConfigurations[i] + if inputConfig then for _, keyName in ipairs(consts.KEY_NAMES) do - local keyMapping = config[keyName] + local keyMapping = inputConfig[keyName] if keyMapping and type(keyMapping) == "string" then -- Extract GUID from mapping format like "guid:id:button" local guid = keyMapping:match("^([^:]+):") @@ -765,7 +761,6 @@ function inputManager:updateUnconfiguredJoysticksCache() -- Update the cache self.unconfiguredJoysticksCache = unconfiguredJoysticks - return unconfiguredJoysticks end -- Gets a list of joysticks that don't have input configurations @@ -848,21 +843,21 @@ function inputManager:autoConfigureJoystick(joystick, shouldSave) -- Only proceed if we got at least some mappings if next(basicMapping) then -- Ensure the configuration slot has all the keys we need - local config = self.inputConfigurations[configIndex] + local inputConfig = self.inputConfigurations[configIndex] for keyName, keyMapping in pairs(basicMapping) do - self:changeKeyBindingOnInputConfiguration(config, keyName, keyMapping, true) + self:changeKeyBindingOnInputConfiguration(inputConfig, keyName, keyMapping, true) end -- Make sure all required keys are set (fill any missing ones with nil to be explicit) for _, keyName in ipairs(consts.KEY_NAMES) do - if config[keyName] == nil and not basicMapping[keyName] then - self:changeKeyBindingOnInputConfiguration(config, keyName, nil, true) + if inputConfig[keyName] == nil and not basicMapping[keyName] then + self:changeKeyBindingOnInputConfiguration(inputConfig, keyName, nil, true) end end self:updateUnconfiguredJoysticksCache() if shouldSave then - self:write_key_file() + self:writeKeyConfigurationToFile() end return configIndex end @@ -877,9 +872,9 @@ function inputManager:getAssignableDevices() local devices = {} -- Add all non-empty InputConfigurations - for _, config in ipairs(self.inputConfigurations) do - if not config:isEmpty() then - devices[#devices + 1] = config + for _, inputConfig in ipairs(self.inputConfigurations) do + if not inputConfig:isEmpty() then + devices[#devices + 1] = inputConfig end end diff --git a/client/src/scenes/components/InputDeviceOverlay.lua b/client/src/scenes/components/InputDeviceOverlay.lua index f718e990..3759a898 100644 --- a/client/src/scenes/components/InputDeviceOverlay.lua +++ b/client/src/scenes/components/InputDeviceOverlay.lua @@ -442,7 +442,7 @@ end ---@param dt number Delta time in seconds function InputDeviceOverlay:updateSelf(dt) if not self.active then - if not self.battleRoom.spectating and GAME.input:checkForUnassignedConfigurationInputs(self.battleRoom) then + if not self.battleRoom.spectating and GAME.input:checkForUnassignedConfigurationInputs(self.battleRoom:getLocalHumanPlayers()) then self.battleRoom:releaseAllLocalAssignments() end diff --git a/client/src/ui/UIElement.lua b/client/src/ui/UIElement.lua index 0ab2c398..a86166f8 100644 --- a/client/src/ui/UIElement.lua +++ b/client/src/ui/UIElement.lua @@ -271,7 +271,7 @@ end ---Returns a formatted tree of this element and all children with class name, TYPE, and root position ---@return string -function UIElement:debugTree() +function UIElement:toStringWithDepth() local function getElementInfo(element, depth) local indent = string.rep(" ", depth) local typeStr = element.TYPE and (" [" .. element.TYPE .. "]") or "" @@ -292,7 +292,7 @@ end ---Returns a formatted list of this element and its direct children only (non-recursive) ---@return string -function UIElement:debugChildren() +function UIElement:toString() local typeStr = self.TYPE and (" [" .. self.TYPE .. "]") or "" local x, y = self:getScreenPos() local lines = {string.format("%s @ (%.1f, %.1f)", typeStr, x, y)} diff --git a/common/engine/Match.lua b/common/engine/Match.lua index 09516308..cb7433fb 100644 --- a/common/engine/Match.lua +++ b/common/engine/Match.lua @@ -12,7 +12,6 @@ local LegacyPanelSource = require("common.compatibility.LegacyPanelSource") local InputCompression = require("common.data.InputCompression") local ReplayV3 = require("common.data.ReplayV3") local MatchRules = require("common.data.MatchRules") -local DebugSettings = require("client.src.debug.DebugSettings") ---@class Match ---@field stacks (Stack | SimulatedStack)[] The stacks to run as part of the match diff --git a/common/lib/joystickManager.lua b/common/lib/joystickManager.lua index 069a6aa0..39b395fc 100644 --- a/common/lib/joystickManager.lua +++ b/common/lib/joystickManager.lua @@ -40,6 +40,7 @@ local joystickHatToDirs = { rd = {"right", "down"} } +---@param joystick love.Joystick function joystickManager:getJoystickButtonName(joystick, button) return string.format("%s:%s:%s", joystick:getGUID(), joystickManager.guidsToJoysticks[joystick:getGUID()][joystick:getID()], button) end @@ -90,6 +91,7 @@ end -- end -- maps dpad dir to buttons +---@param joystick love.Joystick function joystickManager:getDPadState(joystick, hatIndex) local dir = joystick:getHat(hatIndex) local activeButtons = joystickHatToDirs[dir] @@ -101,12 +103,14 @@ function joystickManager:getDPadState(joystick, hatIndex) } end -function joystickManager:registerJoystick(joystick) - - if joystickManager.devices[joystick:getID()] then - return - end +---@param joystick love.Joystick +function joystickManager:isRegistered(joystick) + -- converting the joystick into a bool + return not not joystickManager.devices[joystick:getID()] +end +---@param joystick love.Joystick +function joystickManager:registerJoystick(joystick) -- GUID identifies the device type, 2 controllers of the same type will have a matching GUID -- the GUID is consistent across sessions local guid = joystick:getGUID() @@ -170,4 +174,27 @@ function joystickManager:registerJoystick(joystick) joystickManager.devices[id] = device end +---@param joystick love.Joystick +function joystickManager:unregisterJoystick(joystick) +-- GUID identifies the device type, 2 controllers of the same type will have a matching GUID + -- the GUID is consistent across sessions + local guid = joystick:getGUID() + -- ID is a per-session identifier for each controller regardless of type + local id = joystick:getID() + + local vendorID, productID, productVersion = joystick:getDeviceInfo() + + logger.info("Disconnecting device " .. vendorID .. ";" .. productID .. ";" .. productVersion .. ";" .. joystick:getName() .. ";" .. guid .. ";" .. id) + + if joystickManager.guidsToJoysticks[guid] then + joystickManager.guidsToJoysticks[guid][id] = nil + + if tableUtils.length(joystickManager.guidsToJoysticks[guid]) == 0 then + joystickManager.guidsToJoysticks[guid] = nil + end + end + + joystickManager.devices[id] = nil +end + return joystickManager \ No newline at end of file diff --git a/main.lua b/main.lua index 7f2c5f4e..45a34938 100644 --- a/main.lua +++ b/main.lua @@ -196,7 +196,7 @@ end -- Intentional override ---@diagnostic disable-next-line: duplicate-set-field function love.joystickremoved(joystick) - inputManager:onJoystickRemoved(joystick) + GAME:onJoystickRemoved(joystick) end -- Handle a touch press