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.");
|
||||
}
|
||||
Reference in new issue
Block a user