mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 16:21:30 +02:00
324 lines
12 KiB
Lua
324 lines
12 KiB
Lua
-- ..(engine/movie/title.asm ln 321 DrawPlayerCharacter, ball at ln 99)
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local P = require("src.render.PaletteFX")
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local TitleState = require("src.ui.TitleState")
|
|
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
|
|
local REST_OBJ_MIN, REST_OBJ_MAX = 340, 520
|
|
local MENU_OBJ_SLACK = 8
|
|
-- ..(engine/movie/title.asm ln 321): 40x56 at (82,80), plus a margin
|
|
local BAND = { 74, 74, 132, 144 }
|
|
|
|
local fails = 0
|
|
local function check(label, ok, detail)
|
|
U.log(ok and "PASS" or "FAIL", label, detail or "")
|
|
if not ok then fails = fails + 1 end
|
|
return ok
|
|
end
|
|
|
|
local rendered = 0
|
|
local hostDraw = love.draw
|
|
love.draw = function(...)
|
|
local r = hostDraw(...)
|
|
rendered = rendered + 1
|
|
return r
|
|
end
|
|
|
|
local function waitRenderedFrames(frames)
|
|
local target = rendered + (frames or 2)
|
|
for _ = 1, 2400 do
|
|
if rendered >= target then return true end
|
|
U.wait(1)
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function waitFor(pred, limit)
|
|
for _ = 1, limit or 1800 do
|
|
if pred() then return true end
|
|
U.wait(1)
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function redrawCount(frames)
|
|
local m = 0
|
|
for _ = 1, frames or 4 do
|
|
waitRenderedFrames(1)
|
|
local n = #P.uiSpriteRedraws()
|
|
if n > m then m = n end
|
|
end
|
|
return m
|
|
end
|
|
|
|
local objRamp
|
|
local function isObj(R, G, B)
|
|
for _, c in ipairs(objRamp) do
|
|
if math.abs(R - c[1]) <= 12 and math.abs(G - c[2]) <= 12
|
|
and math.abs(B - c[3]) <= 12 then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function letterbox(img)
|
|
local W, H = img:getWidth(), img:getHeight()
|
|
local x1, y1, x2, y2 = W, H, -1, -1
|
|
for y = 0, H - 1 do
|
|
for x = 0, W - 1 do
|
|
local r, g, b = img:getPixel(x, y)
|
|
if r > 0.02 or g > 0.02 or b > 0.02 then
|
|
if x < x1 then x1 = x end
|
|
if x > x2 then x2 = x end
|
|
if y < y1 then y1 = y end
|
|
if y > y2 then y2 = y end
|
|
end
|
|
end
|
|
end
|
|
local bw, bh = x2 - x1 + 1, y2 - y1 + 1
|
|
if x2 >= x1 and math.abs(bw / 160 - bh / 144) < 0.05 then
|
|
return x1, y1, bh / 144
|
|
end
|
|
local s = math.min(W / 160, H / 144)
|
|
return math.floor((W - 160 * s) / 2), math.floor((H - 144 * s) / 2), s
|
|
end
|
|
|
|
local function census(path, bx1, by1, bx2, by2)
|
|
local f = io.open(path, "rb")
|
|
if not f then return nil end
|
|
local bytes = f:read("*a")
|
|
f:close()
|
|
local ok, img = pcall(function()
|
|
return love.image.newImageData(
|
|
love.filesystem.newFileData(bytes, "shot.png"))
|
|
end)
|
|
if not ok or not img then return nil end
|
|
local W, H = img:getWidth(), img:getHeight()
|
|
local ox, oy, scale = letterbox(img)
|
|
local x1 = ox + math.floor((bx1 or 0) * scale)
|
|
local y1 = oy + math.floor((by1 or 0) * scale)
|
|
local x2 = ox + math.ceil((bx2 or 160) * scale) - 1
|
|
local y2 = oy + math.ceil((by2 or 144) * scale) - 1
|
|
local c = { grey = 0, obj = 0, ink = 0, scale = scale, w = W, h = H }
|
|
for y = math.max(0, y1), math.min(H - 1, y2) do
|
|
for x = math.max(0, x1), math.min(W - 1, x2) do
|
|
local r, g, b = img:getPixel(x, y)
|
|
local R = math.floor(r * 255 + 0.5)
|
|
local G = math.floor(g * 255 + 0.5)
|
|
local B = math.floor(b * 255 + 0.5)
|
|
if R == G and G == B and R > 60 and R < 240 then c.grey = c.grey + 1 end
|
|
if R < 30 and G < 30 and B < 30 then c.ink = c.ink + 1 end
|
|
if isObj(R, G, B) then c.obj = c.obj + 1 end
|
|
end
|
|
end
|
|
local d = scale * scale
|
|
c.rawGrey, c.rawObj, c.rawInk = c.grey, c.obj, c.ink
|
|
c.grey = math.floor(c.grey / d + 0.5)
|
|
c.obj = math.floor(c.obj / d + 0.5)
|
|
c.ink = math.floor(c.ink / d + 0.5)
|
|
return c
|
|
end
|
|
|
|
local function report(tag, c)
|
|
U.log(string.format(
|
|
"%s: obj=%d grey=%d ink=%d (raw obj=%d grey=%d at %dx%d, scale %.2f)",
|
|
tag, c.obj, c.grey, c.ink, c.rawObj, c.rawGrey, c.w, c.h, c.scale))
|
|
end
|
|
|
|
local version = GameVersion.get()
|
|
|
|
U.wait(5)
|
|
U.tap(game, "start")
|
|
local title
|
|
check("reached the title screen", waitFor(function()
|
|
title = game.stack:top()
|
|
return getmetatable(title) == TitleState
|
|
end, 300))
|
|
if getmetatable(title) ~= TitleState then
|
|
U.log("BUG #1214 DRIVER ABORTED: never reached TitleState")
|
|
love.event.quit(1)
|
|
return
|
|
end
|
|
|
|
game.save.options.colors = "ogred"
|
|
P.applyOptions(game.save.options)
|
|
check("COLORS is ogred", P.mode == "ogred", P.mode)
|
|
check("usesSpriteObp() true, so the OBJ ramp is baked",
|
|
P.usesSpriteObp() == true)
|
|
objRamp = { P.ogObj()[2], P.ogObj()[3] }
|
|
U.log("version:", tostring(version), "boot-ROM OBJ ramp:",
|
|
string.format("(%d,%d,%d)/(%d,%d,%d)",
|
|
objRamp[1][1], objRamp[1][2], objRamp[1][3],
|
|
objRamp[2][1], objRamp[2][2], objRamp[2][3]))
|
|
|
|
check("title reached the loop phase",
|
|
waitFor(function() return title.phase == "loop" end))
|
|
check("mon cycle reached the hold beat",
|
|
waitFor(function() return title.scrollPhase == "hold" end))
|
|
waitRenderedFrames(3)
|
|
|
|
local restObj
|
|
do
|
|
check("nothing is occluding the title", game.stack:top() == title
|
|
and title.titleUiBox == nil)
|
|
check("at rest the four player draws are recorded for replay",
|
|
redrawCount(6) == 4, "#" .. #P.uiSpriteRedraws())
|
|
local shot = SHOT_DIR .. "/bug1214_1_ogred_rest.png"
|
|
if U.shot(game, shot) then
|
|
local c = census(shot)
|
|
local band = census(shot, BAND[1], BAND[2], BAND[3], BAND[4])
|
|
report("REST screen", c)
|
|
report("REST player band", band)
|
|
restObj = c.obj
|
|
check("#1214 REST: player wears the boot-ROM OBJ ramp (expect ~427"
|
|
.. " canvas px, 12141 raw on a 1024x768 window)",
|
|
c.obj >= REST_OBJ_MIN and c.obj <= REST_OBJ_MAX, c.obj)
|
|
check("#1214 REST: no DMG grey in the player band (r==g==b, 60<r<240)",
|
|
band.grey == 0, band.grey)
|
|
end
|
|
end
|
|
|
|
do
|
|
check("caught the mon sliding through the player bbox",
|
|
waitFor(function()
|
|
return title.scrollPhase == "in" and title.monOffset
|
|
and title.monOffset >= 10 and title.monOffset <= 60
|
|
end))
|
|
local before = title.monOffset
|
|
local shot = SHOT_DIR .. "/bug1214_2_ogred_midscroll.png"
|
|
if U.shot(game, shot) then
|
|
local c = census(shot)
|
|
local band = census(shot, BAND[1], BAND[2], BAND[3], BAND[4])
|
|
U.log("mid-scroll monOffset:", tostring(before), "->",
|
|
tostring(title.monOffset))
|
|
report("MIDSCROLL screen", c)
|
|
report("MIDSCROLL player band", band)
|
|
check("#1214 MIDSCROLL: no grey leaking through the transparent"
|
|
.. " player sprite (was ~6135 raw px before the replay)",
|
|
band.grey == 0, band.grey)
|
|
check("#1214 MIDSCROLL: player still wears the OBJ ramp",
|
|
c.obj >= REST_OBJ_MIN and c.obj <= REST_OBJ_MAX, c.obj)
|
|
end
|
|
end
|
|
|
|
local saveName = require("src.core.SaveData").saveFilename(version)
|
|
do
|
|
check("mon cycle back on the hold beat",
|
|
waitFor(function() return title.scrollPhase == "hold" end))
|
|
waitRenderedFrames(3)
|
|
pcall(function() love.filesystem.write(saveName, "return {}") end)
|
|
title:openMenu()
|
|
waitRenderedFrames(3)
|
|
local menu = game.stack:top()
|
|
local box = menu and menu.titleUiBox
|
|
U.log("menu titleUiBox:", box and table.concat(box, ",") or "nil")
|
|
check("main menu lists CONTINUE, so the box bottom is y 80",
|
|
box ~= nil and box[4] == 9,
|
|
box and tostring(box[4]) or "nil")
|
|
check("#1214 MENU: a box ending at y 80 does not suppress the player,"
|
|
.. " who starts at y 80",
|
|
redrawCount(6) == 4, "#" .. #P.uiSpriteRedraws())
|
|
local shot = SHOT_DIR .. "/bug1214_3_ogred_menu.png"
|
|
if U.shot(game, shot) then
|
|
local c = census(shot)
|
|
local band = census(shot, BAND[1], BAND[2], BAND[3], BAND[4])
|
|
report("MENU screen", c)
|
|
report("MENU player band", band)
|
|
check("#1214 MENU: OBJ ramp count unchanged from rest",
|
|
restObj ~= nil and math.abs(c.obj - restObj) <= MENU_OBJ_SLACK,
|
|
string.format("%d vs rest %d", c.obj, restObj or -1))
|
|
check("#1214 MENU: no DMG grey in the player band",
|
|
band.grey == 0, band.grey)
|
|
end
|
|
game.stack:pop()
|
|
waitRenderedFrames(2)
|
|
end
|
|
|
|
do
|
|
local ContinueInfo
|
|
for i = 1, 40 do
|
|
local name, val = debug.getupvalue(TitleState.openMenu, i)
|
|
if not name then break end
|
|
if name == "ContinueInfo" then ContinueInfo = val break end
|
|
end
|
|
check("ContinueInfo class reachable", ContinueInfo ~= nil)
|
|
local info
|
|
if ContinueInfo then
|
|
local ok, st = pcall(ContinueInfo.new, title,
|
|
{ player = { name = "RED" }, playTime = 0, pokedex = { owned = {} } })
|
|
check("ContinueInfo.new", ok, not ok and tostring(st) or "")
|
|
if ok then info = st end
|
|
end
|
|
if info then
|
|
check("CONTINUE window box is (4,7)-(19,16)",
|
|
info.titleUiBox and info.titleUiBox[1] == 4
|
|
and info.titleUiBox[2] == 7 and info.titleUiBox[3] == 19
|
|
and info.titleUiBox[4] == 16)
|
|
game.stack:push(info)
|
|
waitRenderedFrames(3)
|
|
check("#1214 CONTINUE: every player draw overlapping the window is"
|
|
.. " suppressed", redrawCount(6) == 0,
|
|
"#" .. #P.uiSpriteRedraws())
|
|
local shot = SHOT_DIR .. "/bug1214_4_ogred_continue.png"
|
|
if U.shot(game, shot) then
|
|
local c = census(shot, 33, 57, 159, 135)
|
|
report("CONTINUE box interior", c)
|
|
check("#1214 CONTINUE: no OBJ ramp bleeding over the window",
|
|
c.obj == 0, c.obj)
|
|
local row = census(shot, 33, 103, 159, 113)
|
|
report("CONTINUE POKeDEX row", row)
|
|
check("#1214 CONTINUE: the POKeDEX row is legible ink, not sprite",
|
|
row.ink > 30 and row.obj == 0,
|
|
string.format("ink=%d obj=%d", row.ink, row.obj))
|
|
end
|
|
game.stack:pop()
|
|
waitRenderedFrames(2)
|
|
end
|
|
end
|
|
pcall(function() love.filesystem.remove(saveName) end)
|
|
|
|
do
|
|
for _, mode in ipairs({ "gbc", "og", "og_inv", "gbc_inv", "classic" }) do
|
|
P.applyOptions({ colors = mode })
|
|
waitRenderedFrames(2)
|
|
check(mode .. " records no UI sprite redraws", redrawCount(4) == 0)
|
|
end
|
|
P.applyOptions({ colors = "gbc" })
|
|
check("mon cycle back on the hold beat under gbc",
|
|
waitFor(function() return title.scrollPhase == "hold" end))
|
|
waitRenderedFrames(3)
|
|
local shot = SHOT_DIR .. "/bug1214_5_gbc.png"
|
|
if U.shot(game, shot) then
|
|
local c = census(shot)
|
|
report("GBC", c)
|
|
check("#1214 GBC: the SGB title never wears the boot-ROM OBJ ramp",
|
|
c.obj == 0, c.obj)
|
|
end
|
|
end
|
|
|
|
game.save.options.colors = "ogred"
|
|
P.applyOptions(game.save.options)
|
|
waitRenderedFrames(3)
|
|
|
|
if fails > 0 then
|
|
U.log("################################################################")
|
|
U.log("#1214 FAILED: " .. fails .. " check(s) above, shots in " .. SHOT_DIR)
|
|
U.log("################################################################")
|
|
else
|
|
U.log("#1214 PASS: all checks, shots in " .. SHOT_DIR)
|
|
end
|
|
U.log("On screen now: the OG RED title in COLORS = OG. The player and the")
|
|
U.log("pokeball in his hand must be the boot ROM's OBJ ramp (green on Red,")
|
|
U.log("pink on Blue) while the logo, ribbon, cycling mon and copyright line")
|
|
U.log("stay on the BG ramp. Before #1214 he was BG dark red, and the first")
|
|
U.log("attempt at the fix left grey rectangles around him as mons slid past.")
|
|
U.log("START opens the menu, whose box ends one pixel above his head.")
|
|
|
|
while true do
|
|
coroutine.yield()
|
|
end
|
|
end
|