Files
gen1recomp/tests/game3_doors_table_test.lua
2026-09-21 11:14:17 -04:00

259 lines
11 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 Versions = require("src.import.gba.versions")
local DoorAnimExtract = require("src.import.gba.door_anim_extract")
local TABLE_OFF = Versions.DOOR_GRAPHICS_TABLE
local STRIDE = 12
local function fakeRom(entries)
local bytes = {}
local function put32(off, v)
for i = 0, 3 do
bytes[off + i] = math.floor(v / (256 ^ i)) % 256
end
end
for i, e in ipairs(entries) do
local base = TABLE_OFF + (i - 1) * STRIDE
put32(base, e.mid + e.sound * 0x10000 + e.size * 0x1000000)
put32(base + 4, e.tiles or 0)
put32(base + 8, e.pal or 0)
end
return {
get = function(_, off) return bytes[off] or 0 end,
u16 = function(self, off) return self:get(off) + self:get(off + 1) * 256 end,
u32 = function(self, off)
return self:get(off) + self:get(off + 1) * 256
+ self:get(off + 2) * 65536 + self:get(off + 3) * 16777216
end,
ptrOffset = function(_, ptr)
if ptr < 0x08000000 or ptr >= 0x0A000000 then return nil end
return ptr - 0x08000000
end,
clearCache = function() end,
}
end
local function fakeCache()
local files = {}
return {
files = files,
write = function(_, rel, body) files[rel] = body; return true end,
read = function(_, rel) return files[rel] end,
exists = function(_, rel) return files[rel] ~= nil end,
}
end
print("[test] 1. sDoorGraphics decode reads the sound and size bytes from the ROM")
local ROOT = "data/generated/gba"
local rom = fakeRom({
{ mid = 0x101, sound = 0, size = 0, tiles = 0x08100000, pal = 0x08200000 },
{ mid = 0x102, sound = 1, size = 0, tiles = 0x08100400, pal = 0x08200008 },
{ mid = 0x103, sound = 1, size = 1, tiles = 0x08100800, pal = 0x08200010 },
{ mid = 0x000, sound = 0, size = 0, tiles = 0, pal = 0 },
})
local cache = fakeCache()
local ok = DoorAnimExtract.run(rom, cache, { cacheRoot = ROOT })
check(ok == true, "run() extracted the synthetic table")
local body = cache.files[ROOT .. "/doors/manifest.lua"]
check(type(body) == "string" and #body > 0, "run() wrote doors/manifest.lua")
local manifest = body and assert(loadstring(body))()
check(type(manifest) == "table", "the manifest parses")
if type(manifest) == "table" then
check(type(DoorAnimExtract.MANIFEST_VERSION) == "number"
and manifest.version == DoorAnimExtract.MANIFEST_VERSION,
"the manifest carries the extractor's manifest version")
check(manifest.count == 3, "the walk stopped at the null tiles pointer, count=" .. tostring(manifest.count))
local e = manifest.by_mid or {}
check(e[0x101] ~= nil and e[0x101].sound == "normal" and e[0x101].sound_type == 0,
"sound byte 0 decodes to normal, got " .. tostring(e[0x101] and e[0x101].sound))
check(e[0x102] ~= nil and e[0x102].sound == "sliding" and e[0x102].sound_type == 1,
"sound byte 1 decodes to sliding, got " .. tostring(e[0x102] and e[0x102].sound))
check(e[0x103] ~= nil and e[0x103].sound == "sliding" and e[0x103].sound_type == 1,
"the third entry keeps its sliding sound byte")
check(e[0x101] ~= nil and e[0x101].size == "1x1" and e[0x101].size_type == 0,
"size byte 0 decodes to 1x1")
check(e[0x103] ~= nil and e[0x103].size == "1x2" and e[0x103].size_type == 1,
"size byte 1 decodes to 1x2")
check(type(manifest.entries) == "table" and #manifest.entries == 3
and manifest.entries[1].index == 0 and manifest.entries[3].index == 2,
"the manifest keeps the table order")
end
local smallName = manifest and manifest.by_mid and manifest.by_mid[0x101]
local largeName = manifest and manifest.by_mid and manifest.by_mid[0x103]
local small = smallName and cache.files[ROOT .. "/doors/" .. smallName.tile:lower() .. ".rgba"]
check(type(small) == "string" and #small == 16 * 48 * 4,
"a 1x1 door bakes 16x48 RGBA, got " .. tostring(small and #small))
local large = largeName and cache.files[ROOT .. "/doors/" .. largeName.tile:lower() .. ".rgba"]
check(type(large) == "string" and #large == 16 * 96 * 4,
"a 1x2 door bakes 16x96 RGBA, got " .. tostring(large and #large))
print("[test] 2. ready() repairs a cache written before the sound byte was read")
check(DoorAnimExtract.ready(cache, ROOT) == false,
"ready() is false while the synthetic cache holds fewer entries than the cart")
check(DoorAnimExtract.ready(fakeCache(), ROOT) == false, "ready() is false on an empty cache")
local legacy = fakeCache()
legacy:write(ROOT .. "/doors/manifest.lua", [[
return {
doors = {},
by_mid = {
[347] = { mid = 347, tile = "SlidingDouble", sound = "sliding", size = "1x1", tileset = "primary" },
}
}
]])
check(DoorAnimExtract.ready(legacy, ROOT) == false,
"ready() is false for a manifest with no sound_type")
print("[test] 3. doors.lua holds no metatile table")
local src = assert(io.open("src/core/game3/doors.lua", "r"))
local doorsSrc = src:read("*a")
src:close()
check(not doorsSrc:find("DEFAULT_BY_MID", 1, true),
"doors.lua has no builtin by_mid table")
local literals = 0
for _ in doorsSrc:gmatch("%[0[xX]%x%x%x%]%s*=") do literals = literals + 1 end
check(literals == 0, "doors.lua has no metatile id literals, found " .. literals)
check(not doorsSrc:find("SlidingDouble", 1, true) or doorsSrc:find("tileName == \"SlidingDouble\"", 1, true) ~= nil,
"doors.lua names SlidingDouble only where it branches on the cached tile")
print("[test] 4. the baked cache table")
local Cache = require("tests.game3_cache")
local root = Cache.mount("doors/manifest.lua", { native = true })
if not root then
print("[skip] baked door table: " .. tostring(Cache.reason))
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
print("[info] FireRed cache at " .. root)
local f = assert(io.open(root .. "/doors/manifest.lua", "rb"))
local bakedSrc = f:read("*a")
f:close()
local baked = assert(loadstring(bakedSrc))()
check(type(baked.by_mid) == "table", "the baked manifest has a by_mid table")
local n = 0
for _, entry in pairs(baked.by_mid or {}) do
n = n + 1
if entry.sound ~= "normal" and entry.sound ~= "sliding" then
check(false, "entry " .. tostring(entry.mid) .. " has sound " .. tostring(entry.sound))
end
end
check(n == Versions.DOOR_GRAPHICS_COUNT,
"the baked table holds every sDoorGraphics entry, got " .. n)
-- src/field_door.c:253
local double = (baked.by_mid or {})[0x15B]
check(double ~= nil and double.tile == "SlidingDouble" and double.sound == "sliding"
and double.size == "1x1",
"metatile 0x15B is the sliding double door")
-- src/field_door.c:252
local single = (baked.by_mid or {})[0x062]
check(single ~= nil and single.sound == "sliding", "metatile 0x062 is a sliding door")
-- src/field_door.c:256
local wooden = (baked.by_mid or {})[0x299]
check(wooden ~= nil and wooden.sound == "normal", "metatile 0x299 is a normal door")
if baked.version == DoorAnimExtract.MANIFEST_VERSION then
local typed = true
for _, entry in pairs(baked.by_mid) do
if type(entry.sound_type) ~= "number" then typed = false end
end
check(typed, "every baked entry carries the ROM sound byte")
check(DoorAnimExtract.ready(Cache.cache(), root) == true,
"ready() accepts the baked cache")
else
print("[info] this cache predates the sound byte extraction; the importer repairs it on the next run")
end
print("[test] 5. the engine resolves a door from the cache table")
local GameVersion = require("src.core.GameVersion")
GameVersion.set("firered")
local Doors = require("src.core.game3.doors")
local entry = Doors.getDoorEntryAt("VIRIDIAN_CITY", 36, 10)
check(entry ~= nil and entry.tile == "SlidingDouble",
"the Viridian Gym door cell resolves to SlidingDouble, got " .. tostring(entry and entry.tile))
local snd, kind = Doors.getSoundForWarp("VIRIDIAN_CITY", 36, 10, "MAP_VIRIDIAN_CITY_GYM", true)
check(snd == Doors.SOUND_SLIDING and kind == "SlidingDouble",
"the Viridian Gym door plays SE_SLIDING_DOOR")
local house = Doors.getDoorEntryAt("VIRIDIAN_CITY", 25, 11)
check(house ~= nil and house.sound == "normal",
"the Viridian wooden house door is a normal door, got " .. tostring(house and house.sound))
local hsnd = Doors.getSoundForWarp("VIRIDIAN_CITY", 25, 11, "MAP_VIRIDIAN_CITY_HOUSE", true)
check(hsnd == Doors.SOUND_NORMAL, "the wooden house door plays SE_DOOR")
-- src/field_door.c:510
local dojo = Doors.getDoorEntryAt("FR_SAFFRON_CITY", 40, 12)
check(dojo == nil, "the Saffron Dojo metatile is not in the cart's door table")
local dsnd, dkind = Doors.getSoundForWarp("FR_SAFFRON_CITY", 40, 12, "MAP_SAFFRON_CITY_DOJO", true)
check(dsnd == Doors.SOUND_SLIDING and dkind == nil,
"an unlisted door metatile falls to SE_SLIDING_DOOR with no animation")
print("[test] 6. the metatile behaviour decides, not the tileset name")
local bundle = Cache.bundle("doors/manifest.lua", { native = true })
if not bundle then
print("[skip] behaviour gate: " .. tostring(Cache.reason))
else
local silph = Doors.getDoorEntryAt("FR_SAFFRON_CITY", 33, 30)
check(silph ~= nil and silph.tile == "SilphCo",
"the Silph Co door resolves on a map whose pair is a rom-named tileset, got "
.. tostring(silph and silph.tile))
local ssnd = Doors.getSoundForWarp("FR_SAFFRON_CITY", 33, 30, "MAP_SILPH_CO_1F", true)
check(ssnd == Doors.SOUND_SLIDING, "the Silph Co door plays SE_SLIDING_DOOR")
local dojo2 = Doors.getDoorEntryAt("FR_SAFFRON_CITY", 40, 12)
check(dojo2 == nil, "the Dojo warp cell stays out of the door table")
local floor = Doors.getDoorEntryAt("FR_VIRIDIAN_CITY", 36, 11)
check(floor == nil, "the cell below the gym door is not a door")
end
print("[test] 7. an unlisted door metatile animates nothing and holds nothing up")
love = love or {}
love.graphics = love.graphics or { rectangle = function() end, setColor = function() end }
local realEntryAt = Doors.getDoorEntryAt
local lookups = 0
Doors.getDoorEntryAt = function(...)
lookups = lookups + 1
return realEntryAt(...)
end
local walked = false
local dojoAnim = Doors.open("FR_SAFFRON_CITY", 40, 12,
{ destMap = "MAP_SAFFRON_CITY_DOJO", playSound = false }, function() walked = true end)
check(dojoAnim ~= nil and dojoAnim.tile == nil,
"the Dojo doorway anim carries no door tile, got " .. tostring(dojoAnim and dojoAnim.tile))
lookups = 0
for _ = 1, 30 do Doors.draw(40 * 16 - 100, 12 * 16 - 60) end
-- src/field_door.c:457
check(lookups == 0,
"draw never resolves the door again, got " .. lookups .. " lookups in 30 frames")
-- src/field_fadetransition.c:757
Doors.update(1)
check(walked == true, "the walk starts on the first tick, as a -1 from FieldAnimateDoorOpen does")
Doors.getDoorEntryAt = realEntryAt
Doors.reset()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)