romdevtools 0.13.0 → 0.15.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 (124) hide show
  1. package/AGENTS.md +21 -14
  2. package/CHANGELOG.md +125 -1
  3. package/README.md +13 -8
  4. package/examples/atari2600/main.asm +1 -1
  5. package/examples/atari2600/templates/default.asm +1 -1
  6. package/examples/atari2600/templates/paddle.asm +59 -47
  7. package/examples/atari7800/main.c +1 -1
  8. package/examples/atari7800/templates/default.c +1 -1
  9. package/examples/atari7800/templates/music_demo.c +1 -1
  10. package/examples/c64/main.c +1 -1
  11. package/examples/c64/templates/platformer.c +2 -2
  12. package/examples/c64/templates/puzzle.c +1 -1
  13. package/examples/c64/templates/racing.c +3 -3
  14. package/examples/c64/templates/shmup.c +6 -5
  15. package/examples/c64/templates/sports.c +4 -4
  16. package/examples/gb/main.asm +1 -1
  17. package/examples/gb/main.c +1 -1
  18. package/examples/gb/templates/puzzle.c +1 -1
  19. package/examples/gb/templates/racing.c +1 -1
  20. package/examples/gb/templates/shmup.c +1 -1
  21. package/examples/gba/templates/gba_hello.c +1 -1
  22. package/examples/gba/templates/maxmod_demo.c +1 -1
  23. package/examples/gba/templates/puzzle.c +17 -3
  24. package/examples/gba/templates/racing.c +16 -2
  25. package/examples/gba/templates/shmup.c +23 -4
  26. package/examples/gba/templates/tonc_hello.c +6 -4
  27. package/examples/gbc/main.asm +1 -1
  28. package/examples/gbc/templates/puzzle.c +1 -1
  29. package/examples/gbc/templates/racing.c +1 -1
  30. package/examples/gbc/templates/shmup.c +1 -1
  31. package/examples/genesis/main.s +1 -1
  32. package/examples/genesis/templates/puzzle.c +1 -1
  33. package/examples/genesis/templates/racing.c +45 -1
  34. package/examples/genesis/templates/shmup.c +12 -3
  35. package/examples/genesis/templates/shmup_2p.c +2 -2
  36. package/examples/genesis/templates/sports.c +39 -0
  37. package/examples/gg/templates/hello_sprite.c +38 -23
  38. package/examples/gg/templates/music_demo.c +11 -8
  39. package/examples/gg/templates/platformer.c +37 -15
  40. package/examples/gg/templates/racing.c +25 -12
  41. package/examples/gg/templates/shmup.c +12 -6
  42. package/examples/gg/templates/sports.c +30 -16
  43. package/examples/gg/templates/tile_engine.c +24 -10
  44. package/examples/lynx/templates/platformer.c +7 -1
  45. package/examples/lynx/templates/puzzle.c +8 -2
  46. package/examples/lynx/templates/racing.c +7 -1
  47. package/examples/lynx/templates/sports.c +7 -1
  48. package/examples/nes/main.c +2 -2
  49. package/examples/nes/space-shooter/nes_runtime.h +1 -1
  50. package/examples/nes/templates/default.c +4 -1
  51. package/examples/nes/templates/racing.c +50 -1
  52. package/examples/pce/main.c +1 -1
  53. package/examples/sms/templates/hello_sprite.c +1 -1
  54. package/examples/sms/templates/music_demo.c +1 -1
  55. package/examples/sms/templates/puzzle.c +1 -1
  56. package/examples/sms/templates/racing.c +1 -1
  57. package/examples/sms/templates/shmup.c +1 -1
  58. package/examples/sms/templates/shmup_2p.c +2 -2
  59. package/examples/snes/main.asm +1 -1
  60. package/examples/snes/templates/c-hello-data.asm +309 -14
  61. package/examples/snes/templates/c-hello.c +13 -2
  62. package/examples/snes/templates/default.c +1 -1
  63. package/examples/snes/templates/hello_sprite-data.asm +300 -2
  64. package/examples/snes/templates/hello_sprite.c +10 -1
  65. package/examples/snes/templates/music_demo-data.asm +300 -2
  66. package/examples/snes/templates/music_demo.c +10 -1
  67. package/examples/snes/templates/platformer-data.asm +300 -2
  68. package/examples/snes/templates/platformer.c +10 -1
  69. package/examples/snes/templates/puzzle-data.asm +300 -2
  70. package/examples/snes/templates/puzzle.c +11 -1
  71. package/examples/snes/templates/racing-data.asm +300 -2
  72. package/examples/snes/templates/racing.c +40 -4
  73. package/examples/snes/templates/shmup-data.asm +299 -6
  74. package/examples/snes/templates/shmup.c +11 -7
  75. package/examples/snes/templates/sports-data.asm +300 -2
  76. package/examples/snes/templates/sports.c +40 -5
  77. package/package.json +1 -1
  78. package/src/cheats/lookup.js +39 -18
  79. package/src/http/routes.js +58 -33
  80. package/src/http/skill-doc.js +10 -9
  81. package/src/http/swagger.js +1 -1
  82. package/src/http/tool-registry.js +72 -5
  83. package/src/mcp/server.js +6 -5
  84. package/src/mcp/state.js +8 -6
  85. package/src/mcp/tool-manifest.js +7 -7
  86. package/src/mcp/tools/cheats.js +4 -3
  87. package/src/mcp/tools/index.js +18 -2
  88. package/src/mcp/tools/playtest.js +48 -35
  89. package/src/mcp/tools/project.js +39 -73
  90. package/src/mcp/tools/rom-id.js +49 -4
  91. package/src/mcp/tools/tile-inspect.js +1 -1
  92. package/src/mcp/tools/toolchain.js +183 -19
  93. package/src/mcp/tools/trace-vram-source.js +3 -3
  94. package/src/mcp/tools/watch-memory.js +27 -46
  95. package/src/observer/livestream.html +41 -5
  96. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +5 -5
  97. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  98. package/src/platforms/gb/TROUBLESHOOTING.md +1 -1
  99. package/src/platforms/gb/UPSTREAM_SOURCES.md +1 -1
  100. package/src/platforms/gb/lib/c/README.md +2 -2
  101. package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +1 -1
  102. package/src/platforms/gbc/MENTAL_MODEL.md +3 -3
  103. package/src/platforms/gbc/TROUBLESHOOTING.md +5 -5
  104. package/src/platforms/gbc/UPSTREAM_SOURCES.md +2 -2
  105. package/src/platforms/gbc/lib/c/README.md +2 -2
  106. package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +1 -1
  107. package/src/platforms/gg/MENTAL_MODEL.md +14 -13
  108. package/src/platforms/gg/lib/c/vdp_init.c +10 -8
  109. package/src/platforms/msx/MENTAL_MODEL.md +1 -1
  110. package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
  111. package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
  112. package/src/platforms/pce/MENTAL_MODEL.md +1 -1
  113. package/src/platforms/pce/lib/c/pce_hw.h +1 -0
  114. package/src/platforms/pce/lib/c/pce_video.c +26 -0
  115. package/src/platforms/sms/MENTAL_MODEL.md +12 -12
  116. package/src/platforms/sms/lib/c/vdp_init.c +10 -8
  117. package/src/platforms/sms/lib/vdp_init.s +1 -1
  118. package/src/playtest/playtest.js +25 -0
  119. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
  120. package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
  121. package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
  122. package/src/toolchains/genesis-c/README.md +1 -1
  123. package/src/toolchains/sdcc/preflight-lint.js +47 -7
  124. package/src/toolchains/snes-c/snes-c.js +3 -7
@@ -1,7 +1,7 @@
1
1
  /* ── Hello, Game Boy in C — SDCC sm83 port ─────────────────────────
2
2
  * Minimal: cycle the BG palette on every vblank.
3
3
  *
4
- * Build: buildSource({ platform: "gb", source: <this file>, language: "c" })
4
+ * Build: build({ output: "rom", platform: "gb", source: <this file>, language: "c" })
5
5
  *
6
6
  * SDCC 4.4.0 codegen quirks to avoid in `__sfr __at` register-heavy
7
7
  * code:
@@ -118,7 +118,7 @@ static void lock_piece(void) {
118
118
  grid[r][c] = 0;
119
119
  grid[r][c + 1] = 0;
120
120
  grid[r][c + 2] = 0;
121
- if (score < 65500) score += 30;
121
+ if (score < 65500u) score += 30;
122
122
  }
123
123
  }
124
124
  }
@@ -153,6 +153,6 @@ void main(void) {
153
153
  break;
154
154
  }
155
155
  }
156
- if (score < 65500) score++;
156
+ if (score < 65500u) score++;
157
157
  }
158
158
  }
@@ -162,7 +162,7 @@ void main(void) {
162
162
  if (aabb(&bullets[i], &enemies[j])) {
163
163
  bullets[i].alive = 0;
164
164
  enemies[j].alive = 0;
165
- if (score < 65500) score += 10;
165
+ if (score < 65500u) score += 10;
166
166
  sound_play_noise(6);
167
167
  break;
168
168
  }
@@ -5,7 +5,7 @@
5
5
  * pixel that moves left/right via the d-pad.
6
6
  *
7
7
  * Build via romdev:
8
- * buildSource({platform:"gba", language:"c", runtime:"libgba",
8
+ * build({ output: "rom", platform:"gba", language:"c", runtime:"libgba",
9
9
  * source: <this file>})
10
10
  *
11
11
  * NOTE: the DEFAULT GBA runtime is libtonc (use `tonc_hello.c` instead
@@ -10,7 +10,7 @@
10
10
  * 4. mmStart(MOD_<NAME>_FROM_HEADER, mode) to begin playback.
11
11
  *
12
12
  * Build via romdev:
13
- * buildSource({
13
+ * build({ output: "rom",
14
14
  * platform: "gba",
15
15
  * language: "c",
16
16
  * source: <this file>,
@@ -22,6 +22,20 @@
22
22
  #include <tonc.h>
23
23
  #include "gba_sfx.h"
24
24
 
25
+ /* draw a 5-digit score WITHOUT tte_printf (broken in this libtonc — GBA-1). */
26
+ static void draw_score(int x, unsigned v) {
27
+ char buf[24];
28
+ int i, n = 0;
29
+ buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
30
+ if (x >= 100) buf[n++] = '0' + (x/100)%10;
31
+ if (x >= 10) buf[n++] = '0' + (x/10)%10;
32
+ buf[n++] = '0' + x%10;
33
+ buf[n++]=','; buf[n++]='8'; buf[n++]='}';
34
+ for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
35
+ n += 5; buf[n] = 0;
36
+ tte_write(buf);
37
+ }
38
+
25
39
  #define COLS 6
26
40
  #define ROWS 12
27
41
 
@@ -134,7 +148,7 @@ static void lock_piece(void) {
134
148
  grid[r][c] = 0;
135
149
  grid[r][c + 1] = 0;
136
150
  grid[r][c + 2] = 0;
137
- if (score < 65500) score += 30;
151
+ if (score < 65500u) score += 30;
138
152
  sfx_tone(1, 1700, 10); /* triple-clear chime */
139
153
  }
140
154
  }
@@ -211,7 +225,7 @@ int main(void) {
211
225
  new_piece();
212
226
  prev = now;
213
227
  tte_erase_rect(88 + 6*8, 8, 88 + 11*8, 16);
214
- tte_printf("#{P:%d,8}%05d", 88 + 6*8, score);
228
+ draw_score(88 + 6*8, score);
215
229
  continue;
216
230
  }
217
231
  prev = now;
@@ -231,7 +245,7 @@ int main(void) {
231
245
  draw_piece(piece_x, piece_y, 0);
232
246
 
233
247
  tte_erase_rect(88 + 6*8, 8, 88 + 11*8, 16);
234
- tte_printf("#{P:%d,8}%05d", 88 + 6*8, score);
248
+ draw_score(88 + 6*8, score);
235
249
  }
236
250
  return 0;
237
251
  }
@@ -15,6 +15,20 @@
15
15
  #include <tonc.h>
16
16
  #include "gba_sfx.h"
17
17
 
18
+ /* draw a 5-digit score WITHOUT tte_printf (broken in this libtonc — GBA-1). */
19
+ static void draw_score(int x, unsigned v) {
20
+ char buf[24];
21
+ int i, n = 0;
22
+ buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
23
+ if (x >= 100) buf[n++] = '0' + (x/100)%10;
24
+ if (x >= 10) buf[n++] = '0' + (x/10)%10;
25
+ buf[n++] = '0' + x%10;
26
+ buf[n++]=','; buf[n++]='8'; buf[n++]='}';
27
+ for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
28
+ n += 5; buf[n] = 0;
29
+ tte_write(buf);
30
+ }
31
+
18
32
  #define LANE_LEFT_X 56
19
33
  #define LANE_MID_X 116
20
34
  #define LANE_RIGHT_X 176
@@ -149,7 +163,7 @@ int main(void) {
149
163
  }
150
164
  }
151
165
 
152
- if (score < 65500) score++;
166
+ if (score < 65500u) score++;
153
167
  }
154
168
 
155
169
  /* Sprite slots: 0 = player, 1..4 = obstacles. */
@@ -169,7 +183,7 @@ int main(void) {
169
183
  oam_copy(oam_mem, obj_buffer, 1 + MAX_OBSTACLES);
170
184
 
171
185
  tte_erase_rect(160 + 6*8, 8, 160 + 11*8, 16);
172
- tte_printf("#{P:%d,8}%05d", 160 + 6*8, score);
186
+ draw_score(160 + 6*8, score);
173
187
  }
174
188
  return 0;
175
189
  }
@@ -29,6 +29,26 @@
29
29
  #define TILE_BULLET 2
30
30
  #define TILE_ENEMY 3
31
31
 
32
+ /* Draw a 5-digit score at pixel (x,8) WITHOUT tte_printf. The bundled libtonc's
33
+ * tte_printf with a %d/%05d conversion is broken (it routes through a vsnprintf
34
+ * path that isn't wired in this build — it garbles the output AND wedges the
35
+ * game loop when called per-frame, GBA-1). We build the string ourselves and
36
+ * use tte_write, which processes the #{P:x,y} position command but does NO
37
+ * format conversion → safe every frame. */
38
+ static void draw_score(int x, unsigned v) {
39
+ char buf[24];
40
+ int i, n = 0;
41
+ /* "#{P:<x>,8}" position command, then 5 decimal digits. */
42
+ buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
43
+ if (x >= 100) buf[n++] = '0' + (x/100)%10;
44
+ if (x >= 10) buf[n++] = '0' + (x/10)%10;
45
+ buf[n++] = '0' + x%10;
46
+ buf[n++]=','; buf[n++]='8'; buf[n++]='}';
47
+ for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
48
+ n += 5; buf[n] = 0;
49
+ tte_write(buf);
50
+ }
51
+
32
52
  /* 4bpp tiles (8 rows × 32 bits each = 32 bytes). Each nibble is a
33
53
  * palette index. Index 0 = transparent. */
34
54
  static const u32 tile_ship[8] = {
@@ -153,7 +173,7 @@ int main(void) {
153
173
  if (aabb_hit(&bullets[i], &enemies[j])) {
154
174
  bullets[i].alive = 0;
155
175
  enemies[j].alive = 0;
156
- if (score < 65500) score += 10;
176
+ if (score < 65500u) score += 10;
157
177
  sfx_noise(6); /* explosion */
158
178
  break;
159
179
  }
@@ -188,10 +208,9 @@ int main(void) {
188
208
 
189
209
  oam_copy(oam_mem, obj_buffer, 128);
190
210
 
191
- /* Score: 5-digit ASCII via TTE printf. Tte handles VRAM tile
192
- * writes — safe to call once per frame. */
211
+ /* Score: 5 digits via draw_score (NOT tte_printf see GBA-1). */
193
212
  tte_erase_rect(8 + 6*8, 8, 8 + 11*8, 16);
194
- tte_printf("#{P:%d,8}%05d", 8 + 6*8, score);
213
+ draw_score(8 + 6*8, score);
195
214
  }
196
215
  return 0;
197
216
  }
@@ -5,7 +5,7 @@
5
5
  * canonical "Hello GBA" pattern from gbadev.net/tonc.
6
6
  *
7
7
  * Build via romdev:
8
- * buildSource({platform:"gba", language:"c", source: <this file>})
8
+ * build({ output: "rom", platform:"gba", language:"c", source: <this file>})
9
9
  *
10
10
  * (defaults to runtime:"libtonc" — pass {runtime:"libgba"} to use
11
11
  * devkitPro's libgba instead, or {runtime:"none"} for bare gcc.)
@@ -59,9 +59,11 @@ int main(void) {
59
59
  tte_write("Hello, Tonc!\n");
60
60
  tte_write("Built with romdev\n");
61
61
 
62
- /* Formatted output without needing iprintf or libsysbase. */
63
- int year = 2026;
64
- tte_printf("#{P:32,80}Year: %d\n", year);
62
+ /* NOTE: tte_printf with a %d/%05d conversion is broken in this libtonc
63
+ * build (it garbles output + can wedge the loop — GBA-1). For dynamic
64
+ * numbers, build the string yourself and tte_write it (see the genre
65
+ * scaffolds' draw_score). For static text just tte_write a literal: */
66
+ tte_write("#{P:32,80}Year: 2026\n");
65
67
 
66
68
  /* Game loop. VBlankIntrWait() halts the CPU until next vblank —
67
69
  * saves battery on real hardware. */
@@ -7,7 +7,7 @@
7
7
  ; - palette 0 entry 0 = backdrop (blue here), entry 1 = yellow
8
8
  ;
9
9
  ; Build with:
10
- ; buildSource({platform:"gbc", source: <this>})
10
+ ; build({ output: "rom", platform:"gbc", source: <this>})
11
11
  ;
12
12
  ; rgbfix is invoked automatically to fix the header checksums.
13
13
 
@@ -118,7 +118,7 @@ static void lock_piece(void) {
118
118
  grid[r][c] = 0;
119
119
  grid[r][c + 1] = 0;
120
120
  grid[r][c + 2] = 0;
121
- if (score < 65500) score += 30;
121
+ if (score < 65500u) score += 30;
122
122
  }
123
123
  }
124
124
  }
@@ -159,6 +159,6 @@ void main(void) {
159
159
  break;
160
160
  }
161
161
  }
162
- if (score < 65500) score++;
162
+ if (score < 65500u) score++;
163
163
  }
164
164
  }
@@ -178,7 +178,7 @@ void main(void) {
178
178
  if (aabb(&bullets[i], &enemies[j])) {
179
179
  bullets[i].alive = 0;
180
180
  enemies[j].alive = 0;
181
- if (score < 65500) score += 10;
181
+ if (score < 65500u) score += 10;
182
182
  sound_play_noise(6);
183
183
  break;
184
184
  }
@@ -10,7 +10,7 @@
10
10
  ; 7. Enable display and park forever.
11
11
  ;
12
12
  ; BUILD:
13
- ; buildSource({ platform: "genesis", source: /* this file */ });
13
+ ; build({ output: "rom", platform: "genesis", source: /* this file */ });
14
14
  ;
15
15
  ; vasm syntax gotchas (in case you go beyond this scaffold):
16
16
  ; - NO space after commas in operands: `move.w #$2700, sr` FAILS.
@@ -137,7 +137,7 @@ static void lock_piece(void) {
137
137
  grid[r][c] = 0;
138
138
  grid[r][c + 1] = 0;
139
139
  grid[r][c + 2] = 0;
140
- if (score < 65500) score += 30;
140
+ if (score < 65500u) score += 30;
141
141
  sfx_tone(0, 250, 12); /* triple-clear chime */
142
142
  }
143
143
  }
@@ -27,6 +27,8 @@
27
27
 
28
28
  #define T_CAR_P1 (TILE_USER_INDEX + 0)
29
29
  #define T_CAR_EN (TILE_USER_INDEX + 1)
30
+ #define T_LANE (TILE_USER_INDEX + 2) /* dashed lane divider (BG_B) */
31
+ #define T_EDGE (TILE_USER_INDEX + 3) /* solid road edge (BG_B) */
30
32
 
31
33
  static const u32 tile_car_p1[8] = {
32
34
  0x01111110, 0x11111111, 0x12222221, 0x11111111,
@@ -36,6 +38,40 @@ static const u32 tile_car_enemy[8] = {
36
38
  0x03333330, 0x33333333, 0x34444443, 0x33333333,
37
39
  0x33333333, 0x34444443, 0x33333333, 0x03300330,
38
40
  };
41
+ /* Dashed lane-divider segment (colour 2 = grey): a 2px dash in the
42
+ * centre columns, on/off vertically so a stacked column reads as a
43
+ * dashed road centre-line. */
44
+ static const u32 tile_lane[8] = {
45
+ 0x00022000, 0x00022000, 0x00022000, 0x00000000,
46
+ 0x00000000, 0x00022000, 0x00022000, 0x00022000,
47
+ };
48
+ /* Solid 2px road-edge stripe (colour 2 = grey) down the right side of
49
+ * the tile — used on the left rail; mirrored (hflip) for the right. */
50
+ static const u32 tile_edge[8] = {
51
+ 0x00000022, 0x00000022, 0x00000022, 0x00000022,
52
+ 0x00000022, 0x00000022, 0x00000022, 0x00000022,
53
+ };
54
+
55
+ /* The road lives on BG_B (8×8 cells). Two dashed dividers sit between the
56
+ * three lanes; solid edges frame the outermost lanes. */
57
+ #define ROAD_TOP_ROW 1
58
+ #define ROAD_BOT_ROW 26
59
+ #define LANE_DIV1_COL ((LANE_LEFT_X + 8 + LANE_MID_X) / 16)
60
+ #define LANE_DIV2_COL ((LANE_MID_X + 8 + LANE_RIGHT_X) / 16)
61
+ #define ROAD_EDGE_L ((LANE_LEFT_X - 12) / 8)
62
+ #define ROAD_EDGE_R ((LANE_RIGHT_X + 12) / 8)
63
+
64
+ static void draw_road(void) {
65
+ s16 r;
66
+ for (r = ROAD_TOP_ROW; r <= ROAD_BOT_ROW; r++) {
67
+ /* Left edge (stripe on its right), right edge (hflipped). */
68
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 0, 0, 0, T_EDGE), ROAD_EDGE_L, r);
69
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 0, 0, 1, T_EDGE), ROAD_EDGE_R, r);
70
+ /* Two dashed lane dividers. */
71
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 0, 0, 0, T_LANE), LANE_DIV1_COL, r);
72
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 0, 0, 0, T_LANE), LANE_DIV2_COL, r);
73
+ }
74
+ }
39
75
 
40
76
  typedef struct { s16 x, y; bool alive; } Car;
41
77
 
@@ -97,6 +133,11 @@ int main(bool hard) {
97
133
 
98
134
  VDP_loadTileData(tile_car_p1, T_CAR_P1, 1, DMA);
99
135
  VDP_loadTileData(tile_car_enemy, T_CAR_EN, 1, DMA);
136
+ VDP_loadTileData(tile_lane, T_LANE, 1, DMA);
137
+ VDP_loadTileData(tile_edge, T_EDGE, 1, DMA);
138
+
139
+ /* Draw the static road (edges + dashed lane dividers) once on BG_B. */
140
+ draw_road();
100
141
 
101
142
  VDP_drawText("SCORE", 28, 2);
102
143
  VDP_drawText("L/R MOVES LANE", 13, 27);
@@ -149,7 +190,7 @@ int main(bool hard) {
149
190
  }
150
191
  }
151
192
 
152
- if (score < 65500) score++;
193
+ if (score < 65500u) score++;
153
194
  }
154
195
 
155
196
  /* SAT update — player + up to 4 obstacles = 5 sprites. */
@@ -160,6 +201,9 @@ int main(bool hard) {
160
201
  VDP_setSprite(slot++, obstacles[i].x, ey, SPRITE_SIZE(1, 1),
161
202
  TILE_ATTR_FULL(PAL1, 1, 0, 0, T_CAR_EN));
162
203
  }
204
+ /* Link slots 0..slot-1 so the VDP's SAT walk draws all of them — without
205
+ * this the link bytes stay 0 (= end-of-list) and only slot 0 renders. */
206
+ VDP_linkSprites(0, slot);
163
207
  VDP_updateSprites(slot, DMA);
164
208
 
165
209
  render_score();
@@ -81,11 +81,15 @@ static void fire_bullet(void) {
81
81
  }
82
82
  }
83
83
 
84
+ static u16 spawn_seed; /* free-running, advances every spawn (NOT spawn_timer,
85
+ * which is always 28 at the spawn call → one column) */
84
86
  static void spawn_enemy(void) {
85
87
  for (u16 i = 0; i < MAX_ENEMIES; i++) {
86
88
  if (!enemies[i].alive) {
87
- /* Cheap deterministic-but-varying x wraps every 7 spawns. */
88
- enemies[i].x = ((spawn_timer * 37) & 0xFF) % (320 - 16) + 8;
89
+ /* Cheap LCG-ish spread across the playfield so enemies don't all
90
+ * descend in a single column. */
91
+ spawn_seed = (u16)(spawn_seed * 1103 + 12345);
92
+ enemies[i].x = (s16)((spawn_seed >> 4) % (320 - 16) + 8);
89
93
  enemies[i].y = -8;
90
94
  enemies[i].alive = TRUE;
91
95
  return;
@@ -157,7 +161,7 @@ int main(bool hard) {
157
161
  if (aabb_hit(&bullets[i], &enemies[j])) {
158
162
  bullets[i].alive = FALSE;
159
163
  enemies[j].alive = FALSE;
160
- if (score < 65500) score += 10;
164
+ if (score < 65500u) score += 10;
161
165
  sfx_noise(8); /* explosion */
162
166
  break;
163
167
  }
@@ -177,6 +181,11 @@ int main(bool hard) {
177
181
  VDP_setSprite(7 + i, enemies[i].x, ey, SPRITE_SIZE(1, 1),
178
182
  TILE_ATTR_FULL(PAL2, 1, 0, 0, T_ENEMY));
179
183
  }
184
+ /* CHAIN the sprite linked list before uploading: VDP_setSprite does NOT
185
+ * set the link byte, and the SAT link bytes init to 0 (= "end of list"),
186
+ * so the VDP's sprite walk stops after slot 0 → only ONE sprite draws.
187
+ * VDP_linkSprites(0, N) links slots 0..N-1 so all N render. */
188
+ VDP_linkSprites(0, 1 + MAX_BULLETS + MAX_ENEMIES);
180
189
  VDP_updateSprites(1 + MAX_BULLETS + MAX_ENEMIES, DMA);
181
190
 
182
191
  render_score();
@@ -196,7 +196,7 @@ int main(bool hard) {
196
196
  if (p1_bullets[i].alive && aabb(&p1_bullets[i], &enemies[j])) {
197
197
  p1_bullets[i].alive = FALSE;
198
198
  enemies[j].alive = FALSE;
199
- if (score_p1 < 65500) score_p1 += 10;
199
+ if (score_p1 < 65500u) score_p1 += 10;
200
200
  sfx_noise(8);
201
201
  break;
202
202
  }
@@ -206,7 +206,7 @@ int main(bool hard) {
206
206
  if (p2_bullets[i].alive && aabb(&p2_bullets[i], &enemies[j])) {
207
207
  p2_bullets[i].alive = FALSE;
208
208
  enemies[j].alive = FALSE;
209
- if (score_p2 < 65500) score_p2 += 10;
209
+ if (score_p2 < 65500u) score_p2 += 10;
210
210
  sfx_noise(8);
211
211
  break;
212
212
  }
@@ -32,6 +32,8 @@
32
32
 
33
33
  #define T_PADDLE (TILE_USER_INDEX + 0)
34
34
  #define T_BALL (TILE_USER_INDEX + 1)
35
+ #define T_RAIL (TILE_USER_INDEX + 2) /* solid court rail (BG_B) */
36
+ #define T_NET (TILE_USER_INDEX + 3) /* dashed centre-line segment */
35
37
 
36
38
  /* 4bpp 8×8 tile, all colour 1 → solid white block. */
37
39
  static const u32 tile_solid[8] = {
@@ -39,6 +41,35 @@ static const u32 tile_solid[8] = {
39
41
  0x11111111, 0x11111111, 0x11111111, 0x11111111,
40
42
  };
41
43
 
44
+ /* Centre-net segment: a 2px-wide vertical dash down the middle of an 8×8
45
+ * tile (colour 1 in the centre columns, transparent elsewhere). Stacked
46
+ * down the court's centre column it reads as a dashed Pong net. */
47
+ static const u32 tile_net[8] = {
48
+ 0x00011000, 0x00011000, 0x00011000, 0x00000000,
49
+ 0x00011000, 0x00011000, 0x00011000, 0x00000000,
50
+ };
51
+
52
+ /* The court lives on BG_B (cells are 8×8): top + bottom rails plus a
53
+ * dashed centre net. 320px = 40 cols, COURT_TOP/BOT are pixel rows. */
54
+ #define COURT_COL_L (PADDLE_X1 / 8)
55
+ #define COURT_COL_R (PADDLE_X2 / 8)
56
+ #define COURT_ROW_TOP (COURT_TOP / 8)
57
+ #define COURT_ROW_BOT (COURT_BOT / 8 - 1)
58
+ #define COURT_NET_COL (COURT_W / 16)
59
+
60
+ static void draw_court(void) {
61
+ s16 c, r;
62
+ /* Top + bottom rails span the playfield width. */
63
+ for (c = COURT_COL_L; c <= COURT_COL_R; c++) {
64
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 0, 0, 0, T_RAIL), c, COURT_ROW_TOP);
65
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 0, 0, 0, T_RAIL), c, COURT_ROW_BOT);
66
+ }
67
+ /* Dashed centre net between the rails. */
68
+ for (r = COURT_ROW_TOP + 1; r < COURT_ROW_BOT; r++) {
69
+ VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 0, 0, 0, T_NET), COURT_NET_COL, r);
70
+ }
71
+ }
72
+
42
73
  static s16 p1y, p2y; /* paddle top Y, pixels */
43
74
  static s16 bx, by; /* ball top-left, pixels */
44
75
  static s16 bdx, bdy;
@@ -77,6 +108,11 @@ int main(bool hard) {
77
108
 
78
109
  VDP_loadTileData(tile_solid, T_PADDLE, 1, DMA);
79
110
  VDP_loadTileData(tile_solid, T_BALL, 1, DMA);
111
+ VDP_loadTileData(tile_solid, T_RAIL, 1, DMA);
112
+ VDP_loadTileData(tile_net, T_NET, 1, DMA);
113
+
114
+ /* Draw the static court (rails + centre net) once on BG_B. */
115
+ draw_court();
80
116
 
81
117
  VDP_drawText("PLAYER 1", 2, 1);
82
118
  VDP_drawText("PLAYER 2", 28, 1);
@@ -161,6 +197,9 @@ int main(bool hard) {
161
197
  }
162
198
  VDP_setSprite(slot++, bx, by, SPRITE_SIZE(1, 1),
163
199
  TILE_ATTR_FULL(PAL0, 1, 0, 0, T_BALL));
200
+ /* Link slots 0..slot-1 so the VDP's SAT walk draws all of them — without
201
+ * this the link bytes stay 0 (= end-of-list) and only slot 0 renders. */
202
+ VDP_linkSprites(0, slot);
164
203
  VDP_updateSprites(slot, DMA);
165
204
 
166
205
  render_scores();
@@ -1,21 +1,22 @@
1
- /* ── hello_sprite.c — SMS starter (one sprite + d-pad) ──────────────
1
+ /* ── hello_sprite.c — Game Gear starter (one sprite + d-pad) ────────
2
2
  *
3
- * Drives one sprite around the SMS screen with the directional pad.
4
- * Uses the bundled SMS runtime helpers (gg_vdp_init, gg_load_tiles,
5
- * gg_load_palette, sms_sprite_*, gg_vblank_wait, gg_joypad_read).
3
+ * Drives one sprite around the Game Gear screen with the directional
4
+ * pad. Uses the bundled GG runtime helpers (gg_vdp_init, gg_load_tiles,
5
+ * gg_load_palette, gg_sprite_*, gg_vblank_wait, gg_joypad_read).
6
6
  *
7
- * SMS hardware notes the templates assume:
8
- * - 256×192 visible area in mode 4
7
+ * GG hardware notes the templates assume:
8
+ * - 256×192 internal frame; only the centered 160×144 region SHOWS.
9
+ * Keep gameplay sprites inside [VIS_X0..VIS_X1] x [VIS_Y0..VIS_Y1].
9
10
  * - Sprite attribute table at VRAM $3F00 (configured by gg_vdp_init)
10
- * - Sprite tile data at VRAM $2000 (R6 = 0xFB)
11
+ * - Sprite tile data at VRAM $2000 (R6 = 0xFF → SA13 set → $2000)
11
12
  * - 64 sprite slots × 4 bytes (Y / X / tile / unused)
12
13
  *
13
14
  * Multi-file project — main.c plus the runtime .c files. Build with:
14
- * buildSource({platform:"sms", language:"c",
15
+ * build({ output: "rom", platform:"gg", language:"c",
15
16
  * sources: { "main.c": ..., "vdp_init.c": ..., ... },
16
17
  * includes: { "gg_hw.h": ... }})
17
18
  *
18
- * createProject({platform:"sms", template:"hello_sprite"}) copies all
19
+ * createProject({platform:"gg", template:"hello_sprite"}) copies all
19
20
  * the bits into your project tree.
20
21
  */
21
22
  #include "gg_hw.h"
@@ -34,16 +35,29 @@ extern void gg_sprite_init(void);
34
35
  extern void gg_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
35
36
  extern void gg_sat_upload(void);
36
37
 
37
- /* BG palette: backdrop blue + yellow. Sprite palette (entries 16-31)
38
- * we set white at index 17 so our sprite is visible.
39
- * SMS CRAM is 2-2-2 BGR: 0x00=black, 0x3F=white. */
40
- static const uint8_t palette[32] = {
41
- 0x10,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
42
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
43
- 0x00,0x3F,0x00,0x00, 0x00,0x00,0x00,0x00,
44
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
38
+ /* GG palette = 32 entries × 2 bytes (4-4-4 BGR LE): low=(g<<4)|r, high=b.
39
+ * Entries 0-15 = BG, 16-31 = SPRITE. gg_load_palette reads 64 bytes, so a
40
+ * 32-byte SMS-style array leaves the sprite palette (16-31) reading past the
41
+ * array = garbage = INVISIBLE sprites. Sprite colour index N uses entry 16+N,
42
+ * so sprite colour 1 = entry 17 (white here). */
43
+ static const uint8_t palette[64] = {
44
+ /* BG 0-15: entry 0 = dark navy backdrop */
45
+ 0x20,0x02, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
46
+ 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
47
+ /* SPRITE 16-31: 16=transparent, 17=white */
48
+ 0,0, 0xFF,0x0F, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
49
+ 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
45
50
  };
46
51
 
52
+ /* ── Game Gear visible viewport ──────────────────────────────────────
53
+ * Sprite OAM uses SMS HARDWARE coordinates (256x192 space), but the GG
54
+ * LCD only shows the CENTER 160x144. Keep the sprite inside this box or
55
+ * it's placed "correctly" in hardware yet INVISIBLE on screen. */
56
+ #define VIS_X0 48
57
+ #define VIS_Y0 24
58
+ #define VIS_X1 207 /* 48 + 160 - 1 */
59
+ #define VIS_Y1 167 /* 24 + 144 - 1 */
60
+
47
61
  /* One 8×8 sprite tile (4bpp interleaved). Filled square in color 1. */
48
62
  static const uint8_t sprite_tile[32] = {
49
63
  0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
@@ -53,8 +67,8 @@ static const uint8_t sprite_tile[32] = {
53
67
  };
54
68
 
55
69
  void main(void) {
56
- uint8_t x = 124; /* mid-screen X */
57
- uint8_t y = 88; /* mid-screen Y */
70
+ uint8_t x = (VIS_X0 + VIS_X1) / 2; /* center of the visible window */
71
+ uint8_t y = (VIS_Y0 + VIS_Y1) / 2;
58
72
  uint8_t prev = 0;
59
73
 
60
74
  gg_vdp_init();
@@ -80,10 +94,11 @@ void main(void) {
80
94
  gg_sat_upload();
81
95
 
82
96
  pad = gg_joypad_read();
83
- if (pad & JOY_LEFT && x > 0) x = (uint8_t)(x - 2);
84
- if (pad & JOY_RIGHT && x < 248) x = (uint8_t)(x + 2);
85
- if (pad & JOY_UP && y > 0) y = (uint8_t)(y - 2);
86
- if (pad & JOY_DOWN && y < 184) y = (uint8_t)(y + 2);
97
+ /* Clamp to the visible window so the sprite never slides off-screen. */
98
+ if (pad & JOY_LEFT && x > VIS_X0) x = (uint8_t)(x - 2);
99
+ if (pad & JOY_RIGHT && x < VIS_X1 - 8) x = (uint8_t)(x + 2);
100
+ if (pad & JOY_UP && y > VIS_Y0) y = (uint8_t)(y - 2);
101
+ if (pad & JOY_DOWN && y < VIS_Y1 - 8) y = (uint8_t)(y + 2);
87
102
  prev = pad;
88
103
  (void)prev;
89
104
  } while (1);
@@ -36,14 +36,17 @@ extern void gg_sprite_init(void);
36
36
  extern void gg_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
37
37
  extern void gg_sat_upload(void);
38
38
 
39
- /* Background = black. Sprite palette (entries 16+):
40
- * 16 = transparent backdrop, 17 = white, 18 = green, 19 = red.
41
- * CRAM bytes on SMS/GG are 2-2-2 BGR. */
42
- static const uint8_t palette[32] = {
43
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
44
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
45
- 0x00,0x3F,0x0C,0x03, 0x00,0x00,0x00,0x00,
46
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
39
+ /* GG palette = 32 entries × 2 bytes (4-4-4 BGR LE): low=(g<<4)|r, high=b.
40
+ * gg_load_palette reads 64 bytes; a 32-byte array leaves the sprite palette
41
+ * (entries 16-31) reading garbage = invisible sprites. Sprite palette:
42
+ * 16 = transparent, 17 = white, 18 = green, 19 = red. */
43
+ static const uint8_t palette[64] = {
44
+ /* BG 0-15: entry 0 = dark navy backdrop */
45
+ 0x20,0x02, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
46
+ 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
47
+ /* SPRITE 16-31: 16=transparent, 17=white, 18=green, 19=red */
48
+ 0,0, 0xFF,0x0F, 0xF0,0x00, 0x0F,0x00, 0,0, 0,0, 0,0, 0,0,
49
+ 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
47
50
  };
48
51
 
49
52
  /* Three 8×8 sprite tiles, 4bpp interleaved (4 planes × 8 rows):