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 and reference symbols.", ); test("all ten MAIL screens match cartridge tilemaps, graphics, message placement and palettes", async ({ page, }) => { const { at, routine } = loadSymbols(); const labels = [ "Flower", "Surf", "LiteBlue", "Portrait", "Lovely", "Eon", "Morph", "BlueSky", "Music", "Mirage", ]; const defs = labels.map((name) => routine(`Load${name}MailGFX`)); const a = Object.fromEntries( [ "hROMBank", "hCGB", "wTilemap", "wAttrmap", "wCurPartySpecies", "wUnownLetter", "wCurMailIndex", "wBoxAlignment", "wBGPals1", "sPartyMon1Mail", "sPartyMon1MailAuthor", ].map((n) => [n, at(n)]), ); const skip = ["ApplyPals", "ApplyAttrmap"].map(routine); const palette = routine("LoadMailPalettes"); await openReference(page); const result = await page.evaluate( async ({ defs, a, skip, palette }) => { const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts"); const { CrystalAssets } = await import("/src/game/rom/assets.ts"); const { mailScreen } = await import("/src/game/scripts/mail.ts"); const { textTiles } = await import("/src/game/rom/font.ts"); const { MAIL } = await import("/src/game/rom/profile.ts"); const rom = new Uint8Array( await ( document.querySelector("#reference-fixture") as HTMLInputElement ).files![0].arrayBuffer(), ); const assets = new CrystalAssets(rom); // Exclude hardware palette transfer only. The original graphics, // message/author placement and palette selection execute. for (const { bank, address } of skip) rom[bank * 0x4000 + address - 0x4000] = 0xc9; const ref = new BinjgbEmulator(); await ref.load(rom, document.createElement("canvas")); const write = (p: number, v: number) => ref.writeMemory(p, v & 255); const read = (p: number) => ref.readMemory(p); const call = (routine: { bank: number; address: number }, e = 0) => { const program = [ 0xf3, 0x31, 0xfe, 0xc7, 0xaf, 0xe0, 0x40, 0xe0, 0x4f, 0x3e, routine.bank, 0xe0, a.hROMBank & 255, 0xea, 0, 0x20, 0x01, a.sPartyMon1Mail & 255, a.sPartyMon1Mail >> 8, 0x1e, e, 0xcd, routine.address & 255, routine.address >> 8, 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe, ]; write(0xc0f0, 0); program.forEach((v, i) => write(0xc000 + i, v)); ref.setProgramCounter(0xc000); for (let f = 0; f < 60 && read(0xc0f0) !== 0x42; f++) ref.advanceFrames(1); if (read(0xc0f0) !== 0x42) throw Error( `MAIL routine ${routine.address.toString(16)} did not return`, ); }; const failures: unknown[] = []; for (let index = 0; index < defs.length; index++) { write(0xff40, 0); write(0xff4f, 0); write(0xff70, 1); write(a.hCGB, 1); for (let p = 0x8000; p < 0xa000; p++) write(p, 0); for (let i = 0; i < 360; i++) write(a.wTilemap + i, 0x7f); write(a.wCurMailIndex, index); write(a.wBoxAlignment, 0); write(a.wCurPartySpecies, 201); write(a.wUnownLetter, 1); const message = "“HELLO…” FRIEND! "; write(0x0000, 0x0a); write(0x4000, 0); const bytes = [ ...textTiles(message), 0x4e, ...textTiles("SEE YOU SOON!"), 0x50, ]; for (let i = 0; i < 47; i++) write(a.sPartyMon1Mail + i, 0x50); bytes.forEach((v, i) => write(a.sPartyMon1Mail + i, v)); textTiles("CHRIS").forEach((v, i) => write(a.sPartyMon1MailAuthor + i, v), ); call(defs[index]); const native = mailScreen(assets.pokemon, { message: message + "\nSEE YOU SOON!", author: "CHRIS", authorId: 1, species: 201, type: assets.reader.byte(MAIL.MailGFXPointers + index * 3), }); const map = Array.from({ length: 360 }, (_, i) => read(a.wTilemap + i)); const mapDiff = map.flatMap((v, i) => v === native.map[i] ? [] : [{ i, original: v, native: native.map[i] }], ); const tileDiff: unknown[] = []; for (const tile of new Set(map)) { if (tile >= 0x60) continue; // font is loaded by ReadAnyMail, before these routines for (let b = 0; b < 16; b++) if (read(0x9000 + tile * 16 + b) !== native.tiles[tile * 16 + b]) { tileDiff.push({ tile, b, original: read(0x9000 + tile * 16 + b), native: native.tiles[tile * 16 + b], }); break; } } call(palette, index); write(0xff70, 5); const colors = Array.from( { length: 4 }, (_, i) => read(a.wBGPals1 + i * 2) | (read(a.wBGPals1 + i * 2 + 1) << 8), ); const rgb = colors.map((v) => [v & 31, (v >> 5) & 31, (v >> 10) & 31].map((c) => Math.round((c * 255) / 31), ), ); write(0xff70, 1); if ( mapDiff.length || tileDiff.length || JSON.stringify(rgb) !== JSON.stringify(native.palettes[0]) ) failures.push({ index, mapDiff: mapDiff.slice(0, 12), tileDiff: tileDiff.slice(0, 12), rgb, nativePalette: native.palettes[0], }); } ref.dispose(); return { count: defs.length, failures }; }, { defs, a, skip, palette }, ); expect(result.count).toBe(10); expect(result.failures).toEqual([]); });