romdevtools 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -41
- package/CHANGELOG.md +46 -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 +1 -1
- package/src/host/LibretroHost.js +59 -1
- 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/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/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +4 -3
- 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/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,201 +1,452 @@
|
|
|
1
|
-
/* ── sports.c — SNES
|
|
1
|
+
/* ── sports.c — SNES head-to-head court game (complete example game) ─────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* A COMPLETE, working game — NET SURGE, a head-to-head court duel (Pong
|
|
4
|
+
* lineage): title screen, 1P vs a beatable CPU and 2P simultaneous versus
|
|
5
|
+
* (controller 2), first-to-5 match flow with a result screen, SPC music +
|
|
6
|
+
* SFX, a PRNG that keeps rallies from looping forever, and a battery-SRAM
|
|
7
|
+
* record (longest win streak vs the CPU) that survives power cycles.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
10
|
+
* very different one. The markers tell you what's what:
|
|
11
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented SNES footgun; reshape
|
|
12
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
13
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
14
|
+
* reshape freely.
|
|
15
|
+
*
|
|
16
|
+
* What depends on what:
|
|
17
|
+
* data.asm — font + sprite/wallpaper tiles, and sram_read16/sram_write16
|
|
18
|
+
* (battery SRAM lives at $70:0000, reachable only with long addressing —
|
|
19
|
+
* that's why they're asm). Load-bearing.
|
|
20
|
+
* hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
|
|
21
|
+
* SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
|
|
22
|
+
* silently stop existing — the build still succeeds.
|
|
23
|
+
* snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
|
|
24
|
+
* driver (music + 2 one-shot samples). #include'd, not separately built.
|
|
9
25
|
*
|
|
10
26
|
* tcc-65816 is C89 — all declarations at block top, no inline `for (u16 i …)`.
|
|
27
|
+
*
|
|
28
|
+
* Frame budget: 7 sprites, 2 collision tests, a few consoleDrawText calls —
|
|
29
|
+
* a tiny fraction of a frame. Plenty of headroom for fancier ball physics.
|
|
11
30
|
*/
|
|
12
31
|
|
|
13
32
|
#include <snes.h>
|
|
14
33
|
#include "snes_sfx.c"
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
36
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
37
|
+
#define GAME_TITLE "NET SURGE"
|
|
38
|
+
|
|
39
|
+
extern char tilfont, palfont; /* console font + text palette (data.asm) */
|
|
40
|
+
extern char tilsprite, palsprite; /* solid 8x8 block tile + OBJ palette */
|
|
41
|
+
extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
|
|
19
42
|
|
|
20
43
|
/* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
|
|
21
44
|
* No public prototype in console.h, so declare it; call once per frame. */
|
|
22
45
|
extern void consoleVblank(void);
|
|
23
46
|
|
|
47
|
+
/* data.asm exports — battery SRAM accessors ($70:0000, long addressing). */
|
|
48
|
+
extern u16 sram_read16(u16 offset);
|
|
49
|
+
extern void sram_write16(u16 offset, u16 value);
|
|
50
|
+
|
|
24
51
|
/* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
|
|
25
52
|
* court reads as a real backdrop, not flat blank. Filled at runtime. */
|
|
26
53
|
static u16 bg_map[32 * 32];
|
|
27
54
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
55
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
56
|
+
* oamSet's FIRST arg is a BYTE OFFSET into OAM, not a slot number: slot N
|
|
57
|
+
* lives at byte offset N*4. Passing the raw slot writes every sprite into
|
|
58
|
+
* OAM bytes 0-9, corrupting each other → black/garbled screen. */
|
|
59
|
+
#define SPR(slot) ((slot) << 2)
|
|
60
|
+
|
|
61
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
62
|
+
* Court geometry + match rules. The court is framed by '-' rails drawn on
|
|
63
|
+
* the text BG at rows 4 and 23 (pixels 32-39 and 184-191); COURT_TOP/BOT
|
|
64
|
+
* keep the ball between them. The text grid is 32x28 cells (8px each). */
|
|
65
|
+
#define COURT_ROW_TOP 4
|
|
66
|
+
#define COURT_ROW_BOT 23
|
|
67
|
+
#define COURT_NET_COL 16
|
|
68
|
+
#define COURT_TOP 40 /* first pixel row below the top rail */
|
|
69
|
+
#define COURT_BOT 184 /* first pixel row of the bottom rail */
|
|
70
|
+
#define PADDLE_H 24 /* 3 stacked 8x8 sprites */
|
|
31
71
|
#define BALL_SIZE 8
|
|
32
|
-
#define PADDLE_X1 16
|
|
33
|
-
#define PADDLE_X2 232
|
|
72
|
+
#define PADDLE_X1 16 /* P1 — left side */
|
|
73
|
+
#define PADDLE_X2 232 /* P2/CPU — right side */
|
|
74
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
34
75
|
|
|
35
|
-
/*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
* this right; sports/racing did not — SNES-1.) */
|
|
39
|
-
#define SPR(slot) ((slot) << 2)
|
|
76
|
+
/* SRAM layout: [0]=magic "NS", [2]=best streak, [4]=best ^ 0xA5C3.
|
|
77
|
+
* Magic is written LAST in streak_save so a torn write never validates. */
|
|
78
|
+
#define SRAM_MAGIC 0x534Eu
|
|
40
79
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
/* Game states — the shell every example shares: title → play → result. */
|
|
81
|
+
#define ST_TITLE 0
|
|
82
|
+
#define ST_PLAY 1
|
|
83
|
+
#define ST_OVER 2
|
|
45
84
|
|
|
46
|
-
static
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
static u8 state;
|
|
86
|
+
static s16 p1y, p2y; /* paddle top Y */
|
|
87
|
+
static s16 bx, by; /* ball position */
|
|
88
|
+
static s8 bdx, bdy; /* ball velocity (px/frame) */
|
|
89
|
+
static u8 score_p1, score_p2;
|
|
90
|
+
static u8 serve_timer; /* freeze frames between points */
|
|
91
|
+
static u8 two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
|
|
92
|
+
static u8 streak; /* current 1P-vs-CPU win streak (RAM) */
|
|
93
|
+
static u16 best_streak; /* battery-backed record — see end_match */
|
|
94
|
+
static u8 new_record; /* result screen shows NEW RECORD */
|
|
95
|
+
static u8 sound_ok;
|
|
96
|
+
static u16 prev_pad0;
|
|
97
|
+
static char nbuf[8]; /* fmt_u16 output */
|
|
98
|
+
|
|
99
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call).
|
|
100
|
+
* A versus game NEEDS this: the SNES is fully deterministic, so without a
|
|
101
|
+
* noise source two fixed strategies lock into an infinite rally loop (the
|
|
102
|
+
* exact same few-hundred-frame cycle, forever — an idle 1P match would
|
|
103
|
+
* never end). random8() is ticked once per play frame so identical game
|
|
104
|
+
* states a few seconds apart still diverge. */
|
|
105
|
+
static u16 rng = 0xC0A7;
|
|
106
|
+
static u8 random8(void) {
|
|
107
|
+
u16 r = rng;
|
|
108
|
+
r ^= r << 7;
|
|
109
|
+
r ^= r >> 9;
|
|
110
|
+
r ^= r << 8;
|
|
111
|
+
rng = r;
|
|
112
|
+
return (u8)r;
|
|
52
113
|
}
|
|
53
114
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
115
|
+
/* ── GAME LOGIC (clay) — battery-SRAM record (see sram_* in data.asm) ─────── */
|
|
116
|
+
static u16 streak_load(void) {
|
|
117
|
+
u16 v;
|
|
118
|
+
if (sram_read16(0) != SRAM_MAGIC) return 0;
|
|
119
|
+
v = sram_read16(2);
|
|
120
|
+
if (sram_read16(4) != (u16)(v ^ 0xA5C3u)) return 0;
|
|
121
|
+
return v;
|
|
60
122
|
}
|
|
61
123
|
|
|
62
|
-
static void
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
consoleDrawText(6, 2, buf);
|
|
67
|
-
buf[0] = '0' + (score_p2 % 10);
|
|
68
|
-
consoleDrawText(24, 2, buf);
|
|
124
|
+
static void streak_save(u16 v) {
|
|
125
|
+
sram_write16(2, v);
|
|
126
|
+
sram_write16(4, (u16)(v ^ 0xA5C3u));
|
|
127
|
+
sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
|
|
69
128
|
}
|
|
70
129
|
|
|
71
|
-
/*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
130
|
+
/* ── GAME LOGIC (clay) — text helpers ──────────────────────────────────────── */
|
|
131
|
+
static void fmt_u16(u16 v) { /* decimal, no leading zeros, into nbuf */
|
|
132
|
+
char tmp[6];
|
|
133
|
+
u8 n = 0, i;
|
|
134
|
+
do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v);
|
|
135
|
+
for (i = 0; i < n; i++) nbuf[i] = tmp[n - 1 - i];
|
|
136
|
+
nbuf[n] = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static void clear_row(u16 y) {
|
|
140
|
+
consoleDrawText(0, y, " ");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static void clear_rows(u16 a, u16 b) {
|
|
144
|
+
u16 y;
|
|
145
|
+
for (y = a; y <= b; y++) clear_row(y);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side ────── */
|
|
149
|
+
static void serve_ball(u8 to_left) {
|
|
150
|
+
bx = 124;
|
|
151
|
+
by = 108;
|
|
152
|
+
bdx = to_left ? -2 : 2;
|
|
153
|
+
bdy = ((score_p1 + score_p2) & 1) ? -1 : 1; /* alternate the angle */
|
|
154
|
+
serve_timer = 30; /* half-second breather */
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── GAME LOGIC (clay) — HUD: labels row 1, scores redrawn after points ───── */
|
|
158
|
+
static void draw_scores(void) {
|
|
159
|
+
nbuf[0] = (char)('0' + score_p1); nbuf[1] = 0;
|
|
160
|
+
consoleDrawText(6, 1, nbuf);
|
|
161
|
+
nbuf[0] = (char)('0' + score_p2);
|
|
162
|
+
consoleDrawText(28, 1, nbuf);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Draw the court out of font glyphs on the text BG (no extra tile data
|
|
166
|
+
* needed): a dashed rail across the top and bottom of the playfield plus
|
|
167
|
+
* a dashed centre net. */
|
|
77
168
|
static void draw_court(void) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
169
|
+
char rail[29];
|
|
170
|
+
u16 i;
|
|
171
|
+
for (i = 0; i < 28; i++) rail[i] = '-';
|
|
172
|
+
rail[28] = 0;
|
|
173
|
+
consoleDrawText(2, COURT_ROW_TOP, rail);
|
|
174
|
+
consoleDrawText(2, COURT_ROW_BOT, rail);
|
|
175
|
+
for (i = COURT_ROW_TOP + 1; i < COURT_ROW_BOT; i += 2)
|
|
176
|
+
consoleDrawText(COURT_NET_COL, i, ":");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
|
|
180
|
+
static void hide_sprites(void) {
|
|
181
|
+
u16 i;
|
|
182
|
+
for (i = 0; i < 7; i++) oamSet(SPR(i), 0, 240, 3, 0, 0, 0, 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static void title_enter(void) {
|
|
186
|
+
clear_rows(0, 27);
|
|
187
|
+
consoleDrawText((32 - (sizeof(GAME_TITLE) - 1)) / 2, 2, GAME_TITLE);
|
|
188
|
+
consoleDrawText(9, 4, "BEST STREAK");
|
|
189
|
+
fmt_u16(best_streak);
|
|
190
|
+
consoleDrawText(21, 4, nbuf);
|
|
191
|
+
consoleDrawText(9, 7, "A - 1P VS CPU");
|
|
192
|
+
consoleDrawText(9, 9, "B - 2P VERSUS");
|
|
193
|
+
consoleDrawText(8, 12, "FIRST TO 5 WINS");
|
|
194
|
+
hide_sprites();
|
|
195
|
+
state = ST_TITLE;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static void match_enter(u8 players) {
|
|
199
|
+
two_player = players;
|
|
200
|
+
p1y = 100; p2y = 100;
|
|
201
|
+
score_p1 = 0; score_p2 = 0;
|
|
202
|
+
new_record = 0;
|
|
203
|
+
serve_ball(0);
|
|
204
|
+
clear_rows(0, 27);
|
|
205
|
+
consoleDrawText(2, 1, "P1");
|
|
206
|
+
consoleDrawText(24, 1, two_player ? "P2 " : "CPU");
|
|
207
|
+
draw_scores();
|
|
208
|
+
draw_court();
|
|
209
|
+
if (sound_ok) sfx_play(1); /* serve-up blip */
|
|
210
|
+
state = ST_PLAY;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ── GAME LOGIC (clay) — match over: result + record bookkeeping.
|
|
214
|
+
* Persistence choice: for a VERSUS sports game a raw hi-score is
|
|
215
|
+
* meaningless (every match ends 5-x), so we persist the longest 1P win
|
|
216
|
+
* streak against the CPU — the stat a returning player actually chases.
|
|
217
|
+
* 2P matches never touch it (humans beating each other isn't a record). */
|
|
218
|
+
static void end_match(void) {
|
|
219
|
+
clear_rows(11, 17); /* result card overlays the court */
|
|
220
|
+
if (score_p1 >= WIN_SCORE) {
|
|
221
|
+
consoleDrawText(8, 12, two_player ? "P1 WINS THE MATCH" : "YOU BEAT THE CPU");
|
|
222
|
+
if (!two_player) {
|
|
223
|
+
++streak;
|
|
224
|
+
if (streak > best_streak) {
|
|
225
|
+
best_streak = streak;
|
|
226
|
+
new_record = 1;
|
|
227
|
+
streak_save(best_streak); /* battery SRAM — see hdr.asm note up top */
|
|
228
|
+
}
|
|
87
229
|
}
|
|
230
|
+
} else if (two_player) {
|
|
231
|
+
consoleDrawText(8, 12, "P2 WINS THE MATCH");
|
|
232
|
+
} else {
|
|
233
|
+
consoleDrawText(7, 12, "CPU TAKES THE MATCH");
|
|
234
|
+
streak = 0; /* the streak dies with the loss */
|
|
235
|
+
}
|
|
236
|
+
if (!two_player) {
|
|
237
|
+
consoleDrawText(10, 14, "STREAK");
|
|
238
|
+
fmt_u16(streak);
|
|
239
|
+
consoleDrawText(17, 14, nbuf);
|
|
240
|
+
if (new_record) consoleDrawText(20, 14, "- NEW RECORD");
|
|
241
|
+
}
|
|
242
|
+
consoleDrawText(10, 16, "PRESS START");
|
|
243
|
+
if (sound_ok) sfx_play(2); /* end-of-match flourish */
|
|
244
|
+
state = ST_OVER;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
248
|
+
static void score_point(u8 for_p1) {
|
|
249
|
+
if (for_p1) ++score_p1; else ++score_p2;
|
|
250
|
+
if (sound_ok) sfx_play(2);
|
|
251
|
+
draw_scores();
|
|
252
|
+
if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
|
|
253
|
+
else serve_ball(for_p1); /* winner of the point receives */
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
|
|
257
|
+
* Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU moves at 1,
|
|
258
|
+
* so an edge hit is exactly how a human beats it. A ±1 random "spin" on
|
|
259
|
+
* every return keeps rallies from repeating (see the PRNG note above). */
|
|
260
|
+
static void deflect(s16 paddle_y) {
|
|
261
|
+
s16 rel = (s16)(by + BALL_SIZE / 2) - (s16)(paddle_y + PADDLE_H / 2);
|
|
262
|
+
bdy = (s8)(rel >> 3);
|
|
263
|
+
bdy += (s8)((random8() & 2) - 1); /* spin: -1 or +1 */
|
|
264
|
+
if (bdy > 2) bdy = 2;
|
|
265
|
+
if (bdy < -2) bdy = -2;
|
|
266
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
267
|
+
if (sound_ok) sfx_play(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Headless-test telemetry — written once per frame into this block. A test
|
|
271
|
+
* harness finds it by scanning WRAM for the "NS"+0xBD signature, then plays
|
|
272
|
+
* the game from real state instead of parsing pixels. Delete freely. */
|
|
273
|
+
static u8 telem[16];
|
|
274
|
+
static void telem_update(void) {
|
|
275
|
+
telem[0] = 'N'; telem[1] = 'S'; telem[2] = 0xBD;
|
|
276
|
+
telem[3] = state;
|
|
277
|
+
telem[4] = score_p1;
|
|
278
|
+
telem[5] = score_p2;
|
|
279
|
+
telem[6] = (u8)((sound_ok << 7) | two_player);
|
|
280
|
+
telem[7] = (u8)p1y;
|
|
281
|
+
telem[8] = (u8)p2y;
|
|
282
|
+
telem[9] = (u8)bx; telem[10] = (u8)(bx >> 8);
|
|
283
|
+
telem[11] = (u8)by;
|
|
284
|
+
telem[12] = serve_timer;
|
|
285
|
+
telem[13] = streak;
|
|
286
|
+
telem[14] = (u8)best_streak; telem[15] = (u8)(best_streak >> 8);
|
|
88
287
|
}
|
|
89
288
|
|
|
90
289
|
int main(void) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
290
|
+
u16 pad, pad2;
|
|
291
|
+
u16 i, slot;
|
|
292
|
+
s16 target;
|
|
293
|
+
|
|
294
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
295
|
+
* Init order: console text pointers FIRST, then mode, then BG bases.
|
|
296
|
+
* consoleInitText DMAs the font but does NOT set the PPU BG base
|
|
297
|
+
* registers — point BG0 at the same font ($3000) + map ($6800) yourself
|
|
298
|
+
* or the text layer renders garbage. */
|
|
299
|
+
consoleSetTextMapPtr(0x6800);
|
|
300
|
+
consoleSetTextGfxPtr(0x3000);
|
|
301
|
+
consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font at BG char base */
|
|
302
|
+
consoleInitText(0, 16 * 2, &tilfont, &palfont);
|
|
303
|
+
setMode(BG_MODE1, 0);
|
|
304
|
+
bgSetGfxPtr(0, 0x3000);
|
|
305
|
+
bgSetMapPtr(0, 0x6800, SC_32x32);
|
|
306
|
+
|
|
307
|
+
/* BG1 = full-screen wallpaper so the court never reads as blank.
|
|
308
|
+
* Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
|
|
309
|
+
* the console gfx $3000 / map $6800). Map entries use palette block 1
|
|
310
|
+
* (0x0400) so the wallpaper palette doesn't disturb the console font
|
|
311
|
+
* palette in block 0 (HUD/court text stays legible). */
|
|
312
|
+
bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
|
|
313
|
+
32, 32, BG_16COLORS, 0x2000);
|
|
314
|
+
|
|
315
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────
|
|
316
|
+
* Court-green backdrop tint: recolor the wallpaper's CGRAM entries
|
|
317
|
+
* (block 1 = entries 16+). Swap these for your own arena's mood. */
|
|
318
|
+
setPaletteColor(0, RGB5(2, 9, 4));
|
|
319
|
+
setPaletteColor(17, RGB5(5, 14, 7));
|
|
320
|
+
setPaletteColor(18, RGB5(3, 11, 5));
|
|
321
|
+
for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
|
|
322
|
+
bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
|
|
323
|
+
bgSetEnable(1);
|
|
324
|
+
bgSetDisable(2); /* BG3 carries garbage in mode 1 */
|
|
325
|
+
|
|
326
|
+
oamInitGfxSet(&tilsprite, 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
|
|
327
|
+
hide_sprites();
|
|
328
|
+
|
|
329
|
+
setScreenOn();
|
|
330
|
+
|
|
331
|
+
/* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
|
|
332
|
+
* the return: a wedged SPC700 must not take the video down with it. ── */
|
|
333
|
+
sound_ok = (sfx_init() == 0);
|
|
334
|
+
/* ── HARDWARE IDIOM (load-bearing) — one frame between init and the first
|
|
335
|
+
* command. sfx_init returns the instant the SPC echoes the jump command,
|
|
336
|
+
* but the driver then spends ~50 port writes initialising the DSP BEFORE
|
|
337
|
+
* it seeds its command edge-detector from $2140. Send a command in that
|
|
338
|
+
* window and the seed swallows it — music silently never starts (found
|
|
339
|
+
* via getAudioState: voice 1 pitch 0, ARAM prev_cmd already = 3). A
|
|
340
|
+
* WaitForVBlank is thousands of SPC cycles — deterministic cure. ── */
|
|
341
|
+
WaitForVBlank();
|
|
342
|
+
if (sound_ok) sfx_music_play();
|
|
343
|
+
|
|
344
|
+
/* ── HARDWARE IDIOM (load-bearing) — initialize EVERY mutable global.
|
|
345
|
+
* PVSnesLib's crt0 does NOT zero BSS, and SNES WRAM powers up dirty
|
|
346
|
+
* ($55 fill in snes9x). A static you never assigned holds garbage —
|
|
347
|
+
* here that meant two_player=0x55 picked "2P mode" paths before the
|
|
348
|
+
* first match ever set it. C's "statics start at 0" does not apply. ── */
|
|
349
|
+
best_streak = streak_load(); /* battery SRAM — 0 on first boot */
|
|
350
|
+
streak = 0;
|
|
351
|
+
two_player = 0;
|
|
352
|
+
score_p1 = score_p2 = 0;
|
|
353
|
+
p1y = p2y = 100;
|
|
354
|
+
bx = 124; by = 108;
|
|
355
|
+
bdx = bdy = 0;
|
|
356
|
+
serve_timer = 0;
|
|
357
|
+
new_record = 0;
|
|
358
|
+
rng = 0xC0A7; /* the data segment isn't trustworthy either */
|
|
359
|
+
prev_pad0 = 0;
|
|
360
|
+
title_enter();
|
|
361
|
+
|
|
362
|
+
while (1) {
|
|
363
|
+
pad = padsCurrent(0);
|
|
364
|
+
|
|
365
|
+
if (state == ST_TITLE) {
|
|
366
|
+
/* ── GAME LOGIC (clay) — title: A/START = 1P vs CPU, B = 2P versus ── */
|
|
367
|
+
if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
|
|
368
|
+
(pad & KEY_START && !(prev_pad0 & KEY_START))) {
|
|
369
|
+
match_enter(0);
|
|
370
|
+
} else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
|
|
371
|
+
match_enter(1);
|
|
372
|
+
}
|
|
373
|
+
} else if (state == ST_OVER) {
|
|
374
|
+
/* Freeze the final scene; START or A returns to the title. */
|
|
375
|
+
if (pad & (KEY_START | KEY_A) && !(prev_pad0 & (KEY_START | KEY_A)))
|
|
376
|
+
title_enter();
|
|
377
|
+
} else {
|
|
378
|
+
/* ── ST_PLAY — GAME LOGIC (clay) from here down ──────────────────── */
|
|
379
|
+
random8(); /* tick the noise source every play frame */
|
|
380
|
+
|
|
381
|
+
/* P1 — port 0, up/down, 2px/frame. */
|
|
382
|
+
if ((pad & KEY_UP) && p1y > COURT_TOP) p1y -= 2;
|
|
383
|
+
if ((pad & KEY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
|
|
384
|
+
|
|
385
|
+
if (two_player) {
|
|
386
|
+
/* P2 — port 1 (controller 2), same speed: a fair versus match. */
|
|
387
|
+
pad2 = padsCurrent(1);
|
|
388
|
+
if ((pad2 & KEY_UP) && p2y > COURT_TOP) p2y -= 2;
|
|
389
|
+
if ((pad2 & KEY_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 2;
|
|
390
|
+
} else {
|
|
391
|
+
/* CPU — chases the ball centre at 1px/frame (half player speed)
|
|
392
|
+
* with a small dead zone. Beatable by design: steep deflections
|
|
393
|
+
* outrun it. */
|
|
394
|
+
target = by + BALL_SIZE / 2 - PADDLE_H / 2;
|
|
395
|
+
if (p2y + 2 < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
|
|
396
|
+
else if (p2y > target + 2 && p2y > COURT_TOP) p2y -= 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* Ball update (frozen during the post-point serve pause). */
|
|
400
|
+
if (serve_timer > 0) {
|
|
401
|
+
serve_timer--;
|
|
402
|
+
} else if (state == ST_PLAY) {
|
|
403
|
+
bx = (s16)(bx + bdx);
|
|
404
|
+
by = (s16)(by + bdy);
|
|
405
|
+
|
|
406
|
+
/* Rail bounce. */
|
|
407
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = (s8)(-bdy); }
|
|
408
|
+
if (by + BALL_SIZE > COURT_BOT) { by = COURT_BOT - BALL_SIZE; bdy = (s8)(-bdy); }
|
|
409
|
+
|
|
410
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
411
|
+
if (bdx < 0
|
|
412
|
+
&& bx <= PADDLE_X1 + 8 && bx + BALL_SIZE >= PADDLE_X1
|
|
413
|
+
&& by + BALL_SIZE > p1y && by < p1y + PADDLE_H) {
|
|
414
|
+
bdx = (s8)(-bdx);
|
|
415
|
+
bx = PADDLE_X1 + 8;
|
|
416
|
+
deflect(p1y);
|
|
146
417
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
p1 = padsCurrent(0);
|
|
154
|
-
p2 = padsCurrent(1);
|
|
155
|
-
|
|
156
|
-
if ((p1 & KEY_UP) && p1y > COURT_TOP) p1y -= 2;
|
|
157
|
-
if ((p1 & KEY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
|
|
158
|
-
|
|
159
|
-
if (p2 != 0) {
|
|
160
|
-
if ((p2 & KEY_UP) && p2y > COURT_TOP) p2y -= 2;
|
|
161
|
-
if ((p2 & KEY_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 2;
|
|
162
|
-
} else {
|
|
163
|
-
target = by - PADDLE_H / 2;
|
|
164
|
-
if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
|
|
165
|
-
else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
|
|
418
|
+
if (bdx > 0
|
|
419
|
+
&& bx + BALL_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + 8
|
|
420
|
+
&& by + BALL_SIZE > p2y && by < p2y + PADDLE_H) {
|
|
421
|
+
bdx = (s8)(-bdx);
|
|
422
|
+
bx = PADDLE_X2 - BALL_SIZE;
|
|
423
|
+
deflect(p2y);
|
|
166
424
|
}
|
|
167
425
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
&& bx <= PADDLE_X2 + 8
|
|
189
|
-
&& by + BALL_SIZE > p2y
|
|
190
|
-
&& by < p2y + PADDLE_H) {
|
|
191
|
-
bdx = (s8)(-bdx);
|
|
192
|
-
bx = PADDLE_X2 - BALL_SIZE;
|
|
193
|
-
sfx_play(1);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (bx < 4) { if (score_p2 < 9) score_p2++; sfx_play(2); serve_ball(0); }
|
|
197
|
-
if (bx > 252) { if (score_p1 < 9) score_p1++; sfx_play(2); serve_ball(1); }
|
|
198
|
-
}
|
|
426
|
+
/* Off either side → point. */
|
|
427
|
+
if (bx < 4) score_point(0); /* past P1 → right side scores */
|
|
428
|
+
else if (bx > 244) score_point(1); /* past P2 → P1 scores */
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
prev_pad0 = pad;
|
|
432
|
+
telem_update();
|
|
433
|
+
|
|
434
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
435
|
+
* Stage ALL sprites, then oamUpdate(), then WaitForVBlank. PVSnesLib's
|
|
436
|
+
* NMI handler DMAs shadow OAM → real OAM every vblank (channel 7);
|
|
437
|
+
* oamUpdate marks the shadow dirty so that DMA carries THIS frame's
|
|
438
|
+
* positions. Stage-after-wait shows last frame's sprites. */
|
|
439
|
+
if (state == ST_PLAY || state == ST_OVER) {
|
|
440
|
+
slot = 0;
|
|
441
|
+
for (i = 0; i < PADDLE_H / 8; i++)
|
|
442
|
+
oamSet(SPR(slot++), PADDLE_X1, (u16)(p1y + i * 8), 3, 0, 0, 0, 0);
|
|
443
|
+
for (i = 0; i < PADDLE_H / 8; i++)
|
|
444
|
+
oamSet(SPR(slot++), PADDLE_X2, (u16)(p2y + i * 8), 3, 0, 0, 0, 0);
|
|
445
|
+
oamSet(SPR(slot), (u16)bx, (u16)by, 3, 0, 0, 0, 0);
|
|
199
446
|
}
|
|
200
|
-
|
|
447
|
+
oamUpdate();
|
|
448
|
+
WaitForVBlank();
|
|
449
|
+
consoleVblank();
|
|
450
|
+
}
|
|
451
|
+
return 0;
|
|
201
452
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "romdevtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 14 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, ...) via WASM toolchains + emulator cores. Use over plain HTTP, as an Agent Skill, or as an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp/server.js",
|