romdevtools 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -177
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -180
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -156
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +19 -6
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +64 -19
|
@@ -1,209 +1,662 @@
|
|
|
1
|
-
/* ── shmup.c — SMS vertical
|
|
1
|
+
/* ── shmup.c — SMS vertical shooter (complete example game) ──────────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* A COMPLETE, working game — title screen, 1P and 2P co-op modes, lives,
|
|
4
|
+
* score + hi-score (cart-RAM save code included — see the honesty note at
|
|
5
|
+
* hiscore_save), music + SFX, and the SMS's signature LINE INTERRUPT split:
|
|
6
|
+
* a fixed HUD strip over a drifting starfield, with the scroll change timed
|
|
7
|
+
* by the VDP's programmable line counter.
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
9
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
10
|
+
* very different one. The markers tell you what's what:
|
|
11
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented SMS footgun; reshape
|
|
12
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
13
|
+
* GAME LOGIC (clay) — enemy patterns, scoring, tuning, art: reshape freely.
|
|
14
|
+
*
|
|
15
|
+
* What depends on what:
|
|
16
|
+
* sms_hw.h / vdp_init.c / load_tiles.c / load_palette.c / sprite_table.c /
|
|
17
|
+
* joypad_read.c — the bundled VDP + input runtime (this file's externs).
|
|
18
|
+
* sms_sfx.{h,c} + sms_music.{h,c} — SN76489 PSG sound layers.
|
|
19
|
+
* sms_crt0.s — boot + vector table. Its $0038 IM-1 handler is the OTHER
|
|
20
|
+
* HALF of the line-interrupt idiom below: it acks the VDP (one status
|
|
21
|
+
* read clears BOTH the frame and line IRQ flags) and returns with
|
|
22
|
+
* ei/reti. Load-bearing; edit with TROUBLESHOOTING open.
|
|
23
|
+
*
|
|
24
|
+
* Frame budget (NTSC, 60fps): SAT upload (192 OUTs) + HUD redraw fit easily
|
|
25
|
+
* in vblank (70 lines); the whole update (2 ships × 6 bullets × 6 enemies
|
|
26
|
+
* AABB ≈ 72 checks worst case) fits in one frame with room to spare.
|
|
8
27
|
*/
|
|
9
28
|
#include "sms_hw.h"
|
|
10
29
|
#include "sms_sfx.h"
|
|
30
|
+
#include "sms_music.h"
|
|
11
31
|
#include <stdint.h>
|
|
12
32
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
extern void sms_load_palette(const uint8_t *palette);
|
|
17
|
-
extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
|
|
18
|
-
extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
|
|
19
|
-
extern void sms_vblank_wait(void);
|
|
20
|
-
extern uint8_t sms_joypad_read(void);
|
|
21
|
-
extern void sms_sprite_init(void);
|
|
22
|
-
extern void sms_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
|
|
23
|
-
extern void sms_sat_upload(void);
|
|
24
|
-
|
|
25
|
-
#define MAX_BULLETS 4
|
|
26
|
-
#define MAX_ENEMIES 4
|
|
33
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
34
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
35
|
+
#define GAME_TITLE "ASTRO PICKET"
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
extern void sms_vdp_init(void);
|
|
38
|
+
extern void sms_vdp_write_reg(uint8_t reg, uint8_t value);
|
|
39
|
+
extern void sms_vdp_display_on(void);
|
|
40
|
+
extern void sms_vdp_display_off(void);
|
|
41
|
+
extern void sms_vdp_set_addr(uint16_t addr, uint8_t prefix);
|
|
42
|
+
extern void sms_load_palette(const uint8_t *palette);
|
|
43
|
+
extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
|
|
44
|
+
extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
|
|
45
|
+
extern uint8_t sms_joypad_read(void);
|
|
46
|
+
extern uint8_t sms_joypad_read_p2(void);
|
|
47
|
+
extern void sms_sprite_init(void);
|
|
48
|
+
extern void sms_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
|
|
49
|
+
extern void sms_sat_upload(void);
|
|
31
50
|
|
|
51
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
52
|
+
* Palettes. SMS CRAM is 2-2-2 BGR (--BBGGRR): R bits 0-1, G bits 2-3,
|
|
53
|
+
* B bits 4-5. White = 0x3F. BG colour 0 doubles as the backdrop/border. */
|
|
32
54
|
static const uint8_t palette[32] = {
|
|
33
|
-
/* BG: 0 =
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
0x00,0x3F,0x0F,0x03,
|
|
41
|
-
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
55
|
+
/* BG: 0 = space black, 1 = HUD-bar blue, 2 = dim star, 3 = white (text),
|
|
56
|
+
* 4 = deep-navy nebula band */
|
|
57
|
+
0x00, 0x20, 0x2A, 0x3F, 0x10, 0x00, 0x00, 0x00,
|
|
58
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
59
|
+
/* Sprites: 1 = white (P1 ship), 2 = yellow (bullet), 3 = red (enemy),
|
|
60
|
+
* 4 = green (P2 ship). One shared sprite palette on SMS — per-"sprite"
|
|
61
|
+
* colour means per-TILE colour indices, not per-sprite palettes. */
|
|
62
|
+
0x00, 0x3F, 0x0F, 0x03, 0x0C, 0x00, 0x00, 0x00,
|
|
63
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
42
64
|
};
|
|
43
65
|
|
|
44
|
-
/*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
/* ── GAME LOGIC (clay) — BG tile inventory (BG bank $0000) ───────────────────
|
|
67
|
+
* tile 0 = blank space (colour 0)
|
|
68
|
+
* tiles 1..37 = font: digits 0-9, A-Z, '-' (uploaded 1bpp→4bpp below)
|
|
69
|
+
* tile 38 = dim star (one colour-2 pixel)
|
|
70
|
+
* tile 39 = bright star(one colour-3 pixel + glow)
|
|
71
|
+
* tile 40 = solid HUD bar (colour 1) — the split seam hides in it
|
|
72
|
+
* tile 41 = nebula band (solid colour 4) — keeps the screen from
|
|
73
|
+
* reading as one flat colour (render-health floor) */
|
|
74
|
+
#define FONT_BASE 1
|
|
75
|
+
#define BG_STAR 38
|
|
76
|
+
#define BG_BRITE 39
|
|
77
|
+
#define BG_HUDBAR 40
|
|
78
|
+
#define BG_BAND 41
|
|
79
|
+
|
|
80
|
+
/* 1bpp font (same glyph set as the NES/GB examples — 0-9, A-Z, '-').
|
|
81
|
+
* Stored 8 bytes/glyph; expanded to the SMS's 32-byte 4bpp tiles at upload
|
|
82
|
+
* (see load_font below), so the ROM carries 296 bytes instead of 1184. */
|
|
83
|
+
static const uint8_t font8[37][8] = {
|
|
84
|
+
/* 0-9 */
|
|
85
|
+
{0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
86
|
+
{0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
|
|
87
|
+
{0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
|
|
88
|
+
{0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
|
|
89
|
+
{0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
|
|
90
|
+
/* A-Z */
|
|
91
|
+
{0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
|
|
92
|
+
{0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
|
|
93
|
+
{0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
|
|
94
|
+
{0x3C,0x66,0x60,0x6E,0x66,0x66,0x3E,0x00}, {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
|
|
95
|
+
{0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00}, {0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00},
|
|
96
|
+
{0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
|
|
97
|
+
{0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
|
|
98
|
+
{0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
|
|
99
|
+
{0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
|
|
100
|
+
{0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
|
101
|
+
{0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
|
|
102
|
+
{0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
|
|
103
|
+
{0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
|
|
104
|
+
/* '-' */
|
|
105
|
+
{0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
|
64
106
|
};
|
|
65
107
|
|
|
66
|
-
/*
|
|
67
|
-
*
|
|
68
|
-
static void
|
|
69
|
-
uint8_t
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
108
|
+
/* Expand 1bpp glyphs into 4bpp SMS tiles as colour 3 (planes 0+1 set).
|
|
109
|
+
* SMS tile rows are 4 bytes: plane0, plane1, plane2, plane3. */
|
|
110
|
+
static void load_font(void) {
|
|
111
|
+
uint8_t g, r, bits;
|
|
112
|
+
sms_vdp_set_addr((uint16_t)(FONT_BASE * 32), VDP_VRAM_WRITE);
|
|
113
|
+
for (g = 0; g < 37; g++) {
|
|
114
|
+
for (r = 0; r < 8; r++) {
|
|
115
|
+
bits = font8[g][r];
|
|
116
|
+
PORT_VDP_DATA = bits; /* plane 0 */
|
|
117
|
+
PORT_VDP_DATA = bits; /* plane 1 → colour index 3 */
|
|
118
|
+
PORT_VDP_DATA = 0; /* plane 2 */
|
|
119
|
+
PORT_VDP_DATA = 0; /* plane 3 */
|
|
75
120
|
}
|
|
76
121
|
}
|
|
77
122
|
}
|
|
78
123
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
0x00,
|
|
87
|
-
|
|
88
|
-
0x00,
|
|
89
|
-
0x00,
|
|
90
|
-
/*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
0x18,0x18,0x00,0x00, 0x24,0x24,0x00,0x00,
|
|
94
|
-
0x42,0x42,0x00,0x00, 0x81,0x81,0x00,0x00,
|
|
124
|
+
/* Star + HUD-bar + band tiles (4bpp, 32 bytes each — rows of plane0..3). */
|
|
125
|
+
static const uint8_t deco_tiles[128] = {
|
|
126
|
+
/* BG_STAR: one colour-2 pixel */
|
|
127
|
+
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x10,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
128
|
+
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
129
|
+
/* BG_BRITE: colour-3 dot with colour-2 glow */
|
|
130
|
+
0x00,0x00,0x00,0x00, 0x00,0x10,0x00,0x00, 0x10,0x28,0x10,0x00, 0x00,0x10,0x00,0x00,
|
|
131
|
+
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
132
|
+
/* BG_HUDBAR: solid colour 1 — the split seam lands inside this row */
|
|
133
|
+
0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
|
|
134
|
+
0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
|
|
135
|
+
/* BG_BAND: solid colour 4 (binary 100 → plane 2 only) */
|
|
136
|
+
0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
|
|
137
|
+
0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
|
|
95
138
|
};
|
|
96
139
|
|
|
97
|
-
|
|
140
|
+
/* Sprite tiles (sprite bank $2000 — vdp_init's R6=0xFF baseline reads
|
|
141
|
+
* sprite patterns from $2000, so upload there, not $0000). */
|
|
142
|
+
static const uint8_t sprite_tiles[32 * 4] = {
|
|
143
|
+
/* T_SHIP1 — arrowhead, colour 1 (white) */
|
|
144
|
+
0x18,0x00,0x00,0x00, 0x18,0x00,0x00,0x00, 0x3C,0x00,0x00,0x00, 0x3C,0x00,0x00,0x00,
|
|
145
|
+
0x7E,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xDB,0x00,0x00,0x00, 0x81,0x00,0x00,0x00,
|
|
146
|
+
/* T_SHIP2 — same hull, colour 4 = binary 100 → plane 2 only (green) */
|
|
147
|
+
0x00,0x00,0x18,0x00, 0x00,0x00,0x18,0x00, 0x00,0x00,0x3C,0x00, 0x00,0x00,0x3C,0x00,
|
|
148
|
+
0x00,0x00,0x7E,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xDB,0x00, 0x00,0x00,0x81,0x00,
|
|
149
|
+
/* T_BULLET — slug, colour 2 (yellow, plane 1) */
|
|
150
|
+
0x00,0x18,0x00,0x00, 0x00,0x3C,0x00,0x00, 0x00,0x3C,0x00,0x00, 0x00,0x3C,0x00,0x00,
|
|
151
|
+
0x00,0x3C,0x00,0x00, 0x00,0x3C,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x00,0x00,0x00,
|
|
152
|
+
/* T_ENEMY — X fighter, colour 3 (red, planes 0+1) */
|
|
153
|
+
0x81,0x81,0x00,0x00, 0x42,0x42,0x00,0x00, 0x24,0x24,0x00,0x00, 0xFF,0xFF,0x00,0x00,
|
|
154
|
+
0xFF,0xFF,0x00,0x00, 0x24,0x24,0x00,0x00, 0x42,0x42,0x00,0x00, 0x81,0x81,0x00,0x00,
|
|
155
|
+
};
|
|
156
|
+
#define T_SHIP1 0
|
|
157
|
+
#define T_SHIP2 1
|
|
158
|
+
#define T_BULLET 2
|
|
159
|
+
#define T_ENEMY 3
|
|
160
|
+
|
|
161
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
162
|
+
* Object pools — fixed slots, no allocation (3.58MHz Z80, 8KB WRAM: a heap
|
|
163
|
+
* buys you nothing). SAT slot map: 0 = P1 ship, 1 = P2 ship, 2-7 bullets,
|
|
164
|
+
* 8-13 enemies — 14 of 64 slots; mind the 8-sprites-PER-SCANLINE limit when
|
|
165
|
+
* adding rows of objects (the 9th sprite on a line silently vanishes). */
|
|
166
|
+
#define MAX_BULLETS 6
|
|
167
|
+
#define MAX_ENEMIES 6
|
|
168
|
+
#define START_LIVES 3
|
|
169
|
+
/* HUD layout: row 0 = text (SC / HI / LV), row 1 = blank, row 2 = solid bar.
|
|
170
|
+
* The bar row is both the visual divider AND where the split seam hides. */
|
|
171
|
+
#define HUD_ROWS 3
|
|
172
|
+
#define HUD_PX (HUD_ROWS * 8)
|
|
173
|
+
|
|
174
|
+
static uint8_t bullet_active[MAX_BULLETS];
|
|
175
|
+
static uint8_t bullet_x[MAX_BULLETS];
|
|
176
|
+
static uint8_t bullet_y[MAX_BULLETS];
|
|
177
|
+
static uint8_t enemy_active[MAX_ENEMIES];
|
|
178
|
+
static uint8_t enemy_x[MAX_ENEMIES];
|
|
179
|
+
static uint8_t enemy_y[MAX_ENEMIES];
|
|
98
180
|
|
|
99
|
-
|
|
100
|
-
static
|
|
101
|
-
static
|
|
181
|
+
/* Players: index 0 = P1 (port A), 1 = P2 (port B, 2P co-op only). */
|
|
182
|
+
static uint8_t ship_x[2], ship_y[2], ship_alive[2], fire_cd[2];
|
|
183
|
+
static uint8_t two_player; /* mode chosen on the title screen */
|
|
184
|
+
static uint8_t lives; /* shared pool in co-op (arcade style) */
|
|
102
185
|
static uint16_t score;
|
|
186
|
+
static uint16_t hiscore;
|
|
103
187
|
static uint8_t spawn_timer;
|
|
188
|
+
static uint8_t scroll_x; /* starfield drift (split-scrolled below HUD) */
|
|
189
|
+
static uint8_t over_pending; /* defer GAME OVER text to the next vblank */
|
|
190
|
+
static uint8_t hud_dirty; /* score/lives changed → redraw next vblank */
|
|
191
|
+
static uint16_t rng = 0xACE1;
|
|
192
|
+
|
|
193
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
194
|
+
#define ST_TITLE 0
|
|
195
|
+
#define ST_PLAY 1
|
|
196
|
+
#define ST_OVER 2
|
|
197
|
+
static uint8_t state;
|
|
198
|
+
|
|
199
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
200
|
+
* LINE-INTERRUPT SPLIT SCROLL — the SMS's signature trick (fixed status bar
|
|
201
|
+
* over a moving field, palette splits, water effects). The VDP has ONE
|
|
202
|
+
* scroll register pair for the whole frame; to keep the HUD fixed while the
|
|
203
|
+
* starfield drifts you change the scroll MID-FRAME. Where the NES needs the
|
|
204
|
+
* sprite-0-hit HACK (park a sprite, busy-poll a status bit, burn scanlines
|
|
205
|
+
* spinning), the SMS has a real, PROGRAMMABLE line interrupt:
|
|
206
|
+
*
|
|
207
|
+
* R10 = N line counter: a down-counter reloaded with N every line
|
|
208
|
+
* outside the active area; underflow → IRQ at line N.
|
|
209
|
+
* R0 bit 4 (IE1) line-IRQ enable (already set in vdp_init's 0x36 baseline).
|
|
210
|
+
* R1 bit 5 (IE0) frame(vblank)-IRQ enable (set by sms_vdp_display_on's 0xE0).
|
|
211
|
+
*
|
|
212
|
+
* Both IRQs land on the Z80's IM-1 vector at $0038. The crt0's handler does
|
|
213
|
+
* the canonical minimal handshake: push af / in a,($BF) / pop af / ei / reti
|
|
214
|
+
* — reading the status port ACKS the VDP (clears BOTH pending flags; skip
|
|
215
|
+
* the read and the IRQ line stays asserted = interrupt storm), and EI must
|
|
216
|
+
* precede RETI or interrupts stay off forever after the first one.
|
|
217
|
+
*
|
|
218
|
+
* Because the handler does no work, the MAIN loop synchronizes with HALT:
|
|
219
|
+
* the Z80 sleeps until the next interrupt, then we read the V-counter (port
|
|
220
|
+
* $7E) to learn WHICH one woke us — line IRQs only fire during the active
|
|
221
|
+
* area (V < 0xC0 here), the frame IRQ fires at vblank (V ≥ 0xC0).
|
|
222
|
+
*
|
|
223
|
+
* wait_vblank(): sleep until the frame IRQ → do per-frame VRAM work,
|
|
224
|
+
* write R8 = 0 so the HUD strip renders unscrolled.
|
|
225
|
+
* wait_split(): sleep until the line IRQ at line 23 (R10 = HUD_PX-1,
|
|
226
|
+
* the last line of the solid bar row — any single-line
|
|
227
|
+
* tear from the mid-row write hides inside solid colour)
|
|
228
|
+
* → write R8 = scroll_x; everything below drifts.
|
|
229
|
+
*
|
|
230
|
+
* FOOTGUN — you cannot poll once IRQs are on: sms_vblank_wait() spins on
|
|
231
|
+
* the same status port the ISR reads. The ISR always wins the race (the
|
|
232
|
+
* IRQ fires the instant the flag sets), eats the flag, and the poll loop
|
|
233
|
+
* hangs forever. HALT + V-counter is the IRQ-era replacement.
|
|
234
|
+
*
|
|
235
|
+
* FOOTGUN — why the field drifts HORIZONTALLY: the Y-scroll register (R9)
|
|
236
|
+
* is LATCHED ONCE PER FRAME by the VDP; mid-frame R9 writes do nothing
|
|
237
|
+
* until the next frame, so a "vertical scroll below the HUD" split is
|
|
238
|
+
* impossible on this chip. X-scroll (R8) is sampled per line — that's the
|
|
239
|
+
* one you can change mid-frame. (Vertical motion: animate the star tiles
|
|
240
|
+
* or stream the name table instead.)
|
|
241
|
+
*
|
|
242
|
+
* Requires: R10 programmed, IE1 + IE0 enabled, EI executed once after
|
|
243
|
+
* display-on, the crt0's ack-only ISR, and wait_vblank/wait_split called
|
|
244
|
+
* EVERY frame in this order. R10 reloads after each underflow, so the line
|
|
245
|
+
* IRQ re-fires every HUD_PX lines all the way down the frame — the later
|
|
246
|
+
* wakes harmlessly interrupt game logic (the ISR acks them) and re-halt
|
|
247
|
+
* inside the NEXT wait_vblank(). */
|
|
248
|
+
#define SPLIT_LINE (HUD_PX - 1)
|
|
249
|
+
|
|
250
|
+
static void wait_vblank(void) {
|
|
251
|
+
/* check-first: if game logic overran into vblank, don't sleep a frame */
|
|
252
|
+
while (PORT_V_COUNTER < 0xC0) { __asm__("halt"); }
|
|
253
|
+
sms_vdp_write_reg(8, 0); /* HUD strip renders with X scroll 0 */
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static void wait_split(void) {
|
|
257
|
+
/* halt-first: vblank work always ends inside vblank (V ≥ 0xC0), and the
|
|
258
|
+
* first wake at V < 0xC0 is the line IRQ at SPLIT_LINE */
|
|
259
|
+
do { __asm__("halt"); } while (PORT_V_COUNTER >= 0xC0);
|
|
260
|
+
sms_vdp_write_reg(8, scroll_x); /* field below the bar drifts */
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ── HARDWARE IDIOM (load-bearing) — hi-score in Sega-mapper cart RAM ────────
|
|
264
|
+
* The Sega mapper's control register at $FFFC: bit 3 maps the cart's 8KB
|
|
265
|
+
* battery RAM into $8000-$BFFF (bank slot 2). Map → copy → unmap; keep the
|
|
266
|
+
* window short so stray pointer bugs can't shred the save. The block is
|
|
267
|
+
* magic + value + checksum so a never-written cart (all $FF) reads back as
|
|
268
|
+
* "no save" instead of a garbage hi-score.
|
|
269
|
+
*
|
|
270
|
+
* NOTE the $FFFC address: it's IN the WRAM mirror ($C000-$DFFF mirrors at
|
|
271
|
+
* $E000-$FFFF), so this write also lands in WRAM at $DFFC — the mapper
|
|
272
|
+
* just snoops the bus. That's why the crt0 parks SP at $DFF0: the bytes
|
|
273
|
+
* above it ($DFFC-$FFFF) belong to the mapper registers' shadow.
|
|
274
|
+
*
|
|
275
|
+
* HONESTY (verified 2026-06-10 against the bundled gpgx core): gpgx only
|
|
276
|
+
* instantiates the Sega mapper for ROMs LARGER than 48KB, and this build
|
|
277
|
+
* pipeline emits 32KB ROMs — so in-emulator the $8000 window stays open-bus
|
|
278
|
+
* (reads $FF), the magic check fails, and the game falls back to the WRAM
|
|
279
|
+
* hi-score (in-session only). The code below is still the correct
|
|
280
|
+
* real-hardware idiom and lights up unchanged on a >48KB build or a cart
|
|
281
|
+
* with battery RAM: the load path is self-falsifying, never wrong. */
|
|
282
|
+
#define MAPPER_CTRL (*(volatile uint8_t *)0xFFFC)
|
|
283
|
+
#define CART_RAM ((volatile uint8_t *)0x8000)
|
|
284
|
+
|
|
285
|
+
static void hiscore_save(uint16_t v) {
|
|
286
|
+
uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
|
|
287
|
+
MAPPER_CTRL = 0x08; /* map cart RAM at $8000 */
|
|
288
|
+
CART_RAM[0] = 0x48; /* 'H' */
|
|
289
|
+
CART_RAM[1] = 0x53; /* 'S' */
|
|
290
|
+
CART_RAM[2] = lo;
|
|
291
|
+
CART_RAM[3] = hi;
|
|
292
|
+
CART_RAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
|
|
293
|
+
MAPPER_CTRL = 0x00; /* back to ROM in slot 2 */
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static uint16_t hiscore_load(void) {
|
|
297
|
+
uint16_t v = 0;
|
|
298
|
+
MAPPER_CTRL = 0x08;
|
|
299
|
+
if (CART_RAM[0] == 0x48 && CART_RAM[1] == 0x53 &&
|
|
300
|
+
CART_RAM[4] == (uint8_t)(CART_RAM[2] ^ CART_RAM[3] ^ 0xA5)) {
|
|
301
|
+
v = (uint16_t)(CART_RAM[2] | ((uint16_t)CART_RAM[3] << 8));
|
|
302
|
+
}
|
|
303
|
+
MAPPER_CTRL = 0x00;
|
|
304
|
+
return v;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
308
|
+
static uint8_t random8(void) {
|
|
309
|
+
uint16_t r = rng;
|
|
310
|
+
r ^= r << 7;
|
|
311
|
+
r ^= r >> 9;
|
|
312
|
+
r ^= r << 8;
|
|
313
|
+
rng = r;
|
|
314
|
+
return (uint8_t)r;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* ── GAME LOGIC (clay) — text via the font tiles ─────────────────────────────
|
|
318
|
+
* These write the name table directly, so call them only during vblank (or
|
|
319
|
+
* with the display off): VRAM access during active display races the VDP's
|
|
320
|
+
* own fetches and drops/garbles bytes on real hardware. */
|
|
321
|
+
static uint8_t font_tile(char ch) {
|
|
322
|
+
if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE + (ch - '0'));
|
|
323
|
+
if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE + 10 + (ch - 'A'));
|
|
324
|
+
if (ch == '-') return (uint8_t)(FONT_BASE + 36);
|
|
325
|
+
return 0; /* space → blank tile */
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static void text_draw(uint8_t row, uint8_t col, const char *s) {
|
|
329
|
+
while (*s) sms_set_tilemap_cell(row, col++, font_tile(*s++), 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
|
|
333
|
+
uint8_t d[5], i;
|
|
334
|
+
for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
|
|
335
|
+
for (i = 0; i < 5; i++)
|
|
336
|
+
sms_set_tilemap_cell(row, (uint8_t)(col + i), (uint8_t)(FONT_BASE + d[4 - i]), 0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ── GAME LOGIC (clay) — HUD: SC sssss HI hhhhh LV n on row 0 ── */
|
|
340
|
+
static void draw_hud_labels(void) {
|
|
341
|
+
text_draw(0, 1, "SC");
|
|
342
|
+
text_draw(0, 11, "HI");
|
|
343
|
+
text_draw(0, 21, "LV");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
static void draw_hud(void) {
|
|
347
|
+
draw_u16(0, 4, score);
|
|
348
|
+
draw_u16(0, 14, hiscore);
|
|
349
|
+
sms_set_tilemap_cell(0, 24, (uint8_t)(FONT_BASE + (lives > 9 ? 9 : lives)), 0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ── GAME LOGIC (clay) — screen painters ─────────────────────────────────────
|
|
353
|
+
* Full-screen repaints happen with the DISPLAY OFF (free VRAM access, and a
|
|
354
|
+
* clean cut instead of a visible wipe). While the display is off the frame
|
|
355
|
+
* IRQ doesn't fire — so no halt-based waits in here, or you hang forever. */
|
|
356
|
+
/* PERF FOOTGUN (found the slow way): the obvious per-cell version of this —
|
|
357
|
+
* sms_set_tilemap_cell(r, c, (r*7 + c*5) % 11 ? ... ) — costs ~35 FRAMES:
|
|
358
|
+
* SDCC's 16-bit `%` is a software-division call and set_tilemap_cell redoes
|
|
359
|
+
* the 4-OUT address setup for every cell; 672 cells of that is over 2M
|
|
360
|
+
* cycles of "black screen" between title and game. So: set the VRAM address
|
|
361
|
+
* ONCE per row (the data port autoincrements through the row's 64 bytes)
|
|
362
|
+
* and keep the star pattern in add/compare counters. Paints in ~1 frame. */
|
|
363
|
+
static void paint_starfield(uint8_t from_row) {
|
|
364
|
+
uint8_t r, c, t, s, q;
|
|
365
|
+
for (r = from_row; r < 24; r++) {
|
|
366
|
+
sms_vdp_set_addr((uint16_t)(0x3800 + (uint16_t)r * 64), VDP_VRAM_WRITE);
|
|
367
|
+
/* s = (r*7) mod 11, q = (r*3) mod 29 — then walk +5 mod 11 / +13 mod 29
|
|
368
|
+
* across the columns (same field as the % expressions, no division). */
|
|
369
|
+
s = (uint8_t)(r * 7); while (s >= 11) s -= 11;
|
|
370
|
+
q = (uint8_t)(r * 3); while (q >= 29) q -= 29;
|
|
371
|
+
for (c = 0; c < 32; c++) {
|
|
372
|
+
t = ((r & 3) == 2) ? BG_BAND : 0; /* nebula bands every 4th row */
|
|
373
|
+
if (s == 0) t = BG_STAR;
|
|
374
|
+
if (q == 0) t = BG_BRITE;
|
|
375
|
+
PORT_VDP_DATA = t; /* name-table entry low byte: tile */
|
|
376
|
+
PORT_VDP_DATA = 0; /* high byte: flips/palette/priority */
|
|
377
|
+
s += 5; if (s >= 11) s -= 11;
|
|
378
|
+
q += 13; if (q >= 29) q -= 29;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
104
382
|
|
|
105
|
-
static
|
|
106
|
-
|
|
107
|
-
|
|
383
|
+
static void paint_title(void) {
|
|
384
|
+
sms_vdp_display_off();
|
|
385
|
+
paint_starfield(0);
|
|
386
|
+
text_draw(6, (uint8_t)((32 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
|
|
387
|
+
text_draw(11, 10, "1P START - 1");
|
|
388
|
+
text_draw(13, 10, "2P CO-OP - 2");
|
|
389
|
+
text_draw(17, 12, "HI");
|
|
390
|
+
draw_u16(17, 15, hiscore);
|
|
391
|
+
sms_sprite_init(); /* park every sprite off-screen */
|
|
392
|
+
sms_sat_upload();
|
|
393
|
+
sms_vdp_write_reg(8, 0);
|
|
394
|
+
sms_vdp_display_on(); /* re-enables the frame IRQ too */
|
|
108
395
|
}
|
|
109
396
|
|
|
110
|
-
static void
|
|
397
|
+
static void paint_field(void) {
|
|
398
|
+
uint8_t c;
|
|
399
|
+
sms_vdp_display_off();
|
|
400
|
+
for (c = 0; c < 32; c++) {
|
|
401
|
+
sms_set_tilemap_cell(0, c, 0, 0); /* row 0: HUD text row */
|
|
402
|
+
sms_set_tilemap_cell(1, c, 0, 0); /* row 1: breathing room */
|
|
403
|
+
sms_set_tilemap_cell(2, c, BG_HUDBAR, 0); /* row 2: bar = divider + seam */
|
|
404
|
+
}
|
|
405
|
+
paint_starfield(HUD_ROWS);
|
|
406
|
+
draw_hud_labels();
|
|
407
|
+
draw_hud();
|
|
408
|
+
sms_sprite_init();
|
|
409
|
+
sms_sat_upload();
|
|
410
|
+
sms_vdp_write_reg(8, 0);
|
|
411
|
+
sms_vdp_display_on();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* ── GAME LOGIC (clay) — pools ── */
|
|
415
|
+
static void fire_bullet(uint8_t p) {
|
|
111
416
|
uint8_t i;
|
|
112
417
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
113
|
-
if (!
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
418
|
+
if (!bullet_active[i]) {
|
|
419
|
+
bullet_active[i] = 1;
|
|
420
|
+
bullet_x[i] = ship_x[p];
|
|
421
|
+
bullet_y[i] = (uint8_t)(ship_y[p] - 8);
|
|
422
|
+
/* Voice 2 doubles as the SFX channel: the blip steals the bass for a
|
|
423
|
+
* few frames, then sfx_update() silences it and the tracker re-tones
|
|
424
|
+
* it on its next step — classic "sfx wins over music" arbitration. */
|
|
425
|
+
sfx_tone(2, 180, 3);
|
|
117
426
|
return;
|
|
118
427
|
}
|
|
119
428
|
}
|
|
120
429
|
}
|
|
121
430
|
|
|
122
|
-
static void
|
|
431
|
+
static void spawn_enemy(void) {
|
|
123
432
|
uint8_t i;
|
|
124
433
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
434
|
+
if (!enemy_active[i]) {
|
|
435
|
+
enemy_active[i] = 1;
|
|
436
|
+
enemy_x[i] = (uint8_t)(16 + (random8() & 0x7F) + (random8() & 0x3F));
|
|
437
|
+
enemy_y[i] = HUD_PX + 8; /* spawn just below the HUD bar */
|
|
129
438
|
return;
|
|
130
439
|
}
|
|
131
440
|
}
|
|
132
441
|
}
|
|
133
442
|
|
|
134
|
-
|
|
135
|
-
|
|
443
|
+
/* AABB, both boxes 8x8. */
|
|
444
|
+
static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
|
|
445
|
+
uint8_t dx = (ax > bx) ? (uint8_t)(ax - bx) : (uint8_t)(bx - ax);
|
|
446
|
+
uint8_t dy = (ay > by) ? (uint8_t)(ay - by) : (uint8_t)(by - ay);
|
|
447
|
+
return (uint8_t)((dx < 8) && (dy < 8));
|
|
448
|
+
}
|
|
136
449
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
|
|
147
|
-
for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
|
|
148
|
-
}
|
|
450
|
+
/* ── GAME LOGIC (clay) — start a run / end a run ── */
|
|
451
|
+
static void start_game(uint8_t players) {
|
|
452
|
+
uint8_t i;
|
|
453
|
+
two_player = players;
|
|
454
|
+
for (i = 0; i < MAX_BULLETS; i++) bullet_active[i] = 0;
|
|
455
|
+
for (i = 0; i < MAX_ENEMIES; i++) enemy_active[i] = 0;
|
|
456
|
+
ship_x[0] = two_player ? 96 : 124; ship_y[0] = 160; ship_alive[0] = 1; fire_cd[0] = 0;
|
|
457
|
+
ship_x[1] = 152; ship_y[1] = 160; ship_alive[1] = two_player; fire_cd[1] = 0;
|
|
458
|
+
lives = START_LIVES;
|
|
149
459
|
score = 0;
|
|
150
460
|
spawn_timer = 0;
|
|
461
|
+
scroll_x = 0;
|
|
462
|
+
over_pending = 0;
|
|
463
|
+
paint_field();
|
|
464
|
+
state = ST_PLAY;
|
|
465
|
+
}
|
|
151
466
|
|
|
467
|
+
static void game_over(void) {
|
|
468
|
+
if (score > hiscore) {
|
|
469
|
+
hiscore = score;
|
|
470
|
+
hiscore_save(hiscore); /* cart RAM (real hardware); WRAM copy is live */
|
|
471
|
+
}
|
|
472
|
+
sfx_noise(20);
|
|
473
|
+
state = ST_OVER;
|
|
474
|
+
over_pending = 1; /* text is drawn next vblank — not mid-frame */
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* ── GAME LOGIC (clay) — per-player update ── */
|
|
478
|
+
static void update_ship(uint8_t p, uint8_t pad) {
|
|
479
|
+
if (!ship_alive[p]) return;
|
|
480
|
+
if ((pad & JOY_LEFT) && ship_x[p] > 8) ship_x[p] = (uint8_t)(ship_x[p] - 2);
|
|
481
|
+
if ((pad & JOY_RIGHT) && ship_x[p] < 240) ship_x[p] = (uint8_t)(ship_x[p] + 2);
|
|
482
|
+
if ((pad & JOY_UP) && ship_y[p] > (HUD_PX + 8)) ship_y[p] = (uint8_t)(ship_y[p] - 2);
|
|
483
|
+
if ((pad & JOY_DOWN) && ship_y[p] < 182) ship_y[p] = (uint8_t)(ship_y[p] + 2);
|
|
484
|
+
if ((pad & JOY_B1) && fire_cd[p] == 0) {
|
|
485
|
+
fire_bullet(p);
|
|
486
|
+
fire_cd[p] = 8;
|
|
487
|
+
}
|
|
488
|
+
if (fire_cd[p] > 0) fire_cd[p]--;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* Stage the SAT shadow for this frame. Inactive slots park at Y=$E0 (below
|
|
492
|
+
* the 192-line area). NEVER park at Y=$D0 — that's the SAT terminator: the
|
|
493
|
+
* VDP stops scanning at the first $D0 and every later slot vanishes. */
|
|
494
|
+
static void stage_sprites(void) {
|
|
495
|
+
uint8_t i;
|
|
496
|
+
sms_sprite_set(0, ship_x[0], ship_alive[0] ? ship_y[0] : 0xE0, T_SHIP1);
|
|
497
|
+
sms_sprite_set(1, ship_x[1], ship_alive[1] ? ship_y[1] : 0xE0, T_SHIP2);
|
|
498
|
+
for (i = 0; i < MAX_BULLETS; i++)
|
|
499
|
+
sms_sprite_set((uint8_t)(2 + i), bullet_x[i], bullet_active[i] ? bullet_y[i] : 0xE0, T_BULLET);
|
|
500
|
+
for (i = 0; i < MAX_ENEMIES; i++)
|
|
501
|
+
sms_sprite_set((uint8_t)(8 + i), enemy_x[i], enemy_active[i] ? enemy_y[i] : 0xE0, T_ENEMY);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
void main(void) {
|
|
505
|
+
uint8_t i, pad, pad2, prev_pad = 0;
|
|
506
|
+
|
|
507
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
508
|
+
* Init order: VDP regs (display off) → palette → tiles → name table →
|
|
509
|
+
* SAT → R10 → display on (which also enables the frame IRQ) → EI. The
|
|
510
|
+
* one hard rule: EI comes LAST, after every register is in place — the
|
|
511
|
+
* crt0 boots with DI and the FIRST halt would hang forever if interrupts
|
|
512
|
+
* were never enabled. */
|
|
513
|
+
sms_vdp_init(); /* R0=0x36 already has IE1 (line IRQ) set */
|
|
514
|
+
sms_load_palette(palette);
|
|
515
|
+
load_font();
|
|
516
|
+
sms_load_tiles((uint16_t)(BG_STAR * 32), deco_tiles, 128);
|
|
517
|
+
sms_load_tiles(0x2000, sprite_tiles, 32 * 4);
|
|
152
518
|
sms_sprite_init();
|
|
153
519
|
sfx_init();
|
|
154
|
-
|
|
520
|
+
music_init();
|
|
521
|
+
music_play(0);
|
|
155
522
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
sms_vblank_wait();
|
|
160
|
-
sfx_update();
|
|
523
|
+
/* R10 = SPLIT_LINE arms the line counter: IRQ at the last bar line. Set
|
|
524
|
+
* once — it reloads itself every underflow. */
|
|
525
|
+
sms_vdp_write_reg(10, SPLIT_LINE);
|
|
161
526
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
527
|
+
hiscore = hiscore_load(); /* cart RAM if present — else 0 */
|
|
528
|
+
state = ST_TITLE;
|
|
529
|
+
paint_title();
|
|
530
|
+
__asm__("ei"); /* interrupts live from here on */
|
|
531
|
+
|
|
532
|
+
for (;;) {
|
|
533
|
+
if (state == ST_TITLE) {
|
|
534
|
+
/* ── GAME LOGIC (clay) — title: button 1 = 1P, button 2 = 2P co-op ── */
|
|
535
|
+
wait_vblank();
|
|
536
|
+
sfx_update();
|
|
537
|
+
music_update();
|
|
538
|
+
pad = sms_joypad_read();
|
|
539
|
+
if ((pad & JOY_B1) && !(prev_pad & JOY_B1)) start_game(0);
|
|
540
|
+
else if ((pad & JOY_B2) && !(prev_pad & JOY_B2)) start_game(1);
|
|
541
|
+
prev_pad = pad;
|
|
542
|
+
continue;
|
|
167
543
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
544
|
+
|
|
545
|
+
if (state == ST_OVER) {
|
|
546
|
+
/* Freeze the final frame; button 1 or 2 returns to the title. */
|
|
547
|
+
wait_vblank();
|
|
548
|
+
if (over_pending) { /* deferred draw — now we're in vblank */
|
|
549
|
+
over_pending = 0;
|
|
550
|
+
text_draw(11, 11, "GAME OVER");
|
|
551
|
+
draw_hud(); /* show the (possibly new) hi-score */
|
|
552
|
+
}
|
|
553
|
+
wait_split(); /* keep the HUD/field split alive */
|
|
554
|
+
sfx_update();
|
|
555
|
+
music_update();
|
|
556
|
+
pad = sms_joypad_read();
|
|
557
|
+
if ((pad & (JOY_B1 | JOY_B2)) && !(prev_pad & (JOY_B1 | JOY_B2))) {
|
|
558
|
+
state = ST_TITLE;
|
|
559
|
+
paint_title();
|
|
560
|
+
}
|
|
561
|
+
prev_pad = pad;
|
|
562
|
+
continue;
|
|
171
563
|
}
|
|
172
|
-
sms_sat_upload();
|
|
173
564
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
565
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────────
|
|
566
|
+
* Frame shape: [vblank: SAT + HUD writes, R8=0] → [line IRQ at the bar:
|
|
567
|
+
* R8=scroll] → [rest of frame: game logic]. VRAM traffic stays inside
|
|
568
|
+
* vblank; logic runs while the VDP draws the field.
|
|
569
|
+
*
|
|
570
|
+
* BUDGET FOOTGUN: everything between wait_vblank() and wait_split()
|
|
571
|
+
* must finish before the line IRQ at line 23 — vblank (70 lines) + the
|
|
572
|
+
* HUD strip (23) ≈ 21k cycles. The SAT upload eats ~7k of that. An
|
|
573
|
+
* unconditional draw_hud() here (10 software 16-bit divisions for the
|
|
574
|
+
* digits) blew the budget EVERY frame: the seam slipped to a later
|
|
575
|
+
* reload of the line counter and the top of the starfield rendered
|
|
576
|
+
* unscrolled in jittery stripes. Hence the dirty flag — the HUD only
|
|
577
|
+
* redraws on the frame after the score/lives actually changed. */
|
|
578
|
+
wait_vblank();
|
|
579
|
+
sms_sat_upload(); /* shadow SAT staged at end of last frame */
|
|
580
|
+
if (hud_dirty) {
|
|
581
|
+
hud_dirty = 0;
|
|
582
|
+
draw_hud();
|
|
583
|
+
}
|
|
584
|
+
sfx_update();
|
|
585
|
+
music_update();
|
|
586
|
+
wait_split(); /* the line-interrupt split — every frame */
|
|
587
|
+
|
|
588
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
589
|
+
pad = sms_joypad_read();
|
|
590
|
+
pad2 = two_player ? sms_joypad_read_p2() : 0;
|
|
591
|
+
update_ship(0, pad);
|
|
592
|
+
if (two_player) update_ship(1, pad2);
|
|
593
|
+
|
|
594
|
+
/* Starfield drift (the split keeps the HUD strip out of it). */
|
|
595
|
+
spawn_timer++;
|
|
596
|
+
if ((spawn_timer & 3) == 0) scroll_x++;
|
|
181
597
|
|
|
182
598
|
for (i = 0; i < MAX_BULLETS; i++) {
|
|
183
|
-
if (!
|
|
184
|
-
if (
|
|
185
|
-
|
|
599
|
+
if (!bullet_active[i]) continue;
|
|
600
|
+
if (bullet_y[i] < HUD_PX + 4) bullet_active[i] = 0;
|
|
601
|
+
else bullet_y[i] = (uint8_t)(bullet_y[i] - 4);
|
|
186
602
|
}
|
|
603
|
+
|
|
187
604
|
for (i = 0; i < MAX_ENEMIES; i++) {
|
|
188
|
-
if (!
|
|
189
|
-
|
|
190
|
-
|
|
605
|
+
if (!enemy_active[i]) continue;
|
|
606
|
+
if (enemy_y[i] >= 190) enemy_active[i] = 0;
|
|
607
|
+
else enemy_y[i]++;
|
|
191
608
|
}
|
|
192
|
-
spawn_timer = (uint8_t)(spawn_timer + 1);
|
|
193
|
-
if (spawn_timer >= 28) { spawn_timer = 0; spawn(); }
|
|
194
609
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
610
|
+
/* Bullets ↔ enemies. */
|
|
611
|
+
{
|
|
612
|
+
uint8_t b, e;
|
|
613
|
+
for (b = 0; b < MAX_BULLETS; b++) {
|
|
614
|
+
if (!bullet_active[b]) continue;
|
|
615
|
+
for (e = 0; e < MAX_ENEMIES; e++) {
|
|
616
|
+
if (!enemy_active[e]) continue;
|
|
617
|
+
if (hits(bullet_x[b], bullet_y[b], enemy_x[e], enemy_y[e])) {
|
|
618
|
+
bullet_active[b] = 0;
|
|
619
|
+
enemy_active[e] = 0;
|
|
620
|
+
score++;
|
|
621
|
+
hud_dirty = 1;
|
|
622
|
+
sfx_noise(6);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
205
625
|
}
|
|
206
626
|
}
|
|
207
627
|
}
|
|
208
|
-
|
|
628
|
+
|
|
629
|
+
/* Enemies ↔ ships: shared life pool (arcade co-op). */
|
|
630
|
+
{
|
|
631
|
+
uint8_t e, p;
|
|
632
|
+
for (e = 0; e < MAX_ENEMIES; e++) {
|
|
633
|
+
if (!enemy_active[e]) continue;
|
|
634
|
+
for (p = 0; p < 2; p++) {
|
|
635
|
+
if (!ship_alive[p]) continue;
|
|
636
|
+
if (hits(enemy_x[e], enemy_y[e], ship_x[p], ship_y[p])) {
|
|
637
|
+
enemy_active[e] = 0;
|
|
638
|
+
sfx_noise(14);
|
|
639
|
+
if (lives > 0) lives--;
|
|
640
|
+
hud_dirty = 1;
|
|
641
|
+
if (lives == 0) {
|
|
642
|
+
game_over();
|
|
643
|
+
} else {
|
|
644
|
+
/* respawn knockback */
|
|
645
|
+
ship_y[p] = 160;
|
|
646
|
+
ship_x[p] = p ? 152 : (two_player ? 96 : 124);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (spawn_timer >= 32) {
|
|
654
|
+
spawn_timer = 0;
|
|
655
|
+
spawn_enemy();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* Stage the SAT shadow NOW (RAM only — cheap, any time); the actual
|
|
659
|
+
* VRAM upload waits for the next vblank at the top of the loop. */
|
|
660
|
+
stage_sprites();
|
|
661
|
+
}
|
|
209
662
|
}
|