mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
296 lines
10 KiB
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")
|