import { test, expect } from "@playwright/test"; import { existsSync } from "node:fs"; import { C } from "../src/game/battle/constants"; import { romPath, symbolsPath, loadSymbols, openReference } from "./reference-fixture"; test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs the private ROM and pinned reference rebuild."); test("Metal Powder matches the original species check and byte overflow", async ({ page }) => { const { at, routine } = loadSymbols(); const addresses = { command: routine("DittoMetalPowder"), romBank: at("hROMBank"), turn: at("hBattleTurn"), curBattleMon: at("wCurBattleMon"), partySpecies: at("wPartyMon1Species"), enemySpecies: at("wTempEnemyMonSpecies"), playerItem: at("wBattleMonItem"), enemyItem: at("wEnemyMonItem"), }; await openReference(page); const differences = await page.evaluate(async ({ addresses, ditto, powder }) => { const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts"); const { CrystalAssets } = await import("/src/game/rom/assets.ts"); const { Battle } = await import("/src/game/battle/battle.ts"); const file = (document.querySelector("#reference-fixture") as HTMLInputElement).files![0]; const rom = new Uint8Array(await file.arrayBuffer()); const assets = new CrystalAssets(rom); const reference = new BinjgbEmulator(); await reference.load(rom, document.createElement("canvas")); const write = (address: number, value: number) => reference.writeMemory(address, value & 255); const read = (address: number) => reference.readMemory(address); const failures: unknown[] = []; let compared = 0; write(0xff70, 1); write(addresses.curBattleMon, 0); for (const turn of [0, 1]) for (const species of [ditto, 25]) for (const item of [powder, 0]) for (const attack of [1, 2, 101, 255]) for (const defense of [1, 2, 100, 169, 170, 171, 172, 200, 254, 255]) { const battle = new Battle({ data: assets.pokemon, party: [{ species }] as never, pokedex: { seen: new Set(), caught: new Set() }, playerId: 1, playerName: "CHRIS", timeOfDay: 1, text: () => ({}) as never, random: () => 0, }); const internal = battle as unknown as { enemySpecies: number; dittoMetalPowder(side: unknown, attack: number, defense: number): number[]; }; internal.enemySpecies = species; const target = turn ? battle.player : battle.enemy; target.mon = { species: 25, item } as never; // transformed battle species const native = internal.dittoMetalPowder(target, attack, defense); write(addresses.turn, turn); write(addresses.partySpecies, species); write(addresses.enemySpecies, species); write(addresses.playerItem, item); write(addresses.enemyItem, item); const { bank, address } = addresses.command; // DI; SP; ROM bank; BC; call the unmodified routine; save B/C; // sentinel in audio RAM (no interrupts); loop. const program = [ 0xf3, 0x31, 0xfe, 0xc7, 0x3e, bank, 0xe0, addresses.romBank & 255, 0xea, 0x00, 0x20, 0x06, attack, 0x0e, defense, 0xcd, address & 255, address >> 8, 0x78, 0xea, 0xe0, 0xc0, 0x79, 0xea, 0xe1, 0xc0, 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe, ]; write(0xc0f0, 0); program.forEach((byte, i) => write(0xc000 + i, byte)); reference.setProgramCounter(0xc000); for (let frame = 0; frame < 60 && read(0xc0f0) !== 0x42; frame++) reference.advanceFrames(1); if (read(0xc0f0) !== 0x42) throw new Error("DittoMetalPowder did not return"); const original = [read(0xc0e0), read(0xc0e1)]; if (original.some((value, i) => value !== native[i])) failures.push({ turn, species, item, attack, defense, original, native }); compared++; } reference.dispose(); return { compared, failures }; }, { addresses, ditto: C.DITTO, powder: C.METAL_POWDER }); expect(differences.compared).toBe(320); expect(differences.failures).toEqual([]); });