mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
326 lines
13 KiB
Lua
326 lines
13 KiB
Lua
-- The Ruins of Alph wall words: engine/events/unown_walls.asm:102
|
|
-- DisplayUnownWords and data/events/unown_walls.asm:7 UnownWalls.
|
|
-- CRYSTAL_CACHE="..." luajit tests/gen2_unown_words_test.lua
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 unown words")
|
|
local check, eq = S.check, S.eq
|
|
|
|
love = require("tests.love_stub")
|
|
|
|
local Json = require("src.link.Json")
|
|
local UnownWords = require("src.world.gen2.UnownWords")
|
|
|
|
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 function pngSize(body)
|
|
local function be32(s, i)
|
|
local a, b, c, d = s:byte(i, i + 3)
|
|
return ((a * 256 + b) * 256 + c) * 256 + d
|
|
end
|
|
return be32(body, 17), be32(body, 21)
|
|
end
|
|
|
|
local function loadTable(path)
|
|
local chunk = loadfile(path)
|
|
return chunk and chunk() or nil
|
|
end
|
|
|
|
-- engine/events/unown_walls.asm:201 .ConvertChar
|
|
do
|
|
local tl, tr, bl, br = UnownWords.square(0x00) -- 'A'
|
|
eq(tl, 0x80, "A's top-left is bank 1 tile $00")
|
|
eq(tr, 0x81, "top-right is the next tile")
|
|
eq(bl, 0x90, "bottom-left is a 16-tile row down")
|
|
eq(br, 0x91, "and bottom-right beside it")
|
|
eq((UnownWords.square(0x0e)), 0x8e, "H is bank 1 tile $0e")
|
|
eq((UnownWords.square(0x20)), 0xa0, "I steps a whole row: bank 1 tile $20")
|
|
eq((UnownWords.square(0x4e)), 0xce, "X is the last computed letter")
|
|
local y1, y2, y3, y4 = UnownWords.square(0x60)
|
|
eq(y1 * 0x1000000 + y2 * 0x10000 + y3 * 0x100 + y4,
|
|
0x5b5c4d5d, "Y is .YChar's four bank 0 tiles")
|
|
local z1, z2, z3, z4 = UnownWords.square(0x62)
|
|
eq(z1 * 0x1000000 + z2 * 0x10000 + z3 * 0x100 + z4,
|
|
0x4e4f5e5f, "Z is .ZChar's")
|
|
local d1, d2, d3, d4 = UnownWords.square(0x64)
|
|
eq(d1 * 0x1000000 + d2 * 0x10000 + d3 * 0x100 + d4,
|
|
0x02030302, "and the dash is .DashChar's")
|
|
end
|
|
|
|
-- data/events/unown_walls.asm:15 MenuHeaders_UnownWalls, the n = 6 row.
|
|
do
|
|
local escape = { x1 = 3, y1 = 4, x2 = 16, y2 = 9,
|
|
chars = { 0x08, 0x44, 0x04, 0x00, 0x2e, 0x08 } }
|
|
local bx, by, bw, bh = UnownWords.boxRect(escape)
|
|
eq(bx, 3, "the box starts at column 3")
|
|
eq(by, 4, "row 4")
|
|
eq(bw, 14, "and spans the coordinate pair inclusive: 14 columns")
|
|
eq(bh, 6, "by 6 rows")
|
|
local ox, oy = UnownWords.origin(escape)
|
|
eq(ox, 4, "the first square is one column inside the frame")
|
|
eq(oy, 6, "and two rows down")
|
|
local squares = UnownWords.layout(escape)
|
|
eq(#squares, 6, "ESCAPE is six squares")
|
|
eq(squares[1].tx, 4, "the E starts at column 4")
|
|
eq(squares[2].tx, 6, "and every letter is two tiles wide")
|
|
eq(squares[6].tx, 14, "so the last one ends on the frame's inner edge")
|
|
eq(squares[1].ty, 6, "all of them on the same row")
|
|
eq(squares[4].tl, 0x80, "the A in ESCAPE is bank 1 tile $00")
|
|
end
|
|
|
|
-- constants/charmap.asm:424 the `unown` charmap.
|
|
do
|
|
local body = readFile("tools/rom_manifest_crystal.json", "r")
|
|
if not body then
|
|
check(true, "no tools/rom_manifest_crystal.json (SKIP)")
|
|
else
|
|
local manifest = Json.decode(body)
|
|
local map = manifest.unownCharmap
|
|
if not map then
|
|
check(false, "the Crystal manifest carries unownCharmap")
|
|
else
|
|
eq(map["0"], "A", "$00 is A")
|
|
eq(map["2"], "B", "$02 is B, two tiles along")
|
|
eq(map["14"], "H", "$0e is H, the last of the first row")
|
|
eq(map["32"], "I", "$20 is I: the row step is $10 every eight letters")
|
|
eq(map["78"], "X", "$4e is X")
|
|
eq(map["96"], "Y", "$60 is Y")
|
|
eq(map["98"], "Z", "$62 is Z")
|
|
eq(map["100"], "-", "$64 is the dash")
|
|
eq(map["255"], "@", "and $ff terminates a word")
|
|
local count = 0
|
|
for _ in pairs(map) do count = count + 1 end
|
|
eq(count, 28, "27 printable characters plus the terminator")
|
|
end
|
|
-- constants/charmap.asm:5 -- $00 is <NULL> in the main charmap.
|
|
local main = manifest.charmap or {}
|
|
check(main["0"] ~= "A", "the main charmap did not absorb the Unown rows")
|
|
eq(manifest.symbols.UnownWalls[1], 0x22,
|
|
"UnownWalls is in the bank pokecrystal.sym says")
|
|
eq(manifest.symbols.UnownWalls[2], 0x6ebc, "and at its address")
|
|
eq(manifest.symbols.MenuHeaders_UnownWalls[2], 0x6ed5,
|
|
"with the menu headers immediately after it")
|
|
end
|
|
end
|
|
|
|
do
|
|
for _, edition in ipairs({ "gold", "silver" }) do
|
|
local body = readFile("tools/rom_manifest_" .. edition .. ".json", "r")
|
|
if not body then
|
|
check(true, "no " .. edition .. " manifest (SKIP)")
|
|
else
|
|
local manifest = Json.decode(body)
|
|
check(manifest.unownCharmap == nil,
|
|
edition .. " has no Unown charmap: the block is Crystal only")
|
|
check(manifest.symbols.UnownWalls == nil,
|
|
edition .. " has no UnownWalls either")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- constants/charmap.asm:423,433 the two `pushc` blocks the main parser skips.
|
|
do
|
|
local source = readFile("tools/make_gold_manifest.py", "r")
|
|
if not source then
|
|
check(true, "no tools/make_gold_manifest.py (SKIP)")
|
|
else
|
|
check(source:find('if re.match(r"(pushc|newcharmap)\\b", stripped):',
|
|
1, true) ~= nil, "the main charmap parser still stops at newcharmap")
|
|
end
|
|
local crystal = readFile("tools/make_crystal_manifest.py", "r")
|
|
if not crystal then
|
|
check(true, "no tools/make_crystal_manifest.py (SKIP)")
|
|
else
|
|
check(crystal:find('data["unownCharmap"] = unown_charmap(', 1, true) ~= nil,
|
|
"and the Unown one is generated beside it, under its own key")
|
|
check(crystal:find("PRINTABLE_UNOWN", 1, true) == nil,
|
|
"with the letter set read out of charmap.asm, not copied here")
|
|
end
|
|
end
|
|
|
|
-- home/map.asm:1357-1370 LoadTilesetGFX's two CopyBytes.
|
|
do
|
|
local source = assert(readFile("src/import/RomExtractorGen2.lua", "r"))
|
|
check(source:find("local function crystalTilesetSheet(pixels)", 1, true)
|
|
~= nil, "the Crystal sheet lays both VRAM banks out at their tile ids")
|
|
check(source:find("if twoBank then pixels = crystalTilesetSheet(pixels) end",
|
|
1, true) ~= nil, "and extractTilesets uses it")
|
|
check(source:find("local CRYSTAL_PAL_MAP_BYTES = 112", 1, true) ~= nil,
|
|
"the PalMap read covers the bank 1 rows too")
|
|
check(source:find("function RomExtractorGen2:readCrystalPalMap", 1, true)
|
|
~= nil, "Crystal PalMap skips $ff padding and maps bank 1 to $80+")
|
|
check(source:find("out.unownWalls = walls", 1, true) ~= nil,
|
|
"and readEventTables emits the wall words")
|
|
end
|
|
|
|
-- gfx/tilesets/ruins_of_alph.png, whose bottom half is the Unown alphabet.
|
|
do
|
|
local png = readFile(
|
|
"../pokecrystal/gfx/tilesets/ruins_of_alph.png")
|
|
if not png then
|
|
check(true, "no ../pokecrystal: the tileset's shape is not pinned (SKIP)")
|
|
else
|
|
local w, h = pngSize(png)
|
|
eq(w, 128, "pret's ruins_of_alph.png is 16 tiles across")
|
|
eq(h, 96, "and 12 down: $60 tiles per VRAM bank, two banks")
|
|
end
|
|
end
|
|
|
|
local cache = os.getenv("CRYSTAL_CACHE")
|
|
if not cache then
|
|
cache = (os.getenv("HOME") or "")
|
|
.. "/Library/Application Support/LOVE/crystal-dev/crystal"
|
|
end
|
|
|
|
local CacheContract = require("src.import.CacheContract")
|
|
local stale = not CacheContract.markerMatches(
|
|
"crystal", readFile(cache .. "/rom-cache.complete"))
|
|
|
|
local events = not stale and loadTable(cache .. "/data/generated/events.lua") or nil
|
|
local tilesets = not stale and loadTable(cache .. "/data/generated/tilesets.lua") or nil
|
|
|
|
-- data/events/unown_walls.asm:2-5
|
|
local EXPECTED = {
|
|
{ word = "ESCAPE", x1 = 3, y1 = 4, x2 = 16, y2 = 9 },
|
|
{ word = "LIGHT", x1 = 4, y1 = 4, x2 = 15, y2 = 9 },
|
|
{ word = "WATER", x1 = 4, y1 = 4, x2 = 15, y2 = 9 },
|
|
{ word = "HO-OH", x1 = 4, y1 = 4, x2 = 15, y2 = 9 },
|
|
}
|
|
|
|
if not events then
|
|
check(true, "no Crystal cache: the wall words are not read (SKIP)")
|
|
elseif not events.unownWalls then
|
|
check(true, "cache predates the wall words: re-import Crystal (SKIP)")
|
|
else
|
|
local walls = events.unownWalls
|
|
eq(#walls, 4, "NUM_UNOWN_WALLS rows came out")
|
|
for index, want in ipairs(EXPECTED) do
|
|
local got = walls[index] or {}
|
|
eq(got.word, want.word, want.word .. " decoded through the Unown charmap")
|
|
eq(got.id, index - 1, "at the UNOWNWORDS_* value the setval uses")
|
|
eq(#(got.chars or {}), #want.word, "with one character byte per letter")
|
|
eq(got.x1, want.x1, want.word .. "'s box left edge")
|
|
eq(got.y1, want.y1, "top edge")
|
|
eq(got.x2, want.x2, "right edge")
|
|
eq(got.y2, want.y2, "bottom edge")
|
|
-- data/events/unown_walls.asm:20 MENU_BACKUP_TILES
|
|
eq(got.flags, 0x40, "and MENU_BACKUP_TILES set")
|
|
-- data/events/unown_walls.asm:21 `menu_coords 9 - n, 4, 10 + n, 9`
|
|
local _, _, bw = UnownWords.boxRect(got)
|
|
eq(bw - 2, #want.word * 2, "the frame is 2 columns per letter wide")
|
|
end
|
|
-- engine/events/unown_walls.asm:249 .DashChar
|
|
local dash = UnownWords.layout(walls[4])[3]
|
|
eq(dash.tl, 0x02, "HO-OH's dash comes out of .DashChar, not the alphabet")
|
|
end
|
|
|
|
if not tilesets then
|
|
check(true, "no Crystal cache: the tileset sheet is not read (SKIP)")
|
|
else
|
|
local roa = tilesets.TILESET_RUINS_OF_ALPH
|
|
if not roa then
|
|
check(false, "the Ruins of Alph tileset is in the cache")
|
|
elseif roa.imageHeight ~= 128 then
|
|
check(true, "cache predates the two-bank sheet: re-import Crystal (SKIP)")
|
|
else
|
|
eq(roa.imageWidth, 128, "the sheet is 16 tiles across")
|
|
eq(roa.imageHeight, 128, "and 16 down: 256 tile ids, both VRAM banks")
|
|
eq(roa.tilesPerRow, 16, "so a tile id indexes it directly")
|
|
check(roa.tileAttrs ~= nil, "Crystal tilesets carry tileAttrs")
|
|
local bank1 = roa.tileAttrs and roa.tileAttrs[0x80 + 1]
|
|
check(bank1 ~= nil, "bank-1 tile $80 has attrs after re-import")
|
|
if bank1 then
|
|
eq(bank1.vramBank, 1, "bank-1 tile $80 maps to VRAM bank 1")
|
|
end
|
|
check(roa.tilePalettes[0x80 + 1] ~= nil,
|
|
"bank-1 palette slot is not shifted by $ff padding")
|
|
eq(roa.tilePalettes[97], nil,
|
|
"padding bytes do not assign palettes to tile 96")
|
|
local png = readFile(cache .. "/assets/generated/tilesets/ruins_of_alph.png")
|
|
if not png then
|
|
check(false, "the sheet PNG is actually in the cache")
|
|
else
|
|
local w, h = pngSize(png)
|
|
eq(w, 128, "the written PNG is 128 wide")
|
|
eq(h, 128, "and 128 tall")
|
|
end
|
|
-- constants/tileset_constants.asm:34-38, the five Crystal-only tilesets.
|
|
for _, name in ipairs({ "TILESET_KABUTO_WORD_ROOM",
|
|
"TILESET_OMANYTE_WORD_ROOM", "TILESET_AERODACTYL_WORD_ROOM",
|
|
"TILESET_HO_OH_WORD_ROOM", "TILESET_BETA_WORD_ROOM" }) do
|
|
local set = tilesets[name]
|
|
if not set then
|
|
check(false, name .. " extracted")
|
|
else
|
|
eq(set.imageHeight, 128, name .. " is a two-bank sheet")
|
|
-- engine/tilesets/map_palettes.asm:40 -- bit 7 is the VRAM bank.
|
|
local high = 0
|
|
for _, block in ipairs(set.blocks) do
|
|
for _, tile in ipairs(block) do
|
|
if tile >= 0x80 and tile < 0xe0 then high = high + 1 end
|
|
end
|
|
end
|
|
check(high > 0, name .. " really does draw out of bank 1")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- data/events/special_pointers.asm:151 add_special DisplayUnownWords
|
|
do
|
|
local Specials = require("src.script.gen2.Specials")
|
|
check(type(Specials.ALL.DisplayUnownWords) == "function",
|
|
"DisplayUnownWords is a handler and not a stub")
|
|
eq(Specials.HANDLER_SOURCE.DisplayUnownWords,
|
|
"specials/unown_words.lua", "owned by this unit's module")
|
|
end
|
|
|
|
do
|
|
local maps = loadTable(cache .. "/data/generated/maps.lua")
|
|
local scripts = loadTable(cache .. "/data/generated/scripts.lua")
|
|
local consts = loadTable(cache .. "/data/generated/constants.lua")
|
|
if not (maps and scripts and consts and events and events.unownWalls) then
|
|
check(true, "no Crystal cache: the chambers are not walked (SKIP)")
|
|
else
|
|
local index
|
|
for id, name in pairs(consts.specialOrder or {}) do
|
|
if name == "DisplayUnownWords" then index = id - 1 end
|
|
end
|
|
check(index ~= nil, "the cache's specialOrder names DisplayUnownWords")
|
|
local chambers = {
|
|
RUINS_OF_ALPH_KABUTO_CHAMBER = 0,
|
|
RUINS_OF_ALPH_AERODACTYL_CHAMBER = 1,
|
|
RUINS_OF_ALPH_OMANYTE_CHAMBER = 2,
|
|
RUINS_OF_ALPH_HO_OH_CHAMBER = 3,
|
|
}
|
|
for mapId, want in pairs(chambers) do
|
|
local def = maps[mapId]
|
|
local walls = 0
|
|
for _, bg in ipairs((def or {}).bgEvents or {}) do
|
|
local rows = bg.scriptKey and scripts[bg.scriptKey]
|
|
local pending
|
|
for _, row in ipairs(rows or {}) do
|
|
if row.op == "setval" then
|
|
pending = row.value or (row.args and row.args[1])
|
|
elseif row.op == "special" and row.id == index then
|
|
walls = walls + 1
|
|
eq(pending, want, mapId .. " asks for its own word")
|
|
check(UnownWords.wallFor(
|
|
{ gen2EventTables = events }, pending) ~= nil,
|
|
"and that value indexes a real wall row")
|
|
end
|
|
end
|
|
end
|
|
eq(walls, 2, mapId .. " has both of its wall patterns")
|
|
end
|
|
end
|
|
end
|
|
|
|
S.finish()
|