import { test, expect } from "@playwright/test"; import { existsSync } from "node:fs"; import { romPath, symbolsPath, loadSymbols, openReference } from "./reference-fixture"; test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM/reference."); /** The cartridge's PlaceDiplomaOnScreen with the LCD kept off (so its * graphics load at once) and only frame waits stubbed: the tilemap, the * tiles it shows and DiplomaPalettes' palette 0 match diplomaScreen for * several names and textbox frames. */ test("PlaceDiplomaOnScreen matches the native diploma", async ({ page }) => { const { at, routine } = loadSymbols(); const flat = (name: string) => { const { bank, address } = routine(name); return address < 0x4000 ? address : bank * 0x4000 + address - 0x4000; }; const a = { romBank: at("hROMBank"), cgb: at("hCGB"), tilemap: at("wTilemap"), palettes: at("wBGPals1"), frame: at("wTextboxFrame"), name: at("wPlayerName"), }; const routines = { font: routine("_LoadStandardFont"), place: routine("PlaceDiplomaOnScreen") }; const skip = ["WaitBGMap", "DelayFrame", "DelayFrames", "EnableLCD", "DisableLCD"].map(flat); await openReference(page); const result = await page.evaluate( async ({ a, routines, skip }) => { const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts"); const { CrystalAssets } = await import("/src/game/rom/assets.ts"); const { diplomaScreen } = await import("/src/game/scripts/kanto.ts"); const { textTiles } = await import("/src/game/rom/font.ts"); const { rgb } = await import("/src/game/scripts/screen.ts"); const rom = new Uint8Array( await (document.querySelector("#reference-fixture") as HTMLInputElement).files![0].arrayBuffer(), ); const assets = new CrystalAssets(rom); for (const p of skip) rom[p] = 0xc9; const reference = new BinjgbEmulator(); await reference.load(rom, document.createElement("canvas")); const write = (p: number, n: number) => reference.writeMemory(p, n & 255); const read = (p: number) => reference.readMemory(p); const call = (r: { bank: number; address: number }) => [ 0x3e, r.bank, 0xe0, a.romBank & 255, 0xea, 0, 0x20, 0xcd, r.address & 255, r.address >> 8, ]; const failures: unknown[] = []; for (const [index, name] of ["KAWA", "ABCDEFG", "ᴾᴷᴹᴺ", "GOLD"].entries()) { write(0xff70, 1); write(0xff40, 0); write(a.cgb, 1); write(a.frame, index * 2); [...textTiles(name), 0x50].forEach((tile, i) => write(a.name + i, tile)); const program = [0xf3, 0x31, 0xfe, 0xc7, ...call(routines.font), ...call(routines.place), 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe]; write(0xc0f0, 0); program.forEach((value, offset) => write(0xc000 + offset, value)); reference.setProgramCounter(0xc000); for (let frame = 0; frame < 100 && read(0xc0f0) !== 0x42; frame++) reference.advanceFrames(1); const native = diplomaScreen(assets.reader, name, index * 2); const map = Array.from({ length: 360 }, (_, i) => read(a.tilemap + i)); write(0xff70, 5); const palette = Array.from({ length: 4 }, (_, i) => rgb(read(a.palettes + i * 2) | (read(a.palettes + i * 2 + 1) << 8))); write(0xff70, 1); write(0xff4f, 0); const tileDiff = [...new Set(native.map)].flatMap((tile) => Array.from({ length: 16 }, (_, byte) => { const actual = read((tile < 128 ? 0x9000 : 0x8800) + (tile % 128) * 16 + byte); return actual === native.tiles[tile * 16 + byte] ? [] : [{ tile, byte }]; }).flat(), ); const mapDiff = map.flatMap((tile, i) => (tile === native.map[i] ? [] : [{ i, original: tile, native: native.map[i] }])); if (read(0xc0f0) !== 0x42 || mapDiff.length || tileDiff.length || JSON.stringify(palette) !== JSON.stringify(native.palettes[0])) failures.push({ name, returned: read(0xc0f0) === 0x42, mapDiff: mapDiff.slice(0, 8), tileDiff: tileDiff.slice(0, 4), palette, native: native.palettes[0] }); } reference.dispose(); return failures; }, { a, routines, skip }, ); expect(result, JSON.stringify(result)).toEqual([]); });