PRET's crystal and crystal_au builds reproduce both retail cartridges, and every label the profile pins keeps its address in all three; check:reference now verifies that when the rebuilds are present. The revision comes from the header. Revision 1.0's Battle Tower draws only the first 21 trainers and picks its texts' gender from the name's sixth character, checked against each cartridge. A native backup imports across the three, which share every map, event and object. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
4.9 KiB
TypeScript
93 lines
4.9 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { existsSync } from "node:fs";
|
|
import { romPath, symbolsPath, loadSymbols, openReference, revisionRoms } from "./reference-fixture";
|
|
|
|
test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM/reference.");
|
|
|
|
/** BattleTowerText on each English cartridge, with PrintTextboxTextAt
|
|
* patched to keep the text pointer: for every BattleTowerTrainers entry,
|
|
* several hRandomAdd values and each of the three texts, the pointer matches
|
|
* battleTowerText's. Revision 1.0 indexes BTTrainerClassGenders with the
|
|
* name's sixth character instead of the class. */
|
|
for (const [revision, rom] of [["1.1", romPath], ["1.0", revisionRoms["1.0"]], ["au", revisionRoms.au]] as const)
|
|
test(`BattleTowerText matches the ${revision} cartridge`, async ({ page }) => {
|
|
test.skip(!existsSync(rom), `Needs PRET's ${revision} rebuild.`);
|
|
test.setTimeout(120_000);
|
|
const { at, routine } = loadSymbols();
|
|
const a = {
|
|
romBank: at("hROMBank"),
|
|
program: at("wOverworldMapBlocks"),
|
|
stackTop: at("wStackTop"),
|
|
randomAdd: at("hRandomAdd"),
|
|
name: routine("wBT_OTName"),
|
|
textIndex: routine("wBT_TrainerTextIndex"),
|
|
};
|
|
const routines = { text: routine("BattleTowerText"), print: routine("PrintTextboxTextAt") };
|
|
await openReference(page, rom);
|
|
const result = await page.evaluate(
|
|
async ({ a, routines, revision }) => {
|
|
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
|
|
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
|
|
const { romRevision } = await import("/src/game/rom/revision.ts");
|
|
const { JOHTO } = await import("/src/game/rom/profile.ts");
|
|
const { battleTowerText, emptyBattleTowerState } = await import("/src/game/scripts/battle-tower.ts");
|
|
const rom = new Uint8Array(
|
|
await (document.querySelector("#reference-fixture") as HTMLInputElement).files![0].arrayBuffer(),
|
|
);
|
|
const assets = new CrystalAssets(rom);
|
|
const r = assets.reader;
|
|
// PrintTextboxTextAt: keep HL at program + $f0 and return.
|
|
const kept = a.program + 0xf0;
|
|
[0x7d, 0xea, kept & 255, kept >> 8, 0x7c, 0xea, (kept + 1) & 255, (kept + 1) >> 8, 0xc9].forEach(
|
|
(value, i) => (rom[routines.print.address + i] = value),
|
|
);
|
|
const reference = new BinjgbEmulator();
|
|
await reference.load(rom, document.createElement("canvas"));
|
|
reference.setMuted(true);
|
|
const read = (p: number) => reference.readMemory(p);
|
|
const write = (p: number, v: number) => reference.writeMemory(p, v & 255);
|
|
reference.advanceFrames(300);
|
|
const sentinel = a.program + 0xf8;
|
|
const failures: unknown[] = [];
|
|
let compared = 0;
|
|
for (let trainer = 0; trainer < 70; trainer++)
|
|
for (const roll of [0x00, 0x0e, 0x13, 0x1f, 0x9b, 0xff]) {
|
|
const bt = emptyBattleTowerState();
|
|
const entry = JOHTO.BattleTowerTrainers + trainer * 11;
|
|
bt.opponent = { trainer, name: "", trainerClass: r.byte(entry + 10), party: [] };
|
|
for (const which of [1, 2, 3] as const) {
|
|
write(0xff70, a.name.bank);
|
|
for (let i = 0; i < 11; i++) write(a.name.address + i, r.byte(entry + i));
|
|
write(0xff70, 1);
|
|
write(a.randomAdd, roll);
|
|
write(sentinel, 0);
|
|
const program = [
|
|
0xf3, 0x31, a.stackTop & 255, a.stackTop >> 8,
|
|
0x3e, routines.text.bank, 0xe0, a.romBank & 255, 0xea, 0, 0x20,
|
|
0x0e, which,
|
|
0xcd, routines.text.address & 255, routines.text.address >> 8,
|
|
0x3e, 0x42, 0xea, sentinel & 255, sentinel >> 8,
|
|
0x18, 0xfe,
|
|
];
|
|
program.forEach((v, i) => write(a.program + i, v));
|
|
reference.setProgramCounter(a.program);
|
|
for (let frame = 0; frame < 10 && read(sentinel) !== 0x42; frame++) reference.advanceFrames(1);
|
|
const original = read(kept) | (read(kept + 1) << 8);
|
|
// A flat offset in BattleTowerText's bank, as the CPU sees it.
|
|
const flat = battleTowerText({ reader: r, random: () => roll } as never, bt, which);
|
|
const native = r.bank(flat) === routines.text.bank ? (flat & 0x3fff) | 0x4000 : -flat;
|
|
compared++;
|
|
if (read(sentinel) !== 0x42 || original !== native)
|
|
failures.push({ trainer, roll, which, original: original.toString(16), native: native.toString(16) });
|
|
}
|
|
}
|
|
reference.dispose();
|
|
return { revision: romRevision(r), compared, failures: failures.slice(0, 8), failed: failures.length };
|
|
},
|
|
{ a, routines, revision },
|
|
);
|
|
expect(result.revision).toBe(revision);
|
|
expect(result.failures, JSON.stringify(result)).toEqual([]);
|
|
expect(result.compared).toBe(70 * 6 * 3);
|
|
});
|