import { test, expect } from "@playwright/test"; import { existsSync, readFileSync } from "node:fs"; import { CrystalAssets } from "../src/game/rom/assets"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; const symbolsPath = ".reference/pokecrystal/pokecrystal11.sym"; test("all tileset palettes match the original LoadMapPals routine", async ({ page, }, info) => { test.skip( info.project.name !== "chromium" || !existsSync(romPath) || !existsSync(symbolsPath), "Requires the local ROM/reference; engine comparison runs in Chromium.", ); const symbols: Record = {}; for (const line of readFileSync(symbolsPath, "utf8").split("\n")) { const m = /^([0-9a-f]{2}):([0-9a-f]{4}) (\S+)$/.exec(line); if (m) symbols[m[3]] = { bank: parseInt(m[1], 16), address: parseInt(m[2], 16) }; } const bytes = new Uint8Array(readFileSync(romPath)); const cases = Array.from({ length: 36 * 4 }, (_, i) => ({ tileset: 1 + Math.floor(i / 4), time: i % 4, environment: 3, group: 24, })); for (const environment of [1, 2, 4, 7]) for (const time of [0, 1, 2, 3]) for (const tileset of [1, 29, 30]) cases.push({ tileset, time, environment, group: 24 }); await page.goto("/reference.html"); const original = await page.evaluate( async ({ rom, symbols, cases }) => { const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts"); const emu = new BinjgbEmulator(); await emu.load(new Uint8Array(rom), document.createElement("canvas")); const write = (name: string, value: number) => emu.writeMemory(symbols[name].address, value); const target = symbols.LoadMapPals; const program = [ 0xf3, 0x31, 0xfe, 0xc7, 0x3e, target.bank, 0xe0, 0x9d, 0xea, 0, 0x20, 0xcd, target.address & 255, target.address >> 8, 0x3e, 0x42, 0xea, 0xff, 0xc6, 0x18, 0xfe, ]; const output = cases.map((c) => { emu.writeMemory(0xff70, 1); write("wMapTileset", c.tileset); write("wEnvironment", c.environment); write("wTimeOfDayPal", c.time); write("wMapGroup", c.group); emu.writeMemory(0xc6ff, 0); program.forEach((v, i) => emu.writeMemory(0xc000 + i, v)); emu.setProgramCounter(0xc000); emu.advanceFrames(2); if (emu.readMemory(0xc6ff) !== 0x42) throw new Error("LoadMapPals did not return"); emu.writeMemory(0xff70, symbols.wBGPals1.bank); return Array.from({ length: 32 }, (_, i) => { const p = symbols.wBGPals1.address + i * 2; const value = emu.readMemory(p) | (emu.readMemory(p + 1) << 8); return [0, 5, 10].map((shift) => Math.round((((value >> shift) & 31) * 255) / 31), ); }); }); emu.dispose(); return output; }, { rom: [...bytes], symbols, cases }, ); const assets = new CrystalAssets(bytes), base = assets.map("24:6"); cases.forEach((c, i) => expect( assets.palettes({ ...base, ...c }, c.time).flat(), JSON.stringify(c), ).toEqual(original[i]), ); });