romdevtools 0.26.0 → 0.28.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 +5 -3
- package/CHANGELOG.md +322 -3
- package/README.md +1 -1
- package/examples/README.md +1 -1
- package/examples/atari2600/templates/platformer.asm +18 -9
- package/examples/atari2600/templates/racing.asm +25 -4
- package/examples/atari2600/templates/shmup.asm +30 -5
- package/examples/atari2600/templates/sports.asm +41 -9
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +12 -8
- package/examples/atari7800/templates/puzzle.c +7 -4
- package/examples/atari7800/templates/racing.c +5 -2
- package/examples/atari7800/templates/shmup.c +8 -4
- package/examples/atari7800/templates/sports.c +6 -3
- package/examples/c64/templates/platformer.c +28 -24
- package/examples/c64/templates/puzzle.c +77 -16
- package/examples/c64/templates/racing.c +9 -0
- package/examples/c64/templates/shmup.c +13 -1
- package/examples/c64/templates/sports.c +9 -4
- package/examples/gb/templates/platformer.c +6 -2
- package/examples/gb/templates/puzzle.c +279 -101
- package/examples/gb/templates/racing.c +13 -1
- package/examples/gb/templates/shmup.c +13 -1
- package/examples/gb/templates/sports.c +9 -3
- package/examples/gba/templates/platformer.c +7 -13
- package/examples/gba/templates/puzzle.c +93 -15
- package/examples/gba/templates/racing.c +13 -1
- package/examples/gba/templates/shmup.c +13 -1
- package/examples/gba/templates/sports.c +17 -5
- package/examples/gbc/templates/platformer.c +6 -2
- package/examples/gbc/templates/puzzle.c +878 -178
- package/examples/gbc/templates/racing.c +13 -1
- package/examples/gbc/templates/shmup.c +13 -1
- package/examples/gbc/templates/sports.c +9 -3
- package/examples/genesis/templates/puzzle.c +76 -15
- package/examples/genesis/templates/racing.c +13 -1
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/gg/templates/platformer.c +4 -0
- package/examples/gg/templates/puzzle.c +80 -14
- package/examples/gg/templates/racing.c +17 -1
- package/examples/gg/templates/shmup.c +17 -1
- package/examples/gg/templates/sports.c +4 -0
- package/examples/lynx/templates/platformer.c +25 -6
- package/examples/lynx/templates/puzzle.c +77 -14
- package/examples/lynx/templates/shmup.c +13 -1
- package/examples/lynx/templates/sports.c +5 -2
- package/examples/msx/platformer/main.c +2 -0
- package/examples/msx/puzzle/main.c +78 -15
- package/examples/msx/racing/main.c +1 -0
- package/examples/msx/shmup/main.c +1 -0
- package/examples/msx/sports/main.c +3 -2
- package/examples/nes/templates/platformer.c +11 -3
- package/examples/nes/templates/puzzle.c +81 -21
- package/examples/nes/templates/racing.c +15 -1
- package/examples/nes/templates/shmup.c +1 -0
- package/examples/nes/templates/sports.c +1 -0
- package/examples/pce/platformer/main.c +3 -1
- package/examples/pce/puzzle/main.c +78 -12
- package/examples/pce/racing/main.c +1 -0
- package/examples/pce/shmup/main.c +5 -4
- package/examples/pce/sports/main.c +4 -3
- package/examples/sms/templates/platformer.c +4 -0
- package/examples/sms/templates/puzzle.c +80 -14
- package/examples/sms/templates/racing.c +17 -1
- package/examples/sms/templates/shmup.c +17 -1
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +4 -0
- package/examples/snes/templates/platformer.c +32 -15
- package/examples/snes/templates/puzzle.c +84 -16
- package/examples/snes/templates/racing.c +20 -1
- package/examples/snes/templates/shmup.c +20 -2
- package/examples/snes/templates/sports.c +7 -0
- 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 +245 -10
- package/src/mcp/server.js +6 -0
- 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 +11 -4
- package/src/mcp/tools/index.js +15 -1
- package/src/mcp/tools/input.js +26 -3
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/project.js +35 -9
- package/src/mcp/tools/toolchain.js +43 -10
- package/src/mcp/tools/watch-memory.js +172 -25
- 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 +27 -6
- package/src/platforms/gb/MENTAL_MODEL.md +16 -1
- package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
- package/src/platforms/gb/lib/c/patch-header.js +7 -4
- package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/patch-header.js +7 -4
- package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
- 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/TROUBLESHOOTING.md +13 -17
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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 +6 -0
- 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 +2 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
- package/src/platforms/nes/MENTAL_MODEL.md +10 -3
- package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
- package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
- package/src/platforms/pce/MENTAL_MODEL.md +9 -0
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +2 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/sms/MENTAL_MODEL.md +5 -0
- 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 +5 -0
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/index.js +37 -8
package/src/mcp/tools/project.js
CHANGED
|
@@ -92,7 +92,7 @@ const TEMPLATES = {
|
|
|
92
92
|
linkerConfig: { presetSrc: "presets/nes/chr-ram-runtime.cfg", dst: "chr-ram-runtime.cfg" },
|
|
93
93
|
lang: "C (cc65)",
|
|
94
94
|
ext: ".nes",
|
|
95
|
-
describe: "Match-3 falling-block puzzle. 6×12 grid, 1×3 active piece (3 colors), rotate via A, soft-drop on DOWN,
|
|
95
|
+
describe: "Match-3 falling-block puzzle. 6×12 grid, 1×3 active piece (3 colors), rotate via A, soft-drop on DOWN, 3+-in-a-row clears in all 4 directions (H/V/diagonals) with gravity + cascade chains.",
|
|
96
96
|
},
|
|
97
97
|
sports: {
|
|
98
98
|
main: "templates/sports.c",
|
|
@@ -214,7 +214,7 @@ const TEMPLATES = {
|
|
|
214
214
|
],
|
|
215
215
|
lang: "C (SDCC sm83)",
|
|
216
216
|
ext: ".gb",
|
|
217
|
-
describe: "Match-3 falling-block puzzle scaffold for GB. 6×12 grid rendered via BG tilemap, 1×3 active piece (3 colours via 3 BG tile shapes), rotate via A, hard-drop on START,
|
|
217
|
+
describe: "Match-3 falling-block puzzle scaffold for GB. 6×12 grid rendered via BG tilemap, 1×3 active piece (3 colours via 3 BG tile shapes), rotate via A, hard-drop on START, 3+-in-a-row clears in all 4 directions (H/V/diagonals) with gravity + cascade chains.",
|
|
218
218
|
},
|
|
219
219
|
sports: {
|
|
220
220
|
main: "templates/sports.c",
|
|
@@ -284,7 +284,7 @@ const TEMPLATES = {
|
|
|
284
284
|
catch_game: mk("catch_game", "A complete tiny game: a paddle catches a falling object with the d-pad; full game loop with waitvsync(), two sprites, collision, scoring."),
|
|
285
285
|
shmup: mk("shmup", "Vertical shoot-'em-up for PC Engine. Player ship + bullet/enemy object pools, a wave spawner, AABB collisions, score HUD, scrolling-band starfield BG. d-pad flies, button I fires. The base for any action shooter."),
|
|
286
286
|
platformer: mk("platformer", "Side-scrolling platformer for PC Engine. Gravity + jump + land-on-top platform collision, a multi-screen world streamed via BG X-scroll (BXR), solid platform tiles, sub-pixel physics. d-pad moves, button I jumps."),
|
|
287
|
-
puzzle: mk("puzzle", "Match-3 / falling-block puzzle for PC Engine. A 6x12 well drawn with BG tiles, a 1x3 active piece you move/rotate/soft-drop/hard-drop,
|
|
287
|
+
puzzle: mk("puzzle", "Match-3 / falling-block puzzle for PC Engine. A 6x12 well drawn with BG tiles, a 1x3 active piece you move/rotate/soft-drop/hard-drop, 3+-in-a-row clears (H+V) with gravity + cascade chains, score. d-pad moves, I rotates, II hard-drops."),
|
|
288
288
|
sports: mk("sports", "Pong-style sports game for PC Engine. Two paddles + a bouncing ball on a netted court, score to 9, paddle-deflect physics; player 2 falls back to chase-AI when no input. d-pad moves P1."),
|
|
289
289
|
racing: mk("racing", "Top-down lane racer for PC Engine. Player car at the bottom, obstacle cars spawn from the top and slide down, LEFT/RIGHT switches lanes, speed grows with score, crash freeze + auto-reset. Scrolling road BG."),
|
|
290
290
|
};
|
|
@@ -305,7 +305,7 @@ const TEMPLATES = {
|
|
|
305
305
|
catch_game: mk("catch_game", "A complete tiny game: a paddle catches falling fruit with the joystick; full game loop with vblank sync, two sprites, collision, scoring."),
|
|
306
306
|
shmup: mk("shmup", "Vertical-shmup scaffold for MSX (screen 2). Player ship (sprite plane 0) + 4 bullet + 4 enemy object pools, a wave spawner, AABB collision, on-screen SCORE tiles, over a banded starfield filling the whole 32x24 name table. Joystick PORT 1 moves the ship (UP/DOWN/LEFT/RIGHT), trigger A (GTTRIG) fires; PSG blip on fire, noise-ish tone on a kill. Interrupt-free vsync via VDP status S#0. Extend with enemy fire, lives, scrolling stars."),
|
|
307
307
|
platformer: mk("platformer", "Side-scrolling platformer for MSX (screen 2). Subpixel gravity/jump/land-on-top collision against a table of platforms across a 512-px (64-cell) world, drawn by COLUMN STREAMING into the wrapping screen-2 name table as the camera follows the player; the player sprite draws in screen space. Joystick LEFT/RIGHT walks, trigger A jumps (only when grounded); PSG jump blip. Interrupt-free vsync. Extend with enemies, pickups, goal."),
|
|
308
|
-
puzzle: mk("puzzle", "Match-3 / falling-block puzzle for MSX (screen 2). A 6-wide x 12-tall well drawn with the BG tilemap (distinct R/G/B cell tiles + grey border + dim field interior so the playfield is always visible). A 1x3 active piece: joystick LEFT/RIGHT shifts, trigger A rotates the colour order, DOWN soft-drops, trigger B hard-drops;
|
|
308
|
+
puzzle: mk("puzzle", "Match-3 / falling-block puzzle for MSX (screen 2). A 6-wide x 12-tall well drawn with the BG tilemap (distinct R/G/B cell tiles + grey border + dim field interior so the playfield is always visible). A 1x3 active piece: joystick LEFT/RIGHT shifts, trigger A rotates the colour order, DOWN soft-drops, trigger B hard-drops; 3+-in-a-row clears in all 4 directions with gravity + cascade chains; PSG chime per clear. Interrupt-free vsync. Extend with levels/next-piece preview."),
|
|
309
309
|
sports: mk("sports", "Pong-style 2-player sports for MSX (screen 2). Court (green field + white sidelines + dashed centre net) fills the 32x24 name table; two paddles (stacked sprites) + a ball. Player 1 = joystick PORT 1 UP/DOWN; Player 2 = joystick PORT 2 UP/DOWN, falling back to chase-the-ball AI when no second pad is present so it is playable solo. Wall/paddle bounces + scoring with PSG bonks. Interrupt-free vsync. Extend with serve angles, score display, win condition."),
|
|
310
310
|
racing: mk("racing", "Top-down 3-lane racing for MSX (screen 2). Grey road + green-grass shoulders fill the name table; player car at the bottom, obstacle cars (object pool) spawn at the top and slide down. Joystick LEFT/RIGHT (edge-detected) switches lanes; obstacle speed grows with score; an AABB crash triggers a ~60-frame freeze then auto-reset, with a PSG crash tone. SCORE drawn as tiles. Interrupt-free vsync. Extend with pseudo-3D road, fuel, multiple cars."),
|
|
311
311
|
};
|
|
@@ -351,9 +351,19 @@ TEMPLATES.gbc = {
|
|
|
351
351
|
describe: "SIDE-SCROLLING platformer for GBC. Full CGB color palette (BG + sprite via BCPS/OCPS) over the GB side-scroller core: subpixel gravity + jump + land-on-top collision against platforms across a 256-px world (the wrapping BG map). The camera follows the player and scrolls the BG via SCX; the player sprite draws in screen space. A=jump, d-pad=move. One BG map wide (no streaming) — for a wider world, stream a new BG-map column on each 8px camera step (window for a fixed HUD). See the GBC MENTAL_MODEL.md 'Horizontal scrolling'. Extend with enemies, goals, pickups.",
|
|
352
352
|
},
|
|
353
353
|
puzzle: {
|
|
354
|
-
main: "templates/puzzle.c",
|
|
354
|
+
main: "templates/puzzle.c",
|
|
355
|
+
runtime: [
|
|
356
|
+
...GBC_RUNTIME,
|
|
357
|
+
{ src: "lib/c/font.h", dst: "font.h" }, /* digits+A-Z 2bpp glyphs for the HUD */
|
|
358
|
+
],
|
|
355
359
|
lang: GBC_LANG, ext: ".gbc",
|
|
356
|
-
describe: "
|
|
360
|
+
describe: "Falling-jewel matcher for GBC (the polished reference puzzle). 8x17 well, 6 jewel colors with " +
|
|
361
|
+
"real CGB palettes, matches in all 4 directions (H/V/both diagonals), gravity + cascade chains, magic " +
|
|
362
|
+
"jewel every 18th piece, level speedup, 6-digit score, title + game-over screens, SFX + toggleable " +
|
|
363
|
+
"music. Rendering: falling column + NEXT preview are OAM sprites; the locked well is BG tiles via a " +
|
|
364
|
+
"COLLECT/FLUSH vblank queue with an idle scrub (writes outside vblank silently drop on this core — " +
|
|
365
|
+
"never bypass the queue). Statics need dataLoc 0xC200 (above shadow_oam at $C100) — the project build " +
|
|
366
|
+
"recipe sets that automatically.",
|
|
357
367
|
},
|
|
358
368
|
sports: {
|
|
359
369
|
main: "templates/sports.c", runtime: GBC_RUNTIME,
|
|
@@ -398,6 +408,11 @@ TEMPLATES.gbc = {
|
|
|
398
408
|
// against. Factored to a constant so adding a new template is a one-line
|
|
399
409
|
// change at the bottom.
|
|
400
410
|
const SMS_RUNTIME = [
|
|
411
|
+
// The crt0 ships IN the project (like GG/MSX) so the dir is genuinely
|
|
412
|
+
// self-contained: build({output:'project'}) routes it via the crt0 channel
|
|
413
|
+
// (projectBuildRecipe), and an external stock-SDCC rebuild has the real
|
|
414
|
+
// boot stub on disk instead of silently linking SDCC's non-booting one.
|
|
415
|
+
{ src: "lib/c/sms_crt0.s", dst: "sms_crt0.s" },
|
|
401
416
|
{ src: "lib/c/sms_hw.h", dst: "sms_hw.h" },
|
|
402
417
|
{ src: "lib/c/vdp_init.c", dst: "vdp_init.c" },
|
|
403
418
|
{ src: "lib/c/load_palette.c", dst: "load_palette.c" },
|
|
@@ -764,7 +779,7 @@ TEMPLATES.snes = {
|
|
|
764
779
|
runtimeDirs: SNES_PVSNESLIB_VENDOR_DIRS,
|
|
765
780
|
lang: "C (tcc-65816 + PVSnesLib)",
|
|
766
781
|
ext: ".sfc",
|
|
767
|
-
describe: "Match-3 falling-block puzzle for SNES. 6×12 grid (text mode), rotate/soft-drop/hard-drop,
|
|
782
|
+
describe: "Match-3 falling-block puzzle for SNES. 6×12 grid (text mode), rotate/soft-drop/hard-drop, 3+-in-a-row clears in all 4 directions with gravity + cascade chains. Rotate click + clear chime via bundled SPC700 sfx.",
|
|
768
783
|
},
|
|
769
784
|
sports: {
|
|
770
785
|
main: "templates/sports.c",
|
|
@@ -939,7 +954,7 @@ TEMPLATES.genesis = {
|
|
|
939
954
|
runtimeDirs: SGDK_RUNTIME_DIRS,
|
|
940
955
|
lang: SGDK_LANG,
|
|
941
956
|
ext: ".bin",
|
|
942
|
-
describe: "Match-3 falling-block puzzle genre scaffold. 6×12 grid, 1×3 active piece (3 colours), rotate via A, soft-drop on DOWN, hard-drop on START,
|
|
957
|
+
describe: "Match-3 falling-block puzzle genre scaffold. 6×12 grid, 1×3 active piece (3 colours), rotate via A, soft-drop on DOWN, hard-drop on START, 3+-in-a-row clears in all 4 directions with gravity + cascade chains. xorshift RNG so cell colours actually vary.",
|
|
943
958
|
},
|
|
944
959
|
sports: {
|
|
945
960
|
main: "templates/sports.c",
|
|
@@ -1757,7 +1772,18 @@ Compiles **C89**, not C99/C11. Stick to:
|
|
|
1757
1772
|
let filesSection = `## Files\n\n- \`${mainFilename}\` — your game's entry point.\n`;
|
|
1758
1773
|
if (tmpl?.runtime) {
|
|
1759
1774
|
for (const { dst } of tmpl.runtime) {
|
|
1760
|
-
|
|
1775
|
+
if (dst === "patch-header.js") {
|
|
1776
|
+
// NOT game code — calling it a "runtime helper" implied it compiles
|
|
1777
|
+
// into the ROM and confused readers. It's a standalone sidecar tool.
|
|
1778
|
+
filesSection += `- \`${dst}\` — sidecar TOOL, not game code (never compiled into the ROM). ` +
|
|
1779
|
+
`Stamps the Nintendo logo + header/global checksums a GB ROM needs to boot ` +
|
|
1780
|
+
`(\`node patch-header.js game.gb\`) — a zero-install stand-in for RGBDS's rgbfix when you ` +
|
|
1781
|
+
`rebuild OUTSIDE romdev with stock SDCC. romdev's own builds fix the header automatically.\n`;
|
|
1782
|
+
} else if (dst.endsWith("_crt0.s")) {
|
|
1783
|
+
filesSection += `- \`${dst}\` — startup assembly (reset/interrupt vectors, RAM init; routed as the crt0 by the project build). **You own this.**\n`;
|
|
1784
|
+
} else {
|
|
1785
|
+
filesSection += `- \`${dst}\` — runtime helper. **You own this** — edit or replace at will.\n`;
|
|
1786
|
+
}
|
|
1761
1787
|
}
|
|
1762
1788
|
}
|
|
1763
1789
|
if (tmpl?.crt0) {
|
|
@@ -266,7 +266,7 @@ export function installToolchainCore({ id }) {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
export function registerToolchainTools(server, z, sessionKey) {
|
|
269
|
-
async function buildSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, crt0, crt0Path, codeLoc, dataLoc, options, linkerConfig, inesHeader, outputPath, inline = false, includeSymbols = false, lint = "advisory", runtime, maxmod, rebuildSdk }) {
|
|
269
|
+
async function buildSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, crt0, crt0Path, codeLoc, dataLoc, options, linkerConfig, linkerConfigPath, inesHeader, outputPath, inline = false, includeSymbols = false, lint = "advisory", runtime, maxmod, rebuildSdk }) {
|
|
270
270
|
// Reject conflicting inline vs path args — fail loud, not silent.
|
|
271
271
|
if (source != null && sourcePath != null) {
|
|
272
272
|
throw new Error("build({output:'rom'}): pass either `source` OR `sourcePath`, not both.");
|
|
@@ -281,6 +281,12 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
281
281
|
if (crt0Path) {
|
|
282
282
|
crt0 = await readFile(crt0Path, "utf-8");
|
|
283
283
|
}
|
|
284
|
+
// linkerConfigPath: read the .cfg from disk so a large multi-bank
|
|
285
|
+
// config (e.g. a disasm'd mapper-2 rebuild) isn't re-streamed through
|
|
286
|
+
// context on every build (0.27.0 feedback #2).
|
|
287
|
+
if (linkerConfigPath && linkerConfig == null) {
|
|
288
|
+
linkerConfig = await readFile(linkerConfigPath, "utf-8");
|
|
289
|
+
}
|
|
284
290
|
// Auto-inject the bundled crt0 for SMS/GG when caller didn't pass
|
|
285
291
|
// one. Stock SDCC crt0 doesn't boot these targets; without this,
|
|
286
292
|
// user main() is never called → black screen. See AUTO_CRT0_PLATFORMS.
|
|
@@ -464,7 +470,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
464
470
|
return jsonContent(payload);
|
|
465
471
|
}
|
|
466
472
|
|
|
467
|
-
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, inesHeader, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
473
|
+
async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, linkerConfigPath, inesHeader, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
|
|
468
474
|
const { buildForPlatform } = await import("../../toolchains/index.js");
|
|
469
475
|
const resolved = resolveCore(platform);
|
|
470
476
|
if (!resolved) throw new Error(`no core available for platform '${platform}'`);
|
|
@@ -480,6 +486,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
480
486
|
binaryIncludes = { ...(binaryIncludes ?? {}), ...r.binaryIncludes };
|
|
481
487
|
if (r.crt0 != null) crt0 = r.crt0;
|
|
482
488
|
if (r.codeLoc != null) codeLoc = r.codeLoc;
|
|
489
|
+
if (r.dataLoc != null && dataLoc == null) dataLoc = r.dataLoc;
|
|
483
490
|
if (r.linkerConfig != null && linkerConfig == null) linkerConfig = r.linkerConfig;
|
|
484
491
|
if (r.runtime != null && runtime == null) runtime = r.runtime;
|
|
485
492
|
if (r.maxmod != null && maxmod == null) maxmod = r.maxmod;
|
|
@@ -510,6 +517,9 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
510
517
|
if (crt0Path) {
|
|
511
518
|
crt0 = await readFile(crt0Path, "utf-8");
|
|
512
519
|
}
|
|
520
|
+
if (linkerConfigPath && linkerConfig == null) {
|
|
521
|
+
linkerConfig = await readFile(linkerConfigPath, "utf-8");
|
|
522
|
+
}
|
|
513
523
|
// Auto-inject bundled crt0 for SMS/GG when caller didn't pass one
|
|
514
524
|
// (stock SDCC crt0 doesn't boot these targets — see buildSource).
|
|
515
525
|
if (crt0 == null) {
|
|
@@ -714,6 +724,7 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
714
724
|
dataLoc: z.coerce.number().int().optional().describe("SDCC — _DATA (WRAM) load address (default $C000 on Z80). NOT read by output:'romWithDebug'."),
|
|
715
725
|
options: z.array(z.string()).optional().describe("output:'rom' — extra toolchain CLI options."),
|
|
716
726
|
linkerConfig: z.string().optional().describe("ld65 linker config (cc65). NES presets: 'chr-ram-runtime' (RECOMMENDED for homebrew C — full crt0 + iNES header + NMI w/ OAM DMA + `_shadow_oam` at $0200), 'chr-ram' (bare nmi:rti stub), 'chr-rom' (cc65-C with FIXED CHR-ROM art — segment split + CHARS segment; supply CHR via binaryIncludePaths into a CHARS source + the header via `inesHeader`). Or full .cfg contents. Preset NAMES only resolve on output:'rom'/'run'; output:'romWithDebug' takes raw .cfg contents only. **For rebuilding a commercial NROM game from its disassembly, prefer `inesHeader` over a raw .cfg.**"),
|
|
727
|
+
linkerConfigPath: z.string().optional().describe("Path-based `linkerConfig`: absolute path to a .cfg file on disk (the server reads it — the cfg never enters your context; e.g. the multi-bank cfg a banked-NES disasm project ships). Ignored when `linkerConfig` is passed inline."),
|
|
717
728
|
inesHeader: z.object({
|
|
718
729
|
prgBanks: z.coerce.number().int().min(1).max(255).describe("16KB PRG-ROM banks (1 = NROM-128, 2 = NROM-256)."),
|
|
719
730
|
chrBanks: z.coerce.number().int().min(0).max(255).optional().describe("8KB CHR-ROM banks (0 = CHR-RAM, no CHARS segment). Default 0."),
|
|
@@ -785,8 +796,8 @@ export function registerToolchainTools(server, z, sessionKey) {
|
|
|
785
796
|
*/
|
|
786
797
|
export function projectBuildRecipe(platform, names) {
|
|
787
798
|
const has = (n) => names.includes(n);
|
|
788
|
-
/** @type {{crt0File:string|null, codeLoc:number|undefined, linkerConfig:string|undefined, runtime:string|undefined, maxmod:boolean|undefined, skip:Set<string>, includeAsC:Set<string>}} */
|
|
789
|
-
const r = { crt0File: null, codeLoc: undefined, linkerConfig: undefined, runtime: undefined, maxmod: undefined, skip: new Set(), includeAsC: new Set() };
|
|
799
|
+
/** @type {{crt0File:string|null, codeLoc:number|undefined, dataLoc:number|undefined, linkerConfig:string|undefined, runtime:string|undefined, maxmod:boolean|undefined, skip:Set<string>, includeAsC:Set<string>}} */
|
|
800
|
+
const r = { crt0File: null, codeLoc: undefined, dataLoc: undefined, linkerConfig: undefined, runtime: undefined, maxmod: undefined, skip: new Set(), includeAsC: new Set() };
|
|
790
801
|
|
|
791
802
|
// Reference/upstream sources ship for grepping, not compiling (e.g. GB
|
|
792
803
|
// music_demo's hUGEDriver.upstream.asm — the .c port is what builds). Skip
|
|
@@ -796,7 +807,11 @@ export function projectBuildRecipe(platform, names) {
|
|
|
796
807
|
if (platform === "gb" || platform === "gbc") {
|
|
797
808
|
// GB/GBC ship gb_crt0.s — it MUST go via crt0+codeLoc:0x150, never as a
|
|
798
809
|
// source (SDCC emits its own gsinit → "Multiple definition of gsinit").
|
|
799
|
-
|
|
810
|
+
// dataLoc 0xC200: statics start ABOVE shadow_oam ($C100-$C19F, fixed by
|
|
811
|
+
// the runtime). The sdld default of $C000 let any project with >256 bytes
|
|
812
|
+
// of statics silently overlap the OAM shadow — oam_clear() then zeroed
|
|
813
|
+
// game state (grid/RNG seed). 512 bytes of 8KB WRAM is cheap insurance.
|
|
814
|
+
if (has("gb_crt0.s")) { r.crt0File = "gb_crt0.s"; r.codeLoc = 0x150; r.dataLoc = 0xC200; }
|
|
800
815
|
} else if (platform === "nes") {
|
|
801
816
|
// A SCAFFOLDED NES project ships nes_runtime.c + a crt0 + a .cfg and needs
|
|
802
817
|
// the chr-ram-runtime preset (it defines the OAM/CHARS segments + a NMI with
|
|
@@ -824,9 +839,17 @@ export function projectBuildRecipe(platform, names) {
|
|
|
824
839
|
// msx_crt0.s through crt0 makes ITS header + init the cartridge entry.
|
|
825
840
|
if (has("msx_crt0.s")) { r.crt0File = "msx_crt0.s"; r.codeLoc = 0x4010; }
|
|
826
841
|
} else if (platform === "sms" || platform === "gg") {
|
|
827
|
-
// SMS/GG
|
|
828
|
-
//
|
|
829
|
-
|
|
842
|
+
// SMS/GG: route the project's *_crt0.s through the crt0 channel (like
|
|
843
|
+
// GB/MSX), NOT as a plain source TU. The OLD recipe skipped it on the
|
|
844
|
+
// belief that "buildForPlatform auto-injects the bundled crt0" — IT DOES
|
|
845
|
+
// NOT (only the output:'rom'/'run' MCP handlers auto-inject). So every
|
|
846
|
+
// output:'project' SMS/GG build linked SDCC's STOCK z80 crt0, whose boot
|
|
847
|
+
// is `ld a,#2 / rst $08 / halt` — main() never ran and every scaffold
|
|
848
|
+
// booted to a BLACK SCREEN (the RetroDECK "all broken" report; our
|
|
849
|
+
// output:'run' verifications were false-green via the other path).
|
|
850
|
+
// readProjectDir falls back to the bundled crt0 when the dir has none.
|
|
851
|
+
const crt0Name = names.find((n) => /_crt0\.s$/i.test(n));
|
|
852
|
+
if (crt0Name) r.crt0File = crt0Name;
|
|
830
853
|
} else if (platform === "genesis" || platform === "megadrive" || platform === "md") {
|
|
831
854
|
// SGDK supplies sega startup + rom header. The scaffold dir may contain
|
|
832
855
|
// generated intermediates (sega.s, sega.preprocessed.s, rom_header.*, and an
|
|
@@ -919,6 +942,15 @@ export async function readProjectDir(projPath, platform) {
|
|
|
919
942
|
}
|
|
920
943
|
}
|
|
921
944
|
|
|
945
|
+
// SMS/GG with no crt0 file in the dir → fall back to the bundled crt0,
|
|
946
|
+
// exactly like the output:'rom'/'run' handlers do. Without this the link
|
|
947
|
+
// silently uses SDCC's stock z80 crt0, which never calls main() (black
|
|
948
|
+
// screen at boot). The SMS scaffold historically shipped without a crt0
|
|
949
|
+
// file, so this fallback is load-bearing for existing project dirs.
|
|
950
|
+
if (crt0 == null && (platform === "sms" || platform === "gg")) {
|
|
951
|
+
crt0 = await resolveAutoCrt0(platform);
|
|
952
|
+
}
|
|
953
|
+
|
|
922
954
|
// GBA runtime refinement: libgba if the entry includes <gba.h>, else the
|
|
923
955
|
// libtonc default the recipe set.
|
|
924
956
|
let runtime = recipe.runtime;
|
|
@@ -926,11 +958,11 @@ export async function readProjectDir(projPath, platform) {
|
|
|
926
958
|
runtime = "libgba";
|
|
927
959
|
}
|
|
928
960
|
|
|
929
|
-
return { sources, includes, binaryIncludes, crt0, codeLoc: recipe.codeLoc, linkerConfig: recipe.linkerConfig, runtime, maxmod: recipe.maxmod };
|
|
961
|
+
return { sources, includes, binaryIncludes, crt0, codeLoc: recipe.codeLoc, dataLoc: recipe.dataLoc, linkerConfig: recipe.linkerConfig, runtime, maxmod: recipe.maxmod };
|
|
930
962
|
}
|
|
931
963
|
|
|
932
964
|
export async function buildProjectCore({ path: projPath, platform, outputPath }) {
|
|
933
|
-
const { sources, includes, binaryIncludes, crt0, codeLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform);
|
|
965
|
+
const { sources, includes, binaryIncludes, crt0, codeLoc, dataLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform);
|
|
934
966
|
|
|
935
967
|
// Linker preset: the recipe names it (e.g. NES 'chr-ram-runtime', which ships
|
|
936
968
|
// the OAM/CHARS segments + its own crt0). resolveLinkerConfig also returns any
|
|
@@ -968,6 +1000,7 @@ export async function buildProjectCore({ path: projPath, platform, outputPath })
|
|
|
968
1000
|
linkerConfig: resolvedLinkerConfig,
|
|
969
1001
|
crt0: crt0Rel,
|
|
970
1002
|
codeLoc,
|
|
1003
|
+
dataLoc,
|
|
971
1004
|
});
|
|
972
1005
|
if (outputPath && result.binary) {
|
|
973
1006
|
await mkdir(path.dirname(outputPath), { recursive: true });
|