mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
276 lines
11 KiB
Lua
276 lines
11 KiB
Lua
-- Manual check that FLASH in a dark cave costs one short blink and not a long
|
|
-- white hang (#610). pokered engine/menus/start_sub_menus.asm .flash clears
|
|
-- wMapPalOffset, prints _FlashLightsAreaText over the party menu and only then
|
|
-- runs GBPalWhiteOutWithDelay3, so the cave is already lit when the blink
|
|
-- starts; ADVANCED rebakes its atlas there (#383), which must not ride the blink.
|
|
-- POKEPORT_DRIVER=tests/drivers/rock_tunnel_flash_blink_bug610_test.lua \
|
|
-- POKEPORT_IDENTITY=bug610 POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots610 love .
|
|
-- No POKEPORT_SPEED: fast-forward desynchronizes audio and logic ordering.
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local PartyMenu = require("src.ui.PartyMenu")
|
|
local PaletteFX = require("src.render.PaletteFX")
|
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots610"
|
|
|
|
local fails = 0
|
|
local function check(ok, msg)
|
|
U.log(ok and "PASS" or "FAIL", msg)
|
|
if not ok then fails = fails + 1 end
|
|
return ok
|
|
end
|
|
|
|
-- wall clock, not os.clock: the bake this bug is about blocks the frame, and
|
|
-- CPU time would hide any of it that waits on the GPU
|
|
local now = (love and love.timer and love.timer.getTime)
|
|
and love.timer.getTime or os.clock
|
|
|
|
-- one frame, remembering the longest frame seen since the last reset and
|
|
-- what was drawn on it: "the hitch happened under the party menu" is the
|
|
-- whole claim of the fix, and it is not visible in a total
|
|
local worst, worstWhere = 0, "nothing"
|
|
local function label()
|
|
local top = game.stack:top()
|
|
if top == nil then return "nothing" end
|
|
if getmetatable(top) == PartyMenu then return "the party menu" end
|
|
if top.pages then return "the FLASH message" end
|
|
if top == game.overworld then return "the map" end
|
|
if type(top.t) == "number" and type(top.frames) == "number" then
|
|
return "the white blink"
|
|
end
|
|
return "a menu"
|
|
end
|
|
local function step(n)
|
|
for _ = 1, n do
|
|
local where = label()
|
|
local t0 = now()
|
|
coroutine.yield()
|
|
local dt = now() - t0
|
|
if dt > worst then worst, worstWhere = dt, where end
|
|
end
|
|
end
|
|
local function resetWorst() worst, worstWhere = 0, "nothing" end
|
|
|
|
-- U.tap spends a frame of its own outside step(), and on the last A of the
|
|
-- FLASH message that is precisely the frame the work lands on, so press
|
|
-- through the tracked step instead of losing it
|
|
local function tap(btn)
|
|
table.insert(game.input.pressQueue, btn)
|
|
step(1)
|
|
game.input.state[btn] = false
|
|
end
|
|
|
|
-- the blink is the transition record's own frame count, not a state class:
|
|
-- Transition keeps WhiteFlash local, so recognize it by shape (a counter, a
|
|
-- length, no text pages) the way the stack itself sees it
|
|
local function isBlink(s)
|
|
return s ~= nil and s.pages == nil and type(s.t) == "number"
|
|
and type(s.frames) == "number"
|
|
end
|
|
|
|
game.save.flags.EVENT_GOT_STARTER = true
|
|
-- every list-time badge gate in PartyMenu: FLASH is the subject, STRENGTH is
|
|
-- the reference blink (it always prints and always blinks), SURF is there so
|
|
-- the Route 10 pond can be tried too
|
|
game.save.inventory.BOULDERBADGE = true
|
|
game.save.inventory.RAINBOWBADGE = true
|
|
game.save.inventory.SOULBADGE = true
|
|
game.save.options = game.save.options or {}
|
|
game.save.options.textSpeed = 1
|
|
game.save.options.colors = "redpp" -- ADVANCED, the mode whose rebake is dear
|
|
PaletteFX.setMode("redpp")
|
|
|
|
local function giveMon()
|
|
local mon = Pokemon.new(game.data, "PIKACHU", 30)
|
|
mon.moves = { { id = "FLASH", pp = 5 }, { id = "STRENGTH", pp = 15 },
|
|
{ id = "SURF", pp = 15 } }
|
|
game.save.party = { mon }
|
|
return mon
|
|
end
|
|
giveMon()
|
|
|
|
-- ---- what has to be true before any of this can be judged ---------------
|
|
check(PaletteFX.gbcPack() ~= nil,
|
|
"the ADVANCED colour pack is installed (without it nothing rebakes "
|
|
.. "and this driver proves nothing)")
|
|
check(PaletteFX.usesGbcPack(), "and the active mode is the baking one")
|
|
|
|
local darkDef = game.data.field.darkMaps
|
|
local listed = false
|
|
for _, m in ipairs(darkDef and darkDef.maps or {}) do
|
|
if m == "ROCK_TUNNEL_1F" then listed = true end
|
|
end
|
|
check(listed, "ROCK_TUNNEL_1F is in field.darkMaps.maps")
|
|
|
|
local flashText = game.data.text._FlashLightsAreaText
|
|
check(type(flashText) == "string" and flashText ~= "",
|
|
"_FlashLightsAreaText extracted from the ROM")
|
|
if type(flashText) == "string" then
|
|
U.log("the message reads:", (flashText:gsub("\n", " / ")))
|
|
end
|
|
|
|
local rec = (game.data.transitions and game.data.transitions.white_flash)
|
|
or require("src.render.Transition").STYLES.white_flash
|
|
U.log("white_flash is", tostring(rec and rec.frames), "frames")
|
|
check(rec and rec.frames and rec.frames <= 12,
|
|
"the blink is a handful of frames, so anything longer on screen is "
|
|
.. "work riding it")
|
|
|
|
local vol = game.save.options.sfxVol
|
|
if vol == 0 then
|
|
U.log("sfxVol is 0: the blink and the menu beeps are silent, turn SFX up",
|
|
"in OPTION before judging any of this by ear")
|
|
else
|
|
U.log("sfxVol =", tostring(vol))
|
|
end
|
|
|
|
-- ---- park just inside the Route 10 entrance -----------------------------
|
|
-- pokered data/maps/objects/RockTunnel1F.asm: warp_event 15, 3 is the
|
|
-- Route 10 mouth, so (15, 4) is walkable floor one cell inside it, with the
|
|
-- warp still one step north for the second half of the check.
|
|
local MAP, STAND = "ROCK_TUNNEL_1F", { x = 15, y = 4, facing = "down" }
|
|
local WARP = { x = 15, y = 3 }
|
|
|
|
local function enter(lit)
|
|
game.save.flashLit = lit or nil
|
|
U.teleport(game, MAP, STAND.x, STAND.y, STAND.facing)
|
|
step(12)
|
|
local ow = game.overworld
|
|
-- a map edit or a mod can drop a rock on the entrance floor: fall back to
|
|
-- any walkable neighbour of the warp that is not the warp itself
|
|
if ow and ow.map and not ow.map:isWalkableCell(STAND.x, STAND.y) then
|
|
local sides = { { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 } }
|
|
for _, s in ipairs(sides) do
|
|
local cx, cy = WARP.x + s[1], WARP.y + s[2]
|
|
if ow.map:isWalkableCell(cx, cy) and not ow.map:warpAtCell(cx, cy)
|
|
and not ow:npcAtCell(cx, cy) then
|
|
U.log(("(%d, %d) is blocked, standing on"):format(STAND.x, STAND.y),
|
|
cx, cy)
|
|
STAND.x, STAND.y = cx, cy
|
|
U.teleport(game, MAP, cx, cy, STAND.facing)
|
|
step(12)
|
|
ow = game.overworld
|
|
break
|
|
end
|
|
end
|
|
end
|
|
return ow
|
|
end
|
|
|
|
local ow = enter(nil)
|
|
check(ow ~= nil and ow.dark == true, "the tunnel is dark on arrival")
|
|
check(ow.map:warpAtCell(WARP.x, WARP.y) ~= nil,
|
|
("the Route 10 warp is still at (%d, %d), so the walk back out is "
|
|
.. "reachable"):format(WARP.x, WARP.y))
|
|
local key = ow.map.renderer and ow.map.renderer.gbcAtlasKey
|
|
U.log("atlas key, dark:", tostring(key))
|
|
check(type(key) == "string" and key:find("#dark", 1, true) ~= nil,
|
|
"and it is drawing the dark bake, not a shaded lit one")
|
|
check(U.shot(game, DIR .. "/bug610_1_dark.png"),
|
|
"a frame reached disk, so there is a window to watch")
|
|
|
|
-- ---- run the moment once, watching where the cost lands -----------------
|
|
-- START -> POKeMON -> the mon -> FLASH, looked up by row rather than counted
|
|
-- so a mod that inserts a row cannot silently pick something else
|
|
tap("start")
|
|
step(12)
|
|
local menu = game.stack:top()
|
|
local row
|
|
for i, it in ipairs(menu and menu.items or {}) do
|
|
if tostring(it.label):upper():find("MON", 1, true) then row = i break end
|
|
end
|
|
check(row ~= nil, "the START menu lists POKeMON")
|
|
for _ = 2, (row or 1) do tap("down"); step(2) end
|
|
tap("a")
|
|
step(12)
|
|
local pm = game.stack:top()
|
|
check(getmetatable(pm) == PartyMenu, "POKeMON opens the party menu")
|
|
tap("a")
|
|
step(8)
|
|
local sub
|
|
for i, it in ipairs((pm and pm.subItems) or {}) do
|
|
if it.action == "flash" then sub = i break end
|
|
end
|
|
check(sub ~= nil, "FLASH is listed for this PIKACHU on a dark floor")
|
|
for _ = 2, (sub or 1) do tap("down"); step(2) end
|
|
tap("a")
|
|
step(6)
|
|
check(game.stack:top() ~= nil and game.stack:top().pages ~= nil,
|
|
"FLASH puts its message up")
|
|
check(ow.dark == true, "the tunnel is still dark while that message types")
|
|
|
|
-- drain the text, then watch every frame from the last A to the map coming
|
|
-- back: this is the window the bug filled with a white hang
|
|
resetWorst()
|
|
local blinkStart, blinkEnd, litAtBlink, keyAtBlink
|
|
for _ = 1, 400 do
|
|
local top = game.stack:top()
|
|
if top and top.pages then
|
|
tap("a")
|
|
step(3)
|
|
elseif isBlink(top) then
|
|
if not blinkStart then
|
|
blinkStart = now()
|
|
litAtBlink = (ow.dark == false)
|
|
keyAtBlink = ow.map.renderer and ow.map.renderer.gbcAtlasKey
|
|
end
|
|
step(1)
|
|
else
|
|
if blinkStart and not blinkEnd then blinkEnd = now() end
|
|
break
|
|
end
|
|
end
|
|
step(20)
|
|
|
|
check(blinkStart ~= nil, "the blink ran")
|
|
-- the fix itself, stated as an invariant: OverworldState:setDark drops every
|
|
-- resident map and rebakes this one, and that has to be finished before the
|
|
-- first white frame is drawn (start_sub_menus.asm .flash order)
|
|
check(litAtBlink == true,
|
|
"the cave was already lit on the blink's first frame, not lit by it")
|
|
check(type(keyAtBlink) == "string"
|
|
and keyAtBlink:find("#dark", 1, true) == nil,
|
|
"and the lit atlas was already baked and bound by then")
|
|
if blinkStart and blinkEnd then
|
|
U.log(("the white was on screen for %.2f s"):format(blinkEnd - blinkStart))
|
|
end
|
|
U.log(("the longest single frame in the whole sequence was %.2f s, drawn "
|
|
.. "over %s"):format(worst, worstWhere))
|
|
check(worstWhere ~= "the white blink",
|
|
"the expensive frame was not one of the white ones")
|
|
check(ow.dark == false, "the tunnel is lit now")
|
|
U.shot(game, DIR .. "/bug610_2_lit.png")
|
|
|
|
-- ---- and it stays lit across the Route 10 round trip --------------------
|
|
-- pokered data/maps/objects/Route10.asm: warp_event 8, 17 is the north
|
|
-- tunnel mouth, so (8, 18) is the grass one step below it
|
|
U.teleport(game, "ROUTE_10", 8, 18, "up")
|
|
step(12)
|
|
ow = enter(true)
|
|
check(ow.dark == false, "coming back in from Route 10 the cave is still lit")
|
|
key = ow.map.renderer and ow.map.renderer.gbcAtlasKey
|
|
check(type(key) == "string" and key:find("#dark", 1, true) == nil,
|
|
"on the lit bake, with no second rebake to blink through")
|
|
U.shot(game, DIR .. "/bug610_3_relit.png")
|
|
|
|
U.log(fails == 0 and "all #610 machine checks passed"
|
|
or (fails .. " #610 machine check(s) FAILED, read up"))
|
|
U.log("shots in", DIR)
|
|
|
|
-- ---- hand the pad over, dark, FLASH unused ------------------------------
|
|
giveMon()
|
|
enter(nil)
|
|
|
|
U.log("You are back in the dark tunnel in ADVANCED colours with FLASH unused:")
|
|
U.log("START, POKeMON, A, FLASH. The message should end over the party list, any")
|
|
U.log("hitch should happen with that list still on screen, and then a blink about")
|
|
U.log("as short as STRENGTH's on the same menu hands back a lit cave. A white that")
|
|
U.log("sits noticeably longer than the STRENGTH one means the rebake is riding the")
|
|
U.log("blink again. Then walk up to the ladder, out to Route 10 and back in: lit,")
|
|
U.log("no second blink.")
|
|
|
|
while true do
|
|
coroutine.yield()
|
|
end
|
|
end
|