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/playtest/playtest.js
CHANGED
|
@@ -231,6 +231,32 @@ const KEY_TO_LIBRETRO_BIT = {
|
|
|
231
231
|
w: 11, // R shoulder
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
+
// C64-only keyboard fallback: PC key → the virtual C64 button name the host's
|
|
235
|
+
// C64 layer maps to the key matrix (Space/Run-Stop/Return/F1-F7). Lets a human
|
|
236
|
+
// with NO controller still reach the C64 keyboard keys games need to start.
|
|
237
|
+
// (Arrows + Z give the joystick + Fire via KEY_TO_LIBRETRO_BIT above.)
|
|
238
|
+
const C64_KEYBOARD_FALLBACK = {
|
|
239
|
+
f1: "c64_f1", f2: "c64_f3", f3: "c64_f5", f4: "c64_f7", // F1-F4 → C64 F1/F3/F5/F7
|
|
240
|
+
space: "west", // Space
|
|
241
|
+
return: "r2", // Return (also START via the standard map — harmless)
|
|
242
|
+
escape: "l2", // Run/Stop (note: ESC also closes — see handler order)
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Human-readable C64 controls (controller + keyboard), relayed to the user when
|
|
246
|
+
// a C64 game is in the playtest window so they're not guessing.
|
|
247
|
+
export const C64_BINDINGS_HELP = `C64 — a CONTROLLER alone is enough (no keyboard needed):
|
|
248
|
+
D-pad / Left stick Joystick (port 2 by default)
|
|
249
|
+
Z / bottom face Fire
|
|
250
|
+
X face / Space key Space
|
|
251
|
+
L2 Run/Stop
|
|
252
|
+
R2 / Enter Return
|
|
253
|
+
Right stick ↑/←/→/↓ F1 / F3 / F5 / F7 (the 1-player / start keys)
|
|
254
|
+
Top face F1 (also)
|
|
255
|
+
|
|
256
|
+
No controller? Keyboard fallback: Arrows = joystick, Z = Fire, F1-F4 = C64
|
|
257
|
+
F1/F3/F5/F7, Space = Space, Enter = Return, ESC = Run/Stop (hold; ESC tapped
|
|
258
|
+
also closes the window). Switch joystick port with input({op:'joyport'}).`;
|
|
259
|
+
|
|
234
260
|
// Human-readable summary printed by --help and at playtest startup.
|
|
235
261
|
export const KEYBOARD_BINDINGS_HELP = `Keyboard:
|
|
236
262
|
Arrow keys D-pad
|
|
@@ -578,6 +604,7 @@ export async function playtest(args) {
|
|
|
578
604
|
// into its own port object; the agent's setInput is overwritten each
|
|
579
605
|
// tick (matching prior behavior). Select+Start on any controller quits.
|
|
580
606
|
let quit = false;
|
|
607
|
+
const isC64 = h.status?.platform === "c64";
|
|
581
608
|
function readControllerInto(port, inst) {
|
|
582
609
|
if (!inst) return;
|
|
583
610
|
const btn = inst.buttons || {};
|
|
@@ -595,6 +622,18 @@ export async function playtest(args) {
|
|
|
595
622
|
else if (lx < -STICK_DEADZONE) port.left = true;
|
|
596
623
|
if (ly > STICK_DEADZONE) port.down = true;
|
|
597
624
|
else if (ly < -STICK_DEADZONE) port.up = true;
|
|
625
|
+
// C64: the RIGHT stick selects the function keys (F1/F3/F5/F7) — the
|
|
626
|
+
// Batocera/RetroDeck convention so a controller alone reaches the keyboard
|
|
627
|
+
// keys C64 setup screens need. Emitted as virtual buttons the host's C64
|
|
628
|
+
// layer maps to the key matrix; harmless on other platforms (no mapping).
|
|
629
|
+
if (isC64) {
|
|
630
|
+
const rx = axes.rightStickX ?? 0;
|
|
631
|
+
const ry = axes.rightStickY ?? 0;
|
|
632
|
+
if (ry < -STICK_DEADZONE) port.c64_f1 = true; // up → F1
|
|
633
|
+
else if (ry > STICK_DEADZONE) port.c64_f7 = true; // down → F7
|
|
634
|
+
if (rx < -STICK_DEADZONE) port.c64_f3 = true; // left → F3
|
|
635
|
+
else if (rx > STICK_DEADZONE) port.c64_f5 = true; // right → F5
|
|
636
|
+
}
|
|
598
637
|
}
|
|
599
638
|
|
|
600
639
|
const port0 = {};
|
|
@@ -619,6 +658,15 @@ export async function playtest(args) {
|
|
|
619
658
|
for (const [keyName, bit] of Object.entries(KEY_TO_LIBRETRO_BIT)) {
|
|
620
659
|
if (heldKeys.has(keyName)) port0[bitToName(bit)] = true;
|
|
621
660
|
}
|
|
661
|
+
// C64 keyboard fallback (no controller / mixing): map PC keys to the C64
|
|
662
|
+
// KEYBOARD keys games need — the host's C64 layer routes these virtual
|
|
663
|
+
// button names to the key matrix. (Arrows + Z=Fire already give the
|
|
664
|
+
// joystick above.) The agent relays these to the human.
|
|
665
|
+
if (isC64) {
|
|
666
|
+
for (const [keyName, vbtn] of Object.entries(C64_KEYBOARD_FALLBACK)) {
|
|
667
|
+
if (heldKeys.has(keyName)) port0[vbtn] = true;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
622
670
|
const isRewinding = heldKeys.has("r") && rewindBuffer.length > 0;
|
|
623
671
|
if (isRewinding) {
|
|
624
672
|
// Restore the previous snapshot and run one frame to produce its visual.
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { buildForPlatform } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/toolchains/index.js";
|
|
3
|
-
import { resolveCore } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/cores/registry.js";
|
|
4
|
-
import { LibretroHost } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/host/LibretroHost.js";
|
|
5
|
-
|
|
6
|
-
const LIBDIR = "/home/monteslu/code/cliemu/romdev/packages/romdev/src/platforms/msx/lib/c";
|
|
7
|
-
const DIR = "/home/monteslu/code/cliemu/romdev/packages/romdev/examples/msx/catch_game";
|
|
8
|
-
|
|
9
|
-
const SRC = readFileSync(`${DIR}/main.c`, "utf8");
|
|
10
|
-
const LIB = readFileSync(`${LIBDIR}/msx_vdp.c`, "utf8");
|
|
11
|
-
const CRT0 = readFileSync(`${LIBDIR}/msx_crt0.s`, "utf8");
|
|
12
|
-
const HDR = readFileSync(`${LIBDIR}/msx_hw.h`, "utf8");
|
|
13
|
-
|
|
14
|
-
const PLATFORM = "msx";
|
|
15
|
-
|
|
16
|
-
console.log("== building ==");
|
|
17
|
-
const build = await buildForPlatform({
|
|
18
|
-
platform: PLATFORM,
|
|
19
|
-
sources: { "main.c": SRC, "msx_vdp.c": LIB, "msx_crt0.s": CRT0 },
|
|
20
|
-
includes: { "msx_hw.h": HDR },
|
|
21
|
-
crt0: ".module empty\n",
|
|
22
|
-
sourceName: "main.c",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
if (!build.binary) {
|
|
26
|
-
console.log(build.log || build.stderr || JSON.stringify(build));
|
|
27
|
-
console.log("BUILD FAILED stage=", build.stage, "exit=", build.exitCode);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
console.log("build OK, bytes=", build.binary.length);
|
|
31
|
-
|
|
32
|
-
const c = resolveCore(PLATFORM);
|
|
33
|
-
const h = new LibretroHost();
|
|
34
|
-
await h.loadCore(c.jsPath, c.wasmPath);
|
|
35
|
-
await h.loadMedia({ platform: PLATFORM, bytes: build.binary });
|
|
36
|
-
|
|
37
|
-
// ---- honest pixel helpers over raw RGBA ----
|
|
38
|
-
function nonBlackFraction(rgba) {
|
|
39
|
-
let nb = 0;
|
|
40
|
-
for (let i = 0; i < rgba.length; i += 4) {
|
|
41
|
-
if (rgba[i] > 16 || rgba[i + 1] > 16 || rgba[i + 2] > 16) nb++;
|
|
42
|
-
}
|
|
43
|
-
return nb / (rgba.length / 4);
|
|
44
|
-
}
|
|
45
|
-
// Find the brightest-pixel X centroid in a horizontal band of rows (the basket
|
|
46
|
-
// rides near the bottom; the basket sprite is white). Returns -1 if no bright px.
|
|
47
|
-
function brightCentroidX(frame, y0, y1) {
|
|
48
|
-
const { width, height, rgba } = frame;
|
|
49
|
-
let sx = 0, n = 0;
|
|
50
|
-
for (let y = y0; y < Math.min(y1, height); y++) {
|
|
51
|
-
for (let x = 0; x < width; x++) {
|
|
52
|
-
const i = (y * width + x) * 4;
|
|
53
|
-
// white basket: all channels high
|
|
54
|
-
if (rgba[i] > 180 && rgba[i + 1] > 180 && rgba[i + 2] > 180) { sx += x; n++; }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return n ? sx / n : -1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// boot past the C-BIOS logo + a few game frames
|
|
61
|
-
for (let i = 0; i < 320; i++) h.stepFrames(1);
|
|
62
|
-
|
|
63
|
-
const sa = h.screenshotRgba();
|
|
64
|
-
writeFileSync(`${DIR}/shot_before.png`, Buffer.from(h.screenshot().pngBase64, "base64"));
|
|
65
|
-
// basket band: BASKET_Y=160 in a 192-tall internal buffer; band 150..180
|
|
66
|
-
const fracA = nonBlackFraction(sa.rgba);
|
|
67
|
-
const basketA = brightCentroidX(sa, Math.floor(sa.height * 0.78), Math.floor(sa.height * 0.95));
|
|
68
|
-
console.log(`before: ${sa.width}x${sa.height} nonBlack=${(fracA*100).toFixed(2)}% basketX=${basketA.toFixed(1)}`);
|
|
69
|
-
|
|
70
|
-
// Drive RIGHT for many frames
|
|
71
|
-
for (let i = 0; i < 120; i++) { h.setInput({ ports: [{ right: true }] }); h.stepFrames(1); }
|
|
72
|
-
const sb = h.screenshotRgba();
|
|
73
|
-
writeFileSync(`${DIR}/shot_after_right.png`, Buffer.from(h.screenshot().pngBase64, "base64"));
|
|
74
|
-
const basketB = brightCentroidX(sb, Math.floor(sb.height * 0.78), Math.floor(sb.height * 0.95));
|
|
75
|
-
console.log(`after RIGHT: basketX=${basketB.toFixed(1)}`);
|
|
76
|
-
|
|
77
|
-
// Drive LEFT for many frames
|
|
78
|
-
for (let i = 0; i < 200; i++) { h.setInput({ ports: [{ left: true }] }); h.stepFrames(1); }
|
|
79
|
-
const sc = h.screenshotRgba();
|
|
80
|
-
writeFileSync(`${DIR}/shot_after_left.png`, Buffer.from(h.screenshot().pngBase64, "base64"));
|
|
81
|
-
const basketC = brightCentroidX(sc, Math.floor(sc.height * 0.78), Math.floor(sc.height * 0.95));
|
|
82
|
-
console.log(`after LEFT: basketX=${basketC.toFixed(1)}`);
|
|
83
|
-
|
|
84
|
-
// ---- verdict ----
|
|
85
|
-
const visible = fracA > 0.005; // playfield is not black
|
|
86
|
-
const movedRight = basketB > basketA + 5; // basket moved right with input
|
|
87
|
-
const movedLeft = basketC < basketB - 5; // basket moved back left
|
|
88
|
-
console.log("VISIBLE:", visible, "MOVED_RIGHT:", movedRight, "MOVED_LEFT:", movedLeft);
|
|
89
|
-
if (visible && movedRight && movedLeft) {
|
|
90
|
-
console.log("VERIFIED_OK");
|
|
91
|
-
} else {
|
|
92
|
-
console.log("VERIFY_INCOMPLETE");
|
|
93
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { buildForPlatform } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/toolchains/index.js";
|
|
3
|
-
import { resolveCore } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/cores/registry.js";
|
|
4
|
-
import { LibretroHost } from "/home/monteslu/code/cliemu/romdev/packages/romdev/src/host/LibretroHost.js";
|
|
5
|
-
|
|
6
|
-
const LIB = "/home/monteslu/code/cliemu/romdev/packages/romdev/src/platforms/pce/lib/c";
|
|
7
|
-
const DIR = "/home/monteslu/code/cliemu/romdev/packages/romdev/examples/pce/catch_game";
|
|
8
|
-
|
|
9
|
-
const read = (p) => fs.readFileSync(p, "utf8");
|
|
10
|
-
|
|
11
|
-
const sources = {
|
|
12
|
-
"main.c": read(DIR + "/main.c"),
|
|
13
|
-
"pce_video.c": read(LIB + "/pce_video.c"),
|
|
14
|
-
"pce_input.c": read(LIB + "/pce_input.c"),
|
|
15
|
-
"pce_sound.c": read(LIB + "/pce_sound.c"),
|
|
16
|
-
};
|
|
17
|
-
const headers = { "pce_hw.h": read(LIB + "/pce_hw.h") };
|
|
18
|
-
|
|
19
|
-
console.log("Building catch_game...");
|
|
20
|
-
const build = await buildForPlatform({
|
|
21
|
-
platform: "pce",
|
|
22
|
-
sources,
|
|
23
|
-
headers,
|
|
24
|
-
includes: headers,
|
|
25
|
-
sourceName: "main.c",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (!build || !build.binary) {
|
|
29
|
-
console.log("BUILD FAILED");
|
|
30
|
-
console.log(JSON.stringify(build, null, 2));
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
console.log("BUILD OK, binary bytes =", build.binary.length);
|
|
34
|
-
if (build.log) console.log("log tail:", String(build.log).slice(-400));
|
|
35
|
-
|
|
36
|
-
const PLATFORM = "pce";
|
|
37
|
-
const h = new LibretroHost();
|
|
38
|
-
const c = resolveCore(PLATFORM);
|
|
39
|
-
await h.loadCore(c.jsPath, c.wasmPath);
|
|
40
|
-
await h.loadMedia({ platform: PLATFORM, bytes: build.binary });
|
|
41
|
-
|
|
42
|
-
// boot
|
|
43
|
-
for (let i = 0; i < 120; i++) h.stepFrames(1);
|
|
44
|
-
|
|
45
|
-
function nonBlack(shot) {
|
|
46
|
-
const buf = Buffer.from(shot.pngBase64, "base64");
|
|
47
|
-
// crude: count distinct-ish bytes; rely on file size + later pixel scan
|
|
48
|
-
return buf.length;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// screenshot BEFORE input
|
|
52
|
-
let shotA = h.screenshot();
|
|
53
|
-
fs.writeFileSync(DIR + "/shot_before.png", Buffer.from(shotA.pngBase64, "base64"));
|
|
54
|
-
console.log("before:", shotA.width, "x", shotA.height, "png bytes", nonBlack(shotA));
|
|
55
|
-
|
|
56
|
-
// drive RIGHT for ~60 frames to move catcher and let fruit fall/catch
|
|
57
|
-
for (let i = 0; i < 90; i++) {
|
|
58
|
-
h.setInput({ ports: [{ right: true }] });
|
|
59
|
-
h.stepFrames(1);
|
|
60
|
-
}
|
|
61
|
-
// then LEFT for a bit
|
|
62
|
-
for (let i = 0; i < 60; i++) {
|
|
63
|
-
h.setInput({ ports: [{ left: true }] });
|
|
64
|
-
h.stepFrames(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let shotB = h.screenshot();
|
|
68
|
-
fs.writeFileSync(DIR + "/shot_after.png", Buffer.from(shotB.pngBase64, "base64"));
|
|
69
|
-
console.log("after:", shotB.width, "x", shotB.height, "png bytes", nonBlack(shotB));
|
|
70
|
-
|
|
71
|
-
// pixel diff: decode both PNGs to raw via the host's framebuffer if available
|
|
72
|
-
const same = shotA.pngBase64 === shotB.pngBase64;
|
|
73
|
-
console.log("identical PNG?", same);
|
|
74
|
-
|
|
75
|
-
console.log("done — see shot_before.png / shot_after.png");
|