Files
gen1recomp/tests/game3_void_fill_test.lua
2026-09-23 16:34:15 -04:00

232 lines
8.9 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local VoidFill = require("src.core.game3.void_fill")
local T = { 11, 12, 13, 14 }
local W = 21
local LAYOUTS = {
FR_PALLET_TOWN = {
pair = "pallet_outdoor", borderWidth = 2, borderHeight = 2,
borderMids = { T[1], T[2], T[3], T[4] },
},
FR_CINNABAR_ISLAND = {
pair = "sevii_outdoor", borderWidth = 2, borderHeight = 2,
borderMids = { W, W, W, W },
},
}
local lookups = 0
VoidFill.layoutFor = function(mapId)
lookups = lookups + 1
return LAYOUTS[mapId]
end
VoidFill.invalidate()
local function fill(mode, cx, cy, hasMid)
return VoidFill.fillAt(mode, cx, cy, hasMid, "general")
end
print("[test] 1. mode plumbing")
eq(VoidFill.normalize("trees"), "trees", "normalize trees")
eq(VoidFill.normalize("nope"), "map", "normalize unknown -> map")
eq(VoidFill.normalize(nil), "map", "normalize nil -> map")
eq(VoidFill.cycle("map", 1), "trees", "cycle map -> trees")
eq(VoidFill.cycle("trees", 1), "water", "cycle trees -> water")
eq(VoidFill.cycle("water", 1), "black", "cycle water -> black")
eq(VoidFill.cycle("black", 1), "map", "cycle black wraps to map")
eq(VoidFill.cycle("map", -1), "black", "cycle map backwards -> black")
eq(VoidFill.label("trees"), "TREES", "label trees")
eq(VoidFill.label("black"), "BLACK", "label black")
print("[test] 2. map / black branches")
eq(VoidFill.fillAt("map", 0, 0), nil, "map fill is nil")
eq(VoidFill.fillAt("map", 7, -3), nil, "map fill is nil off-origin")
eq(VoidFill.fillAt("black", 0, 0), false, "black fill is false")
eq(VoidFill.fillAt("black", -5, 9), false, "black fill is false off-origin")
print("[test] 3. tree border is the cart 2x2 (PalletTown/border.bin)")
eq(fill("trees", 0, 0), T[1], "trees (0,0)")
eq(fill("trees", 1, 0), T[2], "trees (1,0)")
eq(fill("trees", 0, 1), T[3], "trees (0,1)")
eq(fill("trees", 1, 1), T[4], "trees (1,1)")
eq(fill("trees", 2, 2), T[1], "trees (2,2) wraps to (0,0)")
eq(fill("trees", 43, 16), T[2], "trees (43,16) odd/even")
print("[test] 4. negative coords wrap non-negative")
eq(fill("trees", -1, -1), T[4], "trees (-1,-1)")
eq(fill("trees", -2, -2), T[1], "trees (-2,-2)")
eq(fill("trees", -1, 0), T[2], "trees (-1,0)")
eq(fill("trees", 0, -1), T[3], "trees (0,-1)")
print("[test] 5. water border is one metatile (CinnabarIsland/border.bin)")
for _, xy in ipairs({ { 0, 0 }, { 1, 0 }, { 0, 1 }, { -3, 7 }, { 12, -9 } }) do
eq(fill("water", xy[1], xy[2]), W,
string.format("water (%d,%d)", xy[1], xy[2]))
end
print("[test] 6. availability gate falls through to the map's own border")
eq(fill("trees", 0, 0, function() return false end), nil,
"trees gated off when the pair has no tree mids")
eq(fill("water", 0, 0, function() return false end), nil,
"water gated off when the pair lacks the water mid")
local partial = { [T[1]] = true, [T[2]] = true, [T[3]] = true }
eq(fill("trees", 1, 1, function(m) return partial[m] == true end), nil,
"no partial tree when one quadrant is missing")
eq(fill("trees", 1, 1, function() return true end), T[4],
"gate passes when every mid is packed")
eq(VoidFill.fillAt("black", 0, 0, function() return false end), false,
"gate does not apply to black")
print("[test] 7. regression: the fill is not a single metatile (#2309)")
local seen, n = {}, 0
for cy = 0, 1 do
for cx = 0, 1 do
local mid = fill("trees", cx, cy)
if not seen[mid] then
seen[mid] = true
n = n + 1
end
end
end
eq(n, 4, "a 2x2 tree window yields 4 distinct mids")
eq(VoidFill.borderFor("trees").w, 2, "tree border width")
eq(VoidFill.borderFor("trees").h, 2, "tree border height")
check(VoidFill.midFor == nil, "modal-scan midFor is gone")
check(VoidFill.TREE_BORDER == nil and VoidFill.WATER_BORDER == nil,
"no literal border tables in the module")
print("[test] 8. primary tileset gate (building pairs never get General mids)")
local all = function() return true end
eq(VoidFill.fillAt("trees", 0, 0, all, "building"), nil,
"trees on a building primary with every mid packed -> nil")
eq(VoidFill.fillAt("water", 0, 0, all, "building"), nil,
"water on a building primary with every mid packed -> nil")
eq(VoidFill.fillAt("trees", 0, 0, all, nil), nil, "unknown primary fails closed")
eq(VoidFill.fillAt("trees", 0, 0, all), nil, "omitted primary fails closed")
eq(VoidFill.fillAt("trees", 0, 0, all, "general"), T[1], "general primary passes")
eq(VoidFill.fillAt("black", 0, 0, all, "building"), false, "black ignores the primary")
eq(VoidFill.primaryFor("player_house"), "building", "primaryFor player_house")
eq(VoidFill.primaryFor("pallet_outdoor"), "general", "primaryFor pallet_outdoor")
eq(VoidFill.primaryFor("general__rom_082d4af4"), "general", "primaryFor auto general pair")
eq(VoidFill.primaryFor("building__rom_082d4bcc"), "building", "primaryFor auto building pair")
eq(VoidFill.primaryFor("no_such_pair"), nil, "primaryFor unknown pair")
eq(VoidFill.primaryFor(nil), nil, "primaryFor nil")
print("[test] 9. borders come from the cached layouts, resolved once")
VoidFill.invalidate()
lookups = 0
for cy = 0, 9 do
for cx = 0, 9 do fill("trees", cx, cy, all) end
end
eq(lookups, 1, "one layout lookup for 100 cells")
eq(VoidFill.borderFromLayout(nil), nil, "no layout -> no border")
eq(VoidFill.borderFromLayout({
pair = "player_house", borderWidth = 1, borderHeight = 1, borderMids = { T[1] },
}), nil, "a building-primary source layout is refused")
eq(VoidFill.borderFromLayout({
pair = "pallet_outdoor", borderWidth = 1, borderHeight = 1,
borderMids = { VoidFill.PRIMARY_MIDS },
}), nil, "a secondary-tileset mid is refused")
LAYOUTS.FR_PALLET_TOWN = nil
VoidFill.invalidate()
lookups = 0
eq(fill("trees", 0, 0, all), nil, "missing source layout falls through")
eq(fill("trees", 1, 0, all), nil, "missing source layout falls through again")
eq(lookups, 1, "the miss is cached until invalidate")
local realLayoutFor = VoidFill.layoutFor
VoidFill.layoutFor = function() return nil, true end
VoidFill.invalidate()
eq(fill("trees", 0, 0, all), nil, "no game yet falls through")
VoidFill.layoutFor = realLayoutFor
LAYOUTS.FR_PALLET_TOWN = {
pair = "pallet_outdoor", borderWidth = 2, borderHeight = 2,
borderMids = { T[1], T[2], T[3], T[4] },
}
eq(fill("trees", 0, 0, all), T[1], "a pending lookup is not cached as a miss")
print("[test] 10. cached Pallet / Cinnabar borders are usable (skipped without cache)")
local root = require("tests.game3_cache").root("native/layouts/FR_PALLET_TOWN.mid", { native = true })
local function readAll(path)
local f = io.open(path, "rb")
if not f then return nil end
local s = f:read("*a")
f:close()
return s
end
local blob = root and readAll(root .. "/native/layouts/FR_PALLET_TOWN.mid")
if not blob then
print("[skip] no current FireRed cache; canonical mids unverified against ROM")
else
local NativePack = require("src.import.gba.native_pack")
local decoded = NativePack.decodeMidLayout(blob)
check(decoded ~= nil, "decoded FR_PALLET_TOWN.mid")
local Versions = require("src.import.gba.versions")
local function pairOf(mapId)
local spec = Versions.MAPS and Versions.MAPS[mapId]
if spec and spec.pair then return spec.pair end
local NATIVE = root .. "/native/manifest.lua"
local chunk = loadfile(NATIVE)
local manifest = chunk and chunk() or {}
local info = manifest.layouts and manifest.layouts[mapId]
return info and info.pair
end
if decoded then
decoded.pair = pairOf("FR_PALLET_TOWN")
local b = VoidFill.borderFromLayout(decoded)
check(b ~= nil, "Pallet border accepted as a General border")
if b then
eq(b.w, 2, "Pallet border width")
eq(b.h, 2, "Pallet border height")
local distinct, saw = 0, {}
for _, mid in ipairs(b.mids) do
if not saw[mid] then saw[mid] = true; distinct = distinct + 1 end
end
eq(distinct, 4, "Pallet border is four distinct metatiles")
end
end
local cin = readAll(root .. "/native/layouts/FR_CINNABAR_ISLAND.mid")
if cin then
local d2 = NativePack.decodeMidLayout(cin)
if d2 then
d2.pair = pairOf("FR_CINNABAR_ISLAND")
local b2 = VoidFill.borderFromLayout(d2)
check(b2 ~= nil, "Cinnabar border accepted as a General border")
if b2 then
for i = 2, #b2.mids do
eq(b2.mids[i], b2.mids[1], "Cinnabar border is uniform [" .. i .. "]")
end
end
end
end
local house = readAll(root .. "/native/layouts/FR_PLAYERS_HOUSE_1F.mid")
if house then
local d3 = NativePack.decodeMidLayout(house)
if d3 then
d3.pair = pairOf("FR_PLAYERS_HOUSE_1F")
eq(VoidFill.primaryFor(d3.pair), "building", "player's house primary is building")
eq(VoidFill.borderFromLayout(d3), nil, "player's house border is refused as a source")
end
end
end
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")