romdevtools 0.29.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/AGENTS.md +14 -5
  2. package/CHANGELOG.md +114 -12
  3. package/README.md +2 -1
  4. package/examples/gb/templates/tile_engine.c +1 -1
  5. package/examples/gbc/templates/tile_engine.c +1 -1
  6. package/examples/genesis/templates/two_plane_parallax.c +4 -4
  7. package/examples/nes/templates/tile_engine.c +1 -1
  8. package/package.json +14 -12
  9. package/src/analysis/analyze.js +263 -0
  10. package/src/analysis/decompile.js +108 -0
  11. package/src/analysis/decompiler/sleigh/6502.cspec +34 -0
  12. package/src/analysis/decompiler/sleigh/6502.ldefs +33 -0
  13. package/src/analysis/decompiler/sleigh/6502.pspec +16 -0
  14. package/src/analysis/decompiler/sleigh/6502.sla +3414 -0
  15. package/src/analysis/decompiler/sleigh/65816-snes.pspec +249 -0
  16. package/src/analysis/decompiler/sleigh/65816.cspec +17 -0
  17. package/src/analysis/decompiler/sleigh/65816.ldefs +15 -0
  18. package/src/analysis/decompiler/sleigh/65816.sla +23670 -0
  19. package/src/analysis/decompiler/sleigh/65c02.sla +4683 -0
  20. package/src/analysis/decompiler/sleigh/68000.cspec +67 -0
  21. package/src/analysis/decompiler/sleigh/68000.ldefs +68 -0
  22. package/src/analysis/decompiler/sleigh/68000.pspec +9 -0
  23. package/src/analysis/decompiler/sleigh/68000_register.cspec +118 -0
  24. package/src/analysis/decompiler/sleigh/68040.sla +81693 -0
  25. package/src/analysis/decompiler/sleigh/ARM.ldefs +377 -0
  26. package/src/analysis/decompiler/sleigh/ARM4t_le.sla +53758 -0
  27. package/src/analysis/decompiler/sleigh/ARM_v45.cspec +209 -0
  28. package/src/analysis/decompiler/sleigh/ARM_v45.pspec +40 -0
  29. package/src/analysis/decompiler/sleigh/ARMt_v45.pspec +43 -0
  30. package/src/analysis/decompiler/sleigh/HuC6280.cspec +67 -0
  31. package/src/analysis/decompiler/sleigh/HuC6280.ldefs +18 -0
  32. package/src/analysis/decompiler/sleigh/HuC6280.pspec +150 -0
  33. package/src/analysis/decompiler/sleigh/HuC6280.sla +7524 -0
  34. package/src/analysis/decompiler/sleigh/sm83.cspec +70 -0
  35. package/src/analysis/decompiler/sleigh/sm83.ldefs +29 -0
  36. package/src/analysis/decompiler/sleigh/sm83.pspec +19 -0
  37. package/src/analysis/decompiler/sleigh/sm83.sla +7695 -0
  38. package/src/analysis/decompiler/sleigh/z80.cspec +122 -0
  39. package/src/analysis/decompiler/sleigh/z80.ldefs +57 -0
  40. package/src/analysis/decompiler/sleigh/z80.pspec +43 -0
  41. package/src/analysis/decompiler/sleigh/z80.sla +20518 -0
  42. package/src/analysis/decompiler/wasm/decompile.js +2 -0
  43. package/src/analysis/decompiler/wasm/decompile.wasm +0 -0
  44. package/src/analysis/rizin.js +129 -0
  45. package/src/analysis/wasm/rizin.js +6032 -0
  46. package/src/analysis/wasm/rizin.wasm +0 -0
  47. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  48. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  49. package/src/cores/wasm/fceumm_libretro.js +1 -1
  50. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  51. package/src/cores/wasm/gambatte_libretro.js +1 -1
  52. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  53. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  54. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  55. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  56. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  57. package/src/cores/wasm/handy_libretro.js +1 -1
  58. package/src/cores/wasm/handy_libretro.wasm +0 -0
  59. package/src/cores/wasm/mgba_libretro.js +1 -1
  60. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  61. package/src/cores/wasm/prosystem_libretro.js +1 -1
  62. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  63. package/src/cores/wasm/snes9x_libretro.js +1 -1
  64. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  65. package/src/cores/wasm/stella2014_libretro.js +1 -1
  66. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  67. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  68. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  69. package/src/host/LibretroHost.js +25 -7
  70. package/src/http/routes.js +1 -1
  71. package/src/http/skill-doc.js +1 -1
  72. package/src/mcp/tools/cart-parts.js +5 -2
  73. package/src/mcp/tools/disasm.js +32 -5
  74. package/src/mcp/tools/font-map.js +3 -3
  75. package/src/mcp/tools/index.js +2 -2
  76. package/src/mcp/tools/memory.js +131 -24
  77. package/src/mcp/tools/project.js +1 -1
  78. package/src/mcp/tools/record.js +6 -7
  79. package/src/mcp/tools/reinject.js +1 -1
  80. package/src/mcp/tools/run-until.js +8 -2
  81. package/src/mcp/tools/symbols.js +10 -4
  82. package/src/mcp/tools/trace-vram-source.js +1 -1
  83. package/src/mcp/tools/watch-memory.js +50 -8
  84. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +80 -6
  85. package/src/platforms/atari2600/MENTAL_MODEL.md +6 -0
  86. package/src/platforms/atari7800/MENTAL_MODEL.md +6 -0
  87. package/src/platforms/c64/MENTAL_MODEL.md +6 -0
  88. package/src/platforms/gb/MENTAL_MODEL.md +6 -0
  89. package/src/platforms/gb/lib/c/README.md +1 -1
  90. package/src/platforms/gba/MENTAL_MODEL.md +7 -1
  91. package/src/platforms/gbc/MENTAL_MODEL.md +6 -0
  92. package/src/platforms/gbc/lib/c/README.md +1 -1
  93. package/src/platforms/genesis/MENTAL_MODEL.md +8 -2
  94. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  95. package/src/platforms/genesis/lib/wram.s +1 -1
  96. package/src/platforms/gg/MENTAL_MODEL.md +6 -0
  97. package/src/platforms/lynx/MENTAL_MODEL.md +6 -0
  98. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  99. package/src/platforms/nes/MENTAL_MODEL.md +6 -0
  100. package/src/platforms/pce/MENTAL_MODEL.md +6 -0
  101. package/src/platforms/sms/MENTAL_MODEL.md +6 -0
  102. package/src/platforms/snes/MENTAL_MODEL.md +10 -4
  103. package/src/toolchains/_worker/wasm-worker.js +5 -0
@@ -18,6 +18,10 @@ read that platform's `platform({op:'doc', platform, name:'mental_model'})`.
18
18
  loose name (fuzzy) before assuming it's absent. Cheats are a STARTING point,
19
19
  not the whole job — combine with disassembly below.
20
20
  3. `symbols({op:'map', platform})` / the platform MENTAL_MODEL for the layout.
21
+ 4. `symbols({op:'analyze', romPath})` — for an unknown ROM with no cheats and no
22
+ debug file, this carves the structure (functions + strings + entrypoints) in
23
+ one call. The static map you hang everything else onto (see §5f for the full
24
+ Rizin/Ghidra analysis loop — cfg, xrefs, decompile).
21
25
 
22
26
  The cheat DB is bundled (`romdev_game_codes`). Do **not** scan the user's disk for
23
27
  `.cht` files — if it's not in the bundled DB, treat it as absent and RE it.
@@ -65,6 +69,14 @@ thousands of bytes and you'll drown).
65
69
 
66
70
  This is the Cheat-Engine/RetroArch loop. It is THE bread-and-butter primitive.
67
71
 
72
+ **Don't know the value? (lives/timer/ammo not on the HUD)** Use the
73
+ unknown-initial-value hunt: `memory({op:'searchUnknown', region, size})` seeds
74
+ the WHOLE region with no value, then narrow across events with
75
+ `searchNext({compare:'dec'|'inc'|'unchanged'|'changed'|'gt'|'lt'})` — e.g.
76
+ `searchUnknown` → lose a life → `searchNext({compare:'dec'})` → repeat until 1–2
77
+ remain. `op:'search'` needs a value; `op:'searchUnknown'` is for when you can't
78
+ see the number.
79
+
68
80
  **Stored ≠ displayed.** When a correct-looking seed returns 0, the byte usually isn't the
69
81
  raw number: seed `as:'bcd'` for packed-BCD scores (2 decimal digits per byte — very common
70
82
  on NES), or `as:'digits'` for one byte per ON-SCREEN digit at any constant tile base (HUD
@@ -78,7 +90,13 @@ not for value hunting. `memory({op:'diff'})` defaults to a **clustered summary**
78
90
  stride) so it won't flood you — a reported stride (e.g. "islands at 0x80") is
79
91
  usually a struct/entity array, each island one record. Small clusters (≤8 bytes) carry
80
92
  `before`/`after` hex inline, and `minDelta:N` drops |after−before| < N so RNG/counter
81
- wiggle disappears from the report.
93
+ wiggle disappears from the report. For the locate-value-via-diff case, predicate
94
+ filters cut a 500-byte death-window diff to the ~3 rows you want in one call:
95
+ `changeDir:'dec'|'inc'` (direction), `deltaEq:N` (signed exact delta — `deltaEq:-1`
96
+ = "lost one life"), and `beforeMin/Max` + `afterMin/Max` (value-range gates, e.g.
97
+ `beforeMax:9` = a small counter, not a coordinate). `outputPath` writes the full
98
+ diff JSON to your path regardless of size (`echo:false` returns just the
99
+ counts+path so a big diff never streams through context).
82
100
 
83
101
  **"Which byte does this INPUT drive?" → `memory({op:'diffRuns'})`** — runs the same start
84
102
  state twice (savestate restore in between) under two different held inputs (`portsA` vs
@@ -96,7 +114,7 @@ The #1 trap: visible names/labels are often **pre-rendered tile GRAPHICS**, not
96
114
  font-rendered from an ASCII string. Patching the ASCII string then does nothing.
97
115
 
98
116
  1. `text({op:'learn'})` on the on-screen text — it infers the game's char→tile-ID map
99
- (games use their own encoding: Excitebike A=$0A, Mario ASCII-offset, FF sparse). Two
117
+ (games use their own encoding: e.g. one NES racer maps A=$0A, another uses an ASCII offset, a third a sparse table). Two
100
118
  modes: ROM mode `knownStrings:[{text, offset}]` when you found the bytes; **LIVE mode
101
119
  `fromScreen:[{text, row, col}]`** reads the tile IDs straight off the live BG map at a
102
120
  tile position (`background({view:'map'})` shows where the text sits) — this breaks the
@@ -136,8 +154,11 @@ a string — find a terminator / font map before treating the bytes as values.
136
154
  platforms (Genesis/Mega Drive, GB/GBC, SMS/GG, PCE, Lynx) the **file offset IS
137
155
  the CPU ROM address** — `memory({op:'readCart', offset:0x21FF00})` answers "does the
138
156
  running ROM have my bytes at 0x21FF00?" in one call. (NES/SNES: bytes are
139
- correct but mapper-banked — `mapped:true` in the response; map a CPU PC→offset
140
- via `breakpoint({on:'write'})`'s prgOffset/bank.)
157
+ correct but mapper-banked — `mapped:true` in the response.) For a BANKED CPU
158
+ address, read it directly: `memory({op:'readCart', cpuAddress:0x8654, bank:6})`
159
+ maps the bank→PRG offset for you (NES/SNES) — the inverse of the breakpoint
160
+ result's bank/prgOffset, so you stop hand-computing `cpuAddr−0x8000+bank*0x4000`.
161
+ A NES `$C000+` address resolves to the fixed top bank automatically.
141
162
 
142
163
  When a write "doesn't show up", check the ROM here before assuming the patch
143
164
  failed — it's usually live and the bug is elsewhere (wrong source, see §2/§5).
@@ -170,6 +191,18 @@ can pass `{startAddress, bank}` to `disasm({target:'rom'})`. The lighter
170
191
  and returns a frame-boundary PC — a lead, not a guarantee under interrupts; use it for the
171
192
  value timeline or when you just want the change history, and cross-check the value trace.
172
193
 
194
+ **Stop on the MEANINGFUL write, not the churn.** `breakpoint({on:'write'})` runs
195
+ to END OF FRAME and reports the LAST matching write that frame (with `hits` =
196
+ the count of all matching writes) — so a frequent **restoring** write (a pointer-
197
+ arithmetic `inc`/`dec` that touches the byte every frame, a re-arm) can mask the
198
+ write you actually want. Filter to the real change with `condition` (all 14
199
+ platforms): `condition:'decrease'` / `'increase'` stop only when the stored byte
200
+ actually went down/up (a real lives−1, not a restore), and `condition:'equals',
201
+ conditionValue:N` stops on the byte becoming N (e.g. a $00→$01 respawn re-arm).
202
+ The hit then reports `oldValueByte`→`valueByte` so you see the exact transition.
203
+ This is the difference between pinning a genuine decrement instantly and chasing
204
+ net-zero restoring churn.
205
+
173
206
  ---
174
207
 
175
208
  ## 5b. To READ a register at an instruction — execution breakpoints (all 14)
@@ -281,6 +314,40 @@ Breakpoints are great once you KNOW the address. To FIND it:
281
314
 
282
315
  ---
283
316
 
317
+ ## 5f. Carve the program STRUCTURE before you label it — the RE engine (all 14)
318
+
319
+ The watch/breakpoint tools above find routines *dynamically* (run the game, see
320
+ what touches an address). The **Rizin/Ghidra analysis engine** carves the program
321
+ *statically* — the map you label the dynamic findings onto. All 14 platforms.
322
+
323
+ - **`symbols({op:'analyze', romPath})`** — one call, the whole shape: auto-detected
324
+ functions + strings + entrypoints. Start here on an unknown ROM.
325
+ - **`disasm({target:'functions', path})`** — the function list with sizes,
326
+ basic-block counts, and caller/callee counts. The most-called functions are
327
+ usually the engine primitives (read-joypad, draw-tile, RNG).
328
+ - **`disasm({target:'cfg', path, address})`** — the basic-block control-flow graph
329
+ of one function (nodes + typed branch edges). "Is this a loop? where does it
330
+ bail?" without reading the whole disassembly.
331
+ - **`disasm({target:'xrefs', path, address})`** — every cross-reference TO an
332
+ address, following the analysis graph. DEEPER than `target:'references'` (a flat
333
+ operand scan): once a function pass has run, `xrefs` resolves calls the flat
334
+ scan misses. Use it to answer "what calls this routine / reads this table?"
335
+ - **`disasm({target:'decompile', path, address})`** — Ghidra **C-like pseudocode**
336
+ for a function. Read it to UNDERSTAND a routine fast; it is NOT the edit path
337
+ (use `target:'project'`, §7b, to change and rebuild). Quality tracks the CPU —
338
+ see the `qualityNote` it returns: excellent on ARM (GBA) / 68000 (Genesis),
339
+ good on SM83 (GB) / Z80 (SMS/GG/MSX), medium on 65816 (SNES) / HuC6280 (PCE),
340
+ rough on the 6502 family (carry-flag idioms and 16-bit-math-on-8-bit decompile
341
+ to noise — read the disassembly there, or let an LLM fold the pseudocode).
342
+
343
+ **The loop:** `symbols({op:'analyze'})` or `disasm({target:'functions'})` to carve →
344
+ `disasm({target:'cfg'/'xrefs'/'decompile'})` to understand a candidate → then the
345
+ dynamic tools (memory search, `breakpoint({on:'write'})`, `watch`) to CONFIRM and
346
+ label which carved function owns the value you care about. Static narrows the
347
+ search space; dynamic proves it.
348
+
349
+ ---
350
+
284
351
  ## 6. Driving menus (the real wall-clock sink)
285
352
 
286
353
  Use `input({op:'navigate', steps:[{button, maxWaitFrames}]})` — it advances on **screen
@@ -368,7 +435,11 @@ watch the screen react — cheaper than shipping a wrong ROM patch.
368
435
  ## 7b. Whole-ROM rebuildable disassembly — `disasm({target:'project'})`
369
436
 
370
437
  For a STRUCTURAL hack (new logic, not a byte poke), turn the whole ROM into a
371
- re-buildable project in one call: `disasm({target:'project', path, outputDir})`. It splits
438
+ re-buildable project in one call: `disasm({target:'project', path, outputDir})`.
439
+ (To UNDERSTAND a routine before you edit it, read its `disasm({target:'decompile'})`
440
+ pseudocode or `disasm({target:'cfg'})` graph first — §5f. `project` is the *edit*
441
+ path; decompile is the *understanding* path. They pair: read the C, edit the asm.)
442
+ It splits
372
443
  the ROM into regions (per-bank on EVERY banked format: 16KB banks for NES/GB/SMS-GG/MSX/
373
444
  7800-SuperGame, 32KB for SNES LoROM, 4KB for banked 2600, 8KB pages for >32KB HuCards;
374
445
  one flat region for Genesis/C64/Lynx/GBA and small carts), disassembles each through the CPU's
@@ -467,7 +538,10 @@ For sprite/tile edits (not text), don't hand-roll the tile-format math:
467
538
  | Find / encode a font-rendered string | `text({op:'find'})` → `text({op:'encode'})` |
468
539
  | Assemble asm → raw patch bytes | `assembleSnippet({cpu, origin, code})` |
469
540
  | Mapper-aware diff of two ROMs | `romPatch({op:'diff'})` (CPU addrs, CHR `tile:N`) |
470
- | Who references this address (static) | `disasm({target:'references'})` (direct modes only) |
541
+ | Who references this address (static) | `disasm({target:'references'})` (flat scan) / `disasm({target:'xrefs'})` (deeper, graph-following) |
542
+ | Map an unknown ROM's structure | `symbols({op:'analyze'})` / `disasm({target:'functions'})` (functions + strings + entrypoints) |
543
+ | Graph one function's control flow | `disasm({target:'cfg', address})` (basic blocks + branch edges) |
544
+ | Read a routine as C pseudocode | `disasm({target:'decompile', address})` (Ghidra; all 14, quality per CPU) |
471
545
  | Split / rebuild a ROM into parts | `cart({op:'extract'})` / `cart({op:'wrap'})` |
472
546
  | Swap a sprite/tile (PNG round-trip) | `tiles({op:'png'})` → edit → `romPatch({op:'spliceCHR'})` |
473
547
  | Lift art from another game's ROM | `importArt({from:'rom'})` |
@@ -233,3 +233,9 @@ Memory regions for **`memory({op:'read'})`**:
233
233
  audio voices (`AUDC/AUDF/AUDV` at `$15-$1A`), not a standard PSG/FM chip,
234
234
  so there's no `audioDebug` decode — read the audio state directly out of
235
235
  the `a26_tia_regs` snapshot instead.
236
+
237
+ ## Reverse-engineering & decompilation
238
+
239
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
240
+
241
+ **Decompiler quality on 6502: ROUGH.** Carry-flag idioms and 16-bit math on an 8-bit CPU decompile to noise that only reads cleanly once an LLM folds it — on this CPU the disassembly is often more honest than the pseudocode. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -379,3 +379,9 @@ Memory regions for **`memory({op:'read'})`**:
379
379
  chip carried over from the 2600 (`$15-$1A`), not a decodable PSG/FM chip,
380
380
  so there's no `audioDebug` decode. (Some carts add a POKEY, but it's
381
381
  non-standard — don't assume it's present.)
382
+
383
+ ## Reverse-engineering & decompilation
384
+
385
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
386
+
387
+ **Decompiler quality on 6502: ROUGH.** Carry-flag idioms and 16-bit math on an 8-bit CPU decompile to noise that only reads cleanly once an LLM folds it — on this CPU the disassembly is often more honest than the pseudocode. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -334,3 +334,9 @@ moving further is a software char-cell shift.
334
334
 
335
335
  Track `camX` in pixels: `fine = camX & 7` → `$D016`; `coarseCol = camX >> 3`
336
336
  indexes your world map for which columns are on screen.
337
+
338
+ ## Reverse-engineering & decompilation
339
+
340
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
341
+
342
+ **Decompiler quality on 6502: ROUGH.** Carry-flag idioms and 16-bit math on an 8-bit CPU decompile to noise that only reads cleanly once an LLM folds it — on this CPU the disassembly is often more honest than the pseudocode. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -359,3 +359,9 @@ The `platformer` example is single-screen. To make it a side-scroller:
359
359
  Pattern: keep a `world_map[col][row]` array, a `camX` in pixels, convert
360
360
  actor world-X → screen-X as `worldX - camX`, and only ever touch the one
361
361
  column entering the screen per 8-px step.
362
+
363
+ ## Reverse-engineering & decompilation
364
+
365
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
366
+
367
+ **Decompiler quality on SM83: GOOD.** A dedicated SLEIGH plugin gives clean block-level pseudocode. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -65,7 +65,7 @@ Templates ship in `examples/{gb,gbc}/templates/`:
65
65
  |---|---|
66
66
  | `default` | Minimal palette-cycle hello-world. Use when you're not sure what to build yet. |
67
67
  | `hello_sprite` | LCD init + 16-byte tile upload + 4-color OBJ palette + sprite slot 0 + d-pad movement. ~80 lines, tested end-to-end. |
68
- | `tile_engine` | 20×18 BG map render from a `room[]` array + collision + multi-room transitions via doors. ~200 lines. Covers the Adventure / Zelda-1 / Sokoban shape. |
68
+ | `tile_engine` | 20×18 BG map render from a `room[]` array + collision + multi-room transitions via doors. ~200 lines. Covers the top-down dungeon-crawler / room-puzzle shape. |
69
69
 
70
70
  ## SDCC 4.4.0 quirks
71
71
 
@@ -55,7 +55,7 @@ mixing ARM + Thumb in the same binary. libgba is built Thumb-interwork.
55
55
  ```
56
56
  240×160 pixels visible, 6 BG modes (0-5)
57
57
 
58
- Mode 0: 4 tile BGs, scrolling. The classic 2D Mario-style mode.
58
+ Mode 0: 4 tile BGs, scrolling. The classic 2D platformer mode.
59
59
  Mode 1: 2 tile BGs + 1 affine BG (rotation/scale).
60
60
  Mode 2: 2 affine BGs only. Mode 7-style perspective.
61
61
  Mode 3: 240×160 BGR555 framebuffer at $06000000. 16-bit per pixel.
@@ -268,3 +268,9 @@ entering view into the map's screen-blocks as the camera advances. A fixed HUD
268
268
  goes on its own BG layer left unscrolled (or via an HBlank IRQ that resets the
269
269
  offset for the HUD scanlines). Track camX in pixels; actor screen-X = worldX -
270
270
  camX.
271
+
272
+ ## Reverse-engineering & decompilation
273
+
274
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
275
+
276
+ **Decompiler quality on ARM7TDMI: EXCELLENT.** Most GBA code was compiled C, so the decompiler often recovers something close to the original source — lean on it. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -222,3 +222,9 @@ on CGB, its BG attribute byte in VRAM bank 1) each time the camera crosses an
222
222
  8-px boundary. Use the Window (LCDC bit 5) for a fixed HUD. CGB adds nothing
223
223
  that changes the scroll mechanism — just remember the per-tile attribute in
224
224
  bank 1 when you stream columns. See the GB MENTAL_MODEL for the full pattern.
225
+
226
+ ## Reverse-engineering & decompilation
227
+
228
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
229
+
230
+ **Decompiler quality on SM83: GOOD.** A dedicated SLEIGH plugin gives clean block-level pseudocode. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -65,7 +65,7 @@ Templates ship in `examples/{gb,gbc}/templates/`:
65
65
  |---|---|
66
66
  | `default` | Minimal palette-cycle hello-world. Use when you're not sure what to build yet. |
67
67
  | `hello_sprite` | LCD init + 16-byte tile upload + 4-color OBJ palette + sprite slot 0 + d-pad movement. ~80 lines, tested end-to-end. |
68
- | `tile_engine` | 20×18 BG map render from a `room[]` array + collision + multi-room transitions via doors. ~200 lines. Covers the Adventure / Zelda-1 / Sokoban shape. |
68
+ | `tile_engine` | 20×18 BG map render from a `room[]` array + collision + multi-room transitions via doors. ~200 lines. Covers the top-down dungeon-crawler / room-puzzle shape. |
69
69
 
70
70
  ## SDCC 4.4.0 quirks
71
71
 
@@ -219,7 +219,7 @@ size and treat your logical world coords separately.
219
219
  | 32×64 | 256×512 | vertical scroller |
220
220
  | 64×64 | 512×512 | uses the most VRAM for name tables |
221
221
 
222
- ### How Sonic-style large maps REALLY work (wider than one plane)
222
+ ### How large scrolling maps REALLY work (wider than one plane)
223
223
 
224
224
  You do NOT make the plane "as wide as the level," and you do NOT redraw
225
225
  the plane. The 64-cell hardware plane is a **circular buffer**: as the
@@ -242,7 +242,7 @@ if (newTileCol != lastTileCol) {
242
242
 
243
243
  That's ~28 tile writes per 8 px of travel, not a 1792-cell plane redraw.
244
244
  The `platformer` example scrolls within one plane (no
245
- streaming); add the column-stream above to go wider. (Real Sonic also
245
+ streaming); add the column-stream above to go wider. (Real large-scroller engines also
246
246
  splits the screen with H-blank raster effects for independent strips —
247
247
  that's an IRQ/raster topic, see the `asm` template.)
248
248
 
@@ -518,3 +518,9 @@ When you call `build({output:'rom'})`:
518
518
  image from the ELF → `.bin` Genesis ROM.
519
519
 
520
520
  Loadable via genesis_plus_gx (`loadMedia`).
521
+
522
+ ## Reverse-engineering & decompilation
523
+
524
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
525
+
526
+ **Decompiler quality on 68000: EXCELLENT.** An orthogonal ISA with real stack frames decompiles close to readable C — lean on it. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -260,5 +260,5 @@ Diagnose it without guessing (no core rebuild):
260
260
 
261
261
  For a world WIDER than one 512-px plane, don't make the plane bigger and
262
262
  don't redraw it — stream ONE offscreen column per 8-px camera step
263
- (circular-buffer the 64-cell plane). See MENTAL_MODEL.md "How Sonic-style
264
- large maps REALLY work".
263
+ (circular-buffer the 64-cell plane). See MENTAL_MODEL.md "How large scrolling
264
+ maps REALLY work".
@@ -35,7 +35,7 @@ pad1_released equ WRAM_BASE + $0006 ; 2 bytes
35
35
  game_state equ WRAM_BASE + $0010 ; 1 byte: 0=title, 1=play, 2=gameover
36
36
  state_timer equ WRAM_BASE + $0012 ; 2 bytes: frames in current state
37
37
 
38
- ; Player / score (Tetris-shaped example).
38
+ ; Player / score (falling-block puzzle example).
39
39
  score equ WRAM_BASE + $0020 ; 4 bytes (BCD or binary)
40
40
  lines_cleared equ WRAM_BASE + $0024 ; 2 bytes
41
41
  level equ WRAM_BASE + $0026 ; 1 byte
@@ -197,3 +197,9 @@ Everything else (Z80, VDP control protocol, tile format, sprite SAT
197
197
  layout, joypad polling, BG name table at $3800) is identical to SMS.
198
198
  You can use sms_hw.h notes + helpers as a reference; the GG runtime
199
199
  files in lib/c/ are direct ports.
200
+
201
+ ## Reverse-engineering & decompilation
202
+
203
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
204
+
205
+ **Decompiler quality on Z80: GOOD.** Register-rich hand asm decompiles cleanly at the block level. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -272,3 +272,9 @@ into `.lnx`. handy accepts both.
272
272
  - One controller (handheld) — no port 2 fallback patterns.
273
273
  - 64 KB total RAM, mapped ROM. C64 has 64 KB RAM but most is shadowed
274
274
  by ROM by default.
275
+
276
+ ## Reverse-engineering & decompilation
277
+
278
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
279
+
280
+ **Decompiler quality on 65C02: ROUGH.** Carry-flag idioms and 16-bit math on an 8-bit CPU decompile to noise that only reads cleanly once an LLM folds it — on this CPU the disassembly is often more honest than the pseudocode. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -150,3 +150,9 @@ the emulator core** to expose the extra register/VRAM regions, then wiring a
150
150
  decoder. To add deep introspection to ColecoVision (or any thin platform),
151
151
  follow the existing core-patch pattern used for snes9x / gpgx / fceumm / vice
152
152
  under **`scripts/patches/`**.
153
+
154
+ ## Reverse-engineering & decompilation
155
+
156
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
157
+
158
+ **Decompiler quality on Z80: GOOD.** Register-rich hand asm decompiles cleanly at the block level. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -415,6 +415,12 @@ multi-bank `.cfg` referenced via `linkerConfigPath`. Either way: feed
415
415
  is the RE workhorse loop: `disasm({target:'project'})` → edit the `.asm` →
416
416
  rebuild → `diffRoms` to confirm your patch landed.
417
417
 
418
+ ## Reverse-engineering & decompilation
419
+
420
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
421
+
422
+ **Decompiler quality on 6502: ROUGH.** Carry-flag idioms and 16-bit math on an 8-bit CPU decompile to noise that only reads cleanly once an LLM folds it — on this CPU the disassembly is often more honest than the pseudocode. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
423
+
418
424
  ## When to drop to asm
419
425
 
420
426
  Game-loop in C is fine for ~80% of homebrew. Drop to asm when:
@@ -110,3 +110,9 @@ screen. Keep at least one (2+ byte) global. See TROUBLESHOOTING.md.
110
110
  PCE asm toolchain IS cc65/ca65 — a **one-call byte-identical `build()`
111
111
  rebuild** via `rebuild.json` (flat and banked; a 512-byte copier header is
112
112
  split out and re-emitted as a HEADER segment).
113
+
114
+ ## Reverse-engineering & decompilation
115
+
116
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
117
+
118
+ **Decompiler quality on HuC6280: MEDIUM.** The Ghidra HuC6280 SLEIGH covers all custom opcodes + MPR banking; pseudocode is usable. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -340,3 +340,9 @@ The `platformer` example is single-screen. To make it a side-scroller:
340
340
 
341
341
  Track `camX` in pixels; actor screen-X = `worldX - camX`. (Game Gear is the
342
342
  same VDP — only the visible window differs.)
343
+
344
+ ## Reverse-engineering & decompilation
345
+
346
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
347
+
348
+ **Decompiler quality on Z80: GOOD.** Register-rich hand asm decompiles cleanly at the block level. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -47,13 +47,13 @@ The SNES has 8 BG modes selected via PPU register $2105 (BGMODE):
47
47
 
48
48
  ```
49
49
  0 4 BGs × 4 colors — text-mode look
50
- 1 3 BGs (16+16+4 col) — default for most games (Super Mario World)
51
- 2 2 BGs × 16 col + tilemap offset-per-tile (Yoshi's Island)
52
- 3 1 BG × 256 col + 1 BG × 16 col (Donkey Kong Country)
50
+ 1 3 BGs (16+16+4 col) — default for most games (typical 2D platformer)
51
+ 2 2 BGs × 16 col + tilemap offset-per-tile (a pre-rendered-sprite platformer)
52
+ 3 1 BG × 256 col + 1 BG × 16 col (pre-rendered-sprite platformer)
53
53
  4 1 BG × 256 col + 1 BG × 4 col with offset-per-tile
54
54
  5 2 BGs hi-res (512 px wide, half-height)
55
55
  6 hi-res mosaic
56
- 7 1 BG with affine transform (Mario Kart, F-Zero)
56
+ 7 1 BG with affine transform (mode-7 racers)
57
57
  ```
58
58
 
59
59
  PVSnesLib's default is `BG_MODE1` (`setMode(BG_MODE1, 0)`) — three
@@ -313,3 +313,9 @@ and parallax is nearly free.
313
313
  scanlines.
314
314
 
315
315
  Track `camX` in pixels; actor screen-X = `worldX - camX`.
316
+
317
+ ## Reverse-engineering & decompilation
318
+
319
+ The Rizin/Ghidra analysis engine works here like everywhere: `disasm({target:'functions'})` to carve the program, `disasm({target:'cfg'|'xrefs'})` to trace it, `symbols({op:'analyze'})` for a one-shot structural map.
320
+
321
+ **Decompiler quality on 65816: MEDIUM.** The M/X register-width flags make instruction meaning context-dependent, but the Ghidra 65816 SLEIGH tracks them, so pseudocode is usable. `disasm({target:'decompile', address})` returns C-like pseudocode (the `qualityNote` field restates this). Read it to UNDERSTAND a routine; use `disasm({target:'project'})` to actually edit + rebuild. See the cross-platform ROM-hacking playbook §5f for the full loop.
@@ -109,6 +109,11 @@ async function runJob(job) {
109
109
  return buf[idx++];
110
110
  };
111
111
  }
112
+ // Optional environment variables for the WASM (e.g. SLEIGHHOME for the Ghidra
113
+ // decompiler). Seed Module.ENV in preRun, before libc reads getenv().
114
+ if (job.env && typeof job.env === "object") {
115
+ moduleArgs.preRun = [(m) => { Object.assign((m.ENV || (m.ENV = {})), job.env); }];
116
+ }
112
117
 
113
118
  const mod = await factory(moduleArgs);
114
119