refactor(input): share gamepad map between launcher and gameplay

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Andrew Quenehen
2026-08-01 03:53:13 -03:00
parent 228a0baf81
commit 83cf1fdb00
4 changed files with 118 additions and 24 deletions
+61
View File
@@ -0,0 +1,61 @@
-- Shared gamepad + raw joystick button tables for launcher and gameplay.
-- Hardware-measured NX overrides live in NX_* tables (see docs/switch-development.md).
local GamepadMap = {}
-- LÖVE SDL game-controller mapping (D-pad / face / menu).
GamepadMap.DEFAULT_GAMEPAD_BINDINGS = {
dpup = "up", dpdown = "down", dpleft = "left", dpright = "right",
a = "a", b = "b",
start = "start", back = "select",
}
-- Generic SDL joysticks without a game-controller DB entry (Linux handhelds).
GamepadMap.RAW_BUTTON_BINDINGS = {
[1] = "a", [2] = "b",
[7] = "select", [8] = "start", [9] = "select", [10] = "start",
}
-- Raw index -> gamepad button name for RomImporter routing.
GamepadMap.RAW_TO_GAMEPAD_BUTTON = {
[1] = "a", [2] = "b",
[7] = "back", [8] = "start", [9] = "back", [10] = "start",
}
-- Test hook: force NX raw tables without stubbing love.
GamepadMap._forceNXForTests = false
GamepadMap.NX_RAW_BUTTON_BINDINGS = nil
GamepadMap.NX_RAW_TO_GAMEPAD_BUTTON = nil
function GamepadMap._setForceNXForTests(v)
GamepadMap._forceNXForTests = not not v
end
local function nxActive()
if GamepadMap._forceNXForTests then return true end
if love and love._os == "NX" then return true end
if love and love.system and love.system.getOS() == "NX" then return true end
return false
end
function GamepadMap.mapGamepadButton(button)
return GamepadMap.DEFAULT_GAMEPAD_BINDINGS[button]
end
function GamepadMap.mapRawButton(index)
if nxActive() and GamepadMap.NX_RAW_BUTTON_BINDINGS then
local nx = GamepadMap.NX_RAW_BUTTON_BINDINGS[index]
if nx then return nx end
end
return GamepadMap.RAW_BUTTON_BINDINGS[index]
end
function GamepadMap.mapRawToGamepadButton(index)
if nxActive() and GamepadMap.NX_RAW_TO_GAMEPAD_BUTTON then
local nx = GamepadMap.NX_RAW_TO_GAMEPAD_BUTTON[index]
if nx then return nx end
end
return GamepadMap.RAW_TO_GAMEPAD_BUTTON[index]
end
return GamepadMap
+7 -22
View File
@@ -1,6 +1,8 @@
-- Input abstraction: maps keyboard to Game Boy buttons.
-- `down` = held this frame; `pressed` = edge, consumed per fixed step.
local GamepadMap = require("src.core.GamepadMap")
local Input = {}
local DEFAULT_BINDINGS = {
@@ -22,31 +24,12 @@ local DEFAULT_BINDINGS = {
-- keys that map to "start" but also to "a" would conflict; keep Enter = a,
-- Escape = start for desktop friendliness.
-- LÖVE's standard gamepad mapping (SDL game controller DB), consistent
-- across Xbox/PlayStation/generic controllers on desktop and mobile. Some
-- third-party pads report their own SDL mapping for a given physical
-- button (e.g. Select/Back/View on off-brand XInput pads), which is what
-- src/ui/BindingsMenu.lua's rebinding is for -- see applyBindings below.
local DEFAULT_GAMEPAD_BINDINGS = {
dpup = "up", dpdown = "down", dpleft = "left", dpright = "right",
a = "a", b = "b",
start = "start", back = "select",
}
-- left-stick deadzones: press past STICK_ON, release once back under
-- STICK_OFF. The gap (hysteresis) stops the direction from flickering
-- while the stick sits near the threshold.
local STICK_ON = 0.5
local STICK_OFF = 0.3
-- Generic SDL joysticks expose the left stick as the first two numbered
-- axes and the D-pad as a hat. This is common on Linux handhelds whose
-- controller has no game-controller database entry.
local RAW_BUTTON_BINDINGS = {
[1] = "a", [2] = "b",
[7] = "select", [8] = "start", [9] = "select", [10] = "start",
}
local HAT_DIRECTIONS = {
u = { "up" }, d = { "down" }, l = { "left" }, r = { "right" },
lu = { "left", "up" }, ru = { "right", "up" },
@@ -68,7 +51,9 @@ end
function Input:applyBindings(overlay)
local keys, pads = {}, {}
for key, action in pairs(DEFAULT_BINDINGS) do keys[key] = action end
for button, action in pairs(DEFAULT_GAMEPAD_BINDINGS) do pads[button] = action end
for button, action in pairs(GamepadMap.DEFAULT_GAMEPAD_BINDINGS) do
pads[button] = action
end
for actionId, binding in pairs(overlay or {}) do
if type(binding) == "table" then
if binding.key then keys[binding.key] = actionId end
@@ -195,12 +180,12 @@ function Input:gamepadreleased(joystick, button)
end
function Input:joystickpressed(joystick, button)
local btn = RAW_BUTTON_BINDINGS[button]
local btn = GamepadMap.mapRawButton(button)
if btn then press(self, btn, "joy:" .. button) end
end
function Input:joystickreleased(joystick, button)
local btn = RAW_BUTTON_BINDINGS[button]
local btn = GamepadMap.mapRawButton(button)
if btn then release(self, btn, "joy:" .. button) end
end
+5 -2
View File
@@ -1,4 +1,5 @@
local GameVersion = require("src.core.GameVersion")
local GamepadMap = require("src.core.GamepadMap")
local Strings = require("src.core.Strings")
local HostShell = require("src.core.HostShell")
local Platform = require("src.core.Platform")
@@ -1547,11 +1548,13 @@ function RomImporter:gamepadaxis(_, axis, value)
end
function RomImporter:joystickpressed(joystick, button)
if button == 1 then self:gamepadpressed(joystick, "a") end
local padButton = GamepadMap.mapRawToGamepadButton(button)
if padButton then self:gamepadpressed(joystick, padButton) end
end
function RomImporter:joystickreleased(joystick, button)
if button == 1 then self:gamepadreleased(joystick, "a") end
local padButton = GamepadMap.mapRawToGamepadButton(button)
if padButton then self:gamepadreleased(joystick, padButton) end
end
function RomImporter:joystickaxis(joystick, axis, value)
+45
View File
@@ -0,0 +1,45 @@
-- Shared gamepad/raw map: launcher and gameplay use identical converters (SWNX-10/11).
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local T = require("tests.harness")
local check = T.check
local eq = T.eq
local GamepadMap = require("src.core.GamepadMap")
local Input = require("src.core.Input")
local RomImporter = require("src.import.RomImporter")
-- Gamepad names map to GB actions.
eq(GamepadMap.mapGamepadButton("a"), "a", "gamepad A -> GB A")
eq(GamepadMap.mapGamepadButton("back"), "select", "gamepad back -> select")
eq(GamepadMap.mapGamepadButton("dpup"), "up", "gamepad d-pad up")
-- Generic raw indices (Linux handheld fallback).
eq(GamepadMap.mapRawButton(1), "a", "raw #1 -> A")
eq(GamepadMap.mapRawButton(8), "start", "raw #8 -> start")
eq(GamepadMap.mapRawToGamepadButton(2), "b", "raw #2 routes to gamepad b")
-- Input and RomImporter agree on the same GB action for a raw press.
Input:init()
Input:joystickpressed(nil, 1)
Input:step()
check(Input:isDown("a"), "Input raw #1 holds A")
local importer = setmetatable({
_padCursor = { x = 0, y = 0 }, _padCursorActive = false,
_padAxis = { leftx = 0, lefty = 0, righty = 0 },
_padDir = {}, _rawHatDirs = {}, _padInited = true,
}, RomImporter)
local clicked = false
function importer:mousepressed(_, _, button)
clicked = button == 1
end
importer:joystickpressed(nil, 1)
check(clicked, "RomImporter raw #1 clicks via shared map (not hardcoded-only)")
importer:joystickpressed(nil, 2)
check(importer._padDir.dpright == nil,
"raw #2 maps to B, not spurious d-pad")
T.finish()