Files
gen1recomp/tests/gen2_border_test.lua
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

140 lines
6.3 KiB
Lua

-- The border block a map is surrounded by: home/map.asm LoadBlockData /
-- LoadMetatiles. `luajit tests/gen2_border_test.lua`; also dofile'd by
-- tests/run_tests.lua. Fixture-driven, with a final section that reads a real
-- Gold cache and SKIPs when there is none.
--
-- Three things fail differently here:
--
-- the SUBSTITUTION block id 0 is not tileset block 0, it is the map
-- header's border block. Reading it literally paints
-- whatever block 0 of the tileset happens to be.
-- the COVERAGE the map canvas is exactly the map's own blocks, so a
-- map smaller than the 20x18 viewport left the rest of
-- the screen on the clear colour. The tiled quad has to
-- cover the view whatever the camera fraction is.
-- the CACHE KEY the bake carries the map's palettes, so it is keyed by
-- daytime / COLOR mode / flicker like the map canvas is,
-- and World:dropMapImages' "<mapId>|" prefix sweep has to
-- still reach it.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 border fill")
local check, eq = S.check, S.eq
local BorderFill = require("src.world.gen2.BorderFill")
local Map = require("src.world.gen2.Map")
-- ---------------------------------------------------------------------------
-- LoadMetatiles' `ld a,[de] / and a / jr nz / ld a,[wMapBorderBlock]`
-- ---------------------------------------------------------------------------
eq(BorderFill.blockFor(0, 42), 42, "block id 0 reads as the border block")
eq(BorderFill.blockFor(nil, 42), 42, "a hole in the block list does too")
eq(BorderFill.blockFor(3, 42), 3, "any other id is itself")
eq(BorderFill.blockFor(0, 0), 0, "border block 0 stays block 0")
eq(BorderFill.blockFor(0, nil), 0, "no border block in the header is 0")
-- Out of bounds already came back as the border block, and now the inside
-- agrees with it: a 1x1 map of block 0 is the border block everywhere.
local hole = Map.new({
id = "HOLE", width = 1, height = 1, borderBlock = 7, blocks = { 0 },
}, { collision = {} })
eq(hole:blockId(-1, 0), 7, "off the left edge is the border block")
eq(BorderFill.blockFor(hole:blockId(0, 0), hole.borderBlock), 7,
"and so is the map's own block 0")
-- ---------------------------------------------------------------------------
-- Cache key
-- ---------------------------------------------------------------------------
local mapKey = "GOLDENROD_DEPT_STORE_ELEVATOR|DAY|gbc|1"
local borderKey = BorderFill.cacheKey(mapKey)
check(borderKey ~= mapKey, "the border bake does not share the map's entry")
check(borderKey:sub(1, #mapKey) == mapKey,
"it keeps the daytime / COLOR / flicker key it was built under")
-- World:dropMapImages sweeps every key beginning "<mapId>|", which is what a
-- changeblock or a CUT leans on. The border bake has to go with it.
local prefix = "GOLDENROD_DEPT_STORE_ELEVATOR|"
check(borderKey:sub(1, #prefix) == prefix,
"dropMapImages' prefix sweep reaches the border bake")
-- ---------------------------------------------------------------------------
-- Coverage: the wrap-tiled quad over the view
-- ---------------------------------------------------------------------------
eq(BorderFill.SIZE, 32, "one block is 32 pixels")
-- 160x144 at scale 4 is the plain fit; the camera fractions are the ones a
-- walk animation actually produces (a step is 8 pixels over 8 frames at 1x).
for _, cam in ipairs({ { 0, 0 }, { 3, 5 }, { 12.5, 0.25 }, { -6.75, -3.5 } }) do
for _, s in ipairs({ 1, 2, 4, 7 }) do
local w, h = 640, 576
local ix, iy, vw, vh, sx, sy =
BorderFill.viewport(cam[1], cam[2], w, h, s)
check(ix == math.floor(cam[1]) and iy == math.floor(cam[2]),
"the source origin is whole texels")
check(sx <= 0 and sy <= 0, "the quad starts at or before the screen edge")
check(sx > -s * BorderFill.SIZE, "and no more than a block before it")
check(sx + vw * s >= w, "the quad covers the width")
check(sy + vh * s >= h, "the quad covers the height")
end
end
-- A degenerate scale must not divide by zero or hand back a quad of nothing.
local _, _, vw0, vh0 = BorderFill.viewport(0, 0, 160, 144, 0)
check(vw0 > 0 and vh0 > 0, "scale 0 falls back to 1 rather than collapsing")
-- ---------------------------------------------------------------------------
-- The real cache: the map the bug was reported on
-- ---------------------------------------------------------------------------
local cache = os.getenv("GOLD_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
end
local mapsPath = cache .. "/data/generated/maps.lua"
local mf = io.open(mapsPath, "r")
if not mf then
check(true, "gold cache absent : fixture checks only (SKIP cache facts)")
S.finish()
return
end
mf:close()
local maps = assert(loadfile(mapsPath))()
local tilesets = assert(loadfile(cache .. "/data/generated/tilesets.lua"))()
-- 20x18 tiles of viewport is 5x4.5 blocks, so anything under 5 wide or 5 tall
-- shows border on at least one axis at every camera position.
local elevator = maps.GOLDENROD_DEPT_STORE_ELEVATOR
check(elevator ~= nil, "the elevator is in the cache")
eq(elevator.width, 2, "GOLDENROD_DEPT_STORE_ELEVATOR is 2 blocks wide")
eq(elevator.height, 2, "and 2 blocks tall")
check(elevator.width < 5 and elevator.height < 5,
"which is smaller than the 20x18 viewport in both axes")
local small = 0
for _, def in pairs(maps) do
if (def.width or 99) < 5 or (def.height or 99) < 5 then small = small + 1 end
end
check(small > 100, "and it is not alone: " .. small .. " maps need the fill")
-- Every map's border block has to resolve to a real block in its own tileset,
-- or the fill bakes nothing and the map goes back to a black surround.
local missing, checked = {}, 0
for id, def in pairs(maps) do
local ts = def.tileset and tilesets[def.tileset]
if ts and ts.blocks then
checked = checked + 1
local block = ts.blocks[BorderFill.blockFor(0, def.borderBlock) + 1]
if type(block) ~= "table" or #block ~= 16 then
missing[#missing + 1] = id
end
end
end
check(checked > 100, "checked " .. checked .. " maps against their tilesets")
eq(#missing, 0, "every border block is a 4x4 metatile in its own tileset")
S.finish()