107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
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);
|
|
}
|
|
});
|