Files
gen1recomp/tests/game3_import_assets_test.lua
thibautbus 3252a3f2c9 Draw Japanese text in FireRed with the cart's own Japanese fonts
game3 prints every string with the US cart's Latin font, and FrlgFont has
no glyph for a kana, so a Japanese translation of FireRed would print
blank. The US cart still carries its Japanese fonts, which its text
printer draws for a string in Japanese mode (pokefirered/src/text.c:141,
:227): pret builds them into the US ROM, with the normal font's width
table.

The font extractor now bakes both, the normal font from its eight-to-a-row
layout (DecompressGlyph_Normal, text.c:1452) with its widths and the small
one sixteen to a row (DecompressGlyph_Small, text.c:1372), as
chrome/fonts/japanese_{normal,small}_{fg,shadow}.rgba and
japanese_widths.lua. LeafGreen's addresses follow the same -0x24 shift as
the rest of text.o. Cache version 120 re-imports existing caches.

FrlgFont numbers a Japanese character by its byte in the Japanese block of
pokefirered/charmap.txt (kana 01-A0, the full-width space, !?。ー), plus
the symbols the font keeps at the Latin block's codes in their full-width
forms (digits, letters, 『』「」, 円, /, :), offset past the Latin
glyphs. A character with no Latin glyph draws from the Japanese sheet, at
the width the cart gives it (the table for the normal font, 8px for the
small one). Latin text is drawn as before.
2026-09-23 18:07:32 +02:00

392 lines
14 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 function finish()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
local PokemonExtract = require("src.import.gba.pokemon_extract")
local TextChrome = require("src.import.gba.text_chrome_extract")
local Seagallop = require("src.import.gba.seagallop_extract")
local BattleChrome = require("src.import.gba.battle_chrome_extract")
local CacheContract = require("src.import.CacheContract")
local Versions = require("src.import.gba.versions")
local function fakeRom(str)
return {
size = #str,
get = function(_, off) return str:byte(off + 1) or 0 end,
readBytes = function(_, off, len)
local out = {}
for i = 0, len - 1 do out[i + 1] = str:byte(off + i + 1) or 0 end
return out
end,
}
end
print("[test] 1. safariZoneFleeRate rides the base-stats row")
-- include/pokemon.h:233
check(Versions.SPECIES_INFO_SIZE >= 0x19,
"the SpeciesInfo stride covers offset 0x18 (" .. tostring(Versions.SPECIES_INFO_SIZE) .. ")")
check(PokemonExtract.FORMAT_VERSION >= 6,
"the pokemon pack format version is bumped for the flee rate ("
.. tostring(PokemonExtract.FORMAT_VERSION) .. ")")
print("[test] 2. the braille font decodes at the pret addressing")
-- src/braille_text.c:15
check(TextChrome.BRAILLE_GFX == 0x46FB0C, "sBrailleGlyphs is at 0x46FB0C")
check(TextChrome.BRAILLE_GLYPHS == 64, "the braille font has 64 glyphs")
do
-- src/braille_text.c:333
local probes = {
{ gid = 0, part = 0 },
{ gid = 1, part = 0 },
{ gid = 7, part = 0 },
{ gid = 8, part = 0 },
{ gid = 63, part = 0 },
{ gid = 9, part = 16 },
{ gid = 9, part = 256 },
{ gid = 9, part = 272 },
}
for _, probe in ipairs(probes) do
local off = 512 * math.floor(probe.gid / 8) + 32 * (probe.gid % 8) + probe.part
local bytes = {}
for i = 1, 4096 do bytes[i] = "\0" end
for i = 1, 16 do bytes[off + i] = string.char(0x55) end
local rom = fakeRom(("\0"):rep(TextChrome.BRAILLE_GFX) .. table.concat(bytes))
local sheet = TextChrome.extractBraille(rom)
check(sheet.width == 256 and sheet.height == 64,
"the braille sheet is 256x64 (" .. sheet.width .. "x" .. sheet.height .. ")")
local ox = (probe.gid % 16) * 16 + ((probe.part == 16 or probe.part == 272) and 8 or 0)
local oy = math.floor(probe.gid / 16) * 16 + (probe.part >= 256 and 8 or 0)
local lit, total = 0, 0
for y = 0, 63 do
for x = 0, 255 do
if sheet.fgRgba:byte((y * 256 + x) * 4 + 4) == 255 then
total = total + 1
if x >= ox and x < ox + 8 and y >= oy and y < oy + 8 then lit = lit + 1 end
end
end
end
check(lit == 64 and total == 64,
string.format("glyph %d quarter +%d lands at (%d,%d) alone (%d/%d)",
probe.gid, probe.part, ox, oy, lit, total))
end
end
print("[test] 3. the seagallop blobs are the pret INCBIN run")
-- src/seagallop.c:41
local EXPECT_BLOBS = {
{ "water.4bpp", 0x468C98, 1312 },
{ "water.gbapal", 0x4691B8, 32 },
{ "wb_tilemap.bin", 0x4691D8, 2048 },
{ "eb_tilemap.bin", 0x4699D8, 2048 },
{ "ferry.4bpp", 0x46A1D8, 1280 },
{ "ferry_wake.gbapal", 0x46A6D8, 32 },
{ "wake.4bpp", 0x46A6F8, 2048 },
}
check(#Seagallop.BLOBS == #EXPECT_BLOBS,
"seven blobs are declared (" .. #Seagallop.BLOBS .. ")")
for i, want in ipairs(EXPECT_BLOBS) do
local got = Seagallop.BLOBS[i]
check(got ~= nil and got.rel == want[1] and got.offset == want[2] and got.size == want[3],
string.format("blob %d is %s at 0x%X, %d bytes", i, want[1], want[2], want[3]))
end
do
local prev = nil
local contiguous = true
for _, blob in ipairs(Seagallop.BLOBS) do
if prev and prev ~= blob.offset then contiguous = false end
prev = blob.offset + blob.size
end
check(contiguous, "the blobs are one contiguous run inside src/seagallop.o(.rodata)")
end
check(Seagallop.MAP_W == 32 and Seagallop.MAP_H == 32,
"the water tilemaps are 32x32 entries")
local seagallopRomBytes
do
local romLen = 0x46B000
local parts = {}
for i = 1, romLen do parts[i] = "\0" end
for i, blob in ipairs(Seagallop.BLOBS) do
for j = 1, blob.size do parts[blob.offset + j] = string.char(i) end
end
seagallopRomBytes = table.concat(parts)
local blobs = Seagallop.readBlobs(fakeRom(seagallopRomBytes))
for i, blob in ipairs(Seagallop.BLOBS) do
local data = blobs[blob.key]
check(type(data) == "string" and #data == blob.size
and data == string.char(i):rep(blob.size),
blob.rel .. " is read from its own offset")
end
end
do
local written = {}
local refusing = {
write = function(_, rel, data)
if rel:find("manifest%.lua$") then return false, "read-only cache" end
written[rel] = #data
return true
end,
}
local ok, err = pcall(Seagallop.run, fakeRom(seagallopRomBytes), refusing,
{ cacheRoot = "data/generated/gba" })
check(ok == false and tostring(err):find("could not write") ~= nil,
"a refused seagallop write aborts the bake instead of publishing a half cache")
check(written["data/generated/gba/seagallop/water.4bpp"] == Seagallop.BLOBS[1].size,
"the blobs before the refused write did reach the cache")
end
print("[test] 4. the 4bpp seagallop art decodes through the real baker")
do
local t0 = string.char(0x11):rep(32)
local t1 = (string.char(0x22, 0x22, 0x00, 0x00)):rep(8)
local pal = string.char(0, 0) .. string.char(0x1F, 0x00) .. string.char(0xE0, 0x03)
.. string.char(0, 0):rep(13)
local rgba, w, h, tiles = Seagallop.bakeSheet(t0 .. t1, pal, 2)
check(w == 16 and h == 8 and tiles == 2, "two tiles at two columns bake 16x8")
local function px(x, y)
local o = (y * w + x) * 4
return rgba:byte(o + 1), rgba:byte(o + 2), rgba:byte(o + 3), rgba:byte(o + 4)
end
local r, g, b, a = px(0, 0)
check(r == 255 and g == 0 and b == 0 and a == 255, "colour 1 is the first palette entry")
r, g, b, a = px(8, 0)
check(r == 0 and g == 255 and b == 0 and a == 255, "colour 2 is the second palette entry")
r, g, b, a = px(12, 0)
check(a == 0, "colour 0 stays transparent in a sprite sheet")
local map = {}
for i = 1, 4 do map[i] = "\0" end
map[1] = string.char(0x01)
map[2] = string.char(0x04)
map[3] = string.char(0x01)
map[4] = string.char(0x00)
local bg, bw, bh = Seagallop.bakeTilemap(t0 .. t1, pal, table.concat(map), 2, 1)
check(bw == 16 and bh == 8, "a 2x1 tilemap bakes 16x8")
local function bpx(x, y)
local o = (y * bw + x) * 4
return bg:byte(o + 1), bg:byte(o + 2), bg:byte(o + 3), bg:byte(o + 4)
end
r, g, b, a = bpx(7, 0)
check(r == 0 and g == 255 and b == 0 and a == 255,
"entry 0 draws tile 1 hflipped, its coloured half on the right")
r, g, b, a = bpx(0, 0)
check(a == 255 and r == 0 and g == 0 and b == 0,
"the hflip moved colour 0 to the left edge")
r, g, b, a = bpx(8, 0)
check(r == 0 and g == 255 and b == 0 and a == 255, "entry 1 draws tile 1 unflipped")
r, g, b, a = bpx(15, 0)
check(a == 255, "colour 0 is opaque in a background layer")
end
print("[test] 5. the contract requires the new keys")
local required = {}
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.firered) do
required[path] = true
end
local NEW_KEYS = {
"data/generated/gba/chrome/fonts/japanese_normal_fg.rgba",
"data/generated/gba/chrome/fonts/japanese_small_fg.rgba",
"data/generated/gba/chrome/fonts/japanese_widths.lua",
"data/generated/gba/chrome/fonts/braille_fg.rgba",
"data/generated/gba/chrome/fonts/braille_shadow.rgba",
"data/generated/gba/chrome/fonts/braille.lua",
"data/generated/gba/seagallop/manifest.lua",
"data/generated/gba/seagallop/water.4bpp",
"data/generated/gba/seagallop/ferry.4bpp",
"data/generated/gba/seagallop/wake.4bpp",
"data/generated/gba/seagallop/wb_tilemap.bin",
"data/generated/gba/seagallop/eb_tilemap.bin",
"data/generated/gba/seagallop/wb.rgba",
"data/generated/gba/seagallop/eb.rgba",
"data/generated/gba/pokemon/battle/terrain_grass.rgba",
"data/generated/gba/pokemon/battle/terrain_cave.rgba",
"data/generated/gba/pokemon/battle/terrain_water.rgba",
"data/generated/gba/pokemon/battle/terrain_champion.rgba",
"data/generated/gba/pokemon/battle/terrain_bg_cave.rgba",
}
for _, path in ipairs(NEW_KEYS) do
check(required[path] == true, "the firered contract requires " .. path)
local fs = {
getInfo = function(candidate) return candidate ~= path and { type = "file" } or nil end,
exists = function(candidate) return candidate ~= path end,
}
local complete, missing = CacheContract.allRequiredFilesExist("firered", fs)
check(complete == false and missing == path,
"a cache without " .. path .. " is incomplete (" .. tostring(missing) .. ")")
end
print("[test] 6. every sBattleTerrainTable id is a contract-visible key")
do
local keys = {}
for id = 0, 19 do keys[id] = BattleChrome.TERRAIN_KEYS[id] end
check(keys[7] == "cave" and keys[4] == "water" and keys[19] == "champion",
"the terrain key list still names cave, water and champion")
end
print("[test] 7. a built cache carries the baked assets")
local Cache = require("tests.game3_cache")
local root = Cache.root("pokemon/manifest.lua")
if not root then
print("[skip] baked import assets: " .. tostring(Cache.reason))
finish()
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 pokeManifest = readFile("pokemon/manifest.lua")
local pm = pokeManifest and loadstring(pokeManifest)
pm = pm and pm()
if type(pm) ~= "table" or (tonumber(pm.format) or 0) < 6 then
print("[skip] baked import assets: pokemon manifest is format "
.. tostring(pm and pm.format) .. ", these assets need 6")
finish()
end
do
local metaSrc = readFile("pokemon/meta.lua")
local meta = metaSrc and loadstring(metaSrc)
meta = meta and meta()
check(type(meta) == "table", "pokemon/meta.lua loads")
if type(meta) == "table" then
local seen, missing, outOfRange, nonZero = 0, 0, 0, 0
local values = {}
for _, row in pairs(meta) do
seen = seen + 1
local v = row.safariZoneFleeRate
if v == nil then
missing = missing + 1
elseif type(v) ~= "number" or v < 0 or v > 255 or v % 1 ~= 0 then
outOfRange = outOfRange + 1
elseif v > 0 then
nonZero = nonZero + 1
values[v] = (values[v] or 0) + 1
end
end
check(seen >= (Versions.NUM_SPECIES or 412) - 1,
"every species has a meta row (" .. seen .. ")")
check(missing == 0, "every meta row carries safariZoneFleeRate (" .. missing .. " missing)")
check(outOfRange == 0, "every flee rate is a byte in 0..255 (" .. outOfRange .. " bad)")
-- src/data/pokemon/species_info.h:30
local EXPECT_COUNTS = { [25] = 1, [50] = 9, [75] = 7, [100] = 1, [125] = 6 }
local wrong = {}
for v, n in pairs(values) do
if EXPECT_COUNTS[v] ~= n then
wrong[#wrong + 1] = string.format("%d x%d", v, n)
end
end
for v, n in pairs(EXPECT_COUNTS) do
if values[v] == nil then wrong[#wrong + 1] = string.format("%d missing (want %d)", v, n) end
end
check(#wrong == 0,
"the nonzero flee rates are the pret histogram (" .. table.concat(wrong, ", ") .. ")")
check(nonZero == 24,
"24 species carry a Safari Zone flee rate (" .. nonZero .. ")")
end
end
do
local fg = readFile("chrome/fonts/braille_fg.rgba")
local sh = readFile("chrome/fonts/braille_shadow.rgba")
local metaSrc = readFile("chrome/fonts/braille.lua")
local meta = metaSrc and loadstring(metaSrc)
meta = meta and meta()
check(fg ~= nil and #fg == 256 * 64 * 4,
"braille_fg.rgba is 256x64 (" .. tostring(fg and #fg) .. ")")
check(sh ~= nil and #sh == 256 * 64 * 4,
"braille_shadow.rgba is 256x64 (" .. tostring(sh and #sh) .. ")")
check(type(meta) == "table" and meta.glyphCount == 64 and meta.glyphW == 16
and meta.glyphH == 16, "braille.lua declares 64 glyphs of 16x16")
if fg then
local ink = 0
for i = 4, #fg, 4 do
if fg:byte(i) == 255 then ink = ink + 1 end
end
check(ink > 1000 and ink < 256 * 64,
"the braille sheet has ink and empty space (" .. ink .. ")")
end
end
do
local manifestSrc = readFile("seagallop/manifest.lua")
local manifest = manifestSrc and loadstring(manifestSrc)
manifest = manifest and manifest()
check(type(manifest) == "table" and manifest.format == Seagallop.FORMAT_VERSION,
"seagallop/manifest.lua is format " .. Seagallop.FORMAT_VERSION)
for _, blob in ipairs(Seagallop.BLOBS) do
local data = readFile("seagallop/" .. blob.rel)
check(data ~= nil and #data == blob.size,
"seagallop/" .. blob.rel .. " is " .. blob.size .. " bytes ("
.. tostring(data and #data) .. ")")
end
for _, sheet in ipairs({ { "water.rgba", 64, 48 }, { "ferry.rgba", 64, 40 },
{ "wake.rgba", 32, 128 } }) do
local data = readFile("seagallop/" .. sheet[1])
check(data ~= nil and #data == sheet[2] * sheet[3] * 4,
"seagallop/" .. sheet[1] .. " is " .. sheet[2] .. "x" .. sheet[3]
.. " (" .. tostring(data and #data) .. ")")
end
local wb = readFile("seagallop/wb.rgba")
local eb = readFile("seagallop/eb.rgba")
check(wb ~= nil and #wb == 256 * 256 * 4, "seagallop/wb.rgba is 256x256")
check(eb ~= nil and #eb == 256 * 256 * 4, "seagallop/eb.rgba is 256x256")
check(wb ~= nil and eb ~= nil and wb ~= eb,
"the westbound and eastbound water is different art")
if wb then
local opaque = 0
for i = 4, #wb, 4 do
if wb:byte(i) == 255 then opaque = opaque + 1 end
end
check(opaque == 256 * 256, "the ferry background is a solid layer (" .. opaque .. ")")
end
end
do
local missing = {}
for id = 0, 19 do
local key = BattleChrome.TERRAIN_KEYS[id]
local full = readFile("pokemon/battle/terrain_" .. key .. ".rgba")
if full == nil or #full ~= 256 * 256 * 4 then
missing[#missing + 1] = key
end
for _, layer in ipairs({ "bg", "enemy", "player" }) do
local split = readFile("pokemon/battle/terrain_" .. layer .. "_" .. key .. ".rgba")
if split == nil or #split ~= 256 * 160 * 4 then
missing[#missing + 1] = layer .. "_" .. key
end
end
end
check(#missing == 0,
"all 20 terrain sets are in the cache (missing: "
.. (#missing > 0 and table.concat(missing, ",") or "none") .. ")")
end
finish()