romdevtools 0.27.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.
Files changed (144) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +309 -0
  3. package/README.md +1 -1
  4. package/examples/README.md +1 -1
  5. package/examples/atari2600/templates/platformer.asm +18 -9
  6. package/examples/atari2600/templates/racing.asm +25 -4
  7. package/examples/atari2600/templates/shmup.asm +30 -5
  8. package/examples/atari2600/templates/sports.asm +41 -9
  9. package/examples/atari7800/templates/hello_sprite.c +8 -4
  10. package/examples/atari7800/templates/platformer.c +12 -8
  11. package/examples/atari7800/templates/puzzle.c +7 -4
  12. package/examples/atari7800/templates/racing.c +5 -2
  13. package/examples/atari7800/templates/shmup.c +8 -4
  14. package/examples/atari7800/templates/sports.c +6 -3
  15. package/examples/c64/templates/platformer.c +28 -24
  16. package/examples/c64/templates/puzzle.c +77 -16
  17. package/examples/c64/templates/racing.c +9 -0
  18. package/examples/c64/templates/shmup.c +13 -1
  19. package/examples/c64/templates/sports.c +9 -4
  20. package/examples/gb/templates/platformer.c +6 -2
  21. package/examples/gb/templates/puzzle.c +279 -101
  22. package/examples/gb/templates/racing.c +13 -1
  23. package/examples/gb/templates/shmup.c +13 -1
  24. package/examples/gb/templates/sports.c +9 -3
  25. package/examples/gba/templates/platformer.c +7 -13
  26. package/examples/gba/templates/puzzle.c +93 -15
  27. package/examples/gba/templates/racing.c +13 -1
  28. package/examples/gba/templates/shmup.c +13 -1
  29. package/examples/gba/templates/sports.c +17 -5
  30. package/examples/gbc/templates/platformer.c +6 -2
  31. package/examples/gbc/templates/puzzle.c +878 -178
  32. package/examples/gbc/templates/racing.c +13 -1
  33. package/examples/gbc/templates/shmup.c +13 -1
  34. package/examples/gbc/templates/sports.c +9 -3
  35. package/examples/genesis/templates/puzzle.c +76 -15
  36. package/examples/genesis/templates/racing.c +13 -1
  37. package/examples/genesis/templates/shmup_2p.c +13 -1
  38. package/examples/gg/templates/platformer.c +4 -0
  39. package/examples/gg/templates/puzzle.c +80 -14
  40. package/examples/gg/templates/racing.c +17 -1
  41. package/examples/gg/templates/shmup.c +17 -1
  42. package/examples/gg/templates/sports.c +4 -0
  43. package/examples/lynx/templates/platformer.c +25 -6
  44. package/examples/lynx/templates/puzzle.c +77 -14
  45. package/examples/lynx/templates/shmup.c +13 -1
  46. package/examples/lynx/templates/sports.c +5 -2
  47. package/examples/msx/platformer/main.c +2 -0
  48. package/examples/msx/puzzle/main.c +78 -15
  49. package/examples/msx/racing/main.c +1 -0
  50. package/examples/msx/shmup/main.c +1 -0
  51. package/examples/msx/sports/main.c +3 -2
  52. package/examples/nes/templates/platformer.c +11 -3
  53. package/examples/nes/templates/puzzle.c +81 -21
  54. package/examples/nes/templates/racing.c +15 -1
  55. package/examples/nes/templates/shmup.c +1 -0
  56. package/examples/nes/templates/sports.c +1 -0
  57. package/examples/pce/platformer/main.c +3 -1
  58. package/examples/pce/puzzle/main.c +78 -12
  59. package/examples/pce/racing/main.c +1 -0
  60. package/examples/pce/shmup/main.c +5 -4
  61. package/examples/pce/sports/main.c +4 -3
  62. package/examples/sms/templates/platformer.c +4 -0
  63. package/examples/sms/templates/puzzle.c +80 -14
  64. package/examples/sms/templates/racing.c +17 -1
  65. package/examples/sms/templates/shmup.c +17 -1
  66. package/examples/sms/templates/shmup_2p.c +17 -1
  67. package/examples/sms/templates/sports.c +4 -0
  68. package/examples/snes/templates/platformer.c +32 -15
  69. package/examples/snes/templates/puzzle.c +84 -16
  70. package/examples/snes/templates/racing.c +20 -1
  71. package/examples/snes/templates/shmup.c +20 -2
  72. package/examples/snes/templates/sports.c +7 -0
  73. package/package.json +12 -12
  74. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  75. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  76. package/src/cores/wasm/fceumm_libretro.js +1 -1
  77. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  78. package/src/cores/wasm/gambatte_libretro.js +1 -1
  79. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  80. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  81. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  82. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  83. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  84. package/src/cores/wasm/handy_libretro.js +1 -1
  85. package/src/cores/wasm/handy_libretro.wasm +0 -0
  86. package/src/cores/wasm/mgba_libretro.js +1 -1
  87. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  88. package/src/cores/wasm/prosystem_libretro.js +1 -1
  89. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  90. package/src/cores/wasm/snes9x_libretro.js +1 -1
  91. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  92. package/src/cores/wasm/stella2014_libretro.js +1 -1
  93. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  94. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  95. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  96. package/src/host/LibretroHost.js +245 -10
  97. package/src/mcp/server.js +6 -0
  98. package/src/mcp/tools/disasm-rebuild.js +315 -65
  99. package/src/mcp/tools/disasm.js +149 -28
  100. package/src/mcp/tools/find-references.js +216 -51
  101. package/src/mcp/tools/frame.js +11 -4
  102. package/src/mcp/tools/index.js +15 -1
  103. package/src/mcp/tools/input.js +26 -3
  104. package/src/mcp/tools/memory.js +208 -39
  105. package/src/mcp/tools/playtest.js +56 -4
  106. package/src/mcp/tools/project.js +35 -9
  107. package/src/mcp/tools/toolchain.js +43 -10
  108. package/src/mcp/tools/watch-memory.js +141 -24
  109. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  110. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  111. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  112. package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
  113. package/src/platforms/gb/MENTAL_MODEL.md +16 -1
  114. package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
  115. package/src/platforms/gb/lib/c/patch-header.js +7 -4
  116. package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
  117. package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
  118. package/src/platforms/gbc/lib/c/font.h +43 -0
  119. package/src/platforms/gbc/lib/c/patch-header.js +7 -4
  120. package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
  121. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  122. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  123. package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
  124. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  125. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  126. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  127. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  128. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  129. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  130. package/src/platforms/msx/lib/c/msx_hw.h +2 -0
  131. package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
  132. package/src/platforms/nes/MENTAL_MODEL.md +10 -3
  133. package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
  134. package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
  135. package/src/platforms/pce/MENTAL_MODEL.md +9 -0
  136. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  137. package/src/platforms/pce/lib/c/pce_hw.h +2 -1
  138. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  139. package/src/platforms/sms/MENTAL_MODEL.md +5 -0
  140. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  141. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  142. package/src/platforms/snes/MENTAL_MODEL.md +5 -0
  143. package/src/playtest/playtest.js +73 -3
  144. package/src/toolchains/index.js +37 -8
@@ -63,6 +63,13 @@ static const u32 tile_blue[8] = {
63
63
  /* Backdrop tile (colour index 4 = steel grey): a dither so the whole screen
64
64
  * reads as a "cabinet" behind the playfield instead of flat black — a lone
65
65
  * 6x12 grid floating on black looks blank to a human (frame verify <92%). */
66
+ /* Solid light-grey wall tile for the well border. */
67
+ static const u32 tile_wall[8] = {
68
+ 0x55555555, 0x55555555, 0x55555555, 0x55555555,
69
+ 0x55555555, 0x55555555, 0x55555555, 0x55555555,
70
+ };
71
+ #define TILE_WALL 5
72
+
66
73
  static const u32 tile_back[8] = {
67
74
  0x40404040, 0x04040404, 0x40404040, 0x04040404,
68
75
  0x40404040, 0x04040404, 0x40404040, 0x04040404,
@@ -141,26 +148,87 @@ static int collides(int col, int row) {
141
148
  return 0;
142
149
  }
143
150
 
151
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
152
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
153
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
154
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
155
+ * 4 directions, clears them, applies per-column gravity, and loops so
156
+ * cascades chain (score scales with chain depth). */
157
+ static u8 matched[ROWS][COLS];
158
+ static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
159
+
160
+ static u8 mark_and_count(void) {
161
+ u8 r, c, d, len, k, cnt;
162
+ u8 col;
163
+ s8 dr, dc;
164
+ int sr, sc;
165
+ cnt = 0;
166
+ for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
167
+ for (r = 0; r < ROWS; r++) {
168
+ for (c = 0; c < COLS; c++) {
169
+ col = grid[r][c];
170
+ if (col == 0) continue;
171
+ for (d = 0; d < 4; d++) {
172
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
173
+ sr = (int)r - dr; sc = (int)c - dc;
174
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
175
+ && grid[sr][sc] == col) continue; /* not the run's start */
176
+ len = 1;
177
+ sr = (int)r + dr; sc = (int)c + dc;
178
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
179
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
180
+ if (len >= 3) {
181
+ sr = r; sc = c;
182
+ for (k = 0; k < len; k++) {
183
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
184
+ sr += dr; sc += dc;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return cnt;
191
+ }
192
+
193
+ /* collapse each column so survivors rest on the floor (in place: walk
194
+ * from the bottom, copying gems down to a write cursor, then zero above) */
195
+ static void apply_gravity(void) {
196
+ u8 c;
197
+ int r, w;
198
+ for (c = 0; c < COLS; c++) {
199
+ w = ROWS - 1;
200
+ for (r = ROWS - 1; r >= 0; r--) {
201
+ if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
202
+ }
203
+ for (; w >= 0; w--) grid[w][c] = 0;
204
+ }
205
+ }
206
+
207
+ static void resolve_board(void) {
208
+ u8 n, r, c, chain;
209
+ unsigned int amt;
210
+ chain = 0;
211
+ while (1) {
212
+ n = mark_and_count();
213
+ if (n == 0) break;
214
+ chain++;
215
+ for (r = 0; r < ROWS; r++)
216
+ for (c = 0; c < COLS; c++)
217
+ if (matched[r][c]) grid[r][c] = 0;
218
+ amt = (unsigned int)n * 10u;
219
+ if (chain > 1) amt = amt * chain;
220
+ if (score < 65500u) score += amt;
221
+ sfx_tone(1, 1700, 10); /* clear chime */
222
+ apply_gravity();
223
+ }
224
+ }
225
+
144
226
  static void lock_piece(void) {
145
227
  for (int i = 0; i < 3; i++) {
146
228
  int r = piece_y + i;
147
229
  if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
148
230
  }
149
- /* Horizontal triples on the affected rows. */
150
- for (int i = 0; i < 3; i++) {
151
- int r = piece_y + i;
152
- if (r < 0 || r >= ROWS) continue;
153
- for (int c = 0; c <= COLS - 3; c++) {
154
- u8 a = grid[r][c], b = grid[r][c + 1], d = grid[r][c + 2];
155
- if (a != 0 && a == b && b == d) {
156
- grid[r][c] = 0;
157
- grid[r][c + 1] = 0;
158
- grid[r][c + 2] = 0;
159
- if (score < 65500u) score += 30;
160
- sfx_tone(1, 1700, 10); /* triple-clear chime */
161
- }
162
- }
163
- }
231
+ resolve_board();
164
232
  draw_grid();
165
233
  }
166
234
 
@@ -177,18 +245,28 @@ int main(void) {
177
245
  pal_bg_mem[2] = CLR_LIME;
178
246
  pal_bg_mem[3] = CLR_BLUE;
179
247
  pal_bg_mem[4] = RGB15(6, 6, 9); /* steel grey backdrop */
248
+ pal_bg_mem[5] = RGB15(20, 20, 22); /* well border grey */
180
249
 
181
250
  /* BG tile graphics in char-block 3 (separate from TTE which used 2). */
182
251
  tonccpy(&tile_mem[3][TILE_RED], tile_red, sizeof(tile_red));
183
252
  tonccpy(&tile_mem[3][TILE_GREEN], tile_green, sizeof(tile_green));
184
253
  tonccpy(&tile_mem[3][TILE_BLUE], tile_blue, sizeof(tile_blue));
185
254
  tonccpy(&tile_mem[3][TILE_BACK], tile_back, sizeof(tile_back));
255
+ tonccpy(&tile_mem[3][TILE_WALL], tile_wall, sizeof(tile_wall));
186
256
 
187
257
  /* Fill screen-block 28 (BG0 map) with the backdrop tile so the whole
188
258
  * screen is covered; the grid cells draw over it. (A blank/black map left
189
259
  * the playfield floating on black — reads as blank.) */
190
260
  SCR_ENTRY *map = se_mem[28];
191
261
  for (int i = 0; i < 32 * 32; i++) map[i] = SE_BUILD(TILE_BACK, 0, 0, 0);
262
+ /* Well border — playtest: "needs border around play area". One wall
263
+ * cell left/right of the grid columns + a floor row underneath. */
264
+ for (int r = 0; r <= ROWS; r++) {
265
+ map[(GRID_TY + r) * 32 + (GRID_TX - 1)] = SE_BUILD(TILE_WALL, 0, 0, 0);
266
+ map[(GRID_TY + r) * 32 + (GRID_TX + COLS)] = SE_BUILD(TILE_WALL, 0, 0, 0);
267
+ }
268
+ for (int c = -1; c <= COLS; c++)
269
+ map[(GRID_TY + ROWS) * 32 + (GRID_TX + c)] = SE_BUILD(TILE_WALL, 0, 0, 0);
192
270
 
193
271
  REG_BG0CNT = BG_CBB(3) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(0);
194
272
  /* Bump TTE's BG1 to a LOWER priority so the grid (BG0, prio 0) renders
@@ -100,10 +100,22 @@ static void reset_run(void) {
100
100
  game_over_timer = 0;
101
101
  }
102
102
 
103
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
104
+ * The old code derived the spawn column from spawn_timer, but the caller
105
+ * resets spawn_timer just before calling here, so it was CONSTANT and
106
+ * every enemy spawned in the same left column/lane. */
107
+ static u8 rng_state = 0xA5;
108
+ static u8 rand8(void) {
109
+ u8 lsb = (u8)(rng_state & 1);
110
+ rng_state >>= 1;
111
+ if (lsb) rng_state ^= 0xB8;
112
+ return rng_state;
113
+ }
114
+
103
115
  static void spawn_obstacle(void) {
104
116
  for (int i = 0; i < MAX_OBSTACLES; i++) {
105
117
  if (!obstacles[i].alive) {
106
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
118
+ obstacles[i].x = lane_x[rand8() % 3];
107
119
  obstacles[i].y = -8;
108
120
  obstacles[i].alive = 1;
109
121
  return;
@@ -104,11 +104,23 @@ static void fire_bullet(void) {
104
104
  }
105
105
  }
106
106
 
107
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
108
+ * The old code derived the spawn column from spawn_timer, but the caller
109
+ * resets spawn_timer just before calling here, so it was CONSTANT and
110
+ * every enemy spawned in the same left column/lane. */
111
+ static u8 rng_state = 0xA5;
112
+ static u8 rand8(void) {
113
+ u8 lsb = (u8)(rng_state & 1);
114
+ rng_state >>= 1;
115
+ if (lsb) rng_state ^= 0xB8;
116
+ return rng_state;
117
+ }
118
+
107
119
  static void spawn_enemy(void) {
108
120
  for (int i = 0; i < MAX_ENEMIES; i++) {
109
121
  if (!enemies[i].alive) {
110
122
  /* cheap deterministic x scatter */
111
- enemies[i].x = ((spawn_timer * 37) & 0xFF) % (240 - 16) + 8;
123
+ enemies[i].x = rand8() % (240 - 16) + 8;
112
124
  enemies[i].y = -8;
113
125
  enemies[i].alive = 1;
114
126
  return;
@@ -201,11 +201,23 @@ int main(void) {
201
201
 
202
202
  oam_copy(oam_mem, obj_buffer, 7);
203
203
 
204
- /* Score digits. */
205
- tte_erase_rect(28, 2, 36, 14);
206
- tte_printf("#{P:28,2}%d", score_p1 % 10);
207
- tte_erase_rect(220, 2, 228, 14);
208
- tte_printf("#{P:220,2}%d", score_p2 % 10);
204
+ /* Score digits — via tte_write, NOT tte_printf. tte_printf is
205
+ * broken in this libtonc build (GBA-1): it crashes with an
206
+ * undefined-instruction exception, and since this ran EVERY
207
+ * frame the whole game froze on iteration 1 ("game never
208
+ * starts"). racing/puzzle already avoided it the same way. */
209
+ {
210
+ char sb[12];
211
+ sb[0]='#'; sb[1]='{'; sb[2]='P'; sb[3]=':';
212
+ tte_erase_rect(28, 2, 36, 14);
213
+ sb[4]='2'; sb[5]='8'; sb[6]=','; sb[7]='2'; sb[8]='}';
214
+ sb[9] = (char)('0' + (score_p1 % 10)); sb[10] = 0;
215
+ tte_write(sb);
216
+ tte_erase_rect(220, 2, 228, 14);
217
+ tte_write("#{P:220,2}");
218
+ sb[0] = (char)('0' + (score_p2 % 10)); sb[1] = 0;
219
+ tte_write(sb);
220
+ }
209
221
  }
210
222
  return 0;
211
223
  }
@@ -119,10 +119,11 @@ void main(void) {
119
119
  const Rect *p;
120
120
  const int16_t GRAVITY = 10;
121
121
  const int16_t MOVE = 20;
122
- const int16_t JUMP = -180;
122
+ const int16_t JUMP = -140; /* was -180: ~100px peak (most of the screen) — 'jumps a little too high' */
123
123
  const int16_t MAXFALL = 280;
124
124
 
125
125
  lcd_init_default();
126
+ sound_init();
126
127
  enable_vblank_irq(); /* MANDATORY: HALT-driven wait_vblank. Without this,
127
128
  * busy-poll wait_vblank runs ~1/30 speed on the WASM
128
129
  * emulator and the game loop appears to hang. */
@@ -172,7 +173,10 @@ void main(void) {
172
173
  if (pad & PAD_RIGHT) vx = MOVE;
173
174
 
174
175
  grounded = on_platform(ipx, ipy);
175
- if ((pad & PAD_A) && !(prev & PAD_A) && grounded) vy = JUMP;
176
+ if ((pad & PAD_A) && !(prev & PAD_A) && grounded) {
177
+ vy = JUMP;
178
+ sound_play_tone(1, 1750, 8); /* jump blip (ch2 square) */
179
+ }
176
180
  prev = pad;
177
181
 
178
182
  vy += GRAVITY;