Files
gen1recomp/tests/engine/shoulder_input_h17.lua
T

284 lines
17 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness").suite("Resolved shoulder input H17")
if os.getenv("H17_CANDIDATE") then
local root = os.getenv("H17_CANDIDATE")
for _, name in ipairs({"Input", "Game", "Game2", "Game3"}) do
package.preload["src.core." .. name] = assert(loadfile(root .. "/src/core/" .. name .. ".lua"))
end
end
do
local Input = require("src.core.Input")
local Controls = require("src.ui.game3.controls_menu")
local Stack = require("src.ui.game3.stack")
local Game = require("src.core.Game")
local Game2 = require("src.core.Game2")
local Game3 = require("src.core.Game3")
local Bag = require("src.core.game3.bag")
local BagMenu = require("src.ui.game3.bag_menu")
local Items = require("src.core.game3.items_data")
local Options = require("src.core.game3.options")
local Map = require("src.core.GamepadMap")
local safe = true
local pass, fail = 0, 0
local function check(ok, label)
T.check(ok, label)
end
local function newGame(class, options)
local g = setmetatable({input = Input, options = options or {}, save = {options = options or {}}, stack = {top = function() return nil end}, speeds = 0,
writeOptions = function(self) self.writes = (self.writes or 0) + 1 end,
_cycleSpeed = function(self, n) self.speeds = self.speeds + n end}, {__index = class})
return g
end
local function bindAtoLB()
Input:init(); Stack.clear()
local beforeLB = Input.padBindings.leftshoulder
local g = newGame(Game3)
Controls.show({game = g})
local menu = assert(Controls._bm)
for i, item in ipairs(menu.items) do if item.button.id == "a" then menu.index = i end end
Input:overlayPressed("a"); Input:step(); Controls.handleInput(Input)
Input:overlayReleased("a"); Input:step(); Controls.handleInput(Input)
g:gamepadpressed(nil, "leftshoulder"); Input:step(); Controls.handleInput(Input)
g:gamepadreleased(nil, "leftshoulder"); Input:step(); Controls.handleInput(Input)
check(g.options.bindings.a.pad == "leftshoulder" and g.options.bindings.l.pad == "a", "actual Controls A←LB/L←oldA swap")
check(g.writes == 1 and Input.padBindings.leftshoulder == beforeLB, "one detached write and delayed apply")
Input:overlayPressed("b"); Input:step(); Controls.handleInput(Input)
Input:overlayReleased("b"); Input:step()
check(not Controls.open and not Stack.has("controls"), "real menu closes and applies")
return g
end
local g3 = bindAtoLB()
Items.installPack({items = {
[13] = {name = "POTION", pocket = "ITEMS", description = "Fixture potion"},
[360] = {name = "BICYCLE", pocket = "KEY_ITEMS", description = "Fixture bike", registrability = 1},
[4] = {name = "POKE BALL", pocket = "POKE_BALLS", description = "Fixture ball"},
}})
for _, lr in ipairs({true, false}) do
local session = {version = "firered", options = {buttonMode = lr and 1 or 0}, bag = Bag.new(), party = {}}
session.bag.pockets.KEY_ITEMS = {{id = 360, qty = 1}}
BagMenu.show(session, {pocketIdx = 2})
BagMenu.settle()
check(Options.lrMode(session) == lr and BagMenu.currentPocket() == "KEY_ITEMS", "actual LR option and settled KEY pocket")
g3:gamepadpressed(nil, "leftshoulder"); Input:step()
check(Input:wasPressed("a"), "configured A reaches actual Bag input")
BagMenu.handleInput(Input)
if lr then
check(BagMenu.pocketIdx == (safe and 2 or 1) and BagMenu.mode == (safe and "action" or "list"), "LR mode: A must open KEY action instead of paging left")
print("OBS bag LR=true pocket=" .. BagMenu.currentPocket() .. " mode=" .. BagMenu.mode .. " extraL=" .. tostring(Input:wasPressed("l")))
else
check(BagMenu.pocketIdx == 2 and BagMenu.mode == "action", "LR off: actual A action consumer positive control")
end
g3:gamepadreleased(nil, "leftshoulder"); Input:step(); BagMenu.close()
end
for _, row in ipairs({{Game, "Gen1"}, {Game2, "Gen2"}}) do
Input:reset(); Input:applyBindings(g3.options.bindings)
local g = newGame(row[1], g3.options)
g:gamepadpressed(nil, "leftshoulder"); Input:step()
check(g.speeds == (safe and 0 or -1) and Input:wasPressed("a") == safe, row[2] .. " plain assigned LB must defeat inherited default speed")
g:gamepadreleased(nil, "leftshoulder"); Input:step()
end
local physical = {}
local pad = {isGamepad = function() return true end, isGamepadDown = function(_, b) return physical[b] == true end,
getGamepadAxis = function() return 0 end, isConnected = function() return true end, getName = function() return "Xbox Controller" end}
love.joystick = {getJoystickCount = function() return 1 end, getJoysticks = function() return {pad} end}
Input:reset(); Input:applyBindings(g3.options.bindings); Input:step()
physical.leftshoulder = true
g3:gamepadpressed(pad, "leftshoulder"); Input:step()
physical.leftshoulder = false
Input:step()
check(not Input:isDown("a"), "poll repair releases configured A when release event is lost")
check(Input:isDown("l") == not safe, "poll repair must also avoid stranded hardwired extra L")
print("OBS missed release: A=" .. tostring(Input:isDown("a")) .. " L=" .. tostring(Input:isDown("l")))
Input:init(); Input:step(); physical.leftshoulder = true; Input:step()
check(Input:isDown("l") == safe, "default first-class L must use same pollable map on lost press")
physical.leftshoulder = false; Input:step()
Input:reset(); physical.leftshoulder = true; Input:reconcile(); Input:step()
check(Input:isDown("l") == safe, "default first-class L must use same map during reconcile")
physical.leftshoulder = false; Input:step()
love.joystick = nil
for _, button in ipairs({"leftshoulder", "rightshoulder"}) do
Input:init(); Input:applyBindings({speedUp = {pad = button}})
local g = newGame(Game3)
g:gamepadpressed(nil, button); Input:step()
check(g.speeds == (safe and 1 or 0), "explicit Gen3 SPEED+ on " .. button .. " must execute")
check(Input:wasPressed(button == "leftshoulder" and "l" or "r") == not safe, "explicit speed action must not emit shoulder gameplay")
g:gamepadreleased(nil, button); Input:step()
end
for _, row in ipairs({{Game, "Gen1"}, {Game2, "Gen2"}, {Game3, "Gen3"}}) do
Input:init(); Input:applyBindings({a = {pad = "lefttrigger"}})
check(Input.padBindings.triggerleft == "a", "legacy trigger alias canonicalizes")
local g = newGame(row[1]); g:gamepadaxis(nil, "triggerleft", 1); Input:step()
check(g.speeds == (safe and 0 or -1) and Input:wasPressed("a") == safe, row[2] .. " plain trigger binding must defeat inherited default action")
g:gamepadaxis(nil, "triggerleft", 0); Input:step()
end
for _, row in ipairs({{Game, "Gen1"}, {Game2, "Gen2"}, {Game3, "Gen3"}}) do
Input:init(); Input:applyBindings({a = {pad = "joy5"}, speedUp = {pad = "joy6"}})
local g = newGame(row[1])
g:joystickpressed(nil, 5); Input:step()
check(Input:wasPressed("a") and g.speeds == 0, row[2] .. " unrecognized raw JOY5 plain binding positive")
g:joystickreleased(nil, 5); Input:step()
g:joystickpressed(nil, 6); Input:step()
check(g.speeds == 1, row[2] .. " unrecognized raw JOY6 explicit speed positive")
g:joystickreleased(nil, 6); Input:step()
end
check(Map.DEFAULT_GAMEPAD_BINDINGS.leftshoulder == nil and Map.DEFAULT_PAD_ACTIONS.leftshoulder == "speedDown", "launcher/shared defaults unchanged during read-only probe")
end
do
local Input = require("src.core.Input")
local Game = require("src.core.Game")
local Game3 = require("src.core.Game3")
local Controls = require("src.ui.game3.controls_menu")
local Stack = require("src.ui.game3.stack")
local safe = true
local pass, fail = 0, 0
local function check(ok, label)
T.check(ok, label)
end
local function newGame(class)
return setmetatable({input = Input, options = {}, save = {options = {}}, stack = {top = function() return nil end}, speeds = 0,
writeOptions = function(self) self.writes = (self.writes or 0) + 1 end,
_cycleSpeed = function(self, n) self.speeds = self.speeds + n end}, {__index = class})
end
Input:init(); Stack.clear()
local g3 = newGame(Game3)
Controls.show({game = g3})
for i, item in ipairs(Controls._bm.items) do if item.button.id == "speedUp" then Controls._bm.index = i end end
Input:overlayPressed("a"); Input:step(); Controls.handleInput(Input)
Input:overlayReleased("a"); Input:step(); Controls.handleInput(Input)
g3:gamepadpressed(nil, "leftshoulder"); Input:step(); Controls.handleInput(Input)
g3:gamepadreleased(nil, "leftshoulder"); Input:step(); Controls.handleInput(Input)
check(g3.options.bindings.speedUp.pad == "leftshoulder" and g3.options.bindings.l.pad == "triggerright", "actual SPEED+←LB/L←oldR2 swap")
check(g3.writes == 1, "one actual detached configuration write")
Input:overlayPressed("b"); Input:step(); Controls.handleInput(Input)
Input:overlayReleased("b"); Input:step()
g3:gamepadpressed(nil, "leftshoulder"); Input:step()
check(g3.speeds == (safe and 1 or 0) and Input:wasPressed("l") == not safe, "actual speed-row shoulder binding must execute exclusively")
g3:gamepadreleased(nil, "leftshoulder"); Input:step()
g3:gamepadaxis(nil, "triggerright", 1); Input:step()
check(Input:wasPressed("l"), "actual moved L on old R2 remains usable")
g3:gamepadaxis(nil, "triggerright", 0); Input:step()
Input:init(); Input:applyBindings({speedDown = false, a = {pad = "leftshoulder"}})
local g1 = newGame(Game)
g1:gamepadpressed(nil, "leftshoulder"); Input:step()
check(g1.speeds == 0 and Input:wasPressed("a"), "explicit OFF defeats both inherited speedDown aliases")
g1:gamepadreleased(nil, "leftshoulder"); Input:step()
check(Input:padAction("triggerleft") == nil, "explicit OFF disables the trigger alias as today")
Input:init(); Input:applyBindings({a = {pad = "lefttrigger"}, speedUp = {pad = "triggerleft"}})
g1:gamepadaxis(nil, "triggerleft", 1); Input:step()
check(g1.speeds == 1 and not Input:wasPressed("a"), "explicit speed action wins a canonicalized conflicting plain binding")
g1:gamepadaxis(nil, "triggerleft", 0); Input:step()
Input:init(); Input:applyBindings({speedUp = {pad = "joy1"}})
local raw = {isGamepad = function() return false end, isDown = function(_, b) return b == 1 end,
getAxis = function() return 0 end, getHatCount = function() return 0 end, getName = function() return "Generic Controller" end}
love.joystick = {getJoysticks = function() return {raw} end, getJoystickCount = function() return 1 end}
Input:reconcile(); Input:step()
check(Input:isDown("a") == not safe, "explicit raw action must not rehydrate a shadow default A during reconcile")
Input:reset(); love.joystick = nil
end
do
local Input = require("src.core.Input")
local classes = {{require("src.core.Game"), "Gen1"}, {require("src.core.Game2"), "Gen2"}, {require("src.core.Game3"), "Gen3"}}
local expected = true
local pass, fail = 0, 0
for _, row in ipairs(classes) do
Input:init(); Input:applyBindings({speedUp = {pad = "joy1"}})
local g = setmetatable({input = Input, options = {}, save = {options = {}}, stack = {top = function() return nil end}, speeds = 0,
_cycleSpeed = function(self, n) self.speeds = self.speeds + n end}, {__index = row[1]})
local pad = {isGamepad = function() return true end, getName = function() return "Xbox Controller" end}
g:joystickpressed(pad, 1)
Input:step()
local ok = g.speeds == (expected and 0 or 1) and not Input:wasPressed("a")
T.check(ok, row[2] .. " recognized raw event cannot dispatch actions")
print((ok and "PASS " or "FAIL ") .. row[2] .. " recognized pad raw event must be ignored before action lookup; speed=" .. g.speeds)
g:joystickreleased(pad, 1); Input:step()
end
end
do
local Input = require("src.core.Input")
local Map = require("src.core.GamepadMap")
local classes = {require("src.core.Game"), require("src.core.Game2"), require("src.core.Game3")}
local physical = {}
local pad = {isGamepad = function() return true end, getName = function() return "Xbox" end,
isGamepadDown = function(_, b) return physical[b] == true end,
getGamepadAxis = function() return 0 end, isConnected = function() return true end}
love.joystick = {getJoysticks = function() return {pad} end, getJoystickCount = function() return 1 end}
local function fixture(i, bindings)
physical = {}
Input:init(i == 3); Input:applyBindings(bindings); Input:step()
return setmetatable({input = Input, stack = {top = function() return nil end}, speeds = 0,
_cycleSpeed = function(self, n) self.speeds = self.speeds + n end}, {__index = classes[i]})
end
for i = 1, 3 do
local g = fixture(i)
physical.leftshoulder = true; g:gamepadpressed(pad, "leftshoulder"); Input:step()
T.eq(g.speeds, i == 3 and 0 or -1, "generation " .. i .. " default shoulder action")
T.eq(Input:isDown("l"), i == 3, "generation " .. i .. " default shoulder gameplay")
physical.leftshoulder = false; Input:step()
T.check(not Input:isDown("l"), "lost shoulder release clears generation " .. i)
g = fixture(i); physical.rightshoulder = true; Input:step()
T.eq(Input:isDown("r"), i == 3, "lost default press follows generation policy " .. i)
Input:reset(); Input:reconcile(); Input:step()
T.eq(Input:isDown("r"), i == 3, "reconcile follows generation policy " .. i)
g = fixture(i, {a = {pad = "rightshoulder"}})
physical.rightshoulder = true; Input:step()
T.check(Input:isDown("a") and not Input:isDown("r"), "poll resolves configured A exclusively " .. i)
Input:reset(); Input:reconcile(); Input:step()
T.check(Input:isDown("a") and not Input:isDown("r"), "reconcile resolves configured A exclusively " .. i)
g = fixture(i, {speedUp = {pad = "a"}})
physical.a = true; Input:step(); Input:reset(); Input:reconcile(); Input:step()
T.check(not Input:isDown("a"), "explicit face action never rehydrates inherited A " .. i)
g:gamepadpressed(pad, "a"); Input:step()
T.eq(g.speeds, 1, "explicit face speed callback executes " .. i)
g = fixture(i, {speedDown = false})
T.eq(Input:padAction("leftshoulder"), nil, "OFF removes default shoulder action " .. i)
T.eq(Input:padAction("triggerleft"), nil, "OFF removes default trigger action " .. i)
end
local g = fixture(3, {a = {pad = "leftshoulder"}})
Input:keypressed("z"); Input:overlayPressed("a"); Input:sourcePress("a", "mod:test:1")
physical.leftshoulder = true; g:gamepadpressed(pad, "leftshoulder"); Input:step()
T.check(Input.sources.a["pad:leftshoulder"] and not Input.sources.l, "configured source uses physical pad tag only")
physical.leftshoulder = false; Input:step()
T.check(Input:isDown("a") and not Input.sources.a["pad:leftshoulder"], "poll releases only its source while keyboard/touch/mod cohold")
Input:keyreleased("z"); Input:overlayReleased("a"); Input:sourceRelease("a", "mod:test:1"); Input:step()
T.check(not Input:isDown("a"), "last independent cohold clears")
g = fixture(3)
g:gamepadpressed(nil, "leftshoulder"); g:gamepadreleased(nil, "leftshoulder"); Input:step()
T.check(Input:wasPressed("l") and not Input:isDown("l"), "same-frame shoulder tap preserves edge without hold")
g = fixture(3); physical.leftshoulder = true; Input:reset(); Input:step()
T.check(not Input:isDown("l"), "soft reset suppresses physically held shoulder")
physical.leftshoulder = false; Input:step(); physical.leftshoulder = true; Input:step()
T.check(Input:isDown("l"), "physical neutral rearms shoulder poll")
g = fixture(3); Input.captureArmed = true; physical.leftshoulder = true; Input:step()
T.check(not Input:isDown("l"), "poll cannot synthesize during raw capture")
g:gamepadpressed(pad, "leftshoulder"); Input:step()
T.check(Input.captureEvents and Input.captureEvents[1].value == "leftshoulder", "actual capture event keeps shoulder candidate")
g:gamepadreleased(pad, "leftshoulder"); Input.captureArmed = false; physical.leftshoulder = false; Input:step()
local Runtime = require("src.mods.Runtime"); local wants = Runtime.wantsHook
g = fixture(3); Runtime.wantsHook = function(hook) return hook == "input.gamepad" end
physical.leftshoulder = true; Input:step()
T.check(not Input:isDown("l"), "mod pad owner suppresses watchdog press")
Runtime.wantsHook = wants
local Hints = require("src.core.PadHints"); local minimized = Hints.windowMinimized
g = fixture(3); Hints.windowMinimized = function() return true end
physical.leftshoulder = true; Input:step()
T.check(not Input:isDown("l"), "minimized window suppresses watchdog press")
Hints.windowMinimized = minimized
love.joystick = nil
for i = 1, 3 do
g = fixture(i, {speedUp = {pad = "joy1"}}); love.joystick = nil
local sensor = {isGamepad = function() return false end, getName = function() return "Phone Accelerometer" end}
g:joystickpressed(sensor, 1); Input:step()
T.check(g.speeds == 0 and not Input:isDown("a"), "sensor raw callback ignored before action lookup " .. i)
end
Map._setForceNXForTests(true)
Input:init(); Input:applyBindings({a = {pad = "leftshoulder"}})
Input:gamepadpressed(pad, "leftshoulder"); Input:step()
T.check(Input:isDown("a") and not Input:isDown("l"), "NX explicit shoulder resolves exclusively")
T.eq(Input.padBindings.b, "a", "NX face default remains Nintendo A")
T.eq(#Input.padPoll, 0, "NX still excludes unsupported polling")
Map._setForceNXForTests(false)
Input:init(); love.joystick = nil
end
T.finish()