romdevtools 0.13.0 → 0.15.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 (124) hide show
  1. package/AGENTS.md +21 -14
  2. package/CHANGELOG.md +125 -1
  3. package/README.md +13 -8
  4. package/examples/atari2600/main.asm +1 -1
  5. package/examples/atari2600/templates/default.asm +1 -1
  6. package/examples/atari2600/templates/paddle.asm +59 -47
  7. package/examples/atari7800/main.c +1 -1
  8. package/examples/atari7800/templates/default.c +1 -1
  9. package/examples/atari7800/templates/music_demo.c +1 -1
  10. package/examples/c64/main.c +1 -1
  11. package/examples/c64/templates/platformer.c +2 -2
  12. package/examples/c64/templates/puzzle.c +1 -1
  13. package/examples/c64/templates/racing.c +3 -3
  14. package/examples/c64/templates/shmup.c +6 -5
  15. package/examples/c64/templates/sports.c +4 -4
  16. package/examples/gb/main.asm +1 -1
  17. package/examples/gb/main.c +1 -1
  18. package/examples/gb/templates/puzzle.c +1 -1
  19. package/examples/gb/templates/racing.c +1 -1
  20. package/examples/gb/templates/shmup.c +1 -1
  21. package/examples/gba/templates/gba_hello.c +1 -1
  22. package/examples/gba/templates/maxmod_demo.c +1 -1
  23. package/examples/gba/templates/puzzle.c +17 -3
  24. package/examples/gba/templates/racing.c +16 -2
  25. package/examples/gba/templates/shmup.c +23 -4
  26. package/examples/gba/templates/tonc_hello.c +6 -4
  27. package/examples/gbc/main.asm +1 -1
  28. package/examples/gbc/templates/puzzle.c +1 -1
  29. package/examples/gbc/templates/racing.c +1 -1
  30. package/examples/gbc/templates/shmup.c +1 -1
  31. package/examples/genesis/main.s +1 -1
  32. package/examples/genesis/templates/puzzle.c +1 -1
  33. package/examples/genesis/templates/racing.c +45 -1
  34. package/examples/genesis/templates/shmup.c +12 -3
  35. package/examples/genesis/templates/shmup_2p.c +2 -2
  36. package/examples/genesis/templates/sports.c +39 -0
  37. package/examples/gg/templates/hello_sprite.c +38 -23
  38. package/examples/gg/templates/music_demo.c +11 -8
  39. package/examples/gg/templates/platformer.c +37 -15
  40. package/examples/gg/templates/racing.c +25 -12
  41. package/examples/gg/templates/shmup.c +12 -6
  42. package/examples/gg/templates/sports.c +30 -16
  43. package/examples/gg/templates/tile_engine.c +24 -10
  44. package/examples/lynx/templates/platformer.c +7 -1
  45. package/examples/lynx/templates/puzzle.c +8 -2
  46. package/examples/lynx/templates/racing.c +7 -1
  47. package/examples/lynx/templates/sports.c +7 -1
  48. package/examples/nes/main.c +2 -2
  49. package/examples/nes/space-shooter/nes_runtime.h +1 -1
  50. package/examples/nes/templates/default.c +4 -1
  51. package/examples/nes/templates/racing.c +50 -1
  52. package/examples/pce/main.c +1 -1
  53. package/examples/sms/templates/hello_sprite.c +1 -1
  54. package/examples/sms/templates/music_demo.c +1 -1
  55. package/examples/sms/templates/puzzle.c +1 -1
  56. package/examples/sms/templates/racing.c +1 -1
  57. package/examples/sms/templates/shmup.c +1 -1
  58. package/examples/sms/templates/shmup_2p.c +2 -2
  59. package/examples/snes/main.asm +1 -1
  60. package/examples/snes/templates/c-hello-data.asm +309 -14
  61. package/examples/snes/templates/c-hello.c +13 -2
  62. package/examples/snes/templates/default.c +1 -1
  63. package/examples/snes/templates/hello_sprite-data.asm +300 -2
  64. package/examples/snes/templates/hello_sprite.c +10 -1
  65. package/examples/snes/templates/music_demo-data.asm +300 -2
  66. package/examples/snes/templates/music_demo.c +10 -1
  67. package/examples/snes/templates/platformer-data.asm +300 -2
  68. package/examples/snes/templates/platformer.c +10 -1
  69. package/examples/snes/templates/puzzle-data.asm +300 -2
  70. package/examples/snes/templates/puzzle.c +11 -1
  71. package/examples/snes/templates/racing-data.asm +300 -2
  72. package/examples/snes/templates/racing.c +40 -4
  73. package/examples/snes/templates/shmup-data.asm +299 -6
  74. package/examples/snes/templates/shmup.c +11 -7
  75. package/examples/snes/templates/sports-data.asm +300 -2
  76. package/examples/snes/templates/sports.c +40 -5
  77. package/package.json +1 -1
  78. package/src/cheats/lookup.js +39 -18
  79. package/src/http/routes.js +58 -33
  80. package/src/http/skill-doc.js +10 -9
  81. package/src/http/swagger.js +1 -1
  82. package/src/http/tool-registry.js +72 -5
  83. package/src/mcp/server.js +6 -5
  84. package/src/mcp/state.js +8 -6
  85. package/src/mcp/tool-manifest.js +7 -7
  86. package/src/mcp/tools/cheats.js +4 -3
  87. package/src/mcp/tools/index.js +18 -2
  88. package/src/mcp/tools/playtest.js +48 -35
  89. package/src/mcp/tools/project.js +39 -73
  90. package/src/mcp/tools/rom-id.js +49 -4
  91. package/src/mcp/tools/tile-inspect.js +1 -1
  92. package/src/mcp/tools/toolchain.js +183 -19
  93. package/src/mcp/tools/trace-vram-source.js +3 -3
  94. package/src/mcp/tools/watch-memory.js +27 -46
  95. package/src/observer/livestream.html +41 -5
  96. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +5 -5
  97. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  98. package/src/platforms/gb/TROUBLESHOOTING.md +1 -1
  99. package/src/platforms/gb/UPSTREAM_SOURCES.md +1 -1
  100. package/src/platforms/gb/lib/c/README.md +2 -2
  101. package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +1 -1
  102. package/src/platforms/gbc/MENTAL_MODEL.md +3 -3
  103. package/src/platforms/gbc/TROUBLESHOOTING.md +5 -5
  104. package/src/platforms/gbc/UPSTREAM_SOURCES.md +2 -2
  105. package/src/platforms/gbc/lib/c/README.md +2 -2
  106. package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +1 -1
  107. package/src/platforms/gg/MENTAL_MODEL.md +14 -13
  108. package/src/platforms/gg/lib/c/vdp_init.c +10 -8
  109. package/src/platforms/msx/MENTAL_MODEL.md +1 -1
  110. package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
  111. package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
  112. package/src/platforms/pce/MENTAL_MODEL.md +1 -1
  113. package/src/platforms/pce/lib/c/pce_hw.h +1 -0
  114. package/src/platforms/pce/lib/c/pce_video.c +26 -0
  115. package/src/platforms/sms/MENTAL_MODEL.md +12 -12
  116. package/src/platforms/sms/lib/c/vdp_init.c +10 -8
  117. package/src/platforms/sms/lib/vdp_init.s +1 -1
  118. package/src/playtest/playtest.js +25 -0
  119. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
  120. package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
  121. package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
  122. package/src/toolchains/genesis-c/README.md +1 -1
  123. package/src/toolchains/sdcc/preflight-lint.js +47 -7
  124. package/src/toolchains/snes-c/snes-c.js +3 -7
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ You are reading this because romdev is connected. This is the orientation. Read
4
4
 
5
5
  ## What this server does
6
6
 
7
- Drives the full homebrew ROM dev loop for 14 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace which ROM offset a Genesis graphic was DMA'd from (`dmaTrace({precision:'sampled'})`), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
7
+ Drives the full homebrew ROM dev loop for 14 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace which ROM offset a Genesis graphic was DMA'd from (`watch({on:'dma', precision:'sampled'})`), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
8
8
 
9
9
  You drive the work. The human is a director — they may want a game, a ROM disassembly, a tool-assisted reverse-engineering session, or anything else this server can do.
10
10
 
@@ -54,7 +54,7 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
54
54
  - `input` — drive controllers, look up hardware bit layouts. `navigate` walks menus by advancing on SCREEN CHANGE (not fixed frames) and reports whether each press was consumed — the fast, reliable way to script a UI.
55
55
  - `state` — savestates and forensic state inspection (`state({op:'save'})`, `state({op:'load'})`, `state({op:'export'})` a slot to disk without touching the live host, `state({op:'list'})`, `state({op:'dump'})`)
56
56
  - `memory` — read/write VRAM/OAM/CGRAM/ARAM and other regions (all 14 platforms). `memory({op:'read'})` takes `offsets:[…]` to batch scattered reads in one call. **`memory({op:'search'})`/`memory({op:'searchNext'})`** = the Cheat-Engine value-search loop ("find the address of X, narrow as X changes"). **`memory({op:'readCart'})`** reads the loaded cart image to confirm a patch is live. **`memory({op:'classify'})`** says whether bytes look like ASCII/code/tile-data (kills the "found table that's really a string" trap). `memory({op:'snapshot'})` + `memory({op:'diff'})` answer "which bytes changed across this event?" (diff defaults to a clustered summary with stride detection); `state({op:'diff'})` is the coarse whole-machine version.
57
- - `debug` — `sprites({op:'inspect'})`, `palette({source:'live'})`, `cpu({op:'read'})` (all 14), `audioDebug({op:'inspect'})` (the 12 systems with a sound chip — all but Atari 2600/7800; pass `frames:N` to TRACE a per-channel note-timeline for headless melody asserts), `background({view:'renderState'})`, `breakpoint({on:'write'})` (write watchpoint, all 14), **`dmaTrace({precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`disasm({target:'bytes'|'rom'|'references'|'project'})`** (ALL 14 — native binutils objdump per CPU, incl. GBA ARM7/Thumb; the byte-exact `disasm({target:'project'})` reassembles through native as/ld/objcopy), `symbols({op})` lookup, `background({view:'rendered'})`, plus **`cheats({op})`** (`cheats({op:'lookup'})` = a free labeled RAM/code map for known ROMs, `cheats({op:'search'})` to fuzzy-find a game by name, `cheats({op:'apply'})`/`cheats({op:'clear'})` non-destructively, `cheats({op:'make'})` to create codes)
57
+ - `debug` — `sprites({op:'inspect'})`, `palette({source:'live'})`, `cpu({op:'read'})` (all 14), `audioDebug({op:'inspect'})` (the 12 systems with a sound chip — all but Atari 2600/7800; pass `frames:N` to TRACE a per-channel note-timeline for headless melody asserts), `background({view:'renderState'})`, `breakpoint({on:'write'})` (write watchpoint, all 14), **`watch({on:'dma', precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`disasm({target:'bytes'|'rom'|'references'|'project'})`** (ALL 14 — native binutils objdump per CPU, incl. GBA ARM7/Thumb; the byte-exact `disasm({target:'project'})` reassembles through native as/ld/objcopy), `symbols({op})` lookup, `background({view:'rendered'})`, plus **`cheats({op})`** (`cheats({op:'lookup'})` = a free labeled RAM/code map for known ROMs, `cheats({op:'search'})` to fuzzy-find a game by name, `cheats({op:'apply'})`/`cheats({op:'clear'})` non-destructively, `cheats({op:'make'})` to create codes)
58
58
  - `assets` — convert PNGs to tiles (`encodeArt`/`importArt`), WAVs to BRR, identify ROMs (`cart({op:'identify'})`), plus the hacking toolkit (`romPatch({op})` — write/writeMany/spliceCHR/relocate/makeStored/findFree/findPointer/diff, `assembleSnippet`, `cart({op:'extract'})`, `cart({op:'wrap'})`)
59
59
  - `project` — starter snippets per platform
60
60
  - `show` — `playtest({op})`: `op:'open'` opens the live SDL window for a human, `op:'stop'` closes it, `op:'status'` reports liveness, `op:'framebuffer'` captures exactly what the human's window shows
@@ -242,14 +242,21 @@ your project dir, it lands at `./read_joystick.asm` (alongside
242
242
  `main.asm`), NOT under `./include/` or `./lib/`. Every platform
243
243
  follows the same flat layout.
244
244
 
245
- Because the layout is flat, **`build({output:'project', path, platform})` rebuilds the
246
- whole directory in one call no per-iteration file manifest.** It finds `main.c`
247
- (C / SGDK Genesis / GBA / cc65-C / SDCC-C) or `main.s` / `main.asm` (asm), links every
248
- `.c`/`.s` in the dir, treats `.h`/`.inc` as includes, and folds binary assets
249
- (`.bin/.chr/.pcm/.brr/.vgm/...`) in as `binaryIncludes`. So iterating an on-disk project is
250
- just `build({output:'project', path:'/my/proj', platform})` every time you don't re-send
251
- `sources`/`includes` each build. (Use `build({output:'rom'})` with explicit `sources` when
252
- the files aren't on disk, e.g. generated in-context.)
245
+ Because the layout is flat, **the simplest loop is `build({output:'run', path, platform})`
246
+ (build + load + run + screenshot in one call) or `build({output:'project', path, platform})`
247
+ (build the dir to a ROM) no per-iteration file manifest, on EVERY platform.** Point it at
248
+ a scaffolded directory and it does the right per-platform thing automatically: finds the
249
+ entry (`main.c` for C / SGDK Genesis / GBA / cc65-C / SDCC-C, or `main.s` / `main.asm` for
250
+ asm), routes the platform's crt0 correctly (e.g. GB/GBC `gb_crt0.s` via the cart-header path,
251
+ not as a plain source so no `gsinit` collision), applies the right linker preset
252
+ (e.g. NES `chr-ram-runtime`, which supplies the OAM/CHARS segments), skips SDK intermediates
253
+ (e.g. Genesis `sega.preprocessed.s`, the SNES SPC700 driver, any `*.upstream.*`), wires the
254
+ runtime (GBA libtonc/libgba/maxmod by what the source includes), routes `#include`d C/asm
255
+ siblings as includes, treats `.h`/`.inc` as includes, and folds binary assets
256
+ (`.bin/.chr/.pcm/.brr/.vgm/.xgc/...`) in as `binaryIncludes`. So iterating an on-disk project
257
+ is just `build({output:'run', path:'/my/proj', platform})` every time — you do **not** need to
258
+ enumerate `sources`/`includes`/`crt0Path`/`linkerConfig` by hand. (Use `build({output:'rom'})`
259
+ with explicit `sources` only when the files aren't on disk, e.g. generated in-context.)
253
260
 
254
261
  ## Supported platforms
255
262
 
@@ -297,7 +304,7 @@ Different platforms have different levels of MCP-exposed debugging — different
297
304
  > lookup/apply/create), `cpu({op:'read'})`, `memory({op:'search'})`/`memory({op:'searchNext'})`/`memory({op:'readCart'})`/`memory({op:'classify'})`,
298
305
  > `memory({op:'snapshot'})`/`memory({op:'diff'})`/`state({op:'diff'})`, `watch({on:'mem'})`/`breakpoint({on:'write',precision:'sampled'})`.
299
306
  > `audioDebug({op:'inspect'})` covers the 12 systems with a sound chip (all but Atari 2600/7800).
300
- > **`dmaTrace({precision:'exact'})`** (which DMA wrote a VRAM tile, and from where) is **Genesis-only**
307
+ > **`watch({on:'dma', precision:'exact'})`** (which DMA wrote a VRAM tile, and from where) is **Genesis-only**
301
308
  > (VDP DMA) — elsewhere use `breakpoint({on:'write'})`/`watch({on:'range'})`. All other RE tools above
302
309
  > work on every platform that has the register-write/watch core hooks (all 14).
303
310
  > `disasm({target:'rom'})` + `disasm({target:'references'})` + `disasm({target:'project'})` cover **all 14** — every
@@ -881,7 +888,7 @@ OAM format: bytes per sprite are `[y, tileIndex, attributes, x]`.
881
888
 
882
889
  Three shapes, pick the one that matches what you're doing:
883
890
 
884
- - **`scaffold({op:'project', platform, name, path, template?})`** — writes a starter directory: `main.{c,asm,s}` (from `examples/<platform>/templates/`) + every runtime file the template depends on (headers, crt0, linker .cfg) + README + `.gitignore`. Self-contained: take it elsewhere and rebuild with stock cc65/sdcc, no romdev install needed. Defaults to `template:"default"` (smallest visible-and-runnable program); most tier-1 platforms also have `hello_sprite` + `tile_engine` + the 5 genre templates.
891
+ - **`scaffold({op:'project', platform, name, path, template?})`** — writes a starter directory: `main.{c,asm,s}` (from `examples/<platform>/templates/`) + every runtime file the template depends on (headers, crt0, linker .cfg) + README + `.gitignore`. Self-contained: take it elsewhere and rebuild with stock cc65/sdcc, no romdev install needed. Defaults to `template:"default"` (smallest visible-and-runnable program); most tier-1 platforms also have `hello_sprite` + `tile_engine` + the 5 genre templates. **Then build it in ONE call: `build({output:'run', path:<that dir>, platform})`** — the dir build applies the platform's recipe (crt0/linker-preset/runtime/intermediate-skip) automatically, so you never hand-wire `crt0Path`/`codeLoc`/`linkerConfig`. This scaffold→build path is verified to build + render on every platform/template.
885
892
 
886
893
  - **`scaffold({op:'project', ..., withSnippets: true})`** — same as above, **plus** drops every vetted starter snippet for the platform alongside main.c. Use when you want "main.c + every helper file ready to edit" in one shot, without picking a genre. Snippets that overlap with the template's runtime are skipped (no double-writes). Response includes `snippetsCopied: string[]`.
887
894
 
@@ -1044,10 +1051,10 @@ A few platform-tool quirks worth knowing up front:
1044
1051
  - **asar bank-border-crossed** can happen if your `org` + `dw` runs past $00FFFF. Native vectors are at $FFE4-$FFEE; emulation vectors at $FFF4-$FFFF. Use `scaffold({op:'snippets', platform: "snes", mode: "get", name: "lorom_header.asm"})` for the layout.
1045
1052
  - **cc65 (NES, C64, etc.) zero page** starts at $02. cc65 reserves $00-$01 for its runtime. Your first `.res 1` lands at $02, not $00. Use `symbols({op:'map'})` after `build({output:'romWithDebug'})` to confirm.
1046
1053
  - **NES pattern table cap = 256 tiles per nametable**. The tilemap index is 8-bit, so per-frame BG can use at most 256 unique tiles per pattern table. Auto-converting a busy illustration usually overflows. `encodeArt({stage:'tilemap'})` warns; the only workaround is mid-frame CHR bank switching (MMC3-class mapper).
1047
- - **NES + GB/GBC turnkey** (R9/R10 self-contained + sound, 2026-05-25): use `scaffold({op:'project', platform, template, name, path})` to scaffold a project. The pipeline copies every file the template depends on — `{nes,gb}_runtime.{h,c}`, `gb_hardware.h`, custom `crt0.s`, linker `.cfg`, `patch-header.js` (GB) — into the project directory alongside `main.c`. **No auto-injection at build time.** The build pipeline compiles exactly what you tell it via `sources` / `sourcesPaths` / `includes` / `includePaths` / `crt0` / `crt0Path` / `linkerConfig` / `codeLoc`. Take the project elsewhere with stock cc65/sdcc and it builds the same way. The runtime APIs include sprites, BG, input, AND **sound** — `sound_init` / `sound_play_tone(channel, period, vol, length)` / `sound_play_noise` / `sound_off`. NES drives pulse1+pulse2+triangle+noise via $4000-$400F + $4015; GB drives the 4-channel APU via NR10-NR52. SFX-grade, fire-and-forget — for full music tracks, drop in famitone2 (NES) or your own driver. Templates: `default` (palette cycle), `hello_sprite` (sprite + d-pad + **beep on A press**), `tile_engine` (multi-room tile map). Docs: [`src/platforms/nes/MENTAL_MODEL.md`](src/platforms/nes/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/nes/TROUBLESHOOTING.md); [`src/platforms/gb/MENTAL_MODEL.md`](src/platforms/gb/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/gb/TROUBLESHOOTING.md). **Game-loop order matters on NES:** stage `oam_clear`+`oam_spr` BEFORE `ppu_wait_nmi`, not after — the NMI handler DMA's whatever shadow_oam contains at vblank-start. **GB ROM header:** both asm and C builds now auto-run `rgbfix` inside `build({output:'rom'})`, so the Nintendo logo + checksums + CGB flag are correct out of the box — no manual `patchGbHeader` step needed.
1054
+ - **NES + GB/GBC turnkey** (R9/R10 self-contained + sound, 2026-05-25): use `scaffold({op:'project', platform, template, name, path})` to scaffold a project. The pipeline copies every file the template depends on — `{nes,gb}_runtime.{h,c}`, `gb_hardware.h`, custom `crt0.s`, linker `.cfg`, `patch-header.js` (GB) — into the project directory alongside `main.c`. **No auto-injection at build time.** Iterate the whole dir in one call with `build({output:'run', path, platform})` (or supply `sources` / `sourcesPaths` / `includes` / `includePaths` / `crt0` / `crt0Path` / `linkerConfig` / `codeLoc` explicitly when the files aren't on disk). Take the project elsewhere with stock cc65/sdcc and it builds the same way. The runtime APIs include sprites, BG, input, AND **sound** — `sound_init` / `sound_play_tone(channel, period, vol, length)` / `sound_play_noise` / `sound_off`. NES drives pulse1+pulse2+triangle+noise via $4000-$400F + $4015; GB drives the 4-channel APU via NR10-NR52. SFX-grade, fire-and-forget — for full music tracks, drop in famitone2 (NES) or your own driver. Templates: `default` (palette cycle), `hello_sprite` (sprite + d-pad + **beep on A press**), `tile_engine` (multi-room tile map). Docs: [`src/platforms/nes/MENTAL_MODEL.md`](src/platforms/nes/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/nes/TROUBLESHOOTING.md); [`src/platforms/gb/MENTAL_MODEL.md`](src/platforms/gb/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/gb/TROUBLESHOOTING.md). **NES sprite staging:** the bundled `nes_runtime` now handles the OAM-DMA race for you (oam_clear just resets the index; ppu_wait_nmi hides unused slots after you stage), so the old "must stage before ppu_wait_nmi" flicker trap is gone — the standard `oam_clear oam_spr ppu_wait_nmi` loop renders cleanly. **GB ROM header:** both asm and C builds auto-run `rgbfix` inside `build({output:'rom'/'run'})`, so the Nintendo logo + checksums + CGB flag are correct out of the box — no manual header-patch step needed (use `romPatch({op:'gbHeader'})` only to fix up an external ROM).
1048
1055
  - **Game Boy / GBC silent-failure footguns** (R54 cleanup, full detail in `platform({op:'doc', platform:"gb"|"gbc", name:"mental_model"})`):
1049
1056
  - **The bundled `gb_crt0.s` is now actually linked.** Pre-r54 a fundamental bug in `buildZ80C` was shipping the raw .s text to sdld as if pre-assembled — sdld silently rejected it and fell back to SDCC's stock sm83 crt0 (no GB cart boot, no IRQ vectors). Map showed no `init` symbol, $0000 was $FF, $0100 was $FF. Every GB ROM ran on stock crt0 invisibly. Fixed by auto-detecting .s source vs .rel object and running it through sdasgb first. Post-fix: `init` at $0150, entry $0100 = `00 c3 50 01` (nop; jp $0150), reset vector $0000 = $C9. **This was the root cause for #14 audio AND part of why every previous "runtime should work OOTB" round still felt friction-heavy.**
1050
- - **GB/GBC C builds now auto-fix the header at build time** (rgbfix runs inside `build({output:'rom'})`): Nintendo logo at $0104, header checksum at $014D, global checksum, and the CGB flag at $0143 ($00 for `.gb`, $C0 for `.gbc`). You no longer need to call `patchGbHeader` manually — the ROM `build({output:'rom'})` hands back boots on real hardware as-is. `patchGbHeader({path})` still exists if you want to override title / cart type / RAM size / etc. on an existing file.
1057
+ - **GB/GBC C builds now auto-fix the header at build time** (rgbfix runs inside `build({output:'rom'})`): Nintendo logo at $0104, header checksum at $014D, global checksum, and the CGB flag at $0143 ($00 for `.gb`, $C0 for `.gbc`). You no longer need to patch the header manually — the ROM `build({output:'rom'})` hands back boots on real hardware as-is. `romPatch({op:'gbHeader', path})` still exists if you want to override title / cart type / RAM size / etc. on an existing file.
1051
1058
  - **`shadow_oam` is pinned at $C100** in the bundled `gb_runtime.c` via `__at(0xC100)`. OAM DMA reads ONLY the high byte and copies 160 bytes from `$XX00` — a plain `uint8_t my_oam[160]` may land at $C017 and DMA garbage. If you roll your own OAM buffer, pick an address with `0x00` low byte (e.g. $C200) and pass it directly to `oam_dma_copy`.
1052
1059
  - **Call `enable_vblank_irq()` once at boot.** Without it, `wait_vblank()` busy-polls `LY` which updates only at WASM `frame({op:'step'})` quantum boundaries → game loop runs at ~1/30 intended speed on the emulator. After enable, `wait_vblank()` compiles to `HALT` + vblank IRQ wake (~10 cycles per frame).
1053
1060
  - **Use `memcpy_vram(dst, src, n)` for VRAM bulk writes**, NOT raw `(uint8_t*)0x8000` casts — SDCC sm83 may elide the latter as dead code. The bundled `gb_hardware.h` declares every $FFxx register as `volatile`-typed so direct writes like `BGP = 0xE4;` are fine; the hazard is only on cast-through-pointer block copies.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,130 @@ 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.15.0
8
+
9
+ **Scaffold audit: every scaffold on every platform now builds AND renders.** Two
10
+ independent multi-agent audits found the documented "scaffold then build the dir"
11
+ happy path was broken on most platforms, and that many scaffolds built but
12
+ rendered blank. Both are fixed — verified 115/115 templates build via the dir
13
+ path, and every genre game renders recognizable content. This matters most for
14
+ weaker agents: the first build now succeeds, so they build on a working example
15
+ instead of assuming the server is broken and installing their own tools.
16
+
17
+ ### Fixed — project-directory build (the linchpin)
18
+ - **`build({output:'run'|'project', path})` now works on EVERY platform.** It was
19
+ 0/8 on GB/GBC/NES/Genesis and 0/5 on Atari 2600 via the natural path. The
20
+ dir-builder used to glob every file as a source; it now applies a per-platform
21
+ recipe that matches a hand-written build: routes the crt0 correctly (GB/GBC
22
+ `gb_crt0.s` via the cart-header path — no more `Multiple definition of gsinit`),
23
+ applies the linker preset (NES `chr-ram-runtime` — no more `Missing memory area
24
+ 'OAM'`), skips SDK intermediates (Genesis `sega.preprocessed.s`, the SNES SPC700
25
+ driver, any `*.upstream.*`), wires the GBA runtime (libtonc/libgba/maxmod by
26
+ what the source includes), routes `#include`d C/asm siblings as includes, and
27
+ bundles every incbin asset (`.xgc`/`.vgz`/…). `output:'run'` accepts `path` too
28
+ (build+load+run+screenshot a dir in one call). One shared code path so the two
29
+ build routes can't drift. Locked in by `scaffold-build-happypath.test.js`.
30
+ - **SNES multi-`.c` builds** (genre scaffolds ship `main.c` + `snes_sfx.c`) — the
31
+ stale single-`.c` guard in `buildSnesC` is removed (the link path already
32
+ handled multiple TUs).
33
+
34
+ ### Fixed — scaffolds rendered blank/wrong (now show content)
35
+ - **SMS/GG**: `vdp_init` R6 default 0xFB→0xFF — sprite tiles now read from $2000
36
+ where scaffolds upload them (were reading the empty $0000 bank → invisible). GG
37
+ also: 64-byte GG palette format (scaffolds shipped 32) + visible-window coords.
38
+ - **Genesis**: genre scaffolds now call `VDP_linkSprites` (the SAT link bytes were
39
+ 0 = end-of-list → only slot 0 drew); shmup enemies spread across the field.
40
+ - **SNES**: `sports`/`racing` `oamSet` now uses byte offsets (`slot<<2`); a real
41
+ 4bpp console font is embedded (the stub made all text black) + the BG-base /
42
+ text-offset / `consoleVblank` wiring fixed → c_hello/puzzle/platformer show text.
43
+ - **Lynx**: `platformer`/`puzzle`/`sports`/`racing` use the `while(tgi_busy()){}` +
44
+ full-screen `tgi_bar` clear (were black via bare `tgi_clear()`).
45
+ - **C64**: genre sprite data moved $0800→$2000 (it collided with the $0801 `.prg`
46
+ load → `sports` crashed to BASIC, sprites corrupt).
47
+ - **NES**: the bundled runtime no longer races the OAM-DMA — `oam_clear` resets the
48
+ index and `ppu_wait_nmi` hides unused slots after staging, so sprite-light
49
+ scaffolds no longer flicker to black; `default` backdrop no longer cycles to black.
50
+ - **PCE**: `pce_video.c` now programs the VDC display timing (NTSC 256×224) — fixes
51
+ the vertically-doubled picture.
52
+ - **GBA**: per-frame `tte_printf("%05d")` (broken in the bundled libtonc — garbles +
53
+ wedges the loop) replaced with a hand-built score string + `tte_write`.
54
+ - **Atari 2600**: `paddle` kernel rebuilt to a 2-line kernel (the per-line work
55
+ overflowed a 76-cycle scanline → ~250 lines, no vsync lock); now stable at 210.
56
+
57
+ ### Changed
58
+ - **Doc-drift swept**: every agent-facing `runSource`/`buildSource` → the current
59
+ `build({output:'run'|'rom'})`, dead tool names (`loadCategory`, `inspectSprites`,
60
+ …) → the consolidated forms, across the scaffold README emitter, `nextStep`,
61
+ examples, platform docs, and `.cfg` comments. Emitted scaffold-README `frames`
62
+ 60→240 (60 caught the boot logo on some platforms → false "blank").
63
+ - **Thin genres** given a BG world (Genesis sports court / racing road, NES racing
64
+ road, SNES sports/racing) so they read as the genre, not objects on black.
65
+ - **Missing-genre error** for msx/pce/atari2600 now names the working project
66
+ templates instead of a bare "default".
67
+ - The `uint8-loop-bound` preflight lint is scope-aware (no longer false-flags a
68
+ `uint16_t` loop counter that shares a name with a `uint8_t` in another function).
69
+
70
+ ## 0.14.0
71
+
72
+ **Two platform-specific top-level tools folded into their domain verbs, a
73
+ device-free cheat search, and skill-surface polish.** Pre-1.0 the surface keeps
74
+ consolidating with no deprecated aliases (call `catalog({op:'whatsNew'})` for the
75
+ live OLD→NEW map), so the tool count drops 34 → 32.
76
+
77
+ ### Changed (breaking — pre-1.0, no aliases)
78
+ - **`patchGbHeader` → `romPatch({op:'gbHeader'})`.** It's a ROM-file patch, same
79
+ family as romPatch's other ops — not a standalone Game-Boy tool. Same params
80
+ (path/outputPath/cgb/title/cartType/romSize/ramSize/destination), same output.
81
+ - **`dmaTrace` → `watch({on:'dma'})`.** The Genesis VDP-DMA trace is a log-all
82
+ trace like `watch`'s on:'mem'/'range'/'pc' — now a fourth `on`. Same
83
+ `precision:'exact'|'sampled'` + filters.
84
+ - **`cheats({op:'search'})` no longer needs `platform`.** Omit it and the search
85
+ sweeps EVERY indexed platform; each match reports its own `platform`. You don't
86
+ have to know the console to find a game's cheats. Pass `platform` only to scope
87
+ the search to one console.
88
+
89
+ ### Added
90
+ - **`GET /skills/romdev/SKILL.md`** is now the primary skill URL — it mirrors the
91
+ on-disk path agents save skills to (`~/.claude/skills/romdev/SKILL.md`) exactly.
92
+ `/romdev/SKILL.md` and the flat `/romdev-skill.md` are kept as aliases.
93
+ - **The `/livestream` observer header now links to `/documentation`** (the live
94
+ Swagger console), so a human watching can jump to the API docs.
95
+
96
+ ### Fixed (HTTP transport — failures are now unmissable)
97
+ - **A failed tool call returns a non-2xx, never a 200 with the error in the body.**
98
+ Tools that signal failure by RETURNING a failure-shaped result ({ok:false} /
99
+ {opened:false} / {applied:false} / a top-level `error`) — not just by throwing —
100
+ now map to HTTP 400 uniformly for EVERY tool. Before, e.g. `playtest` returning
101
+ `{opened:false}` came back 200, so an agent driving the REST/skill surface saw
102
+ "success," never read the body, and reported "window's up!" while no window
103
+ existed. (Valid "no"/state answers — `notSupported`, `matched:false`,
104
+ `loaded:false` — correctly stay 200.) A failed `playtest` window-open also now
105
+ logs to the server console so a human at the terminal sees it regardless.
106
+ - **`x-romdev-session` is now REQUIRED — a missing header returns 401**, instead
107
+ of the server auto-minting a throwaway one-shot session (which silently dropped
108
+ the loaded ROM and surfaced as "No ROM loaded" a couple calls later). The 401
109
+ message tells the agent to pick one stable id and send it every call.
110
+
111
+ ### Fixed (playtest — no more invisible windows)
112
+ - **`playtest({op:'open'})` now FAILS LOUDLY when there's no real display** instead
113
+ of reporting `opened:true` for a window nothing can see. If the server's SDL
114
+ comes up on the `offscreen`/`dummy` video driver (no desktop session — server
115
+ started over SSH, from a tty, before the desktop login, or as a headless agent
116
+ subprocess), the window would render and play audio but never appear on a
117
+ screen. We now detect the selected driver via SDL itself
118
+ (`sdl.info.drivers.video.current`) — ground truth, cross-platform (Linux/macOS/
119
+ Windows), and it correctly ALLOWS a virtual display like Xvfb (reports `x11`,
120
+ not `offscreen`). On the offscreen case the open throws (→ 400 / MCP error) with
121
+ the exact fix: run the server from inside your logged-in desktop session.
122
+ Headless tools (screenshot/runSource/inspect) are unaffected — offscreen stays
123
+ fine for everything except opening a window for a human.
124
+
125
+ ### Changed
126
+ - **The skill/HTTP session docs now coach a UNIQUE, task-descriptive
127
+ `x-romdev-session` id** (e.g. `nes-platformer-build`) — the id is the label a
128
+ human sees in `/livestream`, and it's how several agents share one server
129
+ without clobbering each other's emulator host.
130
+
7
131
  ## 0.13.0
8
132
 
9
133
  **Renamed `romdev-mcp` → `romdevtools` + a plain-HTTP tool surface and an Agent
@@ -34,7 +158,7 @@ duplication). Same Express app, same localhost trust, per-agent dynamic sessions
34
158
  via zod→JSON-Schema (the same conversion MCP `tools/list` uses).
35
159
  - **`GET /documentation`** — Swagger UI over the spec: a live "try it" console.
36
160
  - **`GET /tool/{name}/schema`** — that tool's JSON Schema (a validator on demand).
37
- - **`GET /romdev-skill.md`** — the Agent Skills open-standard SKILL.md
161
+ - **`GET /skills/romdev/SKILL.md`** — the Agent Skills open-standard SKILL.md
38
162
  (frontmatter + workflow guide + generated tool reference). ~100 tokens of
39
163
  name+description until invoked vs the always-on MCP tool defs — the on-demand
40
164
  context win. Works in Claude Code, opencode, OpenClaw, Hermes, etc. unchanged.
package/README.md CHANGED
@@ -9,7 +9,7 @@ npx romdevtools
9
9
  That's it — one command starts the local romdev **tool server** (no global install, no host compiler/emulator). Point any coding agent at it three ways:
10
10
 
11
11
  - **Plain HTTP** — `POST http://127.0.0.1:7331/tool/{name}`; browse/try every tool at `/documentation`.
12
- - **Agent Skill** — `GET /romdev-skill.md` (the [Agent Skills](https://agentskills.io) standard; ~100 tokens until invoked).
12
+ - **Agent Skill** — `GET /skills/romdev/SKILL.md` (the [Agent Skills](https://agentskills.io) standard; save it to your skills dir as `skills/romdev/SKILL.md`; ~100 tokens until invoked).
13
13
  - **MCP** — it's also a [Model Context Protocol](https://modelcontextprotocol.io/) server at `/mcp` for clients that want it.
14
14
 
15
15
  This package contains all the JavaScript — the tool surface, the WASM emulator host, the per-platform scaffolds, runtime/library source, and debug helpers — but **no emulator or compiler WASM itself.** Those ship in the `romdev-*` binary packages it depends on, loaded on demand the first time you build or run a given platform.
@@ -19,7 +19,7 @@ This package contains all the JavaScript — the tool surface, the WASM emulator
19
19
  ## What's in this package
20
20
 
21
21
  - **`bin`**
22
- - `romdevtools` → the tool server (`src/mcp/server.js`). Serves the HTTP tool routes, `/documentation`, `/romdev-skill.md`, and an MCP endpoint on `http://127.0.0.1:7331` by default (`PORT` / `HOST` to override). `romdev-mcp` is kept as an alias of the same command.
22
+ - `romdevtools` → the tool server (`src/mcp/server.js`). Serves the HTTP tool routes, `/documentation`, `/skills/romdev/SKILL.md`, and an MCP endpoint on `http://127.0.0.1:7331` by default (`PORT` / `HOST` to override). `romdev-mcp` is kept as an alias of the same command.
23
23
  - `romdevtools-cli` → a smoke/utility CLI, incl. `romdevtools-cli play <rom>` (SDL window, hot-plug controllers).
24
24
  - **`src/`** — the server, MCP tools, WASM host, core/toolchain resolvers, per-platform memory interpretation, and bundled library/runtime source (cc65 libs, PVSnesLib, SGDK, libtonc/libgba, hUGEDriver, …) that scaffolded projects link against.
25
25
  - **`examples/`** — per-platform starter projects and genre scaffolds.
@@ -58,12 +58,17 @@ running server:
58
58
  - **Plain HTTP:** `POST http://127.0.0.1:7331/tool/{name}` with the args as a JSON
59
59
  body; the response is JSON. Browse/try every tool at **`/documentation`**
60
60
  (Swagger UI, served locally — no CDN), or get the machine spec at
61
- **`/openapi.json`**. For stateful work (load step read) the first call
62
- returns an `x-romdev-session` header echo it on later calls to keep the same
63
- emulator session. romdev runs **locally** and tool path args (`path`,
64
- `outputPath`, …) are **local filesystem paths**, not uploads — pass absolute
65
- paths on the same machine.
66
- - **Agent Skill:** **`GET /romdev-skill.md`** is a portable [Agent
61
+ **`/openapi.json`**. **The agent picks its own session id** and sends it as the
62
+ `x-romdev-session` header on every call it's **required** (no header `401`;
63
+ the server won't silently run you in a throwaway session). Make it unique and
64
+ task-descriptive (e.g. `nes-platformer-build`), since it's also the label shown
65
+ in the `/livestream` observer. The emulator host is per-session, so the same id
66
+ keeps your ROM across calls, and several agents can share one server by each
67
+ using a different id. A call that fails returns a non-2xx (4xx) with the reason
68
+ in the body — never a 200 that hides an error. romdev runs **locally** and tool
69
+ path args (`path`, `outputPath`, …) are **local filesystem paths**, not uploads
70
+ — pass absolute paths on the same machine.
71
+ - **Agent Skill:** **`GET /skills/romdev/SKILL.md`** is a portable [Agent
67
72
  Skills](https://agentskills.io) `SKILL.md` (works in Claude Code, opencode,
68
73
  OpenClaw, Hermes, …). Drop it in your agent's skills dir; it costs ~100 tokens
69
74
  until invoked (vs always-on MCP tool defs), then teaches the workflows + the
@@ -3,7 +3,7 @@
3
3
  ; standard NTSC frame timing (3 vsync + 37 vblank + 192 visible + 30
4
4
  ; overscan). Read joystick to move the sprite.
5
5
  ;
6
- ; Build: buildSource({ platform: "atari2600", source: <this file> })
6
+ ; Build: build({ output: "rom", platform: "atari2600", source: <this file> })
7
7
 
8
8
  processor 6502
9
9
  org $F000
@@ -3,7 +3,7 @@
3
3
  ; standard NTSC frame timing (3 vsync + 37 vblank + 192 visible + 30
4
4
  ; overscan). Read joystick to move the sprite.
5
5
  ;
6
- ; Build: buildSource({ platform: "atari2600", source: <this file> })
6
+ ; Build: build({ output: "rom", platform: "atari2600", source: <this file> })
7
7
 
8
8
  processor 6502
9
9
  org $F000
@@ -111,9 +111,14 @@ MAIN:
111
111
  STA VSYNC
112
112
 
113
113
  ; ── VBLANK (37 lines) — game logic ────────────────────────────────
114
+ ; 34 here + the 3 STA WSYNC in the P0/P1 positioning block below = 37 VBLANK
115
+ ; lines total. (Bug fix: this loop used to be 37 AND the positioning added 3
116
+ ; more → 265 scanlines/frame → the TV/emulator can't lock vsync → rolling /
117
+ ; black picture. Exactly 262 lines = 3 VSYNC + 37 VBLANK + 192 visible + 30
118
+ ; overscan; the positioning WSYNCs MUST be counted against the 37.)
114
119
  LDA #2
115
120
  STA VBLANK
116
- LDX #37
121
+ LDX #34
117
122
  .vb:
118
123
  STA WSYNC
119
124
  DEX
@@ -212,93 +217,100 @@ MAIN:
212
217
  STA AUDV0
213
218
  .sfx_done:
214
219
 
215
- ; Position P0 horizontally at column 16 (race-the-beam delay)
216
- STA WSYNC
217
- LDA #1
220
+ ; ── Position P0 / P1 / HMOVE — exactly 3 WSYNC-bounded lines ───────
221
+ ; CRITICAL: every RESPx write AND the STA HMOVE must complete inside
222
+ ; the 76-cycle scanline that began with its STA WSYNC. A DEX/BNE delay
223
+ ; loop costs 5 cycles/iteration, so the loop count must be small enough
224
+ ; that RESPx still lands before the line ends. The old code used
225
+ ; LDX #38 (~189 cycles = 2.5 scanlines!) with no WSYNC before RESP1/
226
+ ; HMOVE, so it emitted ~2-3 UNCOUNTED scanlines past the 262 budget →
227
+ ; ~265 lines/frame → vsync never locks (rolling magenta band). HMOVE
228
+ ; was also issued mid-line; it must follow a fresh WSYNC.
229
+
230
+ ; Line 1 of 3: coarse-position P0 (left, ~column 16)
218
231
  STA WSYNC
219
- LDX #15
232
+ LDX #5
220
233
  .p0d:
221
234
  DEX
222
- BNE .p0d
235
+ BNE .p0d ; ~24 cycles in → P0 lands near the left edge
223
236
  STA RESP0
224
- ; Position P1 at column 144
237
+ ; Line 2 of 3: coarse-position P1 (right, ~column 132)
225
238
  STA WSYNC
226
- LDX #38
239
+ LDX #13
227
240
  .p1d:
228
241
  DEX
229
- BNE .p1d
242
+ BNE .p1d ; ~64 cycles in (< 76) → P1 lands near the right
230
243
  STA RESP1
244
+ ; Line 3 of 3: apply HMOVE on a FRESH line, right after WSYNC
245
+ STA WSYNC
231
246
  STA HMOVE
232
247
 
233
248
  LDA #0
234
249
  STA VBLANK
235
250
 
236
- ; ── Visible (192 lines) ───────────────────────────────────────────
251
+ ; ── Visible (192 lines) — TWO-LINE KERNEL ─────────────────────────
252
+ ; CRITICAL: a single scanline is only 76 CPU cycles. The full per-line
253
+ ; render here (playfield walls + P0 + P1 + ball, each a SEC/SBC/CMP +
254
+ ; conditional store) is ~88 cycles — it does NOT fit in one line. In a
255
+ ; 1-line kernel each WSYNC iteration then spills past the line boundary,
256
+ ; so 192 iterations stretch to ~232 emitted lines → ~250-line frame →
257
+ ; vsync never locks (rolling magenta band — THE bug).
258
+ ;
259
+ ; The fix is the standard 2600 "2-line kernel": each loop pass renders
260
+ ; TWO scanlines and splits the work across two WSYNCs, doubling the
261
+ ; budget to ~152 cycles. 96 passes × 2 lines = 192 visible lines.
262
+ ; Y counts 192→2 in steps of 2; paddles/ball move in 2px steps (fine
263
+ ; for Pong). The branchless "LDA #off / CMP / BCS skip / LDA #on" form
264
+ ; also drops the JMPs the old code paid every line.
237
265
  LDY #192
238
266
  .draw:
267
+ ; ---- first line of the pair: playfield walls + left paddle ----
239
268
  STA WSYNC
240
- TYA
241
- ; Top-bottom walls: lines 0..3 and 188..191 draw a full-width PF
242
- CMP #4
243
- BCS .nottop
244
- LDA #$FF
245
- STA PF0
246
- STA PF1
247
- STA PF2
248
- JMP .pf_done
249
- .nottop:
250
- CMP #188
251
- BCC .notbot
252
- LDA #$FF
253
- STA PF0
254
- STA PF1
255
- STA PF2
256
- JMP .pf_done
257
- .notbot:
269
+ ; Top + bottom walls: full-width PF on the outer rows (Y>=189 / Y<5)
258
270
  LDA #0
271
+ CPY #189
272
+ BCS .wall
273
+ CPY #5
274
+ BCS .nowall
275
+ .wall:
276
+ LDA #$FF
277
+ .nowall:
259
278
  STA PF0
260
279
  STA PF1
261
280
  STA PF2
262
- .pf_done:
263
281
  ; Left paddle: 8 lines starting at P0_Y
264
282
  TYA
265
283
  SEC
266
284
  SBC P0_Y
267
285
  CMP #8
268
- BCS .p0blank
269
- LDA #$FF
270
- STA GRP0
271
- JMP .p0done
272
- .p0blank:
273
286
  LDA #0
287
+ BCS .p0off
288
+ LDA #$FF
289
+ .p0off:
274
290
  STA GRP0
275
- .p0done:
291
+ ; ---- second line of the pair: right paddle + ball ----
292
+ STA WSYNC
276
293
  ; Right paddle: 8 lines starting at P1_Y
277
294
  TYA
278
295
  SEC
279
296
  SBC P1_Y
280
297
  CMP #8
281
- BCS .p1blank
282
- LDA #$FF
283
- STA GRP1
284
- JMP .p1done
285
- .p1blank:
286
298
  LDA #0
299
+ BCS .p1off
300
+ LDA #$FF
301
+ .p1off:
287
302
  STA GRP1
288
- .p1done:
289
303
  ; Ball: 2 lines starting at BALL_Y
290
304
  TYA
291
305
  SEC
292
306
  SBC BALL_Y
293
307
  CMP #2
294
- BCS .blblank
295
- LDA #2
296
- STA ENABL
297
- JMP .bldone
298
- .blblank:
299
308
  LDA #0
309
+ BCS .bloff
310
+ LDA #2
311
+ .bloff:
300
312
  STA ENABL
301
- .bldone:
313
+ DEY
302
314
  DEY
303
315
  BNE .draw
304
316
 
@@ -1,6 +1,6 @@
1
1
  // ── Hello, Atari 7800 — MARIA bring-up + single sprite ──────────────
2
2
  //
3
- // Build: buildSource({ platform: "atari7800", source: <this file>,
3
+ // Build: build({ output: "rom", platform: "atari7800", source: <this file>,
4
4
  // language: "c" })
5
5
  //
6
6
  // Sets up a minimal display list, paints the background blue and renders
@@ -34,7 +34,7 @@
34
34
  * the 4KB of internal RAM.
35
35
  *
36
36
  * ── Build ───────────────────────────────────────────────────────
37
- * buildSource({ platform: "atari7800", sources: { "main.c": ... } })
37
+ * build({ output: "rom", platform: "atari7800", sources: { "main.c": ... } })
38
38
  */
39
39
  #include <stdint.h>
40
40
  #include "atari7800_sfx.h"
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * The note tables live in atari7800_music.c — they ARE the song.
12
12
  *
13
- * Build: buildSource({ platform: "atari7800", template: "music_demo" })
13
+ * Build: build({ output: "rom", platform: "atari7800", template: "music_demo" })
14
14
  */
15
15
  #include <stdint.h>
16
16
  #include "atari7800_music.h"
@@ -4,7 +4,7 @@
4
4
  // directly: writes screen codes into screen RAM, sets per-cell colors
5
5
  // in color RAM, and cycles the border color in a main loop.
6
6
  //
7
- // Build: buildSource({ platform: "c64", source: <this file>, language: "c" })
7
+ // Build: build({ output: "rom", platform: "c64", source: <this file>, language: "c" })
8
8
 
9
9
  #include <stdint.h>
10
10
 
@@ -80,7 +80,7 @@ static void wait_vblank(void) {
80
80
 
81
81
  static void copy_sprite(uint8_t slot, const uint8_t *data) {
82
82
  uint8_t i;
83
- volatile uint8_t *dst = (volatile uint8_t*)(0x0800 + slot * 64);
83
+ volatile uint8_t *dst = (volatile uint8_t*)(0x2000 + slot * 64); /* $2000, not $0800 (collides w/ $0801 .prg) */
84
84
  for (i = 0; i < 64; i++) dst[i] = data[i];
85
85
  }
86
86
 
@@ -131,7 +131,7 @@ void main(void) {
131
131
 
132
132
  POKE(VIC_SPR_ENA, 0);
133
133
  copy_sprite(0, player_sprite);
134
- SPRITE_POINTERS[0] = 0x20;
134
+ SPRITE_POINTERS[0] = 0x80; /* $2000/64 */
135
135
  POKE(VIC_SPR_COL(0), 0x07); /* yellow player */
136
136
  POKE(VIC_BORDER, 0x06); /* dark blue */
137
137
  POKE(VIC_BG0, 0x06);
@@ -121,7 +121,7 @@ static void lock_piece(void) {
121
121
  grid[r][c] = 0;
122
122
  grid[r][c+1] = 0;
123
123
  grid[r][c+2] = 0;
124
- if (score < 65500) score += 30;
124
+ if (score < 65500u) score += 30;
125
125
  sfx_tone(0, 0x80, 0x10, 12);
126
126
  }
127
127
  }
@@ -49,7 +49,7 @@ static void wait_vblank(void) {
49
49
 
50
50
  static void copy_sprite(uint8_t slot, const uint8_t *data) {
51
51
  uint8_t i;
52
- volatile uint8_t *dst = (volatile uint8_t*)(0x0800 + slot * 64);
52
+ volatile uint8_t *dst = (volatile uint8_t*)(0x2000 + slot * 64); /* $2000, not $0800 (collides w/ $0801 .prg) */
53
53
  for (i = 0; i < 64; i++) dst[i] = data[i];
54
54
  }
55
55
 
@@ -75,8 +75,8 @@ void main(void) {
75
75
  uint8_t i, pad;
76
76
  POKE(VIC_SPR_ENA, 0);
77
77
  copy_sprite(0, car_sprite);
78
- SPRITE_POINTERS[0] = 0x20;
79
- for (i = 0; i < MAX_OBS; i++) SPRITE_POINTERS[1 + i] = 0x20;
78
+ SPRITE_POINTERS[0] = 0x80; /* $2000/64 */
79
+ for (i = 0; i < MAX_OBS; i++) SPRITE_POINTERS[1 + i] = 0x80;
80
80
  POKE(VIC_SPR_COL(0), 0x07); /* yellow player */
81
81
  for (i = 0; i < MAX_OBS; i++) POKE(VIC_SPR_COL(1 + i), 0x02); /* red obstacles */
82
82
  POKE(VIC_BORDER, 0x00);
@@ -20,7 +20,8 @@
20
20
  #define PEEK(addr) (*(volatile uint8_t*)(addr))
21
21
 
22
22
  #define SPRITE_POINTERS ((volatile uint8_t*)0x07F8)
23
- #define SPRITE_DATA_BASE 0x0800 /* sprite N data at $0800 + N*64 */
23
+ #define SPRITE_DATA_BASE 0x2000 /* sprite N data at $2000 + N*64 — NOT $0800,
24
+ * which collides with the $0801 .prg load (C64-1) */
24
25
 
25
26
  #define JOY_UP 0x01
26
27
  #define JOY_DOWN 0x02
@@ -121,9 +122,9 @@ void main(void) {
121
122
 
122
123
  /* Sprite pointers: slot N points at /64 index into the VIC bank.
123
124
  * Default bank = $0000-$3FFF. $0800 / 64 = 32 = $20. */
124
- SPRITE_POINTERS[SLOT_PLAYER] = 0x20;
125
- for (i = 0; i < MAX_BULLETS; i++) SPRITE_POINTERS[SLOT_BULLET0 + i] = 0x21;
126
- for (i = 0; i < MAX_ENEMIES; i++) SPRITE_POINTERS[SLOT_ENEMY0 + i] = 0x22;
125
+ SPRITE_POINTERS[SLOT_PLAYER] = 0x80; /* $2000/64 */
126
+ for (i = 0; i < MAX_BULLETS; i++) SPRITE_POINTERS[SLOT_BULLET0 + i] = 0x81;
127
+ for (i = 0; i < MAX_ENEMIES; i++) SPRITE_POINTERS[SLOT_ENEMY0 + i] = 0x82;
127
128
 
128
129
  POKE(VIC_SPR_COL(SLOT_PLAYER), 0x07); /* yellow */
129
130
  for (i = 0; i < MAX_BULLETS; i++) POKE(VIC_SPR_COL(SLOT_BULLET0 + i), 0x01); /* white */
@@ -175,7 +176,7 @@ void main(void) {
175
176
  if (aabb(&bullets[i], &enemies[j])) {
176
177
  bullets[i].alive = 0;
177
178
  enemies[j].alive = 0;
178
- if (score < 65500) score += 10;
179
+ if (score < 65500u) score += 10;
179
180
  sfx_noise(8);
180
181
  break;
181
182
  }
@@ -41,7 +41,7 @@ static void wait_vblank(void) {
41
41
 
42
42
  static void copy_sprite(uint8_t slot, const uint8_t *data) {
43
43
  uint8_t i;
44
- volatile uint8_t *dst = (volatile uint8_t*)(0x0800 + slot * 64);
44
+ volatile uint8_t *dst = (volatile uint8_t*)(0x2000 + slot * 64); /* $2000, not $0800 (collides w/ $0801 .prg) */
45
45
  for (i = 0; i < 64; i++) dst[i] = data[i];
46
46
  }
47
47
 
@@ -56,9 +56,9 @@ void main(void) {
56
56
  copy_sprite(0, paddle_sprite);
57
57
  copy_sprite(1, paddle_sprite);
58
58
  copy_sprite(2, ball_sprite);
59
- SPRITE_POINTERS[0] = 0x20;
60
- SPRITE_POINTERS[1] = 0x21;
61
- SPRITE_POINTERS[2] = 0x22;
59
+ SPRITE_POINTERS[0] = 0x80; /* $2000/64 */
60
+ SPRITE_POINTERS[1] = 0x81;
61
+ SPRITE_POINTERS[2] = 0x82;
62
62
  POKE(VIC_SPR_COL(0), 0x01); /* white */
63
63
  POKE(VIC_SPR_COL(1), 0x01);
64
64
  POKE(VIC_SPR_COL(2), 0x07); /* yellow ball */
@@ -4,7 +4,7 @@
4
4
  ; it to the BG map, enables the LCD, then loops reading joypad and
5
5
  ; scrolling the BG. Build with:
6
6
  ;
7
- ; buildSource({platform:"gb", source: <this>})
7
+ ; build({ output: "rom", platform:"gb", source: <this>})
8
8
  ;
9
9
  ; rgbfix gets applied automatically by buildForPlatform so the header
10
10
  ; checksum is valid.