romdevtools 0.22.1 → 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 (54) hide show
  1. package/AGENTS.md +169 -494
  2. package/CHANGELOG.md +103 -0
  3. package/examples/genesis/templates/platformer.c +5 -1
  4. package/examples/genesis/templates/two_plane_parallax.c +166 -0
  5. package/package.json +2 -2
  6. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  7. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  8. package/src/host/LibretroHost.js +225 -2
  9. package/src/host/framebuffer.js +37 -0
  10. package/src/http/skill-doc.js +1 -1
  11. package/src/mcp/tools/audio.js +2 -2
  12. package/src/mcp/tools/frame.js +13 -34
  13. package/src/mcp/tools/index.js +2 -2
  14. package/src/mcp/tools/input-layout.js +10 -0
  15. package/src/mcp/tools/input.js +26 -2
  16. package/src/mcp/tools/metasprite-tools.js +1 -1
  17. package/src/mcp/tools/platform-tools.js +18 -11
  18. package/src/mcp/tools/playtest.js +17 -2
  19. package/src/mcp/tools/project.js +9 -1
  20. package/src/mcp/tools/rendering-context.js +1 -1
  21. package/src/mcp/tools/symbols.js +130 -39
  22. package/src/mcp/tools/tile-inspect.js +1 -1
  23. package/src/mcp/tools/toolchain.js +3 -2
  24. package/src/mcp/tools/watch-memory.js +58 -6
  25. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +155 -6
  26. package/src/platforms/atari2600/MENTAL_MODEL.md +37 -0
  27. package/src/platforms/atari7800/MENTAL_MODEL.md +36 -0
  28. package/src/platforms/c64/MENTAL_MODEL.md +83 -6
  29. package/src/platforms/gb/MENTAL_MODEL.md +74 -0
  30. package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +91 -0
  31. package/src/platforms/gba/MENTAL_MODEL.md +57 -3
  32. package/src/platforms/gba/lib/arm-archives/libc.a +0 -0
  33. package/src/platforms/gba/lib/arm-archives/libgcc.a +0 -0
  34. package/src/platforms/gba/lib/arm-archives/libnosys.a +0 -0
  35. package/src/platforms/gbc/MENTAL_MODEL.md +34 -0
  36. package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +91 -0
  37. package/src/platforms/genesis/MENTAL_MODEL.md +180 -0
  38. package/src/platforms/genesis/TROUBLESHOOTING.md +32 -0
  39. package/src/platforms/genesis/lib/c/libc.a +0 -0
  40. package/src/platforms/genesis/lib/c/libgcc.a +0 -0
  41. package/src/platforms/genesis/lib/c/libm.a +0 -0
  42. package/src/platforms/gg/MENTAL_MODEL.md +24 -0
  43. package/src/platforms/gg/lib/c/gg_crt0.s +30 -0
  44. package/src/platforms/lynx/MENTAL_MODEL.md +33 -7
  45. package/src/platforms/msx/MENTAL_MODEL.md +27 -0
  46. package/src/platforms/nes/MENTAL_MODEL.md +35 -0
  47. package/src/platforms/sms/MENTAL_MODEL.md +51 -0
  48. package/src/platforms/sms/lib/c/sms_crt0.s +40 -0
  49. package/src/platforms/snes/MENTAL_MODEL.md +21 -0
  50. package/src/platforms/snes/TROUBLESHOOTING.md +43 -0
  51. package/src/playtest/playtest.js +48 -0
  52. package/src/toolchains/sdcc/preflight-lint.js +164 -8
  53. package/examples/msx/catch_game/_verify.mjs +0 -93
  54. package/examples/pce/catch_game/_verify.mjs +0 -75
package/CHANGELOG.md CHANGED
@@ -4,6 +4,109 @@ All notable changes to `romdevtools`. Dates are release dates.
4
4
  (Published as `romdev-mcp` through 0.11.0; renamed to `romdevtools` in 0.13.0 —
5
5
  the `romdev-mcp` bin is kept as an alias.)
6
6
 
7
+ ## 0.24.0
8
+
9
+ ### Added — C64 keyboard + joyport input (VICE core patch)
10
+ An agent RE'ing C64 Uridium could reach the intro via joystick but couldn't ENTER
11
+ gameplay — the game needs **F1** (1 player) + fire on **port 2**, and romdev's
12
+ input was joypad-mask-only. Many C64 games gate gameplay behind KEYBOARD setup
13
+ screens that joystick can't reach. The VICE core now exports
14
+ `romdev_key_matrix`/`romdev_kbdbuf_feed`/`romdev_joyport_*`, surfaced as:
15
+ - **`input({op:'pressKey', key})`** — press a C64 keyboard key (F1/F3/F5/F7,
16
+ Return, Space, Run/Stop, a–z, 0–9, …) by driving the C64 8×8 key matrix.
17
+ - **`input({op:'typeText', text})`** — feed a string into the kernal keyboard
18
+ buffer (LOAD/RUN/filenames); `\r` → RETURN.
19
+ - **`input({op:'joyport', joyport?})`** — read/set the active C64 joystick port
20
+ (1 or 2; default 2, most games).
21
+ - `input({op:'layout', platform:'c64'})` now reports the keyboard keys + joyport.
22
+
23
+ **A controller alone plays C64** (the Batocera/RetroDeck model — no physical
24
+ keyboard needed). Spare pad buttons/stick map to the C64 keyboard keys games need
25
+ to start: X/Space=Space, L2=Run/Stop, R2=Return, top face / right-stick =
26
+ F1/F3/F5/F7; d-pad + Fire stay the joystick. Unified in the host so it works the
27
+ same in the playtest window AND for the agent's headless `setInput`. No-controller
28
+ keyboard fallback maps PC F1–F4/Space/Enter to the same C64 keys.
29
+ `playtest({op:'open'})` on a C64 game relays the controls. (romdev-core-vice 0.7.0.)
30
+
31
+ ### Changed — leaner, less-confusing agent docs (AGENTS.md / SKILL.md)
32
+ AGENTS.md was loaded in full every session for every platform — ~30k tokens, of
33
+ which ~13k was platform-specific or duplicated detail an agent on one platform
34
+ never needed (and could misapply across platforms). Cut **~31% (~9.5k tokens)**:
35
+ - Per-platform debug-tooling detail → each platform's `MENTAL_MODEL.md`.
36
+ - The ROM-hacking workflow → folded into the `ROMHACKING_PLAYBOOK.md` guide.
37
+ - Toolchain landmines → per-platform `TROUBLESHOOTING.md`/`SDCC_GOTCHAS.md`.
38
+ - Disassembler flag reference → it's already in the disasm tool's own schema.
39
+ All reachable on demand via `platform({op:'doc'})`; AGENTS.md keeps generic
40
+ guidance + symptom→doc pointers. **"Read your target platform's
41
+ `platform({op:'doc', name:'mental_model'})` BEFORE you write code for it"** is now
42
+ a top-level rule (the footguns live there) — so the on-demand docs actually get
43
+ read. The dynamic SKILL.md inherits all of this.
44
+
45
+ ## 0.23.0
46
+
47
+ Response to real build-session feedback. Theme: bugs found, false alarms
48
+ removed, and — the recurring finding — **tools that already existed but agents
49
+ couldn't find them**.
50
+
51
+ ### Fixed — multi-tenant host cross-talk (a whole class of bugs)
52
+ `sprites({op:'inspect', platform:'genesis'})` returned a *GBC*-flavored error while
53
+ Genesis was loaded. Root cause: `inspectSpritesCore` / `getCPUStateCore` /
54
+ `getAudioStateCore` / `inspectBackgroundMapCore` / `inspectPatternTilesCore` were
55
+ module-global `export let` bindings reassigned per session, each closing over THAT
56
+ registration's `sessionKey` — so the last session to register stole the host for
57
+ everyone. Now the caller's `sessionKey` is threaded through. Verified on all 7
58
+ tile-based sprite platforms with sessions live simultaneously.
59
+
60
+ ### Fixed — SMS/GG C crt0: `static x = N;` initializers booted to 0
61
+ Investigating two reported "silent sm83 miscompiles" (32-bit xorshift, indexed
62
+ loop): NEITHER reproduces on GB/GBC — both produce byte-exact output. The real bug
63
+ was a genuine **z80 crt0 defect**: `sms_crt0.s`/`gg_crt0.s` placed `_INITIALIZER`
64
+ in RAM not ROM, so the gsinit `ldir` copied RAM onto itself → every value-static
65
+ booted 0 and BSS wasn't zeroed. Fixed both (mirrors the correct `gb_crt0.s`).
66
+ Re-verified all SMS/GG scaffolds still build clean + render.
67
+
68
+ ### Changed — lint stops crying wolf on WRAM copies
69
+ The SDCC `xdata-copy-miscompile` warning fired on EVERY `dst[i]=src[i]` loop,
70
+ including plain WRAM arrays (the message even said "ignore if plain WRAM") —
71
+ training agents to ignore lint. Now: provably-VRAM dest → warning, plain RAM array
72
+ → suppressed, unknown → info. (Deliberately did NOT add the requested 32-bit-shift
73
+ / short-loop lint heuristics — they'd be false positives for non-bugs.)
74
+
75
+ ### Added — feedback ergonomics
76
+ - **`frame({op:'screenshot', scale})` up-scales** (integer ≥2, nearest-neighbor)
77
+ for legible handheld shots (GB 160×144 → 640×576 at `scale:4`), plus the
78
+ existing `0<scale<1` downscale. All platforms.
79
+ - **Cheap symbol→address:** `symbols({op:'resolve', dbgPath|mapPath, name})` reads
80
+ the map FILE on disk (no 63 KB round-trip through context); `build({output:
81
+ 'romWithDebug', resolveSymbols:[...]})` folds just those addresses into the
82
+ result. cc65 `.dbg` / sdld `.map` / GNU ld `.map`.
83
+
84
+ ### Added — Genesis feel/perf diagnostics
85
+ - **`watch({on:'dma', perFrame:true})`** — per-frame DMA-bytes timeline + spike
86
+ detection (catches "rewriting tilemaps in the frame loop", the #1 Genesis feel
87
+ bug). A hardware-scroll scaffold settles to a flat ~8 bytes/frame.
88
+ - **`scaffold` template `two_plane_parallax`** — plane-A foreground + plane-B
89
+ repeated starfield + player sprite, hardware scroll only, zero loop-time tilemap
90
+ writes. Builds clean, renders, scrolls (verified).
91
+ - Genesis MENTAL_MODEL/TROUBLESHOOTING: "do NOT rewrite tilemaps in the frame
92
+ loop", logical-vs-hardware plane size, the correct parallax loop, Sonic-style
93
+ column streaming, and a "why does movement feel choppy?" recipe.
94
+
95
+ ### Changed — discoverability (the recurring root cause)
96
+ Several feedback "feature asks" were tools that already existed. `recordSession`
97
+ (motion/telemetry timeline) was mislabeled in the catalog as "capture inputs for
98
+ replay"; relabeled. New AGENTS.md "Diagnosing behavior over time (game-feel)"
99
+ section maps symptom → existing tool (choppy movement → `recordSession`/`watch`
100
+ series on scroll+sprite; wrong-but-clean value → `resolveSymbols` + `memory` read;
101
+ can't-read-the-BG → `background({view:'map'})` + `screenshot scale:4`).
102
+
103
+ ### Other
104
+ - `build({output:'project', path})` already defaults the gb/gbc/sms/gg/msx crt0 +
105
+ codeLoc — documented (the GBC agent was hand-passing them to `output:'rom'`).
106
+ - P4 doc + a new info-level `wram-static-overlap` lint advisory (hardcoded
107
+ `$C000–$C0FF` pointer overlapping SDCC's static-data segment — the actual cause
108
+ of the reporter's "monochrome RNG").
109
+
7
110
  ## 0.22.1
8
111
 
9
112
  Doc-only follow-up to 0.22.0's movement-analysis feedback: the `pressDuring`
@@ -13,7 +13,11 @@
13
13
  * level fits in the scroll plane and plain VDP_setHorizontalScroll moves
14
14
  * it — a real working scroller with no streaming. For a world WIDER than
15
15
  * 512 px you additionally stream the column entering view each time camX
16
- * crosses an 8-px boundary (see MENTAL_MODEL.md "Horizontal scrolling").
16
+ * crosses an 8-px boundary (see MENTAL_MODEL.md "How Sonic-style large
17
+ * maps REALLY work"). For a from-scratch smooth-scroll + parallax
18
+ * starting point with NO per-frame tilemap writes, see the
19
+ * two_plane_parallax template + MENTAL_MODEL.md "Scrolling, parallax &
20
+ * the feel trap".
17
21
  */
18
22
 
19
23
  #include <genesis.h>
@@ -0,0 +1,166 @@
1
+ /* ── two_plane_parallax.c — Genesis SGDK two-plane parallax scaffold ──
2
+ *
3
+ * The "Uridium / Sonic-feel" starting point: a side-scrolling world that
4
+ * moves SMOOTHLY because the frame loop does HARDWARE SCROLL ONLY. There
5
+ * are ZERO tilemap writes inside the loop — the two planes are painted
6
+ * ONCE at setup, and every frame we just nudge two scroll registers and
7
+ * re-stage one sprite. This is the single most important thing to copy:
8
+ *
9
+ * ★ Do NOT rewrite tilemaps in the frame loop. ★
10
+ *
11
+ * Rewriting a plane each frame is a big VDP/DMA burst that overruns
12
+ * vblank and makes movement feel choppy/juddery. Hardware scroll is free.
13
+ *
14
+ * Layout:
15
+ * - Plane A (foreground): a painted "world" — a ground strip + scattered
16
+ * platform blocks across a 512-px (64-cell) plane. Scrolls 1:1 with
17
+ * the camera.
18
+ * - Plane B (background): a repeated starfield filling the whole plane.
19
+ * Scrolls at 1/4 the camera speed for parallax depth (far things
20
+ * move slower). Because Plane B is filled edge-to-edge and the VDP
21
+ * scroll wraps within the plane, it tiles forever with no redraw.
22
+ * - One hardware sprite = the player, drawn in SCREEN space.
23
+ *
24
+ * Drive it: D-pad LEFT/RIGHT scrolls the camera (the player walks within
25
+ * the world). The camera clamps to the world edges.
26
+ *
27
+ * IMPORTANT (logical vs hardware plane size): the Genesis has ONE shared
28
+ * plane-size setting for BOTH planes. We use the default 64x32 cells
29
+ * (512x256 px). A "32-wide level" still lives inside a 64-wide PHYSICAL
30
+ * plane — you don't get an independent per-plane size. See the Genesis
31
+ * MENTAL_MODEL.md "Scrolling, parallax & the feel trap".
32
+ *
33
+ * To go WIDER than 512 px (a true Sonic-size level) you keep this exact
34
+ * loop and add ONE thing: stream the single offscreen column that's about
35
+ * to scroll into view each time the camera crosses an 8-px tile boundary
36
+ * (a circular buffer in the 64-cell plane) — NOT a whole-plane redraw.
37
+ * See MENTAL_MODEL.md "How Sonic-style large maps really work".
38
+ */
39
+
40
+ #include <genesis.h>
41
+
42
+ /* ── Tiles (4bpp, 8 rows x 4 bytes). Indices live at/above TILE_USER_INDEX
43
+ * so they don't clobber SGDK's font + system tiles below it. ── */
44
+ #define T_BLANK (TILE_USER_INDEX + 0)
45
+ #define T_GROUND (TILE_USER_INDEX + 1)
46
+ #define T_BLOCK (TILE_USER_INDEX + 2)
47
+ #define T_PLAYER (TILE_USER_INDEX + 3)
48
+ #define T_STAR (TILE_USER_INDEX + 4)
49
+
50
+ static const u32 tile_blank[8] = { 0,0,0,0,0,0,0,0 };
51
+ /* Solid ground: lit top edge, darker body. */
52
+ static const u32 tile_ground[8] = {
53
+ 0x11111111, 0x12222221, 0x12222221, 0x12222221,
54
+ 0x12222221, 0x12222221, 0x12222221, 0x12222221,
55
+ };
56
+ /* A floating platform block. */
57
+ static const u32 tile_block[8] = {
58
+ 0x33333333, 0x34444443, 0x34444443, 0x34444443,
59
+ 0x34444443, 0x34444443, 0x34444443, 0x33333333,
60
+ };
61
+ /* Player: a chunky diamond so it reads on screen. */
62
+ static const u32 tile_player[8] = {
63
+ 0x00055000, 0x00555500, 0x05555550, 0x55555555,
64
+ 0x55555555, 0x05555550, 0x00555500, 0x00055000,
65
+ };
66
+ /* A single off-center star pixel on a dark field (background twinkle). */
67
+ static const u32 tile_star[8] = {
68
+ 0x00000000, 0x00060000, 0x00000000, 0x00000600,
69
+ 0x00000000, 0x06000000, 0x00000000, 0x00000060,
70
+ };
71
+
72
+ #define PLANE_W_CELLS 64 /* default plane is 64x32 cells */
73
+ #define WORLD_W 512 /* = 64 cells * 8 px (one plane wide) */
74
+ #define SCREEN_W 320 /* H40 */
75
+ #define GROUND_Y 192 /* px — top of the ground strip */
76
+
77
+ /* Static foreground platforms in WORLD PIXEL coords. Painted ONCE. */
78
+ typedef struct { s16 x, y, wcells; } Block;
79
+ static const Block blocks[] = {
80
+ { 48, 152, 5 }, { 160, 128, 4 }, { 256, 160, 6 },
81
+ { 352, 112, 4 }, { 432, 144, 5 },
82
+ };
83
+ #define N_BLOCKS (sizeof(blocks) / sizeof(blocks[0]))
84
+
85
+ /* Player WORLD x exposed for headless motion-trace inspection:
86
+ * symbols({op:'resolve', name:'g_player_x'}) → read it over frames while
87
+ * holding RIGHT and compare against the camera scroll (see MENTAL_MODEL
88
+ * "Why does horizontal movement feel choppy?"). `volatile` keeps it from
89
+ * being optimised into a register so the read resolves. */
90
+ volatile s16 g_player_x = 32; /* world px */
91
+ volatile s16 g_cam_x = 0; /* camera scroll (world px) */
92
+
93
+ int main(bool hard) {
94
+ (void)hard;
95
+
96
+ /* Palettes (BGR, 3 bits/chan). PAL0 = sprite, PAL1 = planes. */
97
+ PAL_setColor(0 + 5, 0x008E); /* player orange (Uridium-ish) */
98
+ PAL_setColor(16 + 1, 0x0A86); /* ground top (light) */
99
+ PAL_setColor(16 + 2, 0x0530); /* ground body (dark) */
100
+ PAL_setColor(16 + 3, 0x0CCC); /* block edge (white) */
101
+ PAL_setColor(16 + 4, 0x0844); /* block body (steel) */
102
+ PAL_setColor(16 + 6, 0x0EEE); /* star (white) */
103
+
104
+ /* Upload all tiles once. */
105
+ VDP_loadTileData(tile_blank, T_BLANK, 1, DMA);
106
+ VDP_loadTileData(tile_ground, T_GROUND, 1, DMA);
107
+ VDP_loadTileData(tile_block, T_BLOCK, 1, DMA);
108
+ VDP_loadTileData(tile_player, T_PLAYER, 1, DMA);
109
+ VDP_loadTileData(tile_star, T_STAR, 1, DMA);
110
+
111
+ /* ── PAINT PLANE B (starfield) ONCE — fills the whole 64x32 plane so
112
+ * it tiles forever as the scroll wraps. NEVER touched again. ── */
113
+ VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_STAR),
114
+ 0, 0, PLANE_W_CELLS, 32);
115
+
116
+ /* ── PAINT PLANE A (foreground world) ONCE. ──
117
+ * Ground strip across the whole world width, then the platform blocks
118
+ * on top. After this, the loop does NOT write the tilemap again. */
119
+ VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_GROUND),
120
+ 0, GROUND_Y >> 3, PLANE_W_CELLS, (256 - GROUND_Y) >> 3);
121
+ for (u16 i = 0; i < N_BLOCKS; i++) {
122
+ const Block* b = &blocks[i];
123
+ VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, T_BLOCK),
124
+ b->x >> 3, b->y >> 3, b->wcells, 1);
125
+ }
126
+
127
+ VDP_drawText("PARALLAX D-PAD = SCROLL", 7, 1);
128
+
129
+ const s16 PLAYER_SPEED = 2;
130
+ s16 player_screen_x = SCREEN_W / 2 - 4; /* player stays mid-screen */
131
+
132
+ while (TRUE) {
133
+ u16 pad = JOY_readJoypad(JOY_1);
134
+
135
+ /* Move the player through the WORLD. */
136
+ if (pad & BUTTON_LEFT) g_player_x -= PLAYER_SPEED;
137
+ if (pad & BUTTON_RIGHT) g_player_x += PLAYER_SPEED;
138
+ if (g_player_x < 0) g_player_x = 0;
139
+ if (g_player_x > WORLD_W - 8) g_player_x = WORLD_W - 8;
140
+
141
+ /* Camera centers on the player, clamped to the world edges. */
142
+ s16 cam = g_player_x - (SCREEN_W / 2 - 4);
143
+ if (cam < 0) cam = 0;
144
+ if (cam > WORLD_W - SCREEN_W) cam = WORLD_W - SCREEN_W;
145
+ g_cam_x = cam;
146
+
147
+ /* When the camera is hard against an edge, let the player walk to
148
+ * the screen edge instead of staying pinned to the centre. */
149
+ player_screen_x = g_player_x - cam;
150
+
151
+ /* ★ THE ENTIRE PER-FRAME RENDER COST: two scroll-register writes.
152
+ * Positive cam scrolls the plane LEFT, so we write the negative.
153
+ * Plane A: 1:1 with the world. Plane B: 1/4 speed = parallax. */
154
+ VDP_setHorizontalScroll(BG_A, -cam);
155
+ VDP_setHorizontalScroll(BG_B, -(cam >> 2));
156
+
157
+ /* Re-stage the one player sprite (screen space) + flush the SAT.
158
+ * SPRITE_SIZE(1,1) = 8x8. */
159
+ VDP_setSprite(0, player_screen_x, GROUND_Y - 8, SPRITE_SIZE(1, 1),
160
+ TILE_ATTR_FULL(PAL0, 1, 0, 0, T_PLAYER));
161
+ VDP_updateSprites(1, DMA);
162
+
163
+ SYS_doVBlankProcess();
164
+ }
165
+ return 0;
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "romdevtools",
3
- "version": "0.22.1",
3
+ "version": "0.24.0",
4
4
  "description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 14 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
@@ -57,7 +57,7 @@
57
57
  "romdev-core-gpgx": "0.10.0",
58
58
  "romdev-core-handy": "0.5.0",
59
59
  "romdev-core-prosystem": "0.6.0",
60
- "romdev-core-vice": "0.6.0",
60
+ "romdev-core-vice": "0.7.0",
61
61
  "romdev-famitone": "0.1.0",
62
62
  "romdev-maxmod": "0.1.0",
63
63
  "romdev-platform-atari2600": "0.6.0",