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.
- package/AGENTS.md +14 -5
- package/CHANGELOG.md +114 -12
- package/README.md +2 -1
- package/examples/gb/templates/tile_engine.c +1 -1
- package/examples/gbc/templates/tile_engine.c +1 -1
- package/examples/genesis/templates/two_plane_parallax.c +4 -4
- package/examples/nes/templates/tile_engine.c +1 -1
- package/package.json +14 -12
- package/src/analysis/analyze.js +263 -0
- package/src/analysis/decompile.js +108 -0
- package/src/analysis/decompiler/sleigh/6502.cspec +34 -0
- package/src/analysis/decompiler/sleigh/6502.ldefs +33 -0
- package/src/analysis/decompiler/sleigh/6502.pspec +16 -0
- package/src/analysis/decompiler/sleigh/6502.sla +3414 -0
- package/src/analysis/decompiler/sleigh/65816-snes.pspec +249 -0
- package/src/analysis/decompiler/sleigh/65816.cspec +17 -0
- package/src/analysis/decompiler/sleigh/65816.ldefs +15 -0
- package/src/analysis/decompiler/sleigh/65816.sla +23670 -0
- package/src/analysis/decompiler/sleigh/65c02.sla +4683 -0
- package/src/analysis/decompiler/sleigh/68000.cspec +67 -0
- package/src/analysis/decompiler/sleigh/68000.ldefs +68 -0
- package/src/analysis/decompiler/sleigh/68000.pspec +9 -0
- package/src/analysis/decompiler/sleigh/68000_register.cspec +118 -0
- package/src/analysis/decompiler/sleigh/68040.sla +81693 -0
- package/src/analysis/decompiler/sleigh/ARM.ldefs +377 -0
- package/src/analysis/decompiler/sleigh/ARM4t_le.sla +53758 -0
- package/src/analysis/decompiler/sleigh/ARM_v45.cspec +209 -0
- package/src/analysis/decompiler/sleigh/ARM_v45.pspec +40 -0
- package/src/analysis/decompiler/sleigh/ARMt_v45.pspec +43 -0
- package/src/analysis/decompiler/sleigh/HuC6280.cspec +67 -0
- package/src/analysis/decompiler/sleigh/HuC6280.ldefs +18 -0
- package/src/analysis/decompiler/sleigh/HuC6280.pspec +150 -0
- package/src/analysis/decompiler/sleigh/HuC6280.sla +7524 -0
- package/src/analysis/decompiler/sleigh/sm83.cspec +70 -0
- package/src/analysis/decompiler/sleigh/sm83.ldefs +29 -0
- package/src/analysis/decompiler/sleigh/sm83.pspec +19 -0
- package/src/analysis/decompiler/sleigh/sm83.sla +7695 -0
- package/src/analysis/decompiler/sleigh/z80.cspec +122 -0
- package/src/analysis/decompiler/sleigh/z80.ldefs +57 -0
- package/src/analysis/decompiler/sleigh/z80.pspec +43 -0
- package/src/analysis/decompiler/sleigh/z80.sla +20518 -0
- package/src/analysis/decompiler/wasm/decompile.js +2 -0
- package/src/analysis/decompiler/wasm/decompile.wasm +0 -0
- package/src/analysis/rizin.js +129 -0
- package/src/analysis/wasm/rizin.js +6032 -0
- package/src/analysis/wasm/rizin.wasm +0 -0
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- 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 +25 -7
- package/src/http/routes.js +1 -1
- package/src/http/skill-doc.js +1 -1
- package/src/mcp/tools/cart-parts.js +5 -2
- package/src/mcp/tools/disasm.js +32 -5
- package/src/mcp/tools/font-map.js +3 -3
- package/src/mcp/tools/index.js +2 -2
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/project.js +1 -1
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/reinject.js +1 -1
- package/src/mcp/tools/run-until.js +8 -2
- package/src/mcp/tools/symbols.js +10 -4
- package/src/mcp/tools/trace-vram-source.js +1 -1
- package/src/mcp/tools/watch-memory.js +50 -8
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +80 -6
- package/src/platforms/atari2600/MENTAL_MODEL.md +6 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +6 -0
- package/src/platforms/c64/MENTAL_MODEL.md +6 -0
- package/src/platforms/gb/MENTAL_MODEL.md +6 -0
- package/src/platforms/gb/lib/c/README.md +1 -1
- package/src/platforms/gba/MENTAL_MODEL.md +7 -1
- package/src/platforms/gbc/MENTAL_MODEL.md +6 -0
- package/src/platforms/gbc/lib/c/README.md +1 -1
- package/src/platforms/genesis/MENTAL_MODEL.md +8 -2
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/wram.s +1 -1
- package/src/platforms/gg/MENTAL_MODEL.md +6 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +6 -0
- package/src/platforms/msx/MENTAL_MODEL.md +6 -0
- package/src/platforms/nes/MENTAL_MODEL.md +6 -0
- package/src/platforms/pce/MENTAL_MODEL.md +6 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -0
- package/src/platforms/snes/MENTAL_MODEL.md +10 -4
- 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:
|
|
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
|
|
140
|
-
|
|
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})`.
|
|
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'})` (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
264
|
-
|
|
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 (
|
|
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 (
|
|
51
|
-
2 2 BGs × 16 col + tilemap offset-per-tile (
|
|
52
|
-
3 1 BG × 256 col + 1 BG × 16 col (
|
|
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 (
|
|
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
|
|