romdevtools 0.27.0 → 0.29.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 (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,169 +1,607 @@
1
- /* ── platformer.c — SNES PVSnesLib SIDE-SCROLLING platformer scaffold
1
+ /* ── platformer.c — SNES side-scrolling platformer (complete example game) ───
2
2
  *
3
- * A horizontally scrolling platformer: gravity + jump physics with
4
- * land-on-top collision against a static platform list spread across a
5
- * 512-px world. A camera follows the player; the BG scrolls in hardware
6
- * via bgSetScroll(0, camX, 0) and the player sprite draws in SCREEN
7
- * space (worldX - camX). Mirrors the GB/Genesis side-scroller scaffolds.
3
+ * CRAG CAPER a COMPLETE, working game: title screen, 1P mode and 2P
4
+ * ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
5
+ * has their own score and own 3 lives; player 2 plays on CONTROLLER 2),
6
+ * coins + distance scoring, persistent hi-score (battery SRAM), SPC music +
7
+ * SFX, and the SNES's answer to the fixed-HUD-over-scrolling-field problem:
8
+ * the HUD is simply ANOTHER BACKGROUND LAYER with its own scroll register.
8
9
  *
9
- * Physics is fixed-point: x/y in 1/16-pixel subpixel units for sub-pixel
10
- * acceleration.
10
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
11
+ * very different one. The markers tell you what's what:
12
+ * HARDWARE IDIOM (load-bearing) — dodges a documented SNES footgun; reshape
13
+ * your gameplay around it (see TROUBLESHOOTING before changing).
14
+ * GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
15
+ * freely.
11
16
  *
12
- * NOTE ON VISUALS: this scaffold uses the PVSnesLib console (text) BG,
13
- * which has no tiled platform art so as you scroll you see the on-BG
14
- * text slide (proof the hardware scroll register is moving) while the
15
- * player stays screen-centered. For visible tiled platforms across a
16
- * wide world, build a real tileset with gfx2snes + bgInitTileSet on a
17
- * 64-wide map and stream tilemap columns into VRAM during vblank as the
18
- * camera advances. See the SNES MENTAL_MODEL.md "Horizontal scrolling".
17
+ * What depends on what:
18
+ * data.asmfont + sprite/level tiles, sram_read16/write16 (battery SRAM
19
+ * needs 24-bit addressing tcc can't emit), and the bank-$7E telem block.
20
+ * Load-bearing.
21
+ * hdr.asm THIS PROJECT OVERRIDES the stock header to declare battery
22
+ * SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
23
+ * silently stop existing the build still succeeds.
24
+ * snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
25
+ * driver (music + 2 one-shot samples). #include'd, not separately built.
26
+ *
27
+ * ── THE TWO-LAYER SPLIT (the SNES bonus this example teaches) ───────────────
28
+ * Mode 1 gives three independent background layers, EACH with its own
29
+ * H/V scroll registers. So a fixed HUD over a scrolling playfield is just:
30
+ * BG0 (text console) — HUD + all menu text. Its scroll stays (0,0). Ever.
31
+ * BG1 — the level. One register write per frame (bgSetScroll) moves it.
32
+ * Zero raster tricks, zero CPU. Contrast the NES platformer example (this
33
+ * game's direct ancestor): the NES has ONE scroll for the WHOLE frame, so
34
+ * its fixed HUD costs a sprite-0-hit polling spin — ~35 scanlines of CPU
35
+ * burned EVERY frame waiting for the beam to clear the HUD before rewriting
36
+ * PPUSCROLL mid-frame. On SNES you only reach for that kind of mid-frame
37
+ * machinery (HDMA) when one layer must be two things at once — see the
38
+ * racing example's Mode 1/Mode 7 split.
39
+ *
40
+ * The level itself: a 256-px-wide COLUMN MAP (ground height + one-way
41
+ * platforms + pits) painted once into a 32x32 tilemap. 256 px is exactly
42
+ * the map's width, so a uint8 scroll wraps seamlessly — an endless looping
43
+ * run of pits, platforms, coins and spikes. Coins/spikes are sprites that
44
+ * drift with the scroll (world-anchored while on screen, respawning at the
45
+ * right edge).
19
46
  */
20
47
 
21
48
  #include <snes.h>
22
49
  #include "snes_sfx.c"
23
50
 
24
- extern char tilfont, palfont;
25
- extern char tilsprite, palsprite;
26
- extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
51
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
52
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
53
+ #define GAME_TITLE "CRAG CAPER"
54
+
55
+ extern char tilfont, palfont; /* HUD font + text palette (data.asm) */
56
+ extern char tilsprite, palsprite; /* player/coin/spike tiles + palette */
57
+ extern char tilbg, palbg; /* level tiles + sky/dirt/grass colours*/
27
58
 
28
59
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
29
60
  * No public prototype in console.h, so declare it; call once per frame. */
30
61
  extern void consoleVblank(void);
31
62
 
32
- /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
33
- * playfield reads as a real backdrop, not flat blank. Filled at runtime. */
34
- static u16 bg_map[32 * 32];
35
-
36
- typedef struct { s16 x, y, w, h; } Rect;
37
-
38
- #define WORLD_W 512
39
- #define SCREEN_W 256
40
-
41
- /* Platforms in WORLD coords, spread across the 512-px world. */
42
- static const Rect platforms[] = {
43
- { 0, 200, 512, 24 }, /* floor spans the world */
44
- { 30, 160, 56, 8 },
45
- { 110, 140, 64, 8 },
46
- { 190, 110, 48, 8 },
47
- { 60, 100, 32, 8 },
48
- { 300, 150, 64, 8 },
49
- { 400, 120, 56, 8 },
50
- { 360, 84, 48, 8 },
51
- { 460, 168, 48, 8 },
63
+ /* data.asm exports battery SRAM accessors ($70:0000 long addressing) and
64
+ * the bank-$7E telemetry block a headless test can find by scanning. */
65
+ extern u16 sram_read16(u16 offset);
66
+ extern void sram_write16(u16 offset, u16 value);
67
+ extern u8 telem[];
68
+
69
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
70
+ * VRAM budget (word addresses):
71
+ * $0000 OBJ tiles, $2000 level tiles, $3000 HUD font,
72
+ * $4000 level map (BG1), $6800 HUD/console text map (BG0).
73
+ * Sprite tile numbers + the level tile numbers the map painter uses. */
74
+ #define TILE_IDLE 0
75
+ #define TILE_JUMP 1
76
+ #define TILE_COIN 2
77
+ #define TILE_SPIKE 3
78
+ #define BG_CLOUD 1
79
+ #define BG_DIRT 2
80
+ #define BG_GRASS 3 /* also used for floating platforms (grass slabs) */
81
+
82
+ /* ── GAME LOGIC (clay) — the level ───────────────────────────────────────────
83
+ * A 32-column map; world x = (screen x + scroll) mod 256.
84
+ * ground_row[c] — tilemap row of the ground's grass top, 0xFF = pit.
85
+ * plat_row[c] — row of a one-way floating platform, 0 = none.
86
+ * Rows are tilemap rows (y = row*8). The SNES screen shows rows 0-27. */
87
+ #define NO_GROUND 0xFF
88
+ static const u8 ground_row[32] = {
89
+ 26, 26, 26, 26, 26, 26, 26, 26, /* start runway */
90
+ 26, NO_GROUND, NO_GROUND, 26, 26, 26, 26, 26, /* pit 1 (16 px) */
91
+ 26, 26, 26, 26, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 2 (24 px) */
92
+ 26, 26, 26, 26, 26, 26, 26, 26, 26,
52
93
  };
53
- #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
54
-
55
- #define GRAVITY 12
56
- #define MOVE_SPEED 24
57
- #define JUMP_VEL -200
58
- #define MAX_FALL 320
59
-
60
- static u8 on_platform(s16 px, s16 py) {
61
- u16 i;
62
- const Rect* p;
63
- for (i = 0; i < N_PLATFORMS; i++) {
64
- p = &platforms[i];
65
- if (py + 8 == p->y && px + 8 > p->x && px < p->x + p->w) return 1;
94
+ static const u8 plat_row[32] = {
95
+ 0, 0, 0, 0, 21, 21, 21, 0, /* slab before pit 1 */
96
+ 0, 0, 0, 0, 0, 0, 20, 20, /* slab mid-level */
97
+ 20, 0, 0, 0, 0, 0, 0, 0,
98
+ 0, 21, 21, 21, 0, 0, 0, 0, /* slab near the loop */
99
+ };
100
+
101
+ /* ── GAME LOGIC (clay) physics + tuning ── */
102
+ #define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
103
+ #define JUMP_VEL_Q44 (-40) /* launch vy (Q4.4) → ~50 px / ~6 tile apex */
104
+ #define MAX_VY_Q44 80 /* terminal velocity, 5 px/frame MUST stay *
105
+ * under 6: the landing probe's 6-px window *
106
+ * can't catch a faster fall (tunnelling) */
107
+ #define MOVE_SPEED 2 /* px/frame walk + scroll speed */
108
+ #define SCROLL_WALL 112 /* px: past this the world scrolls, not you */
109
+ #define GROUND_TOP 208 /* ground_row 26 * 8 */
110
+ #define SPIKE_Y 200 /* spikes stand on the ground */
111
+ #define NUM_COINS 3
112
+ #define NUM_SPIKES 2
113
+ #define START_LIVES 3
114
+
115
+ /* SRAM layout: [0]=magic "CG", [2]=hi-score, [4]=hi ^ 0x5AC3.
116
+ * Magic is written LAST in hi_save so a torn write never validates. */
117
+ #define SRAM_MAGIC 0x4743u
118
+
119
+ /* Game states — the shell every example shares: title → play → game over. */
120
+ #define ST_TITLE 0
121
+ #define ST_PLAY 1
122
+ #define ST_OVER 2
123
+
124
+ static u8 state;
125
+ static u8 px; /* player screen x */
126
+ static u16 py_q44; /* player y, Q4.4 fixed point — gravity adds
127
+ * <1 px/frame near the jump apex, so we
128
+ * need sub-pixel precision */
129
+ static s8 vy_q44;
130
+ static u8 on_ground;
131
+ static u8 scroll_x; /* level scroll — u8 wraps at 256 = exactly *
132
+ * one level loop (seamless) */
133
+ static u8 dist_sub; /* sub-counter: 64 px scrolled = +1 pt */
134
+ static u8 coin_x[NUM_COINS], coin_y[NUM_COINS];
135
+ static u8 spike_x[NUM_SPIKES], spike_active[NUM_SPIKES];
136
+
137
+ /* Players: index 0 = P1 (controller 1), 1 = P2 (controller 2 — alternating
138
+ * turns, arcade-classic style). Each has own score + own lives; the HUD
139
+ * shows the CURRENT player's numbers. */
140
+ static u8 two_player;
141
+ static u8 cur_player;
142
+ static u8 p_lives[2];
143
+ static u16 p_score[2];
144
+ static u16 hiscore;
145
+ static u8 turn_pause; /* freeze frames after a turn change */
146
+ static u8 sound_ok;
147
+ static u16 rng = 0xC0DE;
148
+ static u16 prev_pad0, prev_padP;
149
+ static u8 attract_sub; /* title attract: scroll every 2nd frame */
150
+ static char tbuf[8]; /* 5-digit score formatter output */
151
+
152
+ static u16 bg_map[32 * 32]; /* level tilemap staging (DMA'd at boot) */
153
+
154
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
155
+ static u8 random8(void) {
156
+ u16 r = rng;
157
+ r ^= r << 7;
158
+ r ^= r >> 9;
159
+ r ^= r << 8;
160
+ rng = r;
161
+ return (u8)r;
162
+ }
163
+
164
+ static u8 dist8(u8 a, u8 b) {
165
+ return (a > b) ? (u8)(a - b) : (u8)(b - a);
166
+ }
167
+
168
+ /* ── GAME LOGIC (clay) — battery SRAM hi-score (see sram_* in data.asm) ───── */
169
+ static u16 hi_load(void) {
170
+ u16 v;
171
+ if (sram_read16(0) != SRAM_MAGIC) return 0;
172
+ v = sram_read16(2);
173
+ if (sram_read16(4) != (u16)(v ^ 0x5AC3u)) return 0;
174
+ return v;
175
+ }
176
+
177
+ static void hi_save(u16 v) {
178
+ sram_write16(2, v);
179
+ sram_write16(4, (u16)(v ^ 0x5AC3u));
180
+ sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
181
+ }
182
+
183
+ /* ── GAME LOGIC (clay) — text helpers ──────────────────────────────────────── */
184
+ static void fmt_u16(u16 v) { /* 5 right-aligned digits into tbuf */
185
+ u8 i;
186
+ for (i = 0; i < 5; i++) { tbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
187
+ tbuf[5] = 0;
188
+ }
189
+
190
+ static void clear_row(u16 y) {
191
+ consoleDrawText(0, y, " ");
192
+ }
193
+
194
+ static void clear_rows(u16 a, u16 b) {
195
+ u16 y;
196
+ for (y = a; y <= b; y++) clear_row(y);
197
+ }
198
+
199
+ /* HUD row 1, on BG0 — fixed because BG0's scroll never moves (see the
200
+ * two-layer split note up top). Layout: "P1 L3 SC 00000 HI 00000". */
201
+ static void draw_hud(void) {
202
+ consoleDrawText(1, 1, cur_player ? "P2" : "P1");
203
+ tbuf[0] = 'L'; tbuf[1] = (char)('0' + p_lives[cur_player]); tbuf[2] = 0;
204
+ consoleDrawText(4, 1, tbuf);
205
+ fmt_u16(p_score[cur_player]);
206
+ consoleDrawText(10, 1, tbuf);
207
+ }
208
+
209
+ static void draw_hud_labels(void) {
210
+ consoleDrawText(7, 1, "SC");
211
+ consoleDrawText(17, 1, "HI");
212
+ fmt_u16(hiscore);
213
+ consoleDrawText(20, 1, tbuf);
214
+ }
215
+
216
+ /* ── GAME LOGIC (clay) — paint the level from the column map ─────────────────
217
+ * Composed once in WRAM and DMA'd to VRAM at boot (bgInitMapSet). The level
218
+ * is static; only the scroll register moves it. Rows 0-2 stay sky so the
219
+ * HUD text floats over clean backdrop. Map entries are plain tile numbers
220
+ * (palette block 0, no flips, no priority). */
221
+ static void paint_level(void) {
222
+ u8 r, c, g;
223
+ u16 t;
224
+ for (r = 0; r < 32; r++) {
225
+ for (c = 0; c < 32; c++) {
226
+ g = ground_row[c];
227
+ t = 0; /* sky backdrop */
228
+ if (plat_row[c] && r == plat_row[c]) t = BG_GRASS; /* floating slab */
229
+ else if (g != NO_GROUND) {
230
+ if (r == g) t = BG_GRASS; /* ground surface */
231
+ else if (r > g) t = BG_DIRT; /* ground body */
232
+ }
233
+ if (t == 0 && r >= 14 && r <= 18) { /* sparse cloud band */
234
+ if (((r * 7 + c * 5) & 15) == 0) t = BG_CLOUD;
235
+ }
236
+ bg_map[(u16)(r << 5) + c] = t;
237
+ }
238
+ }
239
+ }
240
+
241
+ /* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
242
+ static const u8 coin_heights[4] = { 184, 160, 128, 152 };
243
+ static void respawn_coin(u8 i) {
244
+ coin_x[i] = (u8)(232 + (random8() & 15)); /* enter at the right */
245
+ coin_y[i] = coin_heights[random8() & 3];
246
+ }
247
+
248
+ static void try_spawn_spike(u8 i) {
249
+ /* Anchor only over ground: an inactive spike rolls a low per-frame
250
+ * chance, and only spawns if the level column entering at the right
251
+ * edge has ground under it (never floats over a pit). */
252
+ u8 c = (u8)(248 + scroll_x) >> 3;
253
+ if (ground_row[c] == NO_GROUND) return;
254
+ if (random8() > 4) return;
255
+ spike_x[i] = 248;
256
+ spike_active[i] = 1;
257
+ }
258
+
259
+ /* Hide every gameplay sprite (OAM ids 0,4,..,20 = player, 3 coins, 2 spikes). */
260
+ static void hide_actors(void) {
261
+ u8 i;
262
+ for (i = 0; i < 24; i += 4) oamSetVisible(i, OBJ_HIDE);
263
+ }
264
+
265
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
266
+ * Stage ALL sprites BEFORE WaitForVBlank. PVSnesLib's NMI handler DMAs the
267
+ * shadow OAM to the real OAM every vblank (on channel 7 — never park HDMA
268
+ * there), copying whatever the shadow holds AT THAT MOMENT. Stage-then-wait;
269
+ * flipping it shows stale/empty sprites. oamSet rewrites x/y, which is also
270
+ * what un-hides a sprite after OBJ_HIDE (hide just parks it off-screen). */
271
+ static void stage_actors(void) {
272
+ u8 i, y8;
273
+ y8 = (u8)(py_q44 >> 4);
274
+ /* Blink the player during the turn-change breather. */
275
+ if (turn_pause == 0 || (turn_pause & 4))
276
+ oamSet(0, px, y8, 3, 0, 0, on_ground ? TILE_IDLE : TILE_JUMP, 0);
277
+ else
278
+ oamSetVisible(0, OBJ_HIDE);
279
+ for (i = 0; i < NUM_COINS; i++)
280
+ oamSet((u16)(4 + (i << 2)), coin_x[i], coin_y[i], 3, 0, 0, TILE_COIN, 0);
281
+ for (i = 0; i < NUM_SPIKES; i++) {
282
+ if (spike_active[i])
283
+ oamSet((u16)(16 + (i << 2)), spike_x[i], SPIKE_Y, 3, 0, 0, TILE_SPIKE, 0);
284
+ else
285
+ oamSetVisible((u16)(16 + (i << 2)), OBJ_HIDE);
286
+ }
287
+ }
288
+
289
+ /* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
290
+ static void title_enter(void) {
291
+ bgSetEnable(1); /* the level scrolls behind the title */
292
+ hide_actors();
293
+ clear_rows(0, 27);
294
+ consoleDrawText(11, 3, GAME_TITLE);
295
+ consoleDrawText(10, 6, "A - 1P GAME");
296
+ consoleDrawText(10, 7, "B - 2P TURNS");
297
+ consoleDrawText(11, 9, "HI");
298
+ fmt_u16(hiscore);
299
+ consoleDrawText(14, 9, tbuf);
300
+ state = ST_TITLE;
301
+ }
302
+
303
+ /* ── GAME LOGIC (clay) — start a turn / a run ── */
304
+ static void begin_turn(void) {
305
+ px = 24;
306
+ py_q44 = (u16)(GROUND_TOP - 8) << 4;
307
+ vy_q44 = 0;
308
+ on_ground = 1;
309
+ scroll_x = 0;
310
+ dist_sub = 0;
311
+ coin_x[0] = 88; coin_y[0] = 184;
312
+ coin_x[1] = 152; coin_y[1] = 160;
313
+ coin_x[2] = 216; coin_y[2] = 128;
314
+ spike_x[0] = 136; spike_active[0] = 1; /* both anchored on ground at */
315
+ spike_x[1] = 224; spike_active[1] = 1; /* scroll 0 — see ground_row */
316
+ turn_pause = 48; /* "P1/P2 GO" breather */
317
+ prev_padP = 0xFFFF; /* swallow held buttons across the turn change —
318
+ * without this the A that picked 1P on the title
319
+ * instantly jumps (classic edge-detect reuse bug) */
320
+ draw_hud();
321
+ if (two_player)
322
+ consoleDrawText(11, 4, cur_player ? "PLAYER 2 GO" : "PLAYER 1 GO");
323
+ }
324
+
325
+ static void start_game(u8 players) {
326
+ u8 i;
327
+ two_player = players;
328
+ cur_player = 0;
329
+ p_score[0] = p_score[1] = 0;
330
+ p_lives[0] = START_LIVES;
331
+ p_lives[1] = players ? START_LIVES : 0;
332
+ clear_rows(0, 27);
333
+ draw_hud_labels();
334
+ for (i = 0; i < 24; i += 4) oamSetEx(i, OBJ_SMALL, OBJ_SHOW);
335
+ begin_turn();
336
+ if (sound_ok) sfx_play(1); /* start blip */
337
+ state = ST_PLAY;
338
+ }
339
+
340
+ static void game_over(void) {
341
+ u16 best = p_score[0];
342
+ if (two_player && p_score[1] > best) best = p_score[1];
343
+ if (best > hiscore) { hiscore = best; hi_save(hiscore); }
344
+ bgSetDisable(1); /* clean card: sky backdrop + text only */
345
+ hide_actors();
346
+ clear_rows(0, 27);
347
+ consoleDrawText(11, 6, "GAME OVER");
348
+ consoleDrawText(9, 10, "P1");
349
+ fmt_u16(p_score[0]); consoleDrawText(15, 10, tbuf);
350
+ if (two_player) {
351
+ consoleDrawText(9, 12, "P2");
352
+ fmt_u16(p_score[1]); consoleDrawText(15, 12, tbuf);
353
+ }
354
+ consoleDrawText(9, 15, "HI");
355
+ fmt_u16(hiscore); consoleDrawText(15, 15, tbuf);
356
+ consoleDrawText(9, 20, "START - TITLE");
357
+ if (sound_ok) sfx_play(2); /* game-over thud */
358
+ state = ST_OVER;
359
+ }
360
+
361
+ /* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
362
+ static void kill_player(void) {
363
+ u8 other;
364
+ if (sound_ok) sfx_play(2);
365
+ if (p_lives[cur_player] > 0) --p_lives[cur_player];
366
+ if (two_player) {
367
+ other = cur_player ^ 1;
368
+ if (p_lives[other] > 0) cur_player = other; /* swap turns */
369
+ else if (p_lives[cur_player] == 0) { game_over(); return; }
370
+ } else if (p_lives[0] == 0) {
371
+ game_over();
372
+ return;
373
+ }
374
+ begin_turn();
375
+ }
376
+
377
+ /* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
378
+ * One-way platforms, classic style: only catch the player while FALLING
379
+ * through a narrow window at the surface. The window is 6 px tall —
380
+ * top-1 (the standing snap parks feet at top, and gravity's sub-pixel
381
+ * trickle doesn't move the integer Y every frame; without the -1 slack the
382
+ * player "stands" with on_ground=0 most frames, so jumps only register on
383
+ * lucky frames and the idle/jump sprite flickers) through top+4 (so a
384
+ * 5 px/frame terminal-velocity fall can't step over it). */
385
+ static u8 land_top(u8 c, u8 feet) {
386
+ u8 r, top;
387
+ r = plat_row[c];
388
+ if (r) {
389
+ top = (u8)(r << 3);
390
+ if ((u8)(feet + 1) >= top && feet <= (u8)(top + 4)) return top;
391
+ }
392
+ r = ground_row[c];
393
+ if (r != NO_GROUND) {
394
+ top = (u8)(r << 3);
395
+ if ((u8)(feet + 1) >= top && feet <= (u8)(top + 4)) return top;
396
+ }
397
+ return 0;
398
+ }
399
+
400
+ /* ── GAME LOGIC (clay) — one frame of gameplay ─────────────────────────────── */
401
+ static void play_update(void) {
402
+ u16 pad;
403
+ u8 i, delta, y8, feet, c0, c1, top, killed;
404
+
405
+ if (turn_pause) { /* freeze gameplay, keep the frame honest */
406
+ --turn_pause;
407
+ if (turn_pause == 0) clear_row(4); /* drop the "Pn GO" banner */
408
+ stage_actors();
409
+ return;
410
+ }
411
+
412
+ /* Input — the CURRENT player's controller (alternating turns: P2 is on
413
+ * controller 2 — padsCurrent(1); that one index IS the 2P wiring). Past
414
+ * SCROLL_WALL the world scrolls instead of the player (the camera never
415
+ * scrolls back — the classic one-way camera). */
416
+ pad = padsCurrent(cur_player);
417
+ delta = 0;
418
+ if (pad & KEY_RIGHT) {
419
+ if (px < SCROLL_WALL) px += MOVE_SPEED;
420
+ else { scroll_x += MOVE_SPEED; delta = MOVE_SPEED; }
421
+ }
422
+ if ((pad & KEY_LEFT) && px > 8) px -= MOVE_SPEED;
423
+ if ((pad & (KEY_B | KEY_A)) && !(prev_padP & (KEY_B | KEY_A)) && on_ground) {
424
+ vy_q44 = JUMP_VEL_Q44;
425
+ on_ground = 0;
426
+ if (sound_ok) sfx_play(1); /* jump blip */
427
+ }
428
+ prev_padP = pad;
429
+
430
+ /* World objects drift left as the level scrolls (world-anchored). */
431
+ if (delta) {
432
+ dist_sub += delta;
433
+ if (dist_sub >= 64) { /* distance pay */
434
+ dist_sub -= 64;
435
+ ++p_score[cur_player];
436
+ draw_hud();
437
+ }
438
+ for (i = 0; i < NUM_COINS; i++) {
439
+ if (coin_x[i] < 16 + delta) respawn_coin(i);
440
+ else coin_x[i] -= delta;
441
+ }
442
+ for (i = 0; i < NUM_SPIKES; i++) {
443
+ if (!spike_active[i]) continue;
444
+ if (spike_x[i] < 16 + delta) spike_active[i] = 0;
445
+ else spike_x[i] -= delta;
66
446
  }
67
- return 0;
447
+ }
448
+ for (i = 0; i < NUM_SPIKES; i++)
449
+ if (!spike_active[i]) try_spawn_spike(i);
450
+
451
+ /* Physics: gravity + sub-pixel Y. */
452
+ if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
453
+ py_q44 += vy_q44;
454
+ y8 = (u8)(py_q44 >> 4);
455
+
456
+ /* Fell into a pit (below the screen) → lose the turn. */
457
+ if (y8 >= 232) {
458
+ kill_player();
459
+ return;
460
+ }
461
+
462
+ /* Landing — probe the two level columns under the player's feet. */
463
+ if (vy_q44 >= 0) {
464
+ feet = (u8)(y8 + 8);
465
+ c0 = (u8)(px + scroll_x) >> 3;
466
+ c1 = (u8)(px + scroll_x + 7) >> 3;
467
+ top = land_top(c0, feet);
468
+ if (top == 0) top = land_top(c1, feet);
469
+ if (top) {
470
+ py_q44 = (u16)(top - 8) << 4;
471
+ vy_q44 = 0;
472
+ on_ground = 1;
473
+ } else {
474
+ on_ground = 0; /* walked off */
475
+ }
476
+ }
477
+
478
+ /* Coins (collect) + spikes (death). */
479
+ for (i = 0; i < NUM_COINS; i++) {
480
+ if (dist8(coin_x[i], px) < 8 && dist8(coin_y[i], y8) < 8) {
481
+ p_score[cur_player] += 10;
482
+ if (sound_ok) sfx_play(1); /* coin ping */
483
+ draw_hud();
484
+ respawn_coin(i);
485
+ }
486
+ }
487
+ killed = 0;
488
+ for (i = 0; i < NUM_SPIKES; i++) {
489
+ if (!spike_active[i]) continue;
490
+ if (dist8(spike_x[i], px) < 7 && dist8(SPIKE_Y, y8) < 7) {
491
+ killed = 1;
492
+ break;
493
+ }
494
+ }
495
+ if (killed) { kill_player(); return; }
496
+
497
+ stage_actors();
498
+ }
499
+
500
+ /* Headless-test telemetry — written once per frame into the bank-$7E telem
501
+ * block (data.asm). A test harness finds it by scanning WRAM for the
502
+ * "CG"+0xBD signature, then plays the game from real state instead of
503
+ * parsing pixels. spike_x is always even (spawns at 248, drifts by 2), so
504
+ * its bit 0 carries the active flag. Costs ~20 byte-writes; delete freely. */
505
+ static void telem_update(void) {
506
+ telem[0] = 'C'; telem[1] = 'G'; telem[2] = 0xBD;
507
+ telem[3] = state;
508
+ telem[4] = (u8)((sound_ok << 7) | (two_player << 1) | cur_player);
509
+ telem[5] = p_lives[0];
510
+ telem[6] = p_lives[1];
511
+ telem[7] = px;
512
+ telem[8] = (u8)(py_q44 >> 4);
513
+ telem[9] = scroll_x;
514
+ telem[10] = on_ground;
515
+ telem[11] = (u8)p_score[0]; telem[12] = (u8)(p_score[0] >> 8);
516
+ telem[13] = (u8)p_score[1]; telem[14] = (u8)(p_score[1] >> 8);
517
+ telem[15] = turn_pause;
518
+ telem[16] = (u8)(spike_x[0] | spike_active[0]);
519
+ telem[17] = (u8)(spike_x[1] | spike_active[1]);
68
520
  }
69
521
 
70
522
  int main(void) {
71
- s32 px = 32 << 4, py = 100 << 4;
72
- s32 vx = 0, vy = 0;
73
- s32 np;
74
- s16 ipx, ipy, npy, camX = 0;
75
- u8 grounded;
76
- u16 i, pad, prev = 0;
77
- const Rect* p;
78
-
79
- consoleSetTextMapPtr(0x6800);
80
- consoleSetTextGfxPtr(0x3000);
81
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
82
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
83
- setMode(BG_MODE1, 0);
84
- /* consoleInitText DMAs the font but does NOT set the PPU BG base
85
- * registers — point BG0 at the same font ($3000) + map ($6800). */
86
- bgSetGfxPtr(0, 0x3000);
87
- bgSetMapPtr(0, 0x6800, SC_32x32);
88
-
89
- /* BG1 = full-screen wallpaper so the playfield never reads as blank.
90
- * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
91
- * the console gfx $3000 / map $6800). Map entries use palette block 1
92
- * (0x0400) so the wallpaper palette doesn't disturb the console font
93
- * palette in block 0 (HUD text stays legible). */
94
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
95
- 32, 32, BG_16COLORS, 0x2000);
96
- for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
97
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
98
- bgSetEnable(1);
99
- bgSetDisable(2);
100
-
101
- oamInitGfxSet(&tilsprite, 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
102
-
103
- consoleDrawText(2, 1, "D-PAD MOVE A JUMP");
104
- /* Column markers across the BG so the hardware scroll is visible as
105
- * you move (the console map is 32 cells / 256 px and wraps). */
106
- consoleDrawText( 1, 12, "|0");
107
- consoleDrawText( 9, 12, "|64");
108
- consoleDrawText(17, 12, "|128");
109
- consoleDrawText(25, 12, "|192");
110
-
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. */
115
- setScreenOn();
116
- sfx_init();
117
-
118
- while (1) {
119
- pad = padsCurrent(0);
120
- vx = 0;
121
- if (pad & KEY_LEFT) vx = -MOVE_SPEED;
122
- if (pad & KEY_RIGHT) vx = MOVE_SPEED;
123
-
124
- ipx = px >> 4;
125
- ipy = py >> 4;
126
- grounded = on_platform(ipx, ipy);
127
- if ((pad & KEY_A) && !(prev & KEY_A) && grounded) { vy = JUMP_VEL; sfx_play(1); }
128
- prev = pad;
129
-
130
- vy += GRAVITY;
131
- if (vy > MAX_FALL) vy = MAX_FALL;
132
- if (grounded && vy > 0) vy = 0;
133
-
134
- px += vx;
135
- if (px < 0) px = 0;
136
- if (px > (WORLD_W - 8) << 4) px = (WORLD_W - 8) << 4;
137
-
138
- np = py + vy;
139
- npy = np >> 4;
140
- if (vy > 0) {
141
- for (i = 0; i < N_PLATFORMS; i++) {
142
- p = &platforms[i];
143
- if (ipy + 8 <= p->y && npy + 8 >= p->y
144
- && ipx + 8 > p->x && ipx < p->x + p->w) {
145
- py = (p->y - 8) << 4;
146
- vy = 0;
147
- goto done;
148
- }
149
- }
150
- }
151
- py = np;
152
- if (py > 224 << 4) { py = 0; vy = 0; }
153
- done: ;
154
-
155
- /* Camera follows the player, centered, clamped to the world.
156
- * bgSetScroll moves the BG in hardware; the player draws in
157
- * SCREEN space (worldX - camX). */
158
- camX = (px >> 4) - (SCREEN_W / 2 - 4);
159
- if (camX < 0) camX = 0;
160
- if (camX > WORLD_W - SCREEN_W) camX = WORLD_W - SCREEN_W;
161
- bgSetScroll(0, camX, 0);
162
-
163
- oamSetXY(0, (px >> 4) - camX, py >> 4);
164
- oamUpdate();
165
- WaitForVBlank();
166
- consoleVblank();
523
+ u16 pad;
524
+
525
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
526
+ * Init order: console text pointers FIRST, then mode, then VRAM uploads
527
+ * while the screen is still off (forced blank — VRAM DMA during active
528
+ * display is lost or corrupts). consoleInitText DMAs the font but does
529
+ * NOT set the PPU BG base registers — bgSetGfxPtr/bgSetMapPtr must agree
530
+ * with the console pointers or text renders as garbage tiles. */
531
+ consoleSetTextMapPtr(0x6800);
532
+ consoleSetTextGfxPtr(0x3000);
533
+ consoleSetTextOffset(0x0000);
534
+ consoleInitText(0, 16 * 2, &tilfont, &palfont);
535
+ setMode(BG_MODE1, 0);
536
+ bgSetGfxPtr(0, 0x3000);
537
+ bgSetMapPtr(0, 0x6800, SC_32x32);
538
+
539
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
540
+ * The two-layer split (see the header essay): BG0 = HUD/text, scroll
541
+ * pinned at (0,0); BG1 = the level, moved by one bgSetScroll per frame.
542
+ * palbg loads into CGRAM block 0 AFTER the font palette and is a superset
543
+ * of it (colour 1 stays white) so HUD ink and level tiles share the
544
+ * block without fighting. BG2 carries power-on garbage in Mode 1 — keep
545
+ * it disabled. */
546
+ paint_level();
547
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 0, 4 * 32, 32, BG_16COLORS, 0x2000);
548
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
549
+ bgSetEnable(1);
550
+ bgSetDisable(2);
551
+
552
+ /* OBJ: 8x8 sprites (player, coins, spikes) at VRAM $0000. */
553
+ oamInitGfxSet(&tilsprite, 4 * 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
554
+
555
+ setScreenOn();
556
+
557
+ /* ── HARDWARE IDIOM (load-bearing) sfx_init AFTER setScreenOn, and CHECK
558
+ * the return: a wedged SPC700 must not take the video down with it. ── */
559
+ sound_ok = (sfx_init() == 0);
560
+ /* ── HARDWARE IDIOM (load-bearing) one frame between init and the first
561
+ * command. sfx_init returns the instant the SPC echoes the jump command,
562
+ * but the driver then spends ~50 port writes initialising the DSP BEFORE
563
+ * it seeds its command edge-detector from $2140. Send a command in that
564
+ * window and the seed swallows it music silently never starts. A
565
+ * WaitForVBlank is thousands of SPC cycles deterministic cure. ── */
566
+ WaitForVBlank();
567
+ if (sound_ok) sfx_music_play();
568
+
569
+ hiscore = hi_load(); /* battery SRAM — 0 on first boot */
570
+ prev_pad0 = prev_padP = 0;
571
+ title_enter();
572
+
573
+ while (1) {
574
+ pad = padsCurrent(0);
575
+
576
+ if (state == ST_TITLE) {
577
+ /* attract: the level drifts by under the title — the scroll register
578
+ * demo, and the first thing a fork breaks if the layers get swapped */
579
+ attract_sub ^= 1;
580
+ if (attract_sub) scroll_x++;
581
+ if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
582
+ (pad & KEY_START && !(prev_pad0 & KEY_START))) {
583
+ start_game(0);
584
+ } else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
585
+ start_game(1);
586
+ }
587
+ } else if (state == ST_PLAY) {
588
+ play_update();
589
+ } else { /* ST_OVER */
590
+ if ((pad & (KEY_START | KEY_A)) && !(prev_pad0 & (KEY_START | KEY_A)))
591
+ title_enter();
167
592
  }
168
- return 0;
593
+ prev_pad0 = pad;
594
+ telem_update();
595
+ oamUpdate();
596
+
597
+ WaitForVBlank();
598
+ /* ── HARDWARE IDIOM (load-bearing) — scroll + text commits in vblank.
599
+ * bgSetScroll writes the BG1 scroll registers directly; mid-frame the
600
+ * beam would render the top of the frame with the old value and the
601
+ * bottom with the new (a shear). BG0 gets NO scroll write, ever —
602
+ * that's the whole fixed-HUD trick. ── */
603
+ bgSetScroll(1, scroll_x, 0);
604
+ consoleVblank();
605
+ }
606
+ return 0;
169
607
  }