romdevtools 0.21.0 → 0.22.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 (127) hide show
  1. package/AGENTS.md +15 -4
  2. package/CHANGELOG.md +58 -0
  3. package/examples/atari7800/templates/hello_sprite.c +48 -4
  4. package/examples/atari7800/templates/music_demo.c +47 -2
  5. package/examples/c64/templates/tile_engine.c +77 -27
  6. package/examples/gb/templates/hello_sprite.c +15 -6
  7. package/examples/gb/templates/music_demo.c +36 -0
  8. package/examples/gb/templates/platformer.c +3 -2
  9. package/examples/gb/templates/puzzle.c +3 -2
  10. package/examples/gb/templates/racing.c +3 -2
  11. package/examples/gb/templates/shmup.c +3 -2
  12. package/examples/gb/templates/sports.c +3 -2
  13. package/examples/gb/templates/tile_engine.c +3 -2
  14. package/examples/gba/templates/maxmod_demo.c +36 -2
  15. package/examples/gba/templates/platformer.c +3 -1
  16. package/examples/gba/templates/tonc_hello_sprite.c +35 -1
  17. package/examples/gbc/templates/hello_sprite.c +12 -3
  18. package/examples/gbc/templates/music_demo.c +56 -12
  19. package/examples/gbc/templates/platformer.c +3 -2
  20. package/examples/gbc/templates/puzzle.c +3 -2
  21. package/examples/gbc/templates/racing.c +3 -2
  22. package/examples/gbc/templates/shmup.c +3 -2
  23. package/examples/gbc/templates/sports.c +3 -2
  24. package/examples/gbc/templates/tile_engine.c +3 -2
  25. package/examples/genesis/main.s +53 -1
  26. package/examples/genesis/templates/hello_sprite.c +25 -3
  27. package/examples/genesis/templates/shmup_2p.c +31 -0
  28. package/examples/genesis/templates/xgm2_demo.c +20 -0
  29. package/examples/gg/templates/hello_sprite.c +25 -2
  30. package/examples/gg/templates/music_demo.c +24 -2
  31. package/examples/gg/templates/racing.c +7 -4
  32. package/examples/gg/templates/sports.c +11 -13
  33. package/examples/gg/templates/tile_engine.c +12 -6
  34. package/examples/lynx/templates/hello_sprite.c +15 -1
  35. package/examples/lynx/templates/music_demo.c +13 -1
  36. package/examples/nes/templates/hello_sprite.c +35 -0
  37. package/examples/nes/templates/music_demo.c +40 -0
  38. package/examples/pce/catch_game/main.c +22 -3
  39. package/examples/pce/music_sfx/main.c +28 -1
  40. package/examples/pce/sprite_move/main.c +7 -2
  41. package/examples/sms/templates/hello_sprite.c +29 -3
  42. package/examples/sms/templates/music_demo.c +18 -4
  43. package/examples/sms/templates/shmup_2p.c +24 -1
  44. package/examples/sms/templates/sports.c +4 -2
  45. package/examples/snes/main.asm +108 -17
  46. package/examples/snes/templates/c-hello-data.asm +23 -0
  47. package/examples/snes/templates/c-hello.c +18 -1
  48. package/examples/snes/templates/hello_sprite-data.asm +23 -0
  49. package/examples/snes/templates/hello_sprite.c +17 -1
  50. package/examples/snes/templates/music_demo-data.asm +23 -0
  51. package/examples/snes/templates/music_demo.c +22 -4
  52. package/examples/snes/templates/platformer.c +4 -1
  53. package/examples/snes/templates/puzzle.c +4 -1
  54. package/package.json +1 -1
  55. package/src/cheats/gamegenie.js +0 -1
  56. package/src/cli/smoke.js +1 -3
  57. package/src/host/LibretroHost.js +69 -15
  58. package/src/host/chafa-render.js +2 -0
  59. package/src/host/dsp-state.js +2 -2
  60. package/src/host/gpgx-state.js +4 -0
  61. package/src/http/routes.js +1 -1
  62. package/src/mcp/server.js +1 -1
  63. package/src/mcp/state.js +36 -0
  64. package/src/mcp/tools/address-to-symbol.js +0 -1
  65. package/src/mcp/tools/art-loaders.js +1 -1
  66. package/src/mcp/tools/cart-parts.js +0 -1
  67. package/src/mcp/tools/classify-region.js +1 -1
  68. package/src/mcp/tools/diff-roms.js +1 -1
  69. package/src/mcp/tools/disasm-rebuild.js +1 -1
  70. package/src/mcp/tools/disasm.js +2 -3
  71. package/src/mcp/tools/find-references.js +1 -2
  72. package/src/mcp/tools/font-map.js +1 -1
  73. package/src/mcp/tools/index.js +0 -49
  74. package/src/mcp/tools/input-layout.js +0 -1
  75. package/src/mcp/tools/input.js +33 -3
  76. package/src/mcp/tools/lifecycle.js +14 -2
  77. package/src/mcp/tools/lospec.js +0 -19
  78. package/src/mcp/tools/platform-docs.js +1 -1
  79. package/src/mcp/tools/platform-tools.js +4 -4
  80. package/src/mcp/tools/project.js +0 -2
  81. package/src/mcp/tools/reinject.js +0 -1
  82. package/src/mcp/tools/rom-id.js +2 -2
  83. package/src/mcp/tools/snippets.js +2 -2
  84. package/src/mcp/tools/sprite-pipeline.js +1 -2
  85. package/src/mcp/tools/tile-inspect.js +1 -1
  86. package/src/mcp/tools/toolchain.js +29 -9
  87. package/src/mcp/tools/watch-memory.js +13 -3
  88. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
  89. package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
  90. package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
  91. package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
  92. package/src/platforms/c64/d64.js +0 -1
  93. package/src/platforms/c64/sid.js +0 -2
  94. package/src/platforms/common/metasprite-adapters.js +1 -1
  95. package/src/platforms/common/metasprite-codegen.js +3 -3
  96. package/src/platforms/common/registers.js +5 -3
  97. package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
  98. package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
  99. package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
  100. package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
  101. package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
  102. package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
  103. package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
  104. package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
  105. package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
  106. package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
  107. package/src/platforms/nes/image-to-tilemap.js +3 -0
  108. package/src/platforms/nes/lib/asm/famitone2.s +5 -1
  109. package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
  110. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  111. package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
  112. package/src/platforms/snes/brr.js +0 -2
  113. package/src/playtest/playtest.js +0 -7
  114. package/src/toolchains/asar/asar.js +0 -9
  115. package/src/toolchains/assemble-snippet.js +30 -12
  116. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
  117. package/src/toolchains/common/reassemble.js +0 -1
  118. package/src/toolchains/common/sdk-cache.js +1 -1
  119. package/src/toolchains/genesis-c/genesis-c.js +5 -3
  120. package/src/toolchains/index.js +27 -3
  121. package/src/toolchains/parse-errors.js +78 -1
  122. package/src/toolchains/sdcc/preflight-lint.js +5 -1
  123. package/src/toolchains/sdcc/sdcc.js +1 -1
  124. package/src/toolchains/sjasm/sjasm.js +1 -1
  125. package/src/toolchains/snes-c/snes-c.js +2 -2
  126. package/src/toolchains/vasm68k/vasm68k.js +2 -4
  127. package/src/toolchains/wladx/wladx.js +1 -1
@@ -33,10 +33,13 @@ extern void gg_sat_upload(void);
33
33
  #define VIS_X1 207 /* 48 + 160 - 1 */
34
34
  #define VIS_Y1 167 /* 24 + 144 - 1 */
35
35
 
36
- #define LANE_LEFT_X (VIS_X0 + 28) /* 76 */
37
- #define LANE_MID_X ((VIS_X0 + VIS_X1) / 2 - 4) /* ~123 */
38
- #define LANE_RIGHT_X (VIS_X1 - 36) /* 171 */
39
- #define PLAYER_Y (VIS_Y1 - 16)
36
+ /* Explicit (uint8_t) casts: the computed int expressions all fit in a byte,
37
+ * but SDCC warns (158) on the implicit int->uint8_t narrowing in the const
38
+ * initializers below unless the conversion is spelled out. */
39
+ #define LANE_LEFT_X ((uint8_t)(VIS_X0 + 28)) /* 76 */
40
+ #define LANE_MID_X ((uint8_t)((VIS_X0 + VIS_X1) / 2 - 4)) /* ~123 */
41
+ #define LANE_RIGHT_X ((uint8_t)(VIS_X1 - 36)) /* 171 */
42
+ #define PLAYER_Y ((uint8_t)(VIS_Y1 - 16))
40
43
  #define MAX_OBSTACLES 4
41
44
 
42
45
  /* GG palette = 32 entries × 2 bytes (4-4-4 BGR LE): low=(g<<4)|r, high=b.
@@ -30,8 +30,11 @@ extern void gg_sat_upload(void);
30
30
  #define COURT_BOT VIS_Y1
31
31
  #define PADDLE_H 24
32
32
  #define BALL_SIZE 8
33
- #define PADDLE_X1 (VIS_X0 + 8) /* near the visible left edge */
34
- #define PADDLE_X2 (VIS_X1 - 16) /* near the visible right edge */
33
+ /* Explicit (uint8_t) casts: these fit a byte but SDCC warns (158) on the
34
+ * implicit int->uint8_t narrowing when the computed macro is passed to the
35
+ * uint8_t x/y args of gg_sprite_set. */
36
+ #define PADDLE_X1 ((uint8_t)(VIS_X0 + 8)) /* near the visible left edge */
37
+ #define PADDLE_X2 ((uint8_t)(VIS_X1 - 16)) /* near the visible right edge */
35
38
 
36
39
  /* GG palette = 32 entries × 2 bytes (4-4-4 BGR LE): low=(g<<4)|r, high=b.
37
40
  * gg_load_palette reads 64 bytes; a 32-byte array leaves the sprite palette
@@ -127,8 +130,9 @@ void main(void) {
127
130
  reset_match();
128
131
 
129
132
  do {
130
- uint8_t p1, p2;
133
+ uint8_t p1;
131
134
  uint8_t slot;
135
+ int16_t target;
132
136
  gg_vblank_wait();
133
137
  sfx_update();
134
138
 
@@ -145,20 +149,14 @@ void main(void) {
145
149
  gg_sat_upload();
146
150
 
147
151
  p1 = gg_joypad_read();
148
- p2 = 0; /* GG has only one controller — always AI for the right paddle */
149
152
 
150
153
  if ((p1 & JOY_UP) && p1y > COURT_TOP) p1y -= 2;
151
154
  if ((p1 & JOY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
152
155
 
153
- /* P2 input if any, otherwise AI. */
154
- if (p2 != 0) {
155
- if ((p2 & JOY_UP) && p2y > COURT_TOP) p2y -= 2;
156
- if ((p2 & JOY_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 2;
157
- } else {
158
- int16_t target = by - PADDLE_H / 2;
159
- if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
160
- else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
161
- }
156
+ /* GG has only one controller — the right paddle is always AI. */
157
+ target = by - PADDLE_H / 2;
158
+ if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
159
+ else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
162
160
 
163
161
  if (serve_timer > 0) {
164
162
  serve_timer--;
@@ -46,8 +46,9 @@ extern void gg_sat_upload(void);
46
46
  * (entries 16-31) reading garbage = invisible sprites. BG colour 1 = entry 1
47
47
  * (dark grey wall); sprite colour 1 = entry 17 (white player). */
48
48
  static const uint8_t palette[64] = {
49
- /* BG 0-15: entry 0 = dark navy backdrop, entry 1 = dark grey wall */
50
- 0x20,0x02, 0x66,0x06, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
49
+ /* BG 0-15: 0 = dark navy backdrop, 1 = grey wall, 2 = teal floor,
50
+ * 3 = blue floor (the two floor-dither tones). */
51
+ 0x20,0x02, 0x66,0x06, 0xC8,0x08, 0x80,0x0C, 0,0, 0,0, 0,0, 0,0,
51
52
  0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
52
53
  /* SPRITE 16-31: 16=transparent, 17=white player */
53
54
  0,0, 0xFF,0x0F, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
@@ -55,10 +56,15 @@ static const uint8_t palette[64] = {
55
56
  };
56
57
 
57
58
  static const uint8_t bg_tiles[32 * 2] = {
58
- /* T_OPEN — blank */
59
- 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
60
- 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
61
- /* T_WALL bordered block */
59
+ /* T_OPEN — dithered floor: plane1=0xFF (colour-2 bit always on), plane0
60
+ * alternates 0xAA/0x55 so pixels flip between colour 2 (teal) and colour 3
61
+ * (blue). The open floor now fills with TWO tones instead of the backdrop,
62
+ * so the screen never reads as a single flat colour. */
63
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
64
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
65
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
66
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
67
+ /* T_WALL — bordered block (colour 1, grey) */
62
68
  0xFF,0x00,0x00,0x00, 0x81,0x00,0x00,0x00,
63
69
  0x81,0x00,0x00,0x00, 0x81,0x00,0x00,0x00,
64
70
  0x81,0x00,0x00,0x00, 0x81,0x00,0x00,0x00,
@@ -22,7 +22,21 @@ void main(void) {
22
22
  sfx_tone(0, 80, 12); /* boot chime */
23
23
 
24
24
  for (;;) {
25
- tgi_clear();
25
+ /* CANONICAL LYNX FRAME LOOP — full redraw every frame:
26
+ * 1. WAIT for Suzy's blitter to finish the previous frame. Drawing
27
+ * while it's mid-flight loses the frame → black screen. This is
28
+ * the #1 "Lynx stays blank" trap (tgi_clear alone leaves the back
29
+ * page stale on this core, so we clear with a full-screen bar). */
30
+ while (tgi_busy()) { }
31
+
32
+ /* Blue field so the screen is obviously not blank... */
33
+ tgi_setcolor(COLOR_BLUE);
34
+ tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy());
35
+ /* ...with a green band so no single colour fills the whole screen. */
36
+ tgi_setcolor(COLOR_GREEN);
37
+ tgi_bar(0, 60, tgi_getmaxx(), 102);
38
+
39
+ /* The joystick-driven player square on top. */
26
40
  tgi_setcolor(COLOR_YELLOW);
27
41
  tgi_bar(x, y, x + 8, y + 8);
28
42
  tgi_setcolor(COLOR_WHITE);
@@ -26,7 +26,19 @@ void main(void) {
26
26
  lynx_snd_play(0, (unsigned char *)demo_music);
27
27
 
28
28
  for (;;) {
29
- tgi_clear();
29
+ /* CANONICAL LYNX FRAME LOOP — full redraw every frame: WAIT for Suzy's
30
+ * blitter (drawing mid-flight loses the frame → black), then clear with
31
+ * a full-screen bar (tgi_clear leaves the back page stale on this core)
32
+ * before drawing. The #1 "Lynx stays blank" trap. */
33
+ while (tgi_busy()) { }
34
+
35
+ /* Two colour bands so the backdrop is obviously not blank and no single
36
+ * colour fills the whole screen (a flat fill still reads as "blank"). */
37
+ tgi_setcolor(COLOR_BLUE);
38
+ tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy());
39
+ tgi_setcolor(COLOR_PURPLE);
40
+ tgi_bar(0, 56, tgi_getmaxx(), 102);
41
+
30
42
  tgi_setcolor(COLOR_WHITE);
31
43
  tgi_outtextxy(20, 20, "LYNX MUSIC DEMO");
32
44
  tgi_setcolor(COLOR_YELLOW);
@@ -32,6 +32,21 @@ static const uint8_t tile_data[16] = {
32
32
  0, 0, 0, 0, 0, 0, 0, 0,
33
33
  };
34
34
 
35
+ /* Two BG tiles so the backdrop isn't a single flat colour (a uniform
36
+ * screen reads >=92% one colour and fails the blank-screen check):
37
+ * tile 1 — solid colour 1
38
+ * tile 2 — solid colour 2
39
+ * Checkerboarded across the nametable below. BG fetches from $1000-$1FFF
40
+ * under the default PPUCTRL, so these upload to the BG pattern table. */
41
+ static const uint8_t bg_tiles[2 * 16] = {
42
+ /* tile 1: solid colour 1 */
43
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
44
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
45
+ /* tile 2: solid colour 2 */
46
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
47
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
48
+ };
49
+
35
50
  /* 32-byte palette: 4 BG palettes + 4 sprite palettes.
36
51
  * BG index 0 ($3F00) is the universal backdrop.
37
52
  * SPR index 0 ($3F10) is transparent — always; the value is written
@@ -49,6 +64,21 @@ static const uint8_t palette[32] = {
49
64
  0x0F, 0x2A, 0x1A, 0x0A,
50
65
  };
51
66
 
67
+ /* Fill nametable 0 ($2000) with a checkerboard of BG tiles 1 and 2 so the
68
+ * screen behind the sprite is visibly NOT blank. Attribute table stays at
69
+ * palette 0. Caller must have the PPU off. */
70
+ static void fill_bg(void) {
71
+ uint16_t addr;
72
+ uint8_t y, x;
73
+ for (y = 0; y < 30; y++) {
74
+ addr = (uint16_t)(0x2000 + (uint16_t)y * 32);
75
+ for (x = 0; x < 32; x++) {
76
+ vram_unsafe_set(addr, (uint8_t)(((x ^ y) & 1) + 1));
77
+ ++addr;
78
+ }
79
+ }
80
+ }
81
+
52
82
  void main(void) {
53
83
  uint8_t px = 124; /* mid-screen X */
54
84
  uint8_t py = 110; /* mid-screen Y */
@@ -64,6 +94,11 @@ void main(void) {
64
94
  * at $0000-$0FFF since the default PPUCTRL has sprite_pattern=0. */
65
95
  chr_ram_upload(0x0010, tile_data, 16);
66
96
 
97
+ /* ── 2b. Upload BG tiles to the BG pattern table at $1000 and paint
98
+ * a checkerboard backdrop so the sprite isn't alone on a blank field. */
99
+ chr_ram_upload(0x1000, bg_tiles, sizeof(bg_tiles));
100
+ fill_bg();
101
+
67
102
  /* ── 3. Load palette ─────────────────────────────────────────── */
68
103
  palette_load(palette);
69
104
 
@@ -43,6 +43,37 @@ extern const unsigned char music_data[];
43
43
 
44
44
  static const unsigned char bg_colors[4] = { 0x0F, 0x01, 0x21, 0x31 };
45
45
 
46
+ /* Two BG tiles so the backdrop isn't a single flat colour (a uniform
47
+ * screen reads >=92% one colour and fails the blank-screen check):
48
+ * tile 1 — solid colour 1
49
+ * tile 2 — solid colour 2
50
+ * We checkerboard them across the nametable below. NES BG fetches from
51
+ * $1000-$1FFF under the default PPUCTRL (bit 4 set), so BG tiles upload
52
+ * there. */
53
+ static const unsigned char bg_tiles[2 * 16] = {
54
+ /* tile 1: solid colour 1 (plane 0 all set, plane 1 clear) */
55
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
56
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
57
+ /* tile 2: solid colour 2 (plane 1 all set, plane 0 clear) */
58
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
59
+ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
60
+ };
61
+
62
+ /* Fill nametable 0 ($2000) with a checkerboard of tiles 1 and 2 so the
63
+ * screen shows two distinct colours. Attribute table left at palette 0.
64
+ * Caller must have the PPU off. */
65
+ static void fill_bg(void) {
66
+ unsigned int addr;
67
+ unsigned char y, x;
68
+ for (y = 0; y < 30; y++) {
69
+ addr = 0x2000 + (unsigned int)y * 32;
70
+ for (x = 0; x < 32; x++) {
71
+ vram_unsafe_set(addr, (unsigned char)(((x ^ y) & 1) + 1));
72
+ ++addr;
73
+ }
74
+ }
75
+ }
76
+
46
77
  void main(void) {
47
78
  unsigned char palette[32];
48
79
  unsigned char i;
@@ -50,9 +81,18 @@ void main(void) {
50
81
  unsigned char frame = 0;
51
82
 
52
83
  for (i = 0; i < 32; i++) palette[i] = bg_colors[0];
84
+ /* BG palette 0: backdrop black, colour 1 blue, colour 2 red — gives the
85
+ * checkerboard two visibly different cells. */
86
+ palette[0] = 0x0F; /* $3F00 backdrop */
87
+ palette[1] = 0x11; /* colour 1 — blue */
88
+ palette[2] = 0x16; /* colour 2 — red */
53
89
 
54
90
  ppu_off();
55
91
  palette_load(palette);
92
+ /* Upload the two BG tiles to the BG pattern table at $1000. */
93
+ chr_ram_upload(0x1000, bg_tiles, sizeof(bg_tiles));
94
+ /* Paint the checkerboard backdrop so the screen is visibly NOT blank. */
95
+ fill_bg();
56
96
  oam_clear();
57
97
 
58
98
  /* Start the music BEFORE rendering — FamiToneInit takes a few
@@ -27,6 +27,8 @@
27
27
  #define BAT_VRAM 0x0000 /* 32x32 background map */
28
28
  #define FONT_VRAM 0x1000 /* digit + glyph tiles (8x8, 16 words each) */
29
29
  #define BLANK_VRAM 0x1000 /* tile 0 of the font = blank */
30
+ #define FIELD_VRAM 0x1700 /* dim "field" BG tile (16 words), drawn behind
31
+ * the HUD so the playfield isn't a blank void */
30
32
  #define CATCHER_VRAM 0x1800 /* catcher sprite cell (16x16 = 64 words) */
31
33
  #define FRUIT_VRAM 0x1840 /* fruit sprite cell (16x16 = 64 words) */
32
34
 
@@ -146,15 +148,30 @@ static void upload_sprites(void) {
146
148
  load_tiles(FRUIT_VRAM, fruit_cell, 64);
147
149
  }
148
150
 
149
- /* ---- clear the whole BAT to blank tiles --------------------------------- */
151
+ /* ---- build the dim "field" BG tile -------------------------------------- */
152
+ /* A solid 8x8 tile in BG colour index 2 (the dim field blue). Filling the BAT
153
+ * with this instead of blank gives the playfield a visible background — an
154
+ * all-blank BAT reads as a near-empty backdrop (one colour > 92% of the screen,
155
+ * which looks blank to a human). plane1 set = colour index 2. */
156
+ static void upload_field(void) {
157
+ u16 i;
158
+ for (i = 0; i < 16; ++i) font_cell[i] = 0;
159
+ for (i = 0; i < 8; ++i) font_cell[i] = 0xFF00; /* plane1 (hi byte) = ci 2 */
160
+ load_tiles(FIELD_VRAM, font_cell, 16);
161
+ }
162
+
163
+ /* ---- fill the whole BAT with the field tile (a 2x2-cell checkerboard of
164
+ * field/blank), behind the HUD glyphs which are written on top afterwards ---- */
150
165
  static void clear_bat(void) {
151
166
  u16 r, c;
167
+ u16 field = BAT_ENTRY(0, FIELD_VRAM);
152
168
  u16 blank = BAT_ENTRY(0, BLANK_VRAM);
153
169
  for (r = 0; r < 32; ++r) {
154
170
  vram_set_write_addr((u16)(BAT_VRAM + r * 32));
155
171
  for (c = 0; c < 32; ++c) {
156
- VDC_DATA_LO = (u8)(blank & 0xFF);
157
- VDC_DATA_HI = (u8)(blank >> 8);
172
+ u16 e = (((r >> 1) ^ (c >> 1)) & 1) ? blank : field;
173
+ VDC_DATA_LO = (u8)(e & 0xFF);
174
+ VDC_DATA_HI = (u8)(e >> 8);
158
175
  }
159
176
  }
160
177
  }
@@ -229,6 +246,7 @@ void main(void) {
229
246
  /* palette: backdrop, BG text colours, sprite colours */
230
247
  vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop: near-black blue */
231
248
  vce_set_color(1, PCE_RGB(7, 7, 7)); /* BG colour 1: white text */
249
+ vce_set_color(2, PCE_RGB(1, 1, 3)); /* BG colour 2: dim field blue */
232
250
  vce_set_color(256, PCE_RGB(0, 0, 0)); /* sprite transparent */
233
251
  vce_set_color(257, PCE_RGB(2, 5, 7)); /* sprite c1: cyan (catcher 0) */
234
252
  vce_set_color(259, PCE_RGB(2, 5, 7)); /* sprite c3: cyan (catcher) */
@@ -238,6 +256,7 @@ void main(void) {
238
256
 
239
257
  upload_font();
240
258
  upload_sprites();
259
+ upload_field(); /* dim field tile for a visible playfield BG */
241
260
 
242
261
  clear_bat();
243
262
  draw_hud_labels();
@@ -76,6 +76,32 @@ static void draw_bar(u8 active) {
76
76
  }
77
77
  }
78
78
 
79
+ /* Paint a decorative full-screen frame so the demo reads as a real UI panel
80
+ * instead of a near-empty backdrop. conio only inits a backdrop + font, so an
81
+ * otherwise text-only screen leaves >92% of pixels one colour, which looks
82
+ * blank to a human. We fill the top/bottom title bands and both side borders
83
+ * with a block character (the labels are drawn ON TOP afterwards). PCE conio is
84
+ * 32 cols x 28 rows. */
85
+ #define SCR_COLS 32
86
+ #define SCR_ROWS 28
87
+ static void draw_frame(void) {
88
+ u8 x, y;
89
+ /* solid top band (rows 0-1) and bottom band (rows 26-27) */
90
+ for (y = 0; y < SCR_ROWS; ++y) {
91
+ if (y < 2 || y >= SCR_ROWS - 2) {
92
+ for (x = 0; x < SCR_COLS; ++x) cputcxy(x, y, '#');
93
+ } else {
94
+ /* left + right vertical borders (two columns each for weight) */
95
+ cputcxy(0, y, '#');
96
+ cputcxy(1, y, '#');
97
+ cputcxy((u8)(SCR_COLS - 2), y, '#');
98
+ cputcxy((u8)(SCR_COLS - 1), y, '#');
99
+ }
100
+ }
101
+ /* a mid separator bar under the title so the panel has visible structure */
102
+ for (x = 2; x < SCR_COLS - 2; ++x) cputcxy(x, 5, '=');
103
+ }
104
+
79
105
  void main(void) {
80
106
  u8 pad, prev_pad;
81
107
  u8 step; /* current melody step 0..7 */
@@ -86,8 +112,9 @@ void main(void) {
86
112
 
87
113
  _keep[0] = 0;
88
114
 
89
- /* conio: clear + enable display, then paint the static labels. */
115
+ /* conio: clear + enable display, paint the frame, then the static labels. */
90
116
  clrscr();
117
+ draw_frame(); /* visible bordered panel (not a blank backdrop) */
91
118
  cputsxy(8, 8, "PC ENGINE MUSIC + SFX");
92
119
  cputsxy(8, 11, "MELODY:");
93
120
  cputsxy(8, BAR_Y - 1, "STEP:");
@@ -90,7 +90,10 @@ static void make_sprite(void) {
90
90
  }
91
91
 
92
92
  /* Draw a 32x32-cell checkerboard of TILE_A / TILE_B across the BAT. The default
93
- * PCE virtual screen is 32x32 cells (256x256 px), which covers the display. */
93
+ * PCE virtual screen is 32x32 cells (256x256 px), which covers the display.
94
+ * A two-colour checkerboard (green + dark teal) makes the whole playfield read
95
+ * as a real, visible background — a SOLID single-colour fill instead looks blank
96
+ * to a human (one colour covers >92% of the screen), so we alternate by cell. */
94
97
  static void fill_bat(void) {
95
98
  u16 ea = BAT_ENTRY(TILE_A_VRAM, 0);
96
99
  u16 eb = BAT_ENTRY(TILE_B_VRAM, 0);
@@ -98,7 +101,9 @@ static void fill_bat(void) {
98
101
  for (r = 0; r < 32; ++r) {
99
102
  vram_set_write_addr((u16)(BAT_VRAM + r * 32));
100
103
  for (col = 0; col < 32; ++col) {
101
- e = ea; /* solid background so the sprite stands out */
104
+ /* 2x2-cell checkerboard: alternates green/teal so the background is
105
+ * unmistakably present while the sprite still stands out clearly. */
106
+ e = (((r >> 1) ^ (col >> 1)) & 1) ? eb : ea;
102
107
  VDC_DATA_LO = (u8)(e & 0xFF);
103
108
  VDC_DATA_HI = (u8)(e >> 8);
104
109
  }
@@ -28,22 +28,37 @@ extern void sms_vdp_write_reg(uint8_t reg, uint8_t value);
28
28
  extern void sms_vdp_set_addr(uint16_t addr, uint8_t prefix);
29
29
  extern void sms_load_palette(const uint8_t *palette);
30
30
  extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
31
+ extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
31
32
  extern void sms_vblank_wait(void);
32
33
  extern uint8_t sms_joypad_read(void);
33
34
  extern void sms_sprite_init(void);
34
35
  extern void sms_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
35
36
  extern void sms_sat_upload(void);
36
37
 
37
- /* BG palette: backdrop blue + yellow. Sprite palette (entries 16-31)
38
- * we set white at index 17 so our sprite is visible.
38
+ /* BG palette: backdrop blue, colour 1 = teal, colour 2 = navy (the two
39
+ * tones of the dithered BG). Sprite palette (entries 16-31) sets white at
40
+ * index 17 so our sprite is visible.
39
41
  * SMS CRAM is 2-2-2 BGR: 0x00=black, 0x3F=white. */
40
42
  static const uint8_t palette[32] = {
41
- 0x10,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
43
+ 0x10,0x38,0x20,0x00, 0x00,0x00,0x00,0x00,
42
44
  0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
43
45
  0x00,0x3F,0x00,0x00, 0x00,0x00,0x00,0x00,
44
46
  0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
45
47
  };
46
48
 
49
+ /* Two BG tiles in the BG bank at $0000. Tile 0 is a dithered checkerboard
50
+ * (plane0/plane1 alternate per row) so the whole BG fills with TWO colours
51
+ * and no single colour dominates the frame — a flat one-colour fill still
52
+ * reads as a blank screen. */
53
+ static const uint8_t bg_tiles[32 * 1] = {
54
+ /* T_BG — dither: plane0=0xAA→colour 1, plane1=0x55→colour 2, swapped
55
+ * each row so it reads as a fine checkerboard. */
56
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
57
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
58
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
59
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
60
+ };
61
+
47
62
  /* One 8×8 sprite tile (4bpp interleaved). Filled square in color 1. */
48
63
  static const uint8_t sprite_tile[32] = {
49
64
  0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
@@ -52,6 +67,14 @@ static const uint8_t sprite_tile[32] = {
52
67
  0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
53
68
  };
54
69
 
70
+ /* Fill the whole 32×28 name table with the dithered BG tile. */
71
+ static void draw_bg(void) {
72
+ uint8_t row, col;
73
+ for (row = 0; row < 28; row++)
74
+ for (col = 0; col < 32; col++)
75
+ sms_set_tilemap_cell(row, col, 0, 0);
76
+ }
77
+
55
78
  void main(void) {
56
79
  uint8_t x = 124; /* mid-screen X */
57
80
  uint8_t y = 88; /* mid-screen Y */
@@ -59,6 +82,9 @@ void main(void) {
59
82
 
60
83
  sms_vdp_init();
61
84
  sms_load_palette(palette);
85
+ /* BG dither tile → BG bank $0000, then paint the whole name table. */
86
+ sms_load_tiles(0x0000, bg_tiles, 32);
87
+ draw_bg();
62
88
  /* Upload one sprite tile to VRAM $2000 (sprite tile area). */
63
89
  sms_load_tiles(0x2000, sprite_tile, 32);
64
90
 
@@ -29,10 +29,11 @@ extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte
29
29
  extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
30
30
  extern void sms_vblank_wait(void);
31
31
 
32
- /* BG palette: backdrop dark blue, fg colour 1 = bright cyan, colour 2 = yellow.
33
- * SMS CRAM is 2-2-2 BGR — 0x30 = bright blue, 0x3F = white, 0x0F = yellow. */
32
+ /* BG palette: backdrop dark blue, fg colour 1 = bright cyan, colour 2 = yellow,
33
+ * colour 3 = navy (the second dither tone behind the text).
34
+ * SMS CRAM is 2-2-2 BGR — 0x20 = blue, 0x3F = white/cyan, 0x0F = yellow. */
34
35
  static const uint8_t palette[32] = {
35
- 0x20, 0x3F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00,
36
+ 0x20, 0x3F, 0x0F, 0x28, 0x00, 0x00, 0x00, 0x00,
36
37
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
37
38
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
38
39
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -100,8 +101,19 @@ static const uint8_t font_tiles[] = {
100
101
  0xC3,0x00,0x00,0x00, 0xC3,0x00,0x00,0x00,
101
102
  0xC3,0x00,0x00,0x00, 0xC3,0x00,0x00,0x00,
102
103
  0x66,0x00,0x00,0x00, 0x3C,0x00,0x00,0x00,
104
+
105
+ /* tile 9 — dithered BG. plane1=0xFF (colour-2 bit always on), plane0
106
+ * alternates 0xAA/0x55 so pixels flip between colour 2 (yellow) and
107
+ * colour 3 (navy). Fills the whole field with TWO tones so no single
108
+ * colour dominates, while the cyan (colour 1) text stays distinct. */
109
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
110
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
111
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
112
+ 0xAA,0xFF,0x00,0x00, 0x55,0xFF,0x00,0x00,
103
113
  };
104
114
 
115
+ #define T_DITHER 9
116
+
105
117
  /* Tile indices for each char in our message — 'S' 'M' 'S' ' ' 'M' 'U'
106
118
  * 'S' 'I' 'C' ' ' 'D' 'E' 'M' 'O'. 14 cells total. */
107
119
  static const uint8_t message[14] = {
@@ -114,9 +126,11 @@ static const uint8_t message[14] = {
114
126
  static void clear_name_table(void) {
115
127
  uint8_t row;
116
128
  uint8_t col;
129
+ /* Fill with the dithered BG tile (not blank) so the whole screen reads
130
+ * as a two-tone field and never as a blank backdrop. */
117
131
  for (row = 0; row < 28; row++) {
118
132
  for (col = 0; col < 32; col++) {
119
- sms_set_tilemap_cell(row, col, 0, 0);
133
+ sms_set_tilemap_cell(row, col, T_DITHER, 0);
120
134
  }
121
135
  }
122
136
  }
@@ -24,6 +24,7 @@ extern void sms_vdp_init(void);
24
24
  extern void sms_vdp_display_on(void);
25
25
  extern void sms_load_palette(const uint8_t *palette);
26
26
  extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
27
+ extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
27
28
  extern void sms_vblank_wait(void);
28
29
  extern uint8_t sms_joypad_read(void);
29
30
  extern uint8_t sms_joypad_read_p2(void);
@@ -40,13 +41,32 @@ extern void sms_sat_upload(void);
40
41
  #define T_ENEMY 3
41
42
 
42
43
  static const uint8_t palette[32] = {
43
- 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
44
+ /* BG palette: 0 backdrop space-blue, 1 mid-blue, 2 dark-blue (starfield dither) */
45
+ 0x10, 0x24, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
44
46
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
45
47
  /* Sprite palette: 1 white (P1), 2 yellow (bullet), 3 red (enemy + P2 highlight) */
46
48
  0x00, 0x3F, 0x0F, 0x03, 0x00, 0x00, 0x00, 0x00,
47
49
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
48
50
  };
49
51
 
52
+ /* One dithered BG tile (BG bank $0000): plane0=0xAA/0x55, plane1=0x55/0xAA
53
+ * so pixels alternate colour 1 (mid-blue) and colour 2 (dark-blue) in a fine
54
+ * checkerboard. Filling the name table with it gives a two-tone "space"
55
+ * backdrop so the screen never reads as a single flat colour. */
56
+ static const uint8_t bg_tiles[32] = {
57
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
58
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
59
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
60
+ 0xAA,0x55,0x00,0x00, 0x55,0xAA,0x00,0x00,
61
+ };
62
+
63
+ static void draw_bg(void) {
64
+ uint8_t row, col;
65
+ for (row = 0; row < 28; row++)
66
+ for (col = 0; col < 32; col++)
67
+ sms_set_tilemap_cell(row, col, 0, 0);
68
+ }
69
+
50
70
  /* 4 sprite tiles back-to-back: P1 ship (col1), P2 ship (col3), bullet (col2), enemy (col3). */
51
71
  static const uint8_t sprite_tiles[32 * 4] = {
52
72
  /* P1 ship — diamond, colour 1 (plane 0) */
@@ -112,6 +132,9 @@ void main(void) {
112
132
  uint8_t prev1 = 0, prev2 = 0;
113
133
  sms_vdp_init();
114
134
  sms_load_palette(palette);
135
+ /* BG dither tile → BG bank $0000, paint the whole name table. */
136
+ sms_load_tiles(0x0000, bg_tiles, 32);
137
+ draw_bg();
115
138
  sms_load_tiles(0x2000, sprite_tiles, 32 * 4);
116
139
 
117
140
  p1.x = 80; p1.y = 160; p1.alive = 1;
@@ -123,6 +123,7 @@ void main(void) {
123
123
  do {
124
124
  uint8_t p1, p2;
125
125
  uint8_t slot;
126
+ int16_t target;
126
127
  sms_vblank_wait();
127
128
  sfx_update();
128
129
 
@@ -144,12 +145,13 @@ void main(void) {
144
145
  if ((p1 & JOY_UP) && p1y > COURT_TOP) p1y -= 2;
145
146
  if ((p1 & JOY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
146
147
 
147
- /* P2 input if any, otherwise AI. */
148
+ /* P2 input if any, otherwise AI. (`target` is declared at the top of the
149
+ * loop body — SDCC is C89, declarations must precede statements.) */
148
150
  if (p2 != 0) {
149
151
  if ((p2 & JOY_UP) && p2y > COURT_TOP) p2y -= 2;
150
152
  if ((p2 & JOY_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 2;
151
153
  } else {
152
- int16_t target = by - PADDLE_H / 2;
154
+ target = by - PADDLE_H / 2;
153
155
  if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
154
156
  else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
155
157
  }