Skip to content
Merged
1 change: 1 addition & 0 deletions client/src/BattleRoom.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions client/src/Game.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion client/src/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions client/src/input/InputConfiguration.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
117 changes: 56 additions & 61 deletions client/src/inputManager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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 ..
Expand All @@ -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")
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -682,30 +678,30 @@ 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

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

Expand All @@ -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
Expand All @@ -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("^([^:]+):")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion client/src/scenes/components/InputDeviceOverlay.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions client/src/ui/UIElement.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand All @@ -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)}
Expand Down
1 change: 0 additions & 1 deletion common/engine/Match.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading