import { test, expect } from "@playwright/test"; import { existsSync } from "node:fs"; import { romPath, symbolsPath, loadSymbols, openReference, revisionRoms } from "./reference-fixture"; test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM/reference."); // The English cartridges share 1.1's symbols; not run with another's. test.skip(Boolean(process.env.CRYSTAL_SYMBOLS_PATH), "Compares the English cartridges."); /** BattleTowerText on each English cartridge, with PrintTextboxTextAt * patched to keep the text pointer: for every BattleTowerTrainers entry, * several hRandomAdd values and each of the three texts, the pointer matches * battleTowerText's. Revision 1.0 indexes BTTrainerClassGenders with the * name's sixth character instead of the class. */ for (const [revision, rom] of [["1.1", romPath], ["1.0", revisionRoms["1.0"]], ["au", revisionRoms.au]] as const) test(`BattleTowerText matches the ${revision} cartridge`, async ({ page }) => { test.skip(!existsSync(rom), `Needs PRET's ${revision} rebuild.`); test.setTimeout(120_000); const { at, routine } = loadSymbols(); const a = { romBank: at("hROMBank"), program: at("wOverworldMapBlocks"), stackTop: at("wStackTop"), randomAdd: at("hRandomAdd"), name: routine("wBT_OTName"), textIndex: routine("wBT_TrainerTextIndex"), }; const routines = { text: routine("BattleTowerText"), print: routine("PrintTextboxTextAt") }; await openReference(page, rom); const result = await page.evaluate( async ({ a, routines, revision }) => { const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts"); const { CrystalAssets } = await import("/src/game/rom/assets.ts"); const { romRevision } = await import("/src/game/rom/revision.ts"); const { JOHTO } = await import("/src/game/rom/profile.ts"); const { battleTowerText, emptyBattleTowerState } = await import("/src/game/scripts/battle-tower.ts"); const rom = new Uint8Array( await (document.querySelector("#reference-fixture") as HTMLInputElement).files![0].arrayBuffer(), ); const assets = new CrystalAssets(rom); const r = assets.reader; // PrintTextboxTextAt: keep HL at program + $f0 and return. const kept = a.program + 0xf0; [0x7d, 0xea, kept & 255, kept >> 8, 0x7c, 0xea, (kept + 1) & 255, (kept + 1) >> 8, 0xc9].forEach( (value, i) => (rom[routines.print.address + i] = value), ); const reference = new BinjgbEmulator(); await reference.load(rom, document.createElement("canvas")); reference.setMuted(true); const read = (p: number) => reference.readMemory(p); const write = (p: number, v: number) => reference.writeMemory(p, v & 255); reference.advanceFrames(300); const sentinel = a.program + 0xf8; const failures: unknown[] = []; let compared = 0; for (let trainer = 0; trainer < 70; trainer++) for (const roll of [0x00, 0x0e, 0x13, 0x1f, 0x9b, 0xff]) { const bt = emptyBattleTowerState(); const entry = JOHTO.BattleTowerTrainers + trainer * 11; bt.opponent = { trainer, name: "", trainerClass: r.byte(entry + 10), party: [] }; for (const which of [1, 2, 3] as const) { write(0xff70, a.name.bank); for (let i = 0; i < 11; i++) write(a.name.address + i, r.byte(entry + i)); write(0xff70, 1); write(a.randomAdd, roll); write(sentinel, 0); const program = [ 0xf3, 0x31, a.stackTop & 255, a.stackTop >> 8, 0x3e, routines.text.bank, 0xe0, a.romBank & 255, 0xea, 0, 0x20, 0x0e, which, 0xcd, routines.text.address & 255, routines.text.address >> 8, 0x3e, 0x42, 0xea, sentinel & 255, sentinel >> 8, 0x18, 0xfe, ]; program.forEach((v, i) => write(a.program + i, v)); reference.setProgramCounter(a.program); for (let frame = 0; frame < 10 && read(sentinel) !== 0x42; frame++) reference.advanceFrames(1); const original = read(kept) | (read(kept + 1) << 8); // A flat offset in BattleTowerText's bank, as the CPU sees it. const flat = battleTowerText({ reader: r, random: () => roll } as never, bt, which); const native = r.bank(flat) === routines.text.bank ? (flat & 0x3fff) | 0x4000 : -flat; compared++; if (read(sentinel) !== 0x42 || original !== native) failures.push({ trainer, roll, which, original: original.toString(16), native: native.toString(16) }); } } reference.dispose(); return { revision: romRevision(r), compared, failures: failures.slice(0, 8), failed: failures.length }; }, { a, routines, revision }, ); expect(result.revision).toBe(revision); expect(result.failures, JSON.stringify(result)).toEqual([]); expect(result.compared).toBe(70 * 6 * 3); });