romdevtools 0.28.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 (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -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 +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -1,217 +1,828 @@
1
- /* ── racing.c — SNES PVSnesLib top-down racing scaffold ────────────
1
+ /* ── racing.c — SNES Mode 7 racer (complete example game) ────────────────────
2
2
  *
3
- * Endless 3-lane top-down racer for SNES. LEFT/RIGHT switches lanes
4
- * (edge-detected), obstacles slide down at speed that grows with
5
- * score. Collision triggers a 60-frame game-over freeze + auto-reset.
3
+ * A COMPLETE, working game title screen with a live rotating-attract road,
4
+ * 1P time trial and 2P relay duel, lap timing, persistent best time (battery
5
+ * SRAM), music + SFX, and the SNES's signature hardware feature done for
6
+ * real: a ROTATING PERSPECTIVE Mode 7 ground plane. Steering yaws the
7
+ * camera and the whole world swings around the car, F-Zero style.
6
8
  *
7
- * tcc-65816 is C89 declarations at block top.
9
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
10
+ * very different one. The markers tell you what's what:
11
+ * HARDWARE IDIOM (load-bearing) — dodges a documented SNES footgun; reshape
12
+ * your gameplay around it (see TROUBLESHOOTING before changing).
13
+ * GAME LOGIC (clay) — track shape, physics, scoring, tuning, art: reshape
14
+ * freely.
15
+ *
16
+ * What depends on what:
17
+ * data.asm — font + car sprite tiles, the Mode 7 HDMA tables (WRAM bank
18
+ * $7E — see why over there), the m7_build hardware-multiply table
19
+ * builder, and sram_read16/write16. Load-bearing.
20
+ * hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
21
+ * SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
22
+ * silently stop existing — the build still succeeds.
23
+ * snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
24
+ * driver (music + 2 one-shot samples). #include'd, not separately built.
25
+ *
26
+ * ── HOW THE MODE 7 CAMERA WORKS (read this before touching any of it) ──────
27
+ * Mode 7 is nothing but a 2x2 matrix + a center: for each screen pixel the
28
+ * PPU computes
29
+ * mapX = A*(SX + HOFS - CX) + B*(SY + VOFS - CY) + CX
30
+ * mapY = C*(SX + HOFS - CX) + D*(SY + VOFS - CY) + CY
31
+ * and samples the 1024x1024px map there. One matrix per frame = a flat
32
+ * rotated/zoomed plane. The racer look needs a DIFFERENT zoom per scanline
33
+ * (far rows zoomed out, near rows zoomed in), so we rewrite the matrix
34
+ * EVERY 2 SCANLINES with HDMA — zero CPU during the frame.
35
+ *
36
+ * Per scanline band we want camera yaw θ and zoom λ(line):
37
+ * A = λcosθ B = -λsinθ ← a plain 2D rotation, scaled
38
+ * C = λsinθ D = λcosθ
39
+ * With HOFS = camX-128 (so SX+HOFS-CX ≡ SX-128, screen-centered)
40
+ * and VOFS = camY-line-FOCALF (so SY+VOFS-CY ≡ -FOCALF, a constant), each
41
+ * line shows the map rotated by θ about (camX,camY), pushed FOCALF*λ(line)
42
+ * "forward", spread λ(line) wide. λ(line) = SCALE_NUM/(line-FOCAL) is the
43
+ * classic perspective hyperbola: line 56 (horizon) sees 5.75x zoomed-out
44
+ * shimmer, line 223 (your bumper) sees 0.5x (2x magnified) asphalt.
45
+ *
46
+ * Why VOFS is per-line too: VOFS = camY-line-FOCALF changes by -1 each
47
+ * line — a second tiny HDMA table. And HOFS/VOFS double as BG1's MODE 1
48
+ * scroll for the HUD strip, which is why both tables hold 0 for lines 0-55
49
+ * (scrolled HUD text is the classic bug here).
50
+ *
51
+ * Per frame the CPU does exactly this (m7_stage → data.asm's m7_build):
52
+ * 168 hardware multiplies to refill the back-buffer tables, then 4 register
53
+ * writes + 2 table patches at vblank (m7_commit). ~30% of a frame, all in.
54
+ *
55
+ * VRAM BUDGET (Mode 7 owns words $0000-$3FFF — it has NO base register):
56
+ * $0000-$3FFF low bytes = the 128x128 Mode 7 tilemap
57
+ * $0000-$017F high bytes = 6 ground tiles (8bpp linear, 64 bytes each)
58
+ * $4000- OBJ tiles, $5000- HUD font, $6800- HUD text map
59
+ * Anything you add below word $4000 lands ON the road map — don't.
8
60
  */
9
61
 
10
62
  #include <snes.h>
11
63
  #include "snes_sfx.c"
12
64
 
13
- extern char tilfont, palfont;
14
- extern char tilsprite, palsprite;
15
- extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
65
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
66
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
67
+ #define GAME_TITLE "EMBER CIRCUIT"
68
+
69
+ extern char tilfont, palfont; /* HUD font + text palette (data.asm) */
70
+ extern char tilsprite, palsprite; /* car sprite page + OBJ palette */
16
71
 
17
72
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
18
73
  * No public prototype in console.h, so declare it; call once per frame. */
19
74
  extern void consoleVblank(void);
20
75
 
21
- /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
22
- * playfield reads as a real backdrop, not flat blank. Filled at runtime. */
23
- static u16 bg_map[32 * 32];
24
-
25
- #define LANE_LEFT_X 72
26
- #define LANE_MID_X 124
27
- #define LANE_RIGHT_X 176
28
- #define PLAYER_Y 180
29
- #define MAX_OBSTACLES 4
30
-
31
- /* oamSet's FIRST arg is a BYTE OFFSET into OAM (slot N → N*4), not a slot
32
- * number passing the raw slot corrupts OAM → black/garbled (SNES-1). */
33
- #define SPR(slot) ((slot) << 2)
34
-
35
- typedef struct { s16 x, y; u8 alive; } Car;
36
-
37
- static Car player;
38
- static Car obstacles[MAX_OBSTACLES];
39
- static u16 score;
40
- static u8 spawn_timer;
41
- static u8 game_over_timer;
42
- static u16 prev_pad;
43
- static u8 player_lane;
44
-
45
- static const s16 lane_x[3] = { LANE_LEFT_X, LANE_MID_X, LANE_RIGHT_X };
46
-
47
- static u8 aabb(Car *a, Car *b) {
48
- return a->x < b->x + 8 && a->x + 8 > b->x
49
- && a->y < b->y + 8 && a->y + 8 > b->y;
50
- }
51
-
52
- static void reset_run(void) {
53
- u8 i;
54
- player_lane = 1;
55
- player.x = lane_x[1];
56
- player.y = PLAYER_Y;
57
- player.alive = 1;
58
- for (i = 0; i < MAX_OBSTACLES; i++) obstacles[i].alive = 0;
59
- score = 0;
60
- spawn_timer = 0;
61
- game_over_timer = 0;
62
- }
63
-
64
- /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
65
- * The old code derived the spawn column from spawn_timer, but the caller
66
- * resets spawn_timer just before calling here, so it was CONSTANT and
67
- * every enemy spawned in the same left column/lane. */
68
- static u8 rng_state = 0xA5;
69
- static u8 rand8(void) {
70
- u8 lsb = (u8)(rng_state & 1);
71
- rng_state >>= 1;
72
- if (lsb) rng_state ^= 0xB8;
73
- return rng_state;
74
- }
75
-
76
- static void spawn_obstacle(void) {
77
- u8 i;
78
- for (i = 0; i < MAX_OBSTACLES; i++) {
79
- if (!obstacles[i].alive) {
80
- obstacles[i].x = lane_x[rand8() % 3];
81
- obstacles[i].y = 0;
82
- obstacles[i].alive = 1;
83
- return;
84
- }
76
+ /* data.asm exports the Mode 7 machinery + SRAM helpers. The tables live in
77
+ * a WRAM bank-$7E RAMSECTION (tcc puts C globals in $7F; HDMA needs a bank
78
+ * byte we control). See the HARDWARE IDIOM blocks in data.asm. */
79
+ extern void m7_build(void);
80
+ extern u16 sram_read16(u16 offset);
81
+ extern void sram_write16(u16 offset, u16 value);
82
+ extern u8 m7_ab0[], m7_cd0[], m7_ab1[], m7_cd1[]; /* matrix HDMA tables x2 */
83
+ extern u8 m7_vo0[], m7_vo1[]; /* M7VOFS tables x2 */
84
+ extern u8 lam8_tab[]; /* per-band zoom, λ>>3 */
85
+ extern u8 hdma_mode_tab[], hdma_hofs_tab[];
86
+ extern s8 m7_cos, m7_sin; /* m7_build inputs */
87
+ extern u16 m7_dst, m7_vdst, m7_vstart;
88
+ extern u8 telem[]; /* headless-test block */
89
+
90
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
91
+ * The screen split. Lines 0..HORIZON-1 are a Mode 1 strip (BG1 = the text
92
+ * HUD, backdrop colour = sky); from line HORIZON down, HDMA flips BGMODE to
93
+ * 7 and the SAME BG1 becomes the perspective ground. One BG, two
94
+ * personalities, zero CPU. HORIZON must be a multiple of 8 so whole text
95
+ * rows sit above it (56 = rows 0-6 usable for HUD text). */
96
+ #define HORIZON 56
97
+ /* Perspective: λ(line) = SCALE_NUM / (line - FOCAL), 8.8 fixed point.
98
+ * FOCAL is the virtual eye line (above HORIZON so the divisor never hits 0).
99
+ * FOCALF is the constant forward push — together with λ it sets how far
100
+ * ahead each row looks: row 56 sees 5.75*48 ≈ 276px ahead, row 223 ≈ 24px. */
101
+ #define FOCAL 40
102
+ #define SCALE_NUM 23552u
103
+ #define FOCALF 48
104
+
105
+ /* HDMA table geometry — MUST match the dsb sizes in data.asm. 84 entries x
106
+ * 2 lines cover lines 56-223; entry stride 5 = count byte + 4 matrix bytes. */
107
+ #define AB_BYTES 426 /* 1+4 strip header + 84*5 + terminator */
108
+ #define VO_BYTES 256 /* 1+2 strip header + 84*3 + terminator */
109
+ #define N_BANDS 84
110
+
111
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
112
+ * Mode 7 ground tiles. LINEAR 8bpp: 64 bytes per tile, ONE BYTE PER PIXEL,
113
+ * no bitplanes the byte IS the CGRAM index (the one tile format on the
114
+ * SNES you can author by typing numbers). Index 0 = transparent (backdrop
115
+ * sky would leak through the ground) so ground pixels use 2..9; index 1 is
116
+ * the text colour, indices 16+ would collide with nothing (free to grow). */
117
+ #define T_GRASSA 0
118
+ #define T_GRASSB 1
119
+ #define T_ROAD 2
120
+ #define T_DASH 3
121
+ #define T_KERB 4
122
+ #define T_FINISH 5
123
+ static const u8 m7_tiles[6 * 64] = {
124
+ /* tile 0 — grass A (mid green, dark speckle) */
125
+ 3,2,2,2,2,2,2,2, 2,2,2,2,2,2,3,2, 2,3,2,2,2,2,2,2, 2,2,2,2,2,2,2,3,
126
+ 2,2,3,2,2,2,2,2, 2,2,2,2,2,2,2,2, 2,2,2,3,2,2,2,2, 2,2,2,2,2,2,2,2,
127
+ /* tile 1 — grass B (dark green, mid speckle) */
128
+ 2,3,3,3,3,3,3,2, 3,3,3,2,3,3,3,3, 3,3,3,3,3,3,2,3, 3,3,2,3,3,3,3,3,
129
+ 3,3,3,3,3,2,3,3, 3,2,3,3,3,3,3,3, 3,3,3,3,2,3,3,3, 2,3,3,3,3,3,3,2,
130
+ /* tile 2 — road (asphalt, light speckle) */
131
+ 5,4,4,4,4,4,4,4, 4,4,4,4,4,4,4,4, 4,4,4,5,4,4,4,4, 4,4,4,4,4,4,4,4,
132
+ 4,4,4,4,4,4,5,4, 4,5,4,4,4,4,4,4, 4,4,4,4,4,4,4,4, 4,4,4,4,5,4,4,4,
133
+ /* tile 3 road with centre-line dash (cols 3-4, yellow) */
134
+ 5,4,4,8,8,4,4,4, 4,4,4,8,8,4,4,4, 4,4,4,8,8,4,4,4, 4,4,4,8,8,4,4,4,
135
+ 4,4,4,8,8,4,5,4, 4,5,4,8,8,4,4,4, 4,4,4,8,8,4,4,4, 4,4,4,8,8,4,4,4,
136
+ /* tile 4 — kerb (4x4 red/white checker — reads as rumble strip when
137
+ * the perspective squeezes it) */
138
+ 6,6,6,6,7,7,7,7, 6,6,6,6,7,7,7,7, 6,6,6,6,7,7,7,7, 6,6,6,6,7,7,7,7,
139
+ 7,7,7,7,6,6,6,6, 7,7,7,7,6,6,6,6, 7,7,7,7,6,6,6,6, 7,7,7,7,6,6,6,6,
140
+ /* tile 5 — finish checker (4px white/grey) */
141
+ 9,9,9,9,7,7,7,7, 9,9,9,9,7,7,7,7, 9,9,9,9,7,7,7,7, 9,9,9,9,7,7,7,7,
142
+ 7,7,7,7,9,9,9,9, 7,7,7,7,9,9,9,9, 7,7,7,7,9,9,9,9, 7,7,7,7,9,9,9,9,
143
+ };
144
+
145
+ /* ── GAME LOGIC (clay) — the circuit ─────────────────────────────────────────
146
+ * The track is a ring road on the 1024x1024 map: centre (512,512), inner
147
+ * radius R_IN, outer R_OUT. A ring needs no waypoints: "on the road" is two
148
+ * compares against per-row half-width tables, and laps are quadrant
149
+ * crossings (below). Reshape: ellipse, figure-8 (two rings + a flag),
150
+ * waypointed splines — anything; only the tables + paint loop change. */
151
+ #define MAP_C 512
152
+ #define R_IN 320
153
+ #define R_OUT 416
154
+ #define R_MID 368
155
+ #define KERB_W 10 /* px of rumble strip each side of the road */
156
+ #define LAPS 3
157
+ #define TIME_CAP 59999u /* 999.98s — a DNF cap so idle runs end */
158
+
159
+ #define SPD_MAX 0x0300 /* 3.0 px/frame, 8.8 fixed */
160
+ #define SPD_MAX_OFF 0x00C0 /* crawl cap in the grass */
161
+ #define ACCEL 6
162
+ #define DRAG 3
163
+ #define BRAKE 16
164
+ #define OFF_DRAG 14
165
+ #define TURN 0x00C0 /* heading change per frame held, 8.8 of a
166
+ * 256-unit circle. At full speed that turns
167
+ * a 163px-radius circle — over twice the
168
+ * authority the R_MID ring needs, so every
169
+ * inch of track is makeable flat out. */
170
+
171
+ #define CAR_X 120 /* 16x16 car, screen-centered near the bottom */
172
+ #define CAR_Y 188
173
+
174
+ /* SRAM layout: [0]=magic "EC", [2]=best time (frames), [4]=best ^ 0xA5C3.
175
+ * Magic is written LAST in best_save so a torn write never validates. */
176
+ #define SRAM_MAGIC 0x4345u
177
+
178
+ /* Game states — the shell every example shares: title → play → result. */
179
+ #define ST_TITLE 0
180
+ #define ST_READY 1 /* "PLAYER n PRESS START" (the 2P relay handoff) */
181
+ #define ST_RACE 2
182
+ #define ST_RESULT 3
183
+
184
+ static u8 state;
185
+ static u8 mode_2p; /* 0 = time trial, 1 = relay duel */
186
+ static u8 run_player; /* whose run is on track (0/1 = pad port) */
187
+ static u16 run_time[2]; /* finished run times, in frames */
188
+ static u16 best; /* best 3-lap time ever (0 = none recorded) */
189
+ static u8 sound_ok;
190
+
191
+ /* the camera IS the car: map position (8.8 sub-pixel) + yaw heading */
192
+ static s32 posX, posY; /* map px in 8.8; wraps at 1024px (0x40000) */
193
+ static u16 camX, camY; /* integer px, derived each frame */
194
+ static u16 heading; /* 8.8 angle: top byte 0..255 = 0..360°, 0 =
195
+ * north (-Y), 64 = east — clockwise on map */
196
+ static u16 spd; /* forward speed, 8.8 px/frame */
197
+ static u8 lap;
198
+ static u16 race_frames;
199
+ static u8 offroad, on_kerb; /* surface flags (edge-detected for SFX) */
200
+ static u8 quad, accum; /* lap tracking: quadrant + signed progress */
201
+ static u16 prev_pad0, prev_padR;
202
+ static char tbuf[8]; /* "SSS.HH" time formatter output */
203
+
204
+ static u16 inner_px[128]; /* ring half-widths per map row (boot-built) */
205
+ static u16 outer_px[128];
206
+
207
+ static u8 backbuf; /* which HDMA table set m7_build fills next */
208
+ static u16 front_ab, front_vo; /* fresh tables, committed at next vblank */
209
+ static u16 ab_base[2], vo_base[2]; /* WRAM addresses of the two table sets */
210
+
211
+ /* sin in s1.6 (64 = 1.0), 256 angle units per circle. cos(a)=sintab[a+64]. */
212
+ static const s8 sintab[256] = {
213
+ 0, 2, 3, 5, 6, 8, 9, 11, 12, 14, 16, 17, 19, 20, 22, 23,
214
+ 24, 26, 27, 29, 30, 32, 33, 34, 36, 37, 38, 39, 41, 42, 43, 44,
215
+ 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58, 59,
216
+ 59, 60, 60, 61, 61, 62, 62, 62, 63, 63, 63, 64, 64, 64, 64, 64,
217
+ 64, 64, 64, 64, 64, 64, 63, 63, 63, 62, 62, 62, 61, 61, 60, 60,
218
+ 59, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46,
219
+ 45, 44, 43, 42, 41, 39, 38, 37, 36, 34, 33, 32, 30, 29, 27, 26,
220
+ 24, 23, 22, 20, 19, 17, 16, 14, 12, 11, 9, 8, 6, 5, 3, 2,
221
+ 0, -2, -3, -5, -6, -8, -9, -11, -12, -14, -16, -17, -19, -20, -22, -23,
222
+ -24, -26, -27, -29, -30, -32, -33, -34, -36, -37, -38, -39, -41, -42, -43, -44,
223
+ -45, -46, -47, -48, -49, -50, -51, -52, -53, -54, -55, -56, -56, -57, -58, -59,
224
+ -59, -60, -60, -61, -61, -62, -62, -62, -63, -63, -63, -64, -64, -64, -64, -64,
225
+ -64, -64, -64, -64, -64, -64, -63, -63, -63, -62, -62, -62, -61, -61, -60, -60,
226
+ -59, -59, -58, -57, -56, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46,
227
+ -45, -44, -43, -42, -41, -39, -38, -37, -36, -34, -33, -32, -30, -29, -27, -26,
228
+ -24, -23, -22, -20, -19, -17, -16, -14, -12, -11, -9, -8, -6, -5, -3, -2,
229
+ };
230
+ #define COS8(a) (sintab[(u8)((a) + 64)])
231
+ #define SIN8(a) (sintab[(u8)(a)])
232
+
233
+ /* ── GAME LOGIC (clay) — integer sqrt for the ring tables (boot only) ───────
234
+ * Classic shift-and-subtract: no multiplies, ~16 iterations, exact. */
235
+ static u16 isqrt32(u32 v) {
236
+ u32 r = 0, bit = 0x40000000ul;
237
+ while (bit > v) bit >>= 2;
238
+ while (bit) {
239
+ if (v >= r + bit) { v -= r + bit; r = (r >> 1) + bit; }
240
+ else r >>= 1;
241
+ bit >>= 2;
242
+ }
243
+ return (u16)r;
244
+ }
245
+
246
+ /* Per map row r: the ring's horizontal cross-section is
247
+ * inner_px[r] <= |x - 512| <= outer_px[r]
248
+ * (inner hits 0 across the top/bottom straights — the road spans the middle
249
+ * there). These tables are BOTH the tilemap painter's input and the entire
250
+ * runtime collision model: "am I on the road" is two compares. */
251
+ static void build_ring_tables(void) {
252
+ u16 r;
253
+ s16 dy;
254
+ u32 dy2;
255
+ for (r = 0; r < 128; r++) {
256
+ dy = (s16)(r * 8 + 4) - MAP_C;
257
+ dy2 = (u32)((s32)dy * dy);
258
+ outer_px[r] = (dy2 >= (u32)R_OUT * R_OUT) ? 0
259
+ : isqrt32((u32)R_OUT * R_OUT - dy2);
260
+ inner_px[r] = (dy2 >= (u32)R_IN * R_IN) ? 0
261
+ : isqrt32((u32)R_IN * R_IN - dy2);
262
+ }
263
+ }
264
+
265
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
266
+ * Mode 7 VRAM upload. Mode 7 interleaves map and tiles in the SAME words:
267
+ * the LOW byte of each word is a tilemap entry, the HIGH byte is tile pixel
268
+ * data. VMAIN picks which stream you're writing ($00 = step after $2118
269
+ * low-byte writes, $80 = step after $2119 high-byte writes) and the DMA
270
+ * B-address must match ($18 low / $19 high). Mismatch them and BOTH planes
271
+ * come out as woven garbage (the classic Mode 7 bug) — dmaCopyVram7 exists
272
+ * for exactly this pairing. PPU off (we run pre-setScreenOn) or vblank only.
273
+ *
274
+ * The map is composed in WRAM as SPANS (grass template, road span, then
275
+ * kerb/dash/finish dabbed on top) and DMA'd in one burst — pushing 16K
276
+ * tiles through a tcc-compiled `REG_VMDATAL = tile` loop costs ~4s of
277
+ * boot; memcpy + DMA is a blink. The grass checker only depends on r&7,
278
+ * so 8 templates cover the field.
279
+ * Column math: tile x covers px 8x..8x+7, centre 8x+4; |8x+4-512| ≤ w
280
+ * ⟺ x ∈ [(508-w+7)>>3, (508+w)>>3]. */
281
+ static u8 map_build[128 * 128]; /* boot-only staging buffer ($7F WRAM) */
282
+ static u8 grass_rows[8][128]; /* static: >255 bytes of locals overflows
283
+ * tcc's 8-bit stack-relative addressing */
284
+
285
+ static void upload_m7_vram(void) {
286
+ u16 r, x, in_, out, mid, x0, x1;
287
+ s16 dy;
288
+ u32 dy2;
289
+ u8 *row;
290
+ for (r = 0; r < 8; r++)
291
+ for (x = 0; x < 128; x++)
292
+ grass_rows[r][x] = (((r ^ x) & 7) != 0) ? T_GRASSA : T_GRASSB;
293
+ for (r = 0; r < 128; r++) {
294
+ in_ = inner_px[r];
295
+ out = outer_px[r];
296
+ dy = (s16)(r * 8 + 4) - MAP_C;
297
+ dy2 = (u32)((s32)dy * dy);
298
+ row = map_build + (r << 7);
299
+ memcpy(row, grass_rows[r & 7], 128);
300
+ if (out >= 8) {
301
+ x0 = (u16)((508 - out + 7) >> 3);
302
+ x1 = (u16)((508 + out) >> 3);
303
+ for (x = x0; x <= x1; x++) row[x] = T_ROAD;
304
+ /* centre-line dash ring (radius R_MID), dashed every other row pair */
305
+ if (dy2 < (u32)R_MID * R_MID && (r & 2)) {
306
+ mid = isqrt32((u32)R_MID * R_MID - dy2);
307
+ row[(508 - mid) >> 3] = T_DASH;
308
+ row[(508 + mid) >> 3] = T_DASH;
309
+ }
310
+ /* infield hole + its kerb ring */
311
+ if (in_ >= 8) {
312
+ x = (u16)((508 - in_ + 7) >> 3);
313
+ x1 = (u16)((508 + in_) >> 3);
314
+ row[x] = T_KERB; row[x1] = T_KERB;
315
+ if (x1 > (u16)(x + 1))
316
+ memcpy(row + x + 1, &grass_rows[r & 7][x + 1], (u16)(x1 - x - 1));
317
+ } else if (dy < 0) {
318
+ /* top straight: the start/finish stripe crosses the road at x≈512 */
319
+ row[63] = T_FINISH; row[64] = T_FINISH;
320
+ }
321
+ row[x0] = T_KERB;
322
+ row[(508 + out) >> 3] = T_KERB;
85
323
  }
324
+ }
325
+ dmaCopyVram7((u8 *)m7_tiles, 0x0000, sizeof(m7_tiles), 0x80, 0x1900);
326
+ dmaCopyVram7(map_build, 0x0000, sizeof(map_build), 0x00, 0x1800);
327
+ }
328
+
329
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
330
+ * One-time skeletons for the HDMA tables (the per-frame DATA comes from
331
+ * m7_build in data.asm — read its header for the table grammar). Counts,
332
+ * strip headers and terminators never change, so they're written once here;
333
+ * the build loop then only touches the 4 (or 2) data bytes per entry. */
334
+ static void m7_tables_init(void) {
335
+ u16 e, p, line;
336
+ u8 *ab, *vo;
337
+ u8 b;
338
+ ab_base[0] = (u16)(m7_ab0); vo_base[0] = (u16)(m7_vo0);
339
+ ab_base[1] = (u16)(m7_ab1); vo_base[1] = (u16)(m7_vo1);
340
+ for (b = 0; b < 2; b++) {
341
+ u8 *cd;
342
+ ab = b ? m7_ab1 : m7_ab0;
343
+ cd = b ? m7_cd1 : m7_cd0;
344
+ vo = b ? m7_vo1 : m7_vo0;
345
+ /* HUD strip: hold identity 56 lines (these lines are Mode 1 text) */
346
+ ab[0] = HORIZON; ab[1] = 0x00; ab[2] = 0x01; ab[3] = 0; ab[4] = 0;
347
+ cd[0] = HORIZON; cd[1] = 0; cd[2] = 0; cd[3] = 0x00; cd[4] = 0x01;
348
+ vo[0] = HORIZON; vo[1] = 0; vo[2] = 0;
349
+ p = 5;
350
+ for (e = 0; e < N_BANDS; e++) { ab[p] = 2; cd[p] = 2; p += 5; }
351
+ ab[p] = 0; cd[p] = 0;
352
+ p = 3;
353
+ for (e = 0; e < N_BANDS; e++) { vo[p] = 2; p += 3; }
354
+ vo[p] = 0;
355
+ }
356
+ /* per-band zoom: λ(line)>>3 so it fits the 8x8 hardware multiplier.
357
+ * Sampled at each band's second line (56+2e+1) — splits the 2-line error. */
358
+ for (e = 0; e < N_BANDS; e++) {
359
+ line = HORIZON + e * 2 + 1;
360
+ lam8_tab[e] = (u8)((SCALE_NUM / (line - FOCAL)) >> 3);
361
+ }
362
+ /* BGMODE split: mode 1 for the HUD strip, then one write of mode 7 that
363
+ * holds to the bottom (terminator keeps the last value). */
364
+ hdma_mode_tab[0] = HORIZON; hdma_mode_tab[1] = 0x01;
365
+ hdma_mode_tab[2] = 1; hdma_mode_tab[3] = 0x07;
366
+ hdma_mode_tab[4] = 0;
367
+ /* M7HOFS: 0 through the HUD strip ($210D doubles as BG1's Mode-1 H scroll
368
+ * — a camera value here scrolls your HUD text sideways), then the camera
369
+ * value from HORIZON down. m7_commit patches bytes [4],[5] every frame —
370
+ * hardware re-reads the table each frame, no re-arm needed. */
371
+ hdma_hofs_tab[0] = HORIZON; hdma_hofs_tab[1] = 0; hdma_hofs_tab[2] = 0;
372
+ hdma_hofs_tab[3] = 1; hdma_hofs_tab[4] = 0; hdma_hofs_tab[5] = 0;
373
+ hdma_hofs_tab[6] = 0;
374
+ }
375
+
376
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
377
+ * Wire the 5 HDMA channels — onto channels 2-6, and the CHOICE is
378
+ * load-bearing: a channel cannot serve general-purpose DMA and HDMA in the
379
+ * same frame, and PVSnesLib's runtime owns two channels for GP-DMA:
380
+ * ch0 — dmaCopyVram (console text upload, oamInitGfxSet, consoleVblank)
381
+ * ch7 — the VBlank ISR's OAM DMA (vblank.asm writes $4370-$4375 EVERY
382
+ * frame). Park HDMA on ch7 and the ISR silently rewrites the
383
+ * channel's params each NMI — your table stops landing and OAM gets
384
+ * fed table bytes instead. (Found the hard way; see TROUBLESHOOTING
385
+ * "HDMA channel fights the OAM DMA".)
386
+ * So: 2=BGMODE, 3=M7A/M7B, 4=M7C/M7D, 5=M7HOFS, 6=M7VOFS.
387
+ * DMAP transfer modes are the whole trick:
388
+ * mode 0 = 1 byte → reg ($2105 BGMODE)
389
+ * mode 2 = 2 bytes → reg,reg (write-twice regs: HOFS/VOFS lo,hi)
390
+ * mode 3 = 4 bytes → r,r,r+1,r+1 ($211B,$211B,$211C,$211C = A lo,hi,B lo,hi)
391
+ * Mode 3 exists precisely FOR the Mode 7 matrix — 4 bytes feed two
392
+ * write-twice registers per line. */
393
+ static void road_hdma_on(void) {
394
+ REG_DMAP2 = 0x00; REG_BBAD2 = 0x05; /* → $2105 BGMODE */
395
+ REG_A1T2LH = (u16)(hdma_mode_tab); REG_A1B2 = 0x7E;
396
+ REG_DMAP3 = 0x03; REG_BBAD3 = 0x1B; /* → $211B/C M7A,M7B */
397
+ REG_A1T3LH = front_ab; REG_A1B3 = 0x7E;
398
+ REG_DMAP4 = 0x03; REG_BBAD4 = 0x1D; /* → $211D/E M7C,M7D */
399
+ REG_A1T4LH = (u16)(front_ab + AB_BYTES); REG_A1B4 = 0x7E;
400
+ REG_DMAP5 = 0x02; REG_BBAD5 = 0x0D; /* → $210D M7HOFS */
401
+ REG_A1T5LH = (u16)(hdma_hofs_tab); REG_A1B5 = 0x7E;
402
+ REG_DMAP6 = 0x02; REG_BBAD6 = 0x0E; /* → $210E M7VOFS */
403
+ REG_A1T6LH = front_vo; REG_A1B6 = 0x7E;
404
+ REG_HDMAEN = 0x7C; /* channels 2-6 live */
405
+ }
406
+
407
+ static void road_hdma_off(void) {
408
+ REG_HDMAEN = 0x00;
409
+ }
410
+
411
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
412
+ * Per-frame Mode 7 work, two halves:
413
+ *
414
+ * m7_stage — runs DURING the frame, fills the BACK buffer: heading → cos/sin
415
+ * (one table lookup), point m7_dst/m7_vdst at the buffer HDMA is NOT
416
+ * reading, then m7_build (data.asm) does the 168 hardware multiplies.
417
+ * Rebuilding the live table instead shears the ground mid-frame.
418
+ *
419
+ * m7_commit — MUST run inside vblank, right after WaitForVBlank: flip the
420
+ * channels to the fresh tables (A1Tx is only re-read at the top of each
421
+ * frame), write the write-twice center regs M7X/M7Y (lo then hi — a single
422
+ * write half-latches and the ground leaps), and patch the camera into the
423
+ * HOFS table. */
424
+ static void m7_stage(void) {
425
+ u8 a = (u8)(heading >> 8);
426
+ m7_cos = COS8(a);
427
+ m7_sin = SIN8(a);
428
+ m7_dst = (u16)(ab_base[backbuf] + 5); /* first entry's count byte */
429
+ m7_vdst = (u16)(vo_base[backbuf] + 3);
430
+ m7_vstart = (u16)(camY - HORIZON - FOCALF);
431
+ m7_build();
432
+ front_ab = ab_base[backbuf];
433
+ front_vo = vo_base[backbuf];
434
+ backbuf ^= 1;
435
+ }
436
+
437
+ static void m7_commit(void) {
438
+ u16 hx = (u16)((camX - 128) & 0x1FFF);
439
+ REG_A1T3LH = front_ab;
440
+ REG_A1T4LH = (u16)(front_ab + AB_BYTES);
441
+ REG_A1T6LH = front_vo;
442
+ REG_M7X = (u8)camX; REG_M7X = (u8)(camX >> 8); /* write-twice, lo→hi */
443
+ REG_M7Y = (u8)camY; REG_M7Y = (u8)(camY >> 8);
444
+ hdma_hofs_tab[4] = (u8)hx; hdma_hofs_tab[5] = (u8)(hx >> 8);
445
+ }
446
+
447
+ /* Leave the split: full-screen Mode 1 text (the result card). HDMA's last
448
+ * BGMODE write said "mode 7" and the scroll regs hold camera values —
449
+ * restore both or the text screen comes up as rotated garbage. */
450
+ static void full_text_screen(void) {
451
+ road_hdma_off();
452
+ REG_BGMODE = 0x01;
453
+ REG_M7HOFS = 0; REG_M7HOFS = 0; /* = BG1HOFS: write-twice, zero it */
454
+ REG_M7VOFS = 0; REG_M7VOFS = 0; /* = BG1VOFS */
455
+ }
456
+
457
+ /* ── GAME LOGIC (clay) — SRAM best time (see sram_* in data.asm) ──────────── */
458
+ static u16 best_load(void) {
459
+ u16 v;
460
+ if (sram_read16(0) != SRAM_MAGIC) return 0;
461
+ v = sram_read16(2);
462
+ if (sram_read16(4) != (u16)(v ^ 0xA5C3u)) return 0;
463
+ return v;
464
+ }
465
+
466
+ static void best_save(u16 v) {
467
+ sram_write16(2, v);
468
+ sram_write16(4, (u16)(v ^ 0xA5C3u));
469
+ sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
470
+ }
471
+
472
+ /* ── GAME LOGIC (clay) — text helpers ──────────────────────────────────────── */
473
+ static void fmt_time(u16 f) { /* frames → "SSS.HH" into tbuf */
474
+ u16 s = f / 60;
475
+ u16 hh = (u16)((f % 60) * 5 / 3); /* x100/60 without overflow */
476
+ tbuf[0] = (char)('0' + (s / 100) % 10);
477
+ tbuf[1] = (char)('0' + (s / 10) % 10);
478
+ tbuf[2] = (char)('0' + s % 10);
479
+ tbuf[3] = '.';
480
+ tbuf[4] = (char)('0' + hh / 10);
481
+ tbuf[5] = (char)('0' + hh % 10);
482
+ tbuf[6] = 0;
86
483
  }
87
484
 
88
- static void render_score(void) {
89
- char buf[6];
90
- u16 v;
91
- s8 i;
92
- buf[0]='0'; buf[1]='0'; buf[2]='0'; buf[3]='0'; buf[4]='0'; buf[5]=0;
93
- v = score;
94
- for (i = 4; i >= 0; i--) { buf[i] = '0' + (v % 10); v /= 10; }
95
- consoleDrawText(22, 2, buf);
96
- }
97
-
98
- /* Draw the road out of font glyphs on the text BG (no extra tile data):
99
- * solid '|' shoulder lines outside the outer lanes and dashed ':' lane
100
- * dividers between the three lanes. Lanes sit at text cols 9/15/22, so
101
- * dividers go at 12/18 and shoulders at 7/24. Text grid is 32x28. */
102
- #define ROAD_TOP_ROW 4
103
- #define ROAD_BOT_ROW 24
104
- #define ROAD_EDGE_L 7
105
- #define ROAD_EDGE_R 24
106
- #define ROAD_DIV_1 12
107
- #define ROAD_DIV_2 18
108
- static void draw_road(void) {
109
- u8 r;
110
- for (r = ROAD_TOP_ROW; r <= ROAD_BOT_ROW; r++) {
111
- consoleDrawText(ROAD_EDGE_L, r, "|");
112
- consoleDrawText(ROAD_EDGE_R, r, "|");
113
- if (r & 1) {
114
- consoleDrawText(ROAD_DIV_1, r, ":");
115
- consoleDrawText(ROAD_DIV_2, r, ":");
116
- }
485
+ static void draw_best(u16 x, u16 y) {
486
+ if (best) { fmt_time(best); consoleDrawText(x, y, tbuf); }
487
+ else consoleDrawText(x, y, "---.--");
488
+ }
489
+
490
+ static void clear_row(u16 y) {
491
+ consoleDrawText(0, y, " ");
492
+ }
493
+
494
+ static void clear_rows(u16 a, u16 b) {
495
+ u16 y;
496
+ for (y = a; y <= b; y++) clear_row(y);
497
+ }
498
+
499
+ /* ── GAME LOGIC (clay) — car placement + state entries ───────────────────────
500
+ * Spawn: on the top straight just EAST of the finish line, heading east
501
+ * (heading 64). The car drives clockwise by default but the lap counter is
502
+ * direction-agnostic — run it backwards if you like. */
503
+ static void place_at_grid(void) {
504
+ posX = (s32)(MAP_C + 24) << 8;
505
+ posY = (s32)(MAP_C - R_MID) << 8;
506
+ camX = MAP_C + 24;
507
+ camY = MAP_C - R_MID;
508
+ heading = 64u << 8;
509
+ quad = 0; /* dx>0, dy<0 — see lap counter */
510
+ accum = 0;
511
+ }
512
+
513
+ static void title_enter(void) {
514
+ clear_rows(0, 27);
515
+ consoleDrawText(9, 1, GAME_TITLE);
516
+ consoleDrawText(9, 2, "BEST"); draw_best(15, 2);
517
+ consoleDrawText(6, 4, "A - 1P TIME TRIAL");
518
+ consoleDrawText(6, 5, "B - 2P RELAY DUEL");
519
+ place_at_grid();
520
+ spd = 0;
521
+ m7_stage(); /* fill a table set before HDMA reads */
522
+ road_hdma_on();
523
+ oamSet(0, CAR_X, CAR_Y, 3, 0, 0, 0, 0);
524
+ oamSetEx(0, OBJ_LARGE, OBJ_SHOW);
525
+ state = ST_TITLE;
526
+ }
527
+
528
+ static void run_reset(void) {
529
+ place_at_grid();
530
+ spd = 0;
531
+ lap = 1;
532
+ race_frames = 0;
533
+ offroad = on_kerb = 0;
534
+ }
535
+
536
+ static void ready_enter(void) {
537
+ run_reset();
538
+ clear_rows(0, 6);
539
+ consoleDrawText(1, 1, mode_2p ? (run_player ? "P2" : "P1") : "1P");
540
+ consoleDrawText(4, 1, "TIME 000.00");
541
+ consoleDrawText(17, 1, "LAP 1/3");
542
+ consoleDrawText(4, 2, "BEST"); draw_best(9, 2);
543
+ consoleDrawText(6, 4, run_player ? "PLAYER 2 TO THE GRID"
544
+ : (mode_2p ? "PLAYER 1 TO THE GRID" : "TO THE GRID"));
545
+ consoleDrawText(10, 5, "PRESS START");
546
+ prev_padR = 0xFFFF; /* swallow the press that ENTERED this state — without
547
+ * this, the A that picked 1P on the title instantly
548
+ * green-lights the run (classic edge-detect reuse bug) */
549
+ state = ST_READY;
550
+ }
551
+
552
+ static void race_enter(void) {
553
+ clear_rows(4, 6);
554
+ if (sound_ok) sfx_play(1); /* green-light blip */
555
+ state = ST_RACE;
556
+ }
557
+
558
+ static void result_enter(void) {
559
+ u8 newbest = 0;
560
+ u16 t1 = run_time[0], t2 = run_time[1];
561
+ u16 winner_t = t1;
562
+ if (mode_2p && t2 < winner_t) winner_t = t2;
563
+ if (winner_t < best || best == 0) { best = winner_t; best_save(best); newbest = 1; }
564
+
565
+ full_text_screen();
566
+ oamSetVisible(0, OBJ_HIDE);
567
+ clear_rows(0, 27);
568
+ consoleDrawText(10, 6, mode_2p ? "DUEL OVER" : "RUN COMPLETE");
569
+ consoleDrawText(9, 10, mode_2p ? "P1" : "TIME");
570
+ fmt_time(t1); consoleDrawText(15, 10, tbuf);
571
+ if (mode_2p) {
572
+ consoleDrawText(9, 12, "P2");
573
+ fmt_time(t2); consoleDrawText(15, 12, tbuf);
574
+ if (t1 < t2) consoleDrawText(9, 15, "PLAYER 1 WINS");
575
+ else if (t2 < t1) consoleDrawText(9, 15, "PLAYER 2 WINS");
576
+ else consoleDrawText(12, 15, "DEAD HEAT");
577
+ }
578
+ consoleDrawText(9, 18, "BEST"); draw_best(15, 18);
579
+ if (newbest) consoleDrawText(8, 20, "NEW TRACK RECORD");
580
+ consoleDrawText(10, 24, "PRESS START");
581
+ if (sound_ok) sfx_play(2); /* finish flourish */
582
+ state = ST_RESULT;
583
+ }
584
+
585
+ /* ── GAME LOGIC (clay) — driving model ───────────────────────────────────────
586
+ * Forward motion integrates the heading: dx = spd·sinθ, dy = -spd·cosθ
587
+ * (heading 0 = north = -Y). The multiply stays in s16: (spd>>2) ≤ 192 times
588
+ * |trig| ≤ 64 = 12288 — tcc's s16 multiply is fine at 2/frame, it's the 168
589
+ * PER-LINE multiplies that needed data.asm's hardware multiplier. */
590
+ static void integrate_motion(u8 a) {
591
+ posX += (s32)(((s16)(spd >> 2) * (s16)SIN8(a)) >> 4);
592
+ posY -= (s32)(((s16)(spd >> 2) * (s16)COS8(a)) >> 4);
593
+ posX &= 0x3FFFF; /* wrap at 1024px (the map wraps too) */
594
+ posY &= 0x3FFFF;
595
+ camX = (u16)(posX >> 8);
596
+ camY = (u16)(posY >> 8);
597
+ }
598
+
599
+ /* Surface at a map point, from the ring tables: 0 road, 1 kerb, 2 grass.
600
+ * Sampled 24px AHEAD of the camera — that's where the car sprite's nose
601
+ * sits on screen (see the camera math in the header: the bottom-centre
602
+ * pixel shows cam + λ(223)·FOCALF ≈ 24px forward). */
603
+ static u8 surface_at(u8 a) {
604
+ u16 sx = (u16)((camX + (((s16)SIN8(a) * 24) >> 6)) & 1023);
605
+ u16 sy = (u16)((camY - (((s16)COS8(a) * 24) >> 6)) & 1023);
606
+ u16 in_, out, adx;
607
+ s16 dxs;
608
+ u8 row = (u8)(sy >> 3);
609
+ in_ = inner_px[row];
610
+ out = outer_px[row];
611
+ dxs = (s16)sx - MAP_C;
612
+ adx = (u16)(dxs < 0 ? -dxs : dxs);
613
+ if (out == 0) return 2;
614
+ if (adx > out + KERB_W || (in_ > KERB_W && adx < in_ - KERB_W)) return 2;
615
+ if (adx > out - KERB_W || (in_ > 0 && adx < in_ + KERB_W)) return 1;
616
+ return 0;
617
+ }
618
+
619
+ /* ── GAME LOGIC (clay) — lap counting by quadrant walk ───────────────────────
620
+ * The map centre splits the world into 4 quadrants; driving the ring visits
621
+ * them in order. Each adjacent crossing nudges a signed counter (+1
622
+ * clockwise, -1 counter-clockwise); ±4 = a full circle = a lap, counted
623
+ * exactly at the finish-line quadrant boundary. Backtracking un-counts
624
+ * itself — you can't farm laps by wiggling over the line. */
625
+ static void lap_check(void) {
626
+ s16 dxs = (s16)camX - MAP_C, dys = (s16)camY - MAP_C;
627
+ u8 q = (dys < 0) ? (dxs >= 0 ? 0 : 3) : (dxs >= 0 ? 1 : 2);
628
+ u8 d;
629
+ if (q == quad) return;
630
+ d = (u8)((q - quad) & 3);
631
+ if (d == 1) accum++;
632
+ else if (d == 3) accum--;
633
+ quad = q;
634
+ if (accum == 4 || accum == (u8)-4) {
635
+ accum = 0;
636
+ lap++;
637
+ if (lap > LAPS) {
638
+ run_time[run_player] = race_frames;
639
+ if (mode_2p && run_player == 0) { run_player = 1; ready_enter(); }
640
+ else result_enter();
641
+ return;
117
642
  }
643
+ if (sound_ok) sfx_play(1);
644
+ tbuf[0] = (char)('0' + lap); tbuf[1] = 0;
645
+ consoleDrawText(21, 1, tbuf);
646
+ }
647
+ }
648
+
649
+ static void race_update(void) {
650
+ u16 pad = padsCurrent(run_player);
651
+ u8 a = (u8)(heading >> 8);
652
+ u8 surf;
653
+
654
+ /* throttle / brake (8.8 speed) */
655
+ if (pad & (KEY_A | KEY_B)) { if (spd < SPD_MAX) spd += ACCEL; }
656
+ else if (spd > DRAG) spd -= DRAG; else spd = 0;
657
+ if (pad & KEY_Y) { if (spd > BRAKE) spd -= BRAKE; else spd = 0; }
658
+
659
+ /* steering = yaw. THE Mode 7 moment: this one += is what swings the
660
+ * whole world around the car. */
661
+ if (spd > 0x0010) {
662
+ if (pad & KEY_LEFT) heading -= TURN;
663
+ if (pad & KEY_RIGHT) heading += TURN;
664
+ }
665
+ a = (u8)(heading >> 8);
666
+
667
+ /* surface response */
668
+ surf = surface_at(a);
669
+ if (surf == 2) { /* grass */
670
+ if (!offroad && sound_ok) sfx_play(2); /* one thump on exit */
671
+ offroad = 1;
672
+ if (spd > SPD_MAX_OFF) spd = (u16)(spd - OFF_DRAG);
673
+ } else {
674
+ offroad = 0;
675
+ if (surf == 1) { /* kerb rumble strip */
676
+ if (!on_kerb && sound_ok) sfx_play(1);
677
+ on_kerb = 1;
678
+ if (spd > DRAG * 2) spd -= DRAG; /* mild scrub */
679
+ } else on_kerb = 0;
680
+ }
681
+
682
+ integrate_motion(a);
683
+ lap_check();
684
+ if (state != ST_RACE) return; /* lap_check may have ended the run */
685
+
686
+ race_frames++;
687
+ if (race_frames >= TIME_CAP) { /* DNF cap — idle runs still end */
688
+ run_time[run_player] = TIME_CAP;
689
+ if (mode_2p && run_player == 0) { run_player = 1; ready_enter(); }
690
+ else result_enter();
691
+ return;
692
+ }
693
+ if ((race_frames & 7) == 0) { /* HUD time, every 8 frames */
694
+ fmt_time(race_frames);
695
+ consoleDrawText(9, 1, tbuf);
696
+ }
697
+ }
698
+
699
+ /* ── GAME LOGIC (clay) — title attract: the world pirouettes ─────────────────
700
+ * The car parks on the grid and the camera yaws slowly — the cheapest
701
+ * possible demo that rotation is real (and the first thing a fork breaks
702
+ * if the matrix handedness gets flipped). */
703
+ static void attract_update(void) {
704
+ heading += 0x0020;
705
+ }
706
+
707
+ /* Headless-test telemetry — written once per frame into the bank-$7E telem
708
+ * block (data.asm). A test harness finds it by scanning WRAM for the
709
+ * "EC"+0xBD signature, then steers the car from real game state instead of
710
+ * parsing pixels. Costs 14 byte-writes per frame; delete freely. */
711
+ static void telem_update(void) {
712
+ telem[0] = 'E'; telem[1] = 'C'; telem[2] = 0xBD;
713
+ telem[3] = state;
714
+ telem[4] = lap;
715
+ telem[5] = (u8)(heading >> 8);
716
+ telem[6] = (u8)((sound_ok << 7) | (mode_2p << 1) | run_player);
717
+ telem[7] = (u8)camX; telem[8] = (u8)(camX >> 8);
718
+ telem[9] = (u8)camY; telem[10] = (u8)(camY >> 8);
719
+ telem[11] = (u8)spd; telem[12] = (u8)(spd >> 8);
720
+ telem[13] = (u8)race_frames; telem[14] = (u8)(race_frames >> 8);
721
+ telem[15] = accum;
118
722
  }
119
723
 
120
724
  int main(void) {
121
- u16 pad;
122
- u8 i;
123
- u16 bi;
124
- s16 step;
125
-
126
- consoleSetTextMapPtr(0x6800);
127
- consoleSetTextGfxPtr(0x3000);
128
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
129
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
130
- setMode(BG_MODE1, 0);
131
- /* consoleInitText DMAs the font but does NOT set the PPU BG base
132
- * registers — point BG0 at the same font ($3000) + map ($6800). */
133
- bgSetGfxPtr(0, 0x3000);
134
- bgSetMapPtr(0, 0x6800, SC_32x32);
135
-
136
- /* BG1 = full-screen wallpaper so the playfield never reads as blank.
137
- * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
138
- * the console gfx $3000 / map $6800). Map entries use palette block 1
139
- * (0x0400) so the wallpaper palette doesn't disturb the console font
140
- * palette in block 0 (HUD/road text stays legible). */
141
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
142
- 32, 32, BG_16COLORS, 0x2000);
143
-
144
- /* Per-genre backdrop tint every SNES scaffold used to ship the same
145
- * blue checkered wallpaper ('no variety'). Recolor the wallpaper's
146
- * CGRAM entries (block 1 = entries 16+) to a asphalt grey scheme. */
147
- setPaletteColor(0, RGB5(4,4,5));
148
- setPaletteColor(17, RGB5(8,8,9));
149
- setPaletteColor(18, RGB5(6,6,7));
150
- for (bi = 0; bi < 32 * 32; bi++) bg_map[bi] = 0x0400;
151
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
152
- bgSetEnable(1);
153
- bgSetDisable(2);
154
-
155
- /* 2 sprite tiles × 32 bytes = 64 bytes */
156
- oamInitGfxSet(&tilsprite, 64, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
157
-
158
- consoleDrawText(2, 2, "SCORE");
159
- consoleDrawText(6, 26, "L/R SWITCH LANES");
160
- draw_road(); /* shoulder lines + dashed lane dividers (font glyphs) */
161
-
162
- /* Hide all OAM. */
163
- for (i = 0; i < 1 + MAX_OBSTACLES; i++) oamSet(SPR(i), 0, 240, 3, 0, 0, 0, 0);
164
-
165
- setScreenOn();
166
- sfx_init();
167
- reset_run();
168
- prev_pad = 0;
169
-
170
- while (1) {
171
- /* Stage OAM. */
172
- oamSet(SPR(0), player.x, player.y, 3, 0, 0, 0, 0);
173
- for (i = 0; i < MAX_OBSTACLES; i++) {
174
- u16 ey = obstacles[i].alive ? obstacles[i].y : 240;
175
- oamSet(SPR((u16)(1 + i)), obstacles[i].x, ey, 3, 0, 0, 1, 0); /* gfxoffset = tile INDEX 1 (SNES-5; was 32) */
176
- }
177
- oamUpdate();
178
- render_score();
179
- WaitForVBlank();
180
- consoleVblank();
181
-
182
- pad = padsCurrent(0);
183
-
184
- if (game_over_timer > 0) {
185
- game_over_timer--;
186
- if (game_over_timer == 0) reset_run();
187
- prev_pad = pad;
188
- continue;
189
- }
190
-
191
- if ((pad & KEY_LEFT) && !(prev_pad & KEY_LEFT) && player_lane > 0) { player_lane--; sfx_play(1); }
192
- if ((pad & KEY_RIGHT) && !(prev_pad & KEY_RIGHT) && player_lane < 2) { player_lane++; sfx_play(1); }
193
- player.x = lane_x[player_lane];
194
- prev_pad = pad;
195
-
196
- step = (s16)(2 + (score / 500));
197
- if (step > 4) step = 4;
198
-
199
- for (i = 0; i < MAX_OBSTACLES; i++) {
200
- if (!obstacles[i].alive) continue;
201
- obstacles[i].y = (s16)(obstacles[i].y + step);
202
- if (obstacles[i].y >= 224) obstacles[i].alive = 0;
203
- }
204
- spawn_timer++;
205
- if (spawn_timer >= 36) { spawn_timer = 0; spawn_obstacle(); }
206
-
207
- for (i = 0; i < MAX_OBSTACLES; i++) {
208
- if (obstacles[i].alive && aabb(&player, &obstacles[i])) {
209
- game_over_timer = 60;
210
- sfx_play(2); /* crash */
211
- break;
212
- }
213
- }
214
- if (score < 65500) score++;
725
+ u16 pad, padR;
726
+
727
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
728
+ * Init order: console text pointers FIRST (the font/map live ABOVE word
729
+ * $4000 because Mode 7 owns $0000-$3FFF and has no base register to move
730
+ * it), then mode, then VRAM uploads while the screen is still off. */
731
+ consoleSetTextMapPtr(0x6800);
732
+ consoleSetTextGfxPtr(0x5000);
733
+ consoleSetTextOffset(0x0000);
734
+ consoleInitText(0, 16 * 2, &tilfont, &palfont);
735
+ setMode(BG_MODE1, 0);
736
+ bgSetGfxPtr(0, 0x5000);
737
+ bgSetMapPtr(0, 0x6800, SC_32x32);
738
+ bgSetDisable(1); /* BG2/BG3 carry garbage in mode 1 — */
739
+ bgSetDisable(2); /* the road + HUD both live on BG1 */
740
+
741
+ /* CGRAM: Mode 7 is 8bpp, the tile byte IS the palette index so the
742
+ * ground colours share the font palette's block. 0 = backdrop = the sky;
743
+ * 1 stays white (text); 2..9 are the ground inks the tiles use. */
744
+ setPaletteColor(0, RGB5(11, 18, 28)); /* sky */
745
+ setPaletteColor(2, RGB5(6, 18, 6)); /* grass mid */
746
+ setPaletteColor(3, RGB5(4, 13, 4)); /* grass dark */
747
+ setPaletteColor(4, RGB5(11, 11, 12)); /* asphalt */
748
+ setPaletteColor(5, RGB5(15, 15, 16)); /* asphalt fleck */
749
+ setPaletteColor(6, RGB5(26, 5, 4)); /* kerb red */
750
+ setPaletteColor(7, RGB5(31, 31, 31)); /* kerb/finish white */
751
+ setPaletteColor(8, RGB5(30, 27, 6)); /* centre-line yellow */
752
+ setPaletteColor(9, RGB5(20, 20, 21)); /* finish grey */
753
+
754
+ build_ring_tables();
755
+ upload_m7_vram();
756
+ m7_tables_init();
757
+
758
+ /* Mode 7 statics: M7SEL=0 wraps the 1024px map (the looping world!);
759
+ * matrix gets sane vblank defaults, HDMA rewrites it every band anyway.
760
+ * ALL of these are write-twice (lo then hi) single writes half-latch. */
761
+ REG_M7SEL = 0;
762
+ REG_M7A = 0x00; REG_M7A = 0x01;
763
+ REG_M7B = 0x00; REG_M7B = 0x00;
764
+ REG_M7C = 0x00; REG_M7C = 0x00;
765
+ REG_M7D = 0x00; REG_M7D = 0x01;
766
+
767
+ /* OBJ: 16x16 car at VRAM $4000 (clear of the Mode 7 area). The car page
768
+ * is laid out for large sprites: quadrants at page tiles 0,1,16,17. */
769
+ oamInitGfxSet(&tilsprite, 1024, &palsprite, 32, 0, 0x4000, OBJ_SIZE8_L16);
770
+
771
+ setScreenOn();
772
+
773
+ /* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
774
+ * the return: a wedged SPC700 must not take the video down with it. ── */
775
+ sound_ok = (sfx_init() == 0);
776
+ /* ── HARDWARE IDIOM (load-bearing) one frame between init and the first
777
+ * command. sfx_init returns the instant the SPC echoes the jump command,
778
+ * but the driver then spends ~50 port writes initialising the DSP BEFORE
779
+ * it seeds its command edge-detector from $2140. Send a command in that
780
+ * window and the seed swallows it — music silently never starts (found
781
+ * via getAudioState: voice 1 pitch 0, ARAM prev_cmd already = 3). A
782
+ * WaitForVBlank is thousands of SPC cycles — deterministic cure. ── */
783
+ WaitForVBlank();
784
+ if (sound_ok) sfx_music_play();
785
+
786
+ best = best_load(); /* battery SRAM — 0 on first boot */
787
+ prev_pad0 = prev_padR = 0;
788
+ backbuf = 0;
789
+ title_enter();
790
+
791
+ while (1) {
792
+ pad = padsCurrent(0);
793
+
794
+ if (state == ST_TITLE) {
795
+ attract_update();
796
+ if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
797
+ (pad & KEY_START && !(prev_pad0 & KEY_START))) {
798
+ mode_2p = 0; run_player = 0; ready_enter();
799
+ } else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
800
+ mode_2p = 1; run_player = 0; ready_enter();
801
+ }
802
+ } else if (state == ST_READY) {
803
+ /* the handoff reads THE RUNNER'S pad — port 1 (controller 2) when
804
+ * it's player 2's run. That's the whole 2P wiring: padsCurrent(1). */
805
+ padR = padsCurrent(run_player);
806
+ if ((padR & (KEY_START | KEY_A)) && !(prev_padR & (KEY_START | KEY_A)))
807
+ race_enter();
808
+ prev_padR = padR;
809
+ } else if (state == ST_RACE) {
810
+ race_update();
811
+ } else { /* ST_RESULT */
812
+ if (pad & KEY_START && !(prev_pad0 & KEY_START)) title_enter();
215
813
  }
216
- return 0;
814
+ prev_pad0 = pad;
815
+ telem_update();
816
+
817
+ /* Build the back-buffer HDMA tables NOW (takes ~30% of the frame),
818
+ * then commit them in the next vblank. Result screen = plain Mode 1,
819
+ * nothing to build. */
820
+ if (state != ST_RESULT) m7_stage();
821
+ oamUpdate();
822
+
823
+ WaitForVBlank();
824
+ if (state != ST_RESULT) m7_commit(); /* vblank-only writes — first! */
825
+ consoleVblank();
826
+ }
827
+ return 0;
217
828
  }