romdevtools 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +139 -494
- package/CHANGELOG.md +41 -3
- package/package.json +2 -2
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +170 -1
- package/src/http/skill-doc.js +1 -1
- package/src/mcp/tools/input-layout.js +10 -0
- package/src/mcp/tools/input.js +26 -2
- package/src/mcp/tools/playtest.js +17 -2
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +155 -6
- package/src/platforms/atari2600/MENTAL_MODEL.md +37 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +36 -0
- package/src/platforms/c64/MENTAL_MODEL.md +83 -6
- package/src/platforms/gb/MENTAL_MODEL.md +56 -0
- package/src/platforms/gba/MENTAL_MODEL.md +57 -3
- package/src/platforms/gba/lib/arm-archives/libc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libgcc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libnosys.a +0 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +21 -0
- package/src/platforms/genesis/MENTAL_MODEL.md +19 -0
- package/src/platforms/genesis/lib/c/libc.a +0 -0
- package/src/platforms/genesis/lib/c/libgcc.a +0 -0
- package/src/platforms/genesis/lib/c/libm.a +0 -0
- package/src/platforms/gg/MENTAL_MODEL.md +24 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +33 -7
- package/src/platforms/msx/MENTAL_MODEL.md +27 -0
- package/src/platforms/nes/MENTAL_MODEL.md +35 -0
- package/src/platforms/sms/MENTAL_MODEL.md +51 -0
- package/src/platforms/snes/MENTAL_MODEL.md +21 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +43 -0
- package/src/playtest/playtest.js +48 -0
- package/examples/msx/catch_game/_verify.mjs +0 -93
- package/examples/pce/catch_game/_verify.mjs +0 -75
package/src/host/LibretroHost.js
CHANGED
|
@@ -138,6 +138,51 @@ export const PLATFORM_VIRTUAL_EXT = {
|
|
|
138
138
|
};
|
|
139
139
|
import { RETRO_DEVICE_JOYPAD } from "./retroConstants.js";
|
|
140
140
|
|
|
141
|
+
// C64 controller→keyboard map (the Batocera/RetroDeck model: a CONTROLLER alone
|
|
142
|
+
// plays C64 — no physical keyboard needed — by mapping spare buttons/stick to
|
|
143
|
+
// the C64 keyboard keys games need at setup screens). Keyed by the button NAME
|
|
144
|
+
// setInput receives (libretro + spatial names + the playtest right-stick virtual
|
|
145
|
+
// buttons c64_f1..f7). The joystick itself (d-pad + Fire) is NOT here — those
|
|
146
|
+
// stay a real joypad. Everything here is routed to the key matrix instead.
|
|
147
|
+
// B / south = Fire → joystick (handled as joypad, not a key)
|
|
148
|
+
// X / west = Space L2 = Run/Stop R2 / start = Return
|
|
149
|
+
// R-stick = F1/F3/F5/F7 (playtest emits c64_f1..f7 from the right stick)
|
|
150
|
+
const C64_BUTTON_KEYS = {
|
|
151
|
+
west: "space", y: "space",
|
|
152
|
+
l2: "run/stop",
|
|
153
|
+
r2: "return", start: "return",
|
|
154
|
+
c64_f1: "f1", c64_f3: "f3", c64_f5: "f5", c64_f7: "f7",
|
|
155
|
+
north: "f1", x: "f1", // also expose F1 on the top face (1-player select is the #1 need)
|
|
156
|
+
};
|
|
157
|
+
// The buttons that ARE the C64 joystick (pass through to the joypad mask):
|
|
158
|
+
const C64_JOY_BUTTONS = new Set(["up", "down", "left", "right", "b", "south", "a", "east"]);
|
|
159
|
+
|
|
160
|
+
// C64 keyboard matrix: key name (lowercase) → [row, col] in the 8×8 matrix the
|
|
161
|
+
// KERNAL scans via CIA1. Drives romdev_key_matrix() in the VICE core. Values
|
|
162
|
+
// taken from VICE's own C64 positional keymap (data/C64/sdl_pos_uk.vkm).
|
|
163
|
+
// Covers the keys RE/startup flows need; extend as needed.
|
|
164
|
+
const C64_KEY_MATRIX = {
|
|
165
|
+
// function keys (the common "1 player / start" selectors)
|
|
166
|
+
f1: [0, 4], f3: [0, 5], f5: [0, 6], f7: [0, 3],
|
|
167
|
+
// editing / control
|
|
168
|
+
return: [0, 1], enter: [0, 1], space: [7, 4], del: [0, 0], backspace: [0, 0],
|
|
169
|
+
"run/stop": [7, 7], runstop: [7, 7], stop: [7, 7],
|
|
170
|
+
ctrl: [7, 2], cbm: [7, 5], commodore: [7, 5],
|
|
171
|
+
lshift: [1, 7], rshift: [6, 4], home: [6, 3], clr: [6, 3],
|
|
172
|
+
// cursor keys (C64 has DOWN + RIGHT; UP/LEFT are the shifted forms — use
|
|
173
|
+
// shift+down / shift+right for those)
|
|
174
|
+
"crsr-down": [0, 7], "crsr-right": [0, 2], down: [0, 7], right: [0, 2],
|
|
175
|
+
// digits
|
|
176
|
+
"0": [4, 3], "1": [7, 0], "2": [7, 3], "3": [1, 0], "4": [1, 3],
|
|
177
|
+
"5": [2, 0], "6": [2, 3], "7": [3, 0], "8": [3, 3], "9": [4, 0],
|
|
178
|
+
// letters
|
|
179
|
+
a: [1, 2], b: [3, 4], c: [2, 4], d: [2, 2], e: [1, 6], f: [2, 5],
|
|
180
|
+
g: [3, 2], h: [3, 5], i: [4, 1], j: [4, 2], k: [4, 5], l: [5, 2],
|
|
181
|
+
m: [4, 4], n: [4, 7], o: [4, 6], p: [5, 1], q: [7, 6], r: [2, 1],
|
|
182
|
+
s: [1, 5], t: [2, 6], u: [3, 6], v: [3, 7], w: [1, 1], x: [2, 7],
|
|
183
|
+
y: [3, 1], z: [1, 4],
|
|
184
|
+
};
|
|
185
|
+
|
|
141
186
|
export class LibretroHost {
|
|
142
187
|
/**
|
|
143
188
|
* @param {Object} [opts]
|
|
@@ -570,12 +615,55 @@ export class LibretroHost {
|
|
|
570
615
|
/** @param {import("./types.js").FrameInput} input */
|
|
571
616
|
setInput(input) {
|
|
572
617
|
const platform = this.status.platform ?? undefined;
|
|
618
|
+
// C64: route the keyboard-mapped controller buttons (Space/Run-Stop/Return/
|
|
619
|
+
// F1-F7) to the key matrix so a CONTROLLER alone can play — the
|
|
620
|
+
// Batocera/RetroDeck model. The joystick bits (d-pad + Fire) still flow to
|
|
621
|
+
// the joypad mask below. Applies to BOTH playtest and the agent's setInput.
|
|
622
|
+
if (platform === "c64" && this.mod && typeof this.mod._romdev_key_matrix === "function") {
|
|
623
|
+
this._applyC64ButtonKeys(input.ports[0] || {});
|
|
624
|
+
}
|
|
573
625
|
for (let port = 0; port < this.state.inputPorts.length; port++) {
|
|
574
|
-
const portInput = input.ports[port];
|
|
626
|
+
const portInput = this._c64StripKeyButtons(input.ports[port], platform);
|
|
575
627
|
this.state.inputPorts[port][0] = portInputToMask(portInput, platform);
|
|
576
628
|
}
|
|
577
629
|
}
|
|
578
630
|
|
|
631
|
+
/** C64: press/release matrix keys for the held keyboard-mapped buttons on a
|
|
632
|
+
* port, edge-tracked so a key releases when its button is no longer held. */
|
|
633
|
+
_applyC64ButtonKeys(portInput) {
|
|
634
|
+
const held = this._c64HeldKeys || (this._c64HeldKeys = new Set());
|
|
635
|
+
const want = new Set();
|
|
636
|
+
for (const [btn, key] of Object.entries(C64_BUTTON_KEYS)) {
|
|
637
|
+
if (portInput[btn] === true) want.add(key);
|
|
638
|
+
}
|
|
639
|
+
// press newly-wanted, release no-longer-wanted
|
|
640
|
+
for (const key of want) {
|
|
641
|
+
if (!held.has(key)) {
|
|
642
|
+
const pos = C64_KEY_MATRIX[key];
|
|
643
|
+
if (pos) this.mod._romdev_key_matrix(pos[0], pos[1], 1);
|
|
644
|
+
held.add(key);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
for (const key of [...held]) {
|
|
648
|
+
if (!want.has(key)) {
|
|
649
|
+
const pos = C64_KEY_MATRIX[key];
|
|
650
|
+
if (pos) this.mod._romdev_key_matrix(pos[0], pos[1], 0);
|
|
651
|
+
held.delete(key);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Strip the C64 keyboard-mapped buttons out of a port so they don't ALSO
|
|
657
|
+
* press a joystick direction/fire (only the real joystick bits remain). */
|
|
658
|
+
_c64StripKeyButtons(portInput, platform) {
|
|
659
|
+
if (platform !== "c64" || !portInput) return portInput;
|
|
660
|
+
const out = {};
|
|
661
|
+
for (const k of Object.keys(portInput)) {
|
|
662
|
+
if (C64_JOY_BUTTONS.has(k)) out[k] = portInput[k];
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
|
|
579
667
|
/** @param {string} name */
|
|
580
668
|
saveState(name) {
|
|
581
669
|
const snapshot = this.serializeState();
|
|
@@ -802,6 +890,87 @@ export class LibretroHost {
|
|
|
802
890
|
}
|
|
803
891
|
}
|
|
804
892
|
|
|
893
|
+
// ── C64 keyboard + joyport (VICE core only) ──────────────────────────
|
|
894
|
+
// C64 games need KEYBOARD input (F1 = 1 player, RUN/STOP, SPACE/RETURN at
|
|
895
|
+
// setup screens) before joystick gameplay — joystick alone can't pass them.
|
|
896
|
+
// The VICE core exports romdev_key_matrix / romdev_kbdbuf_feed /
|
|
897
|
+
// romdev_joyport_* (see scripts/patches/vice-romdev-memory-regions.patch).
|
|
898
|
+
|
|
899
|
+
/** True if the loaded core exposes C64 keyboard injection (VICE only). */
|
|
900
|
+
keyboardSupported() {
|
|
901
|
+
const mod = this.mod;
|
|
902
|
+
return !!(mod && typeof mod._romdev_key_matrix === "function");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Press (and optionally auto-release) a single C64 key by name. Drives the
|
|
907
|
+
* C64 8×8 key matrix directly via the core. Held for `frames` then released.
|
|
908
|
+
* @param {string} key a name from C64_KEY_MATRIX (case-insensitive)
|
|
909
|
+
* @param {number} [frames] frames to hold before release (default 4)
|
|
910
|
+
* @returns {{key:string, row:number, col:number, frames:number}}
|
|
911
|
+
*/
|
|
912
|
+
pressC64Key(key, frames = 4) {
|
|
913
|
+
const mod = this._needMod();
|
|
914
|
+
if (typeof mod._romdev_key_matrix !== "function") {
|
|
915
|
+
throw new Error("this core build does not expose C64 keyboard input (C64/VICE only).");
|
|
916
|
+
}
|
|
917
|
+
const pos = C64_KEY_MATRIX[String(key).toLowerCase()];
|
|
918
|
+
if (!pos) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`unknown C64 key '${key}'. Known: ${Object.keys(C64_KEY_MATRIX).join(", ")}.`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const [row, col] = pos;
|
|
924
|
+
mod._romdev_key_matrix(row, col, 1); // press
|
|
925
|
+
this.stepFrames(Math.max(1, frames | 0));
|
|
926
|
+
mod._romdev_key_matrix(row, col, 0); // release
|
|
927
|
+
this.stepFrames(1);
|
|
928
|
+
return { key: String(key).toLowerCase(), row, col, frames: Math.max(1, frames | 0) };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Feed a PETSCII string into the C64 kernal keyboard buffer (for typing
|
|
933
|
+
* LOAD/RUN/filenames). `\r` (or `\n`) becomes RETURN. Non-blocking — the
|
|
934
|
+
* kernal drains it as the screen editor runs, so step frames after.
|
|
935
|
+
* @param {string} text
|
|
936
|
+
* @returns {number} kbdbuf_feed's result (>=0 ok)
|
|
937
|
+
*/
|
|
938
|
+
typeC64Text(text) {
|
|
939
|
+
const mod = this._needMod();
|
|
940
|
+
if (typeof mod._romdev_kbdbuf_feed !== "function") {
|
|
941
|
+
throw new Error("this core build does not expose C64 text input (C64/VICE only).");
|
|
942
|
+
}
|
|
943
|
+
const s = String(text).replace(/\n/g, "\r");
|
|
944
|
+
const bytes = Buffer.from(s + "\0", "latin1");
|
|
945
|
+
const ptr = mod._malloc(bytes.length);
|
|
946
|
+
try {
|
|
947
|
+
mod.HEAPU8.set(bytes, ptr);
|
|
948
|
+
return mod._romdev_kbdbuf_feed(ptr) | 0;
|
|
949
|
+
} finally {
|
|
950
|
+
mod._free(ptr);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** Get the active C64 joystick port (1 or 2) the RetroPad drives. */
|
|
955
|
+
getC64JoyPort() {
|
|
956
|
+
const mod = this._needMod();
|
|
957
|
+
if (typeof mod._romdev_joyport_get !== "function") {
|
|
958
|
+
throw new Error("this core build does not expose C64 joyport (C64/VICE only).");
|
|
959
|
+
}
|
|
960
|
+
return mod._romdev_joyport_get() | 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Set the active C64 joystick port (1 or 2). Default is 2 (most games). */
|
|
964
|
+
setC64JoyPort(port) {
|
|
965
|
+
const mod = this._needMod();
|
|
966
|
+
if (typeof mod._romdev_joyport_set !== "function") {
|
|
967
|
+
throw new Error("this core build does not expose C64 joyport (C64/VICE only).");
|
|
968
|
+
}
|
|
969
|
+
if (port !== 1 && port !== 2) throw new Error("C64 joyport must be 1 or 2.");
|
|
970
|
+
mod._romdev_joyport_set(port | 0);
|
|
971
|
+
return port | 0;
|
|
972
|
+
}
|
|
973
|
+
|
|
805
974
|
reset() {
|
|
806
975
|
const mod = this._needMod();
|
|
807
976
|
mod._retro_reset();
|
package/src/http/skill-doc.js
CHANGED
|
@@ -112,7 +112,7 @@ export function sanitizeForSkillChannel(text) {
|
|
|
112
112
|
// there's no session-id header / reconnect / "connect your agent" step here;
|
|
113
113
|
// the skillPreamble already gives the skill-appropriate intro + prereq).
|
|
114
114
|
if (/Mcp-Session-Id|re-?initialize|session not found|MCP client|MCP connection|MCP sessions/i.test(line)) continue;
|
|
115
|
-
if (/
|
|
115
|
+
if (/connect your (agent|coding agent)|restart its MCP connection|restart your MCP|your MCP client should/i.test(line)) continue;
|
|
116
116
|
|
|
117
117
|
let l = line
|
|
118
118
|
// section header that frames romdev as "a server you connect to"
|
|
@@ -74,6 +74,16 @@ const HARDWARE_LAYOUTS = {
|
|
|
74
74
|
readSequence: "Bits 0-4 are Up/Down/Left/Right/Fire, active-low.",
|
|
75
75
|
bitOrder: ["Up", "Down", "Left", "Right", "Fire"],
|
|
76
76
|
faceButtons: FACE_BUTTON_MAP.c64,
|
|
77
|
+
// The C64 needs more than joystick: many games use KEYBOARD setup screens
|
|
78
|
+
// (F1=1 player, RUN/STOP, SPACE/RETURN) before joystick gameplay starts.
|
|
79
|
+
keyboard: {
|
|
80
|
+
note: "C64 games often need keyboard input to START (F1 to pick 1 player, fire/RETURN to begin). Joystick alone can't pass these. Use input({op:'pressKey', key}) for a single key, input({op:'typeText', text}) to type a string (LOAD/RUN/filenames).",
|
|
81
|
+
keys: ["f1", "f3", "f5", "f7", "return", "space", "run/stop", "ctrl", "cbm", "home", "down", "right", "lshift", "rshift", "0-9", "a-z"],
|
|
82
|
+
},
|
|
83
|
+
joyport: {
|
|
84
|
+
note: "The RetroPad drives ONE C64 joystick port at a time. Default is port 2 (most games). Use input({op:'joyport'}) to read it, input({op:'joyport', joyport:1|2}) to switch.",
|
|
85
|
+
default: 2,
|
|
86
|
+
},
|
|
77
87
|
},
|
|
78
88
|
sms: {
|
|
79
89
|
register: "I/O port $DC (controllers, read via `in a,($DC)`) and $DD (port 2 high bits + reset)",
|
package/src/mcp/tools/input.js
CHANGED
|
@@ -213,17 +213,21 @@ export function registerInputTools(server, z, sessionKey) {
|
|
|
213
213
|
"when ITS code polls; re-apply immediately before the consuming stepFrames and verify via the held-buttons RAM " +
|
|
214
214
|
"byte, not this echo.",
|
|
215
215
|
{
|
|
216
|
-
op: z.enum(["set", "press", "sequence", "navigate", "layout"]).describe("set/hold buttons; press one button; run a sequence; navigate a menu;
|
|
216
|
+
op: z.enum(["set", "press", "sequence", "navigate", "layout", "pressKey", "typeText", "joyport"]).describe("set/hold buttons; press one button; run a sequence; navigate a menu; get the input layout. C64-ONLY: pressKey (press a C64 KEYBOARD key — F1/Return/Space/Run-Stop — many C64 games need these to start, joystick alone can't); typeText (feed a string into the C64 keyboard buffer — LOAD/RUN/filenames); joyport (get/set which C64 joystick port the pad drives, 1 or 2; default 2)."),
|
|
217
217
|
// set
|
|
218
218
|
ports: z.array(port).min(1).max(2).optional().describe("op=set: per-port input. [{a:true,right:true}] holds A+Right on port 0."),
|
|
219
219
|
// press
|
|
220
220
|
button: z.enum(BUTTON_ENUM).optional().describe("op=press: button to press (native aliases + spatial names accepted)."),
|
|
221
|
-
frames: z.number().int().min(1).max(600).default(2).describe("op=press: frames to hold the button."),
|
|
221
|
+
frames: z.number().int().min(1).max(600).default(2).describe("op=press: frames to hold the button. op=pressKey: frames to hold the C64 key (default 4)."),
|
|
222
222
|
port: z.number().int().min(0).max(1).default(0).describe("op=press: which port (default 0)."),
|
|
223
223
|
// sequence
|
|
224
224
|
steps: z.array(z.any()).optional().describe("op=sequence: [{input:{ports:[...]}, frames}]. op=navigate: [{button, holdFrames?, maxWaitFrames?, settleFrames?}]. (Two distinct step shapes by op.)"),
|
|
225
225
|
// layout
|
|
226
226
|
platform: z.string().optional().describe("op=layout: platform id (nes, gb, snes, genesis, ...)."),
|
|
227
|
+
// C64 keyboard
|
|
228
|
+
key: z.string().optional().describe("op=pressKey (C64): key name — f1/f3/f5/f7, return, space, run/stop, a-z, 0-9, ctrl, cbm, home, down, right, lshift, rshift."),
|
|
229
|
+
text: z.string().optional().describe("op=typeText (C64): string fed into the keyboard buffer; \\r / \\n become RETURN. e.g. 'LOAD\"*\",8,1\\rRUN\\r'."),
|
|
230
|
+
joyport: z.number().int().min(1).max(2).optional().describe("op=joyport (C64): set the active joystick port (1 or 2). Omit to just GET the current port. Default is 2 (most C64 games)."),
|
|
227
231
|
},
|
|
228
232
|
safeTool(async (args) => {
|
|
229
233
|
switch (args.op) {
|
|
@@ -249,6 +253,26 @@ export function registerInputTools(server, z, sessionKey) {
|
|
|
249
253
|
if (!args.platform) throw new Error("input({op:'layout'}): `platform` is required.");
|
|
250
254
|
return jsonContent(getInputLayoutCore(args));
|
|
251
255
|
}
|
|
256
|
+
case "pressKey": {
|
|
257
|
+
if (!args.key) throw new Error("input({op:'pressKey'}): `key` is required (C64 keyboard key, e.g. 'f1', 'return', 'run/stop').");
|
|
258
|
+
const host = getHost(sessionKey);
|
|
259
|
+
const r = host.pressC64Key(args.key, args.frames ?? 4);
|
|
260
|
+
return jsonContent({ pressedKey: r.key, matrix: [r.row, r.col], frames: r.frames, frameCount: host.status.frameCount });
|
|
261
|
+
}
|
|
262
|
+
case "typeText": {
|
|
263
|
+
if (typeof args.text !== "string") throw new Error("input({op:'typeText'}): `text` is required (string fed into the C64 keyboard buffer).");
|
|
264
|
+
const host = getHost(sessionKey);
|
|
265
|
+
const rc = host.typeC64Text(args.text);
|
|
266
|
+
return jsonContent({ typed: args.text, fedResult: rc, note: "Queued into the C64 keyboard buffer — step frames so the screen editor drains it." });
|
|
267
|
+
}
|
|
268
|
+
case "joyport": {
|
|
269
|
+
const host = getHost(sessionKey);
|
|
270
|
+
if (args.joyport === undefined) {
|
|
271
|
+
return jsonContent({ joyport: host.getC64JoyPort(), note: "Active C64 joystick port. Most C64 games use port 2 (the default). Pass `joyport:1` or `joyport:2` to change it." });
|
|
272
|
+
}
|
|
273
|
+
const set = host.setC64JoyPort(args.joyport);
|
|
274
|
+
return jsonContent({ joyport: set, set: true });
|
|
275
|
+
}
|
|
252
276
|
default: throw new Error(`input: unknown op '${args.op}'`);
|
|
253
277
|
}
|
|
254
278
|
}),
|
|
@@ -126,7 +126,7 @@ export function registerPlaytestTools(server, z, sessionKey) {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const { playtest, KEYBOARD_BINDINGS_HELP } = await import("../../playtest/playtest.js");
|
|
129
|
+
const { playtest, KEYBOARD_BINDINGS_HELP, C64_BINDINGS_HELP } = await import("../../playtest/playtest.js");
|
|
130
130
|
let session;
|
|
131
131
|
try {
|
|
132
132
|
// Pass a live-host accessor so the window FOLLOWS rebuilds: runSource/
|
|
@@ -214,6 +214,7 @@ export function registerPlaytestTools(server, z, sessionKey) {
|
|
|
214
214
|
// isn't left guessing which keys drive the game. (A pad hot-plugged later
|
|
215
215
|
// is picked up automatically — this is just the at-open state.)
|
|
216
216
|
const noController = session.controllerCount === 0;
|
|
217
|
+
const isC64 = host.status?.platform === "c64";
|
|
217
218
|
return jsonContent({
|
|
218
219
|
opened: true,
|
|
219
220
|
reusedExistingWindow: false,
|
|
@@ -222,7 +223,21 @@ export function registerPlaytestTools(server, z, sessionKey) {
|
|
|
222
223
|
scale,
|
|
223
224
|
aspect,
|
|
224
225
|
controllerCount: session.controllerCount,
|
|
225
|
-
|
|
226
|
+
// C64 input is non-obvious (games need keyboard keys to START), so ALWAYS
|
|
227
|
+
// relay the controls — a controller alone IS enough (spare buttons/stick
|
|
228
|
+
// map to F1/Run-Stop/Space/Return), and the keyboard fallback covers the
|
|
229
|
+
// no-controller case. This is the Batocera/RetroDeck model.
|
|
230
|
+
...(isC64
|
|
231
|
+
? {
|
|
232
|
+
c64Controls: C64_BINDINGS_HELP,
|
|
233
|
+
tellUser:
|
|
234
|
+
"C64 game: RELAY `c64Controls` to the user. A CONTROLLER ALONE is " +
|
|
235
|
+
"enough — they do NOT need a keyboard. Most C64 games need a " +
|
|
236
|
+
"keyboard key to START (e.g. F1 for 1 player); the pad's spare " +
|
|
237
|
+
"buttons/right-stick map to those (F1/F3/F5/F7, Space, Run/Stop, " +
|
|
238
|
+
"Return). Default joystick port is 2; change with input({op:'joyport'}).",
|
|
239
|
+
}
|
|
240
|
+
: noController
|
|
226
241
|
? {
|
|
227
242
|
keyboardControls: KEYBOARD_BINDINGS_HELP,
|
|
228
243
|
tellUser:
|
|
@@ -22,6 +22,32 @@ read that platform's `platform({op:'doc', platform, name:'mental_model'})`.
|
|
|
22
22
|
The cheat DB is bundled (`romdev_game_codes`). Do **not** scan the user's disk for
|
|
23
23
|
`.cht` files — if it's not in the bundled DB, treat it as absent and RE it.
|
|
24
24
|
|
|
25
|
+
**Reading a `cheats({op:'lookup'})` hit as a RAM/code map.** Each decoded part carries an
|
|
26
|
+
`address`, a `value`, a `kind` (`ram` = a labeled variable; `code` = a labeled ROM
|
|
27
|
+
patch site, has a `compare`), and a `device` (`game-genie`/`pro-action-replay`/
|
|
28
|
+
`gameshark`/`action-replay`/`raw`). So "which byte holds magic?" → one `lookup` call.
|
|
29
|
+
Filter a long list with `filter:"health"` or `kind:"ram"`. A match is by No-Intro
|
|
30
|
+
**name/filename, NOT a verified CRC** — it's PROBABLE (a different region/revision can
|
|
31
|
+
move addresses), so confirm before patching. The cheapest confirmation is to apply it
|
|
32
|
+
and watch: `cheats({op:'apply', path, desc})` → `frame({op:'screenshot'})`.
|
|
33
|
+
|
|
34
|
+
`cheats({op:'apply'})` is non-destructive (volatile core state, the RetroArch way — the ROM
|
|
35
|
+
file is never touched; `host({op:'reset'})`/`state({op:'load'})`/`cheats({op:'clear'})` removes it). It takes a
|
|
36
|
+
matched `desc`, a raw `code`, or `loadMedia({cheats:[…]})` to seed codes BEFORE frame 0.
|
|
37
|
+
`appliedAs` reports how it went in (`ram` poke / `rom` read-intercept / `raw` device
|
|
38
|
+
code). DB coverage is 13/14 (every tier-1 system except C64); GBA cheats are
|
|
39
|
+
encrypted, so apply-only (no labeled-address map — see `mapNote`).
|
|
40
|
+
|
|
41
|
+
**Creating a NEW code — `cheats({op:'make', platform, address, value, compare?})`.** The inverse of
|
|
42
|
+
decoding: turn a byte you found (via §1 or `breakpoint`) into a shareable, verified code,
|
|
43
|
+
for ANY ROM incl. your own homebrew/WIP. A RAM cheat needs just `address`+`value`; a ROM
|
|
44
|
+
patch adds `compare` (the byte currently there). It encodes for the platform's native
|
|
45
|
+
device(s) and labels each (NES/Genesis → Game Genie; SNES → Pro Action Replay **and**
|
|
46
|
+
Game Genie; GB/GBC → Game Genie + GameShark; SMS/GG → Action Replay) plus the raw
|
|
47
|
+
`ADDR:VAL`; each carries `verified:true` (round-trips against the full DB). Systems with no
|
|
48
|
+
letter-code device (Atari 2600/7800, Lynx, GBA, C64, PC Engine, MSX) get a verified raw
|
|
49
|
+
code. Works on all 14. Nothing is ever written to a ROM file.
|
|
50
|
+
|
|
25
51
|
---
|
|
26
52
|
|
|
27
53
|
## 1. To find the RAM address of a value (score / timer / stat / HP / record-id)
|
|
@@ -50,12 +76,20 @@ usually a struct/entity array, each island one record.
|
|
|
50
76
|
The #1 trap: visible names/labels are often **pre-rendered tile GRAPHICS**, not
|
|
51
77
|
font-rendered from an ASCII string. Patching the ASCII string then does nothing.
|
|
52
78
|
|
|
53
|
-
1. `text({op:'learn'})` on the on-screen text
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
1. `text({op:'learn'})` on the on-screen text — it infers the game's char→tile-ID map
|
|
80
|
+
(games use their own encoding: Excitebike A=$0A, Mario ASCII-offset, FF sparse). Two
|
|
81
|
+
modes: ROM mode `knownStrings:[{text, offset}]` when you found the bytes; **LIVE mode
|
|
82
|
+
`fromScreen:[{text, row, col}]`** reads the tile IDs straight off the live BG map at a
|
|
83
|
+
tile position (`background({view:'map'})` shows where the text sits) — this breaks the
|
|
84
|
+
chicken-and-egg of needing the offset you're still hunting. Live mode works on every
|
|
85
|
+
tilemap platform (NES/SNES/Genesis/GB/GBC/SMS/GG/C64); atari2600/7800, lynx, gba have
|
|
86
|
+
no text nametable → ROM mode only. If `learn` reports `likelyPreRenderedGraphic:true`
|
|
87
|
+
(unique sequential tiles, no font reuse), **stop** — the text is a bitmap. Editing it
|
|
88
|
+
means changing tile pixels, not a string. Do not patch any ASCII string you found.
|
|
89
|
+
2. If it IS font-rendered: `text({op:'find', romPath, text, fontMap})` locates the string
|
|
90
|
+
(returns `fileOffset`, `prgFileOffset`, and a bank-aware `cpuAddress`+`bank` to feed
|
|
91
|
+
`disasm({target:'rom'})`; flags a likely length-prefix byte to avoid the overrun trap),
|
|
92
|
+
then `text({op:'encode', text, fontMap})` → bytes for `romPatch({op:'write'})`.
|
|
59
93
|
3. To find where a graphic/text was sourced from: on **Genesis**, `watch({on:'dma', precision:'sampled'})`
|
|
60
94
|
— drive to the screen that shows the graphic, and it reports the ROM offset(s)
|
|
61
95
|
the tiles were DMA'd from (decoded from the VDP DMA registers). Edit the tile
|
|
@@ -103,6 +137,15 @@ from a SOURCE struct rather than written in place. Don't conclude "the address
|
|
|
103
137
|
is wrong." Find the source: `memory({op:'search'})` the live value to locate the struct
|
|
104
138
|
the copy reads from, then `breakpoint({on:'write'})` on THAT.
|
|
105
139
|
|
|
140
|
+
**Precision — exact vs sampled.** The default `breakpoint({on:'write'})` is a core-level write
|
|
141
|
+
watchpoint: it returns the EXACT writing instruction's PC, captured inside the CPU write
|
|
142
|
+
path — correct even for NMI/IRQ-driven writes (the common case where a frame-sampled PC
|
|
143
|
+
is just the idle loop). On a banked mapper it reports the `bank` (NES/GB/SMS-GG) so you
|
|
144
|
+
can pass `{startAddress, bank}` to `disasm({target:'rom'})`. The lighter
|
|
145
|
+
`breakpoint({on:'write', precision:'sampled'})` (a.k.a. `watch({on:'mem'})`) steps until the byte changes
|
|
146
|
+
and returns a frame-boundary PC — a lead, not a guarantee under interrupts; use it for the
|
|
147
|
+
value timeline or when you just want the change history, and cross-check the value trace.
|
|
148
|
+
|
|
106
149
|
---
|
|
107
150
|
|
|
108
151
|
## 5b. To READ a register at an instruction — execution breakpoints (all 14)
|
|
@@ -254,6 +297,101 @@ and `input({op:'set'})` state it too.)
|
|
|
254
297
|
|
|
255
298
|
---
|
|
256
299
|
|
|
300
|
+
## 7. Authoring & verifying the byte patch
|
|
301
|
+
|
|
302
|
+
Once you know WHAT to change, the write loop is a handful of calls — no custom scripts:
|
|
303
|
+
|
|
304
|
+
- **`assembleSnippet({cpu, origin, code})`** — assemble a tiny asm chunk to raw bytes (no
|
|
305
|
+
header/linker/segments). CPUs: `6502 / 65c02 / 65816 / 68k / z80 / sm83 / gb / gbc /
|
|
306
|
+
huc6280`. **Z80 gotcha:** the sdas dialect requires `#` on immediates (`ld a,#5`, not
|
|
307
|
+
`ld a,5`).
|
|
308
|
+
- **`romPatch({op:'write', path, offset, hex, expect})`** — the splicer THE other hack tools
|
|
309
|
+
compose through. **Always pass `expect`** (the current bytes) — it refuses the write if
|
|
310
|
+
they don't match, catching a hex/dec slip or a patch authored against region A applied to
|
|
311
|
+
region B. `allowExpand` for size-changing edits.
|
|
312
|
+
- **`romPatch({op:'diff', platform, a, b})`** — mapper-aware ROM diff: reports CPU addresses
|
|
313
|
+
(NROM-128 mirrors, SNES LoROM `XX:XXXX`), per-region tallies (PRG vs CHR vs header), and
|
|
314
|
+
`tile:N` annotations on CHR changes for direct sprite-hack identification. Use it to
|
|
315
|
+
confirm a patch landed where you meant.
|
|
316
|
+
- **`disasm({target:'references', path, platform, address})`** — find every instruction that
|
|
317
|
+
references a target address, classified `call/jump/branch/read/write/use/ref` (walks the
|
|
318
|
+
vector table too). The fast "who touches this?" for a STATIC image. Limitation: direct
|
|
319
|
+
addressing only — indirect/computed jumps aren't detected (use the runtime `watch`/
|
|
320
|
+
`breakpoint` tools in §5/§5d for those).
|
|
321
|
+
- **`cart({op:'extract', path, outputDir})`** — split a ROM into standard parts (NES header/
|
|
322
|
+
prg/chr; SNES copier_header+rom+internal header; Genesis vectors/header/body; GB boot/
|
|
323
|
+
header/body) + a `manifest.json` (mapper, mirroring…). **`cart({op:'wrap'})`** is the inverse:
|
|
324
|
+
emits `wrapperSource` + `linkerConfig` ready for `build({output:'rom'})`.
|
|
325
|
+
|
|
326
|
+
Verify-before-patch: `memory({op:'write', region:'system_ram', offset, hex})` on the LIVE emulator and
|
|
327
|
+
watch the screen react — cheaper than shipping a wrong ROM patch.
|
|
328
|
+
|
|
329
|
+
## 7b. Whole-ROM rebuildable disassembly — `disasm({target:'project'})`
|
|
330
|
+
|
|
331
|
+
For a STRUCTURAL hack (new logic, not a byte poke), turn the whole ROM into a
|
|
332
|
+
re-buildable project in one call: `disasm({target:'project', path, outputDir})`. It splits
|
|
333
|
+
the ROM into regions (per-16KB bank for banked NES, per-32KB for SNES LoROM, slot0+slotX
|
|
334
|
+
for GB, one flat region for SMS/Genesis/C64/Atari), disassembles each through the CPU's
|
|
335
|
+
native objdump, then **reassembles + verifies byte-exact** against the original; any line
|
|
336
|
+
that won't reproduce faithfully heals to a `.byte`/`db` of its real bytes, so the emitted
|
|
337
|
+
`.asm` ALWAYS rebuilds (`roundTrip.allByteExact`). `readablePercent` per region tells you
|
|
338
|
+
how much came back as real instructions vs. data. Alongside the `.asm` it writes the
|
|
339
|
+
turnkey **rebuild glue**: data blobs (NES CHR-ROM → `chr.bin`; stripped Genesis/GBA/Lynx/MSX
|
|
340
|
+
cartridge header → `*.bin`), a `BUILD.md` with the exact steps, and — where a one-call
|
|
341
|
+
rebuild exists — a `rebuild.json` of the precise `build({...})` args. So the loop is
|
|
342
|
+
`disasm({target:'project'})` → edit a `.asm` → rebuild → `romPatch({op:'diff'})` to confirm.
|
|
343
|
+
|
|
344
|
+
**Two rebuild tiers** (the disasm emits each CPU's native-reassembler syntax — ca65 for
|
|
345
|
+
6502/65816, GNU `as` for m68k/arm/z80/gbz80 — which only some `build()` toolchains consume):
|
|
346
|
+
- **One-call `build()` rebuild, byte-identical** — **NES, C64, Atari 7800, Lynx**. Feed
|
|
347
|
+
`rebuild.json` straight to `build`. (Lynx: `build()` yields the headerless image; prepend
|
|
348
|
+
the shipped `lnx_header.bin` for the full `.lnx`.)
|
|
349
|
+
- **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`** — **SMS,
|
|
350
|
+
GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()` toolchains (SDCC/RGBDS/asar/
|
|
351
|
+
dasm/vasm) can't reassemble ca65/GNU-as syntax, so `BUILD.md` gives the proven native
|
|
352
|
+
`as`/`ld`/`objcopy` chain.
|
|
353
|
+
- **PC Engine** is the one not-yet-byte-exact case (the region trims real padding / doesn't
|
|
354
|
+
strip a copier header) — `BUILD.md` flags it.
|
|
355
|
+
|
|
356
|
+
**Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the most common
|
|
357
|
+
NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{prgBanks, chrBanks, mapper,
|
|
358
|
+
mirroring}, sourcesPaths:{…the PRG…}, binaryIncludePaths:{"chr.bin":…}})` auto-emits the
|
|
359
|
+
16-byte iNES header + CHARS-segment wiring + flat NROM `.cfg` — no hand-derived header bytes.
|
|
360
|
+
`disasm({target:'project'})` puts exactly this call in `rebuild.json`. (For homebrew C that
|
|
361
|
+
ships fixed tile art, `linkerConfig:"chr-rom"` is the segment-split equivalent.)
|
|
362
|
+
|
|
363
|
+
**Readability caveats** (the bytes are ALWAYS correct; only instruction-vs-`.byte` coverage
|
|
364
|
+
varies): SNES and large Genesis ROMs come back byte-exact but DATA-ONLY (flat whole-ROM
|
|
365
|
+
disasm of a mostly-data image heals to `.byte` — meaningful coverage needs recursive
|
|
366
|
+
entry-point following, a known follow-up). GBA reads LOW because GBA C compiles mostly to
|
|
367
|
+
Thumb reached via an ARM crt0 stub, so an ARM-mode disasm decodes Thumb spans as `.byte`.
|
|
368
|
+
Banked-NES is the strongest case (~100% instructions); GB/GBC, SMS/GG, C64, Atari are also
|
|
369
|
+
near-100%.
|
|
370
|
+
|
|
371
|
+
## 8. Graphics swaps — PNG ↔ tiles round-trip
|
|
372
|
+
|
|
373
|
+
For sprite/tile edits (not text), don't hand-roll the tile-format math:
|
|
374
|
+
|
|
375
|
+
- **`tiles({op:'png', source:'path', platform, path, bank, paletteFromEmulator, paletteIndex})`** —
|
|
376
|
+
a source ROM's tiles → PNG. `bank:N` (NES 4 KB CHR bank) replaces magic file-offset math;
|
|
377
|
+
`paletteFromEmulator:true`+`paletteIndex` colors the export with the LIVE palette (vs
|
|
378
|
+
grayscale) so the art is recognizable to edit.
|
|
379
|
+
- **`importArt({from:'rom', sourceRom, sourcePlatform, sourceBank, sourceTileX/Y/W/H, targetPlatform,
|
|
380
|
+
outputPng, intent, paletteIndex})`** — one-call lift of a tile region from a source ROM into
|
|
381
|
+
the target platform's format (extract+crop+quantize). `intent:"homebrew"` reads the live
|
|
382
|
+
source palette; `intent:"rom-hack"` preserves source bytes verbatim.
|
|
383
|
+
- **`encodeArt({stage:'tiles', platform, pngBase64})`** → target-platform tile bytes.
|
|
384
|
+
- **`romPatch({op:'spliceCHR', path, platform, pngBase64, tileIndex, expect, bank, paletteHint})`** —
|
|
385
|
+
PNG → tile bytes → splice into CHR at tile slot N (auto-locates iNES CHR base; `expect`
|
|
386
|
+
checks the existing tile bytes; `paletteHint:["#RRGGBB",…]` gives explicit RGB→index
|
|
387
|
+
mapping). Composes the `encodeArt`+`romPatch({op:'write'})` step in one call.
|
|
388
|
+
- **`background({view:'rendered'})`** — at the current state, the set of tile IDs actually drawn
|
|
389
|
+
(BG nametable + OAM). Sample at title/gameplay/menu and diff the sets to map tile IDs to
|
|
390
|
+
assets without scanning sheets by eye. (`romPatch({op:'findFree'})` locates $FF/$00 runs for asm
|
|
391
|
+
overlays, longest-first.)
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
257
395
|
## Quick reference
|
|
258
396
|
|
|
259
397
|
| Goal | Tool |
|
|
@@ -276,4 +414,15 @@ and `input({op:'set'})` state it too.)
|
|
|
276
414
|
| Where did a VRAM graphic come from (Genesis) | `watch({on:'dma', precision:'sampled'})` (ROM offset of the DMA source) |
|
|
277
415
|
| Drive a menu fast | `input({op:'navigate'})` (advances on screen change) |
|
|
278
416
|
| Free RAM map for a known game | `cheats({op:'lookup'})` / `cheats({op:'search'})` |
|
|
417
|
+
| Apply a cheat live (non-destructive) | `cheats({op:'apply'})` (verify a label / fun) |
|
|
418
|
+
| Create a shareable code from a byte | `cheats({op:'make'})` (verified, all 14) |
|
|
419
|
+
| Read on-screen text's tile map | `text({op:'learn', fromScreen})` (live, no offset needed) |
|
|
420
|
+
| Find / encode a font-rendered string | `text({op:'find'})` → `text({op:'encode'})` |
|
|
421
|
+
| Assemble asm → raw patch bytes | `assembleSnippet({cpu, origin, code})` |
|
|
422
|
+
| Mapper-aware diff of two ROMs | `romPatch({op:'diff'})` (CPU addrs, CHR `tile:N`) |
|
|
423
|
+
| Who references this address (static) | `disasm({target:'references'})` (direct modes only) |
|
|
424
|
+
| Split / rebuild a ROM into parts | `cart({op:'extract'})` / `cart({op:'wrap'})` |
|
|
425
|
+
| Swap a sprite/tile (PNG round-trip) | `tiles({op:'png'})` → edit → `romPatch({op:'spliceCHR'})` |
|
|
426
|
+
| Lift art from another game's ROM | `importArt({from:'rom'})` |
|
|
427
|
+
| Tile IDs actually being drawn now | `background({view:'rendered'})` |
|
|
279
428
|
| Safe patch | `romPatch({op:'write'})`/`romPatch({op:'writeMany'})` with `expect` |
|
|
@@ -192,3 +192,40 @@ When you call `build({output:'rom', platform:"atari2600", source: ...})`:
|
|
|
192
192
|
2. The result is `.a26` — loadable in stella (`loadMedia`).
|
|
193
193
|
|
|
194
194
|
There's no linker — dasm produces a complete cart in one pass.
|
|
195
|
+
|
|
196
|
+
## MCP debug & inspection tooling
|
|
197
|
+
|
|
198
|
+
The 2600 runs on the **stella2014 (patched)** core. Because the 2600 has
|
|
199
|
+
no framebuffer and no standard sound chip, its inspectors look different
|
|
200
|
+
from the tilemap consoles — they decode the live TIA snapshot instead.
|
|
201
|
+
|
|
202
|
+
What you can read:
|
|
203
|
+
|
|
204
|
+
- **`palette({source:'live'})`** — the NTSC 128-color palette as a PNG,
|
|
205
|
+
with the *current* TIA background luma+hue extracted from the live
|
|
206
|
+
snapshot so you can see what color the beam is painting right now. This
|
|
207
|
+
is the same 128-entry `HHHHLLLL` palette the 7800 uses.
|
|
208
|
+
- **`sprites({op:'inspect'})`** — there is **no OAM** on the 2600, so this
|
|
209
|
+
returns the state of the 5 TIA graphics objects (P0, P1, M0, M1, Ball)
|
|
210
|
+
plus a current-scanline PNG showing how the TIA is composing that line.
|
|
211
|
+
- **`cpu({op:'read'})`** — the 6502 register file (A / X / Y / P / SP / PC)
|
|
212
|
+
pulled from the M6502 core's internal regs.
|
|
213
|
+
- **`background({view:'renderState'})`** — decodes the 32-byte TIA snapshot
|
|
214
|
+
into the playfield pattern, per-object enables/positions, and the color
|
|
215
|
+
registers.
|
|
216
|
+
- **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
|
|
217
|
+
both anchor to the top of the bank (`$F000-$FFFF`) and label the vector
|
|
218
|
+
table (NMI / RESET / IRQ at `$FFFA`).
|
|
219
|
+
|
|
220
|
+
Memory regions for **`memory({op:'read'})`**:
|
|
221
|
+
|
|
222
|
+
| Region | Size | What it is |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| `system_ram` | 128 bytes | the RIOT RAM — that's the *entire* console RAM |
|
|
225
|
+
| `a26_tia_regs` | 32 bytes | the live TIA register snapshot |
|
|
226
|
+
| `a26_cpu_regs` | 7 bytes | the 6502 register snapshot |
|
|
227
|
+
|
|
228
|
+
**No `audioDebug` inspector.** The 2600's sound comes from the two TIA
|
|
229
|
+
audio voices (`AUDC/AUDF/AUDV` at `$15-$1A`), not a standard PSG/FM chip,
|
|
230
|
+
so there's no `audioDebug` decode — read the audio state directly out of
|
|
231
|
+
the `a26_tia_regs` snapshot instead.
|
|
@@ -322,3 +322,39 @@ When you call `build({output:'rom', platform:"atari7800", language:"c"})`:
|
|
|
322
322
|
3. ld65 links + atari7800.cfg → flat `.a78` ROM.
|
|
323
323
|
|
|
324
324
|
Loadable via prosystem (`loadMedia`).
|
|
325
|
+
|
|
326
|
+
## MCP debug & inspection tooling
|
|
327
|
+
|
|
328
|
+
The 7800 runs on the **prosystem (patched)** core. Like the 2600 it has
|
|
329
|
+
no framebuffer and no tile/sprite-attribute tables, so its inspectors
|
|
330
|
+
decode MARIA's display-list machinery rather than a tilemap.
|
|
331
|
+
|
|
332
|
+
What you can read:
|
|
333
|
+
|
|
334
|
+
- **`palette({source:'live'})`** — a 256-color master palette PNG, with
|
|
335
|
+
the live MARIA palette block at `$20-$3F` decoded into the 8 palettes ×
|
|
336
|
+
3 colors each, plus the backdrop. This is the Atari NTSC palette shared
|
|
337
|
+
with the 2600.
|
|
338
|
+
- **`sprites({op:'inspect'})`** — there is **no OAM** on the 7800. Instead
|
|
339
|
+
this returns the MARIA control registers and the **DPP** display-list-
|
|
340
|
+
list pointer, leaving the agent to walk the DLL → DL hierarchy itself
|
|
341
|
+
(the same structure described under "MARIA: the unusual one" above).
|
|
342
|
+
- **`cpu({op:'read'})`** — the 6502 ("Sally") register file (A / X / Y /
|
|
343
|
+
P / SP / PC) read from prosystem's `sally` globals.
|
|
344
|
+
- **`background({view:'renderState'})`** — the MARIA CTRL bits, DPP,
|
|
345
|
+
CHARBASE, and the current `dlistPtr`.
|
|
346
|
+
- **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
|
|
347
|
+
both default to the top 16 KB (`$C000-$FFFF`), where the reset vector
|
|
348
|
+
lands.
|
|
349
|
+
|
|
350
|
+
Memory regions for **`memory({op:'read'})`**:
|
|
351
|
+
|
|
352
|
+
| Region | Size | What it is |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| `system_ram` | 64 KB | the *entire* 6502 address space — MARIA regs, RAM, and ROM are all visible through this one region |
|
|
355
|
+
| `a78_cpu_regs` | — | the 6502 register snapshot |
|
|
356
|
+
|
|
357
|
+
**No `audioDebug` inspector.** The 7800's standard audio is the same TIA
|
|
358
|
+
chip carried over from the 2600 (`$15-$1A`), not a decodable PSG/FM chip,
|
|
359
|
+
so there's no `audioDebug` decode. (Some carts add a POKEY, but it's
|
|
360
|
+
non-standard — don't assume it's present.)
|