mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-04 00:20:01 -04:00
284 lines
17 KiB
Lua
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()
|