romdevtools 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/AGENTS.md +15 -4
  2. package/CHANGELOG.md +66 -0
  3. package/examples/atari7800/templates/hello_sprite.c +48 -4
  4. package/examples/atari7800/templates/music_demo.c +47 -2
  5. package/examples/c64/templates/tile_engine.c +77 -27
  6. package/examples/gb/templates/hello_sprite.c +15 -6
  7. package/examples/gb/templates/music_demo.c +36 -0
  8. package/examples/gb/templates/platformer.c +3 -2
  9. package/examples/gb/templates/puzzle.c +3 -2
  10. package/examples/gb/templates/racing.c +3 -2
  11. package/examples/gb/templates/shmup.c +3 -2
  12. package/examples/gb/templates/sports.c +3 -2
  13. package/examples/gb/templates/tile_engine.c +3 -2
  14. package/examples/gba/templates/maxmod_demo.c +36 -2
  15. package/examples/gba/templates/platformer.c +3 -1
  16. package/examples/gba/templates/tonc_hello_sprite.c +35 -1
  17. package/examples/gbc/templates/hello_sprite.c +12 -3
  18. package/examples/gbc/templates/music_demo.c +56 -12
  19. package/examples/gbc/templates/platformer.c +3 -2
  20. package/examples/gbc/templates/puzzle.c +3 -2
  21. package/examples/gbc/templates/racing.c +3 -2
  22. package/examples/gbc/templates/shmup.c +3 -2
  23. package/examples/gbc/templates/sports.c +3 -2
  24. package/examples/gbc/templates/tile_engine.c +3 -2
  25. package/examples/genesis/main.s +53 -1
  26. package/examples/genesis/templates/hello_sprite.c +25 -3
  27. package/examples/genesis/templates/shmup_2p.c +31 -0
  28. package/examples/genesis/templates/xgm2_demo.c +20 -0
  29. package/examples/gg/templates/hello_sprite.c +25 -2
  30. package/examples/gg/templates/music_demo.c +24 -2
  31. package/examples/gg/templates/racing.c +7 -4
  32. package/examples/gg/templates/sports.c +11 -13
  33. package/examples/gg/templates/tile_engine.c +12 -6
  34. package/examples/lynx/templates/hello_sprite.c +15 -1
  35. package/examples/lynx/templates/music_demo.c +13 -1
  36. package/examples/nes/templates/hello_sprite.c +35 -0
  37. package/examples/nes/templates/music_demo.c +40 -0
  38. package/examples/pce/catch_game/main.c +22 -3
  39. package/examples/pce/music_sfx/main.c +28 -1
  40. package/examples/pce/sprite_move/main.c +7 -2
  41. package/examples/sms/templates/hello_sprite.c +29 -3
  42. package/examples/sms/templates/music_demo.c +18 -4
  43. package/examples/sms/templates/shmup_2p.c +24 -1
  44. package/examples/sms/templates/sports.c +4 -2
  45. package/examples/snes/main.asm +108 -17
  46. package/examples/snes/templates/c-hello-data.asm +23 -0
  47. package/examples/snes/templates/c-hello.c +18 -1
  48. package/examples/snes/templates/hello_sprite-data.asm +23 -0
  49. package/examples/snes/templates/hello_sprite.c +17 -1
  50. package/examples/snes/templates/music_demo-data.asm +23 -0
  51. package/examples/snes/templates/music_demo.c +22 -4
  52. package/examples/snes/templates/platformer.c +4 -1
  53. package/examples/snes/templates/puzzle.c +4 -1
  54. package/package.json +1 -1
  55. package/src/cheats/gamegenie.js +0 -1
  56. package/src/cli/smoke.js +1 -3
  57. package/src/host/LibretroHost.js +69 -15
  58. package/src/host/chafa-render.js +2 -0
  59. package/src/host/dsp-state.js +2 -2
  60. package/src/host/gpgx-state.js +4 -0
  61. package/src/http/routes.js +1 -1
  62. package/src/mcp/server.js +1 -1
  63. package/src/mcp/state.js +36 -0
  64. package/src/mcp/tools/address-to-symbol.js +0 -1
  65. package/src/mcp/tools/art-loaders.js +1 -1
  66. package/src/mcp/tools/cart-parts.js +0 -1
  67. package/src/mcp/tools/classify-region.js +1 -1
  68. package/src/mcp/tools/diff-roms.js +1 -1
  69. package/src/mcp/tools/disasm-rebuild.js +1 -1
  70. package/src/mcp/tools/disasm.js +2 -3
  71. package/src/mcp/tools/find-references.js +1 -2
  72. package/src/mcp/tools/font-map.js +1 -1
  73. package/src/mcp/tools/index.js +0 -49
  74. package/src/mcp/tools/input-layout.js +0 -1
  75. package/src/mcp/tools/input.js +33 -3
  76. package/src/mcp/tools/lifecycle.js +14 -2
  77. package/src/mcp/tools/lospec.js +0 -19
  78. package/src/mcp/tools/platform-docs.js +1 -1
  79. package/src/mcp/tools/platform-tools.js +4 -4
  80. package/src/mcp/tools/project.js +0 -2
  81. package/src/mcp/tools/reinject.js +0 -1
  82. package/src/mcp/tools/rom-id.js +2 -2
  83. package/src/mcp/tools/snippets.js +2 -2
  84. package/src/mcp/tools/sprite-pipeline.js +1 -2
  85. package/src/mcp/tools/tile-inspect.js +1 -1
  86. package/src/mcp/tools/toolchain.js +29 -9
  87. package/src/mcp/tools/watch-memory.js +13 -3
  88. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
  89. package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
  90. package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
  91. package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
  92. package/src/platforms/c64/d64.js +0 -1
  93. package/src/platforms/c64/sid.js +0 -2
  94. package/src/platforms/common/metasprite-adapters.js +1 -1
  95. package/src/platforms/common/metasprite-codegen.js +3 -3
  96. package/src/platforms/common/registers.js +5 -3
  97. package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
  98. package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
  99. package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
  100. package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
  101. package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
  102. package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
  103. package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
  104. package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
  105. package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
  106. package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
  107. package/src/platforms/nes/image-to-tilemap.js +3 -0
  108. package/src/platforms/nes/lib/asm/famitone2.s +5 -1
  109. package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
  110. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  111. package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
  112. package/src/platforms/snes/brr.js +0 -2
  113. package/src/playtest/playtest.js +0 -7
  114. package/src/toolchains/asar/asar.js +0 -9
  115. package/src/toolchains/assemble-snippet.js +30 -12
  116. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
  117. package/src/toolchains/common/reassemble.js +0 -1
  118. package/src/toolchains/common/sdk-cache.js +1 -1
  119. package/src/toolchains/genesis-c/genesis-c.js +5 -3
  120. package/src/toolchains/index.js +27 -3
  121. package/src/toolchains/parse-errors.js +78 -1
  122. package/src/toolchains/sdcc/preflight-lint.js +5 -1
  123. package/src/toolchains/sdcc/sdcc.js +1 -1
  124. package/src/toolchains/sjasm/sjasm.js +1 -1
  125. package/src/toolchains/snes-c/snes-c.js +2 -2
  126. package/src/toolchains/vasm68k/vasm68k.js +2 -4
  127. package/src/toolchains/wladx/wladx.js +1 -1
@@ -2,32 +2,49 @@
2
2
  ;
3
3
  ; What this does:
4
4
  ; 1. Standard SNES reset (sei, clc xce → native mode, stack at $1FFF).
5
- ; 2. Uploads a single CGRAM color (bright blue) so you see a colored
6
- ; backdrop proof your build is running. NOT just a black screen.
7
- ; 3. Turns on the screen at full brightness, then parks forever.
5
+ ; 2. Uploads a 4-colour palette + two 2bpp tiles to VRAM via DMA.
6
+ ; 3. Fills a 32x32 BG1 tilemap with a checkerboard of those two tiles
7
+ ; so the whole screen shows a real tiled pattern — NOT a flat one-
8
+ ; colour backdrop (which reads as "blank" to a human / the verifier).
9
+ ; 4. Points BG1 at the tile + map bases, enables BG1, turns the screen
10
+ ; on at full brightness, then parks forever.
8
11
  ;
9
- ; SUFFICIENT FOR: confirming your toolchain works + your user has
10
- ; something to see in playtest. To go further (load tiles, set up BGs,
11
- ; sprites, sound, vblank handler) see src/platforms/snes/lib/* snippets:
12
+ ; SUFFICIENT FOR: confirming your toolchain works AND showing your user a
13
+ ; real rendered screen in playtest. To go further (sprites, scrolling,
14
+ ; sound, a vblank handler) see src/platforms/snes/lib/* snippets:
12
15
  ;
13
16
  ; listStarterSnippets({platform:"snes"})
14
17
  ;
15
18
  ; The key snippets are:
16
19
  ; - lorom_header.asm: full SNES header (this scaffold's is minimal)
17
20
  ; - cgram_upload.asm: palette upload boilerplate
18
- ; - vram_dma_upload.asm: fast tile/map uploads
21
+ ; - vram_dma_upload.asm: fast tile/map uploads (used below)
19
22
  ; - oam_upload.asm: sprite table writes
20
23
  ; - nmi_safe.asm: vblank handler skeleton
21
24
  ;
22
25
  ; BUILD: complete LoROM image, no extra options needed.
23
- ; build({ output: "rom", platform: "snes", source: /* this file */ });
26
+ ; build({ output: "run", platform: "snes", source: /* this file */ });
24
27
 
25
28
  lorom
26
29
 
30
+ ; ── PPU / DMA registers ────────────────────────────────────────────────
27
31
  INIDISP = $2100 ; screen brightness / forced blank
32
+ BGMODE = $2105 ; BG mode + tile-size
33
+ BG1SC = $2107 ; BG1 tilemap base + size
34
+ BG12NBA = $210B ; BG1/BG2 character (tile) base
35
+ TM = $212C ; main-screen layer enable
28
36
  NMITIMEN = $4200
29
37
  CGADD = $2121
30
38
  CGDATA = $2122
39
+ VMAIN = $2115 ; VRAM address increment mode
40
+ VMADDL = $2116 ; VRAM word address (16-bit)
41
+ VMDATAL = $2118 ; VRAM data port (low)
42
+ DMAP0 = $4300 ; DMA0 control
43
+ BBAD0 = $4301 ; DMA0 B-bus address
44
+ A1T0L = $4302 ; DMA0 source address (16-bit)
45
+ A1B0 = $4304 ; DMA0 source bank
46
+ DAS0L = $4305 ; DMA0 byte count (16-bit)
47
+ MDMAEN = $420B ; DMA enable
31
48
 
32
49
  ; -----------------------------------------------------------------------
33
50
  org $008000
@@ -40,29 +57,103 @@ START:
40
57
  txs ; stack at $1FFF
41
58
  sep #$20 ; A = 8-bit
42
59
 
43
- ; Blank screen during init.
60
+ ; Blank screen during init; disable NMI/HDMA.
44
61
  lda #$80
45
62
  sta INIDISP
46
-
47
- ; Disable interrupts.
48
63
  stz NMITIMEN
49
64
 
50
- ; Upload one color to CGRAM[0] (the backdrop / transparency color).
51
- ; BGR-555 little-endian. $7C00 = bright blue.
65
+ ; ── Palette CGRAM ───────────────────────────────────────────
66
+ ; 4 colours (2bpp): 0 = blue backdrop, 1 = white, 2 = green,
67
+ ; 3 = magenta. BGR-555, little-endian (low byte then high byte).
52
68
  stz CGADD
53
69
  lda #$00
54
- sta CGDATA
70
+ sta CGDATA ; colour 0 low ($7C00 = blue)
71
+ lda #$7C
72
+ sta CGDATA ; colour 0 high
73
+ lda #$FF
74
+ sta CGDATA ; colour 1 low ($7FFF = white)
75
+ lda #$7F
76
+ sta CGDATA ; colour 1 high
77
+ lda #$E0
78
+ sta CGDATA ; colour 2 low ($03E0 = green)
79
+ lda #$03
80
+ sta CGDATA ; colour 2 high
81
+ lda #$1F
82
+ sta CGDATA ; colour 3 low ($7C1F = magenta)
55
83
  lda #$7C
56
- sta CGDATA
84
+ sta CGDATA ; colour 3 high
85
+
86
+ ; ── Tile CHR → VRAM word $0000 (DMA channel 0) ────────────────
87
+ ; Two 8x8 2bpp tiles = 32 bytes. VMAIN $80 = +1 word after the
88
+ ; high-byte write; B-bus $18 = VMDATAL (auto-alternates to $2119).
89
+ ldx #$0000
90
+ stx VMADDL
91
+ lda #$80
92
+ sta VMAIN
93
+ lda #$01
94
+ sta DMAP0 ; DMA mode 1 (2 regs, word transfers)
95
+ lda #$18
96
+ sta BBAD0 ; → $2118 / $2119
97
+ ldx #TILES
98
+ stx A1T0L
99
+ lda #TILES>>16
100
+ sta A1B0
101
+ ldx #(TILES_END-TILES)
102
+ stx DAS0L ; byte count
103
+ lda #$01
104
+ sta MDMAEN ; fire channel 0
105
+
106
+ ; ── Fill BG1 tilemap → VRAM word $0400 (byte $0800) ───────────
107
+ ; A 32x32 map = 1024 entries. Each entry is a word: low byte =
108
+ ; tile index, high byte = attributes (0). We write a checkerboard
109
+ ; of tile 0 / tile 1 directly through the data port (no source
110
+ ; buffer needed). VMAIN already = +1 word per write.
111
+ ldx #$0400
112
+ stx VMADDL
113
+ rep #$20 ; A = 16-bit for word writes
114
+ ldy #$0000 ; entry counter (0..1023)
115
+ .maploop:
116
+ tya
117
+ and #$0001 ; checker by column parity
118
+ sta VMDATAL ; entry = tile 0 or tile 1
119
+ iny
120
+ cpy #1024
121
+ bne .maploop
122
+ sep #$20 ; back to 8-bit A
57
123
 
58
- ; Enable screen at full brightness (bit 7 = 0 to disable forced
59
- ; blank; low nybble = brightness 0..15).
124
+ ; ── BG1 base registers ────────────────────────────────────────
125
+ stz BGMODE ; mode 0, 8x8 tiles
126
+ ; BG1SC: bits 2-7 = tilemap base in $0400-word units, bits 0-1 =
127
+ ; size (00 = 32x32). Map is at word $0400 → base = 1 → ($01<<2)=$04.
128
+ lda #$04
129
+ sta BG1SC
130
+ stz BG12NBA ; BG1 char base = word $0000 (our tiles)
131
+ lda #$01
132
+ sta TM ; enable BG1 on the main screen
133
+
134
+ ; ── Screen ON at full brightness ──────────────────────────────
60
135
  lda #$0F
61
136
  sta INIDISP
62
137
 
63
138
  LOOP:
64
139
  bra LOOP
65
140
 
141
+ ; -----------------------------------------------------------------------
142
+ ; Tile CHR: two 8x8 2bpp tiles (16 bytes each). 2bpp = 2 bitplanes
143
+ ; interleaved per row: byte0=row0 plane0, byte1=row0 plane1, ...
144
+ ;
145
+ ; Tile 0 — solid colour 1 (plane0 all set, plane1 clear → colour 1).
146
+ ; Tile 1 — checker of colour 2 / colour 3 (both planes patterned) so the
147
+ ; map's alternating tiles give a busy, multi-colour screen.
148
+ TILES:
149
+ ; tile 0 (solid: every pixel colour 1)
150
+ db $FF, $00, $FF, $00, $FF, $00, $FF, $00
151
+ db $FF, $00, $FF, $00, $FF, $00, $FF, $00
152
+ ; tile 1 (checkerboard: colour 2 / colour 3 alternating per pixel)
153
+ db $AA, $55, $55, $AA, $AA, $55, $55, $AA
154
+ db $AA, $55, $55, $AA, $AA, $55, $55, $AA
155
+ TILES_END:
156
+
66
157
  ; -----------------------------------------------------------------------
67
158
  ; Emulation-mode reset vector (used at boot, before `xce` flips to native).
68
159
  ; asar's `lorom` directive handles header + rest of vector table padding.
@@ -320,4 +320,27 @@ palfont:
320
320
  .db $00, $00, $00, $00, $00, $00, $00, $00
321
321
  .db $00, $00
322
322
 
323
+
324
+ ; ── Background wallpaper (one 8x8 4bpp tile, 4 solid colour quadrants) ──
325
+ ; Tiled across BG1 it paints the whole screen in four muted colours so the
326
+ ; backdrop never reads as flat/blank. Quadrant->colour: TL=1, TR=2, BL=3,
327
+ ; BR=4. 4bpp plane order: bytes 0-15 = rows 0-7 plane0/plane1 pairs, bytes
328
+ ; 16-31 = rows 0-7 plane2/plane3 pairs.
329
+ tilbg:
330
+ .db $F0, $0F, $F0, $0F, $F0, $0F, $F0, $0F ; rows 0-3: p0=left p1=right
331
+ .db $F0, $F0, $F0, $F0, $F0, $F0, $F0, $F0 ; rows 4-7: p0+p1 = left
332
+ .db $00, $00, $00, $00, $00, $00, $00, $00 ; rows 0-3: p2/p3 = 0
333
+ .db $0F, $00, $0F, $00, $0F, $00, $0F, $00 ; rows 4-7: p2 = right
334
+
335
+ palbg:
336
+ ; 16-colour BG palette; only 1-4 used (the four wallpaper quadrant tones).
337
+ .db $00, $00 ; 0 unused (BG fully opaque)
338
+ .db $C4, $30 ; 1 dark blue
339
+ .db $42, $29 ; 2 dark teal
340
+ .db $88, $30 ; 3 dark purple
341
+ .db $C6, $24 ; 4 dark slate
342
+ .db $00, $00, $00, $00, $00, $00, $00, $00
343
+ .db $00, $00, $00, $00, $00, $00, $00, $00
344
+ .db $00, $00, $00, $00, $00, $00
345
+
323
346
  .ends
@@ -27,13 +27,20 @@
27
27
  #include <snes.h>
28
28
 
29
29
  extern char tilfont, palfont;
30
+ extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
30
31
 
31
32
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
32
33
  * It has no public prototype in console.h, so declare it here. Call it
33
34
  * once per frame (after WaitForVBlank) or via nmiSet(consoleVblank). */
34
35
  extern void consoleVblank(void);
35
36
 
37
+ /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
38
+ * screen never reads as a flat/blank backdrop. Filled at runtime. */
39
+ static u16 bg_map[32 * 32];
40
+
36
41
  int main(void) {
42
+ u16 i;
43
+
37
44
  /* ── 1. PVSnesLib text-mode setup ─────────────────────────────
38
45
  * Map + tile-data + palette-offset addresses are conventions —
39
46
  * any free VRAM region will work, these match PVSnesLib's
@@ -54,7 +61,17 @@ int main(void) {
54
61
  setMode(BG_MODE1, 0);
55
62
  bgSetGfxPtr(0, 0x3000);
56
63
  bgSetMapPtr(0, 0x6800, SC_32x32);
57
- bgSetDisable(1);
64
+
65
+ /* BG1 = full-screen wallpaper so the screen never reads as blank.
66
+ * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of the console gfx
67
+ * $3000 / map $6800). Map entries use palette block 1 (0x0400) so the
68
+ * wallpaper palette doesn't disturb the console font palette in block 0
69
+ * (HUD text stays legible). */
70
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
71
+ 32, 32, BG_16COLORS, 0x2000);
72
+ for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
73
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
74
+ bgSetEnable(1);
58
75
  bgSetDisable(2);
59
76
 
60
77
  /* ── 3. Draw text ─────────────────────────────────────────────
@@ -340,4 +340,27 @@ palsprite:
340
340
  .db $00, $00, $00, $00, $00, $00, $00, $00
341
341
  .db $00, $00, $00, $00, $00, $00, $00, $00
342
342
 
343
+
344
+ ; ── Background wallpaper (one 8x8 4bpp tile, 4 solid colour quadrants) ──
345
+ ; Tiled across BG1 it paints the whole screen in four muted colours so the
346
+ ; backdrop never reads as flat/blank. Quadrant->colour: TL=1, TR=2, BL=3,
347
+ ; BR=4. 4bpp plane order: bytes 0-15 = rows 0-7 plane0/plane1 pairs, bytes
348
+ ; 16-31 = rows 0-7 plane2/plane3 pairs.
349
+ tilbg:
350
+ .db $F0, $0F, $F0, $0F, $F0, $0F, $F0, $0F ; rows 0-3: p0=left p1=right
351
+ .db $F0, $F0, $F0, $F0, $F0, $F0, $F0, $F0 ; rows 4-7: p0+p1 = left
352
+ .db $00, $00, $00, $00, $00, $00, $00, $00 ; rows 0-3: p2/p3 = 0
353
+ .db $0F, $00, $0F, $00, $0F, $00, $0F, $00 ; rows 4-7: p2 = right
354
+
355
+ palbg:
356
+ ; 16-colour BG palette; only 1-4 used (the four wallpaper quadrant tones).
357
+ .db $00, $00 ; 0 unused (BG fully opaque)
358
+ .db $C4, $30 ; 1 dark blue
359
+ .db $42, $29 ; 2 dark teal
360
+ .db $88, $30 ; 3 dark purple
361
+ .db $C6, $24 ; 4 dark slate
362
+ .db $00, $00, $00, $00, $00, $00, $00, $00
363
+ .db $00, $00, $00, $00, $00, $00, $00, $00
364
+ .db $00, $00, $00, $00, $00, $00
365
+
343
366
  .ends
@@ -24,14 +24,20 @@
24
24
 
25
25
  extern char tilfont, palfont;
26
26
  extern char tilsprite, palsprite;
27
+ extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
27
28
 
28
29
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
29
30
  * No public prototype in console.h, so declare it; call once per frame. */
30
31
  extern void consoleVblank(void);
31
32
 
33
+ /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
34
+ * screen never reads as a flat/blank backdrop. Filled at runtime. */
35
+ static u16 bg_map[32 * 32];
36
+
32
37
  int main(void) {
33
38
  u16 x = 120, y = 100;
34
39
  u16 prev = 0;
40
+ u16 i;
35
41
 
36
42
  /* Text setup — same layout as c-hello. */
37
43
  consoleSetTextMapPtr(0x6800);
@@ -44,7 +50,17 @@ int main(void) {
44
50
  * registers — point BG0 at the same font ($3000) + map ($6800). */
45
51
  bgSetGfxPtr(0, 0x3000);
46
52
  bgSetMapPtr(0, 0x6800, SC_32x32);
47
- bgSetDisable(1);
53
+
54
+ /* BG1 = full-screen wallpaper so the screen never reads as blank.
55
+ * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
56
+ * the console gfx $3000 / map $6800). Map entries use palette block 1
57
+ * (0x0400) so the wallpaper palette doesn't disturb the console font
58
+ * palette in block 0 (status text stays legible). */
59
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
60
+ 32, 32, BG_16COLORS, 0x2000);
61
+ for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
62
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
63
+ bgSetEnable(1);
48
64
  bgSetDisable(2);
49
65
 
50
66
  /* OAM init — sprite tiles at VRAM $0000, 8x8 default. */
@@ -315,4 +315,27 @@ palfont:
315
315
  .db $00, $00, $00, $00, $00, $00, $00, $00
316
316
  .db $00, $00
317
317
 
318
+
319
+ ; ── Background wallpaper (one 8x8 4bpp tile, 4 solid colour quadrants) ──
320
+ ; Tiled across BG1 it paints the whole screen in four muted colours so the
321
+ ; backdrop never reads as flat/blank. Quadrant->colour: TL=1, TR=2, BL=3,
322
+ ; BR=4. 4bpp plane order: bytes 0-15 = rows 0-7 plane0/plane1 pairs, bytes
323
+ ; 16-31 = rows 0-7 plane2/plane3 pairs.
324
+ tilbg:
325
+ .db $F0, $0F, $F0, $0F, $F0, $0F, $F0, $0F ; rows 0-3: p0=left p1=right
326
+ .db $F0, $F0, $F0, $F0, $F0, $F0, $F0, $F0 ; rows 4-7: p0+p1 = left
327
+ .db $00, $00, $00, $00, $00, $00, $00, $00 ; rows 0-3: p2/p3 = 0
328
+ .db $0F, $00, $0F, $00, $0F, $00, $0F, $00 ; rows 4-7: p2 = right
329
+
330
+ palbg:
331
+ ; 16-colour BG palette; only 1-4 used (the four wallpaper quadrant tones).
332
+ .db $00, $00 ; 0 unused (BG fully opaque)
333
+ .db $C4, $30 ; 1 dark blue
334
+ .db $42, $29 ; 2 dark teal
335
+ .db $88, $30 ; 3 dark purple
336
+ .db $C6, $24 ; 4 dark slate
337
+ .db $00, $00, $00, $00, $00, $00, $00, $00
338
+ .db $00, $00, $00, $00, $00, $00, $00, $00
339
+ .db $00, $00, $00, $00, $00, $00
340
+
318
341
  .ends
@@ -26,16 +26,22 @@
26
26
  #include "snes_sfx.c"
27
27
 
28
28
  extern char tilfont, palfont;
29
+ extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
29
30
 
30
31
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
31
32
  * No public prototype in console.h, so declare it; call once per frame. */
32
33
  extern void consoleVblank(void);
33
34
 
35
+ /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
36
+ * screen never reads as a flat/blank backdrop. Filled at runtime. */
37
+ static u16 bg_map[32 * 32];
38
+
34
39
  int main(void) {
35
40
  u16 pad;
36
41
  u16 prev = 0;
37
42
  u16 frame = 0;
38
43
  u8 music_running;
44
+ u16 i;
39
45
 
40
46
  /* ── Text-mode setup (PVSnesLib convention) ─────────────────── */
41
47
  consoleSetTextMapPtr(0x6800);
@@ -47,11 +53,18 @@ int main(void) {
47
53
  * registers — point BG0 at the same font ($3000) + map ($6800). */
48
54
  bgSetGfxPtr(0, 0x3000);
49
55
  bgSetMapPtr(0, 0x6800, SC_32x32);
50
- bgSetDisable(1);
51
- bgSetDisable(2);
52
56
 
53
- /* ── Upload SPC driver + sample bank + song table to ARAM ──── */
54
- sfx_init();
57
+ /* BG1 = full-screen wallpaper so the screen never reads as blank.
58
+ * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of the console gfx
59
+ * $3000 / map $6800). Map entries use palette block 1 (0x0400) so the
60
+ * wallpaper palette doesn't disturb the console font palette in block 0
61
+ * (HUD text stays legible). */
62
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
63
+ 32, 32, BG_16COLORS, 0x2000);
64
+ for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
65
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
66
+ bgSetEnable(1);
67
+ bgSetDisable(2);
55
68
 
56
69
  consoleDrawText( 8, 6, "SNES MUSIC DEMO");
57
70
  consoleDrawText( 3, 11, "B = SHOOT SFX");
@@ -60,6 +73,11 @@ int main(void) {
60
73
 
61
74
  setScreenOn();
62
75
 
76
+ /* Upload SPC driver + sample bank + song table to ARAM. sfx_init() must
77
+ * run AFTER setScreenOn() (snes_sfx.h:63) — if the SPC stalls before the
78
+ * screen is on you get a black/forced-blank screen forever. */
79
+ sfx_init();
80
+
63
81
  /* Auto-start music. */
64
82
  sfx_music_play();
65
83
  music_running = 1;
@@ -107,10 +107,13 @@ int main(void) {
107
107
  consoleDrawText( 9, 12, "|64");
108
108
  consoleDrawText(17, 12, "|128");
109
109
  consoleDrawText(25, 12, "|192");
110
- sfx_init();
111
110
 
112
111
  oamSet(0, 32, 100, 3, 0, 0, 0, 0);
112
+ /* Screen ON first, THEN sound. sfx_init() must run AFTER setScreenOn()
113
+ * (snes_sfx.h:63) — if the SPC stalls before the screen is on you get a
114
+ * black/forced-blank screen forever. */
113
115
  setScreenOn();
116
+ sfx_init();
114
117
 
115
118
  while (1) {
116
119
  pad = padsCurrent(0);
@@ -176,10 +176,13 @@ int main(void) {
176
176
 
177
177
  consoleDrawText(14, 2, "SCORE");
178
178
  consoleDrawText(2, 26, "LR MOVE A ROT START DROP");
179
- sfx_init();
180
179
  draw_grid();
181
180
 
181
+ /* Screen ON first, THEN sound. sfx_init() must run AFTER setScreenOn()
182
+ * (snes_sfx.h:63) — if the SPC stalls before the screen is on you get a
183
+ * black/forced-blank screen forever. */
182
184
  setScreenOn();
185
+ sfx_init();
183
186
 
184
187
  while (1) {
185
188
  pad = padsCurrent(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "romdevtools",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 14 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
@@ -446,7 +446,6 @@ export function decodeWithDevice(code, platform) {
446
446
 
447
447
  /** Format a raw ADDR:VAL[:COMPARE] code from decoded parts (hex, no 0x). */
448
448
  export function encodeRaw({ address, value, compare }) {
449
- const h = (n, w) => (n & ((1 << (4 * w)) - 1) >>> 0).toString(16).toUpperCase().padStart(w, "0");
450
449
  const addrHex = (address >>> 0).toString(16).toUpperCase();
451
450
  const valHex = (value & 0xFF).toString(16).toUpperCase().padStart(2, "0");
452
451
  return compare != null
package/src/cli/smoke.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // temp file and loaded into the matching libretro core. Platform is
15
15
  // inferred from the file extension if not given.
16
16
 
17
- import { writeFile, mkdtemp, readdir } from "node:fs/promises";
17
+ import { writeFile, mkdtemp } from "node:fs/promises";
18
18
  import { existsSync, statSync } from "node:fs";
19
19
  import { tmpdir } from "node:os";
20
20
  import path from "node:path";
@@ -108,12 +108,10 @@ async function playCommand(romPath, opts) {
108
108
 
109
109
  // Extract zip if needed
110
110
  let actualPath = romPath;
111
- let extracted = false;
112
111
  if (path.extname(romPath).toLowerCase() === ".zip") {
113
112
  console.error(`unzipping ${path.basename(romPath)}...`);
114
113
  const { tempPath, originalName } = await extractFirstRomFromZip(romPath);
115
114
  actualPath = tempPath;
116
- extracted = true;
117
115
  console.error(` → ${originalName} (${statSync(tempPath).size} bytes)`);
118
116
  }
119
117
 
@@ -301,7 +301,20 @@ export class LibretroHost {
301
301
  mod._free(infoPtr);
302
302
 
303
303
  if (!ok) {
304
- throw new Error(`retro_load_game failed for ${mediaPath}`);
304
+ // This is the failure path for EVERY bad/wrong-platform/corrupt/
305
+ // unsupported-mapper image — the most common loadMedia failure. The core
306
+ // returns a bare false, so name the likely causes + the exact checks
307
+ // rather than leaving the agent with "failed".
308
+ throw new Error(
309
+ `The '${platform}' core REFUSED this ${mediaKind || "media"} ` +
310
+ `(${data.length} bytes${ext ? `, ${ext}` : ""}, path ${mediaPath}). ` +
311
+ `retro_load_game returned false — the bytes reached the core but it would not accept them. ` +
312
+ `Common causes: (1) wrong platform for this file (a GB ROM loaded as 'nes', etc.) — ` +
313
+ `confirm the platform matches the file; (2) a corrupt or TRUNCATED image — re-check the byte length; ` +
314
+ `(3) an unsupported mapper/board or a missing/!bad header. ` +
315
+ `Inspect the file with cart({op:'identify'}) to see what platform/mapper it really is, ` +
316
+ `then load with the matching platform.`,
317
+ );
305
318
  }
306
319
 
307
320
  this.status.platform = platform;
@@ -436,7 +449,7 @@ export class LibretroHost {
436
449
  */
437
450
  stepFrames(n) {
438
451
  const mod = this._needMod();
439
- if (!this.status.loaded) throw new Error("no media loaded");
452
+ this._needMedia();
440
453
  if (this.status.paused) return 0;
441
454
  for (let i = 0; i < n; i++) {
442
455
  mod._retro_run();
@@ -455,7 +468,7 @@ export class LibretroHost {
455
468
  * Returns the frame count after. */
456
469
  renderOneFrame() {
457
470
  const mod = this._needMod();
458
- if (!this.status.loaded) throw new Error("no media loaded");
471
+ this._needMedia();
459
472
  mod._retro_run();
460
473
  this.status.frameCount++;
461
474
  if (this.state.lastFrame) {
@@ -594,7 +607,7 @@ export class LibretroHost {
594
607
  /** @param {string} name */
595
608
  loadState(name) {
596
609
  const snapshot = this.namedStates.get(name);
597
- if (!snapshot) throw new Error(`no save state named '${name}'`);
610
+ if (!snapshot) throw new Error(this._noStateError(name));
598
611
  return this.unserializeState(snapshot); // returns # cheats cleared
599
612
  }
600
613
 
@@ -641,10 +654,21 @@ export class LibretroHost {
641
654
  * disturbing the live host). Throws if the slot doesn't exist. */
642
655
  getStateBlob(name) {
643
656
  const blob = this.namedStates.get(name);
644
- if (!blob) throw new Error(`no save state named '${name}'`);
657
+ if (!blob) throw new Error(this._noStateError(name));
645
658
  return blob;
646
659
  }
647
660
 
661
+ /** Build a "no save state named X" error that lists the slots that DO exist
662
+ * (or says there are none) and names the op to create one. */
663
+ _noStateError(name) {
664
+ const names = [...this.namedStates.keys()];
665
+ return names.length
666
+ ? `No save state named '${name}'. Existing in-memory slots: ${names.map((n) => `'${n}'`).join(", ")}. ` +
667
+ `(List them with state({op:'list'}); create one with state({op:'save', name}).)`
668
+ : `No save state named '${name}' — this session has NO in-memory save slots yet. ` +
669
+ `Create one with state({op:'save', name:'${name}'}) first (or load from disk with state({op:'load', path})).`;
670
+ }
671
+
648
672
  /**
649
673
  * @param {import("./types.js").MemoryRegion} region
650
674
  * @param {number} offset
@@ -667,7 +691,7 @@ export class LibretroHost {
667
691
  readMemory(region, offset, length) {
668
692
  const mod = this._needMod();
669
693
  const id = MemoryRegionToRetro[region];
670
- if (id === undefined) throw new Error(`unknown memory region '${region}'`);
694
+ if (id === undefined) throw new Error(this._unknownRegionError(region));
671
695
  const ptr = mod._retro_get_memory_data(id);
672
696
  const size = mod._retro_get_memory_size(id);
673
697
  if (!ptr || !size) throw new Error(this._emptyRegionError(region));
@@ -685,7 +709,7 @@ export class LibretroHost {
685
709
  writeMemory(region, offset, bytes) {
686
710
  const mod = this._needMod();
687
711
  const id = MemoryRegionToRetro[region];
688
- if (id === undefined) throw new Error(`unknown memory region '${region}'`);
712
+ if (id === undefined) throw new Error(this._unknownRegionError(region));
689
713
  const ptr = mod._retro_get_memory_data(id);
690
714
  const size = mod._retro_get_memory_size(id);
691
715
  if (!ptr || !size) throw new Error(this._emptyRegionError(region));
@@ -1058,7 +1082,7 @@ export class LibretroHost {
1058
1082
 
1059
1083
  runUntilPC(address, maxFrames = 600) {
1060
1084
  this._needMod();
1061
- if (!this.status.loaded) throw new Error("no media loaded");
1085
+ this._needMedia();
1062
1086
  if (!this.pcBreakSupported()) {
1063
1087
  throw new Error("PC breakpoint not supported by this core (Genesis today; other cores as patched).");
1064
1088
  }
@@ -1090,7 +1114,7 @@ export class LibretroHost {
1090
1114
  */
1091
1115
  runUntilRead(address, maxFrames = 600) {
1092
1116
  this._needMod();
1093
- if (!this.status.loaded) throw new Error("no media loaded");
1117
+ this._needMedia();
1094
1118
  if (!this.readWatchSupported()) {
1095
1119
  throw new Error("read watchpoint not supported by this core (Genesis today; other cores as patched).");
1096
1120
  }
@@ -1122,7 +1146,7 @@ export class LibretroHost {
1122
1146
  */
1123
1147
  stepInstruction() {
1124
1148
  this._needMod();
1125
- if (!this.status.loaded) throw new Error("no media loaded");
1149
+ this._needMedia();
1126
1150
  if (!this.pcBreakSupported()) {
1127
1151
  throw new Error("single-step not supported by this core (Genesis today; other cores as patched).");
1128
1152
  }
@@ -1190,8 +1214,8 @@ export class LibretroHost {
1190
1214
  * @param {(host:LibretroHost)=>any} [a.capture] read result from core RAM BEFORE restore
1191
1215
  */
1192
1216
  callSubroutine(a) {
1193
- const mod = this._needMod();
1194
- if (!this.status.loaded) throw new Error("no media loaded");
1217
+ this._needMod();
1218
+ this._needMedia();
1195
1219
  if (!this.setRegSupported()) {
1196
1220
  throw new Error("cpu({op:'call'}) not supported by this core (rebuild with romdev_setreg/romdev_getreg).");
1197
1221
  }
@@ -1427,7 +1451,7 @@ export class LibretroHost {
1427
1451
  */
1428
1452
  watchRange(lo, hi, mode, frames) {
1429
1453
  const mod = this._needMod();
1430
- if (!this.status.loaded) throw new Error("no media loaded");
1454
+ this._needMedia();
1431
1455
  if (!this.rangeWatchSupported()) throw new Error("range watch not supported by this core.");
1432
1456
  const m = mode === "read" ? 1 : mode === "write" ? 2 : 3;
1433
1457
  mod._romdev_range_set(lo >>> 0, hi >>> 0, m, 1);
@@ -1460,7 +1484,7 @@ export class LibretroHost {
1460
1484
  */
1461
1485
  logPCRange(lo, hi, frames) {
1462
1486
  const mod = this._needMod();
1463
- if (!this.status.loaded) throw new Error("no media loaded");
1487
+ this._needMedia();
1464
1488
  if (!this.rangeWatchSupported()) throw new Error("coverage trace not supported by this core.");
1465
1489
  mod._romdev_cov_set(lo >>> 0, hi >>> 0, 1);
1466
1490
  this._runFramesExclusive(() => false, frames);
@@ -1500,7 +1524,7 @@ export class LibretroHost {
1500
1524
  */
1501
1525
  watchDma(frames) {
1502
1526
  const mod = this._needMod();
1503
- if (!this.status.loaded) throw new Error("no media loaded");
1527
+ this._needMedia();
1504
1528
  if (!this.dmaWatchSupported()) throw new Error("VDP-DMA watch not supported by this core (Genesis only).");
1505
1529
  mod._romdev_dmawatch_set(1);
1506
1530
  this._runFramesExclusive(() => false, frames);
@@ -1538,6 +1562,21 @@ export class LibretroHost {
1538
1562
  return this.mod;
1539
1563
  }
1540
1564
 
1565
+ // Guard for every op that needs a loaded game. The bare "no media loaded"
1566
+ // left the agent guessing; this names the fix (loadMedia) and the gotcha
1567
+ // (emulator state is in-memory, so a reconnect/restart drops it). The richer
1568
+ // session-aware recovery (echoing the exact prior loadMedia call) lives at the
1569
+ // tool layer in state.js getHost(); this is the host-level twin.
1570
+ _needMedia() {
1571
+ if (!this.status.loaded) {
1572
+ throw new Error(
1573
+ "No media loaded — call loadMedia({platform, path}) before this op. " +
1574
+ "(If you DID load and hit this after a reconnect/restart, the host's in-memory " +
1575
+ "state didn't survive — re-run loadMedia with your ROM to pick back up.)",
1576
+ );
1577
+ }
1578
+ }
1579
+
1541
1580
  /**
1542
1581
  * Build a friendly error message when a memory region is empty (the
1543
1582
  * core didn't expose it). Includes per-platform suggestions when we
@@ -1548,6 +1587,21 @@ export class LibretroHost {
1548
1587
  * "my VRAM writes are being optimized away" spiral — when in fact
1549
1588
  * gambatte exposes VRAM as `gb_vram`, not the generic id.
1550
1589
  */
1590
+ _unknownRegionError(region) {
1591
+ // A bad region name should never leave the agent guessing — list the valid
1592
+ // ones (the single source of truth, MemoryRegionToRetro) so it can pick the
1593
+ // right one. The cross-platform names (system_ram / video_ram / save_ram)
1594
+ // exist everywhere; the rest are platform-specific.
1595
+ const valid = Object.keys(MemoryRegionToRetro).sort();
1596
+ const common = valid.filter((r) => ["system_ram", "video_ram", "save_ram", "rom"].includes(r));
1597
+ return (
1598
+ `Unknown memory region '${region}'. ` +
1599
+ `Common (most platforms): ${common.join(", ")}. ` +
1600
+ `All registered region names: ${valid.join(", ")}. ` +
1601
+ `(Region availability is per platform — some names only resolve on the platform that has that hardware.)`
1602
+ );
1603
+ }
1604
+
1551
1605
  _emptyRegionError(region) {
1552
1606
  const plat = this.status && this.status.platform;
1553
1607
  // SRAM gets an honest, specific answer: empty save_ram almost always means