romdevtools 0.16.0 → 0.21.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.
Files changed (110) hide show
  1. package/AGENTS.md +60 -12
  2. package/CHANGELOG.md +258 -0
  3. package/examples/README.md +2 -0
  4. package/examples/atari2600/templates/platformer.asm +460 -0
  5. package/examples/atari2600/templates/racing.asm +463 -0
  6. package/examples/atari2600/templates/shmup.asm +386 -0
  7. package/examples/atari2600/templates/sports.asm +362 -0
  8. package/examples/atari7800/templates/default.c +49 -5
  9. package/examples/atari7800/templates/platformer.c +43 -4
  10. package/examples/atari7800/templates/puzzle.c +39 -4
  11. package/examples/atari7800/templates/racing.c +39 -4
  12. package/examples/atari7800/templates/shmup.c +40 -2
  13. package/examples/atari7800/templates/sports.c +36 -5
  14. package/examples/c64/templates/platformer.c +19 -5
  15. package/examples/c64/templates/puzzle.c +32 -2
  16. package/examples/c64/templates/shmup.c +28 -2
  17. package/examples/c64/templates/sports.c +30 -2
  18. package/examples/gb/templates/default.c +110 -16
  19. package/examples/gb/templates/platformer.c +25 -4
  20. package/examples/gb/templates/puzzle.c +32 -2
  21. package/examples/gb/templates/racing.c +72 -8
  22. package/examples/gb/templates/shmup.c +38 -1
  23. package/examples/gb/templates/sports.c +48 -1
  24. package/examples/gba/templates/gba_hello.c +29 -11
  25. package/examples/gba/templates/puzzle.c +15 -3
  26. package/examples/gba/templates/racing.c +65 -3
  27. package/examples/gba/templates/shmup.c +41 -4
  28. package/examples/gba/templates/sports.c +36 -2
  29. package/examples/gba/templates/tonc_hello.c +41 -5
  30. package/examples/gbc/templates/default.c +103 -26
  31. package/examples/gbc/templates/platformer.c +25 -4
  32. package/examples/gbc/templates/puzzle.c +32 -2
  33. package/examples/gbc/templates/racing.c +85 -19
  34. package/examples/gbc/templates/shmup.c +34 -1
  35. package/examples/gbc/templates/sports.c +45 -1
  36. package/examples/genesis/templates/puzzle.c +37 -3
  37. package/examples/genesis/templates/racing.c +44 -11
  38. package/examples/genesis/templates/sgdk_hello.c +34 -1
  39. package/examples/genesis/templates/shmup.c +31 -1
  40. package/examples/gg/templates/default.c +56 -18
  41. package/examples/gg/templates/platformer.c +18 -12
  42. package/examples/gg/templates/puzzle.c +38 -7
  43. package/examples/gg/templates/racing.c +51 -5
  44. package/examples/gg/templates/shmup.c +47 -3
  45. package/examples/gg/templates/sports.c +46 -3
  46. package/examples/lynx/templates/default.c +39 -8
  47. package/examples/lynx/templates/puzzle.c +28 -1
  48. package/examples/lynx/templates/racing.c +34 -7
  49. package/examples/lynx/templates/shmup.c +42 -3
  50. package/examples/lynx/templates/sports.c +29 -2
  51. package/examples/msx/platformer/main.c +213 -0
  52. package/examples/msx/puzzle/main.c +250 -0
  53. package/examples/msx/racing/main.c +249 -0
  54. package/examples/msx/shmup/main.c +288 -0
  55. package/examples/msx/sports/main.c +182 -0
  56. package/examples/nes/templates/default.c +67 -19
  57. package/examples/nes/templates/platformer.c +65 -6
  58. package/examples/nes/templates/puzzle.c +67 -6
  59. package/examples/nes/templates/racing.c +45 -13
  60. package/examples/nes/templates/shmup.c +51 -2
  61. package/examples/nes/templates/sports.c +51 -6
  62. package/examples/pce/platformer/main.c +283 -0
  63. package/examples/pce/puzzle/main.c +304 -0
  64. package/examples/pce/racing/main.c +304 -0
  65. package/examples/pce/shmup/main.c +346 -0
  66. package/examples/pce/sports/main.c +254 -0
  67. package/examples/sms/main.c +35 -6
  68. package/examples/sms/templates/puzzle.c +34 -5
  69. package/examples/sms/templates/racing.c +39 -2
  70. package/examples/sms/templates/shmup.c +41 -2
  71. package/examples/sms/templates/sports.c +43 -2
  72. package/examples/snes/templates/default.c +50 -28
  73. package/examples/snes/templates/platformer-data.asm +22 -0
  74. package/examples/snes/templates/platformer.c +16 -1
  75. package/examples/snes/templates/puzzle-data.asm +22 -0
  76. package/examples/snes/templates/puzzle.c +17 -1
  77. package/examples/snes/templates/racing-data.asm +22 -0
  78. package/examples/snes/templates/racing.c +17 -1
  79. package/examples/snes/templates/shmup-data.asm +22 -0
  80. package/examples/snes/templates/shmup.c +20 -1
  81. package/examples/snes/templates/sports-data.asm +22 -0
  82. package/examples/snes/templates/sports.c +16 -1
  83. package/package.json +1 -1
  84. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  85. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  86. package/src/host/LibretroHost.js +122 -1
  87. package/src/host/callbacks.js +9 -1
  88. package/src/host/types.js +15 -8
  89. package/src/http/tool-registry.js +26 -1
  90. package/src/mcp/tools/cart-parts.js +75 -3
  91. package/src/mcp/tools/disasm-rebuild.js +507 -0
  92. package/src/mcp/tools/disasm.js +95 -6
  93. package/src/mcp/tools/frame.js +168 -3
  94. package/src/mcp/tools/lifecycle.js +4 -2
  95. package/src/mcp/tools/project.js +54 -9
  96. package/src/mcp/tools/state.js +201 -14
  97. package/src/mcp/tools/toolchain.js +76 -3
  98. package/src/mcp/tools/watch-memory.js +125 -14
  99. package/src/platforms/c64/MENTAL_MODEL.md +45 -1
  100. package/src/platforms/c64/d64.js +281 -0
  101. package/src/platforms/gb/MENTAL_MODEL.md +10 -0
  102. package/src/platforms/msx/MENTAL_MODEL.md +10 -6
  103. package/src/platforms/nes/MENTAL_MODEL.md +63 -2
  104. package/src/platforms/pce/MENTAL_MODEL.md +9 -4
  105. package/src/platforms/pce/lib/c/pce_video.c +1 -1
  106. package/src/rom-id/identifier.js +15 -0
  107. package/src/toolchains/cc65/ines.js +145 -0
  108. package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
  109. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
  110. package/src/toolchains/common/reassemble.js +10 -2
@@ -53,12 +53,39 @@ static const uint8_t tile_block_b[16] = {
53
53
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
54
54
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
55
55
  };
56
+ /* BG scenery tiles — painted into the nametable so the playfield reads as a
57
+ * real machine on boot (without them the screen is just sprites on flat
58
+ * black). All live in the BACKGROUND pattern table ($1000); the block tiles
59
+ * above are sprites ($0000).
60
+ *
61
+ * BG_WALL — solid bordered block (idx 1, steel blue): the well frame.
62
+ * BG_BRICK — a brick/dither pattern (idx 2) tiling the cabinet wall that
63
+ * surrounds the well, so the whole screen is covered.
64
+ * BG_INNER — a faint grid speck (idx 3) lining the inside of the well so
65
+ * empty cells read as a recessed playfield, not a black hole. */
66
+ static const uint8_t tile_wall[16] = {
67
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
68
+ 0, 0, 0, 0, 0, 0, 0, 0,
69
+ };
70
+ static const uint8_t tile_brick[16] = {
71
+ 0, 0, 0, 0, 0, 0, 0, 0, /* plane 0 clear */
72
+ 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08, /* plane 1 → colour 2 brick */
73
+ };
74
+ static const uint8_t tile_inner[16] = {
75
+ 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, /* plane 0 specks */
76
+ 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, /* plane 1 too → colour 3 */
77
+ };
78
+ #define BG_WALL 1 /* BG tile index 1 → uploaded to $1010 */
79
+ #define BG_BRICK 2 /* BG tile index 2 → uploaded to $1020 */
80
+ #define BG_INNER 3 /* BG tile index 3 → uploaded to $1030 */
56
81
 
57
82
  static const uint8_t palette[32] = {
58
- 0x0F, 0x10, 0x20, 0x30,
59
- 0x0F, 0x10, 0x20, 0x30,
60
- 0x0F, 0x10, 0x20, 0x30,
61
- 0x0F, 0x10, 0x20, 0x30,
83
+ /* BG0: backdrop near-black, wall = steel blue (idx1), brick = brown
84
+ * (idx2), inner grid = dark grey (idx3). */
85
+ 0x0F, 0x11, 0x17, 0x00,
86
+ 0x0F, 0x11, 0x17, 0x00,
87
+ 0x0F, 0x11, 0x17, 0x00,
88
+ 0x0F, 0x11, 0x17, 0x00,
62
89
  /* Sprite palette 0: idx1=red, idx2=green, idx3=blue */
63
90
  0x0F, 0x16, 0x1A, 0x12,
64
91
  0x0F, 0x16, 0x1A, 0x12,
@@ -156,11 +183,45 @@ void main(void) {
156
183
  int8_t i;
157
184
 
158
185
  ppu_off();
159
- chr_ram_upload(0x0000, tile_blank, 16);
160
- chr_ram_upload(0x0010, tile_block_r, 16);
186
+ chr_ram_upload(0x0000, tile_blank, 16); /* sprite slot 0 */
187
+ chr_ram_upload(0x0010, tile_block_r, 16); /* sprite slots 1..3 */
161
188
  chr_ram_upload(0x0020, tile_block_g, 16);
162
189
  chr_ram_upload(0x0030, tile_block_b, 16);
190
+ chr_ram_upload(0x1010, tile_wall, 16); /* BG slot 1 (background table) */
191
+ chr_ram_upload(0x1020, tile_brick, 16); /* BG slot 2 (cabinet wall) */
192
+ chr_ram_upload(0x1030, tile_inner, 16); /* BG slot 3 (well interior) */
163
193
  palette_load(palette);
194
+
195
+ /* Draw the cabinet + well into the nametable while rendering is off
196
+ * (vram_unsafe_set = raw PPU write; the friendly tile_set queue would
197
+ * deadlock before ppu_on). The grid is 6 wide × 12 tall at pixel origin
198
+ * (80,16) → tile cols 10..15, rows 2..13; frame it one cell out. We first
199
+ * tile the WHOLE screen with brick (the machine cabinet), then carve the
200
+ * recessed well interior, then stamp the steel-blue frame on top — so the
201
+ * screen is fully covered instead of sprites floating on black. */
202
+ {
203
+ uint16_t gx0 = ORIGIN_X / 8, gy0 = ORIGIN_Y / 8; /* 10, 2 */
204
+ uint16_t gx1 = gx0 + GRID_W, gy1 = gy0 + GRID_H; /* 16, 14 (exclusive) */
205
+ uint16_t cc, rr;
206
+ /* whole-screen cabinet brick */
207
+ for (rr = 0; rr < 30; rr++)
208
+ for (cc = 0; cc < 32; cc++)
209
+ vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_BRICK);
210
+ /* recessed well interior (inside the frame) */
211
+ for (rr = gy0; rr < gy1; rr++)
212
+ for (cc = gx0; cc < gx1; cc++)
213
+ vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_INNER);
214
+ /* steel frame one cell out around the well */
215
+ for (cc = gx0 - 1; cc <= gx1; cc++) {
216
+ vram_unsafe_set((uint16_t)(0x2000 + (gy0 - 1) * 32 + cc), BG_WALL); /* top */
217
+ vram_unsafe_set((uint16_t)(0x2000 + gy1 * 32 + cc), BG_WALL); /* bottom */
218
+ }
219
+ for (rr = gy0 - 1; rr <= gy1; rr++) {
220
+ vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + (gx0 - 1)), BG_WALL); /* left */
221
+ vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + gx1), BG_WALL); /* right */
222
+ }
223
+ }
224
+
164
225
  oam_clear();
165
226
  ppu_on_all();
166
227
  sound_init();
@@ -59,14 +59,22 @@ static const uint8_t tile_digits[10 * 16] = {
59
59
  /* ── Background road tiles ───────────────────────────────────────────
60
60
  * Default PPUCTRL ($90) reads BG patterns from pattern table 1 ($1000),
61
61
  * so these go to CHR $1000+ and are indexed independently of the sprite
62
- * tiles above. Colour 1 (white, BG palette 0) draws the markings; the
63
- * grey backdrop (colour 0) is the road surface.
62
+ * tiles above. The grey backdrop (colour 0) is the road surface; colour 1
63
+ * (white) draws the markings, colour 2 (green) the grass, colour 3 the
64
+ * dark seam in the tarmac.
64
65
  *
65
- * BG_T_EDGE: a solid 2px vertical stripe — the road shoulder line.
66
- * BG_T_LANE: a 2px vertical dash (on for 4 rows, off for 4) — the
67
- * dashed centre lane marking when stacked down a column. */
66
+ * BG_T_EDGE: a solid 2px vertical stripe — the road shoulder line.
67
+ * BG_T_LANE: a 2px vertical dash (on 4 rows / off 4) — the dashed centre
68
+ * lane marking when stacked down a column.
69
+ * BG_T_GRASS: a textured green roadside (colour 2 hatch) so the area
70
+ * outside the shoulders isn't flat — fills the screen sides.
71
+ * BG_T_ROAD: a faint tarmac texture (a couple of colour-3 specks) tiled
72
+ * across the driving surface so the road doesn't read as one
73
+ * solid grey block. */
68
74
  #define BG_T_EDGE 1
69
75
  #define BG_T_LANE 2
76
+ #define BG_T_GRASS 3
77
+ #define BG_T_ROAD 4
70
78
  static const uint8_t bg_tile_edge[16] = {
71
79
  0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* plane 0 (colour bit 0) */
72
80
  0, 0, 0, 0, 0, 0, 0, 0,
@@ -75,13 +83,21 @@ static const uint8_t bg_tile_lane[16] = {
75
83
  0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, /* dash: 4 on, 4 off */
76
84
  0, 0, 0, 0, 0, 0, 0, 0,
77
85
  };
86
+ static const uint8_t bg_tile_grass[16] = {
87
+ 0, 0, 0, 0, 0, 0, 0, 0, /* plane 0 clear */
88
+ 0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB, /* plane 1 → colour 2 hatch */
89
+ };
90
+ static const uint8_t bg_tile_road[16] = {
91
+ 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, /* plane 0 specks */
92
+ 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, /* plane 1 too → colour 3 */
93
+ };
78
94
 
79
95
  static const uint8_t palette[32] = {
80
- /* BG palettes — light grey backdrop simulates road */
81
- 0x10, 0x30, 0x16, 0x12,
82
- 0x10, 0x30, 0x16, 0x12,
83
- 0x10, 0x30, 0x16, 0x12,
84
- 0x10, 0x30, 0x16, 0x12,
96
+ /* BG palettes — light grey backdrop simulates road; idx2 = grass green */
97
+ 0x10, 0x30, 0x1A, 0x00,
98
+ 0x10, 0x30, 0x1A, 0x00,
99
+ 0x10, 0x30, 0x1A, 0x00,
100
+ 0x10, 0x30, 0x1A, 0x00,
85
101
  /* Sprite palettes */
86
102
  0x10, 0x21, 0x16, 0x30, /* sp0: blue car (P1) */
87
103
  0x10, 0x16, 0x21, 0x30, /* sp1: red enemy */
@@ -125,8 +141,22 @@ static uint8_t aabb(Car *a, Car *b) {
125
141
  #define ROAD_DIV_1 13
126
142
  #define ROAD_DIV_2 17
127
143
  static void draw_road(void) {
128
- uint8_t row;
144
+ uint8_t row, col;
129
145
  uint16_t base;
146
+ /* Fill the WHOLE nametable so nothing reads as flat colour: grass on the
147
+ * roadside (outside the shoulders) and a faint tarmac texture on the
148
+ * driving surface. Then stamp the shoulder + lane markings on top. */
149
+ for (row = 0; row < 30; row++) {
150
+ base = (uint16_t)(0x2000 + (uint16_t)row * 32);
151
+ for (col = 0; col < 32; col++) {
152
+ if (col < ROAD_EDGE_L || col > ROAD_EDGE_R) {
153
+ vram_unsafe_set((uint16_t)(base + col), BG_T_GRASS); /* roadside */
154
+ } else if (((row + col) & 3) == 0) {
155
+ vram_unsafe_set((uint16_t)(base + col), BG_T_ROAD); /* tarmac speck */
156
+ }
157
+ /* else leave colour-0 grey road surface */
158
+ }
159
+ }
130
160
  for (row = ROAD_TOP_ROW; row <= ROAD_BOT_ROW; row++) {
131
161
  base = (uint16_t)(0x2000 + (uint16_t)row * 32);
132
162
  vram_unsafe_set((uint16_t)(base + ROAD_EDGE_L), BG_T_EDGE);
@@ -183,8 +213,10 @@ void main(void) {
183
213
 
184
214
  /* BG road tiles live in pattern table 1 ($1000) — that's where the
185
215
  * default PPUCTRL ($90) tells the PPU to read background patterns. */
186
- chr_ram_upload((uint16_t)(0x1000 + BG_T_EDGE * 16), bg_tile_edge, 16);
187
- chr_ram_upload((uint16_t)(0x1000 + BG_T_LANE * 16), bg_tile_lane, 16);
216
+ chr_ram_upload((uint16_t)(0x1000 + BG_T_EDGE * 16), bg_tile_edge, 16);
217
+ chr_ram_upload((uint16_t)(0x1000 + BG_T_LANE * 16), bg_tile_lane, 16);
218
+ chr_ram_upload((uint16_t)(0x1000 + BG_T_GRASS * 16), bg_tile_grass, 16);
219
+ chr_ram_upload((uint16_t)(0x1000 + BG_T_ROAD * 16), bg_tile_road, 16);
188
220
 
189
221
  palette_load(palette);
190
222
  draw_road(); /* paint the static road while the PPU is off */
@@ -38,10 +38,34 @@ static const uint8_t tile_enemy[16] = {
38
38
  0x81, 0x42, 0x24, 0xFF, 0xFF, 0x24, 0x42, 0x81, /* plane 0 — spider-ish */
39
39
  0, 0, 0, 0, 0, 0, 0, 0,
40
40
  };
41
+ /* BG starfield tiles — painted across the whole nametable so the screen
42
+ * reads as a real "space" scene on boot instead of flat black. They live in
43
+ * the BACKGROUND pattern table ($1000), separate from the sprite tiles above
44
+ * (the runtime puts BG at $1000, sprites at $0000).
45
+ *
46
+ * BG_DUST — a faint checkerboard "space dust" that tiles seamlessly; the
47
+ * base layer that covers the whole field so it never reads blank.
48
+ * BG_STAR — three small stars (colour 1) sprinkled over the dust.
49
+ * BG_BRITE — a single bright + star (colour 2) for the rare close star. */
50
+ static const uint8_t tile_dust[16] = {
51
+ 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, /* plane 0: checker (idx 1) */
52
+ 0, 0, 0, 0, 0, 0, 0, 0,
53
+ };
54
+ static const uint8_t tile_star[16] = {
55
+ 0x00, 0x08, 0x00, 0x42, 0x00, 0x00, 0x20, 0x01, /* plane 0: four small stars */
56
+ 0, 0, 0, 0, 0, 0, 0, 0,
57
+ };
58
+ static const uint8_t tile_brite[16] = {
59
+ 0x00, 0x00, 0x10, 0x38, 0x10, 0x00, 0x00, 0x00, /* plane 0: + arms (idx ... ) */
60
+ 0x00, 0x00, 0x10, 0x38, 0x10, 0x00, 0x00, 0x00, /* plane 1 set too → colour 3 */
61
+ };
62
+ #define BG_DUST 1 /* BG tile index 1 → uploaded to $1010 */
63
+ #define BG_STAR 2 /* BG tile index 2 → uploaded to $1020 */
64
+ #define BG_BRITE 3 /* BG tile index 3 → uploaded to $1030 */
41
65
 
42
66
  /* ── Palette ─────────────────────────────────────────────────────── */
43
67
  static const uint8_t palette[32] = {
44
- /* BG (unused but PPU reads $3F00 backdrop) */
68
+ /* BG0: backdrop near-black, star colour = dim white */
45
69
  0x0F, 0x10, 0x20, 0x30,
46
70
  0x0F, 0x10, 0x20, 0x30,
47
71
  0x0F, 0x10, 0x20, 0x30,
@@ -126,14 +150,39 @@ void main(void) {
126
150
 
127
151
  ppu_off();
128
152
 
129
- /* Upload tile data — blank in slot 0 + 3 sprite tiles in slots 1..3. */
153
+ /* Upload tile data — blank in slot 0 + 3 sprite tiles in slots 1..3,
154
+ * all in the SPRITE pattern table ($0000). */
130
155
  chr_ram_upload(0x0000, tile_blank, 16);
131
156
  chr_ram_upload(0x0010, tile_ship, 16);
132
157
  chr_ram_upload(0x0020, tile_bullet, 16);
133
158
  chr_ram_upload(0x0030, tile_enemy, 16);
159
+ /* Upload the starfield tiles to the BACKGROUND pattern table
160
+ * ($1010/$1020/$1030 = BG slots 1/2/3). */
161
+ chr_ram_upload(0x1010, tile_dust, 16);
162
+ chr_ram_upload(0x1020, tile_star, 16);
163
+ chr_ram_upload(0x1030, tile_brite, 16);
134
164
 
135
165
  palette_load(palette);
136
166
 
167
+ /* Paint a full starfield directly into the nametable while the PPU is off
168
+ * (vram_unsafe_set = raw write; tile_set's NMI queue would deadlock
169
+ * before ppu_on). Every one of the 32×30 cells gets the faint "dust" base,
170
+ * with small stars sprinkled every few cells and the odd bright star — a
171
+ * deterministic scatter so the backdrop is unambiguously "space", densely
172
+ * filled rather than flat black. */
173
+ {
174
+ uint16_t r, cc;
175
+ uint8_t tile;
176
+ for (r = 0; r < 30; r++) {
177
+ for (cc = 0; cc < 32; cc++) {
178
+ tile = BG_DUST; /* base dust everywhere */
179
+ if (((r * 5 + cc * 3) % 7) == 0) tile = BG_STAR; /* sprinkle stars */
180
+ if (((r * 3 + cc * 7) % 23) == 0) tile = BG_BRITE; /* rare bright one */
181
+ vram_unsafe_set((uint16_t)(0x2000 + r * 32 + cc), tile);
182
+ }
183
+ }
184
+ }
185
+
137
186
  oam_clear();
138
187
  ppu_on_all();
139
188
  sound_init();
@@ -34,6 +34,27 @@ static const uint8_t tile_ball[16] = {
34
34
  0x00, 0x3C, 0x7E, 0x7E, 0x7E, 0x7E, 0x3C, 0x00,
35
35
  0, 0, 0, 0, 0, 0, 0, 0,
36
36
  };
37
+ /* BG court tiles (background pattern table $1000), so the court reads as a
38
+ * real Pong arena on boot instead of sprites on flat black:
39
+ * BG_WALL — solid bar (idx1 white): the top/bottom rails.
40
+ * BG_NET — dashed vertical bar (idx1 white): the centre net.
41
+ * BG_FLOOR — a faint court-floor hatch (idx2 green) tiled across the whole
42
+ * playfield so the arena surface is covered, not black. */
43
+ static const uint8_t tile_wall[16] = {
44
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
45
+ 0, 0, 0, 0, 0, 0, 0, 0,
46
+ };
47
+ static const uint8_t tile_net[16] = {
48
+ 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, /* dashed vertical bar */
49
+ 0, 0, 0, 0, 0, 0, 0, 0,
50
+ };
51
+ static const uint8_t tile_floor[16] = {
52
+ 0, 0, 0, 0, 0, 0, 0, 0, /* plane 0 clear */
53
+ 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, /* plane 1 → colour 2 checker */
54
+ };
55
+ #define BG_WALL 1 /* BG slot 1 → $1010 */
56
+ #define BG_NET 2 /* BG slot 2 → $1020 */
57
+ #define BG_FLOOR 3 /* BG slot 3 → $1030 */
37
58
  /* Digits 0-9 (3 wide × 5 tall, padded to 8×8). Used for the score HUD. */
38
59
  static const uint8_t tile_digits[10 * 16] = {
39
60
  /* 0 */ 0xE0,0xA0,0xA0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
@@ -55,11 +76,12 @@ static const uint8_t tile_digits[10 * 16] = {
55
76
  #define T_DIGIT0 3 /* digits live at slots 3..12 */
56
77
 
57
78
  static const uint8_t palette[32] = {
58
- /* BG palettes only the backdrop matters here */
59
- 0x0F, 0x30, 0x10, 0x00,
60
- 0x0F, 0x30, 0x10, 0x00,
61
- 0x0F, 0x30, 0x10, 0x00,
62
- 0x0F, 0x30, 0x10, 0x00,
79
+ /* BG0: backdrop near-black, court walls/net = white (idx1),
80
+ * court-floor hatch = dark green (idx2) */
81
+ 0x0F, 0x30, 0x1A, 0x00,
82
+ 0x0F, 0x30, 0x1A, 0x00,
83
+ 0x0F, 0x30, 0x1A, 0x00,
84
+ 0x0F, 0x30, 0x1A, 0x00,
63
85
  /* Sprite palettes */
64
86
  0x0F, 0x30, 0x16, 0x12, /* sp0: white paddle */
65
87
  0x0F, 0x30, 0x16, 0x12, /* sp1: white ball */
@@ -103,13 +125,36 @@ void main(void) {
103
125
 
104
126
  ppu_off();
105
127
 
106
- /* Upload tiles. */
128
+ /* Upload sprite tiles (sprite pattern table $0000). */
107
129
  chr_ram_upload(T_BLANK * 16, tile_blank, 16);
108
130
  chr_ram_upload(T_PADDLE * 16, tile_paddle, 16);
109
131
  chr_ram_upload(T_BALL * 16, tile_ball, 16);
110
132
  chr_ram_upload(T_DIGIT0 * 16, tile_digits, sizeof(tile_digits));
133
+ /* Upload court tiles to the BACKGROUND pattern table ($1010..$1030). */
134
+ chr_ram_upload(0x1010, tile_wall, 16);
135
+ chr_ram_upload(0x1020, tile_net, 16);
136
+ chr_ram_upload(0x1030, tile_floor, 16);
111
137
 
112
138
  palette_load(palette);
139
+
140
+ /* Paint the court into the nametable while rendering is off
141
+ * (vram_unsafe_set = raw write). First carpet the whole playfield with the
142
+ * green floor hatch so the arena surface is covered, then lay the top/
143
+ * bottom rails (rows 1 and 27) and the dashed centre net (column 15) on
144
+ * top. Without the floor the screen is just sprites on flat black. */
145
+ {
146
+ uint16_t cc, rr;
147
+ for (rr = 0; rr < 30; rr++)
148
+ for (cc = 0; cc < 32; cc++)
149
+ vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_FLOOR);
150
+ for (cc = 0; cc < 32; cc++) {
151
+ vram_unsafe_set((uint16_t)(0x2000 + 1 * 32 + cc), BG_WALL); /* top rail (y≈8) */
152
+ vram_unsafe_set((uint16_t)(0x2000 + 27 * 32 + cc), BG_WALL); /* bottom rail(y≈216)*/
153
+ }
154
+ for (rr = 2; rr < 27; rr++)
155
+ vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + 15), BG_NET); /* centre net */
156
+ }
157
+
113
158
  oam_clear();
114
159
  ppu_on_all();
115
160
  sound_init();
@@ -0,0 +1,283 @@
1
+ /*
2
+ * PC Engine "platformer" — a side-scrolling platformer scaffold.
3
+ *
4
+ * Run and jump across a world wider than one screen. The d-pad moves left/right,
5
+ * button I jumps; gravity pulls you down and you land on top of solid platforms.
6
+ * The camera follows the player and the background scrolls smoothly via the VDC
7
+ * background X-scroll register (BXR/R7).
8
+ *
9
+ * The PCE BAT (background map) is a 32x32 virtual screen (256px) that WRAPS, so
10
+ * a world wider than 256px needs COLUMN STREAMING: each time the camera crosses
11
+ * an 8px boundary we rewrite the BAT column that is about to scroll into view
12
+ * with the next world column's tiles. This mirrors the SMS platformer scaffold,
13
+ * using BXR instead of SMS R8.
14
+ *
15
+ * PCE notes (see pce_hw.h / MENTAL_MODEL.md):
16
+ * - disp_enable() turns on BG + sprites + the VBlank IRQ (waitvsync needs it).
17
+ * - .bss must be non-empty (pce_video.c's _pce_keep[] covers it).
18
+ * - we set BXR every frame via vdc_set_reg(VDC_BXR, camX) for smooth scroll.
19
+ *
20
+ * cc65 is C89 — declare locals at the top of a block.
21
+ */
22
+ #include <pce.h>
23
+ #include <stdint.h> /* int8_t/int16_t/int32_t for sub-pixel physics + camera */
24
+ #include "pce_hw.h"
25
+
26
+ /* ---- VRAM layout (word addresses) --------------------------------------- */
27
+ #define BAT_VRAM 0x0000 /* 32x32 background map */
28
+ #define SKY_VRAM 0x1000 /* BG tile: sky (solid colour 1) */
29
+ #define WALL_VRAM 0x1010 /* BG tile: platform block (colour 2) */
30
+ #define WALLTOP_VRAM 0x1020 /* BG tile: platform top edge (colour 3 strip) */
31
+ #define PLAYER_VRAM 0x1800 /* 16x16 player */
32
+
33
+ #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
34
+
35
+ /* ---- world -------------------------------------------------------------- */
36
+ #define WORLD_COLS 96 /* 96 cells = 768 px world */
37
+ #define WORLD_W (WORLD_COLS * 8)
38
+ #define SCREEN_W 256
39
+ #define VIS_ROWS 28 /* 224-line display = 28 rows */
40
+
41
+ typedef struct { int16_t x, y, w, h; } Rect;
42
+
43
+ /* Platforms in WORLD pixel coords, spread across the 768px world. */
44
+ static const Rect platforms[] = {
45
+ { 0, 200, 768, 24 }, /* floor spans the world */
46
+ { 48, 168, 56, 8 },
47
+ { 140, 152, 64, 8 },
48
+ { 232, 128, 56, 8 },
49
+ { 96, 112, 40, 8 },
50
+ { 320, 160, 72, 8 },
51
+ { 416, 128, 64, 8 },
52
+ { 360, 88, 48, 8 },
53
+ { 512, 152, 80, 8 },
54
+ { 600, 120, 56, 8 },
55
+ { 672, 168, 72, 8 },
56
+ { 560, 80, 48, 8 }
57
+ };
58
+ #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
59
+
60
+ /* ---- state -------------------------------------------------------------- */
61
+ static int16_t px, py; /* player position in 1/16-px units */
62
+ static int16_t vx, vy;
63
+ static int16_t camX, lastCamCol;
64
+ static u8 pad, prev_pad;
65
+ static u16 spr_buf[64];
66
+ static u16 tile_buf[16];
67
+
68
+ static void make_solid_tile(u16 *t, u8 ci) {
69
+ u8 r;
70
+ u8 p0 = (ci & 1) ? 0xFF : 0x00;
71
+ u8 p1 = (ci & 2) ? 0xFF : 0x00;
72
+ u8 p2 = (ci & 4) ? 0xFF : 0x00;
73
+ u8 p3 = (ci & 8) ? 0xFF : 0x00;
74
+ for (r = 0; r < 8; ++r) {
75
+ t[r] = (u16)(p0 | (p1 << 8));
76
+ t[r + 8] = (u16)(p2 | (p3 << 8));
77
+ }
78
+ }
79
+
80
+ /* platform-top tile: colour 2 body with a colour-3 highlight on the top 2 rows */
81
+ static void make_walltop_tile(u16 *t) {
82
+ make_solid_tile(t, 2);
83
+ /* rows 0,1: set plane0 too so those pixels read colour 3 (planes0+1) */
84
+ t[0] = (u16)(0x00FF | (t[0] & 0xFF00));
85
+ t[1] = (u16)(0x00FF | (t[1] & 0xFF00));
86
+ }
87
+
88
+ static void make_player_sprite(void) {
89
+ static const u16 body[16] = {
90
+ 0x07E0, 0x0FF0, 0x1FF8, 0x1818, 0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE,
91
+ 0x7FFE, 0x7FFE, 0x3FFC, 0x1FF8, 0x0E70, 0x0C30, 0x0C30, 0x1818
92
+ };
93
+ static const u16 eyes[16] = {
94
+ 0x0000, 0x0000, 0x0000, 0x0000, 0x0990, 0x0990, 0x0000, 0x0000,
95
+ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
96
+ };
97
+ u8 r;
98
+ for (r = 0; r < 64; ++r) spr_buf[r] = 0;
99
+ for (r = 0; r < 16; ++r) {
100
+ spr_buf[r] = (u16)(body[r] & ~eyes[r]); /* plane0 -> colour 1 */
101
+ spr_buf[r + 16] = eyes[r]; /* plane1 -> colour 2 */
102
+ }
103
+ load_tiles(PLAYER_VRAM, spr_buf, 64);
104
+ }
105
+
106
+ /* Is world cell (col,row) inside any platform? */
107
+ static u8 cell_is_wall(int16_t col, u8 row) {
108
+ int16_t cx = (int16_t)(col << 3);
109
+ int16_t cy = (int16_t)((int16_t)row << 3);
110
+ u8 i;
111
+ const Rect *p;
112
+ for (i = 0; i < N_PLATFORMS; ++i) {
113
+ p = &platforms[i];
114
+ if (cx + 8 > p->x && cx < p->x + p->w &&
115
+ cy + 8 > p->y && cy < p->y + p->h) return 1;
116
+ }
117
+ return 0;
118
+ }
119
+
120
+ /* Is world cell the TOP row of a platform (for the highlighted edge tile)? */
121
+ static u8 cell_is_top(int16_t col, u8 row) {
122
+ int16_t cy = (int16_t)((int16_t)row << 3);
123
+ int16_t cx = (int16_t)(col << 3);
124
+ u8 i;
125
+ const Rect *p;
126
+ for (i = 0; i < N_PLATFORMS; ++i) {
127
+ p = &platforms[i];
128
+ if (cx + 8 > p->x && cx < p->x + p->w && cy >= p->y && cy < p->y + 8) return 1;
129
+ }
130
+ return 0;
131
+ }
132
+
133
+ /* Write one world column into its wrapped BAT column. */
134
+ static void paint_column(int16_t worldCol) {
135
+ u8 ntCol, row;
136
+ u16 e;
137
+ if (worldCol < 0 || worldCol >= WORLD_COLS) return;
138
+ ntCol = (u8)(worldCol & 31);
139
+ for (row = 0; row < 32; ++row) {
140
+ if (row < VIS_ROWS && cell_is_wall(worldCol, row)) {
141
+ e = cell_is_top(worldCol, row)
142
+ ? BAT_ENTRY(0, WALLTOP_VRAM)
143
+ : BAT_ENTRY(0, WALL_VRAM);
144
+ } else {
145
+ e = BAT_ENTRY(0, SKY_VRAM);
146
+ }
147
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + ntCol));
148
+ VDC_DATA_LO = (u8)(e & 0xFF);
149
+ VDC_DATA_HI = (u8)(e >> 8);
150
+ }
151
+ }
152
+
153
+ static void paint_initial(void) {
154
+ int16_t c;
155
+ for (c = 0; c < 32; ++c) paint_column(c);
156
+ }
157
+
158
+ static u8 on_platform(int16_t ipx, int16_t ipy) {
159
+ u8 i;
160
+ const Rect *p;
161
+ for (i = 0; i < N_PLATFORMS; ++i) {
162
+ p = &platforms[i];
163
+ if (ipy + 16 == p->y && ipx + 12 > p->x && ipx + 4 < p->x + p->w) return 1;
164
+ }
165
+ return 0;
166
+ }
167
+
168
+ void main(void) {
169
+ const int16_t GRAVITY = 10;
170
+ const int16_t MOVE = 22;
171
+ const int16_t JUMP = -200;
172
+ const int16_t MAXFALL = 300;
173
+
174
+ _pce_keep[0] = 0;
175
+
176
+ /* palette */
177
+ vce_set_color(0, PCE_RGB(1, 2, 5)); /* backdrop sky blue */
178
+ vce_set_color(1, PCE_RGB(2, 4, 7)); /* BG c1: sky */
179
+ vce_set_color(2, PCE_RGB(3, 2, 1)); /* BG c2: brown platform */
180
+ vce_set_color(3, PCE_RGB(1, 6, 1)); /* BG c3: green grassy top */
181
+ vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr transparent */
182
+ vce_set_color(257, PCE_RGB(7, 1, 1)); /* spr c1: red body */
183
+ vce_set_color(258, PCE_RGB(7, 7, 7)); /* spr c2: white eyes */
184
+
185
+ make_solid_tile(tile_buf, 1); load_tiles(SKY_VRAM, tile_buf, 16);
186
+ make_solid_tile(tile_buf, 2); load_tiles(WALL_VRAM, tile_buf, 16);
187
+ make_walltop_tile(tile_buf); load_tiles(WALLTOP_VRAM, tile_buf, 16);
188
+ make_player_sprite();
189
+
190
+ paint_initial();
191
+
192
+ px = (int16_t)(24 << 4);
193
+ py = (int16_t)(160 << 4);
194
+ vx = 0; vy = 0;
195
+ camX = 0; lastCamCol = 0;
196
+ prev_pad = 0;
197
+
198
+ set_sprite(0, (u16)(px >> 4), (u16)(py >> 4), PLAYER_VRAM >> 6, 0);
199
+ satb_dma();
200
+
201
+ pce_joy_init();
202
+ disp_enable();
203
+
204
+ for (;;) {
205
+ int16_t ipx, ipy, npy, sx;
206
+ int16_t camCol;
207
+ int32_t np;
208
+ u8 grounded;
209
+ u8 i;
210
+ const Rect *p;
211
+
212
+ waitvsync();
213
+ pad = pce_joy_read();
214
+
215
+ ipx = px >> 4;
216
+ ipy = py >> 4;
217
+
218
+ /* camera follows player, clamped to world */
219
+ camX = (int16_t)(ipx - (SCREEN_W / 2 - 8));
220
+ if (camX < 0) camX = 0;
221
+ if (camX > WORLD_W - SCREEN_W) camX = (int16_t)(WORLD_W - SCREEN_W);
222
+
223
+ /* stream columns entering from the edges */
224
+ camCol = camX >> 3;
225
+ while (camCol > lastCamCol) { lastCamCol++; paint_column((int16_t)(lastCamCol + 31)); }
226
+ while (camCol < lastCamCol) { lastCamCol--; paint_column(lastCamCol); }
227
+
228
+ /* smooth pixel scroll via BG X register */
229
+ vdc_set_reg(VDC_BXR, (u16)camX);
230
+
231
+ /* horizontal move */
232
+ vx = 0;
233
+ if (pad & PCE_JOY_LEFT) vx = (int16_t)(-MOVE);
234
+ if (pad & PCE_JOY_RIGHT) vx = MOVE;
235
+
236
+ grounded = on_platform(ipx, ipy);
237
+ if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I) && grounded) {
238
+ vy = JUMP;
239
+ psg_tone(0, 0x200, 24);
240
+ }
241
+ prev_pad = pad;
242
+
243
+ vy = (int16_t)(vy + GRAVITY);
244
+ if (vy > MAXFALL) vy = MAXFALL;
245
+ if (grounded && vy > 0) vy = 0;
246
+
247
+ /* horizontal integrate + clamp */
248
+ px = (int16_t)(px + vx);
249
+ if (px < 0) px = 0;
250
+ if (px > ((WORLD_W - 16) << 4)) px = (int16_t)((WORLD_W - 16) << 4);
251
+
252
+ /* vertical integrate with land-on-top */
253
+ np = (int32_t)py + (int32_t)vy;
254
+ npy = (int16_t)(np >> 4);
255
+ if (vy > 0) {
256
+ u8 landed = 0;
257
+ for (i = 0; i < N_PLATFORMS; ++i) {
258
+ p = &platforms[i];
259
+ if (ipy + 16 <= p->y && npy + 16 >= p->y &&
260
+ ipx + 12 > p->x && ipx + 4 < p->x + p->w) {
261
+ py = (int16_t)((p->y - 16) << 4);
262
+ vy = 0;
263
+ landed = 1;
264
+ break;
265
+ }
266
+ }
267
+ if (!landed) py = (int16_t)np;
268
+ } else {
269
+ py = (int16_t)np;
270
+ }
271
+ if (py > (224 << 4)) { px = (int16_t)(24 << 4); py = (int16_t)(160 << 4); vy = 0; }
272
+
273
+ /* free the jump SFX channel after it rings */
274
+ if (vy == 0) psg_off(0);
275
+
276
+ /* draw player in screen space */
277
+ sx = (int16_t)((px >> 4) - camX);
278
+ if (sx < 0) sx = 0;
279
+ if (sx > 240) sx = 240;
280
+ set_sprite(0, (u16)sx, (u16)(py >> 4), PLAYER_VRAM >> 6, 0);
281
+ satb_dma();
282
+ }
283
+ }