import { test, expect } from "@playwright/test"; import { existsSync } from "node:fs"; import { resolve } from "node:path"; const romPath = resolve( process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc", ); test("native decompression matches original GB assembly executing in the reference emulator", async ({ page, }) => { test.skip(!existsSync(romPath), "Requires the local user ROM."); await page.goto("/reference.html"); // File chooser supplies bytes to an isolated test input, without an HTTP ROM endpoint. await page.evaluate(() => { const input = document.createElement("input"); input.type = "file"; input.id = "reference-fixture"; document.body.append(input); }); await page.locator("#reference-fixture").setInputFiles(romPath); const result = await page.evaluate(async () => { const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts"); const { CrystalAssets } = await import("/src/game/rom/assets.ts"); const { decompress } = await import("/src/game/rom/lz.ts"); const { SYMBOLS, NATIVE_MAPS } = await import("/src/game/rom/profile.ts"); const file = ( document.querySelector("#reference-fixture") as HTMLInputElement ).files![0]; const bytes = new Uint8Array(await file.arrayBuffer()); const canvas = document.createElement("canvas"); const reference = new BinjgbEmulator(); const assets = new CrystalAssets(bytes); await reference.load(bytes, canvas); const comparisons: { map: string; bytes: number; equal: boolean; returned: boolean; }[] = []; for (const id of Object.keys(NATIVE_MAPS)) { const map = assets.map(id); const pointer = assets.reader.far(SYMBOLS.tilesets + map.tileset * 15); const bank = Math.floor(pointer / 0x4000), address = 0x4000 + (pointer % 0x4000); const native = decompress(assets.reader, pointer); // Original test trampoline in WRAM: DI; SP=$c7fe; A=bank; // HL=compressed ROM address; DE=$d000; CALL FarDecompress ($0b40); // write a sentinel and loop. The referenced routine is the user's ROM. const program = [ 0xf3, 0x31, 0xfe, 0xc7, 0x3e, bank, 0x21, address & 255, address >> 8, 0x11, 0, 0xd0, 0xcd, 0x40, 0x0b, 0x3e, 0x42, 0xea, 0xff, 0xc6, 0x18, 0xfe, ]; reference.writeMemory(0xff70, 6); reference.writeMemory(0xff9d, 1); reference.writeMemory(0xc6ff, 0); program.forEach((value, index) => reference.writeMemory(0xc000 + index, value), ); reference.setProgramCounter(0xc000); reference.advanceFrames(30); comparisons.push({ map: id, bytes: native.length, returned: reference.readMemory(0xc6ff) === 0x42, equal: native.every( (value, index) => reference.readMemory(0xd000 + index) === value, ), }); } reference.dispose(); return comparisons; }); expect(result.length).toBe(6); for (const comparison of result) { expect( comparison.returned, `${comparison.map}: original routine returned`, ).toBe(true); expect( comparison.equal, `${comparison.map}: ${comparison.bytes} decompressed bytes match`, ).toBe(true); } });