Files
gen1recomp/tests/gen2_diploma_test.lua
T
Colson Rice e094b73536 Give Gen 2 the ROM text table Gen 1 has had all along
Gold and Silver had no label-keyed string table at all.  The manifests
carried no text section, RomExtractorGen2 had no extractText, and
game.data.text was never assigned, so every call through
src/core/RomText.lua fell back to the literal written beside it.  The only
Gen 2 text the cache held was the script text in data/generated/text.lua,
keyed by bank:address for the overworld VM, which nothing can look a battle
line up in.

make_gold_manifest.py now walks data/text/'s five dialogue files for their
labels, the way make_rom_manifest.text_metadata walks pokered's, and embeds
each one's symbol.  889 labels, all of them resolving in both editions.
make_silver_manifest.py inherits the list unchanged and re-resolves the
addresses from pokesilver.sym.

RomExtractorGen2:extractText decodes them into data/generated/rom_text.lua.
The mechanism is the one extractOakSpeech already used for _OakText1-7:
resolve the label, decode from the cart, key by name.  What is new is that
the list comes from the manifest rather than being written out in Lua, so
all of data/text/ arrives instead of seven strings.

decodeGen2Text also emits the three runtime name slots it used to drop.
PlaceMoveUsersName, PlaceMoveTargetsName and PlaceEnemysName (home/text.asm)
substitute a battler's name as the line prints, so <USER>, <TARGET> and
<ENEMY> are markers, not glyphs.  Skipped as control glyphs, SubTookDamageText
decoded as "The SUBSTITUTE / took damage for" with nothing after it.

Game2:load assigns the table to self.data.text, which is what makes the
existing shared RomText helper work on Gold and Silver at all.

The new cache file is listed in the Gold override rather than bumping
CACHE_FORMAT, so caches built before this stage re-import themselves and Red,
Blue and Yellow are left alone.
2026-08-21 23:08:26 -04:00

286 lines
11 KiB
Lua

-- The #DEX diploma page (engine/events/diploma.asm PlaceDiplomaOnScreen).
-- GOLD_CACHE="..." luajit tests/gen2_diploma_test.lua
--
-- The certificate is a tilemap, not a text box: DiplomaGFX decompresses into
-- vTiles2 and DiplomaPage1Tilemap is copied over the whole background before
-- any string is placed. So there are three things worth pinning here -- that
-- the extractor stage exists AND is called from run(), that what it wrote
-- matches ../pokegold/gfx/diploma/ byte for byte, and that the screen draws
-- that tilemap rather than laying the page out by eye.
--
-- ROM-free. The decomp section SKIPs with no ../pokegold beside the repo and
-- the cache section SKIPs (or asks for a re-import) with no Gold cache.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 diploma")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
local Chrome = require("src.ui.gen2.Chrome")
local Diploma = require("src.ui.gen2.Diploma")
local Screens = require("src.ui.Screens")
-- gfx/diploma/diploma.png is 16x7 tiles and page1.tilemap is one SCREEN_AREA.
local DIPLOMA_TILES = 112
local DIPLOMA_SHEET_TILES = 16
local SCREEN_W, SCREEN_H = 20, 18
local SCREEN_AREA = SCREEN_W * SCREEN_H
-- ---- the extractor stage --------------------------------------------------
-- A stage nothing calls writes nothing, and the cache checks below would then
-- SKIP forever without anyone noticing, so the call site is read out of the
-- source rather than assumed.
local extractorSource
do
local f = assert(io.open("src/import/RomExtractorGen2.lua", "r"))
extractorSource = f:read("*a")
f:close()
end
local RomExtractorGen2 = require("src.import.RomExtractorGen2")
check(type(RomExtractorGen2.extractDiploma) == "function",
"RomExtractorGen2:extractDiploma exists")
check(extractorSource:find("results.diploma = self:extractDiploma()", 1, true)
~= nil, "and RomExtractorGen2:run calls it")
check(extractorSource:find("local STAGE_COUNT = 27", 1, true) ~= nil,
"STAGE_COUNT counts the new stage, so the progress bar still ends at 1")
-- The three symbols the stage reads have to be in the curated manifest set or
-- self.symbols[label] is nil and the stage silently writes nothing.
do
local f = assert(io.open("tools/make_gold_manifest.py", "r"))
local manifestSource = f:read("*a")
f:close()
for _, label in ipairs({ "DiplomaGFX", "DiplomaPage1Tilemap",
"DiplomaPalettes" }) do
check(manifestSource:find('"' .. label .. '"', 1, true) ~= nil,
label .. " is in make_gold_manifest.py's REQUIRED_SYMBOLS")
end
end
local Json = require("src.link.Json")
do
local f = io.open("tools/rom_manifest_gold.json", "r")
if not f then
check(true, "no tools/rom_manifest_gold.json (SKIP)")
else
local manifest = Json.decode(f:read("*a"))
f:close()
local symbols = manifest.symbols or {}
-- pokegold.sym: 38:4105 DiplomaGFX, 38:454b DiplomaPage1Tilemap,
-- 02:7a86 DiplomaPalettes.
local want = {
DiplomaGFX = { 0x38, 0x4105 },
DiplomaPage1Tilemap = { 0x38, 0x454b },
DiplomaPalettes = { 0x02, 0x7a86 },
}
for label, location in pairs(want) do
local got = symbols[label]
if not got then
check(false, "the generated manifest carries " .. label)
else
eq(got[1], location[1], label .. " is in the bank pokegold.sym says")
eq(got[2], location[2], label .. " is at the address pokegold.sym says")
end
end
end
end
-- ---- the decomp -----------------------------------------------------------
local pokegold = "../pokegold"
local function readFile(path, mode)
local f = io.open(path, mode or "rb")
if not f then return nil end
local body = f:read("*a")
f:close()
return body
end
local decompTilemap = readFile(pokegold .. "/gfx/diploma/page1.tilemap")
if not decompTilemap then
check(true, "no ../pokegold: page1.tilemap is not pinned (SKIP)")
else
eq(#decompTilemap, SCREEN_AREA,
"gfx/diploma/page1.tilemap is exactly SCREEN_AREA bytes")
local highest = 0
for i = 1, #decompTilemap do
local id = decompTilemap:byte(i)
if id > highest then highest = id end
end
check(highest < DIPLOMA_TILES,
"every id in it indexes DiplomaGFX's own 112 tiles, nothing past them")
end
-- gfx/diploma/diploma.pal, colour by colour, so the extracted palette can be
-- checked against the source rather than against itself. DiplomaPalettes is
-- eight sets; only set 0 is reachable because _CGB_Diploma calls WipeAttrmap.
local DECOMP_PAL_SET0 = { { 27, 31, 27 }, { 21, 21, 21 }, { 13, 13, 13 },
{ 0, 0, 0 } }
local function scale5(value) return math.floor(value * 255 / 31 + 0.5) end
-- ---- the cache ------------------------------------------------------------
-- Same default every other gen2 suite uses, so a run with no GOLD_CACHE set
-- still reads the cache instead of skipping silently.
local cache = os.getenv("GOLD_CACHE")
or ((os.getenv("HOME") or "") .. "/Library/Application Support/LOVE/gold-dev/gold")
local function loadCache(name)
local chunk = loadfile(cache .. "/data/generated/" .. name .. ".lua")
return chunk and chunk() or nil
end
local diploma = loadCache("diploma")
if not diploma then
check(true,
"cache predates the Diploma stage : re-import for gfx/diploma (SKIP)")
else
eq(diploma.generation, 2, "diploma.lua is a Gen 2 table")
eq(diploma.tiles, DIPLOMA_TILES, "DiplomaGFX decompressed to 112 tiles")
eq(diploma.sheetTiles, DIPLOMA_SHEET_TILES,
"written as the 16-tile-wide sheet pret builds, so a tile id maps by /16")
eq(diploma.width, SCREEN_W, "the tilemap is SCREEN_WIDTH across")
eq(diploma.height, SCREEN_H, "and SCREEN_HEIGHT down")
eq(diploma.image, "assets/generated/diploma/diploma.png",
"the sheet went where the screen looks for it")
local png = readFile(cache .. "/assets/generated/diploma/diploma.png")
if not png then
check(false, "the sheet PNG is actually in the cache")
else
-- PNG IHDR: width and height are big-endian at bytes 17 and 21.
local function be32(s, i)
local a, b, c, d = s:byte(i, i + 3)
return ((a * 256 + b) * 256 + c) * 256 + d
end
eq(be32(png, 17), DIPLOMA_SHEET_TILES * 8, "the sheet is 128px wide")
eq(be32(png, 21), DIPLOMA_TILES / DIPLOMA_SHEET_TILES * 8,
"and 56px tall, which is the pret PNG's own shape")
end
local page1 = diploma.page1
if type(page1) ~= "table" then
check(false, "diploma.lua carries the page 1 tilemap")
else
eq(#page1, SCREEN_AREA, "the tilemap is one whole SCREEN_AREA")
if decompTilemap then
local mismatch
for i = 1, SCREEN_AREA do
if page1[i] ~= decompTilemap:byte(i) then mismatch = i break end
end
check(mismatch == nil, mismatch
and ("the extracted tilemap differs from the decomp at byte "
.. mismatch)
or "the extracted tilemap is ../pokegold/gfx/diploma/page1.tilemap"
.. " byte for byte")
else
check(true, "no ../pokegold: the tilemap is not diffed (SKIP)")
end
end
local palettes = diploma.palettes
if type(palettes) ~= "table" then
check(false, "diploma.lua carries DiplomaPalettes")
else
eq(#palettes, 8, "all eight sets, because the block is one table")
local set0 = palettes[1]
for index, rgb in ipairs(DECOMP_PAL_SET0) do
local got = set0 and set0[index] or {}
eq(got[1], scale5(rgb[1]),
("set 0 colour %d red matches gfx/diploma/diploma.pal"):format(index - 1))
eq(got[2], scale5(rgb[2]),
("set 0 colour %d green matches gfx/diploma/diploma.pal"):format(index - 1))
eq(got[3], scale5(rgb[3]),
("set 0 colour %d blue matches gfx/diploma/diploma.pal"):format(index - 1))
end
end
end
-- ---- the screen -----------------------------------------------------------
local hasId = false
for _, id in ipairs(Screens.GEN2_IDS) do
if id == "Gen2Diploma" then hasId = true end
end
check(hasId, "the diploma is pushed through the Gen2Diploma Screens id")
-- A tilemap standing in for page1: every cell holds its own index modulo the
-- sheet, which makes each quad's expected position arithmetic rather than a
-- lookup.
local fakePage1 = {}
for index = 0, SCREEN_AREA - 1 do
fakePage1[index + 1] = index % DIPLOMA_TILES
end
local fakeGfx = {
image = "assets/generated/diploma/diploma.png",
tiles = DIPLOMA_TILES,
sheetTiles = DIPLOMA_SHEET_TILES,
width = SCREEN_W,
height = SCREEN_H,
page1 = fakePage1,
palettes = { { { 222, 255, 222 }, { 173, 173, 173 }, { 107, 107, 107 },
{ 0, 0, 0 } } },
}
local screen = Diploma.new(nil, { playerName = "GOLD", gfx = fakeGfx })
eq(screen.playerName, "GOLD", "the screen takes the player's name")
local palette = screen:palette()
check(palette ~= nil and palette[1][2] == 255,
"and draws through DiplomaPalettes set 0, the one WipeAttrmap leaves it on")
local batch = screen:batch()
if not batch then
check(false, "the screen builds a sprite batch from the tilemap")
else
eq(#batch.sprites, SCREEN_AREA,
"one 8x8 sprite per cell, the whole background CopyBytes writes")
eq(screen:batch(), batch, "built once, not per frame")
-- Cell 0 is tile 0: sheet column 0, row 0, screen 0,0.
local first = batch.sprites[1]
eq(first[2], 0, "cell 0 lands at x 0")
eq(first[3], 0, "cell 0 lands at y 0")
eq(first[1].x, 0, "and reads sheet column 0")
eq(first[1].y, 0, "and sheet row 0")
-- Cell 21 is screen (1, 1) and, with this tilemap, tile 21: sheet column
-- 21 % 16 = 5, row 1.
local cell21 = batch.sprites[22]
eq(cell21[2], 8, "cell 21 lands one tile in")
eq(cell21[3], 8, "and one tile down")
eq(cell21[1].x, 5 * 8, "reading sheet column 5")
eq(cell21[1].y, 8, "of sheet row 1")
end
-- The strings, at the literal hlcoord operands PlaceDiplomaOnScreen uses.
local placed = {}
local realPrintThrough = Chrome.printThrough
Chrome.printThrough = function(text, tx, ty, pal)
placed[#placed + 1] = { text = text, x = tx, y = ty, palette = pal }
return 0
end
local okDraw, drawErr = pcall(function() screen:draw() end)
Chrome.printThrough = realPrintThrough
check(okDraw, "the screen draws: " .. tostring(drawErr))
eq(#placed, 7, "PLAYER, the name and the five certification lines")
eq(placed[1].text, "PLAYER", "hlcoord 2, 5 is .Player")
eq(placed[1].x, 2, "at column 2")
eq(placed[1].y, 5, "on row 5")
eq(placed[2].text, "GOLD", "hlcoord 9, 5 is wPlayerName")
eq(placed[2].x, 9, "at column 9")
eq(placed[2].y, 5, "on the same row")
local CERTIFICATION = { "This certifies", "that you have", "completed the",
"new #DEX.", "Congratulations!" }
for index, line in ipairs(CERTIFICATION) do
local entry = placed[index + 2]
eq(entry.text, line, ".Certification line " .. index)
eq(entry.x, 2, "at column 2, where PlaceString restarts each `next`")
eq(entry.y, 7 + index,
"on row " .. (7 + index) .. ", one down from hlcoord 2, 8")
end
check(placed[1].palette ~= nil,
"and every string goes through the page's own palette, not a black print")
-- A cache without the stage still opens the screen: the placeholder frame.
local bare = Diploma.new(nil, { playerName = "GOLD" })
check(bare:batch() == nil, "no gen2Diploma in the cache means no tilemap")
local okBare = pcall(function() bare:draw() end)
check(okBare, "and the screen still draws its placeholder frame")
S.finish()