romdevtools 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -177
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -180
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -156
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +19 -6
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +64 -19
|
@@ -1,198 +1,828 @@
|
|
|
1
|
-
/* ── racing.c — SNES
|
|
1
|
+
/* ── racing.c — SNES Mode 7 racer (complete example game) ────────────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
/*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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;
|
|
73
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;
|
|
74
483
|
}
|
|
75
484
|
|
|
76
|
-
static void
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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;
|
|
105
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;
|
|
106
722
|
}
|
|
107
723
|
|
|
108
724
|
int main(void) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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();
|
|
196
813
|
}
|
|
197
|
-
|
|
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;
|
|
198
828
|
}
|