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,204 +1,866 @@
1
- /* ── platformer.c — Game Boy SIDE-SCROLLING platformer scaffold ─────
1
+ /* ── platformer.c — Game Boy side-scrolling platformer (complete example game)
2
2
  *
3
- * A horizontally scrolling platformer: the world is 256 px wide (the
4
- * full wrapping BG map), a camera follows the player and scrolls the BG
5
- * via the SCX register. Gravity + jump + land-on-top collision against
6
- * a static platform list in WORLD coords.
3
+ * GULLY GALLOP a COMPLETE, working game: title screen, gravity + jump
4
+ * physics with sub-pixel precision, one-way platforms, pits and spikes,
5
+ * coins + distance scoring, persistent hi-score (battery cart RAM), music
6
+ * + SFX, and the Game Boy's signature WINDOW-LAYER HUD: a fixed score/
7
+ * lives strip that the SCX-scrolling level slides beneath, with zero
8
+ * mid-frame raster tricks.
7
9
  *
8
- * Subpixel state (x/y in 1/16-pixel units) for fine acceleration. The
9
- * player sprite draws at SCREEN x = (worldX>>4) - camX.
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 GB footgun; reshape
13
+ * your gameplay around it (see TROUBLESHOOTING before changing).
14
+ * GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
15
+ * freely.
10
16
  *
11
- * The world here is one BG map (256 px) so plain SCX scrolls it with no
12
- * streaming. For a WIDER world, stream the next BG-map column each time
13
- * camX crosses an 8-px boundary see the GB MENTAL_MODEL.md.
17
+ * SINGLE-PLAYER BY DESIGN (the honest handheld story): the Game Boy has ONE
18
+ * controller. Multiplayer on real hardware means the link cable, and a
19
+ * single emulator instance cannot emulate a second Game Boy on the other
20
+ * end of that cable — so this game ships 1P only instead of faking a 2P
21
+ * mode the platform can't deliver. (Consoles' examples have real 2P.)
22
+ *
23
+ * What depends on what:
24
+ * gb_hardware.h — register names (LCDC/WX/WY/NRxx/...) + LCDC bit masks.
25
+ * gb_runtime.{h,c} — vblank wait (HALT-driven), joypad, shadow OAM +
26
+ * the OAM-DMA-from-HRAM routine, VRAM-safe memcpy, APU helpers.
27
+ * gb_crt0.s — boot + interrupt vectors + the cartridge header window.
28
+ * It DECLARES the cart as MBC1+RAM+BATTERY ($0147=$03, $0149=$02) —
29
+ * that declaration is what makes hiscore_save() below persist (the
30
+ * emulator sizes battery SAVE_RAM from those two header bytes).
31
+ * Load-bearing; edit with TROUBLESHOOTING open.
32
+ *
33
+ * The level: a 256-px-wide COLUMN MAP (ground height + one-way platforms +
34
+ * pits) painted once into the wrapping 32-wide BG map, so the uint8 SCX
35
+ * scroll wraps PERFECTLY seamless — an endless looping run of pits,
36
+ * platforms, coins and spikes. Coins/spikes are sprites that drift with
37
+ * the scroll (world-anchored while on screen, respawning at the right
38
+ * edge). The camera is one-way (the classic runner camera): past the
39
+ * scroll wall the world scrolls instead of the player.
40
+ *
41
+ * Frame budget (59.7 fps, ~17 556 machine cycles/frame, vblank = 10 of 154
42
+ * lines ≈ 1 140 cycles): everything VRAM/OAM-touching below happens in the
43
+ * vblank slice (OAM DMA ~165 cycles + ≤ 11 queued HUD/map bytes + one SCX
44
+ * write); game logic (player physics, a two-column landing probe, 3 coins
45
+ * + 2 spikes of AABB, staging 6 OAM slots) runs in the other 144 lines.
46
+ * Comfortable.
14
47
  */
15
48
 
16
49
  #include "gb_hardware.h"
17
50
  #include "gb_runtime.h"
18
51
 
52
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
53
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
54
+ #define GAME_TITLE "GULLY GALLOP"
55
+
56
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
57
+ * Tile inventory. GB tiles are 16 bytes: 8 rows × [low-plane byte,
58
+ * high-plane byte]. Pixel colour index = (hi_bit << 1) | lo_bit.
59
+ * lo only = colour 1 hi only = colour 2 both = colour 3
60
+ * With BGP = $E4 below the BG reads 0 = white (sky), 1 = light grey,
61
+ * 2 = dark grey (dirt), 3 = black (text/grass tops). */
19
62
  static const uint8_t tile_blank[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
20
- static const uint8_t tile_player[16] = {
21
- 0x18,0x18, 0x3C,0x3C, 0x7E,0x7E, 0xFF,0xFF,
22
- 0xFF,0xFF, 0x7E,0x7E, 0x3C,0x3C, 0x18,0x18,
63
+ static const uint8_t tile_player[16] = { /* round body + legs */
64
+ 0x3C,0x00, 0x7E,0x24, 0xFF,0x24, 0xFF,0x00, /* body c1, eyes c3 */
65
+ 0xFF,0x00, 0x7E,0x00, 0x66,0x00, 0x66,0x00,
23
66
  };
24
- static const uint8_t tile_platform[16] = {
25
- 0xFF,0xFF, 0x80,0x80, 0x80,0x80, 0x80,0x80,
26
- 0x80,0x80, 0x80,0x80, 0x80,0x80, 0xFF,0xFF,
67
+ static const uint8_t tile_player_jump[16] = { /* arms up */
68
+ 0x18,0x00, 0x7E,0x24, 0xFF,0x24, 0xFF,0x00,
69
+ 0xE7,0x00, 0xC3,0x00, 0x81,0x00, 0x00,0x00,
27
70
  };
28
- /* ── Backdrop tiles ───────────────────────────────────────────────────
29
- * Fill the whole world so the screen is never one flat colour (the #1 GB
30
- * "why is it blank" footgun). tile_sky is a sparse dot pattern over the
31
- * sky; tile_ground is a textured dirt fill under the floor line. */
32
- static const uint8_t tile_sky[16] = {
33
- 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x20,0x20,
34
- 0x00,0x00, 0x00,0x00, 0x02,0x02, 0x00,0x00,
71
+ static const uint8_t tile_coin[16] = { /* disc c1, ring c3 */
72
+ 0x3C,0x00, 0x7E,0x3C, 0xFF,0x66, 0xFF,0x5A,
73
+ 0xFF,0x5A, 0xFF,0x66, 0x7E,0x3C, 0x3C,0x00,
35
74
  };
36
- static const uint8_t tile_ground[16] = {
37
- 0xFF,0x00, 0xDB,0x24, 0xFF,0x00, 0x6D,0x92,
38
- 0xFF,0x00, 0xDB,0x24, 0xFF,0x00, 0x6D,0x92,
75
+ static const uint8_t tile_spike[16] = { /* solid c3 spike */
76
+ 0x00,0x00, 0x18,0x18, 0x18,0x18, 0x3C,0x3C,
77
+ 0x3C,0x3C, 0x7E,0x7E, 0x7E,0x7E, 0xFF,0xFF,
39
78
  };
40
- #define T_BLANK 0
41
- #define T_PLATFORM 2
42
- #define T_SKY 3
43
- #define T_GROUND 4
44
-
45
- static const uint16_t obj_palette[4] = { 0x7FFF, 0x001F, 0x03E0, 0x7C00 };
46
- /* BG palette: 0 sky-blue, 1 mid, 2 dirt-dark, 3 near-black detail. */
47
- static const uint16_t bg_palette[4] = { 0x7E10, 0x5294, 0x114A, 0x0000 };
48
-
49
- typedef struct { int16_t x, y, w, h; } Rect;
50
-
51
- #define WORLD_W 256
52
- #define SCREEN_W 160
53
-
54
- /* Platforms in WORLD coords, spread across the 256-px world. */
55
- static const Rect platforms[] = {
56
- { 0, 128, 256, 16 }, /* floor spans the world */
57
- { 16, 100, 40, 8 },
58
- { 96, 96, 32, 8 },
59
- { 168, 80, 40, 8 },
60
- { 56, 64, 32, 8 },
61
- { 200, 110, 40, 8 },
62
- { 130, 48, 40, 8 },
79
+ /* Backdrop tiles. tile_sky carries two colour-1 dot pixels so even "empty"
80
+ * sky is never one flat colour (the render-health floor every example
81
+ * keeps), and the dots make horizontal scroll motion visible everywhere. */
82
+ static const uint8_t tile_sky[16] = { /* white + c1 specks */
83
+ 0x00,0x00, 0x20,0x00, 0x00,0x00, 0x00,0x00,
84
+ 0x02,0x00, 0x00,0x00, 0x08,0x00, 0x00,0x00,
63
85
  };
64
- #define N_PLATFORMS (sizeof(platforms)/sizeof(platforms[0]))
65
-
66
- static uint8_t on_platform(int16_t px, int16_t py) {
67
- uint8_t i;
68
- const Rect *p;
69
- for (i = 0; i < N_PLATFORMS; i++) {
70
- p = &platforms[i];
71
- if (py + 8 == p->y && px + 8 > p->x && px < p->x + p->w) return 1;
72
- }
73
- return 0;
86
+ static const uint8_t tile_cloud[16] = { /* c1 puff */
87
+ 0x00,0x00, 0x18,0x00, 0x3C,0x00, 0x7E,0x00,
88
+ 0x7E,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
89
+ };
90
+ static const uint8_t tile_dirt[16] = { /* c2 fill, c0 pores */
91
+ 0x00,0xFF, 0x00,0xEF, 0x00,0xFF, 0x00,0xFE,
92
+ 0x00,0xFF, 0x00,0xDF, 0x00,0xFF, 0x00,0xFB,
93
+ };
94
+ static const uint8_t tile_grass[16] = { /* c3 turf over dirt */
95
+ 0xFF,0xFF, 0xFF,0xFF, 0x00,0xFF, 0x00,0xEF,
96
+ 0x00,0xFF, 0x00,0xFE, 0x00,0xFF, 0x00,0xFF,
97
+ };
98
+ static const uint8_t tile_plat[16] = { /* one-way slab */
99
+ 0xFF,0xFF, 0xFF,0xFF, 0x00,0xFF, 0x00,0xDB,
100
+ 0x00,0xFF, 0x00,0x00, 0x00,0x00, 0x00,0x00,
101
+ };
102
+ static const uint8_t tile_hudbar[16] = { /* solid colour 2 */
103
+ 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
104
+ 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
105
+ };
106
+
107
+ /* Tile indices ($8000 unsigned addressing — LCDC bit 4 set below). Sprites
108
+ * and BG share the $8000 table in this layout, so one upload serves both. */
109
+ #define T_PLAYER 1
110
+ #define T_JUMP 2
111
+ #define T_COIN 3
112
+ #define T_SPIKE 4
113
+ #define T_SKY 5
114
+ #define T_CLOUD 6
115
+ #define T_DIRT 7
116
+ #define T_GRASS 8
117
+ #define T_PLAT 9
118
+ #define T_HUDBAR 10
119
+ /* Font: '0'-'9' → 16..25, 'A'-'Z' → 26..51, '-' → 52 (see char_tile). */
120
+ #define T_DIGIT0 16
121
+ #define T_ALPHA 26
122
+ #define T_DASH 52
123
+
124
+ /* 1bpp font (same glyph set as the NES/SMS examples — 0-9, A-Z, '-').
125
+ * Stored 8 bytes/glyph and expanded to 2bpp colour 3 at upload time, so
126
+ * the ROM carries 296 bytes of font instead of 592. */
127
+ static const uint8_t font8[37][8] = {
128
+ /* 0-9 */
129
+ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
130
+ {0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
131
+ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
132
+ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
133
+ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
134
+ /* A-Z */
135
+ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
136
+ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
137
+ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
138
+ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3E,0x00}, {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
139
+ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00}, {0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00},
140
+ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
141
+ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
142
+ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
143
+ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
144
+ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
145
+ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
146
+ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
147
+ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
148
+ /* '-' */
149
+ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
150
+ };
151
+
152
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
153
+ * THE WINDOW-LAYER HUD — the Game Boy's signature "fixed HUD over a
154
+ * scrolling world" technique. The window is a second BG plane with its own
155
+ * 32×32 tile map and NO scroll registers: it always draws its map from
156
+ * (0,0), pinned to the screen, on top of the BG. So the HUD lives in the
157
+ * window and the playfield lives in the BG — SCY/SCX scroll the world all
158
+ * they like and the HUD never moves. No raster splits, no IRQ timing (the
159
+ * NES needs a sprite-0 polling dance for this exact effect; on GB it's
160
+ * three register writes). This game scrolls SCX (horizontal runner); the
161
+ * shmup example scrolls SCY — same idiom, either axis.
162
+ *
163
+ * The three registers, and their two famous footguns:
164
+ * WY ($FF4A) — first screen LINE the window covers. We use 128: lines
165
+ * 0-127 are playfield, 128-143 (two tile rows) are HUD.
166
+ * WX ($FF4B) — screen column PLUS SEVEN. WX=7 means "left edge". The
167
+ * -7 offset is hardware fact, not a library quirk: WX=0..6 glitches
168
+ * (real DMG pixel pipeline artifacts), WX≥167 pushes it off-screen.
169
+ * LCDC bit 5 — window enable; bit 6 — which map it reads ($9800/$9C00).
170
+ *
171
+ * FOOTGUN 1 — "the window ate the bottom of my screen": once the window
172
+ * starts on a line it covers EVERY line from there DOWN, full width from
173
+ * WX to the right edge. There is no window height register. That is why
174
+ * GB HUDs sit at the BOTTOM of the screen (this game, and most of the
175
+ * classic library). A TOP HUD needs a mid-frame trick — STAT-interrupt on
176
+ * LYC, flip LCDC bit 5 off after the HUD rows — which is a different,
177
+ * fragile idiom; don't drift into it by accident by setting WY=0.
178
+ *
179
+ * FOOTGUN 2 — sprites are NOT clipped by the window. OBJs draw on top of
180
+ * it (priority bits notwithstanding), so a sprite that wanders below
181
+ * line 128 sits ON the HUD. Gameplay keeps every object above PLAY_H
182
+ * (spikes stand on the ground, coins float, and a player falling into a
183
+ * pit dies at PLAY_H-8 — the frame before the sprite would touch the HUD).
184
+ *
185
+ * Requires: window map at $9C00 (LCDC bit 6 set — keeps it separate from
186
+ * the BG's $9800 map), tile data at $8000 (LCDC bit 4), WX=7, WY=PLAY_H,
187
+ * LCDC bit 5 set during play (title turns the window off — LCDC bit
188
+ * discipline lives in the two LCDC_* values below, poke those, not LCDC). */
189
+ #define PLAY_H 128 /* first HUD line = window top */
190
+ #define WIN_MAP ((uint8_t *)0x9C00) /* window's 32×32 tile map */
191
+ #define LCDC_TITLE (LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO)
192
+ #define LCDC_PLAY (LCDC_TITLE | LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI)
193
+
194
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
195
+ * BATTERY SRAM — persistent hi-score. MBC1 cart RAM is 8KB at $A000-$BFFF,
196
+ * but it boots DISABLED and writes to a disabled bank are silently
197
+ * discarded (reads float). The gate is the MBC's RAM-enable register: any
198
+ * WRITE to ROM space $0000-$1FFF with $0A in the low nibble enables the
199
+ * RAM; writing $00 disables it again. (Writing "into ROM" feels wrong the
200
+ * first time — ROM-area writes never touch ROM, they're how you talk to
201
+ * the mapper chip.) Leaving RAM enabled all the time "works" in emulators
202
+ * but on real hardware risks corruption at power-off — battery carts since
203
+ * forever do enable → touch → disable, so we do too.
204
+ *
205
+ * The record is magic 'H','S' + score lo,hi + a checksum byte, so a
206
+ * first-boot cart full of $FF garbage reads as "no record" instead of a
207
+ * 65535 hi-score.
208
+ *
209
+ * Requires: gb_crt0.s declaring $0147=$03 (MBC1+RAM+BATTERY) + $0149=$02
210
+ * (8KB) — those header bytes are how the emulator knows to allocate and
211
+ * persist SAVE_RAM. Verify headlessly: play, game over, then
212
+ * memory({op:'read', region:'save_ram'}) shows the block, and the
213
+ * hi-score survives host.hardReset(). */
214
+ #define MBC_RAM_ENABLE (*(volatile uint8_t *)0x0000)
215
+ #define SRAM ((volatile uint8_t *)0xA000)
216
+
217
+ static uint16_t hiscore_load(void) {
218
+ uint16_t v = 0;
219
+ MBC_RAM_ENABLE = 0x0A; /* unlock cart RAM */
220
+ if (SRAM[0] == 'H' && SRAM[1] == 'S' &&
221
+ SRAM[4] == (uint8_t)(SRAM[2] ^ SRAM[3] ^ 0xA5)) {
222
+ v = (uint16_t)(SRAM[2] | ((uint16_t)SRAM[3] << 8));
223
+ }
224
+ MBC_RAM_ENABLE = 0x00; /* re-lock (battery hygiene) */
225
+ return v;
226
+ }
227
+
228
+ static void hiscore_save(uint16_t v) {
229
+ uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
230
+ MBC_RAM_ENABLE = 0x0A;
231
+ SRAM[0] = 'H'; SRAM[1] = 'S';
232
+ SRAM[2] = lo; SRAM[3] = hi;
233
+ SRAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
234
+ MBC_RAM_ENABLE = 0x00;
235
+ }
236
+
237
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
238
+ * The level — a 32-column map; world x = (screen x + scroll_x) mod 256.
239
+ * ground_row[c] — BG-map row of the ground's grass top, 0xFF = pit.
240
+ * plat_row[c] — row of a one-way floating platform, 0 = none.
241
+ * Rows are BG-map rows (y = row*8). The playfield is rows 0..15 (128 px,
242
+ * everything below is under the window HUD). Pits are 4+ columns wide on
243
+ * purpose: at this gravity a 2 px/frame run skims anything narrower (the
244
+ * landing probe's +4 px catch window forgives small sink — see land_top). */
245
+ #define NO_GROUND 0xFF
246
+ #define GROUND 13 /* grass-top row, y = 104 */
247
+ static const uint8_t ground_row[32] = {
248
+ GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, /* runway */
249
+ NO_GROUND, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 1 */
250
+ GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND,
251
+ NO_GROUND, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 2 */
252
+ GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND, GROUND,
253
+ };
254
+ static const uint8_t plat_row[32] = {
255
+ 0, 0, 0, 0, 10, 10, 10, 0, /* slab on the runway */
256
+ 0, 9, 9, 0, 0, 0, 10, 10, /* stepping stone over pit 1 */
257
+ 10, 0, 0, 0, 9, 9, 0, 0, /* stone over pit 2 */
258
+ 0, 0, 10, 10, 10, 0, 0, 0, /* slab before the loop seam */
259
+ };
260
+
261
+ /* ── GAME LOGIC (clay) — physics + tuning (Q4.4 fixed point) ── */
262
+ #define GRAVITY_Q44 2 /* +1/8 px per frame per frame */
263
+ #define JUMP_VEL_Q44 (-52) /* launch vy → ~42 px apex (~5 tile rows) */
264
+ #define MAX_VY_Q44 80 /* terminal velocity, 5 px/frame — MUST stay *
265
+ * under 6: the landing probe's 6-px window *
266
+ * can't catch a faster fall (tunnelling) */
267
+ #define MOVE_SPEED 2 /* px/frame walk + scroll speed */
268
+ #define SCROLL_WALL 72 /* px: past this the world scrolls, not you */
269
+ #define GROUND_TOP 104 /* GROUND row * 8 */
270
+ #define SPIKE_Y 96 /* spikes stand on the ground */
271
+ #define NUM_COINS 3
272
+ #define NUM_SPIKES 2
273
+ #define START_LIVES 3
274
+
275
+ static uint8_t px; /* player screen x */
276
+ static uint16_t py_q44; /* player y, Q4.4 fixed point — gravity
277
+ * adds <1 px/frame near the jump apex,
278
+ * so we need sub-pixel precision */
279
+ static int8_t vy_q44;
280
+ static uint8_t on_ground;
281
+ static uint8_t scroll_x; /* level scroll — uint8 wraps at 256 = *
282
+ * exactly one level loop (seamless) */
283
+ static uint8_t dist_sub; /* sub-counter: 64 px scrolled = +1 pt */
284
+ static uint8_t coin_x[NUM_COINS], coin_y[NUM_COINS];
285
+ static uint8_t spike_x[NUM_SPIKES], spike_active[NUM_SPIKES];
286
+ static uint8_t lives;
287
+ static uint16_t score;
288
+ static uint16_t hiscore; /* live HUD readout: max(score, record) */
289
+ static uint16_t record; /* what the battery SRAM actually holds */
290
+ static uint8_t respawn_pause; /* freeze + blink frames after a death */
291
+ static uint8_t prev_pad;
292
+ static uint8_t hud_dirty; /* queue VRAM writes; vblank commits them */
293
+ static uint8_t msg_stage; /* game-over text: 2 = line 1 pending, 1 = line 2 */
294
+ static uint8_t msg_col; /* BG map col for GAME OVER (scroll-aware) */
295
+
296
+ /* Game states — the shell every example shares: title → play → game over.
297
+ * (Handheld adaptation: title is press-start; consoles add a 1P/2P pick.) */
298
+ #define ST_TITLE 0
299
+ #define ST_PLAY 1
300
+ #define ST_OVER 2
301
+ static uint8_t state;
302
+
303
+ /* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ── */
304
+ static uint8_t rng_state = 0xA5;
305
+ static uint8_t rand8(void) {
306
+ uint8_t lsb = (uint8_t)(rng_state & 1);
307
+ rng_state >>= 1;
308
+ if (lsb) rng_state ^= 0xB8;
309
+ return rng_state;
74
310
  }
75
311
 
312
+ static uint8_t dist8(uint8_t a, uint8_t b) {
313
+ return (a > b) ? (uint8_t)(a - b) : (uint8_t)(b - a);
314
+ }
315
+
316
+ /* ── GAME LOGIC (clay) — VRAM upload + text helpers ──────────────────────────
317
+ * All of these write VRAM, so they run with the LCD OFF (boot/repaints) or
318
+ * inside vblank (the HUD digit commits). Note every loop walks a pointer
319
+ * (*dst++ = v) instead of indexing dst[i] — SDCC's sm83 port miscompiles
320
+ * indexed stores through VRAM-pointing pointers (the documented
321
+ * memcpy_vram footgun; see gb_runtime.c). */
76
322
  static void upload_tile(uint8_t slot, const uint8_t *src) {
77
- uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
78
- /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
79
- * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
80
- memcpy_vram(dst, src, 16);
81
- }
82
-
83
- static void paint_platforms(void) {
84
- /* Each cell of the BG map is 8×8, BG map base $9800. */
85
- uint8_t *map = (uint8_t *)0x9800;
86
- uint8_t i, j;
87
- uint16_t k;
88
- int16_t cx, cy, cw, ch;
89
- const Rect *p;
90
- /* k MUST be uint16_t: 32*18 = 576 > 255, so a uint8_t counter would
91
- * never reach the bound and this loop would spin forever (the BG map
92
- * never clears, main() never starts). Classic SDCC limited-range trap.
93
- * Fill sky above the floor line (row 16 = y 128) and textured ground
94
- * at and below it, so the whole world is a real scene, not blank. */
95
- for (k = 0; k < 32 * 18; k++) map[k] = (k >= 16 * 32) ? T_GROUND : T_SKY;
96
- for (i = 0; i < N_PLATFORMS; i++) {
97
- p = &platforms[i];
98
- cx = p->x >> 3;
99
- cy = p->y >> 3;
100
- cw = (p->w + 7) >> 3;
101
- ch = (p->h + 7) >> 3;
102
- for (j = 0; j < cw; j++) {
103
- if (cx + j < 32 && cy < 32)
104
- map[cy * 32 + cx + j] = T_PLATFORM; /* platform top edge */
105
- }
323
+ memcpy_vram((uint8_t *)(0x8000 + (uint16_t)slot * 16), src, 16);
324
+ }
325
+
326
+ static void upload_font(void) {
327
+ uint8_t *dst = (uint8_t *)(0x8000 + (uint16_t)T_DIGIT0 * 16);
328
+ uint8_t g, r, bits;
329
+ for (g = 0; g < 37; g++) {
330
+ for (r = 0; r < 8; r++) {
331
+ bits = font8[g][r];
332
+ *dst++ = bits; /* low plane ─┐ both set → colour 3 (black) */
333
+ *dst++ = bits; /* high plane ─┘ */
106
334
  }
335
+ }
107
336
  }
108
337
 
109
- void main(void) {
110
- int16_t px = 16 << 4, py = 60 << 4;
111
- int16_t vx = 0, vy = 0;
112
- int16_t ipx, ipy, npy, camX = 0;
113
- int32_t np;
114
- uint8_t grounded;
115
- uint8_t pad, prev = 0, i;
116
- const Rect *p;
117
- const int16_t GRAVITY = 10;
118
- const int16_t MOVE = 20;
119
- const int16_t JUMP = -140; /* was -180: ~100px peak (most of the screen) — 'jumps a little too high' */
120
- const int16_t MAXFALL = 280;
121
-
122
- lcd_init_default();
123
- sound_init();
124
- enable_vblank_irq(); /* MANDATORY: HALT-driven wait_vblank. Without this,
125
- * busy-poll wait_vblank runs ~1/30 speed on the WASM
126
- * emulator and the game loop appears to hang. */
127
- LCDC = 0;
128
-
129
- upload_tile(0, tile_blank);
130
- upload_tile(1, tile_player);
131
- upload_tile(2, tile_platform);
132
- upload_tile(T_SKY, tile_sky);
133
- upload_tile(T_GROUND, tile_ground);
134
-
135
- OCPS = 0x80;
136
- for (i = 0; i < 4; i++) {
137
- OCPD = (uint8_t)(obj_palette[i] & 0xFF);
138
- OCPD = (uint8_t)((obj_palette[i] >> 8) & 0xFF);
338
+ static uint8_t char_tile(char ch) {
339
+ if (ch >= '0' && ch <= '9') return (uint8_t)(T_DIGIT0 + (ch - '0'));
340
+ if (ch >= 'A' && ch <= 'Z') return (uint8_t)(T_ALPHA + (ch - 'A'));
341
+ if (ch == '-') return T_DASH;
342
+ return 0; /* space → blank tile */
343
+ }
344
+
345
+ /* Both 32×32 maps (BG $9800, window $9C00) take the same row/col math. */
346
+ static void draw_text(uint8_t *map, uint8_t row, uint8_t col, const char *s) {
347
+ uint8_t *p = map + (uint16_t)row * 32 + col;
348
+ while (*s) *p++ = char_tile(*s++);
349
+ }
350
+
351
+ /* Decimal digits WITHOUT divide/modulo (the sm83 has neither — SDCC's
352
+ * software % costs ~700 cycles a call). Repeated power-of-ten subtraction
353
+ * caps at 36 SUBs for any u16. */
354
+ static void u16_to_tiles(uint16_t v, uint8_t *out5) {
355
+ static const uint16_t pow10[4] = { 10000, 1000, 100, 10 };
356
+ uint8_t i, d;
357
+ for (i = 0; i < 4; i++) {
358
+ d = 0;
359
+ while (v >= pow10[i]) { v -= pow10[i]; ++d; }
360
+ *out5++ = (uint8_t)(T_DIGIT0 + d);
361
+ }
362
+ *out5 = (uint8_t)(T_DIGIT0 + (uint8_t)v);
363
+ }
364
+
365
+ static void draw_u16(uint8_t *map, uint8_t row, uint8_t col, uint16_t v) {
366
+ uint8_t d[5];
367
+ uint8_t i, *p = map + (uint16_t)row * 32 + col;
368
+ u16_to_tiles(v, d);
369
+ for (i = 0; i < 5; i++) *p++ = d[i];
370
+ }
371
+
372
+ /* Pre-convert a string to tile indices (full-frame time) so the vblank
373
+ * commit is a dumb byte copy. char_tile's compare chain per character is
374
+ * exactly the kind of work that blows the ~1140-cycle vblank budget —
375
+ * the shmup example's first cut called draw_text from the vblank slice and
376
+ * gambatte faithfully dropped the writes that slid into mode 3 (half the
377
+ * GAME OVER text simply missing — see the commit_vram budget note). */
378
+ static uint8_t msg_q[20]; /* 9 "GAME OVER" + 11 "PRESS START" */
379
+ static void stage_text(const char *s, uint8_t *out) {
380
+ while (*s) *out++ = char_tile(*s++);
381
+ }
382
+
383
+ /* ── GAME LOGIC (clay) — screen painters (LCD off = free VRAM access) ────────
384
+ * Paints the level scene from the column map into BG rows 0..17 (the SCY=0
385
+ * screen window; this game never scrolls vertically, so rows 18-31 stay
386
+ * untouched). Clouds use a running divide-free pattern counter — the sm83
387
+ * has no divide instruction; treat every / and % in a loop as a red flag
388
+ * (SDCC's software modulo is ~700 cycles a call). */
389
+ static void paint_scene(uint8_t with_plats) {
390
+ uint8_t *p = BG_MAP_0;
391
+ uint8_t r, c, t, g;
392
+ uint8_t cl = 0; /* (r*7 + c*5) mod 13, incremental */
393
+ uint8_t clr = 0;
394
+ for (r = 0; r < 18; r++) {
395
+ cl = clr;
396
+ for (c = 0; c < 32; c++) {
397
+ g = ground_row[c];
398
+ t = T_SKY;
399
+ if (with_plats && r == plat_row[c]) t = T_PLAT;
400
+ else if (g != NO_GROUND) {
401
+ if (r == g) t = T_GRASS;
402
+ else if (r > g) t = T_DIRT;
403
+ } else if (r >= 16) {
404
+ t = T_DIRT; /* pit walls below the playfield */
405
+ }
406
+ if (t == T_SKY && r >= 2 && r <= 6 && cl == 0) t = T_CLOUD;
407
+ *p++ = t;
408
+ cl += 5; if (cl >= 13) cl -= 13;
139
409
  }
140
- BCPS = 0x80;
141
- for (i = 0; i < 4; i++) {
142
- BCPD = (uint8_t)(bg_palette[i] & 0xFF);
143
- BCPD = (uint8_t)((bg_palette[i] >> 8) & 0xFF);
410
+ clr += 7; if (clr >= 13) clr -= 13;
411
+ }
412
+ }
413
+
414
+ static void paint_title(void) {
415
+ paint_scene(0); /* plain scene — text owns the sky */
416
+ draw_text(BG_MAP_0, 3, (uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
417
+ draw_text(BG_MAP_0, 6, 4, "PRESS START");
418
+ draw_text(BG_MAP_0, 8, 6, "HI");
419
+ draw_u16(BG_MAP_0, 8, 9, hiscore);
420
+ draw_text(BG_MAP_0, 11, 6, "1P ONLY"); /* see header: no link 2P */
421
+ SCX = 0; SCY = 0;
422
+ scroll_x = 0;
423
+ }
424
+
425
+ /* HUD strip = window rows 0-1: a solid divider bar, then the text row.
426
+ * Columns 0-19 are the visible 20 (WX=7 pins map col 0 to screen x 0). */
427
+ static void paint_hud(void) {
428
+ uint8_t *p = WIN_MAP;
429
+ uint8_t c;
430
+ for (c = 0; c < 20; c++) *p++ = T_HUDBAR;
431
+ draw_text(WIN_MAP, 1, 0, "SC");
432
+ draw_u16(WIN_MAP, 1, 3, score);
433
+ draw_text(WIN_MAP, 1, 9, "HI");
434
+ draw_u16(WIN_MAP, 1, 12, hiscore);
435
+ draw_text(WIN_MAP, 1, 18, "L");
436
+ *(WIN_MAP + 32 + 19) = (uint8_t)(T_DIGIT0 + lives);
437
+ }
438
+
439
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
440
+ * LCD-off repaints. Bulk VRAM rewrites (full title/level repaints) happen
441
+ * with the LCD OFF — free access, no per-byte timing worries. The rule:
442
+ * only flip LCDC bit 7 to 0 DURING VBLANK. Killing the LCD mid-scanline
443
+ * is the classic "damages real DMG hardware" move; emulators shrug, real
444
+ * units can be permanently marked. wait_vblank() first, always.
445
+ * Requires: enable_vblank_irq() already called (wait_vblank HALT path);
446
+ * lcd_off-safe runtime (gb_runtime's wait_vblank bails if LCD is off). */
447
+ static void repaint_with_lcd_off(uint8_t to_title) {
448
+ msg_stage = 0; /* a queued game-over line must not land on
449
+ * the freshly painted screen a frame later */
450
+ wait_vblank(); /* never cut the LCD outside vblank */
451
+ LCDC = 0;
452
+ if (to_title) {
453
+ paint_title();
454
+ oam_clear(); /* hide every sprite slot before re-enable */
455
+ LCDC = LCDC_TITLE; /* window OFF on the title */
456
+ } else {
457
+ paint_scene(1);
458
+ paint_hud();
459
+ LCDC = LCDC_PLAY; /* window ON below WY — the HUD appears */
460
+ }
461
+ }
462
+
463
+ /* ── GAME LOGIC (clay) — sound: frame-ticked tune + jump/coin/death SFX ──────
464
+ * Channel plan keeps SFX from cutting the music: ch2 = music (one
465
+ * sound_play_tone trigger per note, the APU sustains it), ch1 = jump and
466
+ * coin blips, ch4 = noise for deaths. music_tick() runs once per frame
467
+ * from the main loop; the APU needs no other upkeep. Periods are the
468
+ * 11-bit GB frequency code: 2048 - (131072 / Hz). 0 = rest. */
469
+ static const uint16_t tune[16] = {
470
+ 1714, 0, 1750, 0, 1783, 0, 1798, 0, /* G4 A4 B4 C5 */
471
+ 1825, 0, 1798, 0, 1783, 0, 1750, 0, /* D5 C5 B4 A4 */
472
+ };
473
+ static uint8_t music_pos, music_timer;
474
+ static void music_tick(void) {
475
+ uint16_t n;
476
+ if (++music_timer < 14) return;
477
+ music_timer = 0;
478
+ n = tune[music_pos];
479
+ music_pos = (uint8_t)((music_pos + 1) & 15);
480
+ if (n) sound_play_tone(2, n, 12);
481
+ }
482
+
483
+ /* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ────────
484
+ * Both live in SCREEN coords and drift left with the scroll delta (world-
485
+ * anchored while visible). Coins respawn at the right edge at a random
486
+ * height; spikes only spawn when the level column entering at the right
487
+ * edge has ground under it (never floating over a pit). */
488
+ static const uint8_t coin_heights[4] = { 80, 64, 48, 72 };
489
+ static void respawn_coin(uint8_t i) {
490
+ coin_x[i] = (uint8_t)(144 + (rand8() & 15)); /* enter at the right */
491
+ coin_y[i] = coin_heights[rand8() & 3];
492
+ }
493
+
494
+ static void try_spawn_spike(uint8_t i) {
495
+ uint8_t c = (uint8_t)((uint8_t)(scroll_x + 160) >> 3);
496
+ if (ground_row[c] == NO_GROUND) return;
497
+ if (rand8() > 4) return; /* ~2% per frame */
498
+ spike_x[i] = 152;
499
+ spike_active[i] = 1;
500
+ }
501
+
502
+ /* ── GAME LOGIC (clay) — landing probe against the column map ────────────────
503
+ * One-way platforms, classic style: only catch the player while FALLING
504
+ * through a narrow window at the surface. The window is 6 px tall —
505
+ * top-1 (the standing snap parks feet at top, and gravity's sub-pixel
506
+ * trickle doesn't move the integer Y every frame; without the -1 slack the
507
+ * player "stands" with on_ground=0 most frames, so jumps only register on
508
+ * lucky frames and the idle/jump sprite flickers) through top+4 (so a
509
+ * 5 px/frame terminal-velocity fall can't step over it). */
510
+ static uint8_t land_top(uint8_t c, uint8_t feet) {
511
+ uint8_t r, top;
512
+ r = plat_row[c];
513
+ if (r) {
514
+ top = (uint8_t)(r << 3);
515
+ if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
516
+ }
517
+ r = ground_row[c];
518
+ if (r != NO_GROUND) {
519
+ top = (uint8_t)(r << 3);
520
+ if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
521
+ }
522
+ return 0;
523
+ }
524
+
525
+ /* ── GAME LOGIC (clay) — state transitions ── */
526
+ static void begin_life(void) {
527
+ uint8_t i;
528
+ px = 24;
529
+ py_q44 = (uint16_t)(GROUND_TOP - 8) << 4;
530
+ vy_q44 = 0;
531
+ on_ground = 1;
532
+ scroll_x = 0;
533
+ dist_sub = 0;
534
+ coin_x[0] = 88; coin_y[0] = 80;
535
+ coin_x[1] = 120; coin_y[1] = 64;
536
+ coin_x[2] = 144; coin_y[2] = 48;
537
+ for (i = 0; i < NUM_SPIKES; i++) spike_active[i] = 0;
538
+ respawn_pause = 48; /* ready breather — player blinks */
539
+ prev_pad = 0xFF; /* swallow held buttons across the reset */
540
+ }
541
+
542
+ static void start_game(void) {
543
+ lives = START_LIVES;
544
+ score = 0;
545
+ hud_dirty = 1; /* restage hud_q — a stale game-over stage queued
546
+ * before the repaint would overwrite the fresh
547
+ * zeros next vblank otherwise */
548
+ begin_life();
549
+ state = ST_PLAY;
550
+ repaint_with_lcd_off(0);
551
+ sound_play_tone(1, 1798, 8); /* start jingle (C5) */
552
+ }
553
+
554
+ static void game_over(void) {
555
+ /* Compare against the SAVED record, not the live `hiscore` readout —
556
+ * the scoring path already raised `hiscore` to track the run, so
557
+ * testing `score > hiscore` here would never fire (a bug the shmup
558
+ * example shipped with for an hour; verified-by-harness is the cure). */
559
+ if (score > record) {
560
+ record = score;
561
+ hiscore_save(record); /* battery write — survives power-off */
562
+ }
563
+ state = ST_OVER;
564
+ /* The BG has scrolled: map col 0 is no longer screen col 0. Anchor the
565
+ * text relative to the CURRENT scroll so it lands mid-screen
566
+ * ((SCX/8 + screen_col) & 31 = the map col under that screen col; the
567
+ * commit handles the map's 32-col wrap). Convert the strings to tile
568
+ * indices HERE (full-frame time) and queue them — commit_vram() copies
569
+ * one line per vblank. */
570
+ msg_col = (uint8_t)(((scroll_x >> 3) + 5) & 31);
571
+ stage_text("GAME OVER", msg_q);
572
+ stage_text("PRESS START", msg_q + 9);
573
+ msg_stage = 2;
574
+ }
575
+
576
+ static void kill_player(void) {
577
+ sound_play_noise(20);
578
+ if (lives) --lives;
579
+ hud_dirty = 1;
580
+ if (lives == 0) { game_over(); return; }
581
+ begin_life(); /* back to the runway, scroll rewinds */
582
+ }
583
+
584
+ /* ── GAME LOGIC (clay) — per-state update (runs OUTSIDE vblank) ── */
585
+ static void update_play(uint8_t pad) {
586
+ uint8_t i, delta, y8, feet, c0, c1, top;
587
+
588
+ delta = 0;
589
+ if (pad & PAD_RIGHT) {
590
+ /* One-way camera: walk until the scroll wall, then the world moves. */
591
+ if (px < SCROLL_WALL) px += MOVE_SPEED;
592
+ else { scroll_x += MOVE_SPEED; delta = MOVE_SPEED; }
593
+ }
594
+ if ((pad & PAD_LEFT) && px > 8) px -= MOVE_SPEED;
595
+ if ((pad & PAD_A) && !(prev_pad & PAD_A) && on_ground) {
596
+ vy_q44 = JUMP_VEL_Q44;
597
+ on_ground = 0;
598
+ sound_play_tone(1, 1849, 6); /* jump whoop (E5) */
599
+ }
600
+
601
+ /* World objects drift left as the level scrolls (world-anchored). */
602
+ if (delta) {
603
+ dist_sub += delta;
604
+ if (dist_sub >= 64) { /* distance pay */
605
+ dist_sub -= 64;
606
+ if (score <= 65525u) ++score;
607
+ if (score > hiscore) hiscore = score; /* live HI readout; SRAM
608
+ * write waits for game over */
609
+ hud_dirty = 1;
610
+ }
611
+ for (i = 0; i < NUM_COINS; i++) {
612
+ if (coin_x[i] < 8 + delta) respawn_coin(i);
613
+ else coin_x[i] -= delta;
144
614
  }
615
+ for (i = 0; i < NUM_SPIKES; i++) {
616
+ if (!spike_active[i]) continue;
617
+ if (spike_x[i] < 8 + delta) spike_active[i] = 0;
618
+ else spike_x[i] -= delta;
619
+ }
620
+ }
621
+ for (i = 0; i < NUM_SPIKES; i++)
622
+ if (!spike_active[i]) try_spawn_spike(i);
623
+
624
+ /* Physics: gravity + sub-pixel Y. */
625
+ if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
626
+ py_q44 += vy_q44;
627
+ y8 = (uint8_t)(py_q44 >> 4);
628
+
629
+ /* Fell into a pit — die at PLAY_H-8, the frame BEFORE the sprite would
630
+ * overlap the window HUD (footgun 2 above: OBJs draw over the window). */
631
+ if (y8 >= PLAY_H - 8) {
632
+ kill_player();
633
+ return;
634
+ }
145
635
 
146
- paint_platforms();
147
- oam_clear();
148
- LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO;
149
-
150
- while (1) {
151
- wait_vblank();
152
-
153
- ipx = px >> 4;
154
- ipy = py >> 4;
155
-
156
- /* Camera follows the player, centered, clamped to the world.
157
- * Write SCX so the BG scrolls; player is drawn in SCREEN space. */
158
- camX = ipx - (SCREEN_W / 2 - 4);
159
- if (camX < 0) camX = 0;
160
- if (camX > WORLD_W - SCREEN_W) camX = WORLD_W - SCREEN_W;
161
- SCX = (uint8_t)camX;
162
-
163
- for (i = 0; i < 40; i++) oam_set(i, 0, 0, 0, 0);
164
- oam_set(0, ipy + 16, (ipx - camX) + 8, 1, 0); /* screen x = world - cam */
165
- oam_dma_flush();
166
-
167
- pad = joypad_read();
168
- vx = 0;
169
- if (pad & PAD_LEFT) vx = -MOVE;
170
- if (pad & PAD_RIGHT) vx = MOVE;
171
-
172
- grounded = on_platform(ipx, ipy);
173
- if ((pad & PAD_A) && !(prev & PAD_A) && grounded) {
174
- vy = JUMP;
175
- sound_play_tone(1, 1750, 8); /* jump blip (ch2 square) */
176
- }
177
- prev = pad;
178
-
179
- vy += GRAVITY;
180
- if (vy > MAXFALL) vy = MAXFALL;
181
- if (grounded && vy > 0) vy = 0;
182
-
183
- px += vx;
184
- if (px < 0) px = 0;
185
- if (px > (WORLD_W - 8) << 4) px = (WORLD_W - 8) << 4;
186
-
187
- np = py + vy;
188
- npy = np >> 4;
189
- if (vy > 0) {
190
- for (i = 0; i < N_PLATFORMS; i++) {
191
- p = &platforms[i];
192
- if (ipy + 8 <= p->y && npy + 8 >= p->y
193
- && ipx + 8 > p->x && ipx < p->x + p->w) {
194
- py = (p->y - 8) << 4;
195
- vy = 0;
196
- goto done;
197
- }
198
- }
199
- }
200
- py = np;
201
- if (py > 144 << 4) { py = 0; vy = 0; }
202
- done: ;
636
+ /* Landing — probe the two level columns under the player's feet.
637
+ * uint8 px+scroll_x wraps at 256 exactly like the level does. */
638
+ if (vy_q44 >= 0) {
639
+ feet = (uint8_t)(y8 + 8);
640
+ c0 = (uint8_t)((uint8_t)(px + scroll_x) >> 3);
641
+ c1 = (uint8_t)((uint8_t)(px + scroll_x + 7) >> 3);
642
+ top = land_top(c0, feet);
643
+ if (top == 0) top = land_top(c1, feet);
644
+ if (top) {
645
+ py_q44 = (uint16_t)(top - 8) << 4;
646
+ vy_q44 = 0;
647
+ if (!on_ground) sound_play_tone(1, 1602, 3); /* landing thud */
648
+ on_ground = 1;
649
+ } else {
650
+ on_ground = 0; /* walked off an edge */
203
651
  }
652
+ }
653
+
654
+ /* Coins (collect) + spikes (death). */
655
+ for (i = 0; i < NUM_COINS; i++) {
656
+ if (dist8(coin_x[i], px) < 8 && dist8(coin_y[i], y8) < 8) {
657
+ if (score <= 65525u) score += 10;
658
+ if (score > hiscore) hiscore = score;
659
+ sound_play_tone(1, 1923, 5); /* coin ping (C6) */
660
+ hud_dirty = 1;
661
+ respawn_coin(i);
662
+ }
663
+ }
664
+ for (i = 0; i < NUM_SPIKES; i++) {
665
+ if (!spike_active[i]) continue;
666
+ if (dist8(spike_x[i], px) < 7 && dist8(SPIKE_Y, y8) < 7) {
667
+ kill_player();
668
+ return;
669
+ }
670
+ }
671
+ }
672
+
673
+ /* ── GAME LOGIC (clay) — stage the shadow OAM for THIS frame ─────────────────
674
+ * Pure WRAM writes (shadow_oam at $C100) — safe any time; only the DMA
675
+ * flush is vblank-sensitive. OAM coords are hardware coords: +16 on Y,
676
+ * +8 on X (Y=0/X=0 park a sprite off-screen, which is what oam_clear's
677
+ * zero-fill does for every unused slot). Slot plan (40 hardware slots, we
678
+ * use 6): 0 = player, 1-3 coins, 4-5 spikes — well under the 10-OBJ/line
679
+ * hardware drop. */
680
+ static void stage_sprites(void) {
681
+ uint8_t i, y8;
682
+ oam_clear();
683
+ if (state == ST_TITLE) {
684
+ /* Guaranteed-visible sprite from the first title frame — proof the
685
+ * whole OAM pipeline (shadow → HRAM DMA stub → OAM) is alive before
686
+ * any gameplay complicates the picture. */
687
+ oam_set(0, 96 + 16, 76 + 8, T_PLAYER, 0);
688
+ return;
689
+ }
690
+ y8 = (uint8_t)(py_q44 >> 4);
691
+ if (respawn_pause == 0 || (respawn_pause & 4)) /* ready-blink */
692
+ oam_set(0, (uint8_t)(y8 + 16), (uint8_t)(px + 8),
693
+ on_ground ? T_PLAYER : T_JUMP, 0);
694
+ for (i = 0; i < NUM_COINS; i++)
695
+ oam_set((uint8_t)(1 + i), (uint8_t)(coin_y[i] + 16),
696
+ (uint8_t)(coin_x[i] + 8), T_COIN, 0x10); /* attr $10 → OBP1 */
697
+ for (i = 0; i < NUM_SPIKES; i++)
698
+ if (spike_active[i])
699
+ oam_set((uint8_t)(4 + i), SPIKE_Y + 16,
700
+ (uint8_t)(spike_x[i] + 8), T_SPIKE, 0x10);
701
+ }
702
+
703
+ /* ── GAME LOGIC (clay) — queued VRAM commits ─────────────────────────────────
704
+ * Two-phase update, mirroring the shadow-OAM discipline: game logic only
705
+ * sets hud_dirty / msg_stage. stage_hud() (full-frame time) does the digit
706
+ * math into hud_q; commit_vram() (vblank time) copies bytes — and commits
707
+ * AT MOST ONE queued item per vblank. The budget after the OAM DMA
708
+ * (~165 cycles of the ~1140) fits one item comfortably; committing
709
+ * everything at once on a busy frame (game over = lives digit + two text
710
+ * lines) overruns into mode 3, where the PPU locks VRAM and the writes
711
+ * are silently discarded — the shmup harness caught exactly that as
712
+ * half-missing GAME OVER text. One item per frame = zero dropped bytes,
713
+ * and a frame of HUD latency nobody can see. */
714
+ static uint8_t hud_q[11]; /* 5 score digits, 5 hi digits, lives tile */
715
+ static uint8_t hud_ready;
716
+
717
+ static void stage_hud(void) {
718
+ if (!hud_dirty) return;
719
+ hud_dirty = 0;
720
+ u16_to_tiles(score, hud_q);
721
+ u16_to_tiles(hiscore, hud_q + 5);
722
+ hud_q[10] = (uint8_t)(T_DIGIT0 + lives);
723
+ hud_ready = 1;
724
+ }
725
+
726
+ /* Copy `len` tiles into BG-map `row` starting at `col`, wrapping at the
727
+ * map's 32-column seam (the game-over text is scroll-anchored, so it can
728
+ * straddle the wrap). Two straight pointer-walk runs — no dst[i] indexing
729
+ * through a VRAM pointer (the SDCC footgun, see the VRAM helpers note). */
730
+ static void commit_row_wrapped(uint8_t row, uint8_t col, const uint8_t *q, uint8_t len) {
731
+ uint8_t *base = BG_MAP_0 + (uint16_t)row * 32;
732
+ uint8_t *p = base + col;
733
+ uint8_t n = (uint8_t)(32 - col);
734
+ if (n > len) n = len;
735
+ len -= n;
736
+ while (n--) *p++ = *q++;
737
+ p = base;
738
+ while (len--) *p++ = *q++;
739
+ }
740
+
741
+ static void commit_vram(void) {
742
+ uint8_t i;
743
+ uint8_t *p;
744
+ const uint8_t *q;
745
+ if (hud_ready) { /* item 1: HUD digits */
746
+ hud_ready = 0;
747
+ p = WIN_MAP + 32 + 3; q = hud_q; for (i = 0; i < 5; i++) *p++ = *q++;
748
+ p = WIN_MAP + 32 + 12; q = hud_q + 5; for (i = 0; i < 5; i++) *p++ = *q++;
749
+ *(WIN_MAP + 32 + 19) = hud_q[10];
750
+ return;
751
+ }
752
+ if (msg_stage == 2) { /* item 2: GAME OVER line */
753
+ msg_stage = 1;
754
+ commit_row_wrapped(5, msg_col, msg_q, 9);
755
+ return;
756
+ }
757
+ if (msg_stage == 1) { /* item 3: PRESS START line */
758
+ msg_stage = 0;
759
+ commit_row_wrapped(7, (uint8_t)((msg_col + 31) & 31), msg_q + 9, 11);
760
+ }
761
+ }
762
+
763
+ void main(void) {
764
+ uint8_t pad;
765
+
766
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
767
+ * Boot order. Three load-bearing calls, in this order:
768
+ * 1. lcd_init_default() — sane LCD state AND it installs the OAM-DMA
769
+ * stub into HRAM ($FF80). During OAM DMA the CPU can only fetch
770
+ * from HRAM; the broken alternative (spinning in ROM) fetches $FF
771
+ * = rst $38 and corrupts the stack — the classic "sprites never
772
+ * show / game dies after a while" GB death. Every oam_dma_flush()
773
+ * below depends on this stub existing.
774
+ * 2. enable_vblank_irq() — flips wait_vblank() from LY-polling to
775
+ * HALT-until-vblank-IRQ. The polling fallback runs at ~1/30 speed
776
+ * on the WASM emulator; the HALT path is full speed everywhere.
777
+ * 3. LCD off (inside vblank) for the bulk VRAM uploads — tiles, font,
778
+ * first screen — then back on. VRAM is only freely writable with
779
+ * the LCD off or during vblank/hblank windows. */
780
+ lcd_init_default();
781
+ enable_vblank_irq();
782
+ sound_init();
783
+
784
+ wait_vblank();
785
+ LCDC = 0; /* LCD off — free VRAM access from here */
786
+
787
+ upload_tile(0, tile_blank);
788
+ upload_tile(T_PLAYER, tile_player);
789
+ upload_tile(T_JUMP, tile_player_jump);
790
+ upload_tile(T_COIN, tile_coin);
791
+ upload_tile(T_SPIKE, tile_spike);
792
+ upload_tile(T_SKY, tile_sky);
793
+ upload_tile(T_CLOUD, tile_cloud);
794
+ upload_tile(T_DIRT, tile_dirt);
795
+ upload_tile(T_GRASS, tile_grass);
796
+ upload_tile(T_PLAT, tile_plat);
797
+ upload_tile(T_HUDBAR, tile_hudbar);
798
+ upload_font();
799
+
800
+ /* DMG palettes (2 bits per colour index, low bits = index 0):
801
+ * BGP $E4 → 0=white (sky) 1=light 2=dark (dirt) 3=black (text/turf).
802
+ * OBP0 $1C → player: body black, eyes white.
803
+ * OBP1 $C4 → coins light grey with black ring; spikes black. */
804
+ BGP = 0xE4;
805
+ OBP0 = 0x1C;
806
+ OBP1 = 0xC4;
807
+
808
+ /* Window position — set once; LCDC bit 5 decides if it shows. */
809
+ WX = 7; /* the +7 quirk: 7 = screen left edge */
810
+ WY = PLAY_H; /* HUD owns lines 128-143 */
811
+
812
+ record = hiscore_load(); /* battery SRAM — 0 on first boot */
813
+ hiscore = record;
814
+ state = ST_TITLE;
815
+ paint_title();
816
+ oam_clear();
817
+ LCDC = LCDC_TITLE;
818
+
819
+ for (;;) {
820
+ /* ── full-frame work: input, game state, shadow-OAM staging ── */
821
+ pad = joypad_read();
822
+
823
+ if (state == ST_TITLE) {
824
+ if ((pad & PAD_START) && !(prev_pad & PAD_START)) start_game();
825
+ else if ((pad & PAD_A) && !(prev_pad & PAD_A)) start_game();
826
+ prev_pad = pad;
827
+ } else if (state == ST_PLAY) {
828
+ if (respawn_pause) { /* ready-blink: freeze gameplay, stay honest */
829
+ --respawn_pause;
830
+ prev_pad = pad;
831
+ } else {
832
+ update_play(pad);
833
+ prev_pad = pad;
834
+ }
835
+ } else { /* ST_OVER — freeze the field; START/A returns to title */
836
+ if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
837
+ state = ST_TITLE;
838
+ repaint_with_lcd_off(1);
839
+ }
840
+ prev_pad = pad;
841
+ }
842
+ stage_sprites();
843
+ stage_hud(); /* digit math out here, not in vblank */
844
+
845
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
846
+ * The vblank slice. wait_vblank() wakes at the START of vblank
847
+ * (~1140 cycles of safe OAM/VRAM access). Order is everything:
848
+ * oam_dma_flush() FIRST — the DMA takes ~165 cycles and MUST finish
849
+ * inside vblank; pushing it later (after VRAM writes that grow
850
+ * over time) slides it into active display, where the PPU is
851
+ * reading OAM = one frame of torn/invisible sprites, intermittent
852
+ * and miserable to debug.
853
+ * commit_vram() second — the few queued HUD/map bytes.
854
+ * SCX last — scroll latches per-scanline, so writing it during
855
+ * vblank (before line 0 renders) moves the WHOLE next frame
856
+ * consistently; the window ignores it by design (the HUD idiom).
857
+ * Game logic above NEVER touches VRAM directly — it sets the dirty
858
+ * flags and shadow OAM, and this slice commits them. Keep that split
859
+ * when you reshape the game. */
860
+ wait_vblank();
861
+ oam_dma_flush();
862
+ commit_vram();
863
+ SCX = scroll_x; /* title resets scroll_x to 0; over freezes it */
864
+ music_tick();
865
+ }
204
866
  }