mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
198 lines
7.8 KiB
Lua
198 lines
7.8 KiB
Lua
-- Soft reset: A+B+SELECT+START held together drops the game back to the
|
|
-- title from anywhere, mid-battle included (#563). _Joypad
|
|
-- (engine/joypad.asm:6) tests the raw read with `cp PAD_BUTTONS` -- an
|
|
-- equality, so a d-pad direction in the mix cancels it -- and does so ahead
|
|
-- of the wJoyIgnore / BIT_DISABLE_JOYPAD masking further down, which is why
|
|
-- the combo still works where ordinary input is being thrown away.
|
|
-- TrySoftReset then decrements hSoftReset, seeded with 16 by Init
|
|
-- (home/init.asm:81), one poll at a time. The on-screen overlay's half is
|
|
-- here too: one finger only ever claims one control.
|
|
-- luajit tests/engine/soft_reset_bug563.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
local Input = require("src.core.Input")
|
|
local TouchControls = require("src.core.TouchControls")
|
|
local Game = require("src.core.Game")
|
|
|
|
-- pokered's 16 polls; the port counts fixed steps, which are the same 60Hz
|
|
local HOLD_STEPS = 16
|
|
|
|
-- The keys the hand-off tells a player to press. Pinned because the combo
|
|
-- is only reachable if all four GB buttons have a key, and because the
|
|
-- letter A is bound to LEFT here -- a player reaching for "A" on the
|
|
-- keyboard adds a direction and cancels the chord instead of arming it.
|
|
Input:init()
|
|
eq(Input.keyBindings["z"], "a", "Z is the A button")
|
|
eq(Input.keyBindings["x"], "b", "X is the B button")
|
|
eq(Input.keyBindings["escape"], "start", "Escape is START")
|
|
eq(Input.keyBindings["tab"], "select", "Tab is SELECT")
|
|
eq(Input.keyBindings["a"], "left", "the letter A is LEFT, not the A button")
|
|
|
|
-- ---- the chord itself --------------------------------------------------
|
|
|
|
local function holdKeys(...)
|
|
Input:init()
|
|
for _, key in ipairs({ ... }) do Input:keypressed(key) end
|
|
end
|
|
|
|
holdKeys("z", "x", "escape", "tab")
|
|
check(Input:softResetHeld(), "all four buttons down arms the combo")
|
|
|
|
for _, missing in ipairs({ "z", "x", "escape", "tab" }) do
|
|
local keys = {}
|
|
for _, key in ipairs({ "z", "x", "escape", "tab" }) do
|
|
if key ~= missing then keys[#keys + 1] = key end
|
|
end
|
|
holdKeys(unpack(keys))
|
|
check(not Input:softResetHeld(),
|
|
"three of the four is not the combo (without " .. missing .. ")")
|
|
end
|
|
|
|
-- `cp PAD_BUTTONS` is an equality: any direction held alongside the four
|
|
-- takes the raw read off PAD_BUTTONS and TrySoftReset is never reached.
|
|
for _, dir in ipairs({ "up", "down", "left", "right" }) do
|
|
holdKeys("z", "x", "escape", "tab", dir)
|
|
check(not Input:softResetHeld(), dir .. " held alongside cancels the combo")
|
|
end
|
|
|
|
-- ---- the 16-step countdown ---------------------------------------------
|
|
|
|
-- one Game:step's worth of the chord; returns whether the reset fired
|
|
local function stepChord()
|
|
Input:step()
|
|
return Input:softResetStep()
|
|
end
|
|
|
|
holdKeys("z", "x", "escape", "tab")
|
|
for i = 1, HOLD_STEPS - 1 do
|
|
check(not stepChord(), "step " .. i .. " of the hold does not reset yet")
|
|
end
|
|
check(stepChord(), "the " .. HOLD_STEPS .. "th consecutive step resets")
|
|
|
|
-- hSoftReset is never re-seeded on release in the original, so its count
|
|
-- leaks across a session; the port re-arms instead, or a session's worth of
|
|
-- stray four-button presses would eventually add up to a reset.
|
|
holdKeys("z", "x", "escape", "tab")
|
|
for _ = 1, HOLD_STEPS - 1 do stepChord() end
|
|
Input:keyreleased("tab")
|
|
check(not stepChord(), "lifting SELECT one step short cancels")
|
|
Input:keypressed("tab")
|
|
for i = 1, HOLD_STEPS - 1 do
|
|
check(not stepChord(),
|
|
"re-pressing it restarts the count from 16, not from 1 (step " .. i .. ")")
|
|
end
|
|
check(stepChord(), "and a full fresh hold resets")
|
|
|
|
-- same re-arm for the direction case: a thumb brushing the d-pad mid-hold
|
|
holdKeys("z", "x", "escape", "tab")
|
|
for _ = 1, HOLD_STEPS - 1 do stepChord() end
|
|
Input:keypressed("up")
|
|
check(not stepChord(), "a direction one step short cancels")
|
|
Input:keyreleased("up")
|
|
for _ = 1, HOLD_STEPS - 1 do
|
|
check(not stepChord(), "and the count starts over")
|
|
end
|
|
check(stepChord(), "reaching 16 again from the restart")
|
|
|
|
-- ---- Game:step routes it above the state stack --------------------------
|
|
|
|
-- Doubles for the two services Game:step touches on the reset path. The
|
|
-- real stack is not needed: what is being pinned is that the combo is read
|
|
-- before stack:update, so it fires from a battle, a menu or a cutscene and
|
|
-- not just from the overworld (#563).
|
|
local function fakeGame()
|
|
local game = { input = Input, save = {}, returned = 0 }
|
|
game.stack = {
|
|
updates = 0,
|
|
update = function(self) self.updates = self.updates + 1 end,
|
|
}
|
|
function game:returnToTitle() self.returned = self.returned + 1 end
|
|
return game
|
|
end
|
|
|
|
local game = fakeGame()
|
|
holdKeys("z", "x", "escape", "tab")
|
|
for _ = 1, HOLD_STEPS do Game.step(game, 1 / 60) end
|
|
eq(game.returned, 1, "Game:step falls back to the title on the 16th step")
|
|
eq(game.stack.updates, HOLD_STEPS - 1,
|
|
"and the state on top never gets that step, whatever it was")
|
|
check(not Input:isDown("a"),
|
|
"the still-physically-held A is cleared, so the title screen does not "
|
|
.. "read it as a menu choice on its first frame")
|
|
|
|
-- A tool mod may hand Game a stand-in input; Game guards on the method
|
|
-- existing rather than assuming it, so those simply never soft reset.
|
|
local bare = fakeGame()
|
|
bare.input = { step = function() end, isDown = function() return false end }
|
|
for _ = 1, HOLD_STEPS * 2 do Game.step(bare, 1 / 60) end
|
|
eq(bare.returned, 0, "an input with no chord bookkeeping never resets")
|
|
|
|
-- ---- the on-screen overlay ---------------------------------------------
|
|
|
|
-- Force the overlay live the way a phone would have it. img is only ever
|
|
-- tested for truthiness on the input path (drawing is what reads it), so a
|
|
-- placeholder keeps this suite off love.graphics.newImage.
|
|
Input:init()
|
|
TouchControls:init()
|
|
TouchControls.active, TouchControls.enabled = true, true
|
|
TouchControls.img = { stub = true }
|
|
local L = TouchControls:layout()
|
|
|
|
-- One finger, one control: touchpressed returns on its first hit, so no
|
|
-- single touch can ever arm more than a quarter of the chord however the
|
|
-- layout editor has moved the controls around.
|
|
for _, btn in ipairs({ "a", "b", "start", "select" }) do
|
|
Input:init()
|
|
TouchControls:reset()
|
|
TouchControls:touchpressed(1, L[btn].cx, L[btn].cy)
|
|
local held = 0
|
|
for _ in pairs(TouchControls.held) do held = held + 1 end
|
|
eq(held, 1, "a finger on " .. btn:upper() .. " presses that button alone")
|
|
check(not Input:softResetHeld(), "...which is not the combo")
|
|
end
|
|
|
|
-- Four fingers on four controls is the only way there, and it still has to
|
|
-- survive the same 16 steps -- better than a quarter second of everything
|
|
-- staying put.
|
|
Input:init()
|
|
TouchControls:reset()
|
|
local ids = { a = 1, b = 2, start = 3, select = 4 }
|
|
for btn, id in pairs(ids) do
|
|
TouchControls:touchpressed(id, L[btn].cx, L[btn].cy)
|
|
end
|
|
check(Input:softResetHeld(), "four fingers on A, B, START and SELECT arm it")
|
|
for i = 1, HOLD_STEPS - 1 do
|
|
check(not stepChord(), "the overlay holds the same countdown (step " .. i .. ")")
|
|
end
|
|
check(stepChord(), "and resets on the 16th")
|
|
|
|
-- the accidental version: one finger slips off part way through
|
|
Input:init()
|
|
TouchControls:reset()
|
|
for btn, id in pairs(ids) do
|
|
TouchControls:touchpressed(id, L[btn].cx, L[btn].cy)
|
|
end
|
|
for _ = 1, HOLD_STEPS - 1 do stepChord() end
|
|
TouchControls:touchreleased(ids.select, L.select.cx, L.select.cy)
|
|
check(not stepChord(), "a finger leaving SELECT one step short cancels")
|
|
check(not Input:softResetHeld(), "and the chord is no longer armed")
|
|
|
|
-- the fifth finger a real two-handed grip has on the d-pad
|
|
Input:init()
|
|
TouchControls:reset()
|
|
for btn, id in pairs(ids) do
|
|
TouchControls:touchpressed(id, L[btn].cx, L[btn].cy)
|
|
end
|
|
TouchControls:touchpressed(5, L.dpad.cx, L.dpad.cy - L.dpad.w * 0.4)
|
|
check(not Input:softResetHeld(),
|
|
"a thumb on the d-pad cancels it the same way a keyboard direction does")
|
|
|
|
Input:init()
|
|
TouchControls:reset()
|
|
T.finish("soft_reset_bug563")
|