romdevtools 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +169 -494
- package/CHANGELOG.md +103 -0
- package/examples/genesis/templates/platformer.c +5 -1
- package/examples/genesis/templates/two_plane_parallax.c +166 -0
- package/package.json +2 -2
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +225 -2
- package/src/host/framebuffer.js +37 -0
- package/src/http/skill-doc.js +1 -1
- package/src/mcp/tools/audio.js +2 -2
- package/src/mcp/tools/frame.js +13 -34
- package/src/mcp/tools/index.js +2 -2
- package/src/mcp/tools/input-layout.js +10 -0
- package/src/mcp/tools/input.js +26 -2
- package/src/mcp/tools/metasprite-tools.js +1 -1
- package/src/mcp/tools/platform-tools.js +18 -11
- package/src/mcp/tools/playtest.js +17 -2
- package/src/mcp/tools/project.js +9 -1
- package/src/mcp/tools/rendering-context.js +1 -1
- package/src/mcp/tools/symbols.js +130 -39
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +3 -2
- package/src/mcp/tools/watch-memory.js +58 -6
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +155 -6
- package/src/platforms/atari2600/MENTAL_MODEL.md +37 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +36 -0
- package/src/platforms/c64/MENTAL_MODEL.md +83 -6
- package/src/platforms/gb/MENTAL_MODEL.md +74 -0
- package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +91 -0
- package/src/platforms/gba/MENTAL_MODEL.md +57 -3
- package/src/platforms/gba/lib/arm-archives/libc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libgcc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libnosys.a +0 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +34 -0
- package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +91 -0
- package/src/platforms/genesis/MENTAL_MODEL.md +180 -0
- package/src/platforms/genesis/TROUBLESHOOTING.md +32 -0
- package/src/platforms/genesis/lib/c/libc.a +0 -0
- package/src/platforms/genesis/lib/c/libgcc.a +0 -0
- package/src/platforms/genesis/lib/c/libm.a +0 -0
- package/src/platforms/gg/MENTAL_MODEL.md +24 -0
- package/src/platforms/gg/lib/c/gg_crt0.s +30 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +33 -7
- package/src/platforms/msx/MENTAL_MODEL.md +27 -0
- package/src/platforms/nes/MENTAL_MODEL.md +35 -0
- package/src/platforms/sms/MENTAL_MODEL.md +51 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +40 -0
- package/src/platforms/snes/MENTAL_MODEL.md +21 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +43 -0
- package/src/playtest/playtest.js +48 -0
- package/src/toolchains/sdcc/preflight-lint.js +164 -8
- package/examples/msx/catch_game/_verify.mjs +0 -93
- package/examples/pce/catch_game/_verify.mjs +0 -75
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# romdev — Agent guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This is romdev's GENERIC orientation — read it once. The platform-specific detail (memory maps, footguns, debug tooling) lives in each platform's docs, which you fetch on demand with `platform({op:'doc'})` as you work; this doc tells you when.
|
|
4
4
|
|
|
5
5
|
## What this server does
|
|
6
6
|
|
|
@@ -18,6 +18,16 @@ Internalize this above all else: **you never need — and must never install —
|
|
|
18
18
|
|
|
19
19
|
This rule is about **compilers and emulators only** — NOT about content tools. ImageMagick, GIMP, Aseprite/LibreSprite, Audacity, Tiled, a tracker (FamiStudio/Deflemask), Python for a quick art script — all fine to use, and fine for the user to install. They produce **raw source art/audio** (a PNG, a sprite sheet, a `.wav`, a `.tmx`); romdev then **imports and packs** that into platform-native data. Use them freely when they help; just don't reach for a *compiler or emulator*.
|
|
20
20
|
|
|
21
|
+
## The second rule: READ YOUR TARGET PLATFORM'S DOCS BEFORE YOU WRITE CODE FOR IT
|
|
22
|
+
|
|
23
|
+
This doc is deliberately GENERIC — it can't hold 14 platforms' worth of detail without bloating every session. The knowledge that actually saves you — the memory map, the input/control quirks, the render-enable order, the codegen traps, the SDK's footguns — lives in each platform's docs, read on demand:
|
|
24
|
+
|
|
25
|
+
- **`platform({op:'doc', platform, name:'mental_model'})`** — read this for EVERY system you're about to build or RE on, BEFORE you write code. It's a couple hundred tokens and most "why won't this work" dead-ends are a documented footgun you'd have seen there (a C64 game that needs a keyboard key to start; an SDCC WRAM-layout trap; a platform's render-enable order; gambatte exposing `gb_vram` not `video_ram`).
|
|
26
|
+
- **`platform({op:'doc', platform, name:'troubleshooting'})`** — the symptom→fix list; read it the moment something's broken.
|
|
27
|
+
- **`platform({op:'doc', platform:'romhacking', name:'playbook'})`** — read FIRST if you're doing a romhack/RE (the cross-platform decision tree).
|
|
28
|
+
|
|
29
|
+
Skipping this is the #1 avoidable time-sink. If you find yourself flailing on platform behavior and you haven't read that platform's `mental_model`, stop and read it — the answer is almost always there.
|
|
30
|
+
|
|
21
31
|
### romdev also packs assets in-server — reach for these first
|
|
22
32
|
|
|
23
33
|
Asset conversion is bundled too, so you often don't need the host tools at all. First-class tools: `encodeArt({stage:'tiles'})`, `encodeArt({stage:'tilemap'})`, `encodeArt({stage:'quantize'})`, `palette({source:'platformMaster'})`, `palette({source:'lospec'})`, `encodeArt({stage:'validate'})`, the loaders `importArt({from:'texturepacker'})` / `importArt({from:'aseprite'})` / `importArt({from:'gif'})` / `importArt({from:'tiled'})`, and helpers like `sprites({op:'capture'})` / `importArt({from:'rom'})`. The canonical quantize→tile→pack path lives here. Typical flow: paint pixels in a host editor (or generate a PNG), then `encodeArt({stage:'quantize'})` → `encodeArt({stage:'tiles'})` to get platform-native tiles. (You can do the whole thing in-server too when the art is procedural.)
|
|
@@ -152,11 +162,13 @@ worry about ground truth:
|
|
|
152
162
|
without round-tripping bytes through your context — preferred
|
|
153
163
|
when you're scaffolding into a project dir.
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
Reminder (it's the second rule up top): **read your platform's
|
|
166
|
+
`platform({op:'doc', platform, name:'mental_model'})` BEFORE you write code for
|
|
167
|
+
it** — that's where the footguns that would otherwise burn your session live.
|
|
168
|
+
|
|
169
|
+
For most workflows, path A is all you need. **When a tool call FAILS, read the
|
|
170
|
+
error message and `issues[]` first — see "When a call fails" below; the error
|
|
171
|
+
usually names the fix.** File a feedback round if the bundled examples are wrong.
|
|
160
172
|
|
|
161
173
|
### Path B — Debug when the bundled code disagrees with behavior
|
|
162
174
|
|
|
@@ -317,18 +329,19 @@ Different platforms have different levels of MCP-exposed debugging — different
|
|
|
317
329
|
> `as`/`ld`/`objcopy`. The per-platform notes below cover the platform-SPECIFIC
|
|
318
330
|
> inspectors + chips (PC Engine + MSX: generic shapes only so far).
|
|
319
331
|
|
|
320
|
-
-
|
|
321
|
-
- **NES**
|
|
322
|
-
- **
|
|
323
|
-
- **
|
|
324
|
-
- **
|
|
325
|
-
|
|
326
|
-
- **
|
|
327
|
-
- **Atari
|
|
328
|
-
- **
|
|
329
|
-
- **
|
|
330
|
-
- **Atari Lynx**
|
|
331
|
-
- **MSX
|
|
332
|
+
The deep per-platform inspectors + the exact memory-region names, core quirks, and any platform-specific traps live in **each platform's `MENTAL_MODEL.md`** (read via `platform({op:'doc', platform, name:'mental_model'})`) — read it for the system you're on. Symptom → doc:
|
|
333
|
+
- **NES** — blank/black screen, wrong sprites/colors, or need live PPU regs / CIRAM-attribute / MMC1-banked CHR state.
|
|
334
|
+
- **SNES** — garbage/flashing sprites, or live OAM/CGRAM/SPC700/S-DSP state (PPU regs read via the FillRAM shadow — no core patch needed).
|
|
335
|
+
- **Genesis** — missing/wrong sprites, palette/scroll, or live SAT/CRAM/VSRAM/VDP/Z80 state (mind the gpgx VRAM byte-swap).
|
|
336
|
+
- **GB / GBC** — wrong sprites/palette/tiles/BG or live SM83/APU/LCDC state; gambatte exposes `gb_vram` (NOT `video_ram`) + `gb_oam`/`gb_io`/`gb_hram`. (GB MENTAL_MODEL also holds the SDCC toolchain notes; GBC adds the CGB palette deltas.)
|
|
337
|
+
- **SMS / GG** — sprite/tile/palette/BG issues or live Z80 + VRAM/CRAM/VDP regions (SMS holds the shared gpgx detail; GG = 12-bit-vs-6-bit palette + `gg_vram`/`gg_cram` deltas).
|
|
338
|
+
- **GBA** — sprite/palette/BG wrong, ARM7 `execPc` (pipeline-adjusted PC), the `gba_*` regions, or ARM-vs-Thumb objdump (Thumb decodes as `.byte`).
|
|
339
|
+
- **Atari 2600** — blank screen / missing sprite / TIA-or-palette state, or `audioDebug` "not supported" (no OAM, no standard sound chip).
|
|
340
|
+
- **Atari 7800** — display-list garbage, MARIA palette/DPP, `sprites({op:'inspect'})` returns no OAM, or no `audioDebug`.
|
|
341
|
+
- **C64** — VIC/sprites/SID/banking misbehaving: live palette/sprites/cpu/renderState inspectors, `c64_*` regions, `.prg` disasm, disk load/save+export (and its keyboard/joyport input — see "Driving input over MCP").
|
|
342
|
+
- **Atari Lynx** — `sprites({op:'inspect'})` returns an SCB list head (no fixed OAM), or you need the Mikey palette/audio, 65C02 regs, or the `lynx_hw_regs` $FC00-$FDFF window.
|
|
343
|
+
- **MSX** — VDP/PSG inspection or AY8910 `audioDebug`. (ColecoVision is bring-up-only: standard `system_ram`/`save_ram`/`video_ram`, no custom inspectors — extend by patching its core per the snes9x/gpgx pattern.)
|
|
344
|
+
- **PC Engine** — generic shapes + the core's native regions only so far (no custom-inspector treatment yet).
|
|
332
345
|
|
|
333
346
|
Starter snippets per platform live under `src/platforms/<platform>/lib/`. Discover via `scaffold({op:'snippets', platform})` (default `mode:'list'`), fetch one via `scaffold({op:'snippets', platform, mode:'get', name})`. SNES + NES + Genesis + SMS + Game Boy + Atari 2600 + Atari 7800 have substantial snippet libraries; others are minimal.
|
|
334
347
|
|
|
@@ -396,9 +409,39 @@ When `build({output:'run'})` is too coarse, the long-form workflow:
|
|
|
396
409
|
2. `loadMediaBytes({ platform, base64 })` → load without disk I/O
|
|
397
410
|
3. `frame({op:'step', frames: N})` or `runUntil({ condition })` → advance time
|
|
398
411
|
4. `frame({op:'screenshot'})` for vibes, `tiles({op:'pixels'})`/`tiles({op:'fingerprints'})` for byte-precise work, `memory({op:'read'})` for game state
|
|
399
|
-
5. `input({op:'set'})` / `input({op:'press'})` / `input({op:'sequence'})` to drive the game
|
|
412
|
+
5. `input({op:'set'})` / `input({op:'press'})` / `input({op:'sequence'})` to drive the game — some platforms have extra control/input modes worth reading in their `MENTAL_MODEL.md` (e.g. C64 needs keyboard keys like F1/RUN-STOP to start many games; `input({op:'pressKey'/'typeText'/'joyport'})`)
|
|
400
413
|
6. `state({op:'save'}, "checkpoint")` / `state({op:'load'}, "checkpoint")` for try/undo
|
|
401
414
|
|
|
415
|
+
## Diagnosing behavior over time (game-feel, not just "is it alive")
|
|
416
|
+
|
|
417
|
+
`frame({op:'verify'})` answers "is it rendering / alive". When the screen looks
|
|
418
|
+
plausible but the game is WRONG — choppy movement, a value that's off, a piece
|
|
419
|
+
that locks mid-air — STOP eyeballing screenshots and trace the state. These tools
|
|
420
|
+
already exist; reach for them by symptom:
|
|
421
|
+
|
|
422
|
+
- **"Movement/scrolling feels choppy / camera desyncs / scroll jumps."**
|
|
423
|
+
`recordSession({frames:180, holdInputs:[{right:true}], includeScreenshots:false,
|
|
424
|
+
memorySamples:[{region, offset, length, label}]})` — holds input over N frames and
|
|
425
|
+
returns an analyzable timeline. Sample the player's screen-X + the scroll
|
|
426
|
+
registers (Genesis: `genesis_vsram` + the HSCROLL table in `video_ram`; per
|
|
427
|
+
platform varies) and look for camera scroll changing while the sprite barely
|
|
428
|
+
moves, or non-monotone deltas. `watch({on:'mem', format:'series', ranges:[...]})`
|
|
429
|
+
gives the same idea as a compact value-vs-frame CURVE per byte. (Genesis: see
|
|
430
|
+
its MENTAL_MODEL "Why does horizontal movement feel choppy?" — the usual cause
|
|
431
|
+
is rewriting tilemaps in the frame loop; `watch({on:'dma', perFrame:true})`
|
|
432
|
+
shows the per-frame DMA bytes that spike when you do.)
|
|
433
|
+
- **"A computed value is wrong but the build is clean."** Don't re-read your C.
|
|
434
|
+
Resolve the variable's address and read it: `build({output:'romWithDebug',
|
|
435
|
+
resolveSymbols:["grid","score"]})` (or `symbols({op:'resolve', mapPath|dbgPath,
|
|
436
|
+
name})`) → `memory({op:'read', region, offset})`. Cheap, zero image tokens, and
|
|
437
|
+
it tells you whether the bug is your logic or your data. (sm83/z80: a "wrong
|
|
438
|
+
value" is far more often a WRAM layout collision than a miscompile — see the
|
|
439
|
+
GB/GBC SDCC_GOTCHAS "codegen traps in plain game logic".)
|
|
440
|
+
- **"I can't tell what's on the background."** `background({view:'map'})` decodes
|
|
441
|
+
the BG tilemap (grid of tile indices, or a rendered PNG) — don't hand-compute
|
|
442
|
+
nametable offsets. Small handheld too tiny to read inline? `frame({op:'screenshot',
|
|
443
|
+
scale:4})` up-scales (nearest-neighbor).
|
|
444
|
+
|
|
402
445
|
## When a call fails: READ THE ERROR FIRST
|
|
403
446
|
|
|
404
447
|
romdev errors are written FOR you — they name what went wrong AND how to recover. Read the message (and `issues[]`) before guessing, screenshotting, or retrying blindly. Two shapes:
|
|
@@ -410,463 +453,79 @@ romdev errors are written FOR you — they name what went wrong AND how to recov
|
|
|
410
453
|
|
|
411
454
|
## ROM hacking workflow
|
|
412
455
|
|
|
413
|
-
The full
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
`
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
```
|
|
458
|
-
Supported on **all 14 tier-1 systems** — NES, GB/GBC, Genesis, SMS/GG, SNES,
|
|
459
|
-
Atari 2600/7800, C64, Lynx (65C02), PC Engine (HuC6280), MSX (Z80), and GBA
|
|
460
|
-
(ARM7) — every bundled CPU family. On a banked mapper a `$8000-$BFFF` pc may be
|
|
461
|
-
in a switchable bank; `breakpoint({on:'write'})` reports the `bank` (NES/GB/SMS-GG) so you can
|
|
462
|
-
pass it to `disasm({target:'rom'})`.
|
|
463
|
-
- **`watch({on:'mem'})` / `breakpoint({on:'write',precision:'sampled'})` — cross-platform, frame-sampled.** Step until
|
|
464
|
-
the byte changes; the returned `pc` is a frame-boundary sample (a lead, not a
|
|
465
|
-
guarantee under interrupts — cross-check the value trace). Use on non-NES, or
|
|
466
|
-
for the value timeline.
|
|
467
|
-
- **`memory({op:'snapshot'})` + `memory({op:'diff'})` — "which bytes did THIS event touch?"** When
|
|
468
|
-
you don't yet know the address: `memory({op:'snapshot'})` before the event, trigger it
|
|
469
|
-
(`input({op:'press'})`/`frame({op:'step'})`), then `memory({op:'diff'})` — you get just the changed offsets
|
|
470
|
-
with before/after, no eyeballing two RAM dumps. The fast way to find an area-id
|
|
471
|
-
/ phase / flag byte a transition writes. (`state({op:'diff'})` is the coarse
|
|
472
|
-
whole-machine "did anything change?" version.)
|
|
473
|
-
|
|
474
|
-
```js
|
|
475
|
-
breakpoint({ on:'write', precision:'sampled', region:"system_ram", offset:0x03B6, maxFrames:300,
|
|
476
|
-
pressDuring:[{ frame:30, button:"A" }] })
|
|
477
|
-
→ { pc: "$E3AF" (frame-sampled), changes:[{ before:31, after:32 }] }
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**Execution breakpoints (all 14 platforms) — read the register at the instruction.**
|
|
481
|
-
When the answer isn't a flat table but a value computed in a register, stop the
|
|
482
|
-
CPU *at the instruction* and read it:
|
|
483
|
-
- **`breakpoint({on:'pc', address, maxFrames, pressDuring})`** — runs until the CPU PC
|
|
484
|
-
reaches `address`, then FREEZES the CPU exactly there. Then `cpu({op:'read'})` reads
|
|
485
|
-
the full register file at that precise moment. The canonical RE move: break at a
|
|
486
|
-
decoder's `move.b (a0),d0`, read `A0` → the source pointer, `memory({op:'readCart'})`/
|
|
487
|
-
`memory({op:'read'})` at it. Turns "infer for hours" into ~3 calls.
|
|
488
|
-
- **`breakpoint({on:'read', address, ...})`** — the read-side mirror of `breakpoint({on:'write'})`: the
|
|
489
|
-
EXACT instruction PC that READ an address (who *consumes* a value).
|
|
490
|
-
- **`frame({op:'stepInstruction'})`** — CPU-level single-step; pair with `cpu({op:'read'})` to watch
|
|
491
|
-
registers change one instruction at a time.
|
|
492
|
-
- These work on all 14 platforms (every bundled CPU family) — including `breakpoint({on:'write'})`
|
|
493
|
-
(as of 0.6.0 PC Engine gained its write watchpoint, so no platform is the exception
|
|
494
|
-
anymore).
|
|
495
|
-
|
|
496
|
-
```js
|
|
497
|
-
breakpoint({ on:'write', address:0xFF2000 }) → { pc:"$49E", ... } // get a real instruction PC
|
|
498
|
-
breakpoint({ on:'pc', address:0x49E }) → { hit:true, pc:"$49E" } // CPU frozen here
|
|
499
|
-
cpu({ op:'read', platform:"genesis", cpu:"main" }) // → registers.A0 = the pointer
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
All in the `assets` category except `disasm({target:'rom'})` (in `debug`); the breakpoint
|
|
503
|
-
trio (`breakpoint({on:'pc'})`/`breakpoint({on:'read'})`/`frame({op:'stepInstruction'})`) is in `advanced`.
|
|
504
|
-
|
|
505
|
-
### Before you hunt — check the cheat database (`cheats({op:'lookup'})`)
|
|
506
|
-
|
|
507
|
-
For a KNOWN commercial ROM, the fastest way to find the byte is to not hunt at
|
|
508
|
-
all: the bundled cheat database is a free, crowd-sourced **map of labeled RAM
|
|
509
|
-
addresses and code sites**. Call `cheats({op:'lookup', path})` FIRST — for a matched
|
|
510
|
-
game it returns that game's cheats with the address decoded out of each one:
|
|
511
|
-
|
|
512
|
-
```js
|
|
513
|
-
cheats({ op:'lookup', path: "Rygar (USA).nes" })
|
|
514
|
-
// → { matched:true, confidence:"name", game:"Rygar (USA)", crc32:"...",
|
|
515
|
-
// entries:[
|
|
516
|
-
// { desc:"Infinite Magic Attack", code:"00CD:FF",
|
|
517
|
-
// parts:[{ address:"$00CD", value:"0xFF", kind:"ram" }] }, // ← labeled RAM var
|
|
518
|
-
// { desc:"Infinite Health", code:"SXUZXTSA",
|
|
519
|
-
// parts:[{ address:"$8E20", value:"0xA5", compare:"0x85", kind:"code" }] }, // ← code site
|
|
520
|
-
// ...] }
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
So "which byte holds magic?" is answered in one call: `$00CD`. A RAM cheat
|
|
524
|
-
(`kind:"ram"`) is a **labeled variable**; a ROM cheat (`kind:"code"`, has a
|
|
525
|
-
`compare`) is a **labeled patch site** — point `disasm({target:'rom'})` at its address
|
|
526
|
-
to read the routine. Filter a long list with `filter:"health"` or `kind:"ram"`.
|
|
527
|
-
|
|
528
|
-
**Device types are labeled — it's not all "Game Genie."** Each decoded part
|
|
529
|
-
carries a `device` so you know exactly what you're looking at:
|
|
530
|
-
`game-genie` (NES/Genesis/SNES/GB ROM patches), `pro-action-replay` (SNES — the
|
|
531
|
-
most common SNES device, RAM pokes like `7E0DBF63`), `gameshark` (GB RAM),
|
|
532
|
-
`action-replay` (SMS/GG), or `raw` (`ADDR:VAL`). A few formats (e.g. the SMS/GG
|
|
533
|
-
Game Genie variant) are labeled with their device but left address-undecoded
|
|
534
|
-
rather than guessing — honest over wrong.
|
|
535
|
-
|
|
536
|
-
**Trust it like you trust disasm — verify, don't assume.** A match is by
|
|
537
|
-
No-Intro name / filename, NOT a verified CRC, so it's a PROBABLE match: very
|
|
538
|
-
likely right, but a different region/revision can use different addresses. The
|
|
539
|
-
`note` says so explicitly. Confirm a label before patching — the cheapest
|
|
540
|
-
confirmation is to apply it and watch:
|
|
541
|
-
|
|
542
|
-
```js
|
|
543
|
-
cheats({ op:'apply', path:"Rygar (USA).nes", desc:"Infinite Magic Attack" }) // enable it live
|
|
544
|
-
frame({ op:'screenshot' }) // see the effect → label confirmed
|
|
545
|
-
// or apply a RAW code from anywhere:
|
|
546
|
-
cheats({ op:'apply', code:"00CD:FF" }) // RAM poke → appliedAs:"ram"
|
|
547
|
-
cheats({ op:'apply', code:"SXIOPO" }) // Game Genie (core decodes it)
|
|
548
|
-
cheats({ op:'apply', code:"C06C:0C:26" }) // raw ROM patch → auto-re-encoded to a read-intercept (appliedAs:"rom", reencodedFrom)
|
|
549
|
-
cheats({ op:'clear' }) // remove all
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
**`appliedAs` tells you how it went in** — `"ram"` (per-frame poke), `"rom"` (in-core
|
|
553
|
-
read-intercept), `"raw"` (core-decoded device code), or `"rom-unencodable"` (a ROM
|
|
554
|
-
address that couldn't be made into a working ROM patch — likely a no-op; add a COMPARE
|
|
555
|
-
byte). A raw `ADDR:VAL:COMPARE` on a ROM address would otherwise silently no-op as a RAM
|
|
556
|
-
poke, so `cheats({op:'apply'})` transparently re-encodes it to the platform's ROM-patch device (NES/
|
|
557
|
-
Genesis/GB Game Genie, SNES Game Genie — NOT Pro Action Replay, which is RAM). **Boot-time
|
|
558
|
-
cheats:** pass `loadMedia({ cheats:[…] })` to apply codes BEFORE frame 0 (iterating on a
|
|
559
|
-
boot-seeded value), and use `host({op:'reset', hard:true})` for a true power-cycle — plain `host({op:'reset'})`
|
|
560
|
-
is the RESET button and leaves work RAM (and boot-seeded state) intact.
|
|
561
|
-
|
|
562
|
-
`cheats({op:'apply'})` is also just **fun** — play any matched game with infinite lives,
|
|
563
|
-
invincibility, etc. It is **NON-DESTRUCTIVE**, exactly like RetroArch: the cheat
|
|
564
|
-
lives in volatile core state (a per-frame RAM write, or an in-core read-intercept
|
|
565
|
-
for ROM cheats), the ROM file on disk is NEVER touched, and `host({op:'reset'})` / `state({op:'load'})`
|
|
566
|
-
/ `cheats({op:'clear'})` removes it. **`cheats({op:'lookup'})` DB coverage (13/14):** NES, GB/GBC,
|
|
567
|
-
SNES, Genesis, SMS/GG, Atari 2600/7800, **Lynx**, **GBA**, **PC Engine**, **MSX** —
|
|
568
|
-
every tier-1 system except **C64** (the cheat database ships no C64 entries, so
|
|
569
|
-
there's nothing to look up; `cheats({op:'make'})` still works on C64). The DB is its own
|
|
570
|
-
package (`romdev_game_codes`), lazy-loaded per platform; `cheats({op:'search', platform,
|
|
571
|
-
query})` fuzzy-finds a game by name. One caveat: **GBA** DB cheats are
|
|
572
|
-
Code Breaker / GameShark (encrypted), so they're **apply-only** — the `code`
|
|
573
|
-
applies live, but the address isn't descrambled into a labeled map the way the
|
|
574
|
-
other systems are (the response says so via `mapNote`). **`cheats({op:'apply'})` /
|
|
575
|
-
`cheats({op:'make'})` work on all 14.** Unmatched ROMs (homebrew, your own WIP, an
|
|
576
|
-
unlisted dump) return `matched:false` with a clear reason — the tool never
|
|
577
|
-
guesses.
|
|
578
|
-
|
|
579
|
-
### Creating NEW cheat codes (`cheats({op:'make'})`)
|
|
580
|
-
|
|
581
|
-
The inverse of decoding: turn a byte you found into a shareable code — for ANY
|
|
582
|
-
ROM, **including your own homebrew/WIP** where no DB entry exists. This closes
|
|
583
|
-
the loop with the byte-hunting tools:
|
|
584
|
-
|
|
585
|
-
```js
|
|
586
|
-
breakpoint({ on:'write', precision:'sampled', region:"system_ram", offset:0xCD }) // 1. find the byte (or use cheats({op:'lookup'}))
|
|
587
|
-
cheats({ op:'make', platform:"nes", address:0x00CD, value:0xFF })
|
|
588
|
-
// → { raw:"CD:FF", note:"RAM cheat...", ... } // 2. RAM poke → raw code
|
|
589
|
-
// For a ROM/Game-Genie patch, read the current byte and pass it as `compare`:
|
|
590
|
-
memory({ op:'read', region:"prg_rom", offset:0x8E20 }) // (current byte = 0x85)
|
|
591
|
-
cheats({ op:'make', platform:"nes", address:0x8E20, value:0xA5, compare:0x85 })
|
|
592
|
-
// → { gameGenie:"SZZAETSA", verified:true, raw:"8E20:A5:85", ... }
|
|
593
|
-
cheats({ op:'apply', code:"SZZAETSA" }) → frame({ op:'screenshot' }) // 3. confirm it works
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
`cheats({op:'make'})` encodes for the platform's NATIVE device(s) and **labels each one**
|
|
597
|
-
— NES/Genesis → Game Genie; SNES → Pro Action Replay **and** Game Genie; GB/GBC
|
|
598
|
-
→ Game Genie (ROM) + GameShark (RAM); SMS/GG → Action Replay — plus the raw
|
|
599
|
-
`ADDR:VAL` always. Each generated code carries `verified:true` (decoded back and
|
|
600
|
-
confirmed; the encoders round-trip 100% against the full DB — NES/Genesis/GB/GBC
|
|
601
|
-
Game Genie, SNES Game Genie + PAR, GB GameShark). Force a specific device with
|
|
602
|
-
`device:`. A RAM cheat needs just `address`+`value`; a ROM patch adds `compare`
|
|
603
|
-
(the byte currently there). Nothing is ever written to a ROM file.
|
|
604
|
-
**`cheats({op:'make'})` works on all 14 tier-1 systems** — the systems with no native
|
|
605
|
-
letter-code device (Atari 2600/7800, Lynx, GBA, C64, PC Engine, MSX) get a
|
|
606
|
-
verified raw `ADDR:VAL` code that `cheats({op:'apply'})` passes straight to the core.
|
|
607
|
-
|
|
608
|
-
```js
|
|
609
|
-
cheats({ op:'make', platform:"snes", address:0x7E0DBF, value:0x63 })
|
|
610
|
-
// → { codes:[ {device:"pro-action-replay", code:"7E0DBF63", verified:true},
|
|
611
|
-
// {device:"game-genie", code:"17D8-9EE8", verified:true} ],
|
|
612
|
-
// raw:"7E0DBF:63", ... }
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
### Editing in-game TEXT (font maps)
|
|
616
|
-
|
|
617
|
-
Games store text as their own tile-index encoding (Excitebike: A=$0A; Mario:
|
|
618
|
-
ASCII-offset; FF: sparse). Three tools automate the round-trip instead of
|
|
619
|
-
hand-deriving the table:
|
|
620
|
-
|
|
621
|
-
- **`text({op:'learn'})`** — infer the char→tile-ID map. TWO modes:
|
|
622
|
-
- ROM mode: `knownStrings:[{text, offset}]` when you found the text's bytes.
|
|
623
|
-
- **LIVE mode: `fromScreen:[{text, row, col}]`** — the text is on screen RIGHT
|
|
624
|
-
NOW; reads the tile IDs straight from the live BG map at a tile position. This
|
|
625
|
-
breaks the chicken-and-egg (you'd otherwise need the ROM offset you're
|
|
626
|
-
hunting). Works on every tilemap platform (NES/SNES/Genesis/GB/GBC/SMS/GG/C64);
|
|
627
|
-
`background({view:'map'})` shows you where the text sits. (atari2600/7800, lynx,
|
|
628
|
-
gba have no text-tile nametable → use ROM mode.)
|
|
629
|
-
- **`text({op:'find', romPath, text, fontMap})`** — locate the string in the
|
|
630
|
-
ROM. Returns `fileOffset` (.nes), `prgFileOffset` (prg.bin), and a bank-aware
|
|
631
|
-
`cpuAddress` + `bank` (NES/GB/GBC in-bank address, Genesis flat; SNES is
|
|
632
|
-
mapper-dependent → use the offsets) — feed `{startAddress, bank}` to
|
|
633
|
-
`disasm({target:'rom'})`. Flags a likely length-prefix byte to avoid the classic
|
|
634
|
-
overrun.
|
|
635
|
-
- **`text({op:'encode'})`** — text + map → bytes, ready for `romPatch({op:'write'})`.
|
|
636
|
-
|
|
637
|
-
```js
|
|
638
|
-
text({ op:'learn', fromScreen:[{ text:"START", row:13, col:11 }] }) // read tiles off the live screen
|
|
639
|
-
text({ op:'find', romPath, text:"MOUNTAIN", fontMap }) // → offsets + bank + context
|
|
640
|
-
text({ op:'encode', text:"NEW TEXT ", fontMap }) → romPatch({ op:'write', ... }) // rewrite it
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
**Tools for hacking, by category:**
|
|
644
|
-
|
|
645
|
-
- `romPatch({op:'write', path, offset, hex, expect, allowExpand})` — generic byte
|
|
646
|
-
splicer with safety check. THE primitive — every other hack tool
|
|
647
|
-
composes through it. `expect` refuses the write if existing bytes don't
|
|
648
|
-
match, catching the silent corruption when a patch authored against
|
|
649
|
-
region A is applied to region B.
|
|
650
|
-
- `assembleSnippet({cpu, origin, code})` — assemble a tiny chunk of asm
|
|
651
|
-
to raw bytes. No header, no linker config, no segments. Supports
|
|
652
|
-
`6502 / 65c02 / 65816 / 68k / z80 / sm83 / gb / gbc / huc6280`.
|
|
653
|
-
Z80 NOTE: sdas dialect requires `#` on immediates (`ld a,#5`, not
|
|
654
|
-
`ld a,5`).
|
|
655
|
-
- `romPatch({op:'diff', platform, a, b})` — mapper-aware ROM diff. Reports CPU
|
|
656
|
-
addresses (NROM-128 mirrors correctly, SNES LoROM banks as `XX:XXXX`),
|
|
657
|
-
per-region tallies (PRG vs CHR vs header), and `tile: N` annotations
|
|
658
|
-
on CHR changes for direct sprite-hack identification.
|
|
659
|
-
- `romPatch({op:'findFree', path, minLength, fillBytes})` — locate runs of $FF
|
|
660
|
-
or $00 for asm overlays. Sorted longest-first.
|
|
661
|
-
- `disasm({target:'references', path, platform, address})` — find every instruction
|
|
662
|
-
that references a target address. Classifies refs as
|
|
663
|
-
`call/jump/branch/read/write/use/ref`. Walks the vector table too.
|
|
664
|
-
Limitation: only direct addressing modes; indirect/computed jumps
|
|
665
|
-
not detected.
|
|
666
|
-
- `romPatch({op:'spliceCHR', path, platform, pngBase64, tileIndex, expect, bank, paletteHint})` —
|
|
667
|
-
composition: PNG → tile bytes → splice into CHR at tile slot N.
|
|
668
|
-
Auto-locates iNES CHR base. `expect` checks the existing tile bytes.
|
|
669
|
-
`bank: N` (NES) replaces magic file offsets; `paletteHint:["#RRGGBB",...]`
|
|
670
|
-
gives explicit RGB→palette-index mapping (skips the default quantization
|
|
671
|
-
that requires PNGs with exactly 4 distinct grayscale levels).
|
|
672
|
-
- `cheats({op:'lookup', path, filter, kind})` — match a KNOWN ROM to the bundled
|
|
673
|
-
cheat DB and return THIS game's labeled RAM addresses + code sites
|
|
674
|
-
(decoded from each cheat). The free "which byte holds X?" map. Probable
|
|
675
|
-
match (name/filename, not CRC) — verify before patching.
|
|
676
|
-
- `cheats({op:'apply', code | desc+path, index, enabled})` /
|
|
677
|
-
`cheats({op:'clear'})` — apply a cheat to the loaded game LIVE and
|
|
678
|
-
non-destructively (the RetroArch way: volatile core state, ROM file
|
|
679
|
-
never touched). Use a raw `code` or a matched `desc`. Doubles as the
|
|
680
|
-
cheapest way to VERIFY a `cheats({op:'lookup'})` label (apply → screenshot), and
|
|
681
|
-
as a fun-bonus (play with infinite lives, etc.).
|
|
682
|
-
- `cheats({op:'make', platform, address, value, compare?, style})` — CREATE a new
|
|
683
|
-
cheat code from an address+value (the inverse of decoding). Returns a
|
|
684
|
-
Game Genie letter code + the raw ADDR:VAL, with a `verified` round-trip
|
|
685
|
-
check. Works on any ROM incl. homebrew/WIP. Pair with `breakpoint({on:'write',precision:'sampled'})`/
|
|
686
|
-
`cheats({op:'lookup'})` (find the byte) → `cheats({op:'make'})` (encode) → `cheats({op:'apply'})` (confirm).
|
|
687
|
-
- `watch({on:'mem', region, offset, length, frames, pressDuring})` /
|
|
688
|
-
`breakpoint({on:'write', precision:'sampled', region, offset, maxFrames, pressDuring})` — frame-level
|
|
689
|
-
memory-write trace. Reports every change with PC, so you can map a
|
|
690
|
-
RAM byte back to the writing code path. Cross-platform. The "find
|
|
691
|
-
the byte" half of hacking, mechanized. (Reach for this when a ROM
|
|
692
|
-
ISN'T in the cheat DB, or to find a byte no cheat covers.)
|
|
693
|
-
- `background({view:'rendered'})` — at the current emulator state, walk the
|
|
694
|
-
BG nametable + OAM and return the set of tile IDs actually being
|
|
695
|
-
drawn. Sample at known game states (title / gameplay / menu) and diff
|
|
696
|
-
the sets to map tile IDs to game assets without scanning sheets by eye.
|
|
697
|
-
- `cart({op:'extract', path, outputDir})` — split ROM into standard parts
|
|
698
|
-
(NES: header.bin/prg.bin/chr.bin; SNES: copier_header + rom + internal
|
|
699
|
-
header; Genesis: vectors/header/body; GB: boot/header/body) plus a
|
|
700
|
-
manifest.json with mapper, mirroring, etc.
|
|
701
|
-
- `cart({op:'wrap', platform, ...})` — counterpart to `cart({op:'extract'})`.
|
|
702
|
-
Emits `wrapperSource` (.s) + `linkerConfig` (cc65 ld65 cfg) ready
|
|
703
|
-
for `build({output:'rom'})`. Per-platform templates.
|
|
704
|
-
- `disasm({target:'rom'})` — see "Disassembler" section below for the full
|
|
705
|
-
annotation set.
|
|
706
|
-
|
|
707
|
-
For graphics swaps specifically:
|
|
708
|
-
- `tiles({op:'png', source:'path', platform, path, bank, paletteFromEmulator, paletteIndex})`
|
|
709
|
-
from a source game → PNG of its tiles. `bank: N` (NES 4 KB CHR bank
|
|
710
|
-
index) replaces magic file-offset math. `paletteFromEmulator: true`
|
|
711
|
-
+ `paletteIndex` colors the export with the live game palette
|
|
712
|
-
(instead of grayscale) — much easier to recognize art and edit in a
|
|
713
|
-
pixel tool.
|
|
714
|
-
- `importArt({from:'rom', sourceRom, sourcePlatform, sourceBank,
|
|
715
|
-
sourceTileX/Y/W/H, targetPlatform, outputPng, intent, paletteIndex})`
|
|
716
|
-
— one-call lift of a tile region from a source game's ROM into the
|
|
717
|
-
target platform's tile format. Combines extract + crop + quantize +
|
|
718
|
-
optional manifest. Under `intent:"homebrew"` reads the live source
|
|
719
|
-
palette automatically (same `paletteFromEmulator` semantics as
|
|
720
|
-
`tiles({op:'png',source:'path'})`); under `intent:"rom-hack"` preserves source
|
|
721
|
-
bytes verbatim. Output PNG + manifest feed straight into
|
|
722
|
-
`importArt({from:'texturepacker'})`.
|
|
723
|
-
- `encodeArt({stage:'tiles', platform, pngBase64})` → target-platform tile bytes
|
|
724
|
-
- `romPatch({op:'spliceCHR'})` to write them into the CHR region of your target ROM
|
|
725
|
-
(handles the `encodeArt({stage:'tiles'})` + `romPatch({op:'write'})` composition in one call)
|
|
456
|
+
**The full RE/romhack workflow is the playbook — read it FIRST:**
|
|
457
|
+
`platform({op:'doc', platform:'romhacking', name:'playbook'})`. It's the cross-platform
|
|
458
|
+
decision tree that wires the primitives below together (with the trap each one exists
|
|
459
|
+
to avoid), plus the per-asset round-trips (text, compressed assets, graphics) and a
|
|
460
|
+
Quick-reference table. Don't reconstruct the flow from this summary — it's only here so
|
|
461
|
+
you know the capability exists and where the detail lives.
|
|
462
|
+
|
|
463
|
+
Key primitives (all bundled, all 14 tier-1 systems unless noted; full detail in the
|
|
464
|
+
playbook):
|
|
465
|
+
|
|
466
|
+
- **Find a value's RAM address** — the Cheat-Engine loop `memory({op:'search'})` →
|
|
467
|
+
`memory({op:'searchNext', compare})`, NOT a full-RAM diff. (`memory({op:'snapshot'})`+`memory({op:'diff'})`
|
|
468
|
+
answers the different question "which bytes did THIS one event touch?".)
|
|
469
|
+
- **Free RAM/code map for a known game** — `cheats({op:'lookup', path})` decodes each cheat
|
|
470
|
+
into labeled addresses (`kind:ram`=variable, `kind:code`=patch site); `cheats({op:'apply'})`
|
|
471
|
+
confirms a label live + non-destructively; `cheats({op:'make'})` mints a verified shareable
|
|
472
|
+
code from a byte you found. Probable (name) match — verify before patching.
|
|
473
|
+
- **Find the instruction that wrote/read a byte** — `breakpoint({on:'write', address})` returns
|
|
474
|
+
the EXACT writer (core-level watchpoint, correct under NMI/IRQ; reports `bank`);
|
|
475
|
+
`precision:'sampled'` is the lighter frame-sampled lead. `breakpoint({on:'read'})` is the
|
|
476
|
+
read-side mirror. `found:false` ⇒ the region is bulk-copied/DMA'd from a SOURCE struct.
|
|
477
|
+
- **Read a register AT an instruction** — `breakpoint({on:'pc', address})` freezes the CPU →
|
|
478
|
+
`cpu({op:'read'})` for the live register file (e.g. a decoder's source pointer);
|
|
479
|
+
`frame({op:'stepInstruction'})` single-steps. The "infer for hours → read it in 3 calls" move.
|
|
480
|
+
- **Discover the unknown routine** — `watch({on:'range'|'pc'})` logs every PC touching a
|
|
481
|
+
region; `watch({on:'dma'})` (Genesis) traces a graphic back to its ROM source offset.
|
|
482
|
+
- **Confirm bytes / classify** — `memory({op:'readCart'})` reads the running program image
|
|
483
|
+
(un-banked: file offset = CPU address); `memory({op:'classify'})` tells a real table from
|
|
484
|
+
ASCII/code before you trust it.
|
|
485
|
+
- **Edit on-screen text** — `text({op:'learn'})` infers the font map (incl. LIVE
|
|
486
|
+
`fromScreen` mode — no offset needed) and flags pre-rendered-graphic text (don't patch
|
|
487
|
+
the "string"); `text({op:'find'})`/`text({op:'encode'})` do the string round-trip.
|
|
488
|
+
- **Compressed assets** — drive the ROM's OWN codec: `cpu({op:'decompress'})`/`cpu({op:'call'})`
|
|
489
|
+
to expand, then the re-inject trio `romPatch({op:'makeStored'})` (verbatim-expand block) →
|
|
490
|
+
`romPatch({op:'findFree'})` → `romPatch({op:'relocate'})`, with `romPatch({op:'findPointer'})` for the
|
|
491
|
+
loader pointer. Don't reimplement the compressor.
|
|
492
|
+
- **Author/verify the patch** — `assembleSnippet({cpu, origin, code})` → bytes;
|
|
493
|
+
`romPatch({op:'write'})` (always pass `expect`); `romPatch({op:'diff'})` mapper-aware verify;
|
|
494
|
+
`disasm({target:'references'})` (static "who touches this?"); `cart({op:'extract'|'wrap'})`.
|
|
495
|
+
- **Graphics swaps** — `tiles({op:'png'})`/`importArt({from:'rom'})` → edit →
|
|
496
|
+
`romPatch({op:'spliceCHR'})`; `background({view:'rendered'})` for the tile IDs drawn now.
|
|
497
|
+
|
|
498
|
+
Category placement: most live in `assets`; `disasm({target:'rom'})` is in `debug`; the
|
|
499
|
+
breakpoint trio (`pc`/`read`/`stepInstruction`) is in `advanced`.
|
|
726
500
|
|
|
727
501
|
## Disassembler
|
|
728
502
|
|
|
729
|
-
`disasm({target:'rom'})`
|
|
503
|
+
`disasm({target:'rom'|'references'|'project'})` covers **all 14 platforms** — every CPU
|
|
504
|
+
family disassembles through a native binutils disassembler compiled to WASM (no
|
|
505
|
+
hand-rolled JS decoders): 6502/65816 via cc65's `da65`, Z80 (SMS/GG/MSX) + SM83
|
|
506
|
+
(GB/GBC) via one z80-elf `objdump` (`-m z80` / `-m gbz80`), m68k (Genesis) via
|
|
507
|
+
`m68k-elf-objdump`, ARM/Thumb (GBA) via `arm-none-eabi-objdump`. The annotations
|
|
508
|
+
(vector labels / hardware-register names / file-offset comments / `untilReturn`) are
|
|
509
|
+
post-processing layered on top.
|
|
730
510
|
|
|
731
511
|
```js
|
|
732
512
|
disasm({target:'rom', path, platform:"nes", startAddress:0xC184,
|
|
733
513
|
length:64, untilReturn:true})
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
// lda #$00 ; C186 A9 00 .. @0x196 (prg @0x186)
|
|
738
|
-
// sta $2000 ; C188 8D 00 20 .. @0x198 (prg @0x188) PPUCTRL
|
|
739
|
-
// ldx #$FF ; C18B A2 FF .. @0x19B (prg @0x18B)
|
|
740
|
-
// ...
|
|
514
|
+
// reset: sei ; C184 78 @0x194 (prg @0x184)
|
|
515
|
+
// lda #$00 ; C186 A9 00 @0x196 (prg @0x186)
|
|
516
|
+
// sta $2000 ; C188 8D 00 20 @0x198 (prg @0x188) PPUCTRL
|
|
741
517
|
```
|
|
742
518
|
|
|
743
|
-
|
|
744
|
-
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
their RST addresses, `irq:` at $0038 (SMS vblank handler), `nmi:` at
|
|
748
|
-
$0066 (pause button). For GB/GBC, the SM83 vectors get the same
|
|
749
|
-
treatment plus the dedicated IRQ vectors: `vblank:` at $0040,
|
|
750
|
-
`lcd_stat:` at $0048, `timer:` at $0050, `serial:` at $0058,
|
|
751
|
-
`joypad:` at $0060, and `entry:` at $0100. `autoLabelVectors:false`
|
|
752
|
-
to turn off.
|
|
753
|
-
- **Hardware register names** (`; PPUCTRL`, `; PPUMASK`, `; SND_CHN`,
|
|
754
|
-
`; VRAM`, `; LCDC`, `; VDP_CTRL`, `; IO_PORT_A` etc) on any operand
|
|
755
|
-
that hits a known platform register. NES + SNES + Genesis + GB + SMS/GG
|
|
756
|
-
tables built in. `annotateRegisters:false`.
|
|
757
|
-
- **File-offset comments** (`; @0xNNNN`) on every disassembled line —
|
|
758
|
-
mapper-aware, so $C184 on NROM-128 correctly reports `@0x194`. Direct
|
|
759
|
-
input to `romPatch({op:'write'})`'s `offset`. For NES iNES files, the header-stripped
|
|
760
|
-
PRG offset is ALSO reported (`@0x194 (prg @0x184)`) so you can patch
|
|
761
|
-
either the `.nes` file or `prg.bin` from `cart({op:'extract'})` without doing
|
|
762
|
-
the -16 math. `annotateFileOffsets:false` to turn off.
|
|
763
|
-
- **Mapper-aware addressing**: NROM-128 mirror at $C000, MMC1/MMC3/UxROM
|
|
764
|
-
top bank fixed at $C000, SMS sega-mapper slot-0/1/2 1:1 file mapping,
|
|
765
|
-
GB/GBC slot 0 fixed + slot 1 banked (pass `bank` to target a non-
|
|
766
|
-
default ROM bank). No more manual `startAddress: 49152` because the
|
|
767
|
-
disassembler understood the mapping.
|
|
768
|
-
- **`endAddress` alternative to `length`** — disassemble "from X to Y"
|
|
769
|
-
without computing byte count yourself.
|
|
770
|
-
- **`untilReturn: true`** — truncates at the first `rts/rti/rtl/bare jmp`
|
|
771
|
-
(6502) or `ret/reti/retn/bare jp` (Z80) or `ret/reti/bare jp/jp hl`
|
|
772
|
-
(SM83). Combine with an auto-tagged `reset:` label to grab exactly
|
|
773
|
-
one routine.
|
|
774
|
-
- **`dataRanges: [{start, length}]`** — mark address ranges as `.byte`
|
|
775
|
-
tables instead of bizarre disassembled "code." Useful for embedded
|
|
776
|
-
sprite tables, music data, lookup tables.
|
|
777
|
-
- **`outputPath`** — writes raw asm to disk instead of returning a
|
|
778
|
-
188KB JSON wad. Returns `{outputPath, asmBytes, asmLines}` for log/inspection.
|
|
779
|
-
|
|
780
|
-
Every CPU family disassembles through a native binutils disassembler compiled to
|
|
781
|
-
WASM: 6502/65816 via cc65's `da65`; Z80 (SMS/GG/MSX) + SM83 (GB/GBC) via one
|
|
782
|
-
z80-elf `objdump` (`-m z80` / `-m gbz80`); m68k (Genesis) via `m68k-elf-objdump`;
|
|
783
|
-
ARM/Thumb (GBA) via `arm-none-eabi-objdump`. No hand-rolled JS decoders. The
|
|
784
|
-
auto-label / register-annotation / file-offset / untilReturn handling is
|
|
785
|
-
post-processing layered on the objdump output.
|
|
786
|
-
|
|
787
|
-
### Whole-ROM, rebuildable projects — `disasm({target:'project'})`
|
|
788
|
-
|
|
789
|
-
`disasm({target:'rom'})` gives you one routine as text. `disasm({target:'project'})` turns an
|
|
790
|
-
**entire ROM into a complete, re-buildable project in one call**, across **all 14
|
|
791
|
-
systems** (NES, SNES, GB/GBC, SMS/GG, Genesis, **GBA**, C64, Atari 2600/7800,
|
|
792
|
-
**Lynx** — 65C02, **PC Engine** — HuC6280, and **MSX** — Z80; byte-exact on 13,
|
|
793
|
-
PC Engine the one current exception — see caveats).
|
|
794
|
-
Each region disassembles through the CPU's native objdump and reassembles through
|
|
795
|
-
the matching native `as`/`ld`/`objcopy`, so the round-trip is guaranteed byte-for-byte:
|
|
796
|
-
|
|
797
|
-
```js
|
|
798
|
-
disasm({ target:'project', path: "game.nes", outputDir: "./game-disasm" })
|
|
799
|
-
// → { ok, platform, regions:[{file, startAddress, roundTripOk, readablePercent}],
|
|
800
|
-
// roundTrip:{ allByteExact, failed:[] }, readablePercentAvg,
|
|
801
|
-
// rebuild:{ blobs:[{file,bytes}], buildCall:{...}|null, verifiable, buildDoc:"BUILD.md", notes } }
|
|
802
|
-
// Writes the .asm regions + chr.bin/header blobs + BUILD.md + rebuild.json to outputDir.
|
|
803
|
-
```
|
|
519
|
+
`disasm({target:'project'})` is the **RE-rebuild workhorse**: one call turns a whole ROM
|
|
520
|
+
into a byte-exact, re-buildable disassembly (per-region `.asm` + rebuild glue), faithful
|
|
521
|
+
where it can be and falling back to `.byte` where it can't — so it *always* reassembles to
|
|
522
|
+
the original image. That's the path for any structural hack (new logic / text / graphics).
|
|
804
523
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
to
|
|
810
|
-
files ALWAYS rebuild to the original bytes (`roundTrip.allByteExact`). The
|
|
811
|
-
`readablePercent` per region tells you how much came back as real instructions
|
|
812
|
-
vs. data. Each `.asm` carries a provenance + round-trip header and is ready to
|
|
813
|
-
edit and rebuild with the platform's native toolchain.
|
|
814
|
-
|
|
815
|
-
**It also writes the REBUILD GLUE** so the project is turnkey, not just byte-exact
|
|
816
|
-
region files. Alongside the `.asm` files you get: any non-code DATA blobs the
|
|
817
|
-
rebuild needs (NES CHR-ROM → `chr.bin`; the stripped Genesis/GBA/Lynx/MSX
|
|
818
|
-
cartridge header → `*.bin`), a **`BUILD.md`** with the exact rebuild steps, and —
|
|
819
|
-
where a one-call rebuild exists — a **`rebuild.json`** holding the precise
|
|
820
|
-
`build({...})` args (absolute paths). The response carries the same under
|
|
821
|
-
`rebuild: { blobs, buildCall, verifiable, buildDoc, notes }`. So the RE loop is:
|
|
822
|
-
`disasm({target:'project'})` → edit a `.asm` → rebuild → `diffRoms` to confirm.
|
|
823
|
-
|
|
824
|
-
Two rebuild tiers (honest — the disasm emits each CPU's native-reassembler
|
|
825
|
-
syntax, which only some platforms' `build()` toolchains can consume):
|
|
826
|
-
- **One-call `build()` rebuild, byte-identical** — **NES, C64, Atari 7800, Lynx**.
|
|
827
|
-
Feed `rebuild.json` straight to `build`. (NES uses the new `inesHeader` option
|
|
828
|
-
— see below. Lynx: build() yields the headerless image; prepend the shipped
|
|
829
|
-
`lnx_header.bin` for the full `.lnx`.)
|
|
830
|
-
- **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`**
|
|
831
|
-
— **SMS, GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()`
|
|
832
|
-
toolchains (SDCC/RGBDS/asar/dasm/vasm) can't reassemble the disasm's ca65/GNU-as
|
|
833
|
-
syntax, so `BUILD.md` gives the proven native `as`/`ld`/`objcopy` chain.
|
|
834
|
-
- **PC Engine** is the one not-yet-byte-exact case (the region trims real padding /
|
|
835
|
-
doesn't strip a copier header) — `BUILD.md` says so.
|
|
836
|
-
|
|
837
|
-
**Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the
|
|
838
|
-
most common NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{
|
|
839
|
-
prgBanks, chrBanks, mapper, mirroring}, sourcesPaths:{...the PRG...},
|
|
840
|
-
binaryIncludePaths:{ "chr.bin":... }})` auto-emits the 16-byte iNES header + the
|
|
841
|
-
CHARS segment wiring + the flat NROM `.cfg` — no hand-derived header bytes, no
|
|
842
|
-
glue `.s`/`.cfg`. (`disasm({target:'project'})` puts exactly this call in
|
|
843
|
-
`rebuild.json`.) For homebrew C that ships fixed tile art, `linkerConfig:"chr-rom"`
|
|
844
|
-
is the segment-split equivalent. See the NES MENTAL_MODEL.md "Rebuilding a CHR-ROM
|
|
845
|
-
NROM image" section.
|
|
846
|
-
|
|
847
|
-
Reassembler per CPU family (all bundled WASM, no installs): **cc65** ca65/ld65
|
|
848
|
-
for 6502 + 65816; native binutils **`as`/`ld`/`objcopy`** for the GNU CPUs —
|
|
849
|
-
`m68k-elf` (Genesis), `arm-none-eabi` (GBA), and one `z80-elf` for both Z80
|
|
850
|
-
(SMS/GG/MSX) and gbz80 (GB/GBC). objdump and `as` share GNU syntax, so objdump's
|
|
851
|
-
output feeds straight back into `as` with no translation; any line the assembler
|
|
852
|
-
won't reproduce exactly is healed to a `.byte` of its real bytes.
|
|
853
|
-
|
|
854
|
-
Caveats worth knowing up front:
|
|
855
|
-
- **SNES and large Genesis ROMs come back byte-exact but DATA-ONLY**
|
|
856
|
-
(low `readablePercent`). Flat whole-ROM disassembly of a mostly-data image
|
|
857
|
-
heals down to `.byte`; meaningful instruction coverage there needs recursive
|
|
858
|
-
entry-point following, a known follow-up. The bytes are always correct.
|
|
859
|
-
- **GBA** rebuilds byte-exact but reads LOW: GBA C compiles mostly to Thumb,
|
|
860
|
-
reached via an ARM crt0 stub, so an ARM-mode disasm decodes the Thumb spans as
|
|
861
|
-
`.byte`. ARM/Thumb mode-tracking is the readability follow-up; the bytes are
|
|
862
|
-
always correct. (The 192-byte GBA header is emitted as a clean data region.)
|
|
863
|
-
- Banked-NES is the strongest case — per-bank regions come back ~100%
|
|
864
|
-
instructions. GB/GBC, SMS/GG, C64, and Atari are also near-100%.
|
|
865
|
-
- **PC Engine** is the one platform that does NOT round-trip byte-exact yet: the
|
|
866
|
-
region trims real trailing $FF padding and doesn't strip a 512-byte copier
|
|
867
|
-
header, so the emitted region is a lossy view of the `.pce`. `BUILD.md` flags
|
|
868
|
-
this; a `planRegions` fix is the follow-up.
|
|
869
|
-
- Platform is sniffed from the file extension; pass `platform:` to override.
|
|
524
|
+
The tool's own params document the flags (`untilReturn` / `dataRanges` / `endAddress` /
|
|
525
|
+
`bank` / `thumb` / `outputPath`; auto vector labels + register-name + file-offset
|
|
526
|
+
annotations, all on by default; NES file offsets report both `.nes` and PRG). The
|
|
527
|
+
ROM-hacking playbook (`platform({op:'doc', platform:'romhacking', name:'playbook'})`) has
|
|
528
|
+
the end-to-end workflow — read those rather than re-deriving the detail here.
|
|
870
529
|
|
|
871
530
|
## CHR/tile tools — file vs emulator source
|
|
872
531
|
|
|
@@ -1104,34 +763,50 @@ If a platform genuinely lacks a tilemap (Atari 2600 races the beam; 7800 uses di
|
|
|
1104
763
|
|
|
1105
764
|
## Known toolchain landmines
|
|
1106
765
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
- **
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
- **SNES
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
766
|
+
Two cross-cutting notes apply broadly; the rest is platform-specific and lives in
|
|
767
|
+
each platform's docs (read them for the system you're on — see below).
|
|
768
|
+
|
|
769
|
+
- **C compilers run a pre-flight lint before the real compile.** When a build
|
|
770
|
+
fails (or even when it succeeds with warnings), read the structured `issues[]`
|
|
771
|
+
— entries with `stage:"lint"` name the exact file:line and carry a `ref:` into
|
|
772
|
+
the relevant GOTCHAS section. Pass `lint:"strict"` to FAIL the build on any
|
|
773
|
+
lint hit (default is advisory). Don't trust the raw compiler `log` line numbers
|
|
774
|
+
— they're often off-by-one; the lint line is the right one.
|
|
775
|
+
- **All SDCC platforms (GB, GBC, SMS, GG, MSX, ColecoVision) are C89.** No inline
|
|
776
|
+
`for (int i = …)`, no mid-block declarations, no compound literals/designated
|
|
777
|
+
initializers — hoist every declaration to the top of its block. The lint catches
|
|
778
|
+
these; the canonical reference (plus the WRAM-layout traps that masquerade as
|
|
779
|
+
"miscompiles") is [`src/platforms/gb/lib/c/SDCC_GOTCHAS.md`](src/platforms/gb/lib/c/SDCC_GOTCHAS.md).
|
|
780
|
+
|
|
781
|
+
**Platform-specific toolchain traps live in each platform's
|
|
782
|
+
`MENTAL_MODEL.md` / `TROUBLESHOOTING.md` (read via
|
|
783
|
+
`platform({op:'doc', platform, name:'mental_model'|'troubleshooting'})`) — read
|
|
784
|
+
them for YOUR platform before you build.** By symptom:
|
|
785
|
+
|
|
786
|
+
- **SNES asm `ok:false` with empty/cryptic `issues[]`** → asar silent-fail idioms
|
|
787
|
+
(`$ - label` size expr, `STA SYMBOL+N` on a `=`-constant, bank-border crossed) +
|
|
788
|
+
**CHR/tilemap VRAM overlap** (garbage BG tiles) → snes `TROUBLESHOOTING`.
|
|
789
|
+
- **SNES has no sound** → audio is a SEPARATE SPC700 build (`platform:"spc700"` →
|
|
790
|
+
`.incbin` → $2140-$2143 handshake; `encodeAudio({target:'brr'})` for samples) →
|
|
791
|
+
snes `MENTAL_MODEL` "Sound" + `TROUBLESHOOTING`.
|
|
792
|
+
- **Hand-asm clobbers the C runtime / a ZP var isn't where you put it** → cc65
|
|
793
|
+
zero-page starts at **$02** ($00-$01 reserved); first `.res 1` = $02. All cc65
|
|
794
|
+
platforms (NES, C64, Atari, Lynx) → nes/c64 `MENTAL_MODEL`.
|
|
795
|
+
- **Busy BG art renders garbage / `encodeArt({stage:'tilemap'})` warns** → NES
|
|
796
|
+
256-unique-tiles-per-pattern-table cap → nes `MENTAL_MODEL`.
|
|
797
|
+
- **GB/GBC: white screen, sprites garbage, VRAM stays empty, or "works until a
|
|
798
|
+
button is held then corrupts"** → header/CGB-flag auto-fix, `shadow_oam`
|
|
799
|
+
page-alignment, `memcpy_vram` (raw VRAM stores get elided), OAM-DMA-from-HRAM,
|
|
800
|
+
crt0 BSS-zeroing, and `gb_vram` (NOT `video_ram`) → gb `MENTAL_MODEL` footguns +
|
|
801
|
+
`SDCC_GOTCHAS`.
|
|
802
|
+
- **SMS/GG: sprites past slot N vanish / text cut off / sprites invisible** →
|
|
803
|
+
8-sprites-per-scanline limit, SAT `$D0` terminator, R6 sprite-tile-base
|
|
804
|
+
($2000 vs $0000), GG OAM hardware-vs-visible coords → sms/gg `MENTAL_MODEL`.
|
|
805
|
+
|
|
806
|
+
Turnkey NES/GB/GBC projects (`scaffold({op:'project'})`) copy every runtime file
|
|
807
|
+
the template needs (`*_runtime.{h,c}`, `gb_hardware.h`, crt0, linker cfg) into the
|
|
808
|
+
project dir and auto-fix the cart header at build — iterate the whole dir with
|
|
809
|
+
`build({output:'run', path, platform})`. Details in those platforms' MENTAL_MODELs.
|
|
1135
810
|
|
|
1136
811
|
## Session continuity — REUSE YOUR SESSION
|
|
1137
812
|
|