romdevtools 0.27.0 → 0.29.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 (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -102,6 +102,48 @@ export function isPlaytestRunning(sessionKey) {
102
102
  return reconcileSession(sessionKey);
103
103
  }
104
104
 
105
+ /**
106
+ * Human co-drive snapshot for one session: is a playtest window open, and has
107
+ * the HUMAN pressed anything (pad / keyboard / rewind-scrub) within the last
108
+ * ~2 s? Drives catalog({op:'status'}) and the co-drive warning attached to
109
+ * frame/input responses. Cheap, never throws; with no window everything is
110
+ * inactive. Frames are window ticks ≈ frames at 60fps real time.
111
+ * @param {string} sessionKey
112
+ * @returns {{windowOpen: boolean, humanInputActive: boolean, framesSinceHumanInput: number | null}}
113
+ */
114
+ export function getPlaytestHumanStatus(sessionKey) {
115
+ if (!reconcileSession(sessionKey)) {
116
+ return { windowOpen: false, humanInputActive: false, framesSinceHumanInput: null };
117
+ }
118
+ const s = sessions.get(sessionKey);
119
+ return {
120
+ windowOpen: true,
121
+ humanInputActive: typeof s.humanInputActive === "function" ? !!s.humanInputActive() : false,
122
+ framesSinceHumanInput: typeof s.framesSinceHumanInput === "function" ? s.framesSinceHumanInput() : null,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * The warning attached to frame({op:'step'/'stepAndShot'}) and input(set/press/
128
+ * sequence/navigate) responses while a human is co-driving this session's
129
+ * playtest window. null when there's no window or the human hasn't pressed
130
+ * recently — so the field only appears when there's a REAL conflict.
131
+ * @param {string} sessionKey
132
+ * @returns {string | null}
133
+ */
134
+ export function humanCoDriveWarning(sessionKey) {
135
+ const st = getPlaytestHumanStatus(sessionKey);
136
+ if (!st.windowOpen || !st.humanInputActive) return null;
137
+ const ago = st.framesSinceHumanInput != null ? `~${st.framesSinceHumanInput} frames ago` : "moments ago";
138
+ return (
139
+ `A playtest window is open and the HUMAN last pressed buttons ${ago} — you are co-driving the same ` +
140
+ "emulator. While they press, the window's input overwrites yours each tick (the human wins), and its " +
141
+ "real-time 60fps loop races your frame-stepping (non-deterministic results). Either host({op:'pause'}) " +
142
+ "while you inspect (the window keeps rendering, frozen), do deterministic work in a SECOND session " +
143
+ "(a different x-romdev-session header = a fully isolated emulator), or wait for the human to stop."
144
+ );
145
+ }
146
+
105
147
  export function registerPlaytestTools(server, z, sessionKey) {
106
148
  // op:'open' — open (or reuse) the SDL window for this session.
107
149
  async function ptOpen({ scale = 3, title, aspect = "tv" }) {
@@ -283,8 +325,14 @@ export function registerPlaytestTools(server, z, sessionKey) {
283
325
  // REPLACED by resetHost() on every runSource/loadMedia. Same object →
284
326
  // screenshot() and the window agree. Different object → they've diverged.
285
327
  const matches = !!windowHost && activeHost === windowHost;
328
+ const human = getPlaytestHumanStatus(sessionKey);
286
329
  return jsonContent({
287
330
  running: true,
331
+ // Is the human ACTIVELY playing right now (pressed within ~2 s)? While
332
+ // true, your input/setInput is overwritten each tick and real-time
333
+ // stepping races yours — pause, or use a second session.
334
+ humanInputActive: human.humanInputActive,
335
+ ...(human.framesSinceHumanInput != null ? { framesSinceHumanInput: human.framesSinceHumanInput } : {}),
288
336
  // What the HUMAN is looking at (the window's own host):
289
337
  windowMediaPath: windowHost?.status?.mediaPath ?? null,
290
338
  windowFrameCount: windowHost?.status?.frameCount ?? session.frameCount,
@@ -354,10 +402,14 @@ export function registerPlaytestTools(server, z, sessionKey) {
354
402
  "eyes (boots, renders, the feature is visible) — a window on a black screen/crash just wastes their attention. " +
355
403
  "BEST FOR diagnosing a USER-REPORTED bug: hand them the window, let them drive to the exact moment, then " +
356
404
  "inspect the SAME live host in real time (memory/watch/sprites/state). Every other tool keeps working against " +
357
- "that live host while the window is open. FOOTGUN — the window's loop owns input AND stepping: each tick it " +
358
- "rebuilds controller state from the human's gamepad+keyboard, calls setInput, then steps a frame, so your " +
359
- "input({op:'set'}) is OVERWRITTEN on the next tick (the human wins). To inspect a moving state freeze it first: " +
360
- "host({op:'pause'}) read host({op:'resume'}). Requires @kmamal/sdl. `scale`/`title`/`aspect` shape the window.\n" +
405
+ "that live host while the window is open. FOOTGUN — the window's loop steps the core in REAL TIME, and while " +
406
+ "the human is pressing (pad/keyboard) it writes their input each tick, overwriting yours the human wins. " +
407
+ "(When the human is idle the window leaves your input({op:'set'}) alone, but its 60fps stepping still races " +
408
+ "your frame({op:'step'}).) You'll KNOW: frame/input responses carry `humanCoDriveWarning` while the human " +
409
+ "pressed within ~2s, and catalog({op:'status'})/playtest({op:'status'}) expose `humanInputActive`. To inspect " +
410
+ "a moving state freeze it first: host({op:'pause'}) → read → host({op:'resume'}); for deterministic stepping " +
411
+ "while the human plays, use a SECOND session (different x-romdev-session = fully isolated emulator). " +
412
+ "Requires @kmamal/sdl. `scale`/`title`/`aspect` shape the window.\n" +
361
413
  "• op:'stop' — close THIS session's window (the host stays loaded; other agents' windows unaffected).\n" +
362
414
  "• op:'status' — is a window open, what ROM/frame it shows, and `activeHostMatchesWindow` (false = a build/" +
363
415
  "loadMedia swapped the active host, so frame({op:'screenshot'}) no longer shows what the human sees — use op:'framebuffer').\n" +
@@ -409,7 +409,10 @@ export async function previewTileArtCore(args) {
409
409
 
410
410
  if (outputPath) {
411
411
  await writeFile(outputPath, png);
412
- return { ...result, outputPath, note: `${png.length} bytes of PNG written to ${outputPath}.` };
412
+ // Livestream sideband: the human sees the rendered sheet even though the
413
+ // agent only gets the path.
414
+ return { ...result, outputPath, note: `${png.length} bytes of PNG written to ${outputPath}.`,
415
+ _observerImages: [{ kind: "image", mimeType: "image/png", base64: png.toString("base64") }] };
413
416
  }
414
417
  return { ...result, pngBase64: png.toString("base64") };
415
418
  }
@@ -467,7 +470,8 @@ async function previewMsxScreen2(args, d) {
467
470
  };
468
471
  if (outputPath) {
469
472
  await writeFile(outputPath, buf);
470
- return { ...result, outputPath };
473
+ return { ...result, outputPath,
474
+ _observerImages: [{ kind: "image", mimeType: "image/png", base64: buf.toString("base64") }] };
471
475
  }
472
476
  return { ...result, pngBase64: buf.toString("base64") };
473
477
  }