-- 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 = 26", 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()