Files
gen1recomp/tests/drivers/faithful_res_mobile_veil_bug864_test.lua
T

297 lines
12 KiB
Lua

-- Eye check: FAITHFUL RATIO's mobile scale lock keeps the display outside the
-- locked 160x144 viewport black through the pre-battle flash (pokered
-- BattleTransition_FlashScreen_, engine/battle/battle_transitions.asm), the
-- post-battle fade (GBFadeInFromWhite, home/fade.asm) and Oak speech (#864).
-- POKEPORT_DRIVER=tests/drivers/faithful_res_mobile_veil_bug864_test.lua POKEPORT_FORCE_MOBILE=1 POKEPORT_IDENTITY=bug864 POKEPORT_TOUCH=0 POKEPORT_VERSION=red SHOT_DIR=/tmp/shots love .
-- No POKEPORT_SPEED anywhere: the flash and the fade ARE the frames under test.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Renderer = require("src.render.Renderer")
local FaithfulRes = require("src.core.FaithfulRes")
local Pokemon = require("src.pokemon.Pokemon")
local BattleState = require("src.battle.BattleState")
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
-- FaithfulRes.isMobile reads the env per call, so the desktop build only
-- takes the scale-cap branch when the launcher command set it. Without it
-- every check below tests the ordinary letterbox and proves nothing.
if not check("POKEPORT_FORCE_MOBILE=1 is set (the branch under test)",
os.getenv("POKEPORT_FORCE_MOBILE") == "1") then
U.log("Re-run with POKEPORT_FORCE_MOBILE=1; nothing below is meaningful.")
while true do coroutine.yield() end
end
-- A phone-shaped window, so the locked viewport (160x144 at the largest
-- whole multiple, here 3x = 480x432) leaves tall bars above and below --
-- the "dead display" FaithfulRes.lua's contract says must stay black.
-- 480x960 keeps the width an exact 3x so the bars are purely vertical.
if love.window and love.window.setMode then
love.window.setMode(480, 960, { resizable = true,
minwidth = FaithfulRes.MIN_W,
minheight = FaithfulRes.MIN_H })
end
U.wait(3)
-- New Game replaces game.save (and Game:applyOptions re-applies its fresh
-- options, which releases the lock), so re-arm before every shot rather
-- than trusting one application to survive the whole run.
local function lock()
game.save.options = game.save.options or {}
game.save.options.faithfulRes = 1
game.save.options.battleBg = "black"
FaithfulRes.applyOptions(game.save.options)
return FaithfulRes.scaleCap()
end
check("the mobile scale lock engaged (FaithfulRes.scaleCap ~= nil)",
lock() ~= nil)
local opts = game.save.options
if (opts.musicVol or 0) == 0 or (opts.sfxVol or 0) == 0 then
U.log("WARN music/sfx volume is zero; the flash's battle theme will be silent")
end
-- Load a captured PNG back as ImageData; love.image cannot read absolute
-- paths, so go through io.open + newFileData.
local function loadShot(path)
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)
return ok and img or nil
end
local function regionMean(img, x, y, w, h)
local sum, n = 0, 0
local x2 = math.min(x + w, img:getWidth()) - 1
local y2 = math.min(y + h, img:getHeight()) - 1
for yy = math.max(y, 0), y2 do
for xx = math.max(x, 0), x2 do
local r, g, b = img:getPixel(xx, yy)
sum = sum + (r + g + b) / 3
n = n + 1
end
end
return n > 0 and sum / n or 0, n
end
-- The locked viewport in framebuffer pixels: the same uiSize * fitScale
-- centring endFrame uses for ox/oy/vpw/vph, which is the rectangle the
-- veil is clamped to under the lock. Screenshots are framebuffer-sized,
-- so no dpi divide.
local function viewBox(img)
local pw, ph = img:getWidth(), img:getHeight()
local uiw, uih = Renderer:uiSize()
local S = Renderer:fitScale()
local bw, bh = uiw * S, uih * S
return math.floor((pw - bw) / 2), math.floor((ph - bh) / 2), bw, bh, pw, ph
end
-- Mean over every bar strip the window has (top/bottom always here,
-- left/right only if the width is not an exact multiple). Inset by 2px so
-- the viewport's own edge pixels cannot bleed into the bar sample.
local function barMean(img)
local bx, by, bw, bh, pw, ph = viewBox(img)
local sum, n = 0, 0
local function add(x, y, w, h)
if w < 1 or h < 1 then return end
local m, c = regionMean(img, x, y, w, h)
sum, n = sum + m * c, n + c
end
if by >= 8 then
add(0, 0, pw, by - 2)
add(0, by + bh + 2, pw, ph - (by + bh) - 2)
end
if bx >= 8 then
add(0, by, bx - 2, bh)
add(bx + bw + 2, by, bx - 2, bh)
end
return n > 0 and sum / n or -1, n
end
local function innerMean(img)
local bx, by, bw, bh = viewBox(img)
return regionMean(img, bx + math.floor(bw / 4), by + math.floor(bh / 4),
math.floor(bw / 2), math.floor(bh / 2))
end
-- shot + the two-sided assertion every moment shares: bars dead black,
-- viewport interior at least `bright` (the effect visibly inside the frame)
local function shotAndCheck(name, bright)
check("lock still held at " .. name, lock() ~= nil)
local path = DIR .. "/bug864_" .. name .. ".png"
U.shot(game, path)
local img = loadShot(path)
if not check(name .. " shot decoded", img ~= nil) then return nil end
local bars, n = barMean(img)
local inner = innerMean(img)
U.log((" %s: bar mean %.3f over %d px, viewport interior %.3f")
:format(name, bars, n, inner))
check(name .. ": window has bars to sample", n > 0)
check(name .. ": bars stay dead black (#864)", n > 0 and bars < 0.05)
check(name .. ": the effect still lights the viewport", inner > bright)
return img
end
-- ---- (3rd symptom first: it is where a boot starts) New Game -----------
-- OakSpeech sets letterboxWhite; before #864 that painted the WHOLE phone
-- paper white, leaving the locked frame indistinguishable from its bars.
U.wait(5)
U.tap(game, "start") -- skip intro movie
U.wait(10)
U.tap(game, "a") -- title -> menu
U.wait(5)
U.tap(game, "a") -- NEW GAME (POKEPORT_IDENTITY=bug864 has no save)
local oak
for _ = 1, 300 do
for i = #game.stack.states, 1, -1 do
local s = game.stack.states[i]
if s and s.letterboxWhite then oak = s break end
end
if oak then break end
U.tap(game, "a")
U.wait(2)
end
check("Oak speech reached (a letterboxWhite state is on the stack)",
oak ~= nil)
U.wait(40) -- let Oak's pic and a line of text land inside the frame
shotAndCheck("oakspeech", 0.5)
-- mash through the rest of the speech into the overworld; the naming
-- screens and the closing shrink-away beat (~103 unskippable frames) eat
-- most of this, so the headroom is generous on purpose
for _ = 1, 900 do
U.tap(game, "a")
U.wait(2)
if game.overworld and game.stack:top() == game.overworld then break end
end
check("New Game landed in the overworld",
game.overworld ~= nil and game.stack:top() == game.overworld)
-- ---- the pre-battle flash ----------------------------------------------
-- pokered data/maps/objects/Route1.asm puts its youngsters at (5,24) and
-- (15,13) and the sign at (9,27), so the top of the road is empty; the
-- battle is pushed straight in, the cell is only somewhere to stand.
local MAP = "ROUTE_1"
local STAND = { x = 5, y = 6, facing = "down" }
game.save.party = { Pokemon.new(game.data, "BULBASAUR", 12) }
U.teleport(game, MAP, STAND.x, STAND.y, STAND.facing)
U.wait(10)
local ow = game.overworld
check("the overworld is up on " .. MAP, ow ~= nil and ow.map.id == MAP)
if ow and not ow.map:isWalkableCell(STAND.x, STAND.y) then
-- a map edit moved the road: any free neighbour serves, the cell is not
-- itself under test
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(("stand cell (%d, %d) blocked, using (%d, %d)")
:format(STAND.x, STAND.y, cx, cy))
U.teleport(game, MAP, cx, cy, STAND.facing)
U.wait(10)
ow = game.overworld
break
end
end
end
U.shot(game, DIR .. "/bug864_route1_base.png") -- unlit reference frame
lock()
-- wild, weaker (L5 vs the L12 lead), not a dungeon map: the 3-bit select
-- (battle_transitions.asm) lands on %000 doublecircle, one of the two
-- wipes that call BattleTransition_FlashScreen first
local battle = BattleState.newWild(game, "RATTATA", 5)
ow:pushBattle(battle)
local trans = game.stack:top()
check("the transition is a flashing wipe (doublecircle)",
trans ~= nil and trans.def ~= nil and trans.def.flash == true)
-- screenVeil is written during draw and cleared at beginFrame, so at
-- update time it holds the LAST rendered frame's veil; catch the white
-- peak (shade 1, near-full alpha) and shoot the very next frames while
-- the 2-frame palette holds keep it bright
local caught = false
for _ = 1, 400 do
local v = game.renderer and game.renderer.screenVeil
if v and v[1] == 1 and v[2] >= 0.9 then caught = true break end
U.wait(1)
end
check("caught the flash at its white peak", caught)
local flashImg = shotAndCheck("flash", 0.5)
local baseImg = loadShot(DIR .. "/bug864_route1_base.png")
if flashImg and baseImg then
local lit, plain = innerMean(flashImg), innerMean(baseImg)
U.log((" viewport interior %.3f unlit -> %.3f mid-flash")
:format(plain, lit))
check("the flash visibly brightens the viewport over the base frame",
lit > plain + 0.15)
end
-- ---- the post-battle fade in from white --------------------------------
for _ = 1, 600 do
if game.stack:top() == battle then break end
U.wait(1)
end
check("the battle reached the screen", game.stack:top() == battle)
for _ = 1, 200 do
if battle.phase == "menu" then break end
U.tap(game, "a")
U.wait(6)
end
check("the battle reached its FIGHT/PKMN/ITEM/RUN menu",
battle.phase == "menu")
-- run away (down+right lands on RUN from anywhere in the 2x2 grid; the
-- L12 lead outspeeds the L5 wild mon, so the escape always succeeds),
-- then watch for BattleReturn's white veil -- battle over, shade 1,
-- alpha 1 through its hold frames
local sawReturn = false
for _ = 1, 1500 do
local top = game.stack:top()
local v = game.renderer and game.renderer.screenVeil
if top ~= battle and v and v[1] == 1 and v[2] >= 0.9 then
sawReturn = true
break
end
if top == battle then
if battle.phase == "menu" then
U.tap(game, "down")
U.wait(1)
U.tap(game, "right")
U.wait(1)
U.tap(game, "a")
U.wait(2)
else
U.tap(game, "a")
U.wait(3)
end
else
U.wait(1)
end
end
check("caught the post-battle fade in from white", sawReturn)
shotAndCheck("return", 0.8)
-- ---- over to you --------------------------------------------------------
U.log("You are back on Route 1 with the picture locked to a 480x432 frame in")
U.log("the middle of a tall window. Open the three shots in " .. DIR .. ":")
U.log("bug864_oakspeech, bug864_flash and bug864_return should each show a lit")
U.log("frame -- paper, white flash, white fade -- with dead-black bars above")
U.log("and below. Before #864 the white spilled over the whole window and the")
U.log("frame had no edge at all. Walking into grass here replays the flash live.")
while true do
coroutine.yield()
end
end