romdevtools 0.30.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 +12 -3
- package/CHANGELOG.md +70 -13
- 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 +3 -1
- 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/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/project.js +1 -1
- package/src/mcp/tools/reinject.js +1 -1
- 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 +1 -1
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +48 -3
- 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
|
Binary file
|
package/src/http/routes.js
CHANGED
|
@@ -189,7 +189,7 @@ export function buildOpenApi(registry, version) {
|
|
|
189
189
|
parameters: [{
|
|
190
190
|
name: SESSION_HEADER, in: "header", required: true,
|
|
191
191
|
schema: { type: "string" },
|
|
192
|
-
description: "REQUIRED. Per-agent session id — pick one stable, UNIQUE, task-DESCRIPTIVE string (e.g. 'nes-platformer-build', '
|
|
192
|
+
description: "REQUIRED. Per-agent session id — pick one stable, UNIQUE, task-DESCRIPTIVE string (e.g. 'nes-platformer-build', 'rpg-romhack-text') and send it on EVERY call. It's the per-session emulator key (load→step→read state lives under it) AND the label shown in the /livestream observer, so a descriptive id tells a watching human which task each call belongs to. Several agents share one server safely by each using a different id. Missing → 401.",
|
|
193
193
|
}],
|
|
194
194
|
},
|
|
195
195
|
};
|
package/src/http/skill-doc.js
CHANGED
|
@@ -39,7 +39,7 @@ export const skillPreamble = [
|
|
|
39
39
|
" • GET /openapi.json — the full machine-readable API; GET /documentation — a browsable console.",
|
|
40
40
|
"",
|
|
41
41
|
"## Sessions — IMPORTANT for stateful work (load → step → read)",
|
|
42
|
-
"**Pick ONE session id for yourself and send it as the `x-romdev-session` header on EVERY call.** Make it UNIQUE and DESCRIPTIVE of what you're doing — e.g. `nes-platformer-build`, `
|
|
42
|
+
"**Pick ONE session id for yourself and send it as the `x-romdev-session` header on EVERY call.** Make it UNIQUE and DESCRIPTIVE of what you're doing — e.g. `nes-platformer-build`, `rpg-romhack-text`, `gba-sprite-debug` (a slug, optionally with a short random suffix to stay unique). A human may be watching the live observer at /livestream, where your session id is the label for all your activity — a descriptive id tells them at a glance which agent/task each call belongs to; a bare uuid or `default` is opaque. The emulator/host is per-session: the ROM you `loadMedia` lives in YOUR session, and the next `frame`/`memory`/`cpu` call only sees it if it carries the SAME id. Do NOT send a new id each call — that's a fresh empty session every time (your loaded ROM vanishes; \"No ROM loaded\"). Several agents can share one server safely: each just sends a DIFFERENT id, so nobody clobbers another's ROM (another reason to make yours distinctive). The header is REQUIRED on every `/tool/{name}` call — omit it and you get a **401** (the server will NOT silently run you in a throwaway session). Pure file tools (romPatch/cart/encodeAudio) still need the header; just reuse your one id everywhere.",
|
|
43
43
|
"",
|
|
44
44
|
"Each tool is a domain VERB keyed by an operation axis — e.g. POST /tool/memory {\"op\":\"read\",…},",
|
|
45
45
|
"POST /tool/build {\"output\":\"rom\",…}, POST /tool/romPatch {\"op\":\"findPointer\",…}. The full per-tool",
|
|
@@ -407,10 +407,12 @@ export async function extractCartCore({ path: romPath, platform, outputDir, inli
|
|
|
407
407
|
* mirror — "horizontal" | "vertical" | "four-screen" (default horizontal)
|
|
408
408
|
* prgBanks — count of 16KB banks (default infer from prgPath size)
|
|
409
409
|
* chrBanks — count of 8KB banks (default infer; 0 for CHR-RAM)
|
|
410
|
+
* hasBattery — battery-backed SRAM (iNES flags6 bit 1); preserved on round-trip
|
|
410
411
|
*/
|
|
411
|
-
function wrapNes({ prgPath, chrPath, mapper, mirror, prgBanks, chrBanks }) {
|
|
412
|
+
function wrapNes({ prgPath, chrPath, mapper, mirror, prgBanks, chrBanks, hasBattery }) {
|
|
412
413
|
const m = mapper ?? 0;
|
|
413
414
|
const mirrorFlag = mirror === "vertical" ? 1 : mirror === "four-screen" ? 8 : 0;
|
|
415
|
+
const batteryFlag = hasBattery ? 0x02 : 0;
|
|
414
416
|
const prg = prgPath;
|
|
415
417
|
const chr = chrPath ?? null;
|
|
416
418
|
|
|
@@ -445,7 +447,7 @@ function wrapNes({ prgPath, chrPath, mapper, mirror, prgBanks, chrBanks }) {
|
|
|
445
447
|
.byte $4E, $45, $53, $1A ; "NES\\x1a"
|
|
446
448
|
.byte $${banks.toString(16).toUpperCase().padStart(2, "0")} ; PRG-ROM banks (16KB each)
|
|
447
449
|
.byte $${chrBanksNum.toString(16).toUpperCase().padStart(2, "0")} ; CHR-ROM banks (8KB each; 0 = CHR-RAM)
|
|
448
|
-
.byte ${hex2((m << 4 | mirrorFlag) & 0xFF)} ; mapper-lo + mirroring
|
|
450
|
+
.byte ${hex2((m << 4 | mirrorFlag | batteryFlag) & 0xFF)} ; mapper-lo + mirroring + battery
|
|
449
451
|
.byte ${hex2(m & 0xF0)} ; mapper-hi + NES 2.0 flags
|
|
450
452
|
.byte $00, $00, $00, $00, $00, $00, $00, $00
|
|
451
453
|
|
|
@@ -682,6 +684,7 @@ export function registerCartPartsTools(server, z) {
|
|
|
682
684
|
mirror: z.enum(["horizontal", "vertical", "four-screen"]).optional().describe("op=wrap NES: nametable mirroring."),
|
|
683
685
|
prgBanks: z.number().int().min(1).max(255).optional().describe("op=wrap NES: PRG bank count (16KB each); only 1 (NROM-128) or 2 (NROM-256) supported, default 1."),
|
|
684
686
|
chrBanks: z.number().int().min(0).max(255).optional().describe("op=wrap NES: CHR bank count (8KB each); 0 = CHR-RAM."),
|
|
687
|
+
hasBattery: z.boolean().optional().describe("op=wrap NES: set the iNES battery-backed-SRAM flag (flags6 bit 1). Pass the value from extractCart's manifest.hasBattery for a byte-exact round-trip."),
|
|
685
688
|
// wrap — SNES
|
|
686
689
|
romPath: z.string().optional().describe("op=wrap SNES/SMS/GG/Atari 2600/Atari 7800/C64: whole-ROM body for a one-shot incbin (skips the per-part paths)."),
|
|
687
690
|
copierHeaderPath: z.string().optional().describe("op=wrap SNES: path to a 512B copier header to prepend."),
|
package/src/mcp/tools/disasm.js
CHANGED
|
@@ -6,6 +6,13 @@ import { jsonContent, safeTool, writeOutput } from "../util.js";
|
|
|
6
6
|
import { parseSymbols, buildSymbolMap } from "../../toolchains/common/symbols.js";
|
|
7
7
|
import { registersForPlatform } from "../../platforms/common/registers.js";
|
|
8
8
|
import { findReferencesCore } from "./find-references.js";
|
|
9
|
+
import { analyzeCfg, analyzeXrefs, analyzeFunctions, analyzeDecompile } from "../../analysis/analyze.js";
|
|
10
|
+
|
|
11
|
+
/** cfg/xrefs/functions all operate on a ROM file. Reuse the `path` arg. */
|
|
12
|
+
function requireRomPath(args) {
|
|
13
|
+
if (!args.path) throw new Error(`disasm target='${args.target}' requires \`path\` (the ROM file).`);
|
|
14
|
+
return args.path;
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
// ── Per-platform CPU-address → file-offset mappers ────────────────
|
|
11
18
|
// Each returns { bytes, fileOffset, cpu, notes } given the full ROM
|
|
@@ -839,7 +846,7 @@ async function disassembleRomCore(args) {
|
|
|
839
846
|
}
|
|
840
847
|
}
|
|
841
848
|
// Dedup vector labels by ADDRESS. Two interrupt vectors legitimately
|
|
842
|
-
// sharing one target is valid 6502 (
|
|
849
|
+
// sharing one target is valid 6502 (e.g. one NES cart points NMI and IRQ both at
|
|
843
850
|
// $C0F6) — but da65's LABELDEF and every dasm injector reject two labels
|
|
844
851
|
// at the same address ("Label for address $XXXX already defined"). Keep
|
|
845
852
|
// the first name (vector iteration order is reset/nmi/irq) and record the
|
|
@@ -1158,9 +1165,25 @@ export function registerDisasmTools(server, z) {
|
|
|
1158
1165
|
"'references' = scan a ROM's code for operands matching a CPU `address` and classify each (call/jump/branch/" +
|
|
1159
1166
|
"read/write); also walks the vector table. Banked carts are scanned PER BANK (all of the formats above) — " +
|
|
1160
1167
|
"refs carry `prgBank` (NES) / `romBank` (everything else). LIMITATION: direct addressing only " +
|
|
1161
|
-
"(indirect/computed jumps are missed)
|
|
1168
|
+
"(indirect/computed jumps are missed).\n" +
|
|
1169
|
+
"── RE ENGINE (Rizin + Ghidra, all 14 platforms) ──\n" +
|
|
1170
|
+
"'functions' = Rizin auto-detected function list {address,size,nbbs,cc,callers,callees}; the structural map of " +
|
|
1171
|
+
"an unknown ROM. 'cfg' = basic-block control-flow graph of the function at `address` (nodes + typed edges: " +
|
|
1172
|
+
"jump/branch_true/branch_false). 'xrefs' = every cross-reference TO `address`, following Rizin's analysis graph " +
|
|
1173
|
+
"(DEEPER than 'references', which is a flat da65 operand scan — prefer 'xrefs' once you've run a function pass, " +
|
|
1174
|
+
"'references' for a quick header-less operand sweep). Typical RE loop: 'functions' to carve → 'cfg'/'xrefs' to " +
|
|
1175
|
+
"trace → then the live tools (memory search, write-breakpoints, watch copy) to LABEL what you carved.\n" +
|
|
1176
|
+
"'decompile' = Ghidra C-like PSEUDOCODE for the function at `address`, with the decompiler's own WARNINGs and a " +
|
|
1177
|
+
"`qualityNote`. ALTITUDE RULE: decompile is for UNDERSTANDING (and as a port spec when retargeting to a bigger " +
|
|
1178
|
+
"machine) — it is NOT the same-platform edit path. To CHANGE a ROM and rebuild it, use target:'project' " +
|
|
1179
|
+
"(byte-exact rebuildable asm); read the pseudocode as documentation alongside it. Per-CPU quality (calibrate, " +
|
|
1180
|
+
"don't treat low quality as a bug): ARM/GBA + M68K/Genesis = excellent (mostly-C games, real stack frames); " +
|
|
1181
|
+
"SM83/GB + Z80/SMS/GG/MSX = good; 65816/SNES + HuC6280/PCE = medium; 6502 family (NES/2600/7800/C64/Lynx) = " +
|
|
1182
|
+
"rough — carry-flag idioms and 16-bit math on an 8-bit CPU decompile to noise that only reads cleanly once an " +
|
|
1183
|
+
"LLM folds it. `address` for all four comes from target:'functions' (a CPU/virtual address; the file-offset " +
|
|
1184
|
+
"mapping is handled for you).",
|
|
1162
1185
|
{
|
|
1163
|
-
target: z.enum(["bytes", "rom", "project", "references"]).describe("bytes = raw chunk; rom = mapper-aware ROM; project = full rebuildable disasm; references =
|
|
1186
|
+
target: z.enum(["bytes", "rom", "project", "references", "cfg", "xrefs", "functions", "decompile"]).describe("bytes = raw chunk; rom = mapper-aware ROM; project = full rebuildable disasm; references = flat da65 operand-refs to an address; functions/cfg/xrefs = Rizin RE engine (function list / control-flow graph / deep graph xrefs); decompile = Ghidra C pseudocode. See the tool description for the RE loop + the decompile altitude rule + per-CPU quality (all 14 platforms)."),
|
|
1164
1187
|
// shared
|
|
1165
1188
|
path: z.string().optional().describe("target=bytes: raw binary path. target=rom/project/references: ROM file path."),
|
|
1166
1189
|
base64: z.string().optional().describe("target=bytes: base64 of the bytes (OR `path`)."),
|
|
@@ -1190,8 +1213,8 @@ export function registerDisasmTools(server, z) {
|
|
|
1190
1213
|
annotateFileOffsets: z.boolean().default(true).describe("target=rom: append `; @0xNNNN` file offset to every line (for romPatch)."),
|
|
1191
1214
|
// project
|
|
1192
1215
|
outputDir: z.string().optional().describe("target=project: directory to write the project into (one .asm per region)."),
|
|
1193
|
-
// references
|
|
1194
|
-
address: z.number().int().min(0).max(
|
|
1216
|
+
// references / cfg / xrefs
|
|
1217
|
+
address: z.number().int().min(0).max(0xFFFFFFFF).optional().describe("target=references: CPU address to find references TO. target=cfg: address inside the function to graph. target=xrefs: address to find cross-references TO. target=decompile: address of the function to decompile (use an address from target='functions')."),
|
|
1195
1218
|
maxRefsReturned: z.number().int().min(1).max(2048).default(256).describe("target=references: cap the references returned."),
|
|
1196
1219
|
},
|
|
1197
1220
|
safeTool(async (args) => {
|
|
@@ -1200,6 +1223,10 @@ export function registerDisasmTools(server, z) {
|
|
|
1200
1223
|
case "rom": return await disassembleRomCore(args);
|
|
1201
1224
|
case "project": return await disassembleProjectCore(args);
|
|
1202
1225
|
case "references": return jsonContent(await findReferencesCore(args));
|
|
1226
|
+
case "cfg": return jsonContent(await analyzeCfg(requireRomPath(args), args.address, args.platform));
|
|
1227
|
+
case "xrefs": return jsonContent(await analyzeXrefs(requireRomPath(args), args.address, args.platform));
|
|
1228
|
+
case "functions": return jsonContent(await analyzeFunctions(requireRomPath(args), args.platform));
|
|
1229
|
+
case "decompile": return jsonContent(await analyzeDecompile(requireRomPath(args), args.address, args.platform));
|
|
1203
1230
|
default: throw new Error(`disasm: unknown target '${args.target}'`);
|
|
1204
1231
|
}
|
|
1205
1232
|
}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// learnFontMap / encodeTextForRom / findEncodedText — text-hack workflow.
|
|
2
2
|
//
|
|
3
|
-
// Every retro game maps characters to tile-IDs differently (
|
|
4
|
-
// A=$0A, B=$0B, ..., Z=$23;
|
|
3
|
+
// Every retro game maps characters to tile-IDs differently (one NES racer:
|
|
4
|
+
// A=$0A, B=$0B, ..., Z=$23; another game: ASCII offset; a third: sparse table). The
|
|
5
5
|
// agent currently reverse-engineers this by hand each session. These
|
|
6
6
|
// three tools automate it:
|
|
7
7
|
//
|
|
@@ -186,7 +186,7 @@ async function makeTilemapReader(host, platform, which) {
|
|
|
186
186
|
* Decide whether a run of (char, tileId) reads from a live tilemap is FONT TEXT
|
|
187
187
|
* (a reusable character→tile map) or a PRE-RENDERED GRAPHIC (a name/logo drawn as
|
|
188
188
|
* a bitmap, where each cell is a unique tile). The trap a long RE session hit:
|
|
189
|
-
*
|
|
189
|
+
* Some games' player names are bitmaps, so patching the ASCII string does nothing.
|
|
190
190
|
* Signals a graphic when: (1) a repeated character used a DIFFERENT tile each
|
|
191
191
|
* time (a real font reuses one tile per letter) — the direct proof; OR (2) every
|
|
192
192
|
* tile is unique AND the ids form a near-contiguous run (tiles X,X+1,X+2,… = one
|
package/src/mcp/tools/index.js
CHANGED
|
@@ -116,8 +116,8 @@ const CATEGORIES = [
|
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
name: "debug",
|
|
119
|
-
description: "Cross-platform debugging
|
|
120
|
-
useWhen: ["sprites rendering wrong", "audio silent or distorted", "CPU stuck in unknown state", "need to read what existing ROM bytes do"],
|
|
119
|
+
description: "Cross-platform debugging + reverse-engineering: inspectSprites, inspectPalette, getCPUState (main/spc700/z80), getAudioState (dsp/psg/ym2612), and the disasm/RE engine — disassemble (raw/ROM/rebuildable-project), plus the Rizin/Ghidra ops disasm({target:'cfg'|'xrefs'|'functions'|'decompile'}) (control-flow graphs, deep xrefs, auto-detected functions, and C pseudocode — all 14 platforms) and symbols({op:'analyze'}) (one-shot structural map).",
|
|
120
|
+
useWhen: ["sprites rendering wrong", "audio silent or distorted", "CPU stuck in unknown state", "need to read what existing ROM bytes do", "reverse-engineering an unknown ROM — carve its functions/structure before labeling them live", "want C-like pseudocode to understand a routine"],
|
|
121
121
|
register: (s, z, k) => {
|
|
122
122
|
registerPlatformSpecificTools(s, z, k); // inspectSprites/Palette, getCPUState, getDspState, ...
|
|
123
123
|
registerSymbolTools(s, z, k); // buildSourceWithDebug, resolveSymbol, lookupAddress, ...
|
package/src/mcp/tools/project.js
CHANGED
|
@@ -1464,7 +1464,7 @@ TEMPLATES.genesis = {
|
|
|
1464
1464
|
runtimeDirs: SGDK_RUNTIME_DIRS,
|
|
1465
1465
|
lang: SGDK_LANG,
|
|
1466
1466
|
ext: ".bin",
|
|
1467
|
-
describe: "Two-plane parallax SCROLLING scaffold — the smooth-feel starting point for a
|
|
1467
|
+
describe: "Two-plane parallax SCROLLING scaffold — the smooth-feel starting point for a parallax side-scroller. Plane A = a painted foreground world (ground + platform blocks), Plane B = a repeated starfield, one player sprite. The frame loop does HARDWARE SCROLL ONLY (two VDP_setHorizontalScroll writes + one VDP_updateSprites) — ZERO tilemap writes per frame, which is what keeps movement smooth (rewriting a plane each frame is the #1 'choppy horizontal movement' bug). Plane B scrolls at 1/4 speed for depth. Exposes volatile g_player_x / g_cam_x so you can motion-trace it headlessly (symbols->memory->recordSession). Extend by streaming one offscreen column per 8-px camera step for worlds wider than 512 px — see Genesis MENTAL_MODEL.md 'Scrolling, parallax & the feel trap'.",
|
|
1468
1468
|
},
|
|
1469
1469
|
puzzle: {
|
|
1470
1470
|
main: "templates/puzzle.c",
|
|
@@ -390,7 +390,7 @@ const PLATFORM_REGISTRY = {
|
|
|
390
390
|
msx: { forms: msxPointerForms, verdict: "literal-escape", formats: ["raw", "konami-rle"],
|
|
391
391
|
note: "RAW when uncompressed; Konami/other RLE has a literal-run token, so a literal-escape stored block is producible." },
|
|
392
392
|
genesis: { forms: genesisPointerForms, verdict: "literal-escape", formats: ["raw", "kosinski-literal"],
|
|
393
|
-
note: "Pointers are 32-bit BE = ROM offset (1:1 at $000000). Kosinski has an all-literal path (experimental terminator — self-verify). Nemesis is Huffman — NO stored escape; custom LZ (e.g.
|
|
393
|
+
note: "Pointers are 32-bit BE = ROM offset (1:1 at $000000). Kosinski has an all-literal path (experimental terminator — self-verify). Nemesis is Huffman — NO stored escape; custom LZ (e.g. some Genesis sports titles) — confirm the literal-run shape per game." },
|
|
394
394
|
snes: { forms: snesPointerForms, verdict: "literal-escape", formats: ["raw", "lz2-direct"],
|
|
395
395
|
note: "Pointers 16-bit (bank-implied) or 24-bit long LE; needs LoROM/HiROM (auto-detected). LC_LZ2 has a clean direct-copy (000) literal command + 0xFF end — common but per-game; confirm the codec." },
|
|
396
396
|
gba: { forms: gbaPointerForms, verdict: "literal-escape", formats: ["raw", "lz77-literal"],
|
package/src/mcp/tools/symbols.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { writeFileSync } from "node:fs";
|
|
11
11
|
import { readFile } from "node:fs/promises";
|
|
12
12
|
import { jsonContent, safeTool, writeOutput } from "../util.js";
|
|
13
|
+
import { analyzeStructure } from "../../analysis/analyze.js";
|
|
13
14
|
import { addressToSymbolCore } from "./address-to-symbol.js";
|
|
14
15
|
|
|
15
16
|
// Tail length kept inline when a big log is written to a sibling file.
|
|
@@ -459,7 +460,7 @@ function registerSymbolsTool(server, z) {
|
|
|
459
460
|
"Auto-detects GNU ld (Genesis/m68k + GBA/ARM), sdld (`XXXX _name`, GB/GBC/SMS/GG/MSX), and ld65 VICE " +
|
|
460
461
|
"(`al XXXX .name`, cc65/dasm).",
|
|
461
462
|
{
|
|
462
|
-
op: z.enum(["resolve", "lookup", "map", "list", "addr"]).describe("resolve name→addr; lookup addr→sym; map = layout by region; list all; addr = PC→nearest symbol."),
|
|
463
|
+
op: z.enum(["resolve", "lookup", "map", "list", "addr", "analyze"]).describe("resolve name→addr; lookup addr→sym; map = layout by region; list all; addr = PC→nearest symbol; analyze = Rizin structural map of a ROM (no .dbg/.map needed — auto-detected functions + strings + entrypoints)."),
|
|
463
464
|
dbg: z.string().optional().describe("op=resolve/lookup/list/map: cc65 .dbg text from build({output:'romWithDebug'}) (NES/C64/Atari7800/Lynx/PCE). Pass this OR `map`/`dbgPath`/`mapPath`."),
|
|
464
465
|
map: z.string().optional().describe("op=resolve/lookup/list/map: .map text (build's `mapText`/`symbols`) — auto-detects sdld (GB/GBC/SMS/GG/MSX) vs GNU ld (Genesis/m68k). Pass this OR `dbg`/`dbgPath`/`mapPath`."),
|
|
465
466
|
dbgPath: z.string().optional().describe("op=resolve/lookup/list/map: ABSOLUTE path to a cc65 .dbg on disk (the `dbgPath` build({output:'romWithDebug'}) returned). Server reads it — the map never enters your context. Inline `dbg` wins if both passed."),
|
|
@@ -472,11 +473,16 @@ function registerSymbolsTool(server, z) {
|
|
|
472
473
|
pc: z.number().int().min(0).max(0xFFFFFF).optional().describe("op=addr: CPU address to look up (e.g. 0x01A7)."),
|
|
473
474
|
symbolsText: z.string().optional().describe("op=addr: inline .map/.sym text (build's `symbols`). Takes precedence over symbolsPath."),
|
|
474
475
|
symbolsPath: z.string().optional().describe("op=addr: path to a .map/.sym file (used only if symbolsText absent)."),
|
|
476
|
+
// analyze
|
|
477
|
+
romPath: z.string().optional().describe("op=analyze: ABSOLUTE path to the ROM file to structurally map via Rizin."),
|
|
475
478
|
},
|
|
476
479
|
safeTool(async (args) => {
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
+
// op=analyze and op=addr don't take a .dbg/.map source. Everything else
|
|
481
|
+
// resolves dbgPath/mapPath → text off disk so the map never enters context.
|
|
482
|
+
if (args.op === "analyze") {
|
|
483
|
+
if (!args.romPath) throw new Error("symbols op='analyze' requires `romPath` (the ROM file).");
|
|
484
|
+
return jsonContent(await analyzeStructure(args.romPath, args.platform));
|
|
485
|
+
}
|
|
480
486
|
const a = args.op === "addr" ? args : { ...args, ...(await loadDebugSource(args)) };
|
|
481
487
|
switch (a.op) {
|
|
482
488
|
case "resolve": return jsonContent(await resolveSymbolCore(a));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// traceVramSource (Genesis) — "which ROM offset did this VRAM graphic come from?"
|
|
2
2
|
//
|
|
3
|
-
// The trap it kills:
|
|
3
|
+
// The trap it kills: some sports games' player names are pre-rendered tile bitmaps
|
|
4
4
|
// DMA'd into VRAM from ROM, not font-rendered from a string. You can SEE the
|
|
5
5
|
// name but patching the ASCII string does nothing — the source is the bitmap in
|
|
6
6
|
// ROM. Genesis makes this traceable: a memory→VRAM DMA leaves its SOURCE address
|
|
@@ -1027,7 +1027,7 @@ export function registerWatchMemoryTools(server, z, sessionKey) {
|
|
|
1027
1027
|
"hours diffing a CORRECT codec against that poisoned output. Prefer pure for every decompressor/codec call.\n" +
|
|
1028
1028
|
"• op:'decompress' — convenience wrapper over op:'call' for the common decompressor shape: call `entryPC` with " +
|
|
1029
1029
|
"A0=`sourceAddress` (and optionally A1=`destAddress`), run until it returns, then read `destAddress`. For the " +
|
|
1030
|
-
"
|
|
1030
|
+
"the 'pre-rendered name + portrait are LZ-compressed' wall: point it at the game's own decompressor.",
|
|
1031
1031
|
{
|
|
1032
1032
|
op: z.enum(["read", "setReg", "call", "decompress"])
|
|
1033
1033
|
.describe("read=CPU registers/flags; setReg=write one register; call=drive a subroutine until it returns; decompress=call shortcut (A0=source, A1=dest)."),
|
|
@@ -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.
|
|
@@ -110,7 +114,7 @@ The #1 trap: visible names/labels are often **pre-rendered tile GRAPHICS**, not
|
|
|
110
114
|
font-rendered from an ASCII string. Patching the ASCII string then does nothing.
|
|
111
115
|
|
|
112
116
|
1. `text({op:'learn'})` on the on-screen text — it infers the game's char→tile-ID map
|
|
113
|
-
(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
|
|
114
118
|
modes: ROM mode `knownStrings:[{text, offset}]` when you found the bytes; **LIVE mode
|
|
115
119
|
`fromScreen:[{text, row, col}]`** reads the tile IDs straight off the live BG map at a
|
|
116
120
|
tile position (`background({view:'map'})` shows where the text sits) — this breaks the
|
|
@@ -310,6 +314,40 @@ Breakpoints are great once you KNOW the address. To FIND it:
|
|
|
310
314
|
|
|
311
315
|
---
|
|
312
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
|
+
|
|
313
351
|
## 6. Driving menus (the real wall-clock sink)
|
|
314
352
|
|
|
315
353
|
Use `input({op:'navigate', steps:[{button, maxWaitFrames}]})` — it advances on **screen
|
|
@@ -397,7 +435,11 @@ watch the screen react — cheaper than shipping a wrong ROM patch.
|
|
|
397
435
|
## 7b. Whole-ROM rebuildable disassembly — `disasm({target:'project'})`
|
|
398
436
|
|
|
399
437
|
For a STRUCTURAL hack (new logic, not a byte poke), turn the whole ROM into a
|
|
400
|
-
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
|
|
401
443
|
the ROM into regions (per-bank on EVERY banked format: 16KB banks for NES/GB/SMS-GG/MSX/
|
|
402
444
|
7800-SuperGame, 32KB for SNES LoROM, 4KB for banked 2600, 8KB pages for >32KB HuCards;
|
|
403
445
|
one flat region for Genesis/C64/Lynx/GBA and small carts), disassembles each through the CPU's
|
|
@@ -496,7 +538,10 @@ For sprite/tile edits (not text), don't hand-roll the tile-format math:
|
|
|
496
538
|
| Find / encode a font-rendered string | `text({op:'find'})` → `text({op:'encode'})` |
|
|
497
539
|
| Assemble asm → raw patch bytes | `assembleSnippet({cpu, origin, code})` |
|
|
498
540
|
| Mapper-aware diff of two ROMs | `romPatch({op:'diff'})` (CPU addrs, CHR `tile:N`) |
|
|
499
|
-
| 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) |
|
|
500
545
|
| Split / rebuild a ROM into parts | `cart({op:'extract'})` / `cart({op:'wrap'})` |
|
|
501
546
|
| Swap a sprite/tile (PNG round-trip) | `tiles({op:'png'})` → edit → `romPatch({op:'spliceCHR'})` |
|
|
502
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
|
|