romdevtools 0.28.0 → 0.30.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 (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -1,27 +1,36 @@
1
- /* ── shmup.c — SNES PVSnesLib vertical-shooter scaffold ─────────────
1
+ /* ── shmup.c — SNES vertical shooter (complete example game) ──────────────────
2
2
  *
3
- * Complete runnable vertical-shmup baseline. Layout mirrors the NES
4
- * and Genesis shmup scaffolds:
5
- * - Player ship (8×8 OAM 0), d-pad moves, B fires
6
- * - 6 bullet slots (OAM 1..6), 6 enemy slots (OAM 7..12)
7
- * - Wave spawner: one enemy from the top every ~28 frames
8
- * - Linear movement, AABB collision (8×8 vs 8×8)
3
+ * A COMPLETE, working game title screen, 1P and 2P SIMULTANEOUS co-op,
4
+ * shared lives, score + persistent hi-score (battery SRAM), SPC music + SFX,
5
+ * and a scrolling Mode 1 starfield under a rock-steady text HUD.
9
6
  *
10
- * SNES screen is 256×224 (NTSC). All movement / spawn coords use
11
- * that range. PVSnesLib's oamSet places OAM entries; oamUpdate
12
- * flushes them via DMA on the NMI.
7
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
8
+ * very different one. The markers tell you what's what:
9
+ * HARDWARE IDIOM (load-bearing) dodges a documented SNES footgun; reshape
10
+ * your gameplay around it (see TROUBLESHOOTING before changing).
11
+ * GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
13
12
  *
14
- * Sibling data.asm provides the font + sprite-tile + sprite-palette
15
- * blobs. We use tile index 0 (ship), 1 (bullet), 2 (enemy).
13
+ * What depends on what:
14
+ * data.asm font + sprite tiles + starfield tiles (rodata), and
15
+ * sram_read16/write16 (battery SRAM needs 24-bit addressing that tcc
16
+ * C pointers don't emit). Load-bearing.
17
+ * hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
18
+ * SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
19
+ * silently stop existing — the build still succeeds.
20
+ * snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
21
+ * driver (music + 2 one-shot samples). #include'd, not separately built.
16
22
  *
17
- * TWO PVSnesLib footguns this scaffold handles for you:
18
- * 1. oamSet's FIRST arg is a BYTE OFFSET into OAM, not a slot number.
19
- * Each sprite is 4 bytes, so sprite slot N lives at offset N*4.
20
- * Passing a plain slot number interleaves/corrupts entries. We use
21
- * the SPR(slot) macro belowalways address sprites through it.
22
- * 2. sfx_init() must run AFTER setScreenOn() and its return must be
23
- * checked. If it stalls before the screen is on, you get a black
24
- * screen forever. See the main() ordering below.
23
+ * Why the HUD never shears (read this if you come from the NES): the SNES
24
+ * Mode 1 gives you THREE independent background layers, each with its own
25
+ * scroll registers. The starfield lives on BG1 and scrolls; the text HUD
26
+ * lives on BG0 and simply never gets a scroll write. No sprite-0 splits, no
27
+ * mid-frame raster trickslayer separation IS the SNES way. (When one
28
+ * layer must be two things a fixed strip over a moving field on the SAME
29
+ * BG that's when you reach for HDMA; see the Mode 7 racing example.)
30
+ *
31
+ * VRAM BUDGET (word addresses):
32
+ * $0000- OBJ tiles, $2000- BG1 starfield tiles, $3000- BG0 console font,
33
+ * $4000- BG1 map, $6800- BG0 text map.
25
34
  */
26
35
 
27
36
  #include <snes.h>
@@ -29,81 +38,84 @@
29
38
  * inline rather than linked separately. */
30
39
  #include "snes_sfx.c"
31
40
 
32
- extern char tilfont, palfont;
33
- extern char tilsprite, palsprite;
34
- extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
41
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
42
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
43
+ #define GAME_TITLE "SOLAR BULWARK"
44
+
45
+ extern char tilfont, palfont; /* HUD font + text palette (data.asm) */
46
+ extern char tilsprite, palsprite; /* ship/bullet/enemy tiles + OBJ pal */
47
+ extern char tilbg, palbg; /* 4 starfield tiles + BG palette */
35
48
 
36
49
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
37
50
  * No public prototype in console.h, so declare it; call once per frame. */
38
51
  extern void consoleVblank(void);
39
52
 
40
- /* OAM is addressed by BYTE OFFSET; sprite slot N = offset N*4. */
41
- #define SPR(slot) ((slot) << 2)
53
+ /* data.asm exports battery SRAM accessors (long addressing to $70:0000). */
54
+ extern u16 sram_read16(u16 offset);
55
+ extern void sram_write16(u16 offset, u16 value);
42
56
 
43
- /* BG1 wallpaper map: a full 32×32 screen of the 4-colour tile so the
44
- * playfield reads as a real backdrop, not flat blank. Filled at runtime. */
45
- static u16 bg_map[32 * 32];
57
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
58
+ * oamSet's FIRST arg is a BYTE OFFSET into OAM, not a slot number. Each
59
+ * sprite is 4 bytes, so sprite slot N lives at offset N*4. Passing a plain
60
+ * slot number interleaves/corrupts entries — always go through SPR(). */
61
+ #define SPR(slot) ((slot) << 2)
46
62
 
47
- #define MAX_BULLETS 6
48
- #define MAX_ENEMIES 6
63
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
64
+ * Object pools — fixed slots, no allocation. OAM slot layout:
65
+ * 0..1 = ships (P1, P2), 2..9 = bullets, 10..15 = enemies. */
66
+ #define MAX_BULLETS 8
67
+ #define MAX_ENEMIES 6
68
+ #define OAM_SHIP 0
69
+ #define OAM_BULLET 2
70
+ #define OAM_ENEMY 10
71
+ #define OAM_COUNT 16
72
+ #define TILE_SHIP 0 /* tile indexes into tilsprite (data.asm) */
73
+ #define TILE_BULLET 1
74
+ #define TILE_ENEMY 2
75
+ #define START_LIVES 3
76
+ #define HUD_Y 24 /* playfield starts below the text HUD row */
77
+ #define FIELD_BOT 208
78
+ #define INV_FRAMES 90 /* post-hit invulnerability (blink) */
79
+
80
+ /* SRAM layout: [0]=magic "SB", [2]=hi-score, [4]=hiscore ^ 0xA5C3.
81
+ * Magic is written LAST in hiscore_save so a torn write never validates. */
82
+ #define SRAM_MAGIC 0x4253u
83
+
84
+ /* Game states — the shell every example shares: title → play → game over. */
85
+ #define ST_TITLE 0
86
+ #define ST_PLAY 1
87
+ #define ST_OVER 2
49
88
 
50
89
  typedef struct { s16 x, y; u8 alive; } Obj;
51
90
 
52
- static Obj player;
91
+ static u8 state;
92
+ static u8 two_player; /* mode chosen on the title screen */
93
+ static u8 sound_ok;
94
+ static Obj ships[2];
95
+ static u8 ship_inv[2]; /* invulnerability frames after a hit */
96
+ static u8 fire_cd[2];
53
97
  static Obj bullets[MAX_BULLETS];
54
98
  static Obj enemies[MAX_ENEMIES];
55
- static u16 score;
99
+ static u8 lives; /* SHARED pool in co-op (arcade style) */
100
+ static u16 score, hiscore;
101
+ static u8 hud_dirty;
56
102
  static u16 spawn_timer;
103
+ static u16 frame_ct;
104
+ static u16 star_v; /* BG1 vertical scroll (starfield drift) */
105
+ static u16 prev_pad0;
106
+ static char nbuf[8]; /* 5-digit number formatter output */
107
+
108
+ /* BG1 starfield map: 32×32 entries, composed once at boot then scrolled in
109
+ * hardware forever. Static (not a local): >255 bytes of locals overflows
110
+ * tcc's 8-bit stack-relative addressing. */
111
+ static u16 bg_map[32 * 32];
57
112
 
58
- static u8 aabb(Obj* a, Obj* b) {
59
- return a->x < b->x + 8 && a->x + 8 > b->x
60
- && a->y < b->y + 8 && a->y + 8 > b->y;
61
- }
62
-
63
- static void render_score(void) {
64
- char buf[8];
65
- u16 v;
66
- s8 i;
67
- buf[0]='0'; buf[1]='0'; buf[2]='0'; buf[3]='0'; buf[4]='0'; buf[5]=0;
68
- v = score;
69
- for (i = 4; i >= 0; i--) { buf[i] = '0' + (v % 10); v /= 10; }
70
- consoleDrawText(20, 1, buf);
71
- }
72
-
73
- static void fire(void) {
74
- u16 i;
75
- for (i = 0; i < MAX_BULLETS; i++) {
76
- if (!bullets[i].alive) {
77
- bullets[i].x = player.x;
78
- bullets[i].y = player.y - 8;
79
- bullets[i].alive = 1;
80
- return;
81
- }
82
- }
83
- }
84
-
85
- /* Write all sprites into the OAM low table. Slot layout: 0=player,
86
- * 1..6=bullets, 7..12=enemies. SPR(slot) = slot*4 (oamSet's id is a BYTE
87
- * offset). Inactive objects park at Y=240 (off-screen) — that's how you
88
- * "hide" a sprite; no oamSetEx needed (oamInitGfxSet leaves slots shown).
89
- * Call oamUpdate() after to DMA the table to hardware. */
90
- static void stage_frame(void) {
91
- u16 i, by, ey;
92
- oamSet(SPR(0), player.x, player.y, 3, 0, 0, 0, 0); /* player tile 0 */
93
- for (i = 0; i < MAX_BULLETS; i++) {
94
- by = bullets[i].alive ? bullets[i].y : 240;
95
- oamSet(SPR(1 + i), bullets[i].x, by, 3, 0, 0, 1, 0); /* gfxoffset = tile INDEX 1 (not a byte offset) */
96
- }
97
- for (i = 0; i < MAX_ENEMIES; i++) {
98
- ey = enemies[i].alive ? enemies[i].y : 240;
99
- oamSet(SPR(7 + i), enemies[i].x, ey, 3, 0, 0, 2, 0); /* gfxoffset = tile INDEX 2 (not a byte offset) */
100
- }
101
- }
113
+ /* Headless-test telemetry — written once per frame; a test harness finds it
114
+ * by scanning WRAM for the "SB"+0xB7 signature, then plays the game from
115
+ * real state instead of parsing pixels. Costs ~30 byte-writes; delete freely. */
116
+ static u8 telem[32];
102
117
 
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. */
118
+ /* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ────────────────── */
107
119
  static u8 rng_state = 0xA5;
108
120
  static u8 rand8(void) {
109
121
  u8 lsb = (u8)(rng_state & 1);
@@ -112,129 +124,378 @@ static u8 rand8(void) {
112
124
  return rng_state;
113
125
  }
114
126
 
115
- static void spawn(void) {
116
- u16 i;
117
- for (i = 0; i < MAX_ENEMIES; i++) {
118
- if (!enemies[i].alive) {
119
- enemies[i].x = rand8() % (256 - 16) + 4;
120
- enemies[i].y = 0;
121
- enemies[i].alive = 1;
122
- return;
123
- }
127
+ /* ── GAME LOGIC (clay) — SRAM hi-score (see sram_* in data.asm) ────────────── */
128
+ static u16 hiscore_load(void) {
129
+ u16 v;
130
+ if (sram_read16(0) != SRAM_MAGIC) return 0;
131
+ v = sram_read16(2);
132
+ if (sram_read16(4) != (u16)(v ^ 0xA5C3u)) return 0;
133
+ return v;
134
+ }
135
+
136
+ static void hiscore_save(u16 v) {
137
+ sram_write16(2, v);
138
+ sram_write16(4, (u16)(v ^ 0xA5C3u));
139
+ sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
140
+ }
141
+
142
+ /* ── GAME LOGIC (clay) — text helpers ──────────────────────────────────────── */
143
+ static void fmt5(u16 v) { /* u16 → "00000" into nbuf */
144
+ s8 i;
145
+ for (i = 4; i >= 0; i--) { nbuf[i] = (char)('0' + v % 10); v /= 10; }
146
+ nbuf[5] = 0;
147
+ }
148
+
149
+ static void clear_rows(u16 a, u16 b) {
150
+ u16 y;
151
+ for (y = a; y <= b; y++)
152
+ consoleDrawText(0, y, " ");
153
+ }
154
+
155
+ static void draw_hud(void) {
156
+ fmt5(score); consoleDrawText(3, 1, nbuf);
157
+ fmt5(hiscore); consoleDrawText(13, 1, nbuf);
158
+ nbuf[0] = (char)('0' + lives); nbuf[1] = 0;
159
+ consoleDrawText(23, 1, nbuf);
160
+ hud_dirty = 0;
161
+ }
162
+
163
+ /* ── GAME LOGIC (clay) — firing + spawning ─────────────────────────────────── */
164
+ static void fire_bullet(u8 p) {
165
+ u8 i;
166
+ for (i = 0; i < MAX_BULLETS; i++) {
167
+ if (!bullets[i].alive) {
168
+ bullets[i].x = ships[p].x;
169
+ bullets[i].y = ships[p].y - 8;
170
+ bullets[i].alive = 1;
171
+ if (sound_ok) sfx_play(1); /* pew (voice 0 one-shot) */
172
+ return;
173
+ }
174
+ }
175
+ }
176
+
177
+ static void spawn_enemy(void) {
178
+ u8 i;
179
+ for (i = 0; i < MAX_ENEMIES; i++) {
180
+ if (!enemies[i].alive) {
181
+ enemies[i].x = (s16)(rand8() % 224) + 8;
182
+ enemies[i].y = HUD_Y;
183
+ enemies[i].alive = 1;
184
+ return;
185
+ }
186
+ }
187
+ }
188
+
189
+ /* AABB, both boxes 8×8. */
190
+ static u8 hits(Obj *a, Obj *b) {
191
+ return a->x < b->x + 8 && a->x + 8 > b->x
192
+ && a->y < b->y + 8 && a->y + 8 > b->y;
193
+ }
194
+
195
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
196
+ * Stage every OAM slot every frame, then ONE oamUpdate(). Inactive objects
197
+ * park at Y=240 (below the 224-line display) — that's how you "hide" a
198
+ * sprite without touching the OAM high table; oamInitGfxSet leaves slots
199
+ * shown. The invulnerability blink also parks the ship every few frames.
200
+ * CHANNEL BUDGET NOTE: oamUpdate only marks the shadow table; PVSnesLib's
201
+ * VBlank ISR DMAs it on CHANNEL 7 every frame, and ch 0 carries the console
202
+ * text upload. If you add HDMA effects (gradient sky, per-line scroll —
203
+ * see the Mode 7 racing example) park them on channels 2-6: a channel can't
204
+ * serve HDMA and that GP-DMA in the same frame, and the ISR silently
205
+ * rewrites ch 7's params each NMI. */
206
+ static void stage_frame(void) {
207
+ u8 i, hide;
208
+ s16 y;
209
+ for (i = 0; i < 2; i++) {
210
+ hide = (u8)(!ships[i].alive || (ship_inv[i] & 4));
211
+ y = hide ? 240 : ships[i].y;
212
+ /* P2 = same tile, OBJ palette 1 (oamSet's LAST arg; CGRAM entry 145
213
+ * is recoloured green in main). */
214
+ oamSet(SPR(OAM_SHIP + i), ships[i].x, y, 3, 0, 0, TILE_SHIP, i);
215
+ }
216
+ for (i = 0; i < MAX_BULLETS; i++) {
217
+ y = bullets[i].alive ? bullets[i].y : 240;
218
+ oamSet(SPR(OAM_BULLET + i), bullets[i].x, y, 3, 0, 0, TILE_BULLET, 0);
219
+ }
220
+ for (i = 0; i < MAX_ENEMIES; i++) {
221
+ y = enemies[i].alive ? enemies[i].y : 240;
222
+ oamSet(SPR(OAM_ENEMY + i), enemies[i].x, y, 3, 0, 0, TILE_ENEMY, 0);
223
+ }
224
+ }
225
+
226
+ /* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
227
+ static void clear_pools(void) {
228
+ u8 i;
229
+ for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
230
+ for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
231
+ ships[0].alive = ships[1].alive = 0;
232
+ }
233
+
234
+ static void title_enter(void) {
235
+ clear_pools();
236
+ clear_rows(0, 27);
237
+ consoleDrawText((32 - sizeof(GAME_TITLE) + 1) / 2, 6, GAME_TITLE);
238
+ consoleDrawText(12, 9, "HI");
239
+ fmt5(hiscore); consoleDrawText(15, 9, nbuf);
240
+ consoleDrawText(10, 12, "A - 1P START");
241
+ consoleDrawText(10, 14, "B - 2P CO-OP");
242
+ consoleDrawText(7, 20, "D-PAD MOVE B FIRE");
243
+ prev_pad0 = 0xFFFF; /* swallow the press that ENTERED this state — without
244
+ * this, the START that left the game-over screen
245
+ * instantly restarts (classic edge-detect reuse bug) */
246
+ state = ST_TITLE;
247
+ }
248
+
249
+ static void respawn_ship(u8 p) {
250
+ ships[p].x = p ? 144 : (two_player ? 96 : 124);
251
+ ships[p].y = 200;
252
+ fire_cd[p] = 0;
253
+ }
254
+
255
+ static void play_enter(u8 players) {
256
+ two_player = players;
257
+ clear_pools();
258
+ ships[0].alive = 1;
259
+ ships[1].alive = two_player;
260
+ respawn_ship(0);
261
+ respawn_ship(1);
262
+ ship_inv[0] = ship_inv[1] = 0;
263
+ lives = START_LIVES;
264
+ score = 0;
265
+ spawn_timer = 0;
266
+ clear_rows(0, 27);
267
+ consoleDrawText(0, 1, "SC");
268
+ consoleDrawText(10, 1, "HI");
269
+ consoleDrawText(20, 1, "LV");
270
+ draw_hud();
271
+ state = ST_PLAY;
272
+ }
273
+
274
+ static void game_over(void) {
275
+ u8 newhi = 0;
276
+ if (score > hiscore) {
277
+ hiscore = score;
278
+ /* ── HARDWARE IDIOM (load-bearing) — persists via battery SRAM at
279
+ * $70:0000; works because hdr.asm declares CARTRIDGETYPE $02 +
280
+ * SRAMSIZE $01. Magic+checksum layout, magic written last. ── */
281
+ hiscore_save(hiscore);
282
+ newhi = 1;
283
+ hud_dirty = 1;
284
+ }
285
+ consoleDrawText(11, 13, "GAME OVER");
286
+ if (newhi) consoleDrawText(10, 15, "NEW HI SCORE");
287
+ consoleDrawText(10, 17, "PRESS START");
288
+ if (sound_ok) sfx_play(2);
289
+ prev_pad0 = 0xFFFF; /* swallow the held pad into ST_OVER */
290
+ state = ST_OVER;
291
+ }
292
+
293
+ /* ── GAME LOGIC (clay) — per-player update. THE 2P wiring is one line:
294
+ * padsCurrent(p) reads controller port p (0 = pad 1, 1 = pad 2). ──────────── */
295
+ static void update_ship(u8 p) {
296
+ u16 pad = padsCurrent(p);
297
+ if (!ships[p].alive) return;
298
+ if ((pad & KEY_LEFT) && ships[p].x > 8) ships[p].x -= 2;
299
+ if ((pad & KEY_RIGHT) && ships[p].x < 240) ships[p].x += 2;
300
+ if ((pad & KEY_UP) && ships[p].y > HUD_Y + 8) ships[p].y -= 2;
301
+ if ((pad & KEY_DOWN) && ships[p].y < FIELD_BOT) ships[p].y += 2;
302
+ if ((pad & KEY_B) && fire_cd[p] == 0) {
303
+ fire_bullet(p);
304
+ fire_cd[p] = 10;
305
+ }
306
+ if (fire_cd[p]) --fire_cd[p];
307
+ if (ship_inv[p]) --ship_inv[p];
308
+ }
309
+
310
+ /* ── GAME LOGIC (clay) — the playfield tick ────────────────────────────────── */
311
+ static void play_update(void) {
312
+ u8 i, j;
313
+ u16 interval;
314
+
315
+ update_ship(0);
316
+ if (two_player) update_ship(1);
317
+
318
+ for (i = 0; i < MAX_BULLETS; i++) {
319
+ if (!bullets[i].alive) continue;
320
+ bullets[i].y -= 4;
321
+ if (bullets[i].y < HUD_Y) bullets[i].alive = 0;
322
+ }
323
+
324
+ for (i = 0; i < MAX_ENEMIES; i++) {
325
+ if (!enemies[i].alive) continue;
326
+ enemies[i].y += 1;
327
+ if (enemies[i].y >= 224) enemies[i].alive = 0; /* escaped — no penalty */
328
+ }
329
+
330
+ /* difficulty ramp: spawn faster as the score grows */
331
+ interval = score >> 6;
332
+ interval = (interval >= 20) ? 12 : (32 - interval);
333
+ if (++spawn_timer >= interval) { spawn_timer = 0; spawn_enemy(); }
334
+
335
+ /* bullets ↔ enemies */
336
+ for (i = 0; i < MAX_BULLETS; i++) {
337
+ if (!bullets[i].alive) continue;
338
+ for (j = 0; j < MAX_ENEMIES; j++) {
339
+ if (!enemies[j].alive) continue;
340
+ if (hits(&bullets[i], &enemies[j])) {
341
+ bullets[i].alive = 0;
342
+ enemies[j].alive = 0;
343
+ if (score < 65500) score += 10;
344
+ hud_dirty = 1;
345
+ if (sound_ok) sfx_play(2); /* boom */
346
+ break;
347
+ }
348
+ }
349
+ }
350
+
351
+ /* enemies ↔ ships: SHARED life pool (arcade co-op) + invulnerability
352
+ * blink, so one bad wave can't drain every life in a single overlap */
353
+ for (j = 0; j < MAX_ENEMIES; j++) {
354
+ if (!enemies[j].alive) continue;
355
+ for (i = 0; i < 2; i++) {
356
+ if (!ships[i].alive || ship_inv[i]) continue;
357
+ if (hits(&enemies[j], &ships[i])) {
358
+ enemies[j].alive = 0;
359
+ if (sound_ok) sfx_play(2);
360
+ if (lives) --lives;
361
+ hud_dirty = 1;
362
+ if (lives == 0) { game_over(); return; }
363
+ respawn_ship(i);
364
+ ship_inv[i] = INV_FRAMES;
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ /* ── GAME LOGIC (clay) — boot-time starfield composition ─────────────────────
371
+ * Two space tones in a checker (so no single colour ever dominates the
372
+ * screen) + LFSR-scattered star tiles. Map entries: tile index | 0x0400 =
373
+ * palette block 1 (bits 10-12), keeping the console font palette (block 0)
374
+ * untouched — HUD text stays white/legible. */
375
+ static void build_starfield(void) {
376
+ u16 r, c, e;
377
+ u8 v;
378
+ for (r = 0; r < 32; r++) {
379
+ for (c = 0; c < 32; c++) {
380
+ e = ((r ^ c) & 1) ? 1 : 0; /* space A / space B checker */
381
+ v = rand8();
382
+ if ((v & 0x1F) == 0) e = 2; /* bright star (on tone A) */
383
+ else if ((v & 0x1F) == 1) e = 3; /* gold star (on tone B) */
384
+ bg_map[(r << 5) + c] = (u16)(0x0400 | e);
124
385
  }
386
+ }
387
+ }
388
+
389
+ static void telem_update(void) {
390
+ u8 i;
391
+ telem[0] = 'S'; telem[1] = 'B'; telem[2] = 0xB7;
392
+ telem[3] = state;
393
+ telem[4] = lives;
394
+ telem[5] = (u8)((sound_ok << 7) | (two_player << 1));
395
+ telem[6] = (u8)score; telem[7] = (u8)(score >> 8);
396
+ telem[8] = (u8)hiscore; telem[9] = (u8)(hiscore >> 8);
397
+ telem[10] = (u8)ships[0].x; telem[11] = (u8)ships[0].y;
398
+ telem[12] = (u8)ships[1].x; telem[13] = (u8)ships[1].y;
399
+ telem[14] = (u8)(ships[0].alive | (ships[1].alive << 1)
400
+ | ((ship_inv[0] != 0) << 2) | ((ship_inv[1] != 0) << 3));
401
+ for (i = 0; i < MAX_ENEMIES; i++) {
402
+ telem[15 + (i << 1)] = enemies[i].alive ? (u8)enemies[i].x : 0xFF;
403
+ telem[16 + (i << 1)] = enemies[i].alive ? (u8)enemies[i].y : 0xFF;
404
+ }
405
+ telem[27] = (u8)frame_ct;
125
406
  }
126
407
 
127
408
  int main(void) {
128
- u16 i, j, pad;
129
- u16 prev = 0;
130
- u16 by, ey;
131
-
132
- dmaClearVram();
133
-
134
- consoleSetTextMapPtr(0x6800);
135
- consoleSetTextGfxPtr(0x3000);
136
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
137
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
138
- setMode(BG_MODE1, 0);
139
- /* BG0 is the text-console HUD layer. consoleInitText DMAs the font
140
- * but does NOT set the PPU BG base registers — point BG0 at the same
141
- * font ($3000) + map ($6800) so the HUD text renders. */
142
- bgSetGfxPtr(0, 0x3000);
143
- bgSetMapPtr(0, 0x6800, SC_32x32);
144
-
145
- /* BG1 = full-screen wallpaper so the playfield never reads as blank.
146
- * Tiles → VRAM $2000, map → VRAM $4000 (clear of sprites $0000 and the
147
- * console gfx $3000 / map $6800). One 8×8 tile = 32 bytes of gfx +
148
- * 32 bytes of palette. */
149
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg,
150
- 1, /* load palbg into CGRAM palette block 1 */
151
- 32, 32, BG_16COLORS, 0x2000);
152
-
153
- /* Per-genre backdrop tint — every SNES scaffold used to ship the same
154
- * blue checkered wallpaper ('no variety'). Recolor the wallpaper's
155
- * CGRAM entries (block 1 = entries 16+) to a near-black space scheme. */
156
- setPaletteColor(0, RGB5(0,0,3));
157
- setPaletteColor(17, RGB5(3,3,8));
158
- setPaletteColor(18, RGB5(1,1,5));
159
- /* Every map entry: tile 0, palette block 1 (bits 10-12 = 1 0x0400),
160
- * so the wallpaper uses palbg and leaves the console font palette
161
- * (block 0) untouched — HUD text stays white/legible. */
162
- for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
163
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
164
- bgSetEnable(1);
165
-
166
- bgSetDisable(2);
167
-
168
- /* 3 sprite tiles (ship/bullet/enemy) × 32 bytes = 96 bytes. */
169
- oamInitGfxSet(&tilsprite, 96, &palsprite, 32, 0,
170
- 0x0000, OBJ_SIZE8_L16);
171
-
172
- player.x = 120; player.y = 180; player.alive = 1;
173
- for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
174
- for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
175
- score = 0;
176
- spawn_timer = 0;
177
-
178
- consoleDrawText(14, 1, "SCORE");
179
- consoleDrawText(2, 26, "D-PAD MOVE B FIRE");
180
-
181
- /* Pre-stage all 13 OAM slots off-screen (SPR() = slot*4 — oamSet's id
182
- * is a BYTE OFFSET). After oamInitGfxSet, sprites default to shown, so
183
- * just set position; hide an inactive sprite by parking it at Y=240. */
184
- for (i = 0; i < 13; i++) oamSet(SPR(i), 0, 240, 3, 0, 0, 0, 0);
185
-
186
- /* Stage the first real frame + flush it to OAM BEFORE the screen turns
187
- * on, so frame 1 shows the game (not power-on garbage). */
409
+ u16 pad;
410
+
411
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
412
+ * Init order: console text pointers FIRST, then mode, then per-BG base
413
+ * registers, then VRAM uploads — all while the screen is still off.
414
+ * consoleInitText DMAs the font but does NOT set the PPU BG base
415
+ * registers; bgSetGfxPtr/bgSetMapPtr for BG0 must repeat the same
416
+ * addresses or the HUD renders garbage. */
417
+ consoleSetTextMapPtr(0x6800);
418
+ consoleSetTextGfxPtr(0x3000);
419
+ consoleSetTextOffset(0x0000); /* tile index = (char - 0x20) */
420
+ consoleInitText(0, 16 * 2, &tilfont, &palfont);
421
+ setMode(BG_MODE1, 0);
422
+ bgSetGfxPtr(0, 0x3000);
423
+ bgSetMapPtr(0, 0x6800, SC_32x32);
424
+
425
+ /* BG1 = the scrolling starfield. 4 tiles → VRAM $2000, map → $4000
426
+ * (clear of sprites $0000, the console font $3000 and map $6800). */
427
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg,
428
+ 1, /* palbg CGRAM palette block 1 */
429
+ 4 * 32, 32, BG_16COLORS, 0x2000);
430
+ build_starfield();
431
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
432
+ bgSetEnable(1);
433
+ bgSetDisable(2); /* BG2 carries garbage in mode 1 — off */
434
+
435
+ setPaletteColor(0, RGB5(0, 0, 3)); /* backdrop: near-black space */
436
+ /* P2's ship: OBJ palette 1 (CGRAM 144+), colour 1 recoloured green.
437
+ * Same tile as P1 — only oamSet's palette argument differs. */
438
+ setPaletteColor(145, RGB5(6, 28, 10));
439
+
440
+ /* 3 sprite tiles (ship/bullet/enemy) × 32 bytes = 96 bytes. */
441
+ oamInitGfxSet(&tilsprite, 96, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
442
+
443
+ /* ── HARDWARE IDIOM (load-bearing) stage + flush OAM BEFORE the screen
444
+ * turns on, so frame 1 shows the game (not power-on OAM garbage). ── */
445
+ clear_pools();
446
+ stage_frame();
447
+ oamUpdate();
448
+
449
+ setScreenOn();
450
+
451
+ /* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
452
+ * the return: a wedged SPC700 must not take the video down with it. ── */
453
+ sound_ok = (sfx_init() == 0);
454
+ /* ── HARDWARE IDIOM (load-bearing) one frame between init and the first
455
+ * command. sfx_init returns the instant the SPC echoes the jump command,
456
+ * but the driver then spends ~50 port writes initialising the DSP BEFORE
457
+ * it seeds its command edge-detector from $2140. Send a command in that
458
+ * window and the seed swallows it — music silently never starts. A
459
+ * WaitForVBlank is thousands of SPC cycles — deterministic cure. ── */
460
+ WaitForVBlank();
461
+ if (sound_ok) sfx_music_play();
462
+
463
+ hiscore = hiscore_load(); /* battery SRAM 0 on first boot */
464
+ star_v = 0;
465
+ frame_ct = 0;
466
+ title_enter();
467
+
468
+ while (1) {
469
+ pad = padsCurrent(0);
470
+
471
+ if (state == ST_TITLE) {
472
+ /* ── GAME LOGIC (clay) — title: A/START = 1P, B = 2P co-op ── */
473
+ if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
474
+ (pad & KEY_START && !(prev_pad0 & KEY_START))) {
475
+ play_enter(0);
476
+ } else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
477
+ play_enter(1);
478
+ }
479
+ } else if (state == ST_PLAY) {
480
+ play_update();
481
+ } else { /* ST_OVER — field frozen, stars keep drifting */
482
+ if (pad & KEY_START && !(prev_pad0 & KEY_START)) title_enter();
483
+ }
484
+ prev_pad0 = pad;
485
+
486
+ /* starfield drift: ~0.5 px/frame downward. VOFS decreasing moves BG
487
+ * content DOWN the screen; the 256-px map wraps in hardware. */
488
+ if (frame_ct & 1) --star_v;
489
+ frame_ct++;
490
+
491
+ telem_update();
188
492
  stage_frame();
189
493
  oamUpdate();
494
+ if (hud_dirty) draw_hud();
190
495
 
191
- /* Screen ON first, THEN sound. If the SPC wedges, sfx_init returns
192
- * nonzero and we keep running silently — never a black screen. */
193
- setScreenOn();
194
- sfx_init();
195
-
196
- while (1) {
197
- pad = padsCurrent(0);
198
-
199
- if (pad & KEY_LEFT && player.x > 8) player.x -= 2;
200
- if (pad & KEY_RIGHT && player.x < 256 - 16) player.x += 2;
201
- if (pad & KEY_UP && player.y > 16) player.y -= 2;
202
- if (pad & KEY_DOWN && player.y < 224 - 16) player.y += 2;
203
- if ((pad & KEY_B) && !(prev & KEY_B)) { fire(); sfx_play(1); }
204
- prev = pad;
205
-
206
- for (i = 0; i < MAX_BULLETS; i++) {
207
- if (!bullets[i].alive) continue;
208
- bullets[i].y -= 4;
209
- if (bullets[i].y < 0 || bullets[i].y > 230) bullets[i].alive = 0;
210
- }
211
- for (i = 0; i < MAX_ENEMIES; i++) {
212
- if (!enemies[i].alive) continue;
213
- enemies[i].y += 1;
214
- if (enemies[i].y >= 224) enemies[i].alive = 0;
215
- }
216
- if (++spawn_timer >= 28) { spawn_timer = 0; spawn(); }
217
-
218
- for (i = 0; i < MAX_BULLETS; i++) {
219
- if (!bullets[i].alive) continue;
220
- for (j = 0; j < MAX_ENEMIES; j++) {
221
- if (!enemies[j].alive) continue;
222
- if (aabb(&bullets[i], &enemies[j])) {
223
- bullets[i].alive = 0;
224
- enemies[j].alive = 0;
225
- if (score < 65500) score += 10;
226
- sfx_play(2);
227
- break;
228
- }
229
- }
230
- }
231
-
232
- stage_frame();
233
- oamUpdate();
234
-
235
- render_score();
236
- WaitForVBlank();
237
- consoleVblank();
238
- }
239
- return 0;
496
+ WaitForVBlank();
497
+ bgSetScroll(1, 0, star_v); /* scroll regs: write inside vblank */
498
+ consoleVblank();
499
+ }
500
+ return 0;
240
501
  }