Files
gen1recomp/tests/emerald_text_test.lua
T
bryanthaboi 690f0cd9a1 TOUCHDOWN
2026-09-27 16:07:31 -04:00

236 lines
8.8 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local ROM_PATH = os.getenv("POKEPORT_EMERALD_ROM") or "../pokeemerald/pokeemerald.gba"
local f = io.open(ROM_PATH, "rb")
if not f then
print("emerald_text_test: skipped (no ROM at " .. ROM_PATH .. ")")
os.exit(0)
end
local raw = f:read("*a")
f:close()
local rom = { size = #raw }
function rom:get(o) return raw:byte(o + 1) end
function rom:u16(o) local a, b = raw:byte(o + 1, o + 2); return a + b * 256 end
function rom:u32(o)
local a, b, c, d = raw:byte(o + 1, o + 4)
return a + b * 256 + c * 65536 + d * 16777216
end
function rom:ptrOffset(p)
if p and p >= 0x08000000 and p < 0x0A000000 then return p - 0x08000000 end
end
local Versions = require("src.import.gba.versions")
Versions.select("emerald")
local TextIR = require("src.core.game3.scripting.text_ir")
local rse = TextIR.dialect("rse")
local extra = rse.CHARMAP_EXTRA or {}
-- pokeemerald/charmap.txt:45
local PENDING = { [0x34] = true, [0x55] = true, [0x56] = true, [0x57] = true, [0x58] = true, [0x59] = true,
[0x77] = true, [0x79] = true, [0x7A] = true, [0x7B] = true, [0x7C] = true }
local function unknownBytes(off, pending)
local out, i = {}, off
while i - off < 2048 do
local c = raw:byte(i + 1)
if c == 0xFF then break end
if c == 0xFD or c == 0xF7 or c == 0xF8 or c == 0xF9 then
i = i + 2
elseif c == 0xFC then
i = i + 2 + (TextIR.EXT_ARGS[raw:byte(i + 2)] or 0)
elseif c == 0xFA or c == 0xFB or c == 0xFE then
i = i + 1
else
if not (TextIR.CHARMAP[c] or TextIR.LIGATURE[c] or extra[c]) and not (pending and PENDING[c]) then
out[#out + 1] = c
end
i = i + 1
end
end
return out
end
local function japanese(name)
return name:find("JP", 1, true) or name:find("Jpn", 1, true) or name:find("Japanese", 1, true)
end
local names = {}
for name in pairs(Versions.NAMED_TEXTS) do
if not japanese(name) then names[#names + 1] = name end
end
table.sort(names)
local seed = tonumber(os.getenv("POKEPORT_TEXT_SEED") or "") or os.time()
math.randomseed(seed)
local picked = {}
for _ = 1, 20 do
local name = names[math.random(#names)]
local bytes = {}
local off = Versions.NAMED_TEXTS[name]
for i = 0, 1023 do
local b = raw:byte(off + i + 1)
bytes[#bytes + 1] = b
if b == 0xFF then break end
end
local ir = TextIR.decode(bytes, { dialect = "rse" })
check(ir[#ir] and ir[#ir].t == "eos", name .. " decodes to an EOS-terminated IR")
local bad = unknownBytes(off, true)
eq(#bad, 0, "random named text " .. name .. " has no unknown glyph bytes (seed " .. seed .. ")")
picked[#picked + 1] = name
end
local seenUnknown, firstAt = {}, {}
local function scan(label, off)
for _, c in ipairs(unknownBytes(off, false)) do
seenUnknown[c] = (seenUnknown[c] or 0) + 1
firstAt[c] = firstAt[c] or label
end
end
for _, name in ipairs(names) do scan(name, Versions.NAMED_TEXTS[name]) end
for name, off in pairs(Versions.NAMED_BATTLE_TEXTS) do
if not japanese(name) then scan(name, off) end
end
local slots, nonPointer = 0, 0
for _, t in ipairs(Versions.TEXT_TABLES) do
if not japanese(t.name) then
for i = 0, t.count * (t.inner or 1) - 1 do
slots = slots + 1
if t.inline then
scan(t.name, t.addr + i * t.stride)
else
local p = rom:u32(t.addr + i * t.stride)
if p ~= 0 then
if rom:ptrOffset(p) then
local target = Versions.SYMS.namesAt(p - 0x08000000)[1] or ""
if not (japanese(target) or t.name:find("Debug", 1, true)) then
scan(t.name .. "[" .. i .. "]", p - 0x08000000)
end
else
nonPointer = nonPointer + 1
end
end
end
end
end
end
eq(nonPointer, 0, "every non-null text table slot (" .. slots .. ") is a ROM pointer")
local outside = {}
for c, n in pairs(seenUnknown) do
if not PENDING[c] then outside[#outside + 1] = string.format("0x%02X x%d (%s)", c, n, firstAt[c]) end
end
table.sort(outside)
eq(#outside, 0, "no unknown glyph bytes outside the rse charmap gap list: " .. table.concat(outside, ", "))
local pending = {}
for c in pairs(PENDING) do
if seenUnknown[c] then pending[#pending + 1] = string.format("0x%02X", c) end
end
table.sort(pending)
if #pending > 0 then
print("emerald_text_test: rse charmap still lacks " .. table.concat(pending, " ") .. " (crossfile W1-T)")
end
local Placeholders = require("src.import.gba.text_placeholders_extract")
local files = {}
local cache = {
write = function(_, rel, data) files[rel] = data; return true end,
exists = function(_, rel) return files[rel] ~= nil end,
read = function(_, rel) return files[rel] end,
}
local ph = Placeholders.run(rom, cache, {})
eq(ph.VERSION, "EMERALD", "placeholder VERSION")
eq(ph.AQUA, "AQUA", "placeholder AQUA")
eq(ph.MAGMA, "MAGMA", "placeholder MAGMA")
eq(ph.ARCHIE, "ARCHIE", "placeholder ARCHIE")
eq(ph.MAXIE, "MAXIE", "placeholder MAXIE")
eq(ph.KYOGRE, "KYOGRE", "placeholder KYOGRE")
eq(ph.GROUDON, "GROUDON", "placeholder GROUDON")
eq(ph.byGender.RIVAL.male, "MAY", "rival placeholder for a male player")
eq(ph.byGender.RIVAL.female, "BRENDAN", "rival placeholder for a female player")
local src = files["data/generated/gba/text/placeholders.lua"]
check(src ~= nil, "placeholders.lua written")
local loaded = src and load(src, "placeholders", "t", {})()
eq(loaded and loaded.KYOGRE, "KYOGRE", "placeholders.lua round trips")
local ir = TextIR.decode({ 0xFD, 0x07, 0x00, 0xFD, 0x0D, 0xFF }, { dialect = "rse" })
eq(TextIR.toPlain(ir, { placeholders = loaded, dialect = "rse" }), "EMERALD GROUDON",
"FD 07 / FD 0D expand from the cached placeholders")
local Chrome = require("src.import.gba.rse.text_chrome_extract")
check(Chrome.run(rom, cache, {}), "rse text chrome extract runs")
local root = "data/generated/gba/chrome/"
local frames = 0
for i = 0, 19 do
local d = files[root .. "user_frame_" .. i .. ".rgba"]
if d and #d == 24 * 24 * 4 then frames = frames + 1 end
end
eq(frames, 20, "20 user_frame_N.rgba at 24x24")
eq(files[root .. "user_frame_20.rgba"], nil, "no user_frame_20")
for _, face in ipairs(Chrome.LATIN) do
local w = load(files[root .. "fonts/" .. Chrome.WIDTH_FILES[face]], "w", "t", {})()
local n = 0
for k in pairs(w) do if k >= 0 then n = n + 1 end end
eq(n, 512, face .. " width table has 512 entries")
eq(#files[root .. "fonts/latin_" .. face .. "_fg.rgba"], 256 * 512 * 4, face .. " fg sheet size")
end
local widths = load(files[root .. "fonts/latin_widths.lua"], "w", "t", {})()
eq(widths[0xBB], 6, "normal font 'A' is 6px wide")
eq(widths[0x00], 3, "normal font space is 3px wide")
eq(#files[root .. "message_box.rgba"], 56 * 16 * 4, "message box sheet 56x16")
eq(#files[root .. "fonts/down_arrow.idx"], 8 * 48, "down arrow index map 8x48")
local metrics = load(files[root .. "fonts/metrics.lua"], "m", "t", {})()
eq(metrics[1].name, "FONT_NORMAL", "font 1 is FONT_NORMAL")
eq(metrics[1].maxLetterHeight, 16, "FONT_NORMAL maxLetterHeight")
eq(metrics[0].maxLetterHeight, 12, "FONT_SMALL maxLetterHeight")
local function pngIndices(png)
local tmp = os.tmpname()
local cmd = string.format(
"python3 -c \"import sys;from PIL import Image;im=Image.open(sys.argv[1]);open(sys.argv[2],'wb').write(bytes([im.size[0]//256,im.size[0]%%256,im.size[1]//256,im.size[1]%%256])+im.tobytes())\" '%s' '%s' 2>/dev/null",
png, tmp)
local ok = os.execute(cmd)
local h = io.open(tmp, "rb")
local data = h and h:read("*a")
if h then h:close() end
os.remove(tmp)
if not (ok == 0 or ok == true) or not data or #data < 4 then return nil end
local w = data:byte(1) * 256 + data:byte(2)
local hgt = data:byte(3) * 256 + data:byte(4)
return data:sub(5), w, hgt
end
local PRET_FONTS = ROM_PATH:gsub("[^/]*$", "") .. "graphics/fonts/"
local idx, pw = pngIndices(PRET_FONTS .. "latin_normal.png")
if not idx then
print("emerald_text_test: glyph pixel check skipped (python3 + PIL or pret graphics missing)")
else
local fg = files[root .. "fonts/latin_normal_fg.rgba"]
local sh = files[root .. "fonts/latin_normal_shadow.rgba"]
local function sheetValue(x, y)
local i = (y * 256 + x) * 4 + 4
if fg:byte(i) ~= 0 then return 1 end
if sh:byte(i) ~= 0 then return 2 end
return 0
end
local function glyphMismatch(gid)
local bad = 0
local ox, oy = (gid % 16) * 16, math.floor(gid / 16) * 16
for y = 0, 15 do
for x = 0, 15 do
local p = idx:byte((oy + y) * pw + ox + x + 1)
local want = (p == 1 or p == 2) and p or 0
if sheetValue(ox + x, oy + y) ~= want then bad = bad + 1 end
end
end
return bad
end
eq(glyphMismatch(0xBB), 0, "latin_normal 'A' glyph is pixel-exact vs pret latin_normal.png")
local total = 0
for gid = 0, 511 do total = total + glyphMismatch(gid) end
eq(total, 0, "all 512 latin_normal glyphs are pixel-exact vs pret latin_normal.png")
end
T.finish("emerald_text_test")