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

203 lines
7.1 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 RegionMapExtract = require("src.import.gba.region_map_extract")
local MultichoiceExtract = require("src.import.gba.multichoice_extract")
print("[test] 1. extractor readiness contract")
check(type(RegionMapExtract.run) == "function", "region_map_extract has a run()")
check(type(RegionMapExtract.ready) == "function", "region_map_extract has a ready()")
check(#RegionMapExtract.FILES == 29, "region_map_extract names 29 baked files")
local function stubCache(present)
return {
read = function(_, rel)
if present[rel] then return string.rep("x", 4096) end
return nil
end,
}
end
local ROOT = "data/generated/gba"
local all = {}
for _, name in ipairs(RegionMapExtract.FILES) do
all[ROOT .. "/region_map/" .. name] = true
end
check(RegionMapExtract.ready(stubCache(all), ROOT) == true,
"ready() is true when every file is baked")
for _, name in ipairs(RegionMapExtract.FILES) do
local missing = {}
for k, v in pairs(all) do missing[k] = v end
missing[ROOT .. "/region_map/" .. name] = nil
check(RegionMapExtract.ready(stubCache(missing), ROOT) == false,
"ready() is false without " .. name)
end
check(MultichoiceExtract.ready(stubCache({}), ROOT) == false,
"multichoice ready() is false on an empty cache")
check(MultichoiceExtract.ready({
read = function() return "return { [0] = { count = 2, labels = { \"YES\", \"NO\" } } }" end,
}, ROOT) == true, "multichoice ready() is true once the list table is cached")
print("[test] 2. baked assets in an imported cache")
local Cache = require("tests.game3_cache")
local root = Cache.root("region_map/extract_status.json")
if not root then
print("[skip] region map assets: " .. 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 function readFile(rel)
local f = io.open(root .. "/" .. rel, "rb")
if not f then return nil end
local data = f:read("*a")
f:close()
return data
end
local function pngSize(data)
if type(data) ~= "string" or #data < 24 or data:sub(1, 8) ~= "\137PNG\r\n\026\n" then
return nil
end
local function be32(at)
local a, b, c, d = data:byte(at, at + 3)
return ((a * 256 + b) * 256 + c) * 256 + d
end
return be32(17), be32(21)
end
local EXPECT = {
["kanto_map.png"] = { 240, 160 },
["sevii123_map.png"] = { 240, 160 },
["sevii45_map.png"] = { 240, 160 },
["sevii67_map.png"] = { 240, 160 },
["frame_normal.png"] = { 240, 160 },
["frame_normal_untinted.png"] = { 240, 160 },
["frame_fly.png"] = { 240, 160 },
["switch_menu_123.png"] = { 240, 160 },
["switch_menu_all.png"] = { 240, 160 },
["switch_button.png"] = { 24, 24 },
["navel_rock_patch.png"] = { 24, 16 },
["birth_island_patch.png"] = { 24, 24 },
["switch_cursor_left.png"] = { 32, 64 },
["switch_cursor_right.png"] = { 32, 64 },
["edge_top_left.png"] = { 32, 64 },
["edge_top_right.png"] = { 32, 64 },
["edge_mid_left.png"] = { 32, 64 },
["edge_mid_right.png"] = { 32, 64 },
["edge_bottom_left.png"] = { 32, 64 },
["edge_bottom_right.png"] = { 32, 64 },
["cursor.png"] = { 16, 32 },
["fly_icon.png"] = { 16, 32 },
["dungeon_icon.png"] = { 8, 8 },
["dungeon_icon_visited.png"] = { 8, 8 },
["player_red.png"] = { 16, 16 },
["player_leaf.png"] = { 16, 16 },
}
for _, name in ipairs(RegionMapExtract.FILES) do
if not name:find("%.png$") then goto continue end
local data = readFile("region_map/" .. name)
local w, h = pngSize(data)
local want = EXPECT[name]
check(w == want[1] and h == want[2], string.format(
"region_map/%s is %sx%s (want %dx%d)", name, tostring(w), tostring(h), want[1], want[2]))
::continue::
end
-- pokefirered/src/region_map.c:939, :959, :2572
local function darken8(u)
local v5 = math.floor(u * 31 / 255 + 0.5)
local d = math.floor(math.floor(math.floor(v5 * 256 / 100) * 95) / 256)
return math.floor(d * 255 / 31 + 0.5)
end
for _, base in ipairs({ "frame_normal" }) do
local tinted = readFile("region_map/" .. base .. ".rgba")
local raw = readFile("region_map/" .. base .. "_untinted.rgba")
check(tinted and raw and #tinted == 240 * 160 * 4 and #raw == #tinted,
base .. " has a same-size untinted twin")
if tinted and raw and #raw == #tinted then
local diff, bad = 0, 0
for i = 1, #raw, 4 do
local same = true
for c = 0, 2 do
if tinted:byte(i + c) ~= raw:byte(i + c) then same = false end
end
if not same then
diff = diff + 1
for c = 0, 2 do
if tinted:byte(i + c) ~= darken8(raw:byte(i + c)) then bad = bad + 1 break end
end
end
if tinted:byte(i + 3) ~= raw:byte(i + 3) then bad = bad + 1 end
end
check(diff > 0, string.format("%s differs from its untinted twin (%d pixels)", base, diff))
check(bad == 0, string.format("every differing %s pixel is the untinted one darkened to 95%% (%d bad)", base, bad))
end
end
-- pokefirered/src/region_map.c:790 sAnim_DungeonIconVisited is frame 1, :795 frame 0
local frame0 = readFile("region_map/dungeon_icon.rgba")
local frame1 = readFile("region_map/dungeon_icon_visited.rgba")
check(frame0 and #frame0 == 8 * 8 * 4, "dungeon_icon.rgba is one 8x8 frame")
check(frame1 and #frame1 == 8 * 8 * 4, "dungeon_icon_visited.rgba is one 8x8 frame")
check(frame0 ~= frame1, "the visited marker is the second frame of the icon sheet")
if frame0 and frame1 then
local function opaque(blob)
local n = 0
for i = 4, #blob, 4 do
if blob:byte(i) == 255 then n = n + 1 end
end
return n
end
check(opaque(frame1) > opaque(frame0), string.format(
"the visited ring paints more of the cell than the unvisited blob (%d > %d)",
opaque(frame1), opaque(frame0)))
end
local mapSections = readFile("region_map/map_sections.lua")
check(type(mapSections) == "string" and #mapSections > 100,
"region_map/map_sections.lua is in the cache")
if mapSections then
local chunk = loadstring(mapSections)
local parsed = chunk and chunk()
check(type(parsed) == "table" and type(parsed.sections) == "table",
"map_sections.lua parses to a section table")
local pallet = parsed and parsed.sections and parsed.sections[88]
check(pallet and pallet.id == "MAPSEC_PALLET_TOWN" and pallet.name == "PALLET TOWN",
"mapsec 88 is PALLET TOWN (" .. tostring(pallet and pallet.name) .. ")")
end
local multichoice = readFile("scripts/multichoice.lua")
check(type(multichoice) == "string" and #multichoice > 100,
"scripts/multichoice.lua is in the cache")
if multichoice then
local chunk = loadstring(multichoice)
local lists = chunk and chunk()
check(type(lists) == "table" and type(lists[0]) == "table",
"multichoice.lua parses to a list table")
local yesNo = lists and lists[0]
check(yesNo and yesNo.labels and yesNo.labels[1] == "YES" and yesNo.labels[2] == "NO",
"list 0 is YES / NO")
end
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")