From 48b39c8519cd8d888964f2a238cdfecc8a6b8f07 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Fri, 21 Aug 2026 08:23:21 -0400 Subject: [PATCH] CLOSES #1570 --- src/core/Game2.lua | 119 ++++++++++++++---- .../engine/gen2_controller_parity_bug1570.lua | 105 ++++++++++++++++ 2 files changed, 201 insertions(+), 23 deletions(-) create mode 100644 tests/engine/gen2_controller_parity_bug1570.lua diff --git a/src/core/Game2.lua b/src/core/Game2.lua index 3eee6a6a..cd3bf3da 100644 --- a/src/core/Game2.lua +++ b/src/core/Game2.lua @@ -18,6 +18,7 @@ local Chrome = require("src.ui.gen2.Chrome") local Clock = require("src.core.gen2.Clock") local FixedStep = require("src.core.FixedStep") local Font = require("src.render.Font") +local GamepadMap = require("src.core.GamepadMap") local Input = require("src.core.Input") local Music = require("src.core.Music") local Save = require("src.core.gen2.Save") @@ -57,20 +58,6 @@ Game2.__index = Game2 local function noop() end -for _, name in ipairs({ - "joystickpressed", "joystickreleased", "joystickaxis", "joystickhat", - "joystickadded", -}) do - Game2[name] = noop -end - --- Not a noop, because the overlay has to come back on its own: a player who --- unplugs the only controller would otherwise have to tap a blind screen to --- get the pad back (src/core/Game.lua:869 does the same). -function Game2:joystickremoved() - TouchControls:joystickremoved() -end - -- THE FRAME AND INPUT SEAMS. -- -- Gold composites its own frame (Game2:draw / drawScene) and pumps its own pad @@ -1993,6 +1980,13 @@ function Game2:applyOptions() end end +function Game2:_cycleSpeed(dir) + local GameSpeed = require("src.core.GameSpeed") + self.options.speed = GameSpeed.cycle(self.options.speed, dir) + if self.save then self.save.options = self.options end + self:persistOptions() +end + -- `back` -- SDL's name for the small left-hand menu button: Xbox VIEW, the PS -- CREATE/SHARE beside the touchpad, the Switch MINUS -- is SELECT, and has been -- since src/core/GamepadMap.lua's DEFAULT_GAMEPAD_BINDINGS was written @@ -2003,27 +1997,49 @@ end -- the PACK's move-item, the party menu's reorder and half the soft-reset chord -- (A+B+SELECT+START) were all unreachable from a pad, and pressing the button -- to find out killed the process. It reaches Input like every other button now. -function Game2:gamepadpressed(_joystick, button) +function Game2:gamepadpressed(joystick, button) -- a controller is being used: the touch overlay steps aside until the next -- screen touch (mobile only; a no-op elsewhere) TouchControls:noteGamepad() - -- The shoulders cycle GAME SPEED, as they do in the Gen 1 path. - if button == "rightshoulder" or button == "leftshoulder" then - local GameSpeed = require("src.core.GameSpeed") - local dir = button == "rightshoulder" and 1 or -1 - self.options.speed = GameSpeed.cycle(self.options.speed, dir) - if self.save then self.save.options = self.options end - self:persistOptions() + local selectHeld = Input:isDown("select") + if not selectHeld and joystick and joystick.isGamepadDown then + local ok, down = pcall(function() + return joystick:isGamepadDown("back") + end) + selectHeld = ok and down == true + end + -- shoulders and triggers cycle GAME SPEED, as in src/core/Game.lua:881 + if not selectHeld then + if button == "rightshoulder" or button == "righttrigger" then + self:_cycleSpeed(1) + return + elseif button == "leftshoulder" or button == "lefttrigger" then + self:_cycleSpeed(-1) + return + end + end + local top = self.stack and self.stack:top() + if top and top.onGamepadPressed then + top:onGamepadPressed(button) return end + if selectHeld then + local digit = GamepadMap.displayChordDigit(button) + if digit then + self:keypressed(digit) + return + end + end -- START opens the start menu in the overworld; it used to quit, from before -- there was a menu to open. - Input:gamepadpressed(_joystick, button) + Input:gamepadpressed(joystick, button) end function Game2:gamepadreleased(joystick, button) Input:gamepadreleased(joystick, button) + local top = self.stack and self.stack:top() + if top and top.onGamepadReleased then top:onGamepadReleased(button) end end function Game2:gamepadaxis(joystick, axis, value) @@ -2032,4 +2048,61 @@ function Game2:gamepadaxis(joystick, axis, value) Input:gamepadaxis(joystick, axis, value) end +-- The raw joystick road, same bodies as src/core/Game.lua:935 (#620, #632, #1570). +local function isRawStick(joystick) + return not (joystick and joystick.isGamepad and joystick:isGamepad()) +end + +function Game2:joystickpressed(joystick, button) + if GamepadMap.isAccelerometer(joystick) then return end + TouchControls:noteGamepad() + local top = self.stack and self.stack:top() + if isRawStick(joystick) and top and top.onJoystickPressed then + top:onJoystickPressed(button) + return + end + Input:joystickpressed(joystick, button) +end + +function Game2:joystickreleased(joystick, button) + if GamepadMap.isAccelerometer(joystick) then return end + Input:joystickreleased(joystick, button) + local top = self.stack and self.stack:top() + if isRawStick(joystick) and top and top.onJoystickReleased then + top:onJoystickReleased(button) + end +end + +function Game2:joystickaxis(joystick, axis, value) + if GamepadMap.isAccelerometer(joystick) then return end + if math.abs(value) > 0.5 then TouchControls:noteGamepad() end + Input:joystickaxis(joystick, axis, value) +end + +function Game2:joystickhat(joystick, hat, direction) + if GamepadMap.isAccelerometer(joystick) then return end + if direction ~= "c" then TouchControls:noteGamepad() end + Input:joystickhat(joystick, hat, direction) +end + +-- src/core/Game.lua:1015 (#799) +function Game2:recoverInput() + Input:reset() + Input:reconcile() + TouchControls:reset() + if self.mods and self.mods.releaseModInput then self.mods:releaseModInput() end + self:cancelPointers() +end + +function Game2:joystickadded() + self:recoverInput() +end + +-- The overlay comes back on its own when the last pad is unplugged +-- (src/core/Game.lua:1044). +function Game2:joystickremoved() + self:recoverInput() + TouchControls:joystickremoved() +end + return Game2 diff --git a/tests/engine/gen2_controller_parity_bug1570.lua b/tests/engine/gen2_controller_parity_bug1570.lua new file mode 100644 index 00000000..9fa429d7 --- /dev/null +++ b/tests/engine/gen2_controller_parity_bug1570.lua @@ -0,0 +1,105 @@ +-- Gold noop'd the raw joystick road, so a stick with no SDL game-controller-db +-- entry pressed nothing in Gold while working in Gen 1 (#1570). +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 Game = require("src.core.Game") +local Game2 = require("src.core.Game2") +local Input = require("src.core.Input") + +local CALLBACKS = { + "keypressed", "keyreleased", + "gamepadpressed", "gamepadreleased", "gamepadaxis", + "joystickpressed", "joystickreleased", "joystickaxis", "joystickhat", + "joystickadded", "joystickremoved", +} + +for _, name in ipairs(CALLBACKS) do + eq(type(Game[name]), "function", "Gen 1 routes " .. name) + eq(type(Game2[name]), "function", "Gold routes " .. name) +end + +local rawJoy = { isGamepad = function() return false end } +local gamepadJoy = { isGamepad = function() return true end } + +local function newGold(top) + return setmetatable({ + options = {}, + stack = { top = function() return top end }, + }, Game2) +end + +Input:init() +local gold = newGold(nil) + +gold:joystickpressed(rawJoy, 1) +Input:step() +check(Input:wasPressed("a"), "raw stick button 1 presses GB A in Gold") +check(Input:isDown("a"), "raw stick hold survives the step in Gold") +gold:joystickreleased(rawJoy, 1) +Input:step() +check(not Input:isDown("a"), "raw stick release clears the hold in Gold") + +gold:joystickhat(rawJoy, 1, "l") +Input:step() +check(Input:wasPressed("left"), "raw hat left presses GB LEFT in Gold") +gold:joystickhat(rawJoy, 1, "c") +Input:step() +check(not Input:isDown("left"), "raw hat centre clears GB LEFT in Gold") + +gold:joystickaxis(rawJoy, 2, 1) +Input:step() +check(Input:wasPressed("down"), "raw axis 2 presses GB DOWN in Gold") +gold:joystickaxis(rawJoy, 2, 0) +Input:step() +check(not Input:isDown("down"), "raw axis 2 back to centre clears GB DOWN") + +-- A recognized pad raises BOTH roads for one press; the raw half must not +-- re-assert the factory map underneath a rebind (#620, #632). +Input:init() +gold = newGold(nil) +gold:gamepadpressed(gamepadJoy, "a") +gold:joystickpressed(gamepadJoy, 1) +gold:joystickpressed(gamepadJoy, 2) +Input:step() +check(Input:wasPressed("a"), "recognized pad A reaches Input in Gold") +check(not Input:wasPressed("b"), "raw must not stack B onto a recognized pad") + +-- src/ui/BindingsMenu.lua's capture slots: Gold now hands the top state first +-- refusal on both roads, the way src/core/Game.lua does. +Input:init() +local captured = {} +local capturingTop = { + onJoystickPressed = function(_, button) captured.joy = button end, + onGamepadPressed = function(_, button) captured.pad = button end, +} +gold = newGold(capturingTop) +gold:joystickpressed(rawJoy, 3) +gold:gamepadpressed(gamepadJoy, "y") +Input:step() +eq(captured.joy, 3, "an armed CONTROLS row captures the raw button") +eq(captured.pad, "y", "an armed CONTROLS row captures the pad button") +check(not Input:wasPressed("a"), "a captured press never reaches gameplay") + +-- Hotplug: the reset+reconcile Gen 1 runs, so a pad that vanished mid-hold +-- cannot strand a direction down (#799). +Input:init() +gold = newGold(nil) +gold:joystickpressed(rawJoy, 1) +Input:step() +check(Input:isDown("a"), "held before the hotplug") +gold:joystickadded(rawJoy) +Input:step() +check(not Input:isDown("a"), "joystickadded drops stranded holds in Gold") + +gold:joystickpressed(rawJoy, 1) +Input:step() +gold:joystickremoved(rawJoy) +Input:step() +check(not Input:isDown("a"), "joystickremoved drops stranded holds in Gold") + +T.finish()