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.
Files changed (34) hide show
  1. package/AGENTS.md +139 -494
  2. package/CHANGELOG.md +41 -3
  3. package/package.json +2 -2
  4. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  5. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  6. package/src/host/LibretroHost.js +170 -1
  7. package/src/http/skill-doc.js +1 -1
  8. package/src/mcp/tools/input-layout.js +10 -0
  9. package/src/mcp/tools/input.js +26 -2
  10. package/src/mcp/tools/playtest.js +17 -2
  11. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +155 -6
  12. package/src/platforms/atari2600/MENTAL_MODEL.md +37 -0
  13. package/src/platforms/atari7800/MENTAL_MODEL.md +36 -0
  14. package/src/platforms/c64/MENTAL_MODEL.md +83 -6
  15. package/src/platforms/gb/MENTAL_MODEL.md +56 -0
  16. package/src/platforms/gba/MENTAL_MODEL.md +57 -3
  17. package/src/platforms/gba/lib/arm-archives/libc.a +0 -0
  18. package/src/platforms/gba/lib/arm-archives/libgcc.a +0 -0
  19. package/src/platforms/gba/lib/arm-archives/libnosys.a +0 -0
  20. package/src/platforms/gbc/MENTAL_MODEL.md +21 -0
  21. package/src/platforms/genesis/MENTAL_MODEL.md +19 -0
  22. package/src/platforms/genesis/lib/c/libc.a +0 -0
  23. package/src/platforms/genesis/lib/c/libgcc.a +0 -0
  24. package/src/platforms/genesis/lib/c/libm.a +0 -0
  25. package/src/platforms/gg/MENTAL_MODEL.md +24 -0
  26. package/src/platforms/lynx/MENTAL_MODEL.md +33 -7
  27. package/src/platforms/msx/MENTAL_MODEL.md +27 -0
  28. package/src/platforms/nes/MENTAL_MODEL.md +35 -0
  29. package/src/platforms/sms/MENTAL_MODEL.md +51 -0
  30. package/src/platforms/snes/MENTAL_MODEL.md +21 -0
  31. package/src/platforms/snes/TROUBLESHOOTING.md +43 -0
  32. package/src/playtest/playtest.js +48 -0
  33. package/examples/msx/catch_game/_verify.mjs +0 -93
  34. package/examples/pce/catch_game/_verify.mjs +0 -75
@@ -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");