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.
- package/AGENTS.md +21 -14
- package/CHANGELOG.md +125 -1
- package/README.md +13 -8
- package/examples/atari2600/main.asm +1 -1
- package/examples/atari2600/templates/default.asm +1 -1
- package/examples/atari2600/templates/paddle.asm +59 -47
- package/examples/atari7800/main.c +1 -1
- package/examples/atari7800/templates/default.c +1 -1
- package/examples/atari7800/templates/music_demo.c +1 -1
- package/examples/c64/main.c +1 -1
- package/examples/c64/templates/platformer.c +2 -2
- package/examples/c64/templates/puzzle.c +1 -1
- package/examples/c64/templates/racing.c +3 -3
- package/examples/c64/templates/shmup.c +6 -5
- package/examples/c64/templates/sports.c +4 -4
- package/examples/gb/main.asm +1 -1
- package/examples/gb/main.c +1 -1
- package/examples/gb/templates/puzzle.c +1 -1
- package/examples/gb/templates/racing.c +1 -1
- package/examples/gb/templates/shmup.c +1 -1
- package/examples/gba/templates/gba_hello.c +1 -1
- package/examples/gba/templates/maxmod_demo.c +1 -1
- package/examples/gba/templates/puzzle.c +17 -3
- package/examples/gba/templates/racing.c +16 -2
- package/examples/gba/templates/shmup.c +23 -4
- package/examples/gba/templates/tonc_hello.c +6 -4
- package/examples/gbc/main.asm +1 -1
- package/examples/gbc/templates/puzzle.c +1 -1
- package/examples/gbc/templates/racing.c +1 -1
- package/examples/gbc/templates/shmup.c +1 -1
- package/examples/genesis/main.s +1 -1
- package/examples/genesis/templates/puzzle.c +1 -1
- package/examples/genesis/templates/racing.c +45 -1
- package/examples/genesis/templates/shmup.c +12 -3
- package/examples/genesis/templates/shmup_2p.c +2 -2
- package/examples/genesis/templates/sports.c +39 -0
- package/examples/gg/templates/hello_sprite.c +38 -23
- package/examples/gg/templates/music_demo.c +11 -8
- package/examples/gg/templates/platformer.c +37 -15
- package/examples/gg/templates/racing.c +25 -12
- package/examples/gg/templates/shmup.c +12 -6
- package/examples/gg/templates/sports.c +30 -16
- package/examples/gg/templates/tile_engine.c +24 -10
- package/examples/lynx/templates/platformer.c +7 -1
- package/examples/lynx/templates/puzzle.c +8 -2
- package/examples/lynx/templates/racing.c +7 -1
- package/examples/lynx/templates/sports.c +7 -1
- package/examples/nes/main.c +2 -2
- package/examples/nes/space-shooter/nes_runtime.h +1 -1
- package/examples/nes/templates/default.c +4 -1
- package/examples/nes/templates/racing.c +50 -1
- package/examples/pce/main.c +1 -1
- package/examples/sms/templates/hello_sprite.c +1 -1
- package/examples/sms/templates/music_demo.c +1 -1
- package/examples/sms/templates/puzzle.c +1 -1
- package/examples/sms/templates/racing.c +1 -1
- package/examples/sms/templates/shmup.c +1 -1
- package/examples/sms/templates/shmup_2p.c +2 -2
- package/examples/snes/main.asm +1 -1
- package/examples/snes/templates/c-hello-data.asm +309 -14
- package/examples/snes/templates/c-hello.c +13 -2
- package/examples/snes/templates/default.c +1 -1
- package/examples/snes/templates/hello_sprite-data.asm +300 -2
- package/examples/snes/templates/hello_sprite.c +10 -1
- package/examples/snes/templates/music_demo-data.asm +300 -2
- package/examples/snes/templates/music_demo.c +10 -1
- package/examples/snes/templates/platformer-data.asm +300 -2
- package/examples/snes/templates/platformer.c +10 -1
- package/examples/snes/templates/puzzle-data.asm +300 -2
- package/examples/snes/templates/puzzle.c +11 -1
- package/examples/snes/templates/racing-data.asm +300 -2
- package/examples/snes/templates/racing.c +40 -4
- package/examples/snes/templates/shmup-data.asm +299 -6
- package/examples/snes/templates/shmup.c +11 -7
- package/examples/snes/templates/sports-data.asm +300 -2
- package/examples/snes/templates/sports.c +40 -5
- package/package.json +1 -1
- package/src/cheats/lookup.js +39 -18
- package/src/http/routes.js +58 -33
- package/src/http/skill-doc.js +10 -9
- package/src/http/swagger.js +1 -1
- package/src/http/tool-registry.js +72 -5
- package/src/mcp/server.js +6 -5
- package/src/mcp/state.js +8 -6
- package/src/mcp/tool-manifest.js +7 -7
- package/src/mcp/tools/cheats.js +4 -3
- package/src/mcp/tools/index.js +18 -2
- package/src/mcp/tools/playtest.js +48 -35
- package/src/mcp/tools/project.js +39 -73
- package/src/mcp/tools/rom-id.js +49 -4
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +183 -19
- package/src/mcp/tools/trace-vram-source.js +3 -3
- package/src/mcp/tools/watch-memory.js +27 -46
- package/src/observer/livestream.html +41 -5
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +5 -5
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +1 -1
- package/src/platforms/gb/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gb/lib/c/README.md +2 -2
- package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/platforms/gbc/MENTAL_MODEL.md +3 -3
- package/src/platforms/gbc/TROUBLESHOOTING.md +5 -5
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +2 -2
- package/src/platforms/gbc/lib/c/README.md +2 -2
- package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/platforms/gg/MENTAL_MODEL.md +14 -13
- package/src/platforms/gg/lib/c/vdp_init.c +10 -8
- package/src/platforms/msx/MENTAL_MODEL.md +1 -1
- package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
- package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
- package/src/platforms/pce/MENTAL_MODEL.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +1 -0
- package/src/platforms/pce/lib/c/pce_video.c +26 -0
- package/src/platforms/sms/MENTAL_MODEL.md +12 -12
- package/src/platforms/sms/lib/c/vdp_init.c +10 -8
- package/src/platforms/sms/lib/vdp_init.s +1 -1
- package/src/playtest/playtest.js +25 -0
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
- package/src/toolchains/genesis-c/README.md +1 -1
- package/src/toolchains/sdcc/preflight-lint.js +47 -7
- 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 (`
|
|
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), **`
|
|
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,
|
|
246
|
-
|
|
247
|
-
(
|
|
248
|
-
|
|
249
|
-
(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
> **`
|
|
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.**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`**.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
|
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:
|
|
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 #
|
|
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
|
|
216
|
-
STA
|
|
217
|
-
|
|
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 #
|
|
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
|
-
;
|
|
237
|
+
; Line 2 of 3: coarse-position P1 (right, ~column 132)
|
|
225
238
|
STA WSYNC
|
|
226
|
-
LDX #
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
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:
|
|
13
|
+
* Build: build({ output: "rom", platform: "atari7800", template: "music_demo" })
|
|
14
14
|
*/
|
|
15
15
|
#include <stdint.h>
|
|
16
16
|
#include "atari7800_music.h"
|
package/examples/c64/main.c
CHANGED
|
@@ -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:
|
|
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*)(
|
|
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] =
|
|
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);
|
|
@@ -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*)(
|
|
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] =
|
|
79
|
-
for (i = 0; i < MAX_OBS; i++) SPRITE_POINTERS[1 + i] =
|
|
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
|
|
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] =
|
|
125
|
-
for (i = 0; i < MAX_BULLETS; i++) SPRITE_POINTERS[SLOT_BULLET0 + i] =
|
|
126
|
-
for (i = 0; i < MAX_ENEMIES; i++) SPRITE_POINTERS[SLOT_ENEMY0 + i] =
|
|
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 <
|
|
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*)(
|
|
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] =
|
|
60
|
-
SPRITE_POINTERS[1] =
|
|
61
|
-
SPRITE_POINTERS[2] =
|
|
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 */
|
package/examples/gb/main.asm
CHANGED
|
@@ -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
|
-
;
|
|
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.
|