diff --git a/src/core/GamepadMap.lua b/src/core/GamepadMap.lua new file mode 100644 index 00000000..68fd5ff4 --- /dev/null +++ b/src/core/GamepadMap.lua @@ -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 diff --git a/src/core/Input.lua b/src/core/Input.lua index f9985613..6d238ede 100644 --- a/src/core/Input.lua +++ b/src/core/Input.lua @@ -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 diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 6048de01..9fb95b3d 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -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) diff --git a/tests/engine/input_shared_map_test.lua b/tests/engine/input_shared_map_test.lua new file mode 100644 index 00000000..3916966b --- /dev/null +++ b/tests/engine/input_shared_map_test.lua @@ -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()