romdevtools 0.27.0 → 0.29.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 +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -177
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -180
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -156
- package/package.json +12 -12
- 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 +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +19 -6
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +64 -19
|
@@ -30,17 +30,19 @@
|
|
|
30
30
|
// and BUILD.md. `build.linkerConfig`, when present, is INLINE .cfg text (not a
|
|
31
31
|
// path) — the emitter does not rewrite it.
|
|
32
32
|
//
|
|
33
|
-
// IMPORTANT —
|
|
33
|
+
// IMPORTANT — which platforms get a one-call build() and why:
|
|
34
34
|
// `disasm` emits each region in the syntax of the NATIVE reassembler for that
|
|
35
35
|
// CPU family (ca65 for 6502/65816; GNU `as` for z80/sm83/m68k/arm), which is
|
|
36
|
-
// what round-trips it byte-exact.
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
36
|
+
// what round-trips it byte-exact. A one-call build() rebuild exists exactly
|
|
37
|
+
// where build()'s asm toolchain IS cc65/ca65: NES (NROM and banked mappers),
|
|
38
|
+
// C64, Atari 7800 (flat and SuperGame banked), Lynx, and PCE (flat and
|
|
39
|
+
// banked HuCards). The rest differ: SNES=asar, Atari2600=dasm,
|
|
40
|
+
// SMS/GG/MSX/GB/GBC=SDCC, Genesis=vasm/SGDK, GBA=arm-gcc(C-only) — none of
|
|
41
|
+
// which consume the disasm's syntax. For those, the byte-exact rebuild is
|
|
42
|
+
// the NATIVE recipe documented in `notes` (per-bank for banked carts; the
|
|
43
|
+
// blobs are still written), so the project is rebuildable even though
|
|
44
|
+
// there's no single build() call. `verifiable` reflects whether that recipe
|
|
45
|
+
// is proven byte-identical, regardless of build()-vs-native.
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* @param {string} platform
|
|
@@ -90,7 +92,7 @@ const PLANNERS = {
|
|
|
90
92
|
// The ONE platform with a one-call build() rebuild: disasm emits ca65, and the
|
|
91
93
|
// NES build() toolchain IS cc65/ca65. inesHeader synthesizes the 16B header +
|
|
92
94
|
// the CHARS segment wiring + the flat NROM .cfg. PROVEN byte-identical.
|
|
93
|
-
nes(data) {
|
|
95
|
+
nes(data, regions) {
|
|
94
96
|
const prgBanks = data[4];
|
|
95
97
|
const chrBanks = data[5];
|
|
96
98
|
const mapper = (data[6] >> 4) | (data[7] & 0xf0);
|
|
@@ -103,30 +105,93 @@ const PLANNERS = {
|
|
|
103
105
|
blobs["chr.bin"] = data.slice(prgEnd, prgEnd + chrBanks * 0x2000);
|
|
104
106
|
binaryIncludePaths["chr.bin"] = "chr.bin";
|
|
105
107
|
}
|
|
106
|
-
const
|
|
108
|
+
const flat = mapper === 0 && prgBanks <= 2;
|
|
109
|
+
if (flat) {
|
|
110
|
+
// NROM: the inesHeader one-call path (PROVEN byte-identical). Unchanged.
|
|
111
|
+
return {
|
|
112
|
+
blobs,
|
|
113
|
+
build: {
|
|
114
|
+
output: "rom",
|
|
115
|
+
platform: "nes",
|
|
116
|
+
sourcesPaths: { "prg.asm": "bank0.asm" },
|
|
117
|
+
...(Object.keys(binaryIncludePaths).length ? { binaryIncludePaths } : {}),
|
|
118
|
+
inesHeader: {
|
|
119
|
+
prgBanks,
|
|
120
|
+
...(chrBanks ? { chrBanks } : {}),
|
|
121
|
+
mirroring,
|
|
122
|
+
...(battery ? { battery: true } : {}),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
verifiable: true,
|
|
126
|
+
notes: `NROM rebuild: inesHeader auto-emits the 16B header + wires chr.bin into the ` +
|
|
127
|
+
`CHARS segment; bank0.asm is the PRG. One-call build() rebuild, byte-identical.`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// BANKED mapper (>0 or >2 PRG banks): emit COMPLETE working glue — the old
|
|
131
|
+
// recipe pointed at bank0 only with the flat NROM cfg, which cannot rebuild
|
|
132
|
+
// a banked ROM and cost a real RE session an hour of hand-written segments
|
|
133
|
+
// + cfg (0.27.0 feedback #1). Components:
|
|
134
|
+
// nes_header.asm — HEADER segment with the ORIGINAL 16 iNES bytes
|
|
135
|
+
// bankN_seg.asm — `.segment "PRGn"` wrapper that includes bankN.asm
|
|
136
|
+
// nes_chars.asm — CHARS segment .incbin of chr.bin (when CHR-ROM)
|
|
137
|
+
// nes_rebuild.cfg — HEADER + one 16KB area per bank (+ CHR), in file order
|
|
138
|
+
// and a rebuild.json build() call wired to ALL of it (linkerConfigPath keeps
|
|
139
|
+
// the cfg out of context).
|
|
140
|
+
const hdrBytes = Array.from(data.slice(0, 16))
|
|
141
|
+
.map((b) => "$" + b.toString(16).toUpperCase().padStart(2, "0")).join(", ");
|
|
142
|
+
blobs["nes_header.asm"] = enc(
|
|
143
|
+
`; iNES header — the ORIGINAL 16 bytes, for a byte-exact rebuild.\n` +
|
|
144
|
+
`.segment "HEADER"\n .byte ${hdrBytes}\n`
|
|
145
|
+
);
|
|
146
|
+
const nBanks = regions.length;
|
|
147
|
+
const memLines = [
|
|
148
|
+
` HEADER: start = $0000, size = $0010, type = ro, file = %O, fill = yes;`,
|
|
149
|
+
];
|
|
150
|
+
const segLines = [
|
|
151
|
+
` HEADER: load = HEADER, type = ro;`,
|
|
152
|
+
];
|
|
153
|
+
const sourcesPaths = { "nes_header.s": "nes_header.asm" };
|
|
154
|
+
const includePaths = {};
|
|
155
|
+
for (let i = 0; i < nBanks; i++) {
|
|
156
|
+
const reg = regions[i];
|
|
157
|
+
blobs[`bank${i}_seg.asm`] = enc(
|
|
158
|
+
`; PRG bank ${i} wrapper (${reg.label}) — auto-generated by disasm({target:'project'}).\n` +
|
|
159
|
+
`.segment "PRG${i}"\n.include "bank${i}.asm"\n`
|
|
160
|
+
);
|
|
161
|
+
memLines.push(` PRG${i}: start = $${reg.startAddress.toString(16).toUpperCase()}, size = $4000, type = ro, file = %O, fill = yes, fillval = $FF;`);
|
|
162
|
+
segLines.push(` PRG${i}: load = PRG${i}, type = ro;`);
|
|
163
|
+
sourcesPaths[`bank${i}_seg.s`] = `bank${i}_seg.asm`;
|
|
164
|
+
includePaths[`bank${i}.asm`] = `bank${i}.asm`;
|
|
165
|
+
}
|
|
166
|
+
if (chrBanks > 0) {
|
|
167
|
+
blobs["nes_chars.asm"] = enc(`; CHR-ROM data.\n.segment "CHARS"\n .incbin "chr.bin"\n`);
|
|
168
|
+
memLines.push(` CHR: start = $0000, size = $${(chrBanks * 0x2000).toString(16).toUpperCase()}, type = ro, file = %O, fill = yes;`);
|
|
169
|
+
segLines.push(` CHARS: load = CHR, type = ro;`);
|
|
170
|
+
sourcesPaths["nes_chars.s"] = "nes_chars.asm";
|
|
171
|
+
}
|
|
172
|
+
blobs["nes_rebuild.cfg"] = enc(
|
|
173
|
+
`# Banked NES rebuild .cfg — auto-generated by disasm({target:'project'}).\n` +
|
|
174
|
+
`# mapper ${mapper}, ${nBanks} x 16KB PRG banks (last bank at $C000)${chrBanks ? ", " + chrBanks + " x 8KB CHR" : ""}.\n` +
|
|
175
|
+
`# MEMORY order = file order: header, banks 0..${nBanks - 1}${chrBanks ? ", CHR" : ""}.\n` +
|
|
176
|
+
"MEMORY {\n" + memLines.join("\n") + "\n}\n" +
|
|
177
|
+
"SEGMENTS {\n" + segLines.join("\n") + "\n}\n"
|
|
178
|
+
);
|
|
107
179
|
return {
|
|
108
180
|
blobs,
|
|
109
181
|
build: {
|
|
110
182
|
output: "rom",
|
|
111
183
|
platform: "nes",
|
|
112
|
-
sourcesPaths
|
|
184
|
+
sourcesPaths,
|
|
185
|
+
includePaths,
|
|
113
186
|
...(Object.keys(binaryIncludePaths).length ? { binaryIncludePaths } : {}),
|
|
114
|
-
|
|
115
|
-
prgBanks,
|
|
116
|
-
...(chrBanks ? { chrBanks } : {}),
|
|
117
|
-
...(mapper ? { mapper } : {}),
|
|
118
|
-
mirroring,
|
|
119
|
-
...(battery ? { battery: true } : {}),
|
|
120
|
-
},
|
|
187
|
+
linkerConfigPath: "nes_rebuild.cfg",
|
|
121
188
|
},
|
|
122
|
-
verifiable,
|
|
123
|
-
notes:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
`places each bank — the per-bank .asm files + chr.bin are ready; see the NES ` +
|
|
129
|
-
`MENTAL_MODEL.md "CHR-ROM / banked rebuilds" note.`,
|
|
189
|
+
verifiable: true,
|
|
190
|
+
notes:
|
|
191
|
+
`Banked NES (mapper ${mapper}, ${nBanks} x 16KB PRG): per-bank PRGn segment wrappers + ` +
|
|
192
|
+
`nes_header.asm (original 16 bytes) + nes_rebuild.cfg are all emitted and wired into ` +
|
|
193
|
+
`rebuild.json — a one-call byte-exact build() rebuild (linkerConfigPath keeps the cfg ` +
|
|
194
|
+
`out of context). Switchable banks land at $8000, the fixed top bank at $C000.`,
|
|
130
195
|
};
|
|
131
196
|
},
|
|
132
197
|
|
|
@@ -172,11 +237,75 @@ const PLANNERS = {
|
|
|
172
237
|
};
|
|
173
238
|
},
|
|
174
239
|
|
|
175
|
-
// ───────────────────────────────────────────── Atari 7800 (.a78
|
|
176
|
-
// build({platform:'atari7800'}) IS cc65/ca65
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
240
|
+
// ───────────────────────────────────────────── Atari 7800 (.a78)
|
|
241
|
+
// build({platform:'atari7800'}) IS cc65/ca65 — one-call build() rebuilds, both
|
|
242
|
+
// shapes:
|
|
243
|
+
// flat (≤48KB cart): planRegions keeps the 128-byte .a78 header inside
|
|
244
|
+
// rom.asm; a single flat cc65 build reproduces the whole .a78. PROVEN.
|
|
245
|
+
// SuperGame banked (>48KB cart): planRegions splits the header into its own
|
|
246
|
+
// data region + one 16KB bank region per bank (last fixed @ $C000). Emit
|
|
247
|
+
// NES-style glue: a78_header_seg.asm (HEADER segment, original 128 bytes),
|
|
248
|
+
// bankN_seg.asm wrappers, multi-bank .cfg via linkerConfigPath.
|
|
249
|
+
atari7800(data, regions) {
|
|
250
|
+
const bankRegions = regions.filter((r) => r.name.startsWith("bank"));
|
|
251
|
+
if (bankRegions.length) {
|
|
252
|
+
const hasA78 = regions.some((r) => r.name === "a78_header");
|
|
253
|
+
const blobs = {};
|
|
254
|
+
const memLines = [];
|
|
255
|
+
const segLines = [];
|
|
256
|
+
const sourcesPaths = {};
|
|
257
|
+
const includePaths = {};
|
|
258
|
+
if (hasA78) {
|
|
259
|
+
const hdrBytes = Array.from(data.slice(0, 128))
|
|
260
|
+
.map((b) => "$" + b.toString(16).toUpperCase().padStart(2, "0")).join(", ");
|
|
261
|
+
// 128 bytes won't fit one .byte line cleanly — wrap at 16/line.
|
|
262
|
+
const rows = [];
|
|
263
|
+
for (let i = 0; i < 128; i += 16) {
|
|
264
|
+
rows.push(" .byte " + hdrBytes.split(", ").slice(i, i + 16).join(", "));
|
|
265
|
+
}
|
|
266
|
+
blobs["a78_header_seg.asm"] = enc(
|
|
267
|
+
`; .a78 header — the ORIGINAL 128 bytes, for a byte-exact rebuild.\n` +
|
|
268
|
+
`.segment "HEADER"\n${rows.join("\n")}\n`
|
|
269
|
+
);
|
|
270
|
+
memLines.push(` HEADER: start = $0000, size = $0080, type = ro, file = %O, fill = yes;`);
|
|
271
|
+
segLines.push(` HEADER: load = HEADER, type = ro;`);
|
|
272
|
+
sourcesPaths["a78_header_seg.s"] = "a78_header_seg.asm";
|
|
273
|
+
}
|
|
274
|
+
for (let i = 0; i < bankRegions.length; i++) {
|
|
275
|
+
const reg = bankRegions[i];
|
|
276
|
+
blobs[`bank${i}_seg.asm`] = enc(
|
|
277
|
+
`; SuperGame bank ${i} wrapper (${reg.label}) — auto-generated by disasm({target:'project'}).\n` +
|
|
278
|
+
`.segment "BANK${i}"\n.include "bank${i}.asm"\n`
|
|
279
|
+
);
|
|
280
|
+
memLines.push(` BANK${i}: start = $${reg.startAddress.toString(16).toUpperCase()}, size = $4000, type = ro, file = %O, fill = yes, fillval = $FF;`);
|
|
281
|
+
segLines.push(` BANK${i}: load = BANK${i}, type = ro;`);
|
|
282
|
+
sourcesPaths[`bank${i}_seg.s`] = `bank${i}_seg.asm`;
|
|
283
|
+
includePaths[`bank${i}.asm`] = `bank${i}.asm`;
|
|
284
|
+
}
|
|
285
|
+
blobs["a78_rebuild.cfg"] = enc(
|
|
286
|
+
`# Banked Atari 7800 (.a78 SuperGame) rebuild .cfg — auto-generated by disasm({target:'project'}).\n` +
|
|
287
|
+
`# MEMORY order = file order: ${hasA78 ? "header, " : ""}banks 0..${bankRegions.length - 1} (last bank fixed at $C000).\n` +
|
|
288
|
+
"MEMORY {\n" + memLines.join("\n") + "\n}\n" +
|
|
289
|
+
"SEGMENTS {\n" + segLines.join("\n") + "\n}\n"
|
|
290
|
+
);
|
|
291
|
+
return {
|
|
292
|
+
blobs,
|
|
293
|
+
build: {
|
|
294
|
+
output: "rom",
|
|
295
|
+
platform: "atari7800",
|
|
296
|
+
language: "asm",
|
|
297
|
+
sourcesPaths,
|
|
298
|
+
includePaths,
|
|
299
|
+
linkerConfigPath: "a78_rebuild.cfg",
|
|
300
|
+
},
|
|
301
|
+
verifiable: true,
|
|
302
|
+
notes:
|
|
303
|
+
`Banked Atari 7800 SuperGame (${bankRegions.length} x 16KB banks${hasA78 ? " + 128-byte .a78 header" : ""}): ` +
|
|
304
|
+
`per-bank BANKn segment wrappers${hasA78 ? " + a78_header_seg.asm (original 128 bytes)" : ""} + ` +
|
|
305
|
+
`a78_rebuild.cfg are emitted and wired into rebuild.json — a one-call byte-exact build() ` +
|
|
306
|
+
`rebuild. Switchable banks land at $8000, the fixed top bank at $C000.`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
180
309
|
const org = (0x10000 - data.length) & 0xffff;
|
|
181
310
|
const cfg =
|
|
182
311
|
"# Atari 7800 .a78 rebuild .cfg — auto-generated by disasm({target:'project'}).\n" +
|
|
@@ -206,12 +335,43 @@ const PLANNERS = {
|
|
|
206
335
|
};
|
|
207
336
|
},
|
|
208
337
|
|
|
209
|
-
// ───────────────────────────────────────────── Atari 2600 (
|
|
338
|
+
// ───────────────────────────────────────────── Atari 2600 (dasm — no build() route)
|
|
210
339
|
// The 2600 disasm emits ca65 syntax, but build({platform:'atari2600'}) is DASM,
|
|
211
340
|
// which can't consume ca65 (.setcpu etc.). So no one-call build() route. The
|
|
212
|
-
// rebuild IS byte-exact via the native ca65/ld65 chain
|
|
213
|
-
//
|
|
341
|
+
// rebuild IS byte-exact via the native ca65/ld65 chain — flat carts via a flat
|
|
342
|
+
// fill .cfg; banked carts (F8/F6/F4 — every 4KB bank pages into $F000) via
|
|
343
|
+
// per-bank BANKn segment wrappers + a multi-area .cfg. Recipe in notes.
|
|
214
344
|
atari2600(data, regions) {
|
|
345
|
+
if (regions.length > 1) {
|
|
346
|
+
const blobs = {};
|
|
347
|
+
const memLines = [];
|
|
348
|
+
const segLines = [];
|
|
349
|
+
for (let i = 0; i < regions.length; i++) {
|
|
350
|
+
blobs[`bank${i}_seg.asm`] = enc(
|
|
351
|
+
`; 2600 banked-cart bank ${i} wrapper — auto-generated by disasm({target:'project'}).\n` +
|
|
352
|
+
`.segment "BANK${i}"\n.include "bank${i}.asm"\n`
|
|
353
|
+
);
|
|
354
|
+
memLines.push(` BANK${i}: start = $F000, size = $1000, type = ro, file = %O, fill = yes, fillval = $FF;`);
|
|
355
|
+
segLines.push(` BANK${i}: load = BANK${i}, type = ro;`);
|
|
356
|
+
}
|
|
357
|
+
blobs["atari2600_rebuild.cfg"] = enc(
|
|
358
|
+
"# Atari 2600 banked-cart rebuild .cfg — auto-generated by disasm({target:'project'}).\n" +
|
|
359
|
+
`# ${regions.length} x 4KB banks, each at $F000, concatenated in file order.\n` +
|
|
360
|
+
"MEMORY {\n" + memLines.join("\n") + "\n}\n" +
|
|
361
|
+
"SEGMENTS {\n" + segLines.join("\n") + "\n}\n"
|
|
362
|
+
);
|
|
363
|
+
return {
|
|
364
|
+
blobs,
|
|
365
|
+
build: null,
|
|
366
|
+
verifiable: true,
|
|
367
|
+
notes:
|
|
368
|
+
`Atari 2600 banked cart (${regions.length} x 4KB banks @ $F000 — F8/F6/F4-style). The disasm ` +
|
|
369
|
+
`emits ca65 syntax, but build({platform:'atari2600'}) is DASM — it can't reassemble ca65. ` +
|
|
370
|
+
`Rebuild with the bundled native ca65/ld65 (byte-identical):\n` +
|
|
371
|
+
` ca65 bank0_seg.asm -o b0.o && … && ld65 -C atari2600_rebuild.cfg -o game.bin b0.o..b${regions.length - 1}.o\n` +
|
|
372
|
+
`(Wrappers + atari2600_rebuild.cfg are shipped as blobs.)`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
215
375
|
const reg = regions[0];
|
|
216
376
|
const cfg =
|
|
217
377
|
"# Atari 2600 flat-cart rebuild .cfg — auto-generated by disasm({target:'project'}).\n" +
|
|
@@ -319,37 +479,76 @@ const PLANNERS = {
|
|
|
319
479
|
};
|
|
320
480
|
},
|
|
321
481
|
|
|
322
|
-
// ───────────────────────────────────────────── PC Engine (HuCard —
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
482
|
+
// ───────────────────────────────────────────── PC Engine (HuCard, ca65 — one-call)
|
|
483
|
+
// planRegions is now FAITHFUL (copier header split into its own region, NO
|
|
484
|
+
// trailing-pad trim), and build({platform:'pce', language:'asm'}) is cc65/ca65
|
|
485
|
+
// — the same match that makes NES a one-call rebuild. Both shapes:
|
|
486
|
+
// flat (≤32KB): one CODE area at the top-of-space org.
|
|
487
|
+
// banked (>32KB): one 8KB PAGE area per page (page 0 @ $E000 with the
|
|
488
|
+
// vectors, pages 1+ @ $8000) via NES-style segment wrappers + .cfg.
|
|
489
|
+
// A copier header, when present, is re-emitted as a HEADER segment so the
|
|
490
|
+
// single output file is the complete original .pce.
|
|
327
491
|
pce(data, regions) {
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
492
|
+
const hasCopier = regions.some((r) => r.name === "copier_header");
|
|
493
|
+
const codeRegions = regions.filter((r) => r.kind !== "data");
|
|
494
|
+
const blobs = {};
|
|
495
|
+
const memLines = [];
|
|
496
|
+
const segLines = [];
|
|
497
|
+
const sourcesPaths = {};
|
|
498
|
+
const includePaths = {};
|
|
499
|
+
if (hasCopier) {
|
|
500
|
+
const rows = [];
|
|
501
|
+
for (let i = 0; i < 512; i += 16) {
|
|
502
|
+
rows.push(" .byte " + Array.from(data.slice(i, i + 16))
|
|
503
|
+
.map((b) => "$" + b.toString(16).toUpperCase().padStart(2, "0")).join(", "));
|
|
504
|
+
}
|
|
505
|
+
blobs["copier_header_seg.asm"] = enc(
|
|
506
|
+
`; 512-byte copier header — the ORIGINAL bytes, for a byte-exact rebuild.\n` +
|
|
507
|
+
`.segment "HEADER"\n${rows.join("\n")}\n`
|
|
508
|
+
);
|
|
509
|
+
memLines.push(` HEADER: start = $0000, size = $0200, type = ro, file = %O, fill = yes;`);
|
|
510
|
+
segLines.push(` HEADER: load = HEADER, type = ro;`);
|
|
511
|
+
sourcesPaths["copier_header_seg.s"] = "copier_header_seg.asm";
|
|
512
|
+
}
|
|
513
|
+
const banked = codeRegions.length > 1;
|
|
514
|
+
for (let i = 0; i < codeRegions.length; i++) {
|
|
515
|
+
const reg = codeRegions[i];
|
|
516
|
+
const segName = banked ? `PAGE${i}` : "CODE";
|
|
517
|
+
blobs[`${reg.name}_seg.asm`] = enc(
|
|
518
|
+
`; ${reg.label} wrapper — auto-generated by disasm({target:'project'}).\n` +
|
|
519
|
+
`.segment "${segName}"\n.include "${reg.file}"\n`
|
|
520
|
+
);
|
|
521
|
+
memLines.push(` ${segName}: start = $${reg.startAddress.toString(16).toUpperCase()}, size = $${reg.bytes.length.toString(16).toUpperCase()}, type = ro, file = %O, fill = yes, fillval = $FF;`);
|
|
522
|
+
segLines.push(` ${segName}: load = ${segName}, type = ro;`);
|
|
523
|
+
sourcesPaths[`${reg.name}_seg.s`] = `${reg.name}_seg.asm`;
|
|
524
|
+
includePaths[reg.file] = reg.file;
|
|
525
|
+
}
|
|
526
|
+
blobs["pce_rebuild.cfg"] = enc(
|
|
527
|
+
`# PC Engine HuCard rebuild .cfg — auto-generated by disasm({target:'project'}).\n` +
|
|
528
|
+
`# MEMORY order = file order: ${hasCopier ? "copier header, " : ""}` +
|
|
529
|
+
(banked ? `pages 0..${codeRegions.length - 1} (8KB each; page 0 holds the vectors).\n` : `the flat HuCard image.\n`) +
|
|
530
|
+
"MEMORY {\n" + memLines.join("\n") + "\n}\n" +
|
|
531
|
+
"SEGMENTS {\n" + segLines.join("\n") + "\n}\n"
|
|
532
|
+
);
|
|
339
533
|
return {
|
|
340
|
-
blobs
|
|
341
|
-
build:
|
|
342
|
-
|
|
534
|
+
blobs,
|
|
535
|
+
build: {
|
|
536
|
+
output: "rom",
|
|
537
|
+
platform: "pce",
|
|
538
|
+
language: "asm",
|
|
539
|
+
sourcesPaths,
|
|
540
|
+
includePaths,
|
|
541
|
+
linkerConfigPath: "pce_rebuild.cfg",
|
|
542
|
+
},
|
|
543
|
+
verifiable: true,
|
|
343
544
|
notes:
|
|
344
|
-
`PC Engine
|
|
345
|
-
|
|
346
|
-
`
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
`build({platform:'pce'}) is cc65 — the native ca65/ld65 chain with pce_rebuild.cfg is ` +
|
|
352
|
-
`the rebuild path once the region is faithful.`,
|
|
545
|
+
`PC Engine HuCard (${banked ? codeRegions.length + " x 8KB pages" : "flat " + codeRegions[0].bytes.length + " B"}` +
|
|
546
|
+
`${hasCopier ? " + 512-byte copier header (re-emitted as a HEADER segment)" : ""}): ` +
|
|
547
|
+
`segment wrappers + pce_rebuild.cfg are emitted and wired into rebuild.json — a one-call ` +
|
|
548
|
+
`byte-exact build() rebuild (the pce asm toolchain IS cc65/ca65, same as NES). ` +
|
|
549
|
+
(banked ? `Page 0 lands at $E000 (reset MPR7 mapping — vectors live there); pages 1+ at $8000 ` +
|
|
550
|
+
`(an ASSUMED window — the game's MPR writes decide at runtime; only branch-label cosmetics ` +
|
|
551
|
+
`depend on it, the bytes are exact regardless).` : ``),
|
|
353
552
|
};
|
|
354
553
|
},
|
|
355
554
|
|
|
@@ -364,8 +563,36 @@ const PLANNERS = {
|
|
|
364
563
|
// + re-prepended. With the reassemble.js .org-floor fix, rom.asm now round-trips
|
|
365
564
|
// (the region is at $4010), so the native recipe is byte-exact.
|
|
366
565
|
msx(data, regions) {
|
|
367
|
-
const reg = regions.find((r) => r.file === "rom.asm") ?? regions[0];
|
|
368
566
|
const hasHeader = data.length >= 2 && data[0] === 0x41 && data[1] === 0x42; // "AB"
|
|
567
|
+
const bankRegions = regions.filter((r) => r.name.startsWith("bank"));
|
|
568
|
+
if (bankRegions.length) {
|
|
569
|
+
// megaROM (>32KB, ASCII16-style 16KB banks). Bank 0's region starts at
|
|
570
|
+
// $4010 (header split out); banks 1+ are full 16KB at $8000. Native
|
|
571
|
+
// per-bank recipe: assemble, extract at each org, concatenate after the
|
|
572
|
+
// header bytes.
|
|
573
|
+
const blobs = {};
|
|
574
|
+
if (hasHeader) blobs["msx_header.bin"] = data.slice(0, 16);
|
|
575
|
+
return {
|
|
576
|
+
blobs,
|
|
577
|
+
build: null,
|
|
578
|
+
verifiable: true,
|
|
579
|
+
notes:
|
|
580
|
+
`MSX megaROM (${bankRegions.length} x 16KB banks: bank 0 @ $4010 after the "AB" header` +
|
|
581
|
+
`${hasHeader ? " — shipped as msx_header.bin" : ""}, banks 1+ @ $8000, an ASSUMED ASCII16-style ` +
|
|
582
|
+
`window — only branch-label cosmetics depend on it, the bytes are exact regardless). ` +
|
|
583
|
+
`build({platform:'msx'}) is SDCC — it can't reassemble the GNU-\`as\` bankN.asm. Rebuild ` +
|
|
584
|
+
`natively (bundled z80 binutils, byte-identical), assembling each bank and concatenating:\n` +
|
|
585
|
+
bankRegions
|
|
586
|
+
.map(
|
|
587
|
+
(r) =>
|
|
588
|
+
` z80-elf-as -march=z80 ${r.file} -o ${r.name}.o && z80-elf-objcopy -O binary ${r.name}.o ${r.name}.full && ` +
|
|
589
|
+
`dd if=${r.name}.full of=${r.name}.bin bs=1 skip=$((0x${r.startAddress.toString(16)})) count=${r.bytes.length}`
|
|
590
|
+
)
|
|
591
|
+
.join("\n") +
|
|
592
|
+
`\n cat ${hasHeader ? "msx_header.bin " : ""}${bankRegions.map((r) => r.name + ".bin").join(" ")} > rebuilt.rom`,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const reg = regions.find((r) => r.file === "rom.asm") ?? regions[0];
|
|
369
596
|
const blobs = {};
|
|
370
597
|
if (hasHeader) blobs["msx_header.bin"] = data.slice(0, 16);
|
|
371
598
|
const bodyStart = hasHeader ? 16 : 0;
|
|
@@ -455,10 +682,33 @@ const PLANNERS = {
|
|
|
455
682
|
* GNU-`as` rom.asm); native recipe in notes, byte-identical proven.
|
|
456
683
|
*/
|
|
457
684
|
function planSegaFlat(platform, data, regions) {
|
|
685
|
+
const name = platform === "sms" ? "Master System" : "Game Gear";
|
|
686
|
+
if (regions.length > 1) {
|
|
687
|
+
// Sega-mapper banked cart: one full 16KB region per bank (no pad trim) —
|
|
688
|
+
// assemble each at its window org, extract, concatenate. Same per-bank
|
|
689
|
+
// native recipe shape as Game Boy.
|
|
690
|
+
return {
|
|
691
|
+
blobs: {},
|
|
692
|
+
build: null,
|
|
693
|
+
verifiable: true,
|
|
694
|
+
notes:
|
|
695
|
+
`${name} Sega-mapper banked cart (${regions.length} x 16KB banks: bank 0 @ $0000, bank 1 @ $4000, ` +
|
|
696
|
+
`banks 2+ @ $8000 — their slot-2 window). build({platform:'${platform}'}) is SDCC — it can't ` +
|
|
697
|
+
`reassemble the GNU-\`as\` bankN.asm. Rebuild natively (bundled z80 binutils, byte-identical), ` +
|
|
698
|
+
`assembling each bank and concatenating:\n` +
|
|
699
|
+
regions
|
|
700
|
+
.map(
|
|
701
|
+
(r) =>
|
|
702
|
+
` z80-elf-as -march=z80 ${r.file} -o ${r.name}.o && z80-elf-objcopy -O binary ${r.name}.o ${r.name}.full && ` +
|
|
703
|
+
`dd if=${r.name}.full of=${r.name}.bin bs=1 skip=$((0x${r.startAddress.toString(16)})) count=${r.bytes.length}`
|
|
704
|
+
)
|
|
705
|
+
.join("\n") +
|
|
706
|
+
`\n cat ${regions.map((r) => r.name + ".bin").join(" ")} > rebuilt.${platform === "sms" ? "sms" : "gg"}`,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
458
709
|
const reg = regions.find((r) => r.file === "rom.asm") ?? regions[0];
|
|
459
710
|
const trimmedLen = reg ? reg.bytes.length : data.length;
|
|
460
711
|
const pad = trailingPad(data, trimmedLen);
|
|
461
|
-
const name = platform === "sms" ? "Master System" : "Game Gear";
|
|
462
712
|
return {
|
|
463
713
|
blobs: {},
|
|
464
714
|
build: null,
|