From efd81d8e3456406852fa7d3f7a4b304502a13456 Mon Sep 17 00:00:00 2001 From: Andrew Quenehen Date: Sat, 1 Aug 2026 04:15:47 -0300 Subject: [PATCH] fix(input): ignore raw face presses when Joy-Con is gamepad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit love-nx emits gamepad+raw on one press; NamingScreen saw a+b and always erased. Skip raw when isGamepad(); align NX Y→a/X→b; prefer A if both. Co-authored-by: Cursor --- src/core/GamepadMap.lua | 20 +++++++++--- src/core/Input.lua | 2 ++ src/import/RomImporter.lua | 2 ++ src/ui/NamingScreen.lua | 37 ++++++++++++++--------- tests/engine/input_dual_path_test.lua | 42 ++++++++++++++++++++++++++ tests/engine/input_nx_raw_map_test.lua | 10 +++--- 6 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 tests/engine/input_dual_path_test.lua diff --git a/src/core/GamepadMap.lua b/src/core/GamepadMap.lua index 851f425a..e6dbf813 100644 --- a/src/core/GamepadMap.lua +++ b/src/core/GamepadMap.lua @@ -16,11 +16,13 @@ GamepadMap.RAW_BUTTON_BINDINGS = { [7] = "select", [8] = "start", [9] = "select", [10] = "start", } --- Switch OLED probe 2026-08-01: love.joystickpressed indices (1-based). --- Gamepad path still preferred when isGamepad(); raw covers the rest. +-- Switch OLED: love.joystickpressed indices (1-based). Used only when the +-- device is NOT a gamepad — love-nx also emits gamepadpressed for Joy-Con, +-- and applying both face paths in one frame breaks NamingScreen (a+b). +-- Y→a / X→b matches operator OLED naming diagnosis (2026-08-01). GamepadMap.NX_RAW_BUTTON_BINDINGS = { [1] = "a", [2] = "b", - [3] = "b", [4] = "a", + [3] = "a", [4] = "b", [9] = "select", [10] = "start", } @@ -32,7 +34,7 @@ GamepadMap.RAW_TO_GAMEPAD_BUTTON = { GamepadMap.NX_RAW_TO_GAMEPAD_BUTTON = { [1] = "a", [2] = "b", - [3] = "y", [4] = "x", + [3] = "a", [4] = "b", [9] = "back", [10] = "start", } @@ -54,6 +56,16 @@ function GamepadMap.mapGamepadButton(button) return GamepadMap.DEFAULT_GAMEPAD_BINDINGS[button] end +-- love-nx / SDL: when isGamepad(), face+menu already arrive via gamepad*. +-- Applying joystickpressed raw on top double-fires GB A/B in one frame. +function GamepadMap.ignoreRawForJoystick(joystick) + if not joystick then return false end + local ok, isPad = pcall(function() + return joystick.isGamepad and joystick:isGamepad() + end) + return ok and isPad == true +end + function GamepadMap.mapRawButton(index) if nxActive() then local nx = GamepadMap.NX_RAW_BUTTON_BINDINGS[index] diff --git a/src/core/Input.lua b/src/core/Input.lua index 6d238ede..76254985 100644 --- a/src/core/Input.lua +++ b/src/core/Input.lua @@ -180,11 +180,13 @@ function Input:gamepadreleased(joystick, button) end function Input:joystickpressed(joystick, button) + if GamepadMap.ignoreRawForJoystick(joystick) then return end local btn = GamepadMap.mapRawButton(button) if btn then press(self, btn, "joy:" .. button) end end function Input:joystickreleased(joystick, button) + if GamepadMap.ignoreRawForJoystick(joystick) then return end 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 9fb95b3d..fbe79c34 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -1548,11 +1548,13 @@ function RomImporter:gamepadaxis(_, axis, value) end function RomImporter:joystickpressed(joystick, button) + if GamepadMap.ignoreRawForJoystick(joystick) then return end local padButton = GamepadMap.mapRawToGamepadButton(button) if padButton then self:gamepadpressed(joystick, padButton) end end function RomImporter:joystickreleased(joystick, button) + if GamepadMap.ignoreRawForJoystick(joystick) then return end local padButton = GamepadMap.mapRawToGamepadButton(button) if padButton then self:gamepadreleased(joystick, padButton) end end diff --git a/src/ui/NamingScreen.lua b/src/ui/NamingScreen.lua index 171d85de..6073ca44 100644 --- a/src/ui/NamingScreen.lua +++ b/src/ui/NamingScreen.lua @@ -153,21 +153,28 @@ function NamingScreen:update(dt) if self.row ~= caseRow then self.col = self.col < #GRID[self.row] and self.col + 1 or 1 end - elseif input:wasPressed("b") then - table.remove(self.glyphs) - elseif input:wasPressed("a") then - if self.row == edRow and self.col == edCol then - self:confirm() - return - end - if self.row == caseRow then - self.lower = not self.lower - return - end - if #self.glyphs < self.maxLen then - Sound.play(self.game.data, "Press_AB") - table.insert(self.glyphs, GRID[self.row][self.col]) - if #self.glyphs >= self.maxLen then self:jumpToEnd() end + else + -- Prefer A over B when both edges fire in one frame (love-nx dual + -- gamepad+raw path historically set both; erase must not win). + local pressedA = input:wasPressed("a") + local pressedB = input:wasPressed("b") + if pressedA and pressedB then pressedB = false end + if pressedB then + table.remove(self.glyphs) + elseif pressedA then + if self.row == edRow and self.col == edCol then + self:confirm() + return + end + if self.row == caseRow then + self.lower = not self.lower + return + end + if #self.glyphs < self.maxLen then + Sound.play(self.game.data, "Press_AB") + table.insert(self.glyphs, GRID[self.row][self.col]) + if #self.glyphs >= self.maxLen then self:jumpToEnd() end + end end end end diff --git a/tests/engine/input_dual_path_test.lua b/tests/engine/input_dual_path_test.lua new file mode 100644 index 00000000..6bb91eb2 --- /dev/null +++ b/tests/engine/input_dual_path_test.lua @@ -0,0 +1,42 @@ +-- When isGamepad(), raw face presses must not stack on gamepad* (NamingScreen a+b). +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 gamepadJoy = { + isGamepad = function() return true end, +} +local rawJoy = { + isGamepad = function() return false end, +} + +check(GamepadMap.ignoreRawForJoystick(gamepadJoy), "ignore raw when isGamepad") +check(not GamepadMap.ignoreRawForJoystick(rawJoy), "allow raw when not gamepad") +check(not GamepadMap.ignoreRawForJoystick(nil), "nil joystick does not ignore raw") + +GamepadMap._setForceNXForTests(true) +eq(GamepadMap.mapRawButton(3), "a", "NX raw Y (#3) -> GB A") +eq(GamepadMap.mapRawButton(4), "b", "NX raw X (#4) -> GB B") +GamepadMap._setForceNXForTests(false) + +Input:init() +Input:gamepadpressed(gamepadJoy, "a") +Input:joystickpressed(gamepadJoy, 1) -- must no-op +Input:joystickpressed(gamepadJoy, 2) -- must no-op (would have set b) +Input:step() +check(Input:wasPressed("a"), "gamepad A edge present") +check(not Input:wasPressed("b"), "raw must not add B alongside gamepad A") +check(Input:isDown("a"), "A held from pad source only") + +Input:init() +Input:joystickpressed(rawJoy, 1) +Input:step() +check(Input:wasPressed("a"), "non-gamepad raw #1 still maps to A") + +T.finish() diff --git a/tests/engine/input_nx_raw_map_test.lua b/tests/engine/input_nx_raw_map_test.lua index 4119dacc..c92e2364 100644 --- a/tests/engine/input_nx_raw_map_test.lua +++ b/tests/engine/input_nx_raw_map_test.lua @@ -9,11 +9,11 @@ local GamepadMap = require("src.core.GamepadMap") GamepadMap._setForceNXForTests(true) --- Phase 0 probe: Y→#3, X→#4; Nintendo B/A at #1/#2. -eq(GamepadMap.mapRawButton(3), "b", "NX raw Y (#3) maps to GB B") -eq(GamepadMap.mapRawButton(4), "a", "NX raw X (#4) maps to GB A") -eq(GamepadMap.mapRawToGamepadButton(3), "y", "NX raw #3 routes to gamepad y") -eq(GamepadMap.mapRawToGamepadButton(4), "x", "NX raw #4 routes to gamepad x") +-- Phase 0 / naming diagnosis: Y→#3→a, X→#4→b; Nintendo B/A at #1/#2. +eq(GamepadMap.mapRawButton(3), "a", "NX raw Y (#3) maps to GB A") +eq(GamepadMap.mapRawButton(4), "b", "NX raw X (#4) maps to GB B") +eq(GamepadMap.mapRawToGamepadButton(3), "a", "NX raw #3 routes to gamepad a") +eq(GamepadMap.mapRawToGamepadButton(4), "b", "NX raw #4 routes to gamepad b") eq(GamepadMap.mapRawButton(9), "select", "NX minus (#9) -> select") eq(GamepadMap.mapRawButton(10), "start", "NX plus (#10) -> start")