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

264 lines
13 KiB
Lua

#!/usr/bin/env luajit
-- Location preview screen (sMapPreviewScreenData) extraction & screen logic.
-- Source of truth: pret/pokefirered src/map_preview_screen.c + src/region_map.c.
--
-- The ROM section SKIPs when no FireRed dump is present, mirroring
-- tests/game3_tm_case_berry_pouch_extract_test.lua.
package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
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 BgBake = require("src.import.gba.bg_bake")
local RegionMapTables = require("src.import.gba.region_map_tables")
local MapPreviewExtract = require("src.import.gba.map_preview_extract")
local Extract = require("src.import.gba.extract_island1")
local MapPreviewScreen = require("src.ui.game3.map_preview_screen")
print("=== 1. Versions offsets ===")
check(Versions.MAP_PREVIEW_SCREEN_DATA == 0x43E9E8, "MAP_PREVIEW_SCREEN_DATA is 0x43E9E8")
check(Versions.MAP_PREVIEW_COUNT == 28, "MAP_PREVIEW_COUNT is 28")
check(Versions.MAP_PREVIEW_ENTRY_SIZE == 16, "MAP_PREVIEW_ENTRY_SIZE is 16")
check(Versions.MAP_PREVIEW_TYPE_CAVE == 0 and Versions.MAP_PREVIEW_TYPE_FOREST == 1,
"MPS_TYPE_CAVE/FOREST are 0/1")
check(Versions.MAPSEC_NAMES == 0x3EECFC, "MAPSEC_NAMES is 0x3EECFC")
check(Versions.MAPSEC_NAME_POINTERS == 0x3F1CAC, "MAPSEC_NAME_POINTERS is 0x3F1CAC")
check(Versions.MAPSEC_FIRST == 88 and Versions.MAPSEC_LAST == 196, "mapsec range is 88..196")
check(Versions.MAPSEC_COUNT == 109, "MAPSEC_COUNT is 109")
check(Versions.DUNGEON_INFO == 0x3F1B3C, "DUNGEON_INFO is 0x3F1B3C")
check(Versions.DUNGEON_INFO_COUNT == 19, "DUNGEON_INFO_COUNT is 19")
check(Versions.DUNGEON_INFO_ENTRY_SIZE == 12, "DUNGEON_INFO_ENTRY_SIZE is 12")
print("\n=== 2. Extractor constants ===")
check(MapPreviewExtract.WIDTH == 240 and MapPreviewExtract.HEIGHT == 160, "artwork is 240x160")
check(MapPreviewExtract.PALETTE_BANK_LO == 13 and MapPreviewExtract.PALETTE_BANK_HI == 14,
"palette banks are 13 and 14")
check(MapPreviewExtract.PALETTE_COLORS == 32 and MapPreviewExtract.PALETTE_BANKS == 2,
"0x40 palette bytes split into 2 banks")
check(MapPreviewExtract.TYPE_CAVE == 0 and MapPreviewExtract.TYPE_FOREST == 1,
"extractor cave/forest types match Versions")
check(MapPreviewScreen.FADE_OUT_FRAMES == 47, "fade-out lasts 47 frames")
check(MapPreviewScreen.DURATION_FIRST_VISIT == 120, "first visit holds 120 frames")
check(MapPreviewScreen.DURATION_REVISIT == 40, "revisit holds 40 frames")
print("\n=== 3. BGR555 -> RGB8 ===")
local r, g, b = BgBake.bgr555ToRgb8(0)
check(r == 0 and g == 0 and b == 0, "0x0000 decodes to black")
r, g, b = BgBake.bgr555ToRgb8(0x7FFF)
check(r == 255 and g == 255 and b == 255, "0x7FFF decodes to white")
r, g, b = BgBake.bgr555ToRgb8(0x001F)
check(r == 255 and g == 0 and b == 0, "0x001F decodes to red")
print("\n=== 4. Screen logic (synthetic manifest) ===")
local SYNTH_ROOT = "data/test_gba_map_preview"
local synthCache = {
files = {},
write = function(self, path, bytes) self.files[path] = bytes; return true end,
exists = function(self, path) return self.files[path] ~= nil end,
read = function(self, path) return self.files[path] end,
}
local FOREST_SEC, CAVE_SEC = 176, 100
local ARTWORK = 999
synthCache:write(SYNTH_ROOT .. "/map_preview/manifest.lua", ([[
return {
format_version = 3,
width = 240, height = 160,
type_cave = 0, type_forest = 1,
artwork_count = 1, entry_count = 2,
name_window = { fill = { 247, 247, 255 }, fg = { 0, 0, 0 },
shadow = { 214, 214, 214 }, bg = { 247, 247, 255 } },
entries = {
{ mapsec = %d, name = "BERRY FOREST", type = 1, flagId = 2231, artwork = %d },
{ mapsec = %d, name = "ROCK TUNNEL", type = 0, flagId = 2200, artwork = %d },
},
by_mapsec = { [%d] = 1, [%d] = 2 },
}
]]):format(FOREST_SEC, ARTWORK, CAVE_SEC, ARTWORK, FOREST_SEC, CAVE_SEC))
synthCache:write(SYNTH_ROOT .. "/map_preview/" .. ARTWORK .. ".rgba", string.rep("\0", 240 * 160 * 4))
Extract.CACHE_ROOT = SYNTH_ROOT
MapPreviewScreen.install(synthCache)
check(MapPreviewScreen.ready(), "screen reports ready from a synthetic manifest")
check(MapPreviewScreen.has(FOREST_SEC), "has() matches any type when type is nil")
check(MapPreviewScreen.has(FOREST_SEC, MapPreviewExtract.TYPE_FOREST), "Berry Forest is a forest preview")
check(not MapPreviewScreen.has(FOREST_SEC, MapPreviewExtract.TYPE_CAVE), "Berry Forest is not a cave preview")
check(MapPreviewScreen.has(CAVE_SEC, MapPreviewExtract.TYPE_CAVE), "Rock Tunnel is a cave preview")
check(not MapPreviewScreen.has(4242), "unknown mapsec has no preview")
local nwc = MapPreviewScreen._nameWindowColors
check(type(nwc) == "function", "name window colour reader is exposed")
if type(nwc) == "function" then
local c = nwc(MapPreviewScreen.manifest())
check(c.fill[3] == 1 and c.fg[1] == 0 and math.abs(c.shadow[1] - 214 / 255) < 1e-9 and c.bg[1] == 247 / 255,
"name window colours come from the manifest")
check(not pcall(nwc, { entries = {} }), "a manifest without name_window is an error")
check(not pcall(nwc, { name_window = { fill = { 1, 2, 3 }, fg = {}, shadow = { 1, 2, 3 }, bg = { 1, 2, 3 } } }),
"a malformed name_window colour is an error")
end
-- MapPreview_SetFlag captures the pre-visit state: first visit 120, revisit 40.
MapPreviewScreen.hasVisitedBefore = false
check(MapPreviewScreen.durationFor(FOREST_SEC) == 40, "forest without a prior visit holds 40")
MapPreviewScreen.setVisitedFlag(2231, false)
check(MapPreviewScreen.hasVisitedBefore == true, "setVisitedFlag records a first visit")
check(MapPreviewScreen.durationFor(FOREST_SEC) == 120, "forest first visit holds 120")
MapPreviewScreen.setVisitedFlag(2231, true)
check(MapPreviewScreen.hasVisitedBefore == false, "setVisitedFlag records a revisit")
check(MapPreviewScreen.durationFor(FOREST_SEC) == 40, "forest revisit holds 40")
check(MapPreviewScreen.durationFor(CAVE_SEC) == 120, "cave with an unset flag holds 120")
check(MapPreviewScreen.durationFor(4242) == 0, "unknown mapsec holds 0")
check(MapPreviewScreen.show(CAVE_SEC) == false, "show() refuses a cave preview by default")
check(MapPreviewScreen.show(FOREST_SEC) == true, "show() accepts a forest preview")
check(MapPreviewScreen.isActive(), "screen is active after show()")
check(MapPreviewScreen.mapsec() == FOREST_SEC, "active mapsec is Berry Forest")
do
local hold, fade, seq = 0, 0, {}
for _ = 1, 400 do
if not MapPreviewScreen.isActive() then break end
local wasFade = MapPreviewScreen._state == MapPreviewScreen.STATE.FADE_OUT
MapPreviewScreen.update(1 / 60)
if wasFade then
fade = fade + 1
seq[#seq + 1] = { MapPreviewScreen._eva, MapPreviewScreen._evb }
else
hold = hold + 1
end
end
check(hold == 41, "forest revisit hold lasts duration + 1 frames")
check(fade == 47, "forest dissolve lasts 47 frames")
check(seq[1] and seq[1][1] == 16 and seq[1][2] == 1, "dissolve frame 0 is BLEND(16, 1)")
check(seq[2] and seq[2][1] == 15 and seq[2][2] == 1, "dissolve frame 1 is BLEND(15, 1)")
check(seq[4] and seq[4][1] == 15 and seq[4][2] == 2, "dissolve frame 3 is BLEND(15, 2)")
check(seq[46] and seq[46][1] == 1 and seq[46][2] == 16, "dissolve frame 45 is BLEND(1, 16)")
end
check(MapPreviewScreen.show(FOREST_SEC) == true, "show() restarts the forest preview")
MapPreviewScreen.dismiss()
check(not MapPreviewScreen.isActive(), "dismiss() clears the active screen")
print("\n=== 5. Extraction from FireRed ROM ===")
local romPath = "1636 - Pokemon Fire Red (U)(Squirrels).gba"
local f = io.open(romPath, "rb")
if not f then
print("[SKIP] ROM not found at " .. romPath)
else
local romData = f:read("*all")
f:close()
local rom = {
size = #romData,
get = function(_, i) return romData:byte(i + 1) or 0 end,
}
local previewBase = RegionMapTables.locatePreviewTable(
function(i) return rom:get(i) end, rom.size)
check(previewBase == Versions.MAP_PREVIEW_SCREEN_DATA,
("sMapPreviewScreenData located at 0x%X"):format(previewBase or -1))
local tables = assert(RegionMapTables.load(rom))
check(#tables.previews == 28, "28 sMapPreviewScreenData entries decoded")
check(tables.namesVerified, "sMapsecName_* pointers match sRegionMapSectionIdToName")
check(#tables.dungeonInfo == 19, "19 sDungeonInfo entries decoded")
check(tables.names[126] == "VIRIDIAN FOREST", "mapsec 126 is VIRIDIAN FOREST")
check(tables.names[88] == "PALLET TOWN", "mapsec 88 is PALLET TOWN")
check((tables.names[196] or ""):len() > 0, "mapsec 196 is non-empty")
local forest, cave = 0, 0
local flagOwners = {}
for _, e in ipairs(tables.previews) do
if e.type == Versions.MAP_PREVIEW_TYPE_FOREST then forest = forest + 1 else cave = cave + 1 end
flagOwners[e.flagId] = flagOwners[e.flagId] or {}
table.insert(flagOwners[e.flagId], e.mapsec)
end
check(forest == 8, "8 forest-type previews")
check(cave == 20, "20 cave-type previews")
check(#(flagOwners[2231] or {}) == 2, "MPS_ROCKET_WAREHOUSE and BERRY FOREST share flag 2231")
local plan = assert(MapPreviewExtract.build(rom))
check(#plan.entries == 28, "build() produces 28 entries")
check(plan.artworkCount == 21, "build() deduplicates 28 entries into 21 artworks")
check(plan.nameCount == 109, "build() decodes 109 mapsec names")
check(#plan.dungeonInfo == 19, "build() decodes 19 dungeon entries")
check(plan.dungeonInfo[1].mapsec == 126 and plan.dungeonInfo[1].name == "VIRIDIAN FOREST",
"dungeon entry 1 is Viridian Forest")
check(plan.dungeonInfo[1].desc:find("\n", 1, true) ~= nil,
"dungeon flavour text keeps its ROM line breaks")
local nw = plan.nameWindow
local function rgbIs(c, r, g, b) return c and c[1] == r and c[2] == g and c[3] == b end
check(nw and rgbIs(nw.fill, 247, 247, 255), "name window fill is bank 14 entry 1 (247,247,255)")
check(nw and rgbIs(nw.fg, 0, 0, 0), "name window glyph fg is bank 14 entry 4 (black)")
check(nw and rgbIs(nw.shadow, 214, 214, 214), "name window shadow is bank 14 entry 3 (214,214,214)")
check(nw and rgbIs(nw.bg, 247, 247, 255), "name window glyph bg is bank 14 entry 1, same as the fill")
local files = 0
for name, bytes in pairs(plan.files) do
files = files + 1
check(#bytes == 240 * 160 * 4, "artwork " .. name .. " is 240x160x4")
end
check(files == 21, "build() returns 21 artwork files")
local memoryCache = {
files = {},
write = function(self, path, bytes) self.files[path] = bytes; return true end,
exists = function(self, path) return self.files[path] ~= nil end,
read = function(self, path) return self.files[path] end,
}
local ROM_ROOT = "data/test_gba_map_preview_rom"
local ok, res = MapPreviewExtract.run(rom, memoryCache, { cacheRoot = ROM_ROOT })
check(ok, "MapPreviewExtract.run() succeeded")
check(res and res.artworks == 21 and res.entries == 28, "run() reports 21 artworks / 28 entries")
check(res and res.names == 109 and res.dungeonInfo == 19, "run() reports 109 names / 19 dungeons")
check(res and res.namesVerified == true, "run() reports verified names")
check(memoryCache:exists(ROM_ROOT .. "/map_preview/manifest.lua"), "manifest.lua written")
check(memoryCache:exists(ROM_ROOT .. "/region_map/names.lua"), "region_map/names.lua written")
check(memoryCache:exists(ROM_ROOT .. "/region_map/dungeon_info.lua"), "region_map/dungeon_info.lua written")
check(MapPreviewExtract.ready(memoryCache, ROM_ROOT), "ready() reports true after run()")
local skipOk, skipRes = MapPreviewExtract.run(rom, memoryCache, { cacheRoot = ROM_ROOT })
check(skipOk and skipRes and skipRes.skipped == true, "run() is a no-op when the cache is ready")
local names = MapPreviewExtract.loadNames(memoryCache, ROM_ROOT)
check(names and names[126] == "VIRIDIAN FOREST", "loadNames() round-trips through the cache")
local dinfo = MapPreviewExtract.loadDungeonInfo(memoryCache, ROM_ROOT)
check(dinfo and dinfo[126] and dinfo[126].name == "VIRIDIAN FOREST",
"loadDungeonInfo() round-trips through the cache")
-- Live screen against the real cache.
Extract.CACHE_ROOT = ROM_ROOT
MapPreviewScreen.install(memoryCache)
MapPreviewScreen._manifest = nil
MapPreviewScreen._manifestTried = false
MapPreviewScreen._images = {}
check(MapPreviewScreen.ready(), "screen loads the real manifest")
check(MapPreviewScreen.has(126, MapPreviewExtract.TYPE_FOREST), "Viridian Forest is a forest preview")
check(MapPreviewScreen.durationFor(126) == 40, "Viridian Forest revisit holds 40 frames")
MapPreviewScreen.setVisitedFlag(2212, false)
check(MapPreviewScreen.show(126) == true, "show() starts the Viridian Forest preview")
check(MapPreviewScreen.mapsec() == 126, "active mapsec is 126")
MapPreviewScreen.dismiss()
check(MapPreviewScreen.has(182, MapPreviewExtract.TYPE_FOREST)
and MapPreviewScreen.artworkFor(182) == 126,
"Pattern Bush reuses Viridian Forest's artwork")
end
print("\n==========================================")
if failed == 0 then
print("ALL LOCATION PREVIEW TESTS PASSED!")
else
print(("LOCATION PREVIEW TESTS FAILED WITH %d ERRORS"):format(failed))
os.exit(1)
end