The European releases keep these routines' instructions and change their operands: where the count and the ID go, which tiles the ID line draws, where the player's name follows on the Diploma. game/rom/code.ts reads those operands from the cartridge (and refuses a routine whose instructions differ). The credits read the copyright's string from ParseCredits (six more strings precede it in the releases) and use spill tables fitted to each release's credits on the cartridge, with Spanish's one group that spills otherwise while B is held. The reference specs for these screens now pass against all four releases, as every other reference spec that applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
2.8 KiB
TypeScript
62 lines
2.8 KiB
TypeScript
import type { RomReader } from "./reader";
|
|
import { WRAM } from "./profile";
|
|
|
|
/** One step of an instruction pattern: an opcode byte that must match, or
|
|
* an operand to read ("b" a byte, "w" a little-endian word). */
|
|
type Step = number | "b" | "w";
|
|
|
|
/** An ID line: ld hl, coord; three tiles (ld a / ld [hli], a twice, then
|
|
* ld [hl]); ld hl, the number's coord. */
|
|
export const ID_LINE: readonly Step[] = [0x21, "w", 0x3e, "b", 0x22, 0x3e, "b", 0x22, 0x36, "b", 0x21, "w"];
|
|
|
|
/** The operands of the instructions at `address`, which must follow
|
|
* `pattern`. The European releases keep many routines' instructions and
|
|
* change only their operands (where a string or a number goes, which tiles a
|
|
* line draws), so the port reads those operands from the cartridge. Another
|
|
* instruction there is logic the port has not ported for that cartridge. */
|
|
export function operands(r: RomReader, address: number, pattern: readonly Step[]): number[] {
|
|
const found = match(r, address, pattern);
|
|
if (!found) throw new Error(`The routine at 0x${address.toString(16)} differs on this cartridge; it is not ported yet.`);
|
|
return found.values;
|
|
}
|
|
|
|
/** The pattern's operands at `address` and where its instructions end, or
|
|
* undefined when the instructions there are others. */
|
|
export function match(r: RomReader, address: number, pattern: readonly Step[]): { values: number[]; end: number } | undefined {
|
|
const values: number[] = [];
|
|
let p = address;
|
|
for (const step of pattern) {
|
|
if (step === "b") values.push(r.byte(p++));
|
|
else if (step === "w") {
|
|
values.push(r.word(p));
|
|
p += 2;
|
|
} else if (r.byte(p++) !== step) return undefined;
|
|
}
|
|
return { values, end: p };
|
|
}
|
|
|
|
/** The operands of the first place in `from` .. `from + limit` where the
|
|
* pattern's instructions are, for a line a routine draws without a label of
|
|
* its own. */
|
|
export function findOperands(r: RomReader, from: number, limit: number, pattern: readonly Step[]): number[] {
|
|
return findMatch(r, from, limit, pattern).values;
|
|
}
|
|
export function findMatch(r: RomReader, from: number, limit: number, pattern: readonly Step[]) {
|
|
for (let p = from; p < from + limit; p++) {
|
|
const found = match(r, p, pattern);
|
|
if (found) return found;
|
|
}
|
|
throw new Error(`The routine at 0x${from.toString(16)} differs on this cartridge; it is not ported yet.`);
|
|
}
|
|
|
|
/** A pointer operand of code in `routine`'s bank, as a ROM offset. */
|
|
export const nearPointer = (r: RomReader, routine: number, pointer: number) =>
|
|
pointer < 0x4000 ? pointer : r.bank(routine) * 0x4000 + pointer - 0x4000;
|
|
|
|
/** hlcoord's tile position from a wTilemap address. */
|
|
export function tileCoord(address: number): [number, number] {
|
|
const offset = address - WRAM.wTilemap;
|
|
if (offset < 0 || offset >= 20 * 18) throw new Error("A screen position lies outside the tilemap.");
|
|
return [offset % 20, Math.floor(offset / 20)];
|
|
}
|