This commit is contained in:
1 parent
2127f5f459
commit
8b80d05ca2
56 files changed
+6910
-1
No files matched your search
@@ -0,0 +1,208 @@
|
||||
import { RomReader } from "../rom/reader";
|
||||
import { SYMBOLS } from "../rom/profile";
|
||||
import { readText } from "./text";
|
||||
|
||||
export interface ScriptState {
|
||||
events: Set<number>;
|
||||
flags: Set<number>;
|
||||
variables: Record<number, number>;
|
||||
}
|
||||
export interface ScriptResult extends ScriptState {
|
||||
pages: string[];
|
||||
facePlayer: boolean;
|
||||
turns: { object: number; direction: number }[];
|
||||
}
|
||||
export class UnsupportedScript extends Error {
|
||||
constructor(
|
||||
readonly offset: number,
|
||||
readonly opcode: number,
|
||||
) {
|
||||
super(
|
||||
`Script command 0x${opcode.toString(16)} at 0x${offset.toString(16)} is not ported.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** A native interpreter for the game's own event format, NOT GB assembly.
|
||||
* Evaluate on a private draft. Unknown commands abort the whole interaction;
|
||||
* callers commit only after successful completion and dialogue acknowledgement. */
|
||||
export function runScript(
|
||||
reader: RomReader,
|
||||
start: number,
|
||||
state: ScriptState,
|
||||
name: string,
|
||||
): ScriptResult {
|
||||
const result: ScriptResult = {
|
||||
events: new Set(state.events),
|
||||
flags: new Set(state.flags),
|
||||
variables: { ...state.variables },
|
||||
pages: [],
|
||||
facePlayer: false,
|
||||
turns: [],
|
||||
};
|
||||
const stack: number[] = [];
|
||||
let p = start,
|
||||
value = 0;
|
||||
const byte = () => reader.byte(p++);
|
||||
const word = () => {
|
||||
const value = reader.word(p);
|
||||
p += 2;
|
||||
return value;
|
||||
};
|
||||
const near = () => {
|
||||
const value = reader.near(p, reader.bank(p));
|
||||
p += 2;
|
||||
return value;
|
||||
};
|
||||
const far = () => {
|
||||
const value = reader.far(p);
|
||||
p += 3;
|
||||
return value;
|
||||
};
|
||||
const text = (address: number) => {
|
||||
const content = readText(reader, address, name);
|
||||
if (content) result.pages.push(...content.split("\n\n"));
|
||||
if (result.pages.length > 64) throw new Error("Too many dialogue pages.");
|
||||
};
|
||||
for (let steps = 0; steps < 2048; steps++) {
|
||||
const offset = p,
|
||||
opcode = byte();
|
||||
switch (opcode) {
|
||||
case 0x00: {
|
||||
const address = near();
|
||||
stack.push(p);
|
||||
p = address;
|
||||
break;
|
||||
}
|
||||
case 0x01: {
|
||||
const address = far();
|
||||
stack.push(p);
|
||||
p = address;
|
||||
break;
|
||||
}
|
||||
case 0x03:
|
||||
p = near();
|
||||
break;
|
||||
case 0x04:
|
||||
p = far();
|
||||
break;
|
||||
case 0x06:
|
||||
case 0x07:
|
||||
case 0x0a:
|
||||
case 0x0b: {
|
||||
const compared = byte(),
|
||||
address = near();
|
||||
const match =
|
||||
opcode === 6
|
||||
? value === compared
|
||||
: opcode === 7
|
||||
? value !== compared
|
||||
: opcode === 10
|
||||
? value > compared
|
||||
: value < compared;
|
||||
if (match) p = address;
|
||||
break;
|
||||
}
|
||||
case 0x08:
|
||||
case 0x09: {
|
||||
const address = near();
|
||||
if (Boolean(value) === (opcode === 9)) p = address;
|
||||
break;
|
||||
}
|
||||
case 0x0c:
|
||||
case 0x0d: {
|
||||
const index = word();
|
||||
if (index >= 52) throw new Error("Invalid standard script.");
|
||||
if (opcode === 0x0d) stack.push(p);
|
||||
p = reader.far(SYMBOLS.standardScripts + index * 3);
|
||||
break;
|
||||
}
|
||||
case 0x15:
|
||||
value = byte();
|
||||
break;
|
||||
case 0x16:
|
||||
value = (value + byte()) & 255;
|
||||
break;
|
||||
case 0x31:
|
||||
value = Number(result.events.has(word()));
|
||||
break;
|
||||
case 0x32:
|
||||
result.events.delete(word());
|
||||
break;
|
||||
case 0x33:
|
||||
result.events.add(word());
|
||||
break;
|
||||
case 0x34:
|
||||
value = Number(result.flags.has(word()));
|
||||
break;
|
||||
case 0x35:
|
||||
result.flags.delete(word());
|
||||
break;
|
||||
case 0x36:
|
||||
result.flags.add(word());
|
||||
break;
|
||||
case 0x47:
|
||||
case 0x49:
|
||||
case 0x54:
|
||||
case 0x55:
|
||||
break; // handled by native dialogue presentation
|
||||
case 0x4b:
|
||||
text(far());
|
||||
break;
|
||||
case 0x4c:
|
||||
text(near());
|
||||
break;
|
||||
case 0x51:
|
||||
result.facePlayer = true;
|
||||
text(near());
|
||||
return result;
|
||||
case 0x52:
|
||||
text(far());
|
||||
return result;
|
||||
case 0x53:
|
||||
text(near());
|
||||
return result;
|
||||
case 0x6b:
|
||||
result.facePlayer = true;
|
||||
break;
|
||||
case 0x6d: {
|
||||
const sprite = byte();
|
||||
result.variables[sprite] = byte();
|
||||
break;
|
||||
}
|
||||
case 0x76:
|
||||
result.turns.push({ object: byte(), direction: byte() });
|
||||
break;
|
||||
// Audio and delays are presentation events. Native sound sequencing is a
|
||||
// separate pending subsystem; consuming their operands keeps data aligned.
|
||||
case 0x7f:
|
||||
case 0x85:
|
||||
word();
|
||||
break;
|
||||
case 0x81:
|
||||
word();
|
||||
byte();
|
||||
break;
|
||||
case 0x82:
|
||||
case 0x83:
|
||||
case 0x86:
|
||||
case 0x87:
|
||||
case 0x88:
|
||||
break;
|
||||
case 0x8b:
|
||||
byte();
|
||||
break;
|
||||
case 0x90:
|
||||
case 0x91:
|
||||
if (!stack.length) return result;
|
||||
p = stack.pop()!;
|
||||
break;
|
||||
case 0x93:
|
||||
return result;
|
||||
default:
|
||||
throw new UnsupportedScript(offset, opcode);
|
||||
}
|
||||
if (stack.length > 16) throw new Error("Script call depth exceeded.");
|
||||
}
|
||||
throw new Error("Script instruction budget exceeded.");
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { RomReader } from "../rom/reader";
|
||||
|
||||
const CHARACTERS: Record<number, string> = {
|
||||
0x1f: " ",
|
||||
0x22: "\n",
|
||||
0x25: "",
|
||||
0x49: "MOM",
|
||||
0x4a: "Pokémon",
|
||||
0x4b: "\n",
|
||||
0x4c: "\n",
|
||||
0x4e: "\n",
|
||||
0x4f: "\n",
|
||||
0x51: "\n\n",
|
||||
0x53: "???",
|
||||
0x54: "POKé",
|
||||
0x55: "\n",
|
||||
0x56: "……",
|
||||
0x5b: "PC",
|
||||
0x5c: "TM",
|
||||
0x5d: "TRAINER",
|
||||
0x5e: "ROCKET",
|
||||
0x70: "PO",
|
||||
0x71: "Ké",
|
||||
0x72: "“",
|
||||
0x73: "”",
|
||||
0x74: "·",
|
||||
0x75: "…",
|
||||
0x7f: " ",
|
||||
0x9a: "(",
|
||||
0x9b: ")",
|
||||
0x9c: ":",
|
||||
0x9d: ";",
|
||||
0x9e: "[",
|
||||
0x9f: "]",
|
||||
0xc0: "Ä",
|
||||
0xc1: "Ö",
|
||||
0xc2: "Ü",
|
||||
0xc3: "ä",
|
||||
0xc4: "ö",
|
||||
0xc5: "ü",
|
||||
0xd0: "'d",
|
||||
0xd1: "'l",
|
||||
0xd2: "'m",
|
||||
0xd3: "'r",
|
||||
0xd4: "'s",
|
||||
0xd5: "'t",
|
||||
0xd6: "'v",
|
||||
0xdf: "←",
|
||||
0xe0: "'",
|
||||
0xe1: "POKé",
|
||||
0xe2: "MON",
|
||||
0xe3: "-",
|
||||
0xe6: "?",
|
||||
0xe7: "!",
|
||||
0xe8: ".",
|
||||
0xe9: "&",
|
||||
0xea: "é",
|
||||
0xeb: "→",
|
||||
0xec: "▷",
|
||||
0xed: "▶",
|
||||
0xee: "▼",
|
||||
0xef: "♂",
|
||||
0xf0: "¥",
|
||||
0xf1: "×",
|
||||
0xf2: ".",
|
||||
0xf3: "/",
|
||||
0xf4: ",",
|
||||
0xf5: "♀",
|
||||
};
|
||||
|
||||
export class UnsupportedText extends Error {
|
||||
constructor(
|
||||
readonly offset: number,
|
||||
readonly opcode: number,
|
||||
) {
|
||||
super(
|
||||
`Text command 0x${opcode.toString(16)} at 0x${offset.toString(16)} is not ported.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function readText(
|
||||
reader: RomReader,
|
||||
start: number,
|
||||
playerName: string,
|
||||
depth = 0,
|
||||
): string {
|
||||
if (depth > 8) throw new Error("Text calls exceed the recursion limit.");
|
||||
let p = start,
|
||||
output = "";
|
||||
for (let count = 0; count < 8192; count++) {
|
||||
const commandOffset = p,
|
||||
command = reader.byte(p++);
|
||||
if (command === 0x50) return output.trim();
|
||||
if (command === 0x16) {
|
||||
output += readText(
|
||||
reader,
|
||||
reader.near(p, reader.byte(p + 2)),
|
||||
playerName,
|
||||
depth + 1,
|
||||
);
|
||||
p += 3;
|
||||
continue;
|
||||
}
|
||||
if (command !== 0) throw new UnsupportedText(commandOffset, command);
|
||||
let ended = false;
|
||||
for (let length = 0; length < 8192; length++) {
|
||||
const offset = p,
|
||||
code = reader.byte(p++);
|
||||
if (code === 0x57 || code === 0x58) return output.trim();
|
||||
if (code === 0x50) {
|
||||
ended = true;
|
||||
break;
|
||||
}
|
||||
if (code === 0x52 || code === 0x14) output += playerName;
|
||||
else if (code >= 0x80 && code <= 0x99)
|
||||
output += String.fromCharCode(65 + code - 0x80);
|
||||
else if (code >= 0xa0 && code <= 0xb9)
|
||||
output += String.fromCharCode(97 + code - 0xa0);
|
||||
else if (code >= 0xf6) output += String(code - 0xf6);
|
||||
else if (CHARACTERS[code] !== undefined) output += CHARACTERS[code];
|
||||
else throw new UnsupportedText(offset, code);
|
||||
}
|
||||
if (!ended) throw new Error("Unterminated text.");
|
||||
}
|
||||
throw new Error("Text command limit exceeded.");
|
||||
}
|
||||
Reference in new issue
Block a user