mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
235 lines
9.7 KiB
Lua
235 lines
9.7 KiB
Lua
-- Ear check: the menu click on backing out of POKéDEX / POKéMON / ITEM /
|
|
-- OPTION, on the first A on an item in ITEM, and on A over CANCEL in OPTION
|
|
-- (#570). HandleMenuInput_ (home/window.asm) replays SFX_PRESS_AB for the
|
|
-- PAD_A | PAD_B half of a menu's watched keys; DisplayOptionMenu beeps only
|
|
-- at .exitMenu (engine/menus/main_menu.asm). Silence where the original is
|
|
-- silent counts too, so this walks the mute cases as well. Invariants are in
|
|
-- tests/engine/menu_click_bug570.lua.
|
|
-- POKEPORT_DRIVER=tests/drivers/menu_click_bug570_test.lua POKEPORT_IDENTITY=bug570 POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love .
|
|
-- No POKEPORT_SPEED: it scales the logic clock only while audio runs on its own
|
|
-- real-time accumulator in Game:update, so it desyncs the exact ordering of
|
|
-- press and click that this run exists to judge.
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
local Bag = require("src.inventory.Bag")
|
|
local ListMenu = require("src.ui.ListMenu")
|
|
local Menu = require("src.ui.Menu")
|
|
local OptionsMenu = require("src.ui.OptionsMenu")
|
|
local PartyMenu = require("src.ui.PartyMenu")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local Sound = require("src.core.Sound")
|
|
local Strings = require("src.core.Strings")
|
|
|
|
-- pokered data/maps/objects/PalletTown.asm: OAK at (8,5), the girl at (3,8),
|
|
-- the fisher at (11,14), warps at (5,5), (13,5) and (12,11). (10,7) is clear
|
|
-- of all of them and off every mat; the menus are what is being judged, the
|
|
-- cell only has to be somewhere the start menu opens.
|
|
local MAP = "PALLET_TOWN"
|
|
local STAND = { x = 10, y = 7, facing = "down" }
|
|
local PARTY = { { "BULBASAUR", 12 }, { "PIDGEY", 9 } }
|
|
|
|
local function check(label, ok)
|
|
U.log(ok and "PASS" or "FAIL", label)
|
|
return ok
|
|
end
|
|
|
|
-- ---- what the ear cannot check -----------------------------------------
|
|
local opts = game.save.options or {}
|
|
local musicVol = opts.musicVol or 7
|
|
local sfxVol = opts.sfxVol or 7
|
|
if sfxVol == 0 then
|
|
U.log("FAIL SFX volume is 0. Every claim below is about a click that is or")
|
|
U.log(" is not there, and at 0 none of them are. Set SFX to 7 in OPTION")
|
|
U.log(" and run this again, or the whole check is worthless.")
|
|
end
|
|
if musicVol == 0 then
|
|
U.log("FAIL MUSIC volume is 0, so Pallet Town's theme is gone and there is")
|
|
U.log(" nothing to tell a dead audio device from a missing click.")
|
|
U.log(" Set MUSIC to 7 in OPTION.")
|
|
end
|
|
check(("SFX volume %d, MUSIC volume %d"):format(sfxVol, musicVol),
|
|
sfxVol > 0 and musicVol > 0)
|
|
|
|
-- an unresolved sfx key is silent in exactly the way the bug was
|
|
local sfx = (game.data.audio or {}).sfx or {}
|
|
for _, key in ipairs({ "Press_AB", "Start_Menu", "Swap" }) do
|
|
check("sfx " .. key .. " is in the cache", sfx[key] ~= nil)
|
|
end
|
|
|
|
-- every cue the run makes, forwarded so playback is untouched
|
|
local cues = {}
|
|
local realPlay = Sound.play
|
|
Sound.play = function(data, name)
|
|
cues[#cues + 1] = name
|
|
return realPlay(data, name)
|
|
end
|
|
local function since(mark)
|
|
local clicks, others = 0, {}
|
|
for i = mark + 1, #cues do
|
|
if cues[i] == "Press_AB" then clicks = clicks + 1
|
|
else others[#others + 1] = cues[i] end
|
|
end
|
|
return clicks, others
|
|
end
|
|
|
|
-- one press, then long enough for the click to be its own sound
|
|
local function press(btn, want, label)
|
|
local mark = #cues
|
|
U.tap(game, btn)
|
|
U.wait(28)
|
|
local clicks, others = since(mark)
|
|
local note = ""
|
|
if #others > 0 then note = " (also: " .. table.concat(others, ", ") .. ")" end
|
|
check(("%s: %s gave %d click%s, expected %d%s")
|
|
:format(label, btn:upper(), clicks, clicks == 1 and "" or "s",
|
|
want, note),
|
|
clicks == want)
|
|
return clicks
|
|
end
|
|
|
|
local function top() return game.stack:top() end
|
|
local function isA(class) return getmetatable(top()) == class end
|
|
|
|
-- ---- set the save up so every start-menu row is live --------------------
|
|
game.save.party = {}
|
|
for _, slot in ipairs(PARTY) do
|
|
table.insert(game.save.party, Pokemon.new(game.data, slot[1], slot[2]))
|
|
end
|
|
game.save.flags = game.save.flags or {}
|
|
game.save.flags.EVENT_GOT_POKEDEX = true -- StartMenu hides POKéDEX without it
|
|
game.save.pokedex = game.save.pokedex or { seen = {}, owned = {} }
|
|
for _, id in ipairs({ "BULBASAUR", "PIDGEY", "RATTATA" }) do
|
|
game.save.pokedex.seen[id] = true
|
|
game.save.pokedex.owned[id] = true
|
|
end
|
|
Bag.add(game.save, "POTION", 3)
|
|
Bag.add(game.save, "ANTIDOTE", 2)
|
|
game.save.startMenuIndex = 1
|
|
|
|
U.teleport(game, MAP, STAND.x, STAND.y, STAND.facing)
|
|
U.wait(20)
|
|
local ow = game.overworld
|
|
if ow and not ow.map:isWalkableCell(STAND.x, STAND.y) then
|
|
-- a map edit blocked the cell: any free walkable neighbour will do
|
|
for _, d in ipairs({ { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 } }) do
|
|
local cx, cy = STAND.x + d[1], STAND.y + d[2]
|
|
if ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then
|
|
U.log(("(%d, %d) is blocked, standing on"):format(STAND.x, STAND.y),
|
|
cx, cy)
|
|
U.teleport(game, MAP, cx, cy, STAND.facing)
|
|
U.wait(20)
|
|
ow = game.overworld
|
|
break
|
|
end
|
|
end
|
|
end
|
|
check("standing on " .. MAP, ow ~= nil and ow.map.id == MAP)
|
|
-- the girl and the fisher both WALK: pin them so the map under the menus
|
|
-- looks the same on every run and in every screenshot
|
|
for _, n in ipairs((ow or {}).npcs or {}) do n.frozen = true end
|
|
|
|
-- ---- walk the menus -----------------------------------------------------
|
|
press("start", 0, "opening the start menu is silent (its own Start_Menu "
|
|
.. "cue is not the A/B click)")
|
|
check("the start menu is up", isA(Menu))
|
|
|
|
-- rows come and go with save state and the ui.start_menu.items hook, so
|
|
-- find each one by label rather than counting cursor steps
|
|
local function rowIndex(label)
|
|
for i, item in ipairs(top().items or {}) do
|
|
if item.label == label then return i end
|
|
end
|
|
return nil
|
|
end
|
|
local function openRow(label)
|
|
local menu = top()
|
|
local i = rowIndex(label)
|
|
if not i then
|
|
check("the start menu has a " .. label .. " row", false)
|
|
return false
|
|
end
|
|
menu.index = i
|
|
menu:clampScroll()
|
|
press("a", 1, "A on " .. label .. " clicks")
|
|
return true
|
|
end
|
|
|
|
if openRow(Strings("POKéDEX")) then
|
|
check("the POKéDEX list opened", isA(ListMenu))
|
|
U.shot(game, DIR .. "/bug570_1_pokedex.png")
|
|
press("b", 1, "backing out of POKéDEX clicks (#570)")
|
|
check("and the start menu came back", isA(Menu))
|
|
end
|
|
|
|
if openRow(Strings("POKéMON")) then
|
|
check("the party menu opened", getmetatable(top()) == PartyMenu)
|
|
press("b", 1, "backing out of POKéMON clicks (#570)")
|
|
check("and the start menu came back", isA(Menu))
|
|
end
|
|
|
|
if openRow(Strings("ITEM")) then
|
|
check("the bag opened", isA(ListMenu))
|
|
check("the bag has items to click on", #(top().items or {}) > 0)
|
|
press("a", 1, "the first A on an item in ITEM clicks (#570)")
|
|
check("the USE / TOSS submenu opened", isA(Menu))
|
|
U.shot(game, DIR .. "/bug570_2_bag_usetoss.png")
|
|
press("b", 1, "backing out of USE / TOSS clicks")
|
|
check("back on the item list", isA(ListMenu))
|
|
-- SELECT is inside DisplayListMenuID's watched mask but outside its
|
|
-- PAD_A | PAD_B sound test (home/list_menu.asm), so arming a swap is
|
|
-- silent and completing it makes the swap chirp, not the menu click
|
|
press("select", 0, "SELECT arms a swap without clicking")
|
|
press("select", 0, "and completing it chirps instead of clicking")
|
|
press("b", 1, "backing out of ITEM clicks (#570)")
|
|
check("and the start menu came back", isA(Menu))
|
|
end
|
|
|
|
if openRow(Strings("OPTION")) then
|
|
check("the options screen opened", getmetatable(top()) == OptionsMenu)
|
|
press("right", 0, "the Left/Right toggles are silent")
|
|
press("left", 0, "in both directions")
|
|
-- up from the first row wraps onto CANCEL, which sits one past the rows
|
|
press("up", 0, "moving the cursor is silent")
|
|
check("the cursor is on CANCEL",
|
|
top().index == #top().rows + 1)
|
|
U.shot(game, DIR .. "/bug570_3_option_cancel.png")
|
|
press("a", 1, "A on CANCEL in OPTION clicks (#570)")
|
|
check("and the start menu came back", isA(Menu))
|
|
end
|
|
|
|
if openRow(Strings("OPTION")) then
|
|
press("b", 1, "B out of OPTION clicks too (#570)")
|
|
check("and the start menu came back", isA(Menu))
|
|
end
|
|
|
|
press("start", 0, "START closes the start menu silently: draw_start_menu's "
|
|
.. "mask watches it, but it is not in the PAD_A | PAD_B sound branch")
|
|
check("back on the map", top() == game.overworld)
|
|
|
|
-- ---- over to you --------------------------------------------------------
|
|
U.log("The run above already pressed all of it; do it again by ear. Open START,")
|
|
U.log("go into POKéDEX, POKéMON, ITEM and OPTION and back out of each with B.")
|
|
U.log("Every one of those B presses makes the same short click that A on the")
|
|
U.log("row made going in, the first A on an item in ITEM makes it, and A on")
|
|
U.log("CANCEL in OPTION closes it with it. #570 was silence on all of those.")
|
|
U.log("The near miss is a click at the wrong moment: OPTION's Left/Right")
|
|
U.log("toggles and its A on a setting row stay silent, START opens and closes")
|
|
U.log("the start menu with its own longer cue and no click, and SELECT in the")
|
|
U.log("bag chirps a swap rather than clicking.")
|
|
U.log("Shots: " .. DIR .. "/bug570_*.png")
|
|
|
|
-- keeps printing the cue stream after the hand-off, so a click that lands a
|
|
-- beat late reads as a late line rather than as the right sound
|
|
local reported = #cues
|
|
while true do
|
|
if #cues > reported then
|
|
for i = reported + 1, #cues do
|
|
U.log("cue", cues[i], "frame", U.frame())
|
|
end
|
|
reported = #cues
|
|
end
|
|
coroutine.yield()
|
|
end
|
|
end
|