Files
gen1recomp/tests/game3_stitchimp_condominiums_test.lua

296 lines
10 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/include/global.fieldmap.h:80
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 Versions = require("src.import.gba.versions")
local MapCatalog = require("src.import.gba.map_catalog")
local NativePack = require("src.import.gba.native_pack")
local ONE_F = "FR_CELADON_CITY_CONDOMINIUMS_1F"
local TWO_F = "FR_CELADON_CITY_CONDOMINIUMS_2F"
local THREE_F = "FR_CELADON_CITY_CONDOMINIUMS_3F"
local ROOF = "FR_CELADON_CITY_CONDOMINIUMS_ROOF"
local HIDEOUT = "FR_ROCKET_HIDEOUT_B2F"
local FLOORS = { ONE_F, TWO_F, THREE_F, ROOF }
local PRET = "../pokefirered"
local LAYOUT_ID = {
[ONE_F] = "LAYOUT_CELADON_CITY_CONDOMINIUMS_1F",
[TWO_F] = "LAYOUT_CELADON_CITY_CONDOMINIUMS_2F",
[THREE_F] = "LAYOUT_CELADON_CITY_CONDOMINIUMS_3F",
[ROOF] = "LAYOUT_CELADON_CITY_CONDOMINIUMS_ROOF",
}
local function slurp(path)
local f = io.open(path, "rb")
if not f then return nil end
local d = f:read("*a")
f:close()
return d
end
local function attributesOf(symbol)
local snake = symbol:gsub("^gTileset_", ""):gsub("(%l)(%u)", "%1_%2")
:gsub("(%a)(%d)", "%1_%2"):lower()
for _, kind in ipairs({ "primary", "secondary" }) do
local blob = slurp(PRET .. "/data/tilesets/" .. kind .. "/" .. snake
.. "/metatile_attributes.bin")
if blob then return blob end
end
return nil
end
-- pokefirered/include/fieldmap.h:8
local NUM_PRIMARY_METATILES = 640
local function pretExpect()
local json = slurp(PRET .. "/data/layouts/layouts.json")
if not json then return nil end
local out = {}
for mapId, layoutId in pairs(LAYOUT_ID) do
local obj
for body in json:gmatch("{(.-)}") do
if body:find('"id"%s*:%s*"' .. layoutId .. '"', 1, false) then obj = body end
end
if not obj then return nil end
local w = tonumber(obj:match('"width"%s*:%s*(%d+)'))
local h = tonumber(obj:match('"height"%s*:%s*(%d+)'))
local primary = attributesOf(obj:match('"primary_tileset"%s*:%s*"([%w_]+)"') or "")
local secondary = attributesOf(obj:match('"secondary_tileset"%s*:%s*"([%w_]+)"') or "")
local blocks = slurp(PRET .. "/" .. (obj:match('"blockdata_filepath"%s*:%s*"([^"]+)"') or ""))
if not (w and h and primary and secondary and blocks) then return nil end
local behavior = {}
for i = 0, w * h - 1 do
local cell = blocks:byte(2 * i + 1) + blocks:byte(2 * i + 2) * 256
local mid = cell % 1024
local attrs, index = primary, mid
if mid >= NUM_PRIMARY_METATILES then
attrs, index = secondary, mid - NUM_PRIMARY_METATILES
end
local b = attrs:byte(4 * index + 1)
behavior[b] = (behavior[b] or 0) + 1
end
out[mapId] = { w = w, h = h, behavior = behavior }
end
return out
end
local EXPECT = pretExpect()
if not EXPECT then
print("[skip] " .. PRET .. " layouts or tileset attributes not present; "
.. "the size and behavior checks against pret are skipped")
end
-- pokefirered/include/constants/metatile_behaviors.h:66-69
local SPIN_LO, SPIN_HI = 0x54, 0x57
print("[test] 1. two tileset structs that share a tiles blob stay two tilesets")
local BASE = 0x09000000
local function fakeRom(structs)
local bytes, words = {}, {}
for _, s in ipairs(structs) do
local o = s.ptr - 0x08000000
bytes[o] = 1
bytes[o + 1] = s.secondary and 1 or 0
words[o + 4] = s.tiles
words[o + 8] = s.palettes
words[o + 12] = s.metatiles
words[o + 16] = 0
words[o + 20] = s.attributes
end
return {
get = function(_, o) return bytes[o] or 0 end,
u32 = function(_, o) return words[o] or 0 end,
ptrOffset = function(_, p)
if p < 0x08000000 or p >= 0x0A000000 then return nil end
return p - 0x08000000
end,
}
end
local PRIMARY = { ptr = BASE + 0x0000, secondary = false,
tiles = BASE + 0x100000, palettes = BASE + 0x140000,
metatiles = BASE + 0x200000, attributes = BASE + 0x204000 }
local SHARED_TILES, SHARED_PALS = BASE + 0x300000, BASE + 0x340000
local SEC_A = { ptr = BASE + 0x0100, secondary = true,
tiles = SHARED_TILES, palettes = SHARED_PALS,
metatiles = BASE + 0x400000, attributes = BASE + 0x404000 }
local SEC_B = { ptr = BASE + 0x0200, secondary = true,
tiles = SHARED_TILES, palettes = SHARED_PALS,
metatiles = BASE + 0x500000, attributes = BASE + 0x504000 }
local SEC_A_AGAIN = { ptr = BASE + 0x0300, secondary = true,
tiles = SHARED_TILES, palettes = SHARED_PALS,
metatiles = SEC_A.metatiles, attributes = SEC_A.attributes }
local rom = fakeRom({ PRIMARY, SEC_A, SEC_B, SEC_A_AGAIN })
local nameA = MapCatalog.tilesetNameForPtr(rom, SEC_A.ptr)
local nameB = MapCatalog.tilesetNameForPtr(rom, SEC_B.ptr)
local nameC = MapCatalog.tilesetNameForPtr(rom, SEC_A_AGAIN.ptr)
check(nameA ~= nil and nameB ~= nil, "both secondary structs resolve to a name")
check(nameA ~= nameB,
"different metatiles + attributes give different names (" ..
tostring(nameA) .. " vs " .. tostring(nameB) .. ")")
eq(nameC, nameA, "the same metatiles + attributes still dedupe to one name")
eq(Versions.TILESETS[nameB] and Versions.TILESETS[nameB].attributes,
SEC_B.attributes - 0x08000000, "the second struct registers its own attributes")
local pairA = MapCatalog.pairForLayout(rom,
{ primaryTilesetPtr = PRIMARY.ptr, secondaryTilesetPtr = SEC_A.ptr })
local pairB = MapCatalog.pairForLayout(rom,
{ primaryTilesetPtr = PRIMARY.ptr, secondaryTilesetPtr = SEC_B.ptr })
check(pairA ~= pairB,
"the two layouts land in different pairs (" .. tostring(pairA) ..
" vs " .. tostring(pairB) .. ")")
print("[test] 2. the census over the real ROM")
local ROM = "../pokefirered/pokefirered.gba"
local romFile = io.open(ROM, "rb")
if not romFile then
print("[skip] " .. ROM .. " not present; ROM section skipped")
else
romFile:close()
local FileIO = require("src.import.gba.file_io")
local imports = FileIO.makeImports(
ROM, "41cb23d8dccc8ebd7c649cd8fbb58eeace6e2fdc", "firered")
local realRom = assert(require("src.import.gba.rom").open(imports, "firered"))
local version = assert(Versions.lookup(realRom.md5))
local MapTree = require("src.import.gba.map_tree")
MapCatalog.rebuildIndex()
local census = assert(MapTree.walk(realRom, version))
local order, byEngine = MapCatalog.allOrder(census)
MapCatalog.registerOrder(realRom, version, order, byEngine)
local hideout = Versions.MAPS[HIDEOUT]
check(hideout ~= nil, HIDEOUT .. " is registered")
local condoPair = nil
for _, id in ipairs(FLOORS) do
local spec = Versions.MAPS[id]
if not spec then
check(false, id .. " is registered")
else
condoPair = condoPair or spec.pair
eq(spec.pair, condoPair, id .. " shares one Condominiums pair")
check(hideout == nil or spec.pair ~= hideout.pair,
id .. " does not borrow " .. HIDEOUT .. "'s pair (" .. tostring(spec.pair) .. ")")
local want = EXPECT and EXPECT[id]
if want then
eq(spec.width .. "x" .. spec.height, want.w .. "x" .. want.h,
id .. " keeps pret's layout size")
end
end
end
local condoSpec = condoPair and Versions.TILESET_PAIRS[condoPair]
local hideoutSpec = hideout and Versions.TILESET_PAIRS[hideout.pair]
local condoSec = condoSpec and Versions.TILESETS[condoSpec.secondary]
local hideoutSec = hideoutSpec and Versions.TILESETS[hideoutSpec.secondary]
if condoSec and hideoutSec then
eq(condoSec.tiles, hideoutSec.tiles,
"both secondaries really do share one tiles blob")
check(condoSec.metatiles ~= hideoutSec.metatiles,
"their metatile blocks differ")
check(condoSec.attributes ~= hideoutSec.attributes,
"their attribute blocks differ")
else
check(false, "both secondary tilesets are registered")
end
imports:_close()
end
print("[test] 3. the baked cache")
local Cache = require("tests.game3_cache")
local root = Cache.root("native/manifest.lua")
if not root then
print("[skip] " .. tostring(Cache.reason))
else
print("[info] FireRed cache at " .. root)
local function readFile(rel)
local f = io.open(root .. "/" .. rel, "rb")
if not f then return nil end
local d = f:read("*a")
f:close()
return d
end
local function loadLua(rel)
local src = readFile(rel)
if not src then return nil end
local chunk = load(src, "@" .. rel, "t", {})
return chunk and chunk() or nil
end
local manifest = loadLua("native/manifest.lua") or {}
local midIndex = loadLua("mid_index.lua") or {}
local function histogram(mapId)
local info = (manifest.layouts or {})[mapId]
local blob = readFile("native/layouts/" .. mapId .. ".mid")
local decoded = blob and NativePack.decodeMidLayout(blob)
if not (info and decoded) then return nil end
local tw = decoded.trueWidth or decoded.width
local th = decoded.trueHeight or decoded.height
local hist, spins = {}, 0
for y = 0, th - 1 do
for x = 0, tw - 1 do
local mid = decoded.cells[y * decoded.width + x + 1].mid
local row = midIndex[info.pair] and midIndex[info.pair][mid]
local b = row and row.behavior or -1
hist[b] = (hist[b] or 0) + 1
if b >= SPIN_LO and b <= SPIN_HI then spins = spins + 1 end
end
end
return hist, spins, tw, th, info.pair
end
local hideoutHist, hideoutSpins, _, _, hideoutPair = histogram(HIDEOUT)
check(hideoutHist ~= nil, HIDEOUT .. " is baked")
check((hideoutSpins or 0) > 0,
HIDEOUT .. " still has its spinners, " .. tostring(hideoutSpins) .. " cells")
for _, id in ipairs(FLOORS) do
local want = EXPECT and EXPECT[id]
local hist, spins, tw, th, pair = histogram(id)
if not hist then
check(false, id .. " is baked")
else
check(pair ~= hideoutPair,
id .. " renders in its own pair (" .. tostring(pair) .. ")")
eq(spins, 0, id .. " has no spin-tile behaviors")
if want then
eq(tw .. "x" .. th, want.w .. "x" .. want.h, id .. " true size is pret's")
local seen, diff = {}, 0
for b in pairs(hist) do seen[b] = true end
for b in pairs(want.behavior) do seen[b] = true end
for b in pairs(seen) do
local got, exp = hist[b] or 0, want.behavior[b] or 0
if got ~= exp then
diff = diff + 1
print(string.format(" behavior 0x%02x: cache %d, pret %d", b, got, exp))
end
end
eq(diff, 0, id .. " behavior histogram matches pret")
end
end
end
end
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")