From fc5eb60c174e47d9b71c74809d4965afad239383 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 13:16:16 +0100 Subject: [PATCH 1/9] move unregister process for joysticks into joystickManager remove return from updating the cache, just call the getter instead in the one location it is required --- client/src/inputManager.lua | 24 +++--------------------- common/lib/joystickManager.lua | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index b450c89f..574859cc 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -82,7 +82,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,25 +95,7 @@ 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 @@ -765,7 +748,6 @@ function inputManager:updateUnconfiguredJoysticksCache() -- Update the cache self.unconfiguredJoysticksCache = unconfiguredJoysticks - return unconfiguredJoysticks end -- Gets a list of joysticks that don't have input configurations diff --git a/common/lib/joystickManager.lua b/common/lib/joystickManager.lua index 069a6aa0..5f6ecd27 100644 --- a/common/lib/joystickManager.lua +++ b/common/lib/joystickManager.lua @@ -170,4 +170,26 @@ function joystickManager:registerJoystick(joystick) joystickManager.devices[id] = device end +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 From 3018ccf1f6603beb8de5365dea242c5ab526d5ba Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 13:36:00 +0100 Subject: [PATCH 2/9] extract check for joystick being registered on inputs for more clarity --- client/src/inputManager.lua | 12 ++++++++++-- common/lib/joystickManager.lua | 15 ++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index 574859cc..ecd6d6ea 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -100,13 +100,21 @@ function inputManager:onJoystickRemoved(joystick) 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 diff --git a/common/lib/joystickManager.lua b/common/lib/joystickManager.lua index 5f6ecd27..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,6 +174,7 @@ 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 From 8442931cd54b19b678fcacd0cb8d81539d13de45 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 13:38:10 +0100 Subject: [PATCH 3/9] rename write_key_file to writeKeyConfigurationToFile --- client/src/inputManager.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index ecd6d6ea..e8ce8d70 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -455,14 +455,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 @@ -610,7 +610,7 @@ function inputManager:changeKeyBindingOnInputConfiguration(inputConfiguration, k self:updateInputConfiguration(inputConfiguration) self:updateUnconfiguredJoysticksCache() if not skipSave then - self:write_key_file() + self:writeKeyConfigurationToFile() end end @@ -623,7 +623,7 @@ function inputManager:clearKeyBindingsOnInputConfiguration(inputConfiguration) self.hasUnsavedChanges = true self:updateInputConfiguration(inputConfiguration) self:updateUnconfiguredJoysticksCache() - self:write_key_file() + self:writeKeyConfigurationToFile() end function inputManager:setupDefaultKeyConfigurations() @@ -852,7 +852,7 @@ function inputManager:autoConfigureJoystick(joystick, shouldSave) self:updateUnconfiguredJoysticksCache() if shouldSave then - self:write_key_file() + self:writeKeyConfigurationToFile() end return configIndex end From ac73821e60fd1b1f772df49572daafeea7e12657 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 13:44:44 +0100 Subject: [PATCH 4/9] rename all local config variables to inputConfig to prevent confusion with the config global --- client/src/inputManager.lua | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/client/src/inputManager.lua b/client/src/inputManager.lua index e8ce8d70..27b59802 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -520,8 +520,8 @@ 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 @@ -558,21 +558,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 @@ -583,10 +583,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 @@ -673,8 +673,8 @@ 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 @@ -682,10 +682,10 @@ end ---@return table? 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 @@ -720,10 +720,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("^([^:]+):") @@ -838,15 +838,15 @@ 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 @@ -867,9 +867,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 From 9dd0443a5fba94447b90cbe304be761ff1f5f059 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 14:08:42 +0100 Subject: [PATCH 5/9] add some annotations for inputManager/InputConfiguration --- client/src/input/InputConfiguration.lua | 6 ++++-- client/src/inputManager.lua | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) 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 27b59802..2beeb811 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, @@ -526,6 +527,8 @@ function inputManager:importConfigurations(configurations) 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 .. @@ -540,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") @@ -679,7 +684,7 @@ function inputManager:setupDefaultKeyConfigurations() 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 inputConfig = self.inputConfigurations[i] From c202411094586219505bd896d2b6fb8bc16572c0 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 14:17:09 +0100 Subject: [PATCH 6/9] change checkForUnassignedConfigurationInputs to directly take a player array as its argument --- client/src/BattleRoom.lua | 1 + client/src/inputManager.lua | 8 ++++---- client/src/scenes/components/InputDeviceOverlay.lua | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) 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/inputManager.lua b/client/src/inputManager.lua index 2beeb811..2a979358 100644 --- a/client/src/inputManager.lua +++ b/client/src/inputManager.lua @@ -698,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 @@ -711,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 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 From 2878e130554c8ab9dd8d2625661a2ed6c51bd797 Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 14:42:19 +0100 Subject: [PATCH 7/9] proxy joystickRemoved through Game and fix extra encode for config write --- client/src/Game.lua | 4 ++++ client/src/config.lua | 2 +- main.lua | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) 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/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 From 188f6ca97b4ec5b3463132da4c1021fbd8db20aa Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 14:44:05 +0100 Subject: [PATCH 8/9] rename UiElement functions for generating debug strings --- client/src/ui/UIElement.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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)} From 4f4e1d174cec7d14a3e38d6af648c90ff61fbf1f Mon Sep 17 00:00:00 2001 From: Endaris Date: Wed, 14 Jan 2026 14:44:41 +0100 Subject: [PATCH 9/9] remove obsolete require from Match --- common/engine/Match.lua | 1 - 1 file changed, 1 deletion(-) 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