mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 21:16:28 +02:00
Rebuild held input after lifecycle resets
A focus flip, visibility flip, joystick add/remove, or resume reset all held input, and a still-held direction never re-fires keypressed, so any spurious reset (macOS Bluetooth re-enumeration fires joystickadded with no hotplug) parked the player until every direction was re-pressed. Reconcile from device ground truth after each reset; only what is physically down comes back, so swallowed releases still clear.
This commit is contained in:
+10
-3
@@ -793,11 +793,13 @@ function Game:joystickhat(joystick, hat, direction)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Window focus/visibility flips: a release due while unfocused/hidden can
|
-- Window focus/visibility flips: a release due while unfocused/hidden can
|
||||||
-- be swallowed by the OS. Reset on both edges -- gaining focus with a
|
-- be swallowed by the OS. Reset on both edges; on the regain, reconcile
|
||||||
-- physically held key won't re-fire keypressed, so trusting leftover
|
-- re-arms only what is still physically held -- a held key won't re-fire
|
||||||
-- state is worse than asking the player to re-press.
|
-- keypressed by itself, and without the rebuild a spurious lifecycle event
|
||||||
|
-- parked the player until every direction was re-pressed (#799).
|
||||||
function Game:focus(f)
|
function Game:focus(f)
|
||||||
Input:reset()
|
Input:reset()
|
||||||
|
if f then Input:reconcile() end
|
||||||
TouchControls:reset()
|
TouchControls:reset()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -812,6 +814,7 @@ end
|
|||||||
|
|
||||||
function Game:onResume()
|
function Game:onResume()
|
||||||
Input:reset()
|
Input:reset()
|
||||||
|
Input:reconcile()
|
||||||
TouchControls:reset()
|
TouchControls:reset()
|
||||||
-- Chip music may survive NX suspend as a duplicate stream; stop it and let
|
-- Chip music may survive NX suspend as a duplicate stream; stop it and let
|
||||||
-- the active screen re-cue on the next frame (hardware audio check: T19).
|
-- the active screen re-cue on the next frame (hardware audio check: T19).
|
||||||
@@ -827,6 +830,10 @@ end
|
|||||||
|
|
||||||
function Game:recoverInput(event, joystick)
|
function Game:recoverInput(event, joystick)
|
||||||
Input:reset()
|
Input:reset()
|
||||||
|
-- A hotplug can arrive with no hotplug (macOS Bluetooth re-enumeration),
|
||||||
|
-- and the blanket reset above also drops unrelated keyboard holds; put
|
||||||
|
-- back whatever is still physically down (#799).
|
||||||
|
Input:reconcile()
|
||||||
TouchControls:reset()
|
TouchControls:reset()
|
||||||
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
||||||
if SwitchDiagnostics.isEnabled() then
|
if SwitchDiagnostics.isEnabled() then
|
||||||
|
|||||||
@@ -286,6 +286,71 @@ function Input:joystickhat(joystick, hat, direction)
|
|||||||
self.hatDirs[hat] = dirs
|
self.hatDirs[hat] = dirs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Lifecycle resets (focus/visibility flips, joystick add/remove, resume)
|
||||||
|
-- wipe held state because a release can be swallowed while the OS owns the
|
||||||
|
-- event stream. A direction the player is STILL holding never re-fires
|
||||||
|
-- keypressed/gamepadpressed after the wipe either, so a spurious reset --
|
||||||
|
-- macOS re-enumerating a Bluetooth pad fires joystickadded with no hotplug,
|
||||||
|
-- and the blanket reset took unrelated keyboard holds down with it --
|
||||||
|
-- parked the player in place until every direction was released and
|
||||||
|
-- pressed again (#799). Rebuild holds from the devices' ground truth
|
||||||
|
-- instead: only what is physically down right now comes back, so the
|
||||||
|
-- swallowed-release hazards the resets guard against stay cleared.
|
||||||
|
-- Deliberately separate from reset(): the soft-reset chord path in
|
||||||
|
-- Game:step needs the clean slate (re-arming A there would read it as a
|
||||||
|
-- title-menu choice).
|
||||||
|
function Input:reconcile()
|
||||||
|
local kb = love and love.keyboard
|
||||||
|
if kb and kb.isDown then
|
||||||
|
for key, btn in pairs(self.keyBindings) do
|
||||||
|
local ok, down = pcall(kb.isDown, key)
|
||||||
|
if ok and down then press(self, btn, "key:" .. key) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local js = love and love.joystick
|
||||||
|
if not (js and js.getJoysticks) then return end
|
||||||
|
local ok, joysticks = pcall(js.getJoysticks)
|
||||||
|
if not ok or type(joysticks) ~= "table" then return end
|
||||||
|
for _, j in ipairs(joysticks) do
|
||||||
|
if GamepadMap.ignoreRawForJoystick(j) then
|
||||||
|
-- SDL-recognized pad: buttons + left stick, the gamepad surfaces
|
||||||
|
if j.isGamepadDown then
|
||||||
|
for button, btn in pairs(self.padBindings) do
|
||||||
|
local ok2, down = pcall(j.isGamepadDown, j, button)
|
||||||
|
if ok2 and down then press(self, btn, "pad:" .. button) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if j.getGamepadAxis then
|
||||||
|
for _, axis in ipairs({ "leftx", "lefty" }) do
|
||||||
|
local ok2, v = pcall(j.getGamepadAxis, j, axis)
|
||||||
|
if ok2 and type(v) == "number" then self:gamepadaxis(j, axis, v) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- raw stick (#620/#632): the surfaces the joystick* events feed
|
||||||
|
if j.isDown then
|
||||||
|
for index, btn in pairs(self.joyBindings) do
|
||||||
|
local ok2, down = pcall(j.isDown, j, index)
|
||||||
|
if ok2 and down then press(self, btn, "joy:" .. index) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if j.getAxis then
|
||||||
|
for _, axis in ipairs({ 1, 2 }) do
|
||||||
|
local ok2, v = pcall(j.getAxis, j, axis)
|
||||||
|
if ok2 and type(v) == "number" then self:joystickaxis(j, axis, v) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if j.getHatCount and j.getHat then
|
||||||
|
local ok2, count = pcall(j.getHatCount, j)
|
||||||
|
for hat = 1, (ok2 and count) or 0 do
|
||||||
|
local ok3, dir = pcall(j.getHat, j, hat)
|
||||||
|
if ok3 and dir then self:joystickhat(j, hat, dir) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Input:isDown(btn)
|
function Input:isDown(btn)
|
||||||
return self.state[btn] or false
|
return self.state[btn] or false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
-- Held directions must survive lifecycle resets while physically held
|
||||||
|
-- (#799). Input state is event-driven, so any Input:reset (focus or
|
||||||
|
-- visibility flip, joystick add/remove, resume) wipes a hold that never
|
||||||
|
-- re-fires keypressed afterwards -- on macOS a Bluetooth controller
|
||||||
|
-- re-enumerating mid-walk fired joystickadded under a held key and parked
|
||||||
|
-- the player until the direction was released and pressed again. Game's
|
||||||
|
-- lifecycle handlers rebuild holds from device ground truth after each
|
||||||
|
-- reset; only what is physically down comes back, so the swallowed-release
|
||||||
|
-- hazards the resets guard against stay cleared.
|
||||||
|
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 Input = require("src.core.Input")
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
|
||||||
|
local realIsDown = love.keyboard.isDown
|
||||||
|
local realJoystick = love.joystick
|
||||||
|
local function restore()
|
||||||
|
love.keyboard.isDown = realIsDown
|
||||||
|
love.joystick = realJoystick
|
||||||
|
end
|
||||||
|
|
||||||
|
Input:init()
|
||||||
|
|
||||||
|
-- Keyboard: direction still physically held across a spurious pad
|
||||||
|
-- re-enumeration must keep walking (the #799 report).
|
||||||
|
love.keyboard.isDown = function(key) return key == "up" end
|
||||||
|
Input:reset()
|
||||||
|
Input:keypressed("up")
|
||||||
|
Input:step()
|
||||||
|
check(Input:isDown("up"), "up held before joystick re-enumeration")
|
||||||
|
Game:joystickadded({ getName = function() return "Wireless Controller" end })
|
||||||
|
check(Input:isDown("up"), "held key survives a spurious joystickadded")
|
||||||
|
|
||||||
|
-- Same hold across a focus bounce (another reset source).
|
||||||
|
Game:focus(false)
|
||||||
|
check(not Input:isDown("up"), "focus loss still drops the hold")
|
||||||
|
Game:focus(true)
|
||||||
|
check(Input:isDown("up"), "held key re-arms on focus regain")
|
||||||
|
|
||||||
|
-- A key released while unfocused (keyup swallowed by the OS, the hazard
|
||||||
|
-- the resets exist for) must NOT come back.
|
||||||
|
love.keyboard.isDown = function() return false end
|
||||||
|
Game:focus(false)
|
||||||
|
Game:focus(true)
|
||||||
|
check(not Input:isDown("up"), "swallowed release still clears the hold")
|
||||||
|
|
||||||
|
-- Controller: held d-pad across a disconnect/reconnect bounce.
|
||||||
|
local pad = {
|
||||||
|
isGamepad = function() return true end,
|
||||||
|
isGamepadDown = function(_, button) return button == "dpleft" end,
|
||||||
|
getGamepadAxis = function() return 0 end,
|
||||||
|
}
|
||||||
|
love.joystick = { getJoysticks = function() return { pad } end }
|
||||||
|
love.keyboard.isDown = function() return false end
|
||||||
|
Input:reset()
|
||||||
|
Input:gamepadpressed(pad, "dpleft")
|
||||||
|
Input:step()
|
||||||
|
check(Input:isDown("left"), "d-pad held before reconnect")
|
||||||
|
Game:joystickremoved(pad)
|
||||||
|
Game:joystickadded(pad)
|
||||||
|
check(Input:isDown("left"), "held d-pad survives a reconnect bounce")
|
||||||
|
|
||||||
|
-- Held stick across the same bounce (axis ground truth re-derived).
|
||||||
|
pad.isGamepadDown = function() return false end
|
||||||
|
pad.getGamepadAxis = function(_, axis) return axis == "leftx" and -0.9 or 0 end
|
||||||
|
Input:reset()
|
||||||
|
Input:gamepadaxis(pad, "leftx", -0.9)
|
||||||
|
Input:step()
|
||||||
|
check(Input:isDown("left"), "stick held before re-enumeration")
|
||||||
|
Game:joystickadded(pad)
|
||||||
|
check(Input:isDown("left"), "held stick survives re-enumeration")
|
||||||
|
|
||||||
|
-- A pad that vanished for real reports nothing held: its stale hold must
|
||||||
|
-- stay cleared (the stuck-flag hazard reset-on-remove guards against).
|
||||||
|
love.joystick = { getJoysticks = function() return {} end }
|
||||||
|
Input:reset()
|
||||||
|
Input:gamepadpressed(pad, "dpleft")
|
||||||
|
Input:step()
|
||||||
|
Game:joystickremoved(pad)
|
||||||
|
check(not Input:isDown("left"), "vanished pad's hold stays cleared")
|
||||||
|
|
||||||
|
restore()
|
||||||
|
T.finish()
|
||||||
Reference in New Issue
Block a user