romdevtools 0.26.0 → 0.28.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 +5 -3
- package/CHANGELOG.md +322 -3
- package/README.md +1 -1
- package/examples/README.md +1 -1
- package/examples/atari2600/templates/platformer.asm +18 -9
- package/examples/atari2600/templates/racing.asm +25 -4
- package/examples/atari2600/templates/shmup.asm +30 -5
- package/examples/atari2600/templates/sports.asm +41 -9
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +12 -8
- package/examples/atari7800/templates/puzzle.c +7 -4
- package/examples/atari7800/templates/racing.c +5 -2
- package/examples/atari7800/templates/shmup.c +8 -4
- package/examples/atari7800/templates/sports.c +6 -3
- package/examples/c64/templates/platformer.c +28 -24
- package/examples/c64/templates/puzzle.c +77 -16
- package/examples/c64/templates/racing.c +9 -0
- package/examples/c64/templates/shmup.c +13 -1
- package/examples/c64/templates/sports.c +9 -4
- package/examples/gb/templates/platformer.c +6 -2
- package/examples/gb/templates/puzzle.c +279 -101
- package/examples/gb/templates/racing.c +13 -1
- package/examples/gb/templates/shmup.c +13 -1
- package/examples/gb/templates/sports.c +9 -3
- package/examples/gba/templates/platformer.c +7 -13
- package/examples/gba/templates/puzzle.c +93 -15
- package/examples/gba/templates/racing.c +13 -1
- package/examples/gba/templates/shmup.c +13 -1
- package/examples/gba/templates/sports.c +17 -5
- package/examples/gbc/templates/platformer.c +6 -2
- package/examples/gbc/templates/puzzle.c +878 -178
- package/examples/gbc/templates/racing.c +13 -1
- package/examples/gbc/templates/shmup.c +13 -1
- package/examples/gbc/templates/sports.c +9 -3
- package/examples/genesis/templates/puzzle.c +76 -15
- package/examples/genesis/templates/racing.c +13 -1
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/gg/templates/platformer.c +4 -0
- package/examples/gg/templates/puzzle.c +80 -14
- package/examples/gg/templates/racing.c +17 -1
- package/examples/gg/templates/shmup.c +17 -1
- package/examples/gg/templates/sports.c +4 -0
- package/examples/lynx/templates/platformer.c +25 -6
- package/examples/lynx/templates/puzzle.c +77 -14
- package/examples/lynx/templates/shmup.c +13 -1
- package/examples/lynx/templates/sports.c +5 -2
- package/examples/msx/platformer/main.c +2 -0
- package/examples/msx/puzzle/main.c +78 -15
- package/examples/msx/racing/main.c +1 -0
- package/examples/msx/shmup/main.c +1 -0
- package/examples/msx/sports/main.c +3 -2
- package/examples/nes/templates/platformer.c +11 -3
- package/examples/nes/templates/puzzle.c +81 -21
- package/examples/nes/templates/racing.c +15 -1
- package/examples/nes/templates/shmup.c +1 -0
- package/examples/nes/templates/sports.c +1 -0
- package/examples/pce/platformer/main.c +3 -1
- package/examples/pce/puzzle/main.c +78 -12
- package/examples/pce/racing/main.c +1 -0
- package/examples/pce/shmup/main.c +5 -4
- package/examples/pce/sports/main.c +4 -3
- package/examples/sms/templates/platformer.c +4 -0
- package/examples/sms/templates/puzzle.c +80 -14
- package/examples/sms/templates/racing.c +17 -1
- package/examples/sms/templates/shmup.c +17 -1
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +4 -0
- package/examples/snes/templates/platformer.c +32 -15
- package/examples/snes/templates/puzzle.c +84 -16
- package/examples/snes/templates/racing.c +20 -1
- package/examples/snes/templates/shmup.c +20 -2
- package/examples/snes/templates/sports.c +7 -0
- 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 +245 -10
- package/src/mcp/server.js +6 -0
- 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 +11 -4
- package/src/mcp/tools/index.js +15 -1
- package/src/mcp/tools/input.js +26 -3
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/project.js +35 -9
- package/src/mcp/tools/toolchain.js +43 -10
- package/src/mcp/tools/watch-memory.js +172 -25
- 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 +27 -6
- package/src/platforms/gb/MENTAL_MODEL.md +16 -1
- package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
- package/src/platforms/gb/lib/c/patch-header.js +7 -4
- package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/patch-header.js +7 -4
- package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
- 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/TROUBLESHOOTING.md +13 -17
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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 +6 -0
- 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 +2 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
- package/src/platforms/nes/MENTAL_MODEL.md +10 -3
- package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
- package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
- package/src/platforms/pce/MENTAL_MODEL.md +9 -0
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +2 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/sms/MENTAL_MODEL.md +5 -0
- 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 +5 -0
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/index.js +37 -8
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
/* ── puzzle.c — Game Boy match-3 falling-block scaffold ─────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* 8-wide × 14-tall well drawn via BG tilemap (each cell = 1 BG tile).
|
|
4
|
+
* 1×3 vertical active piece; LEFT/RIGHT shifts, A/B cycles the colour
|
|
5
|
+
* order, DOWN soft-drops, START hard-drops. Matches of 3+ in a row —
|
|
6
|
+
* horizontal, vertical, or either diagonal — clear, survivors fall
|
|
7
|
+
* (gravity), and cascades chain with rising score.
|
|
7
8
|
*
|
|
8
|
-
* On
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* On DMG we differentiate the three block kinds by SHAPE (2bpp stripe
|
|
10
|
+
* patterns), not colour. The GBC template is the full-colour version.
|
|
11
|
+
*
|
|
12
|
+
* RENDERING CONTRACT (the "pieces flash / don't render" fix): this
|
|
13
|
+
* core silently DROPS VRAM writes during active display — and can
|
|
14
|
+
* even drop one early in vblank. So (mirroring the GBC reference
|
|
15
|
+
* puzzle):
|
|
16
|
+
* - The FALLING piece is OAM sprites 0-2 (one OAM DMA per frame —
|
|
17
|
+
* no BG writes at all to move it, no erase artifacts).
|
|
18
|
+
* - The LOCKED well is BG tiles, written ONLY right after
|
|
19
|
+
* wait_vblank(): a budgeted diff (grid vs shadow) plus a rolling
|
|
20
|
+
* SCRUB that continuously repaints the well from grid[], so any
|
|
21
|
+
* dropped write self-heals within ~half a second.
|
|
22
|
+
* - enable_vblank_irq() at boot → wait_vblank HALTs to the real
|
|
23
|
+
* vblank leading edge (also ~30x faster on the WASM core than
|
|
24
|
+
* the LY-polling fallback).
|
|
12
25
|
*/
|
|
13
26
|
|
|
14
27
|
#include "gb_hardware.h"
|
|
15
28
|
#include "gb_runtime.h"
|
|
16
29
|
|
|
17
|
-
#define COLS
|
|
18
|
-
#define ROWS
|
|
30
|
+
#define COLS 8
|
|
31
|
+
#define ROWS 14
|
|
19
32
|
|
|
20
33
|
#define T_BLANK 0
|
|
21
34
|
#define T_R 1
|
|
@@ -23,6 +36,10 @@
|
|
|
23
36
|
#define T_B 3
|
|
24
37
|
#define T_WALL 4
|
|
25
38
|
|
|
39
|
+
/* Map placement: centre the 8-col well → BG col offset +6, row offset +1. */
|
|
40
|
+
#define WELL_MX 6
|
|
41
|
+
#define WELL_MY 1
|
|
42
|
+
|
|
26
43
|
/* tile_blank is the EMPTY-cell / backdrop tile. It is NOT all-zero: a
|
|
27
44
|
* subtle dither (colour 0 + faint colour 1) so the empty playfield and the
|
|
28
45
|
* area around the well read as a textured surface, never one flat colour
|
|
@@ -38,8 +55,7 @@ static const uint8_t tile_wall[16] = {
|
|
|
38
55
|
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
39
56
|
};
|
|
40
57
|
/* Three distinct tile shapes (since GB BG is 2bpp, we differentiate
|
|
41
|
-
* by *shape*, not colour-on-CGB).
|
|
42
|
-
* real colours; for DMG-compatibility we use shape. */
|
|
58
|
+
* by *shape*, not colour-on-CGB). */
|
|
43
59
|
static const uint8_t tile_r[16] = {
|
|
44
60
|
0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
|
|
45
61
|
0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
|
|
@@ -55,18 +71,30 @@ static const uint8_t tile_b[16] = {
|
|
|
55
71
|
|
|
56
72
|
static const uint16_t bg_palette[4] = { 0x7FFF, 0x5294, 0x294A, 0x0000 };
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
#define NCELL (ROWS * COLS)
|
|
75
|
+
static uint8_t grid[NCELL]; /* 0 = empty, 1..3 = block colour */
|
|
76
|
+
static uint8_t shadow[NCELL]; /* what's currently on the BG (diff redraw) */
|
|
77
|
+
static uint8_t matched[NCELL]; /* scratch: cells flagged to clear */
|
|
59
78
|
static uint8_t piece[3];
|
|
60
79
|
static int16_t piece_x, piece_y;
|
|
61
80
|
static uint8_t fall_timer;
|
|
62
81
|
static uint16_t score;
|
|
63
|
-
static
|
|
82
|
+
static uint16_t rng = 0xACE1;
|
|
83
|
+
|
|
84
|
+
#define G(r,c) grid[(uint8_t)((r) * COLS + (c))]
|
|
85
|
+
#define M(r,c) matched[(uint8_t)((r) * COLS + (c))]
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
/* the 4 line directions scanned for matches: horizontal, vertical, and
|
|
88
|
+
* both diagonals; each line is only walked from its lowest cell. */
|
|
89
|
+
static const int8_t DIRS[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
90
|
+
|
|
91
|
+
/* 16-bit xorshift — kept 16-bit on purpose (sm83 has no fast 32-bit
|
|
92
|
+
* shifts; a wider generator there degenerates toward one value). */
|
|
93
|
+
static uint8_t xorshift(void) {
|
|
94
|
+
rng ^= rng << 7;
|
|
95
|
+
rng ^= rng >> 9;
|
|
96
|
+
rng ^= rng << 8;
|
|
97
|
+
return (uint8_t)(rng >> 8);
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
static uint8_t random_colour(void) { return 1 + (xorshift() % 3); }
|
|
@@ -88,20 +116,6 @@ static uint8_t tile_for(uint8_t c) {
|
|
|
88
116
|
}
|
|
89
117
|
}
|
|
90
118
|
|
|
91
|
-
static void draw_cell(int16_t col, int16_t row, uint8_t cell) {
|
|
92
|
-
/* Map base $9800, 32 cells wide. Centre the 6-col grid → offset +7. */
|
|
93
|
-
uint8_t *map = (uint8_t *)0x9800;
|
|
94
|
-
if (row < 0 || row >= ROWS) return;
|
|
95
|
-
map[(row + 1) * 32 + (col + 7)] = tile_for(cell);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
static void draw_grid(void) {
|
|
99
|
-
int16_t r, c;
|
|
100
|
-
for (r = 0; r < ROWS; r++)
|
|
101
|
-
for (c = 0; c < COLS; c++)
|
|
102
|
-
draw_cell(c, r, grid[r][c]);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
119
|
static uint8_t collides(int16_t col, int16_t row) {
|
|
106
120
|
uint8_t i;
|
|
107
121
|
int16_t r;
|
|
@@ -109,36 +123,128 @@ static uint8_t collides(int16_t col, int16_t row) {
|
|
|
109
123
|
for (i = 0; i < 3; i++) {
|
|
110
124
|
r = row + i;
|
|
111
125
|
if (r >= ROWS) return 1;
|
|
112
|
-
if (r >= 0 &&
|
|
126
|
+
if (r >= 0 && G(r, col) != 0) return 1;
|
|
113
127
|
}
|
|
114
128
|
return 0;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
|
|
131
|
+
/* ── match / clear / gravity core (mirrors the GBC reference) ─────── */
|
|
132
|
+
|
|
133
|
+
/* Flag every cell in a 3+ run (any of the 4 directions) into matched[];
|
|
134
|
+
* return the count. A run is walked once, from its lowest end only. */
|
|
135
|
+
static uint8_t mark_and_count(void) {
|
|
136
|
+
uint8_t r, c, d, len, cnt, col, k;
|
|
137
|
+
int8_t dr, dc;
|
|
138
|
+
int16_t sr, sc;
|
|
139
|
+
|
|
140
|
+
for (r = 0; r < NCELL; r++) matched[r] = 0;
|
|
141
|
+
|
|
142
|
+
for (r = 0; r < ROWS; r++) {
|
|
143
|
+
for (c = 0; c < COLS; c++) {
|
|
144
|
+
col = G(r, c);
|
|
145
|
+
if (col == 0) continue;
|
|
146
|
+
for (d = 0; d < 4; d++) {
|
|
147
|
+
dr = DIRS[d][0];
|
|
148
|
+
dc = DIRS[d][1];
|
|
149
|
+
sr = (int16_t)r - dr;
|
|
150
|
+
sc = (int16_t)c - dc;
|
|
151
|
+
if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
|
|
152
|
+
&& G(sr, sc) == col) continue; /* not the run's start */
|
|
153
|
+
len = 1;
|
|
154
|
+
sr = (int16_t)r + dr;
|
|
155
|
+
sc = (int16_t)c + dc;
|
|
156
|
+
while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
|
|
157
|
+
&& G(sr, sc) == col) {
|
|
158
|
+
len++;
|
|
159
|
+
sr += dr;
|
|
160
|
+
sc += dc;
|
|
161
|
+
}
|
|
162
|
+
if (len >= 3) {
|
|
163
|
+
sr = (int16_t)r;
|
|
164
|
+
sc = (int16_t)c;
|
|
165
|
+
for (k = 0; k < len; k++) {
|
|
166
|
+
M(sr, sc) = 1;
|
|
167
|
+
sr += dr;
|
|
168
|
+
sc += dc;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
cnt = 0;
|
|
176
|
+
for (r = 0; r < NCELL; r++) if (matched[r]) cnt++;
|
|
177
|
+
return cnt;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static void clear_marked(void) {
|
|
118
181
|
uint8_t i;
|
|
182
|
+
for (i = 0; i < NCELL; i++) if (matched[i]) grid[i] = 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* collapse each column so survivors rest on the floor — the "rows move
|
|
186
|
+
* down after a clear" the old template was missing. */
|
|
187
|
+
static void apply_gravity(void) {
|
|
188
|
+
uint8_t c, r, n, w;
|
|
189
|
+
uint8_t buf[ROWS];
|
|
190
|
+
for (c = 0; c < COLS; c++) {
|
|
191
|
+
n = 0;
|
|
192
|
+
for (r = 0; r < ROWS; r++)
|
|
193
|
+
if (G(r, c)) { buf[n] = G(r, c); n++; }
|
|
194
|
+
for (r = 0; r < (uint8_t)(ROWS - n); r++) G(r, c) = 0;
|
|
195
|
+
w = 0;
|
|
196
|
+
for (r = (uint8_t)(ROWS - n); r < ROWS; r++) { G(r, c) = buf[w]; w++; }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* clear chime — one short square blip per cascade step, pitch rises with
|
|
201
|
+
* the chain so combos audibly escalate. */
|
|
202
|
+
static void sfx_clear(uint8_t chain) {
|
|
203
|
+
uint16_t p = 1797 + (uint16_t)chain * 26; /* ~C5 rising */
|
|
204
|
+
if (p > 1980) p = 1980;
|
|
205
|
+
sound_play_tone(1, p, 6);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* settle the board after a lock: match → clear → gravity, looping so
|
|
209
|
+
* cascades chain; score scales with the chain depth. */
|
|
210
|
+
static void resolve_board(void) {
|
|
211
|
+
uint8_t n, chain = 0;
|
|
212
|
+
uint16_t amt;
|
|
213
|
+
while (1) {
|
|
214
|
+
n = mark_and_count();
|
|
215
|
+
if (n == 0) break;
|
|
216
|
+
chain++;
|
|
217
|
+
sfx_clear(chain);
|
|
218
|
+
clear_marked();
|
|
219
|
+
amt = (uint16_t)n * 10;
|
|
220
|
+
if (chain > 1) amt = amt * chain;
|
|
221
|
+
if (score < (uint16_t)(65500u - amt)) score += amt;
|
|
222
|
+
apply_gravity();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
static void lock_piece(void) {
|
|
227
|
+
uint8_t i, written = 0;
|
|
119
228
|
int16_t r;
|
|
120
|
-
int16_t c;
|
|
121
|
-
uint8_t a, b, d;
|
|
122
229
|
for (i = 0; i < 3; i++) {
|
|
123
230
|
r = piece_y + i;
|
|
124
|
-
if (r >= 0 && r < ROWS)
|
|
231
|
+
if (r >= 0 && r < ROWS) { G(r, piece_x) = piece[i]; written++; }
|
|
125
232
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
}
|
|
233
|
+
sound_play_noise(3);
|
|
234
|
+
resolve_board();
|
|
235
|
+
if (written == 0) {
|
|
236
|
+
/* The piece locked entirely ABOVE the well — the stack reached the
|
|
237
|
+
* top. Without this the game silently softlocks (invisible pieces
|
|
238
|
+
* locking off-screen forever). Scaffold behavior: low game-over
|
|
239
|
+
* tone, clear the board, restart the run. */
|
|
240
|
+
sound_play_tone(1, 1548, 30);
|
|
241
|
+
for (i = 0; i < NCELL; i++) grid[i] = 0;
|
|
242
|
+
score = 0;
|
|
138
243
|
}
|
|
139
|
-
draw_grid();
|
|
140
244
|
}
|
|
141
245
|
|
|
246
|
+
/* ── rendering (vblank-budgeted; gameplay code never touches VRAM) ── */
|
|
247
|
+
|
|
142
248
|
static void upload_tile(uint8_t slot, const uint8_t *src) {
|
|
143
249
|
uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
|
|
144
250
|
/* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
|
|
@@ -146,29 +252,106 @@ static void upload_tile(uint8_t slot, const uint8_t *src) {
|
|
|
146
252
|
memcpy_vram(dst, src, 16);
|
|
147
253
|
}
|
|
148
254
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
static void
|
|
153
|
-
|
|
255
|
+
#define VRAM_MAP ((volatile uint8_t *)0x9800)
|
|
256
|
+
|
|
257
|
+
/* Direct cell write — ONLY safe with the LCD off or just after vblank. */
|
|
258
|
+
static void set_cell(uint8_t c, uint8_t r, uint8_t tile) {
|
|
259
|
+
VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX + c] = tile;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* COLLECT/FLUSH split (the reference puzzle's architecture, and the part
|
|
263
|
+
* that actually fixes "pieces flash / don't render"):
|
|
264
|
+
* - collect_well() runs OUTSIDE vblank: scans for grid-vs-shadow diffs
|
|
265
|
+
* (bounded), or queues rolling SCRUB cells when nothing changed, into a
|
|
266
|
+
* tiny queue of precomputed (map offset, tile) pairs. RAM only.
|
|
267
|
+
* - flush_well() runs FIRST thing after wait_vblank: pure pointer writes,
|
|
268
|
+
* no scanning, no multiplies — the whole batch lands inside the ~10-line
|
|
269
|
+
* vblank window every frame. The scrub means even a write the core drops
|
|
270
|
+
* anyway heals itself on the next pass instead of sticking forever. */
|
|
271
|
+
#define WQ_MAX 4
|
|
272
|
+
static uint8_t wq_n;
|
|
273
|
+
static uint16_t wq_off[WQ_MAX];
|
|
274
|
+
static uint8_t wq_tile[WQ_MAX];
|
|
275
|
+
static uint8_t diff_cursor, scrub_cursor;
|
|
276
|
+
|
|
277
|
+
static uint16_t cell_off(uint8_t i) {
|
|
278
|
+
return (uint16_t)(WELL_MY + i / COLS) * 32 + WELL_MX + (i % COLS);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static void collect_well(void) {
|
|
282
|
+
uint8_t scanned = 0, i, k;
|
|
283
|
+
wq_n = 0;
|
|
284
|
+
i = diff_cursor;
|
|
285
|
+
while (scanned < NCELL && wq_n < WQ_MAX) {
|
|
286
|
+
if (grid[i] != shadow[i]) {
|
|
287
|
+
shadow[i] = grid[i];
|
|
288
|
+
wq_off[wq_n] = cell_off(i);
|
|
289
|
+
wq_tile[wq_n] = tile_for(grid[i]);
|
|
290
|
+
wq_n++;
|
|
291
|
+
}
|
|
292
|
+
i++;
|
|
293
|
+
if (i >= NCELL) i = 0;
|
|
294
|
+
scanned++;
|
|
295
|
+
}
|
|
296
|
+
diff_cursor = i;
|
|
297
|
+
if (wq_n == 0) {
|
|
298
|
+
/* idle: queue scrub cells so dropped writes self-heal */
|
|
299
|
+
for (k = 0; k < 2; k++) {
|
|
300
|
+
wq_off[wq_n] = cell_off(scrub_cursor);
|
|
301
|
+
wq_tile[wq_n] = tile_for(grid[scrub_cursor]);
|
|
302
|
+
wq_n++;
|
|
303
|
+
scrub_cursor++;
|
|
304
|
+
if (scrub_cursor >= NCELL) scrub_cursor = 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static void flush_well(void) {
|
|
310
|
+
uint8_t k;
|
|
311
|
+
for (k = 0; k < wq_n; k++) VRAM_MAP[wq_off[k]] = wq_tile[k];
|
|
312
|
+
wq_n = 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* The falling piece = OAM sprites 0-2 (written to shadow_oam, flushed by
|
|
316
|
+
* one OAM DMA right after vblank starts). Rows above the well top (r < 0)
|
|
317
|
+
* park the sprite at Y=0 (offscreen). */
|
|
318
|
+
static void update_piece_sprites(void) {
|
|
319
|
+
uint8_t i, sy, sx;
|
|
320
|
+
int16_t r;
|
|
321
|
+
for (i = 0; i < 3; i++) {
|
|
322
|
+
r = piece_y + i;
|
|
323
|
+
if (r >= 0 && r < ROWS) {
|
|
324
|
+
sy = (uint8_t)((WELL_MY + r) * 8 + 16);
|
|
325
|
+
sx = (uint8_t)((WELL_MX + piece_x) * 8 + 8);
|
|
326
|
+
oam_set(i, sy, sx, tile_for(piece[i]), 0);
|
|
327
|
+
} else {
|
|
328
|
+
oam_set(i, 0, 0, 0, 0);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static void draw_well_frame(void) {
|
|
154
334
|
uint8_t r;
|
|
155
|
-
for (r =
|
|
156
|
-
|
|
157
|
-
|
|
335
|
+
for (r = 0; r < ROWS; r++) {
|
|
336
|
+
VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX - 1] = T_WALL;
|
|
337
|
+
VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX + COLS] = T_WALL;
|
|
158
338
|
}
|
|
159
|
-
for (r =
|
|
160
|
-
|
|
339
|
+
for (r = 0; r < (uint8_t)(COLS + 2); r++)
|
|
340
|
+
VRAM_MAP[(uint16_t)(WELL_MY + ROWS) * 32 + WELL_MX - 1 + r] = T_WALL;
|
|
161
341
|
}
|
|
162
342
|
|
|
163
343
|
void main(void) {
|
|
164
|
-
uint8_t pad, prev = 0, fall_rate, t;
|
|
165
|
-
int16_t
|
|
166
|
-
uint8_t i;
|
|
344
|
+
uint8_t pad, prev = 0, fall_rate, t, i;
|
|
345
|
+
int16_t c;
|
|
167
346
|
uint8_t *map;
|
|
168
|
-
int16_t pr;
|
|
169
347
|
|
|
170
348
|
lcd_init_default();
|
|
349
|
+
enable_vblank_irq();
|
|
350
|
+
sound_init();
|
|
351
|
+
oam_dma_init_hram();
|
|
352
|
+
oam_clear();
|
|
171
353
|
LCDC = 0;
|
|
354
|
+
OBP0 = 0xE4; /* DMG sprite palette: 3=black .. 0=white */
|
|
172
355
|
|
|
173
356
|
upload_tile(T_BLANK, tile_blank);
|
|
174
357
|
upload_tile(T_R, tile_r);
|
|
@@ -185,64 +368,59 @@ void main(void) {
|
|
|
185
368
|
map = (uint8_t *)0x9800;
|
|
186
369
|
for (i = 0; i < 32; i++) {
|
|
187
370
|
c = 0;
|
|
188
|
-
while (c < 32) { map[i * 32 + c] = T_BLANK; c++; }
|
|
371
|
+
while (c < 32) { map[(uint16_t)i * 32 + c] = T_BLANK; c++; }
|
|
189
372
|
}
|
|
190
373
|
|
|
191
|
-
for (
|
|
192
|
-
for (c = 0; c < COLS; c++)
|
|
193
|
-
grid[r][c] = 0;
|
|
374
|
+
for (i = 0; i < NCELL; i++) { grid[i] = 0; shadow[i] = 0; }
|
|
194
375
|
|
|
195
376
|
score = 0;
|
|
196
377
|
fall_timer = 0;
|
|
378
|
+
rng ^= DIV; /* a dash of boot-time entropy */
|
|
197
379
|
new_piece();
|
|
198
|
-
|
|
199
|
-
draw_grid();
|
|
380
|
+
draw_well_frame();
|
|
200
381
|
|
|
201
|
-
LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_TILE_DATA_LO;
|
|
382
|
+
LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO;
|
|
202
383
|
|
|
203
384
|
while (1) {
|
|
204
|
-
wait_vblank();
|
|
205
|
-
|
|
206
|
-
/* Erase current piece visual (overwrite with what's underneath). */
|
|
207
|
-
for (i = 0; i < 3; i++) {
|
|
208
|
-
pr = piece_y + i;
|
|
209
|
-
if (pr >= 0 && pr < ROWS)
|
|
210
|
-
draw_cell(piece_x, pr, grid[pr][piece_x]);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
385
|
pad = joypad_read();
|
|
214
386
|
|
|
215
387
|
if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
|
|
216
|
-
&& !collides(piece_x - 1, piece_y)) piece_x--;
|
|
388
|
+
&& !collides(piece_x - 1, piece_y)) { piece_x--; sound_play_tone(1, 1899, 2); }
|
|
217
389
|
if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
|
|
218
|
-
&& !collides(piece_x + 1, piece_y)) piece_x++;
|
|
390
|
+
&& !collides(piece_x + 1, piece_y)) { piece_x++; sound_play_tone(1, 1899, 2); }
|
|
219
391
|
if ((pad & PAD_A) && !(prev & PAD_A)) {
|
|
220
392
|
t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
|
|
393
|
+
sound_play_tone(1, 1923, 2);
|
|
394
|
+
}
|
|
395
|
+
if ((pad & PAD_B) && !(prev & PAD_B)) {
|
|
396
|
+
t = piece[2]; piece[2] = piece[1]; piece[1] = piece[0]; piece[0] = t;
|
|
397
|
+
sound_play_tone(1, 1923, 2);
|
|
221
398
|
}
|
|
222
399
|
if ((pad & PAD_START) && !(prev & PAD_START)) {
|
|
223
400
|
while (!collides(piece_x, piece_y + 1)) piece_y++;
|
|
224
401
|
lock_piece();
|
|
225
402
|
new_piece();
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
new_piece();
|
|
237
|
-
} else {
|
|
238
|
-
piece_y++;
|
|
403
|
+
} else {
|
|
404
|
+
fall_rate = (pad & PAD_DOWN) ? 4 : 30;
|
|
405
|
+
if (++fall_timer >= fall_rate) {
|
|
406
|
+
fall_timer = 0;
|
|
407
|
+
if (collides(piece_x, piece_y + 1)) {
|
|
408
|
+
lock_piece();
|
|
409
|
+
new_piece();
|
|
410
|
+
} else {
|
|
411
|
+
piece_y++;
|
|
412
|
+
}
|
|
239
413
|
}
|
|
240
414
|
}
|
|
415
|
+
prev = pad;
|
|
241
416
|
|
|
242
|
-
/*
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
417
|
+
/* COLLECT (RAM only, runs in active display) … */
|
|
418
|
+
update_piece_sprites();
|
|
419
|
+
collect_well();
|
|
420
|
+
/* … then FLUSH right after vblank starts: OAM DMA first (sprites
|
|
421
|
+
* tear if it slips out of vblank), then the queued BG writes. */
|
|
422
|
+
wait_vblank();
|
|
423
|
+
oam_dma_flush();
|
|
424
|
+
flush_well();
|
|
247
425
|
}
|
|
248
426
|
}
|
|
@@ -95,11 +95,23 @@ static void reset_run(void) {
|
|
|
95
95
|
game_over_timer = 0;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
|
|
99
|
+
* The old code derived the spawn column from spawn_timer, but the caller
|
|
100
|
+
* resets spawn_timer just before calling here, so it was CONSTANT and
|
|
101
|
+
* every enemy spawned in the same left column/lane. */
|
|
102
|
+
static uint8_t rng_state = 0xA5;
|
|
103
|
+
static uint8_t rand8(void) {
|
|
104
|
+
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
105
|
+
rng_state >>= 1;
|
|
106
|
+
if (lsb) rng_state ^= 0xB8;
|
|
107
|
+
return rng_state;
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
static void spawn_obstacle(void) {
|
|
99
111
|
uint8_t i;
|
|
100
112
|
for (i = 0; i < MAX_OBSTACLES; i++) {
|
|
101
113
|
if (!obstacles[i].alive) {
|
|
102
|
-
obstacles[i].x = lane_x[(
|
|
114
|
+
obstacles[i].x = lane_x[rand8() % 3];
|
|
103
115
|
obstacles[i].y = 0;
|
|
104
116
|
obstacles[i].alive = 1;
|
|
105
117
|
return;
|
|
@@ -86,11 +86,23 @@ static void fire(void) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
|
|
90
|
+
* The old code derived the spawn column from spawn_timer, but the caller
|
|
91
|
+
* resets spawn_timer just before calling here, so it was CONSTANT and
|
|
92
|
+
* every enemy spawned in the same left column/lane. */
|
|
93
|
+
static uint8_t rng_state = 0xA5;
|
|
94
|
+
static uint8_t rand8(void) {
|
|
95
|
+
uint8_t lsb = (uint8_t)(rng_state & 1);
|
|
96
|
+
rng_state >>= 1;
|
|
97
|
+
if (lsb) rng_state ^= 0xB8;
|
|
98
|
+
return rng_state;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
static void spawn(void) {
|
|
90
102
|
uint8_t i;
|
|
91
103
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
92
104
|
if (!enemies[i].alive) {
|
|
93
|
-
enemies[i].x = (
|
|
105
|
+
enemies[i].x = rand8() % (160 - 16) + 8;
|
|
94
106
|
enemies[i].y = 0;
|
|
95
107
|
enemies[i].alive = 1;
|
|
96
108
|
return;
|
|
@@ -130,15 +130,21 @@ void main(void) {
|
|
|
130
130
|
|
|
131
131
|
while (1) {
|
|
132
132
|
wait_vblank();
|
|
133
|
+
/* OAM DMA FIRST — at the leading edge of vblank. The old order staged
|
|
134
|
+
* 45 oam_set CALLS before the DMA; the SDCC call overhead pushed the
|
|
135
|
+
* DMA ~a third of the frame into ACTIVE display, so the sprites tore
|
|
136
|
+
* on one fixed scanline ("horizontal line a 3rd of the way down").
|
|
137
|
+
* Sprites now display the state staged LAST frame (1 frame of latency,
|
|
138
|
+
* imperceptible in Pong). */
|
|
139
|
+
oam_dma_flush();
|
|
133
140
|
|
|
134
|
-
/* Stage
|
|
135
|
-
|
|
141
|
+
/* Stage next frame's OAM (RAM only — safe any time). Slots 5-39 were
|
|
142
|
+
* zeroed once by oam_clear() at boot and never change. */
|
|
136
143
|
oam_set(0, (uint8_t)(p1y + 16), (uint8_t)(PADDLE_X1 + 8), 1, 0);
|
|
137
144
|
oam_set(1, (uint8_t)(p1y + 16 + 8), (uint8_t)(PADDLE_X1 + 8), 1, 0);
|
|
138
145
|
oam_set(2, (uint8_t)(p2y + 16), (uint8_t)(PADDLE_X2 + 8), 1, 0);
|
|
139
146
|
oam_set(3, (uint8_t)(p2y + 16 + 8), (uint8_t)(PADDLE_X2 + 8), 1, 0);
|
|
140
147
|
oam_set(4, (uint8_t)(by + 16), (uint8_t)(bx + 8), 1, 0);
|
|
141
|
-
oam_dma_flush();
|
|
142
148
|
|
|
143
149
|
pad = joypad_read();
|
|
144
150
|
if (pad & PAD_UP && p1y > COURT_TOP) p1y -= 2;
|
|
@@ -65,18 +65,6 @@ static int on_platform(s16 px, s16 py) {
|
|
|
65
65
|
return 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
static int blocked_below(s16 px, s16 py) {
|
|
69
|
-
for (int i = 0; i < N_PLATFORMS; i++) {
|
|
70
|
-
const Rect *p = &platforms[i];
|
|
71
|
-
if (py + 8 <= p->y && py + 9 > p->y
|
|
72
|
-
&& px + 8 > p->x
|
|
73
|
-
&& px < p->x + p->w) {
|
|
74
|
-
return 1;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
68
|
int main(void) {
|
|
81
69
|
/* ── BG palette (for the platform tile) ──────────────────────────
|
|
82
70
|
* pal_bg_mem[i] is the BG palette. */
|
|
@@ -182,7 +170,13 @@ int main(void) {
|
|
|
182
170
|
/* Vertical with platform-stop. */
|
|
183
171
|
s32 np = py + vy;
|
|
184
172
|
s16 npy = np >> 4;
|
|
185
|
-
|
|
173
|
+
/* THE fall-through-the-floor fix: this used to be additionally
|
|
174
|
+
* gated on blocked_below(), which only matches when a platform
|
|
175
|
+
* top is within ONE pixel of the feet — but falls reach 20 px/
|
|
176
|
+
* frame, so the (correct) crossing test below almost never got
|
|
177
|
+
* to run and the player tunnelled through every platform. The
|
|
178
|
+
* crossing test alone is the right check. */
|
|
179
|
+
if (vy > 0) {
|
|
186
180
|
for (int i = 0; i < N_PLATFORMS; i++) {
|
|
187
181
|
const Rect *p = &platforms[i];
|
|
188
182
|
if (ipy + 8 <= p->y && npy + 8 >= p->y
|