romdevtools 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- 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 +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- 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 -196
- 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 -198
- 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 -163
- 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 +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- 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 +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- 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 +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- 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 +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- 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 +13 -3
- 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 +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- 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/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- 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/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- 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 +27 -11
|
@@ -1,239 +1,982 @@
|
|
|
1
|
-
/* ── racing.c —
|
|
1
|
+
/* ── racing.c — Game Gear top-down road racer (complete example game) ────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* CHICANE DASH — a COMPLETE, working game: title screen, 1P endless race with
|
|
4
|
+
* speed control, 2P simultaneous SPLIT-LANE VERSUS (both cars on screen at
|
|
5
|
+
* once — player 2 on PORT B), a vertically-scrolling road done the GG/SMS way
|
|
6
|
+
* (whole-plane R9 vertical scroll, latched once per frame), streamed roadside
|
|
7
|
+
* scenery rows, crash/lives rules, persistent best DISTANCE (Sega-mapper cart
|
|
8
|
+
* RAM — see the honesty note at best_save), PSG music + SFX, and the GG/SMS
|
|
9
|
+
* signature LINE-INTERRUPT split holding a fixed HUD strip over the road.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* THIS FILE IS THE GG TWIN of the SMS racing (FENDER FURY). The GG VDP IS the
|
|
12
|
+
* SMS VDP — same Mode-4 hardware, same SN76489 PSG, same R9 road, same I/O.
|
|
13
|
+
* There is exactly ONE thing that changes everything about placement:
|
|
14
|
+
*
|
|
15
|
+
* THE GG VISIBLE WINDOW — the VDP renders a full 256×192 frame; the LCD
|
|
16
|
+
* shows only the CENTERED 160×144 of it. Every hardware coordinate (sprite
|
|
17
|
+
* OAM x/y, tilemap rows/cols, AND the line counter's scanline number) is in
|
|
18
|
+
* the FULL 256×192 frame; content placed outside the centered window is
|
|
19
|
+
* rendered "correctly" and simply never shown. So the title, the road, the
|
|
20
|
+
* HUD and ALL gameplay must sit INSIDE the window — derive every coordinate
|
|
21
|
+
* from the VIS_* block below, never hardcode an SMS-frame number. (The
|
|
22
|
+
* emulator screenshot is the 160×144 visible crop — "my sprite is at y=10
|
|
23
|
+
* but invisible" means it's parked in the unseen border, not a render bug.)
|
|
24
|
+
*
|
|
25
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
26
|
+
* very different one. The markers tell you what's what:
|
|
27
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GG footgun; reshape
|
|
28
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
29
|
+
* GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
|
|
30
|
+
*
|
|
31
|
+
* What depends on what:
|
|
32
|
+
* gg_hw.h / vdp_init.c / load_tiles.c / load_palette.c / sprite_table.c /
|
|
33
|
+
* joypad_read.c — the bundled VDP + input runtime (this file's externs).
|
|
34
|
+
* gg_sfx.{h,c} + gg_music.{h,c} — SN76489 PSG sound layers.
|
|
35
|
+
* gg_crt0.s — boot + vector table. Its $0038 IM-1 handler is the OTHER
|
|
36
|
+
* HALF of the line-interrupt idiom below: one status-port read acks BOTH
|
|
37
|
+
* the frame and line IRQ flags, then ei/reti. Load-bearing; edit with
|
|
38
|
+
* TROUBLESHOOTING open.
|
|
39
|
+
*
|
|
40
|
+
* THE DESIGN (read before reshaping):
|
|
41
|
+
* Scrolling — the road is the BACKGROUND, scrolled DOWN by DECREMENTING the
|
|
42
|
+
* vertical-scroll register R9 each frame (the driving-up illusion). Cars
|
|
43
|
+
* and traffic are sprites with their own Y. Compare the Genesis version of
|
|
44
|
+
* this game (examples/genesis/templates/racing.c): there a single VSRAM
|
|
45
|
+
* value scrolls the whole plane and the VDP wraps it in hardware at 256.
|
|
46
|
+
* The GG/SMS is the SAME idea — ONE register, whole-plane, single-plane —
|
|
47
|
+
* with two twists this file is built around: (1) R9 is LATCHED ONCE PER
|
|
48
|
+
* FRAME, not per scanline (so the road scrolls per-frame, never mid-frame
|
|
49
|
+
* — see the R9 idiom); (2) in 192-line mode the name table is 32x28 = 224
|
|
50
|
+
* px tall, so R9 WRAPS AT 224, not 256 (the GG/SMS analog of the NES's
|
|
51
|
+
* 240-wrap; plain uint8 math overruns it — see scroll_road_down).
|
|
52
|
+
* Streamed scenery — as the road scrolls, name-table rows re-enter at the
|
|
53
|
+
* TOP; the moment a row becomes the top road row we restamp its roadside
|
|
54
|
+
* cells with fresh random scenery, so the 224-px loop never shows the same
|
|
55
|
+
* scenery twice. The restamp lands UNDER the HUD strip, which hides it.
|
|
56
|
+
* HUD — the line-IRQ split. The road scrolls vertically (R9, whole plane),
|
|
57
|
+
* so the HUD strip's name-table rows scroll with it — a BG HUD over a
|
|
58
|
+
* vertical road would crawl. So HUD GLYPHS ARE SPRITES on the fixed top
|
|
59
|
+
* scanlines (immune to R9, exactly as the NES version uses sprite digits
|
|
60
|
+
* for the same reason). The line-IRQ split still earns its keep: it holds
|
|
61
|
+
* the top HUD band at horizontal scroll 0 while the road BELOW it SWAYS
|
|
62
|
+
* left/right per-strip (R8 — the one scroll axis you CAN change mid-frame),
|
|
63
|
+
* a gentle curve that reads as the road bending ahead — a CHICANE. Fixed
|
|
64
|
+
* un-swayed HUD band on top, curving road below: a real line-IRQ split.
|
|
65
|
+
* 2P VERSUS — ONE VDP means ONE road scroll, so both players share one road
|
|
66
|
+
* at a fixed speed and only steer (the same constraint the NES/Genesis
|
|
67
|
+
* versions explain): solid center divider, P1 (white, port A) owns the left
|
|
68
|
+
* two lanes, P2 (red, port B) the right two. Each starts with 3 crashes;
|
|
69
|
+
* first to use them all LOSES.
|
|
70
|
+
* 1P RACE — all four lanes, button 1/UP accelerates, button 2/DOWN brakes
|
|
71
|
+
* (speed 1-4); 3 crashes end the run. Persistent stat: best DISTANCE
|
|
72
|
+
* (uint16, one unit = 16 scrolled pixels ≈ one car length) via best_save.
|
|
73
|
+
*
|
|
74
|
+
* Frame budget (NTSC, 60fps): SAT upload (192 OUTs) + the HUD sprite stage fit
|
|
75
|
+
* in vblank + the HUD strip; 6 traffic × 2 cars of AABB and one row restamp at
|
|
76
|
+
* most every other frame run in the active frame with room to spare. The GG
|
|
77
|
+
* split budget is BIGGER than the SMS's — the 24 never-shown border lines above
|
|
78
|
+
* the window are free cycles before the split at scanline 47.
|
|
79
|
+
*
|
|
80
|
+
* SDCC FOOTGUN (bites every fork): uint8 loop bounds silently wrap —
|
|
81
|
+
* `for (uint8_t i = 0; i < 24 * 32; i++)` is an INFINITE loop (768 > 255;
|
|
82
|
+
* SDCC even warns "comparison is always true"). Treat that warning as an
|
|
83
|
+
* error: widen the counter to uint16_t or keep loops nested per-row like the
|
|
84
|
+
* painters below.
|
|
12
85
|
*/
|
|
13
86
|
#include "gg_hw.h"
|
|
14
87
|
#include "gg_sfx.h"
|
|
15
88
|
#include "gg_music.h"
|
|
16
89
|
#include <stdint.h>
|
|
17
90
|
|
|
91
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
92
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
93
|
+
#define GAME_TITLE "CHICANE DASH"
|
|
94
|
+
|
|
18
95
|
extern void gg_vdp_init(void);
|
|
96
|
+
extern void gg_vdp_write_reg(uint8_t reg, uint8_t value);
|
|
19
97
|
extern void gg_vdp_display_on(void);
|
|
98
|
+
extern void gg_vdp_display_off(void);
|
|
99
|
+
extern void gg_vdp_set_addr(uint16_t addr, uint8_t prefix);
|
|
20
100
|
extern void gg_load_palette(const uint8_t *palette);
|
|
21
101
|
extern void gg_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
|
|
22
102
|
extern void gg_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
|
|
23
|
-
extern void gg_vblank_wait(void);
|
|
24
103
|
extern uint8_t gg_joypad_read(void);
|
|
104
|
+
extern uint8_t gg_joypad_read_p2(void);
|
|
25
105
|
extern void gg_sprite_init(void);
|
|
26
106
|
extern void gg_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
|
|
27
107
|
extern void gg_sat_upload(void);
|
|
28
108
|
|
|
29
|
-
/* ──
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
109
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
110
|
+
* THE GG VISIBLE WINDOW. The VDP frame is 256×192; the LCD shows the
|
|
111
|
+
* centered 160×144. In FULL-FRAME hardware units the window is:
|
|
112
|
+
*
|
|
113
|
+
* pixels: x ∈ [48..207] y ∈ [24..167] (sprite coords, scanlines)
|
|
114
|
+
* tilemap: col ∈ [6..25] row ∈ [3..20] (20×18 visible cells)
|
|
115
|
+
*
|
|
116
|
+
* EVERYTHING the hardware takes is full-frame: gg_sprite_set x/y, tilemap
|
|
117
|
+
* row/col, and — easy to forget — the LINE COUNTER (VDP R10) counts
|
|
118
|
+
* full-frame scanlines from the top of the 192-line active area, NOT from
|
|
119
|
+
* the top of the LCD. The window's first visible scanline is 24.
|
|
120
|
+
*
|
|
121
|
+
* Requires: nothing — these are constants of the machine. Everything below
|
|
122
|
+
* (HUD placement, split line, road geometry, sprite Y, text columns) is
|
|
123
|
+
* derived from them; if you reshape the layout, derive from VIS_*, never
|
|
124
|
+
* hardcode SMS-frame numbers. */
|
|
125
|
+
#define VIS_X0 48 /* left edge of the LCD window (hardware X) */
|
|
126
|
+
#define VIS_Y0 24 /* top edge (hardware Y / scanline) */
|
|
127
|
+
#define VIS_X1 207 /* right edge: 48 + 160 - 1 */
|
|
128
|
+
#define VIS_Y1 167 /* bottom edge: 24 + 144 - 1 */
|
|
129
|
+
#define VIS_W 160
|
|
130
|
+
#define VIS_H 144
|
|
131
|
+
#define VIS_COL0 6 /* first visible tilemap column (48 / 8) */
|
|
132
|
+
#define VIS_ROW0 3 /* first visible tilemap row (24 / 8) */
|
|
133
|
+
#define VIS_COLS 20 /* 160 / 8 */
|
|
134
|
+
#define VIS_ROWS 18 /* 144 / 8 */
|
|
135
|
+
/* Think in window space (0..19 cols, 0..17 rows), convert at the call: */
|
|
136
|
+
#define VROW(r) ((uint8_t)((r) + VIS_ROW0))
|
|
137
|
+
#define VCOL(c) ((uint8_t)((c) + VIS_COL0))
|
|
138
|
+
|
|
139
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
140
|
+
* Palette. THE GG's HEADLINE UPGRADE over the SMS: CRAM holds 12-bit
|
|
141
|
+
* 4-4-4 BGR colour (4096 colours) instead of the SMS's 6-bit 2-2-2 (64).
|
|
142
|
+
* The WRITE FORMAT differs too — that's the #2 GG footgun:
|
|
143
|
+
*
|
|
144
|
+
* SMS: 32 entries × 1 byte --BBGGRR
|
|
145
|
+
* GG: 32 entries × 2 bytes little-endian: low byte = GGGGRRRR
|
|
146
|
+
* high byte = ----BBBB
|
|
147
|
+
*
|
|
148
|
+
* So a GG palette array is 64 bytes (entries 0-15 BG, 16-31 sprite). Feeding
|
|
149
|
+
* gg_load_palette a 32-byte SMS-style table reads past the array — the sprite
|
|
150
|
+
* palette loads garbage and every sprite renders invisible (this exact bug
|
|
151
|
+
* shipped in an earlier GG scaffold round). Pack an entry with:
|
|
152
|
+
* low = (g << 4) | r, high = b, each channel 0..15. The dusk asphalt + verge
|
|
153
|
+
* green below have no 2-2-2 SMS equivalent — the 4096-colour panel earning
|
|
154
|
+
* its keep. */
|
|
51
155
|
static const uint8_t palette[64] = {
|
|
52
|
-
/* BG 0-15: 0 =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
156
|
+
/* BG 0-15: 0 = dusk asphalt (backdrop = the road itself), 1 = verge green,
|
|
157
|
+
* 2 = white (markings + text), 3 = dark speck, 4 = HUD navy */
|
|
158
|
+
0x65,0x05, 0x83,0x01, 0xFF,0x0F, 0x32,0x03, 0x40,0x05,
|
|
159
|
+
0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
|
|
160
|
+
/* SPRITE 16-31: 16 = transparent, 17 = white (P1 car + HUD digits),
|
|
161
|
+
* 18 = warm red (P2 car + traffic), 19 = gold (flash, unused-by-default).
|
|
162
|
+
* One shared sprite palette on GG/SMS — per-"car" colour means per-TILE
|
|
163
|
+
* colour indices, not per-sprite palettes. */
|
|
164
|
+
0,0, 0xFF,0x0F, 0x12,0x0E, 0x8F,0x0F,
|
|
165
|
+
0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/* ── GAME LOGIC (clay) — BG tile inventory (BG bank $0000) ───────────────────
|
|
169
|
+
* tile 0 = blank (shows the colour-0 asphalt backdrop = the road)
|
|
170
|
+
* tiles 1..37 = font: digits 0-9, A-Z, '-' (uploaded 1bpp→4bpp below)
|
|
171
|
+
* tile 38 = grass (colour 1)
|
|
172
|
+
* tile 39 = tuft (grass + speck, colour 1/3)
|
|
173
|
+
* tile 40 = solid HUD bar (colour 4) — the split seam hides in it
|
|
174
|
+
* tile 41 = tarmac speck (colour 3 dot on asphalt)
|
|
175
|
+
* tile 42 = solid shoulder/divider line (colour 2 = white)
|
|
176
|
+
* tile 43 = dashed lane line (colour 2, 4 px on / 4 off) */
|
|
177
|
+
#define FONT_BASE 1
|
|
178
|
+
#define BG_GRASS 38
|
|
179
|
+
#define BG_TUFT 39
|
|
180
|
+
#define BG_HUDBAR 40
|
|
181
|
+
#define BG_SPECK 41
|
|
182
|
+
#define BG_EDGE 42
|
|
183
|
+
#define BG_DASH 43
|
|
184
|
+
|
|
185
|
+
/* 1bpp font (same glyph set as the platformer/shmup examples — 0-9, A-Z, '-').
|
|
186
|
+
* Expanded to the VDP's 32-byte 4bpp tiles at upload (see load_font), so the
|
|
187
|
+
* ROM carries 296 bytes instead of 1184. */
|
|
188
|
+
static const uint8_t font8[37][8] = {
|
|
189
|
+
/* 0-9 */
|
|
190
|
+
{0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
191
|
+
{0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
|
|
192
|
+
{0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
|
|
193
|
+
{0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
|
|
194
|
+
{0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
|
|
195
|
+
/* A-Z */
|
|
196
|
+
{0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
|
|
197
|
+
{0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
|
|
198
|
+
{0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
|
|
199
|
+
{0x3C,0x66,0x60,0x6E,0x66,0x66,0x3E,0x00}, {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
|
|
200
|
+
{0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00}, {0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00},
|
|
201
|
+
{0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
|
|
202
|
+
{0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
|
|
203
|
+
{0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
|
|
204
|
+
{0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
|
|
205
|
+
{0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
|
206
|
+
{0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
|
|
207
|
+
{0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
|
|
208
|
+
{0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
|
|
209
|
+
/* '-' */
|
|
210
|
+
{0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
|
58
211
|
};
|
|
59
212
|
|
|
60
|
-
/*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
213
|
+
/* Expand 1bpp glyphs into 4bpp tiles as colour 2 (plane 1 set).
|
|
214
|
+
* GG/SMS tile rows are 4 bytes: plane0, plane1, plane2, plane3. */
|
|
215
|
+
static void load_font(void) {
|
|
216
|
+
uint8_t g, r, bits;
|
|
217
|
+
gg_vdp_set_addr((uint16_t)(FONT_BASE * 32), VDP_VRAM_WRITE);
|
|
218
|
+
for (g = 0; g < 37; g++) {
|
|
219
|
+
for (r = 0; r < 8; r++) {
|
|
220
|
+
bits = font8[g][r];
|
|
221
|
+
PORT_VDP_DATA = 0; /* plane 0 */
|
|
222
|
+
PORT_VDP_DATA = bits; /* plane 1 → colour index 2 (white) */
|
|
223
|
+
PORT_VDP_DATA = 0; /* plane 2 */
|
|
224
|
+
PORT_VDP_DATA = 0; /* plane 3 */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Road/roadside/HUD-bar tiles (4bpp, 32 bytes each — rows of plane0..3). */
|
|
230
|
+
static const uint8_t deco_tiles[192] = {
|
|
231
|
+
/* BG_GRASS: solid colour 1 (plane 0) */
|
|
232
|
+
0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
|
|
233
|
+
0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
|
|
234
|
+
/* BG_TUFT: grass (colour 1) with a couple speck dots (colour 3 = planes 0+1) */
|
|
235
|
+
0xFF,0x00,0x00,0x00, 0xFF,0x18,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x42,0x00,0x00,
|
|
236
|
+
0xFF,0x00,0x00,0x00, 0xFF,0x18,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
|
|
237
|
+
/* BG_HUDBAR: solid colour 4 (binary 100 → plane 2 only) — the split seam
|
|
238
|
+
* lands inside this row */
|
|
239
|
+
0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
|
|
240
|
+
0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
|
|
241
|
+
/* BG_SPECK: asphalt (colour 0) with a few colour-3 specks so the scroll is
|
|
242
|
+
* readable on the otherwise-flat road (plane 0+1 dots) */
|
|
243
|
+
0x00,0x00,0x00,0x00, 0x10,0x10,0x00,0x00, 0x00,0x00,0x00,0x00, 0x02,0x02,0x00,0x00,
|
|
244
|
+
0x00,0x00,0x00,0x00, 0x08,0x08,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
245
|
+
/* BG_EDGE: solid white vertical stripe (colour 2 = plane 1), 2 px wide */
|
|
246
|
+
0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00,
|
|
247
|
+
0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00,
|
|
248
|
+
/* BG_DASH: dashed lane line — 4 px white (colour 2) on, 4 off, stacked */
|
|
249
|
+
0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00,
|
|
250
|
+
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
78
251
|
};
|
|
79
252
|
|
|
80
|
-
/*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
0x00,
|
|
89
|
-
|
|
90
|
-
0x00,0x7E,0x00,0x00, 0x00,
|
|
91
|
-
0x00,0x7E,0x00,0x00, 0x00,
|
|
253
|
+
/* Sprite tiles (sprite bank $2000 — vdp_init's R6=0xFF baseline reads sprite
|
|
254
|
+
* patterns from $2000, so upload there, not $0000).
|
|
255
|
+
* T_CAR — player car, nose up, colour 1 (white)
|
|
256
|
+
* T_TRAFFIC — slow traffic, tail up, colour 2 (red)
|
|
257
|
+
* T_DIGIT0 — 3x5 HUD digits 0-9 (sprites, colour 1) on the fixed top line */
|
|
258
|
+
static const uint8_t sprite_tiles[(2 + 10) * 32] = {
|
|
259
|
+
/* T_CAR (white, plane 0) */
|
|
260
|
+
0x18,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00, 0x5A,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00,
|
|
261
|
+
0x3C,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00, 0x5A,0x00,0x00,0x00, 0x66,0x00,0x00,0x00,
|
|
262
|
+
/* T_TRAFFIC (red, plane 1) */
|
|
263
|
+
0x00,0x66,0x00,0x00, 0x00,0x5A,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x00,0x3C,0x00,0x00,
|
|
264
|
+
0x00,0x7E,0x00,0x00, 0x00,0x5A,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x00,0x18,0x00,0x00,
|
|
265
|
+
/* T_DIGIT0..9 — compact 3x5 white digits (plane 0) on a fixed HUD scanline */
|
|
266
|
+
/* 0 */ 0xE0,0,0,0, 0xA0,0,0,0, 0xA0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
267
|
+
/* 1 */ 0x40,0,0,0, 0xC0,0,0,0, 0x40,0,0,0, 0x40,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
268
|
+
/* 2 */ 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0x80,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
269
|
+
/* 3 */ 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
270
|
+
/* 4 */ 0xA0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0x20,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
271
|
+
/* 5 */ 0xE0,0,0,0, 0x80,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
272
|
+
/* 6 */ 0xE0,0,0,0, 0x80,0,0,0, 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
273
|
+
/* 7 */ 0xE0,0,0,0, 0x20,0,0,0, 0x20,0,0,0, 0x40,0,0,0, 0x40,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
274
|
+
/* 8 */ 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
275
|
+
/* 9 */ 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
|
|
92
276
|
};
|
|
277
|
+
#define T_CAR 0
|
|
278
|
+
#define T_TRAFFIC 1
|
|
279
|
+
#define T_DIGIT0 2 /* sprite tiles 2..11 = digits 0..9 */
|
|
280
|
+
|
|
281
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
282
|
+
* Road geometry, in WINDOW columns (0..19). Four lanes between shoulders, a
|
|
283
|
+
* solid center divider (also the 2P territory line). Window columns (cells):
|
|
284
|
+
* 1 = left shoulder, 5/13 = dashed lane lines, 9 = center divider,
|
|
285
|
+
* 17 = right shoulder; grass outside. We convert to hardware cols at paint
|
|
286
|
+
* time so the whole road sits inside the visible 160×144 window. */
|
|
287
|
+
#define COL_EDGE_L 1
|
|
288
|
+
#define COL_DASH_1 5
|
|
289
|
+
#define COL_DIVIDER 9
|
|
290
|
+
#define COL_DASH_2 13
|
|
291
|
+
#define COL_EDGE_R 17
|
|
292
|
+
/* Lane center X (FULL-FRAME hardware) for the 8px-wide car sprite. Each lane
|
|
293
|
+
* spans 16 px inside the window; VIS_X0 (48) baked in so cars stay on screen. */
|
|
294
|
+
static const uint8_t lane_x[4] = { 76, 100, 124, 148 };
|
|
295
|
+
|
|
296
|
+
#define MAX_TRAFFIC 6
|
|
297
|
+
#define CAR_Y 152 /* both players' fixed screen Y (inside window) */
|
|
298
|
+
#define SPAWN_Y 48 /* traffic entry Y — below the HUD strip */
|
|
299
|
+
#define DESPAWN_Y 168 /* traffic exits past the player (= VIS_Y1+1) */
|
|
300
|
+
#define START_LIVES 3 /* crashes per run / per player */
|
|
301
|
+
#define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic moves
|
|
302
|
+
* at road speed, so per-meter density stays
|
|
303
|
+
* constant whatever the player does */
|
|
304
|
+
#define SPEED_2P 2 /* fixed road speed in versus (one VDP =
|
|
305
|
+
* one scroll = one shared speed) */
|
|
306
|
+
#define MAX_SPEED 4 /* px/frame — MUST stay under 8: the row
|
|
307
|
+
* streamer restamps one row per crossing and a
|
|
308
|
+
* >8 px step could skip a row */
|
|
309
|
+
|
|
310
|
+
/* HUD strip: window rows 0-2 (hardware rows 3-5). Row 0 holds nothing in the
|
|
311
|
+
* BG (the sprite digits ride there); row 2 is the solid bar where the split
|
|
312
|
+
* seam hides. The 3-row strip is held un-swayed by the line-IRQ split. */
|
|
313
|
+
#define HUD_ROWS 3
|
|
314
|
+
#define HUD_PX (HUD_ROWS * 8)
|
|
315
|
+
|
|
316
|
+
/* Players: index 0 = P1 (port A), 1 = P2 (port B — versus only). */
|
|
317
|
+
static uint8_t car_lane[2];
|
|
318
|
+
static uint8_t car_active[2];
|
|
319
|
+
static uint8_t crashes_left[2];
|
|
320
|
+
static uint8_t invuln[2]; /* post-crash blink/no-collide frames */
|
|
321
|
+
static uint8_t prev_pad[2];
|
|
322
|
+
static uint8_t lane_min[2], lane_max[2]; /* 2P: split territories */
|
|
323
|
+
static uint8_t two_player;
|
|
324
|
+
static uint8_t winner; /* versus result: 0 = P1, 1 = P2 */
|
|
325
|
+
|
|
326
|
+
static uint8_t traffic_alive[MAX_TRAFFIC];
|
|
327
|
+
static uint8_t traffic_lane[MAX_TRAFFIC];
|
|
328
|
+
static uint8_t traffic_y[MAX_TRAFFIC];
|
|
329
|
+
|
|
330
|
+
static uint8_t speed; /* road px/frame, 1..MAX_SPEED */
|
|
331
|
+
static uint16_t dist; /* 1P distance, 1 unit = 16 scrolled px */
|
|
332
|
+
static uint8_t dist_frac;
|
|
333
|
+
static uint16_t best; /* persisted best 1P distance */
|
|
334
|
+
static uint8_t spawn_timer;
|
|
335
|
+
static uint8_t road_scroll; /* R9 vertical scroll, ALWAYS kept 0..223 */
|
|
336
|
+
static uint8_t prev_top_row; /* last restamped name-table row */
|
|
337
|
+
static uint8_t start_pause; /* freeze frames at green light */
|
|
338
|
+
static uint8_t hud_dirty; /* lives/speed changed → restage sprite HUD */
|
|
339
|
+
static uint8_t over_step; /* result text, one piece per vblank */
|
|
340
|
+
static uint16_t rng = 0xC0DE;
|
|
341
|
+
|
|
342
|
+
/* HUD digit cache — SDCC's 16-bit div/mod helpers cost hundreds of cycles
|
|
343
|
+
* each; recompute the 5 distance digits only when dist actually changes. */
|
|
344
|
+
static uint8_t hud_digits[5];
|
|
345
|
+
static uint16_t hud_cached = 0xFFFF;
|
|
346
|
+
|
|
347
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
348
|
+
#define ST_TITLE 0
|
|
349
|
+
#define ST_PLAY 1
|
|
350
|
+
#define ST_OVER 2
|
|
351
|
+
static uint8_t state;
|
|
352
|
+
|
|
353
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
354
|
+
static uint8_t random8(void) {
|
|
355
|
+
uint16_t r = rng;
|
|
356
|
+
r ^= r << 7;
|
|
357
|
+
r ^= r >> 9;
|
|
358
|
+
r ^= r << 8;
|
|
359
|
+
rng = r;
|
|
360
|
+
return (uint8_t)r;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
static uint8_t dist8(uint8_t a, uint8_t b) {
|
|
364
|
+
return (a > b) ? (uint8_t)(a - b) : (uint8_t)(b - a);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
368
|
+
* WHOLE-PLANE VERTICAL ROAD SCROLL — R9, the GG/SMS road. The vertical-scroll
|
|
369
|
+
* register R9 scrolls the ENTIRE name-table plane up/down; screen line y shows
|
|
370
|
+
* plane line (y + R9) mod (plane height), so DECREMENTING R9 slides the road
|
|
371
|
+
* DOWN — the driving-up illusion — for the cost of ONE register write per
|
|
372
|
+
* frame. Zero tilemap writes for the motion itself (rewriting the tilemap in
|
|
373
|
+
* the loop is the #1 "choppy movement" bug).
|
|
374
|
+
*
|
|
375
|
+
* TWO twists this game is built around (vs the Genesis donor's plain u16):
|
|
376
|
+
* 1. R9 IS LATCHED ONCE PER FRAME. The VDP samples R9 at the start of the
|
|
377
|
+
* active display and ignores mid-frame writes until the next frame. So a
|
|
378
|
+
* vertical scroll is a per-FRAME whole-plane move — you cannot split it
|
|
379
|
+
* mid-screen the way you split X-scroll (R8). (That's why the fixed HUD
|
|
380
|
+
* below uses SPRITE glyphs + an R8 sway split, not a mid-frame R9 swap.)
|
|
381
|
+
* Always write R9 in vblank.
|
|
382
|
+
* 2. R9 WRAPS AT 224, NOT 256. In 192-line mode the name table is 32x28 =
|
|
383
|
+
* 224 px tall; R9 values 224-255 make the VDP fetch the unused rows 28-31
|
|
384
|
+
* (garbage). Plain uint8 math happily produces 224-255, so EVERY change
|
|
385
|
+
* to road_scroll goes through this helper. (The NES analog wraps at 240;
|
|
386
|
+
* the Genesis plane is 32x32 = 256 and wraps in hardware for free.)
|
|
387
|
+
* Scrolling DOWN = the road slides toward the player = R9 DECREASES. */
|
|
388
|
+
#define PLANE_H 224
|
|
389
|
+
static void scroll_road_down(uint8_t px) {
|
|
390
|
+
if (road_scroll >= px) road_scroll = (uint8_t)(road_scroll - px);
|
|
391
|
+
else road_scroll = (uint8_t)(road_scroll + PLANE_H - px);
|
|
392
|
+
gg_vdp_write_reg(9, road_scroll); /* commit vertical scroll (vblank only) */
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
396
|
+
* STREAMED ROADSIDE ROWS. As R9 shrinks, name-table rows recycle into the top
|
|
397
|
+
* of the screen; the road row entering at the top is plane row (R9 >> 3) mod
|
|
398
|
+
* 28. The moment it changes we restamp THAT ONE row's roadside cells with
|
|
399
|
+
* fresh random scenery, so the 224-px loop never shows the same grass twice.
|
|
400
|
+
* Three rules:
|
|
401
|
+
* 1. Restamp in VBLANK only (this game's main loop calls it right after the
|
|
402
|
+
* vblank wait): raw VRAM writes during active display race the VDP's own
|
|
403
|
+
* fetches and drop/garble bytes on real hardware.
|
|
404
|
+
* 2. The restamped row enters UNDER the HUD strip, which hides the swap.
|
|
405
|
+
* Restamp rows lower and the player sees tiles pop.
|
|
406
|
+
* 3. Road speed stays under 8 px/frame (MAX_SPEED) so a frame never skips a
|
|
407
|
+
* whole row crossing.
|
|
408
|
+
* Cols here are HARDWARE name-table cols inside the verge bands. */
|
|
409
|
+
static void stream_road_row(uint8_t row) {
|
|
410
|
+
uint8_t r;
|
|
411
|
+
/* Restamp ONLY the verge grass cols that sit OUTSIDE the centered title
|
|
412
|
+
* text (window cols ~4..15). The left verge is window col 0 (col 1 is the
|
|
413
|
+
* white shoulder edge); the right verge is window cols 18,19 (col 17 is the
|
|
414
|
+
* shoulder edge). Restamping inside the title span would chew the title
|
|
415
|
+
* letters as rows recycle under the idling road — keep to the margins. */
|
|
416
|
+
r = random8(); gg_set_tilemap_cell(row, VCOL(0), (r & 7) == 0 ? BG_TUFT : BG_GRASS, 0);
|
|
417
|
+
r = random8(); gg_set_tilemap_cell(row, VCOL(18), (r & 7) == 0 ? BG_TUFT : BG_GRASS, 0);
|
|
418
|
+
r = random8(); gg_set_tilemap_cell(row, VCOL(19), (r & 7) == 0 ? BG_TUFT : BG_GRASS, 0);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
422
|
+
* LINE-INTERRUPT FIXED-HUD SPLIT + per-strip road SWAY (the CHICANE curve).
|
|
423
|
+
* The VDP has ONE scroll register pair for the whole frame; the line interrupt
|
|
424
|
+
* lets you change the X scroll (R8) MID-FRAME (R8 is sampled per line — R9 is
|
|
425
|
+
* NOT, see above). Where the NES needs the sprite-0-hit HACK (park a sprite,
|
|
426
|
+
* busy-poll, burn scanlines), the GG/SMS has a real, PROGRAMMABLE line counter:
|
|
427
|
+
*
|
|
428
|
+
* R10 = N line counter: down-counter reloaded with N every line
|
|
429
|
+
* outside the active area; underflow → IRQ at line N.
|
|
430
|
+
* R0 bit 4 (IE1) line-IRQ enable (already set in vdp_init's 0x36 baseline).
|
|
431
|
+
* R1 bit 5 (IE0) frame(vblank)-IRQ enable (set by gg_vdp_display_on's 0xE0).
|
|
432
|
+
*
|
|
433
|
+
* GG WINDOW CONTRAST (the part SMS habits get wrong): R10 counts FULL-FRAME
|
|
434
|
+
* scanlines — line 0 is the top of the 192-line active area, which is 24 lines
|
|
435
|
+
* ABOVE the LCD. The HUD strip starts at the window top (scanline VIS_Y0 = 24)
|
|
436
|
+
* and its last bar line is VIS_Y0 + HUD_PX - 1 = 47, so SPLIT_LINE is 47 —
|
|
437
|
+
* NOT 23 as it would be on an SMS with the same 3-row HUD. Lines 0..23 are
|
|
438
|
+
* rendered and never shown; they ride along with the HUD's unscrolled region
|
|
439
|
+
* for free (the GG split budget is BIGGER than the SMS's by those 24 lines).
|
|
440
|
+
*
|
|
441
|
+
* Both IRQs land on the Z80 IM-1 vector at $0038. The crt0 handler does the
|
|
442
|
+
* minimal handshake: push af / in a,($BF) / pop af / ei / reti — the status
|
|
443
|
+
* read ACKS the VDP (clears BOTH flags; skip it and the IRQ line stays
|
|
444
|
+
* asserted = interrupt storm), and EI must precede RETI.
|
|
445
|
+
*
|
|
446
|
+
* The handler does no work, so the MAIN loop syncs with HALT: sleep until an
|
|
447
|
+
* interrupt, then read the V-counter (port $7E) to learn WHICH one woke us —
|
|
448
|
+
* line IRQs fire in the active area (V < 0xC0), the frame IRQ at vblank
|
|
449
|
+
* (V ≥ 0xC0). Here the road scrolls VERTICALLY (R9, whole plane), so we cannot
|
|
450
|
+
* keep the HUD's name-table rows still by splitting R9. Instead:
|
|
451
|
+
* - HUD GLYPHS ARE SPRITES on the fixed top scanline (immune to R9).
|
|
452
|
+
* - The split holds the top HUD band at R8 = 0 (un-swayed), then below the
|
|
453
|
+
* bar applies a per-strip horizontal SWAY so the road bends left/right
|
|
454
|
+
* ahead of the player — a real raster road-curve effect (the CHICANE).
|
|
455
|
+
* Fixed HUD band on top, curving road below.
|
|
456
|
+
*
|
|
457
|
+
* wait_vblank(): sleep to the frame IRQ → R8 = 0 (HUD band un-swayed) and do
|
|
458
|
+
* per-frame VRAM work.
|
|
459
|
+
* wait_split(): sleep to the line IRQ at the bottom of the HUD bar (R10 =
|
|
460
|
+
* SPLIT_LINE) → from here down, the active loop pushes the sway
|
|
461
|
+
* value into R8 as the road draws. (We set a single sway value
|
|
462
|
+
* per frame here; reshape into a per-line table for a deeper
|
|
463
|
+
* curve, budgeting OUTs against the line time.)
|
|
464
|
+
*
|
|
465
|
+
* FOOTGUN — you cannot poll once IRQs are on: a status-port poll races the
|
|
466
|
+
* ISR, which always wins and eats the flag, hanging the poll forever. HALT +
|
|
467
|
+
* V-counter is the IRQ-era replacement.
|
|
468
|
+
*
|
|
469
|
+
* Requires: R10 programmed, IE1 + IE0 enabled, EI executed once after
|
|
470
|
+
* display-on, the crt0 ack-only ISR, and wait_vblank/wait_split called EVERY
|
|
471
|
+
* frame in this order. R10 reloads after each underflow, so the line IRQ
|
|
472
|
+
* re-fires every HUD_PX lines down the frame — the later wakes harmlessly
|
|
473
|
+
* interrupt game logic (the ISR acks them) and we re-halt in the NEXT
|
|
474
|
+
* wait_vblank(). */
|
|
475
|
+
#define SPLIT_LINE (VIS_Y0 + HUD_PX - 1)
|
|
476
|
+
static int8_t sway; /* current road horizontal sway, ±a few px */
|
|
477
|
+
static uint8_t sway_phase;
|
|
478
|
+
static const int8_t sway_wave[8] = { 0, 1, 2, 2, 0, -2, -2, -1 };
|
|
479
|
+
|
|
480
|
+
static void wait_vblank(void) {
|
|
481
|
+
/* check-first: if game logic overran into vblank, don't sleep a frame */
|
|
482
|
+
while (PORT_V_COUNTER < 0xC0) { __asm__("halt"); }
|
|
483
|
+
gg_vdp_write_reg(8, 0); /* HUD band renders with X scroll 0 */
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
static void wait_split(void) {
|
|
487
|
+
/* halt-first: vblank work always ends inside vblank (V ≥ 0xC0), and the
|
|
488
|
+
* first wake at V < 0xC0 is the line IRQ at SPLIT_LINE */
|
|
489
|
+
do { __asm__("halt"); } while (PORT_V_COUNTER >= 0xC0);
|
|
490
|
+
gg_vdp_write_reg(8, (uint8_t)sway); /* road below the bar sways */
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ── HARDWARE IDIOM (load-bearing) — BEST-DISTANCE in Sega-mapper cart RAM ────
|
|
494
|
+
* Same cartridge mapper as the SMS. The control register at $FFFC: bit 3 maps
|
|
495
|
+
* the cart's 8KB battery RAM into $8000-$BFFF (bank slot 2). Map → copy →
|
|
496
|
+
* unmap; keep the window short so stray pointer bugs can't shred the save. The
|
|
497
|
+
* block is magic + value + checksum so a never-written cart (all $FF) reads
|
|
498
|
+
* back as "no save" instead of a garbage best.
|
|
499
|
+
*
|
|
500
|
+
* NOTE the $FFFC address: it's IN the WRAM mirror ($C000-$DFFF mirrors at
|
|
501
|
+
* $E000-$FFFF), so this write also lands in WRAM at $DFFC — the mapper just
|
|
502
|
+
* snoops the bus. That's why the crt0 parks SP at $DFF0.
|
|
503
|
+
*
|
|
504
|
+
* HONESTY (verified 2026-06-10 against the bundled gpgx core, same finding as
|
|
505
|
+
* the SMS example): gpgx only instantiates the Sega mapper for ROMs LARGER
|
|
506
|
+
* than 48KB; this build pipeline emits 32KB ROMs, so in-emulator the $8000
|
|
507
|
+
* window stays open-bus (reads $FF), the magic check fails, and the game falls
|
|
508
|
+
* back to the WRAM best (in-session only). The code is the correct
|
|
509
|
+
* real-hardware idiom and lights up unchanged on a >48KB build or a cart with
|
|
510
|
+
* battery RAM: the load path is self-falsifying, never wrong. (The verify
|
|
511
|
+
* harness proves it end-to-end by padding this exact ROM to 64KB.) */
|
|
512
|
+
#define MAPPER_CTRL (*(volatile uint8_t *)0xFFFC)
|
|
513
|
+
#define CART_RAM ((volatile uint8_t *)0x8000)
|
|
514
|
+
|
|
515
|
+
static void best_save(uint16_t v) {
|
|
516
|
+
uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
|
|
517
|
+
MAPPER_CTRL = 0x08; /* map cart RAM at $8000 */
|
|
518
|
+
CART_RAM[0] = 0x42; /* 'B' */
|
|
519
|
+
CART_RAM[1] = 0x44; /* 'D' */
|
|
520
|
+
CART_RAM[2] = lo;
|
|
521
|
+
CART_RAM[3] = hi;
|
|
522
|
+
CART_RAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
|
|
523
|
+
MAPPER_CTRL = 0x00; /* back to ROM in slot 2 */
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
static uint16_t best_load(void) {
|
|
527
|
+
uint16_t v = 0;
|
|
528
|
+
MAPPER_CTRL = 0x08;
|
|
529
|
+
if (CART_RAM[0] == 0x42 && CART_RAM[1] == 0x44 &&
|
|
530
|
+
CART_RAM[4] == (uint8_t)(CART_RAM[2] ^ CART_RAM[3] ^ 0xA5)) {
|
|
531
|
+
v = (uint16_t)(CART_RAM[2] | ((uint16_t)CART_RAM[3] << 8));
|
|
532
|
+
}
|
|
533
|
+
MAPPER_CTRL = 0x00;
|
|
534
|
+
return v;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/* ── GAME LOGIC (clay) — text via the font tiles (BG name table) ─────────────
|
|
538
|
+
* These write the name table directly, so call them only during vblank (or
|
|
539
|
+
* with the display off): VRAM access during active display races the VDP's
|
|
540
|
+
* own fetches and drops/garbles bytes on real hardware. Rows/cols here are
|
|
541
|
+
* WINDOW coordinates (0..17 / 0..19) — VROW/VCOL add the border offset, so
|
|
542
|
+
* text can never accidentally land in the unseen 256×192 margin. */
|
|
543
|
+
static uint8_t font_tile(char ch) {
|
|
544
|
+
if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE + (ch - '0'));
|
|
545
|
+
if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE + 10 + (ch - 'A'));
|
|
546
|
+
if (ch == '-') return (uint8_t)(FONT_BASE + 36);
|
|
547
|
+
return 0; /* space → blank tile */
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
static void text_draw(uint8_t vrow, uint8_t vcol, const char *s) {
|
|
551
|
+
uint8_t col = VCOL(vcol);
|
|
552
|
+
while (*s) gg_set_tilemap_cell(VROW(vrow), col++, font_tile(*s++), 0);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
static void draw_u16(uint8_t vrow, uint8_t vcol, uint16_t v) {
|
|
556
|
+
uint8_t d[5], i;
|
|
557
|
+
for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
|
|
558
|
+
for (i = 0; i < 5; i++)
|
|
559
|
+
gg_set_tilemap_cell(VROW(vrow), (uint8_t)(VCOL(vcol) + i),
|
|
560
|
+
(uint8_t)(FONT_BASE + d[4 - i]), 0);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ── GAME LOGIC (clay) — sprite HUD (digits ride the fixed top scanline) ─────
|
|
564
|
+
* HUD glyphs are SPRITES (immune to the road's R9 vertical scroll), staged
|
|
565
|
+
* into the SAT shadow each frame the HUD changes. They ride on the fixed top
|
|
566
|
+
* scanline INSIDE the window (HUD_Y = VIS_Y0 + 1 = 25). Slot map (after the
|
|
567
|
+
* cars + traffic): see stage_sprites. 1P: lives digit + 5-digit distance = 6
|
|
568
|
+
* sprites on the line; 2P: one crashes-left digit per player = 2. Mind the
|
|
569
|
+
* 8-sprites-PER-SCANLINE limit — traffic spawns BELOW the HUD line so it never
|
|
570
|
+
* shares. Sprite X stays inside [VIS_X0..VIS_X1] so the digits show. */
|
|
571
|
+
#define HUD_Y (VIS_Y0 + 1)
|
|
572
|
+
static uint8_t hud_slot; /* first SAT slot the HUD digits use */
|
|
573
|
+
static void stage_hud_sprites(void) {
|
|
574
|
+
uint8_t i, s = hud_slot;
|
|
575
|
+
if (two_player) {
|
|
576
|
+
gg_sprite_set(s++, (uint8_t)(VIS_X0 + 4), HUD_Y, (uint8_t)(T_DIGIT0 + crashes_left[0]));
|
|
577
|
+
gg_sprite_set(s++, (uint8_t)(VIS_X1 - 8), HUD_Y, (uint8_t)(T_DIGIT0 + crashes_left[1]));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
gg_sprite_set(s++, (uint8_t)(VIS_X0 + 4), HUD_Y, (uint8_t)(T_DIGIT0 + crashes_left[0]));
|
|
581
|
+
if (dist != hud_cached) { /* recompute digits only on change */
|
|
582
|
+
uint16_t v = dist;
|
|
583
|
+
for (i = 0; i < 5; i++) { hud_digits[4 - i] = (uint8_t)(v % 10); v /= 10; }
|
|
584
|
+
hud_cached = dist;
|
|
585
|
+
}
|
|
586
|
+
for (i = 0; i < 5; i++)
|
|
587
|
+
gg_sprite_set(s++, (uint8_t)(VIS_X0 + 112 + i * 8), HUD_Y, (uint8_t)(T_DIGIT0 + hud_digits[i]));
|
|
588
|
+
}
|
|
93
589
|
|
|
94
|
-
/*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
590
|
+
/* ── GAME LOGIC (clay) — paint the road into the name table ──────────────────
|
|
591
|
+
* Whole-screen repaint with the DISPLAY OFF (free VRAM access, clean cut).
|
|
592
|
+
* The dashed lane lines + shoulders are painted ONCE and never touched again:
|
|
593
|
+
* they live in the BG, so R9 moves them with the road for free.
|
|
594
|
+
*
|
|
595
|
+
* IRQ-RACE FOOTGUN (cost the GG shmup a letter of its own title): repaints
|
|
596
|
+
* also run with INTERRUPTS OFF — the di/ei bracket below. Display-off stops
|
|
597
|
+
* the FRAME IRQ but NOT the LINE IRQ (R0's IE1 stays set; the line counter
|
|
598
|
+
* runs every scanline regardless of blanking). The crt0's ISR acks by READING
|
|
599
|
+
* the control port ($BF) — and that read also resets the VDP's two-byte
|
|
600
|
+
* address-latch state machine. If the line IRQ fires between the two bytes of
|
|
601
|
+
* a gg_vdp_set_addr() control-port pair, the address de-syncs and one cell of
|
|
602
|
+
* your repaint lands somewhere else. Per-frame writes inside wait_vblank don't
|
|
603
|
+
* need the bracket: vblank has no line IRQs and the frame IRQ was already
|
|
604
|
+
* consumed by the halt that woke us.
|
|
605
|
+
*
|
|
606
|
+
* PERF FOOTGUN (inherited from the SMS example): per-cell gg_set_tilemap_cell
|
|
607
|
+
* redoes the 4-OUT address setup for every cell — over a full screen that's
|
|
608
|
+
* seconds of black. Set the VRAM address ONCE per row (the data port
|
|
609
|
+
* autoincrements) and stream. We paint all 32 columns (not just the visible
|
|
610
|
+
* 20): the off-window cells are simply never shown — the road that matters is
|
|
611
|
+
* the centered band. Cols are HARDWARE cols; the road geometry constants are
|
|
612
|
+
* window-relative, so compare against VCOL(...) below. */
|
|
613
|
+
static uint8_t road_cell(uint8_t r, uint8_t c) {
|
|
614
|
+
/* c is a HARDWARE column; map the geometry through VCOL(). */
|
|
615
|
+
if (c == VCOL(COL_EDGE_L) || c == VCOL(COL_EDGE_R) || c == VCOL(COL_DIVIDER)) return BG_EDGE;
|
|
616
|
+
if (c == VCOL(COL_DASH_1) || c == VCOL(COL_DASH_2)) return BG_DASH;
|
|
617
|
+
if (c > VCOL(COL_EDGE_L) && c < VCOL(COL_EDGE_R)) { /* tarmac */
|
|
618
|
+
return (((uint8_t)(r * 5 + c * 3) % 13) == 0) ? BG_SPECK : 0;
|
|
619
|
+
}
|
|
620
|
+
/* roadside grass + sparse tufts */
|
|
621
|
+
if (((uint8_t)(r * 7 + c * 5) & 7) == 0) return BG_TUFT;
|
|
622
|
+
return BG_GRASS;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
static void paint_road(void) {
|
|
626
|
+
uint8_t r, c;
|
|
627
|
+
for (r = 0; r < 28; r++) { /* all 28 plane rows (224 px) */
|
|
628
|
+
gg_vdp_set_addr((uint16_t)(0x3800 + (uint16_t)r * 64), VDP_VRAM_WRITE);
|
|
629
|
+
for (c = 0; c < 32; c++) {
|
|
630
|
+
PORT_VDP_DATA = road_cell(r, c); /* name-table entry low byte */
|
|
631
|
+
PORT_VDP_DATA = 0; /* high byte: flips/palette/priority */
|
|
112
632
|
}
|
|
113
633
|
}
|
|
114
634
|
}
|
|
115
635
|
|
|
116
|
-
|
|
636
|
+
static void paint_title(void) {
|
|
637
|
+
__asm__("di"); /* see IRQ-RACE FOOTGUN above */
|
|
638
|
+
gg_vdp_display_off();
|
|
639
|
+
paint_road(); /* the road itself is the backdrop */
|
|
640
|
+
text_draw(4, (uint8_t)((VIS_COLS - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
|
|
641
|
+
text_draw(8, 4, "1P RACE - 1");
|
|
642
|
+
text_draw(10, 5, "2P VS - 2");
|
|
643
|
+
text_draw(14, 7, "BEST");
|
|
644
|
+
draw_u16(14, 12, best);
|
|
645
|
+
gg_sprite_init(); /* park every sprite off-screen */
|
|
646
|
+
gg_sat_upload();
|
|
647
|
+
road_scroll = 0;
|
|
648
|
+
gg_vdp_write_reg(8, 0);
|
|
649
|
+
gg_vdp_write_reg(9, 0);
|
|
650
|
+
gg_vdp_display_on(); /* re-enables the frame IRQ too */
|
|
651
|
+
__asm__("ei"); /* interrupts back on LAST — regs are set */
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
static void paint_field(void) {
|
|
655
|
+
uint8_t c;
|
|
656
|
+
__asm__("di"); /* see IRQ-RACE FOOTGUN above */
|
|
657
|
+
gg_vdp_display_off();
|
|
658
|
+
paint_road();
|
|
659
|
+
/* HUD strip window rows 0-2 (hardware rows 3-5): clear the BG under the
|
|
660
|
+
* sprite HUD, lay the solid bar on row 2 where the split seam hides. (These
|
|
661
|
+
* rows scroll with the road via R9 — they're a curtain the streamed restamp
|
|
662
|
+
* hides behind, and the sprite digits ride above them.) Paint all 32 hw
|
|
663
|
+
* cols so the off-window ends are covered too. */
|
|
664
|
+
for (c = 0; c < 32; c++) {
|
|
665
|
+
gg_set_tilemap_cell(VROW(0), c, 0, 0);
|
|
666
|
+
gg_set_tilemap_cell(VROW(1), c, 0, 0);
|
|
667
|
+
gg_set_tilemap_cell(VROW(2), c, BG_HUDBAR, 0);
|
|
668
|
+
}
|
|
669
|
+
gg_sprite_init();
|
|
670
|
+
road_scroll = 0;
|
|
671
|
+
prev_top_row = 0;
|
|
672
|
+
hud_cached = 0xFFFF;
|
|
673
|
+
gg_vdp_write_reg(8, 0);
|
|
674
|
+
gg_vdp_write_reg(9, 0);
|
|
675
|
+
gg_vdp_display_on();
|
|
676
|
+
__asm__("ei");
|
|
677
|
+
}
|
|
117
678
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
static uint8_t prev_pad;
|
|
124
|
-
static uint8_t player_lane;
|
|
679
|
+
/* The result card reuses the live road as its backdrop (a bare single-colour
|
|
680
|
+
* card reads as a render failure — the verify tool flags >92% one-colour
|
|
681
|
+
* frames) and draws its text via deferred over_step pieces in the ST_OVER
|
|
682
|
+
* loop, one per vblank, so each vblank→split budget stays honest. The road
|
|
683
|
+
* keeps scrolling under it. */
|
|
125
684
|
|
|
126
|
-
|
|
685
|
+
/* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
|
|
686
|
+
static void spawn_traffic(void) {
|
|
687
|
+
uint8_t i;
|
|
688
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
689
|
+
if (!traffic_alive[i]) {
|
|
690
|
+
traffic_alive[i] = 1;
|
|
691
|
+
traffic_lane[i] = random8() & 3;
|
|
692
|
+
traffic_y[i] = SPAWN_Y;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
127
697
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
698
|
+
/* AABB, both boxes 8x8. */
|
|
699
|
+
static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
|
|
700
|
+
return dist8(ax, bx) < 8 && dist8(ay, by) < 8;
|
|
131
701
|
}
|
|
132
702
|
|
|
133
|
-
|
|
703
|
+
/* ── GAME LOGIC (clay) — start a run / end a run ── */
|
|
704
|
+
static void start_game(uint8_t versus) {
|
|
134
705
|
uint8_t i;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
706
|
+
two_player = versus;
|
|
707
|
+
for (i = 0; i < MAX_TRAFFIC; i++) traffic_alive[i] = 0;
|
|
708
|
+
for (i = 0; i < 2; i++) {
|
|
709
|
+
crashes_left[i] = START_LIVES;
|
|
710
|
+
invuln[i] = 0;
|
|
711
|
+
prev_pad[i] = 0xFF; /* swallow buttons held across the change */
|
|
712
|
+
}
|
|
713
|
+
if (versus) {
|
|
714
|
+
car_active[0] = 1; car_active[1] = 1;
|
|
715
|
+
lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
|
|
716
|
+
lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
|
|
717
|
+
speed = SPEED_2P; /* shared road, fixed speed (see header) */
|
|
718
|
+
} else {
|
|
719
|
+
car_active[0] = 1; car_active[1] = 0;
|
|
720
|
+
lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
|
|
721
|
+
speed = 1;
|
|
722
|
+
}
|
|
723
|
+
dist = 0; dist_frac = 0;
|
|
141
724
|
spawn_timer = 0;
|
|
142
|
-
|
|
725
|
+
sway = 0; sway_phase = 0;
|
|
726
|
+
start_pause = 30; /* green-light breather */
|
|
727
|
+
paint_field(); /* display-off repaint — safe */
|
|
728
|
+
hud_slot = (uint8_t)(2 + MAX_TRAFFIC); /* cars=0,1; traffic=2..7; HUD=8.. */
|
|
729
|
+
hud_dirty = 1;
|
|
730
|
+
sfx_tone(0, 214, 8); /* start jingle (C5) */
|
|
731
|
+
state = ST_PLAY;
|
|
143
732
|
}
|
|
144
733
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (lsb) rng_state ^= 0xB8;
|
|
154
|
-
return rng_state;
|
|
734
|
+
static void game_over(void) {
|
|
735
|
+
if (!two_player && dist > best) {
|
|
736
|
+
best = dist;
|
|
737
|
+
best_save(best); /* cart RAM (real hardware); WRAM copy live */
|
|
738
|
+
}
|
|
739
|
+
sfx_noise(20);
|
|
740
|
+
state = ST_OVER;
|
|
741
|
+
over_step = 5; /* result text, one piece per vblank */
|
|
155
742
|
}
|
|
156
743
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
744
|
+
/* ── GAME LOGIC (clay) — crash rules ── */
|
|
745
|
+
static void crash(uint8_t p) {
|
|
746
|
+
sfx_noise(14);
|
|
747
|
+
invuln[p] = 60; /* blink + no-collide grace */
|
|
748
|
+
if (!two_player) speed = 1; /* a wreck kills your momentum */
|
|
749
|
+
if (crashes_left[p] > 0) --crashes_left[p];
|
|
750
|
+
hud_dirty = 1;
|
|
751
|
+
if (crashes_left[p] == 0) {
|
|
752
|
+
winner = (uint8_t)(1 - p); /* versus: the OTHER player wins */
|
|
753
|
+
game_over();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* ── GAME LOGIC (clay) — per-player input ────────────────────────────────────
|
|
758
|
+
* LEFT/RIGHT steer between lanes (edge-detected — held d-pad shouldn't
|
|
759
|
+
* machine-gun across the road). 1P only: button 1/UP accelerate, button 2/DOWN
|
|
760
|
+
* brake (speed is shared in versus — see the design note). P2 is on PORT B. */
|
|
761
|
+
static void update_player(uint8_t p) {
|
|
762
|
+
uint8_t pad = p ? gg_joypad_read_p2() : gg_joypad_read();
|
|
763
|
+
uint8_t pressed = (uint8_t)(pad & ~prev_pad[p]);
|
|
764
|
+
prev_pad[p] = pad;
|
|
765
|
+
if (!car_active[p]) return;
|
|
766
|
+
if ((pressed & JOY_LEFT) && car_lane[p] > lane_min[p]) {
|
|
767
|
+
--car_lane[p];
|
|
768
|
+
sfx_tone(1, 330, 3); /* lane tick */
|
|
769
|
+
}
|
|
770
|
+
if ((pressed & JOY_RIGHT) && car_lane[p] < lane_max[p]) {
|
|
771
|
+
++car_lane[p];
|
|
772
|
+
sfx_tone(1, 330, 3);
|
|
773
|
+
}
|
|
774
|
+
if (!two_player) { /* speed is shared — only 1P gets it */
|
|
775
|
+
if ((pressed & (JOY_B1 | JOY_UP)) && speed < MAX_SPEED) {
|
|
776
|
+
++speed;
|
|
777
|
+
sfx_tone(2, (uint16_t)(280 - speed * 30), 8); /* engine rev */
|
|
778
|
+
hud_dirty = 1;
|
|
779
|
+
}
|
|
780
|
+
if ((pressed & (JOY_B2 | JOY_DOWN)) && speed > 1) {
|
|
781
|
+
--speed;
|
|
782
|
+
sfx_tone(2, 500, 5); /* brake blip */
|
|
783
|
+
hud_dirty = 1;
|
|
165
784
|
}
|
|
166
785
|
}
|
|
786
|
+
if (invuln[p] > 0) --invuln[p];
|
|
167
787
|
}
|
|
168
788
|
|
|
169
|
-
|
|
789
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites ──────────────────────────
|
|
790
|
+
* Fixed SAT slots: 0 = P1, 1 = P2, 2..7 = traffic, 8.. = HUD digits. Hidden
|
|
791
|
+
* slots park at Y=$E0 (off-screen, NOT the $D0 terminator — that stops the
|
|
792
|
+
* VDP scanning and blanks every later slot). */
|
|
793
|
+
static void stage_sprites(void) {
|
|
170
794
|
uint8_t i;
|
|
171
|
-
|
|
795
|
+
for (i = 0; i < 2; i++) {
|
|
796
|
+
uint8_t vis = (state == ST_PLAY) && car_active[i] && !(invuln[i] & 2);
|
|
797
|
+
gg_sprite_set(i, lane_x[car_lane[i]], vis ? CAR_Y : 0xE0, T_CAR);
|
|
798
|
+
}
|
|
799
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
800
|
+
uint8_t vis = (state == ST_PLAY) && traffic_alive[i];
|
|
801
|
+
gg_sprite_set((uint8_t)(2 + i), lane_x[traffic_lane[i]],
|
|
802
|
+
vis ? traffic_y[i] : 0xE0, T_TRAFFIC);
|
|
803
|
+
}
|
|
804
|
+
stage_hud_sprites();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
void main(void) {
|
|
808
|
+
uint8_t i, p;
|
|
809
|
+
uint8_t top_row;
|
|
810
|
+
|
|
811
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
812
|
+
* Init order: VDP regs (display off) → palette → tiles → name table → SAT →
|
|
813
|
+
* R10 → display on (which also enables the frame IRQ) → EI. The one hard
|
|
814
|
+
* rule: EI comes LAST, after every register is in place — the crt0 boots
|
|
815
|
+
* with DI and the FIRST halt would hang forever if interrupts were never
|
|
816
|
+
* enabled. (paint_title's trailing __asm__("ei") IS that final step here —
|
|
817
|
+
* every repaint ends by re-arming interrupts.) */
|
|
818
|
+
gg_vdp_init(); /* R0=0x36 already has IE1 (line IRQ) set */
|
|
172
819
|
gg_load_palette(palette);
|
|
173
|
-
|
|
174
|
-
gg_load_tiles(
|
|
175
|
-
|
|
820
|
+
load_font();
|
|
821
|
+
gg_load_tiles((uint16_t)(BG_GRASS * 32), deco_tiles, sizeof(deco_tiles));
|
|
822
|
+
gg_load_tiles(0x2000, sprite_tiles, sizeof(sprite_tiles));
|
|
176
823
|
gg_sprite_init();
|
|
177
824
|
sfx_init();
|
|
178
825
|
music_init();
|
|
179
|
-
music_play(0);
|
|
180
|
-
gg_vdp_display_on();
|
|
826
|
+
music_play(0);
|
|
181
827
|
|
|
182
|
-
|
|
183
|
-
|
|
828
|
+
/* R10 = SPLIT_LINE arms the line counter: IRQ at the last bar line —
|
|
829
|
+
* scanline 47 in FULL-FRAME terms (window top 24 + HUD 24 - 1). Set
|
|
830
|
+
* once — it reloads itself every underflow. */
|
|
831
|
+
gg_vdp_write_reg(10, SPLIT_LINE);
|
|
184
832
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
833
|
+
best = best_load(); /* cart RAM if present — else 0 */
|
|
834
|
+
state = ST_TITLE;
|
|
835
|
+
hud_slot = (uint8_t)(2 + MAX_TRAFFIC);
|
|
836
|
+
paint_title(); /* …ends with EI: interrupts live now */
|
|
837
|
+
|
|
838
|
+
for (;;) {
|
|
839
|
+
/* Advance the per-frame sway wave (the chicane curve below the split). */
|
|
840
|
+
sway_phase++;
|
|
841
|
+
sway = sway_wave[(uint8_t)((sway_phase >> 2) & 7)];
|
|
192
842
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
843
|
+
if (state == ST_TITLE) {
|
|
844
|
+
/* ── GAME LOGIC (clay) — title: button 1 = 1P race, button 2 = 2P versus.
|
|
845
|
+
* The road idles under the title card so the screen sells the scroll
|
|
846
|
+
* before anyone presses a button. */
|
|
847
|
+
wait_vblank();
|
|
848
|
+
scroll_road_down(1);
|
|
849
|
+
top_row = (uint8_t)((road_scroll >> 3) % 28);
|
|
850
|
+
if (top_row != prev_top_row) { prev_top_row = top_row; stream_road_row(top_row); }
|
|
851
|
+
sfx_update();
|
|
852
|
+
music_update();
|
|
853
|
+
wait_split();
|
|
854
|
+
{
|
|
855
|
+
uint8_t pad = gg_joypad_read();
|
|
856
|
+
if ((pad & JOY_B1) && !(prev_pad[0] & JOY_B1)) start_game(0);
|
|
857
|
+
else if ((pad & JOY_B2) && !(prev_pad[0] & JOY_B2)) start_game(1);
|
|
858
|
+
prev_pad[0] = pad;
|
|
859
|
+
}
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (state == ST_OVER) {
|
|
864
|
+
/* Freeze the road; deferred result text, one piece per vblank. Button 1
|
|
865
|
+
* or 2 returns to the title. */
|
|
866
|
+
wait_vblank();
|
|
867
|
+
if (over_step) {
|
|
868
|
+
if (over_step == 5) {
|
|
869
|
+
if (two_player) text_draw(8, 6, winner ? "P2 WINS" : "P1 WINS");
|
|
870
|
+
else text_draw(8, 6, "WRECKED");
|
|
871
|
+
} else if (over_step == 4) {
|
|
872
|
+
if (two_player) text_draw(11, 3, "RIVAL WRECKED");
|
|
873
|
+
else text_draw(11, 6, "DIST");
|
|
874
|
+
} else if (over_step == 3) {
|
|
875
|
+
if (!two_player) draw_u16(11, 11, dist);
|
|
876
|
+
} else if (over_step == 2) {
|
|
877
|
+
if (!two_player) text_draw(13, 6, "BEST");
|
|
878
|
+
} else {
|
|
879
|
+
if (!two_player) draw_u16(13, 11, best);
|
|
880
|
+
}
|
|
881
|
+
over_step--;
|
|
882
|
+
if (over_step == 0) text_draw(16, 3, "PRESS - 1 OR 2");
|
|
883
|
+
}
|
|
884
|
+
sfx_update();
|
|
885
|
+
music_update();
|
|
886
|
+
wait_split();
|
|
887
|
+
{
|
|
888
|
+
uint8_t pad = gg_joypad_read();
|
|
889
|
+
if ((pad & (JOY_B1 | JOY_B2)) && !(prev_pad[0] & (JOY_B1 | JOY_B2))) {
|
|
890
|
+
state = ST_TITLE;
|
|
891
|
+
paint_title();
|
|
892
|
+
}
|
|
893
|
+
prev_pad[0] = pad;
|
|
894
|
+
}
|
|
895
|
+
stage_sprites(); /* park cars/traffic off-screen */
|
|
896
|
+
continue;
|
|
199
897
|
}
|
|
200
|
-
gg_sat_upload();
|
|
201
898
|
|
|
202
|
-
|
|
899
|
+
/* ── ST_PLAY ──────────────────────────────────────────────────────────
|
|
900
|
+
* Frame shape: [vblank: SAT + scroll + streamed row, R8=0] → [line IRQ at
|
|
901
|
+
* the bar: R8=sway] → [rest of frame: game logic]. VRAM traffic stays
|
|
902
|
+
* inside vblank; logic runs while the VDP draws the road.
|
|
903
|
+
*
|
|
904
|
+
* BUDGET FOOTGUN (inherited from the shmup): everything between
|
|
905
|
+
* wait_vblank() and wait_split() must finish before the line IRQ at
|
|
906
|
+
* scanline 47 — vblank (70 lines) + the 47 lines above the split ≈ 27k
|
|
907
|
+
* cycles (BIGGER than the SMS's: the 24 never-shown border lines are
|
|
908
|
+
* free), and the SAT upload eats ~7k. The HUD digits are SPRITES (staged
|
|
909
|
+
* into the shadow SAT, uploaded once), and dist digits recompute only when
|
|
910
|
+
* dist changes (see stage_hud_sprites) — so we never blow the budget with
|
|
911
|
+
* division here. */
|
|
912
|
+
wait_vblank();
|
|
913
|
+
gg_sat_upload(); /* shadow SAT staged at end of last frame */
|
|
203
914
|
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
915
|
+
if (start_pause) { /* green light: freeze gameplay, keep
|
|
916
|
+
* frames honest (scroll idles, sprites
|
|
917
|
+
* staged) */
|
|
918
|
+
--start_pause;
|
|
919
|
+
scroll_road_down(0); /* re-commit R9 (no motion) */
|
|
920
|
+
sfx_update();
|
|
921
|
+
music_update();
|
|
922
|
+
wait_split();
|
|
923
|
+
stage_sprites();
|
|
208
924
|
continue;
|
|
209
925
|
}
|
|
210
926
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
prev_pad = pad;
|
|
927
|
+
scroll_road_down(speed);
|
|
928
|
+
top_row = (uint8_t)((road_scroll >> 3) % 28);
|
|
929
|
+
if (top_row != prev_top_row) { prev_top_row = top_row; stream_road_row(top_row); }
|
|
215
930
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
931
|
+
sfx_update();
|
|
932
|
+
music_update();
|
|
933
|
+
wait_split(); /* the line-interrupt split — every frame */
|
|
934
|
+
|
|
935
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
936
|
+
update_player(0);
|
|
937
|
+
if (two_player) update_player(1);
|
|
938
|
+
if (state != ST_PLAY) { stage_sprites(); continue; } /* a crash ended it */
|
|
219
939
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
940
|
+
/* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every 256
|
|
941
|
+
* units marks a checkpoint. */
|
|
942
|
+
if (!two_player) {
|
|
943
|
+
dist_frac = (uint8_t)(dist_frac + speed);
|
|
944
|
+
if (dist_frac >= 16) {
|
|
945
|
+
dist_frac -= 16;
|
|
946
|
+
if (dist < 65535u) ++dist;
|
|
947
|
+
if (dist != 0 && (dist & 0xFF) == 0) sfx_tone(0, 107, 8); /* C6 chime */
|
|
948
|
+
}
|
|
224
949
|
}
|
|
225
950
|
|
|
226
|
-
|
|
227
|
-
|
|
951
|
+
/* Traffic flows down at road speed (slower cars you overtake); despawn
|
|
952
|
+
* past the player with a little pass tick. */
|
|
953
|
+
for (i = 0; i < MAX_TRAFFIC; i++) {
|
|
954
|
+
if (!traffic_alive[i]) continue;
|
|
955
|
+
traffic_y[i] = (uint8_t)(traffic_y[i] + speed);
|
|
956
|
+
if (traffic_y[i] >= DESPAWN_Y) {
|
|
957
|
+
traffic_alive[i] = 0;
|
|
958
|
+
sfx_tone(1, 660, 2);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (++spawn_timer >= SPAWN_PERIOD) {
|
|
962
|
+
spawn_timer = 0;
|
|
963
|
+
spawn_traffic();
|
|
964
|
+
}
|
|
228
965
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
966
|
+
/* Traffic ↔ cars. Crash grace: a just-wrecked car blinks and can't collide
|
|
967
|
+
* for 60 frames. */
|
|
968
|
+
for (i = 0; i < MAX_TRAFFIC && state == ST_PLAY; i++) {
|
|
969
|
+
if (!traffic_alive[i]) continue;
|
|
970
|
+
for (p = 0; p < 2; p++) {
|
|
971
|
+
if (!car_active[p] || invuln[p]) continue;
|
|
972
|
+
if (hits(lane_x[traffic_lane[i]], traffic_y[i], lane_x[car_lane[p]], CAR_Y)) {
|
|
973
|
+
traffic_alive[i] = 0;
|
|
974
|
+
crash(p);
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
234
977
|
}
|
|
235
978
|
}
|
|
236
979
|
|
|
237
|
-
|
|
238
|
-
}
|
|
980
|
+
stage_sprites(); /* stage the SAT shadow for next vblank */
|
|
981
|
+
}
|
|
239
982
|
}
|