romdevtools 0.15.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 (117) hide show
  1. package/AGENTS.md +61 -13
  2. package/CHANGELOG.md +289 -0
  3. package/README.md +1 -1
  4. package/examples/README.md +2 -0
  5. package/examples/atari2600/templates/platformer.asm +460 -0
  6. package/examples/atari2600/templates/racing.asm +463 -0
  7. package/examples/atari2600/templates/shmup.asm +386 -0
  8. package/examples/atari2600/templates/sports.asm +362 -0
  9. package/examples/atari7800/templates/default.c +49 -5
  10. package/examples/atari7800/templates/platformer.c +43 -4
  11. package/examples/atari7800/templates/puzzle.c +39 -4
  12. package/examples/atari7800/templates/racing.c +39 -4
  13. package/examples/atari7800/templates/shmup.c +40 -2
  14. package/examples/atari7800/templates/sports.c +36 -5
  15. package/examples/c64/templates/platformer.c +19 -5
  16. package/examples/c64/templates/puzzle.c +32 -2
  17. package/examples/c64/templates/shmup.c +28 -2
  18. package/examples/c64/templates/sports.c +30 -2
  19. package/examples/gb/templates/default.c +110 -16
  20. package/examples/gb/templates/platformer.c +25 -4
  21. package/examples/gb/templates/puzzle.c +32 -2
  22. package/examples/gb/templates/racing.c +72 -8
  23. package/examples/gb/templates/shmup.c +38 -1
  24. package/examples/gb/templates/sports.c +48 -1
  25. package/examples/gba/templates/gba_hello.c +29 -11
  26. package/examples/gba/templates/puzzle.c +15 -3
  27. package/examples/gba/templates/racing.c +65 -3
  28. package/examples/gba/templates/shmup.c +41 -4
  29. package/examples/gba/templates/sports.c +36 -2
  30. package/examples/gba/templates/tonc_hello.c +41 -5
  31. package/examples/gbc/templates/default.c +103 -26
  32. package/examples/gbc/templates/platformer.c +25 -4
  33. package/examples/gbc/templates/puzzle.c +32 -2
  34. package/examples/gbc/templates/racing.c +85 -19
  35. package/examples/gbc/templates/shmup.c +34 -1
  36. package/examples/gbc/templates/sports.c +45 -1
  37. package/examples/genesis/templates/puzzle.c +37 -3
  38. package/examples/genesis/templates/racing.c +44 -11
  39. package/examples/genesis/templates/sgdk_hello.c +34 -1
  40. package/examples/genesis/templates/shmup.c +31 -1
  41. package/examples/gg/templates/default.c +56 -18
  42. package/examples/gg/templates/platformer.c +18 -12
  43. package/examples/gg/templates/puzzle.c +38 -7
  44. package/examples/gg/templates/racing.c +51 -5
  45. package/examples/gg/templates/shmup.c +47 -3
  46. package/examples/gg/templates/sports.c +46 -3
  47. package/examples/lynx/templates/default.c +39 -8
  48. package/examples/lynx/templates/puzzle.c +28 -1
  49. package/examples/lynx/templates/racing.c +34 -7
  50. package/examples/lynx/templates/shmup.c +42 -3
  51. package/examples/lynx/templates/sports.c +29 -2
  52. package/examples/msx/platformer/main.c +213 -0
  53. package/examples/msx/puzzle/main.c +250 -0
  54. package/examples/msx/racing/main.c +249 -0
  55. package/examples/msx/shmup/main.c +288 -0
  56. package/examples/msx/sports/main.c +182 -0
  57. package/examples/nes/templates/default.c +67 -19
  58. package/examples/nes/templates/platformer.c +65 -6
  59. package/examples/nes/templates/puzzle.c +67 -6
  60. package/examples/nes/templates/racing.c +45 -13
  61. package/examples/nes/templates/shmup.c +51 -2
  62. package/examples/nes/templates/sports.c +51 -6
  63. package/examples/pce/platformer/main.c +283 -0
  64. package/examples/pce/puzzle/main.c +304 -0
  65. package/examples/pce/racing/main.c +304 -0
  66. package/examples/pce/shmup/main.c +346 -0
  67. package/examples/pce/sports/main.c +254 -0
  68. package/examples/sms/main.c +35 -6
  69. package/examples/sms/templates/puzzle.c +34 -5
  70. package/examples/sms/templates/racing.c +39 -2
  71. package/examples/sms/templates/shmup.c +41 -2
  72. package/examples/sms/templates/sports.c +43 -2
  73. package/examples/snes/templates/default.c +50 -28
  74. package/examples/snes/templates/platformer-data.asm +22 -0
  75. package/examples/snes/templates/platformer.c +16 -1
  76. package/examples/snes/templates/puzzle-data.asm +22 -0
  77. package/examples/snes/templates/puzzle.c +17 -1
  78. package/examples/snes/templates/racing-data.asm +22 -0
  79. package/examples/snes/templates/racing.c +17 -1
  80. package/examples/snes/templates/shmup-data.asm +22 -0
  81. package/examples/snes/templates/shmup.c +20 -1
  82. package/examples/snes/templates/sports-data.asm +22 -0
  83. package/examples/snes/templates/sports.c +16 -1
  84. package/package.json +1 -1
  85. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  86. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  87. package/src/host/LibretroHost.js +122 -1
  88. package/src/host/callbacks.js +9 -1
  89. package/src/host/types.js +15 -8
  90. package/src/http/skill-doc.js +1 -1
  91. package/src/http/tool-registry.js +27 -2
  92. package/src/mcp/tools/cart-parts.js +75 -3
  93. package/src/mcp/tools/disasm-rebuild.js +507 -0
  94. package/src/mcp/tools/disasm.js +95 -6
  95. package/src/mcp/tools/frame.js +168 -3
  96. package/src/mcp/tools/index.js +4 -4
  97. package/src/mcp/tools/lifecycle.js +4 -2
  98. package/src/mcp/tools/project.js +54 -9
  99. package/src/mcp/tools/state.js +201 -14
  100. package/src/mcp/tools/toolchain.js +89 -4
  101. package/src/mcp/tools/watch-memory.js +125 -14
  102. package/src/platforms/c64/MENTAL_MODEL.md +45 -1
  103. package/src/platforms/c64/d64.js +281 -0
  104. package/src/platforms/gb/MENTAL_MODEL.md +10 -0
  105. package/src/platforms/msx/MENTAL_MODEL.md +10 -6
  106. package/src/platforms/nes/MENTAL_MODEL.md +63 -2
  107. package/src/platforms/pce/MENTAL_MODEL.md +9 -4
  108. package/src/platforms/pce/lib/c/pce_video.c +1 -1
  109. package/src/rom-id/identifier.js +15 -0
  110. package/src/toolchains/cc65/cc65.js +8 -1
  111. package/src/toolchains/cc65/ines.js +145 -0
  112. package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
  113. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
  114. package/src/toolchains/common/reassemble.js +10 -2
  115. package/src/toolchains/gba-c/gba-c.js +6 -1
  116. package/src/toolchains/genesis-c/genesis-c.js +10 -2
  117. package/src/toolchains/parse-errors.js +67 -5
@@ -11,6 +11,7 @@ extern void gg_vdp_init(void);
11
11
  extern void gg_vdp_display_on(void);
12
12
  extern void gg_load_palette(const uint8_t *palette);
13
13
  extern void gg_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
14
+ extern void gg_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
14
15
  extern void gg_vblank_wait(void);
15
16
  extern uint8_t gg_joypad_read(void);
16
17
  extern void gg_sprite_init(void);
@@ -37,8 +38,8 @@ extern void gg_sat_upload(void);
37
38
  * (entries 16-31) reading garbage = invisible sprites. Sprite colour 1 = entry
38
39
  * 17 (white). */
39
40
  static const uint8_t palette[64] = {
40
- /* BG 0-15: entry 0 = dark navy backdrop */
41
- 0x20,0x02, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
41
+ /* BG 0-15: 0 = dark navy backdrop, 1 = court green, 2 = court line white */
42
+ 0x20,0x02, 0x60,0x00, 0xFF,0x0F, 0,0, 0,0, 0,0, 0,0, 0,0,
42
43
  0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
43
44
  /* SPRITE 16-31: 16=transparent, 17=white */
44
45
  0,0, 0xFF,0x0F, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0,
@@ -52,6 +53,42 @@ static const uint8_t tile_solid[32] = {
52
53
  0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
53
54
  };
54
55
 
56
+ /* Three BG tiles for the court, loaded into the BG tile bank at $0000:
57
+ * tile 0 = court green (colour 1), tile 1 = court line / border
58
+ * (colour 2 = white), tile 2 = dashed net (colour 2 stripe on green). */
59
+ static const uint8_t bg_tiles[96] = {
60
+ /* tile 0 = court green (colour 1 -> plane 0 set) */
61
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
62
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
63
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
64
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
65
+ /* tile 1 = court line / border (colour 2 -> plane 1 set) */
66
+ 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
67
+ 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
68
+ 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
69
+ 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
70
+ /* tile 2 = net: centre column colour 2, rest colour 1 (green) */
71
+ 0xFF,0x18,0x00,0x00, 0xFF,0x18,0x00,0x00,
72
+ 0xFF,0x18,0x00,0x00, 0xFF,0x18,0x00,0x00,
73
+ 0xFF,0x18,0x00,0x00, 0xFF,0x18,0x00,0x00,
74
+ 0xFF,0x18,0x00,0x00, 0xFF,0x18,0x00,0x00,
75
+ };
76
+
77
+ /* Paint the court inside the GG visible region (cols 6..25, rows 3..20):
78
+ * green field, white border lines around the perimeter, dashed net down
79
+ * the centre column. BG tile bank is $0000. */
80
+ static void draw_court(void) {
81
+ uint8_t row, col;
82
+ for (row = 3; row <= 20; row++) {
83
+ for (col = 6; col <= 25; col++) {
84
+ uint8_t t = 0; /* green field */
85
+ if (row == 3 || row == 20 || col == 6 || col == 25) t = 1; /* border */
86
+ else if (col == 15 && (row & 1)) t = 2; /* dashed centre net */
87
+ gg_set_tilemap_cell(row, col, t, 0);
88
+ }
89
+ }
90
+ }
91
+
55
92
  static int16_t p1y, p2y, bx, by;
56
93
  static int8_t bdx, bdy;
57
94
  static uint8_t score_p1, score_p2;
@@ -76,7 +113,13 @@ void main(void) {
76
113
  uint8_t i;
77
114
  gg_vdp_init();
78
115
  gg_load_palette(palette);
79
- gg_load_tiles(0x2000, tile_solid, 32);
116
+ gg_load_tiles(0x0000, bg_tiles, 96); /* BG court tiles -> BG bank $0000 */
117
+ gg_load_tiles(0x2000, tile_solid, 32); /* paddle/ball sprite tile -> $2000 */
118
+ {
119
+ uint8_t r, c;
120
+ for (r = 0; r < 28; r++) for (c = 0; c < 32; c++) gg_set_tilemap_cell(r, c, 0, 0);
121
+ }
122
+ draw_court();
80
123
  gg_sprite_init();
81
124
  sfx_init();
82
125
  gg_vdp_display_on();
@@ -15,9 +15,12 @@
15
15
  #include <lynx.h>
16
16
 
17
17
  void main(void) {
18
- static const unsigned char palette[8] = {
19
- COLOR_RED, COLOR_YELLOW, COLOR_GREEN, COLOR_LIGHTBLUE,
20
- COLOR_BLUE, COLOR_PURPLE, COLOR_LIGHTGREEN, COLOR_WHITE,
18
+ /* Cycle the centre square through warm/bright shades only — NOT the
19
+ * blues, which would blend into the blue background and make the
20
+ * screen read as "almost one colour". */
21
+ static const unsigned char palette[6] = {
22
+ COLOR_RED, COLOR_YELLOW, COLOR_GREEN,
23
+ COLOR_WHITE, COLOR_LIGHTGREEN, COLOR_PURPLE,
21
24
  };
22
25
  unsigned char shade = 0;
23
26
  unsigned int frame = 0;
@@ -26,16 +29,44 @@ void main(void) {
26
29
  tgi_init();
27
30
 
28
31
  for (;;) {
29
- tgi_clear();
32
+ /* CANONICAL LYNX FRAME LOOP — full redraw every frame, in this order:
33
+ * 1. WAIT for Suzy's blitter to finish the previous frame. Drawing
34
+ * while it's mid-flight loses the frame → black screen. This is
35
+ * the #1 "Lynx stays blank" trap.
36
+ * 2. CLEAR with a full-screen tgi_bar in the background colour, NOT
37
+ * tgi_clear() — which leaves the back page stale in this
38
+ * toolchain+emulator path (the other genre scaffolds all do the
39
+ * same; see shmup.c's LYNX-1 note).
40
+ * 3. DRAW everything.
41
+ * 4. tgi_updatedisplay() to push the frame. */
42
+ while (tgi_busy()) { }
43
+
44
+ /* Blue field so the screen is obviously not blank. */
45
+ tgi_setcolor(COLOR_BLUE);
46
+ tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy());
47
+
48
+ /* A fixed green frame around the centre — always a distinct colour,
49
+ * so the screen never collapses to a single shade even while the
50
+ * inner square is cycling. */
51
+ tgi_setcolor(COLOR_GREEN);
52
+ tgi_bar(50, 30, 110, 72);
53
+
54
+ /* Colour-cycling square in the centre. */
30
55
  tgi_setcolor(palette[shade]);
31
- tgi_bar(70, 40, 90, 62);
56
+ tgi_bar(60, 36, 100, 66);
57
+
58
+ /* Greeting on top. The tgi font is 8 px wide; "BUILT WITH ROMDEV"
59
+ * is 17 chars = 136 px, so start near the left edge to stay inside
60
+ * the 160-px-wide screen. */
32
61
  tgi_setcolor(COLOR_WHITE);
33
- tgi_outtextxy(2, 2, "HELLO LYNX");
62
+ tgi_outtextxy(40, 14, "HELLO LYNX");
63
+ tgi_outtextxy(12, 86, "BUILT WITH ROMDEV");
34
64
  tgi_updatedisplay();
35
65
 
36
66
  frame++;
37
- if ((frame & 0x1F) == 0) { /* every 32 frames */
38
- shade = (shade + 1) & 0x07;
67
+ if ((frame % 24) == 0) { /* advance the inner square */
68
+ shade++;
69
+ if (shade >= 6) shade = 0;
39
70
  }
40
71
  }
41
72
  }
@@ -98,8 +98,35 @@ void main(void) {
98
98
  * — drawing while the blitter is mid-flight loses the frame → black.
99
99
  * (Copied from the shmup scaffold, the LYNX-1 fix.) */
100
100
  while (tgi_busy()) { }
101
+
102
+ /* ── Background scene (drawn every frame). Without it the playfield is
103
+ * a near-flat single colour and the render-health audit flags the
104
+ * screen as blank. A framed "well" in the centre with lit side panels
105
+ * keeps several distinct colours well under the threshold:
106
+ * - blue cabinet backdrop
107
+ * - dark-grey side panels flanking the well
108
+ * - black well interior so the falling blocks read clearly
109
+ * - light-grey well frame + a faint grid texture behind the cells. */
110
+ tgi_setcolor(COLOR_BLUE);
111
+ tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* cabinet backdrop */
112
+ tgi_setcolor(COLOR_DARKGREY);
113
+ tgi_bar(0, 0, GRID_X - 5, 101); /* left side panel */
114
+ tgi_bar(GRID_X + COLS * CELL_PX + 4, 0, 159, 101); /* right side panel */
101
115
  tgi_setcolor(COLOR_BLACK);
102
- tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy());
116
+ tgi_bar(GRID_X - 2, GRID_Y - 2,
117
+ GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1); /* well */
118
+ /* faint grid texture so the empty well is never one flat colour */
119
+ tgi_setcolor(COLOR_DARKGREY);
120
+ for (r = 0; r <= ROWS; r++)
121
+ tgi_line(GRID_X, GRID_Y + r * CELL_PX, GRID_X + COLS * CELL_PX - 1, GRID_Y + r * CELL_PX);
122
+ for (c = 0; c <= COLS; c++)
123
+ tgi_line(GRID_X + c * CELL_PX, GRID_Y, GRID_X + c * CELL_PX, GRID_Y + ROWS * CELL_PX - 1);
124
+ /* well frame */
125
+ tgi_setcolor(COLOR_LIGHTGREY);
126
+ tgi_line(GRID_X - 2, GRID_Y - 2, GRID_X - 2, GRID_Y + ROWS * CELL_PX + 1);
127
+ tgi_line(GRID_X + COLS * CELL_PX + 1, GRID_Y - 2, GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1);
128
+ tgi_line(GRID_X - 2, GRID_Y + ROWS * CELL_PX + 1, GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1);
129
+
103
130
  /* grid */
104
131
  for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) {
105
132
  if (grid[r][c] != 0) {
@@ -23,6 +23,8 @@ void main(void) {
23
23
  uint8_t game_over = 0;
24
24
  uint8_t joy, i;
25
25
  uint32_t rng = 1;
26
+ uint8_t scroll = 0; /* animates the road dashes so the track moves */
27
+ int16_t y;
26
28
 
27
29
  tgi_install(&lynx_160_102_16_tgi);
28
30
  tgi_init();
@@ -36,14 +38,37 @@ void main(void) {
36
38
  * — drawing while the blitter is mid-flight loses the frame → black.
37
39
  * (Copied from the shmup scaffold, the LYNX-1 fix.) */
38
40
  while (tgi_busy()) { }
39
- tgi_setcolor(COLOR_BLACK);
40
- tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy());
41
- /* lane lines */
41
+
42
+ /* ── Background scene (drawn every frame). Without it the track is a
43
+ * near-flat single colour and the render-health audit flags the
44
+ * screen as blank. A full road with grass shoulders + animated lane
45
+ * dashes keeps several distinct colours well under the threshold:
46
+ * - green grass shoulders on both sides
47
+ * - mid-grey tarmac with darker-grey lane bands
48
+ * - white scrolling centre dashes + solid edge lines. */
49
+ tgi_setcolor(COLOR_GREEN);
50
+ tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* grass base */
51
+ tgi_setcolor(COLOR_GREY);
52
+ tgi_bar(20, 0, 148, 101); /* tarmac */
53
+ /* darker lane bands so the road isn't one flat grey */
42
54
  tgi_setcolor(COLOR_DARKGREY);
43
- tgi_line(28, 0, 28, 101);
44
- tgi_line(72, 0, 72, 101);
45
- tgi_line(116, 0, 116, 101);
46
- tgi_line(160, 0, 160, 101);
55
+ tgi_bar(20, 0, 53, 101);
56
+ tgi_bar(96, 0, 128, 101);
57
+ /* solid road edges */
58
+ tgi_setcolor(COLOR_WHITE);
59
+ tgi_line(20, 0, 20, 101);
60
+ tgi_line(148, 0, 148, 101);
61
+ /* animated dashed lane dividers (scroll downward) */
62
+ for (y = (int16_t)scroll - 12; y < 102; y += 12) {
63
+ tgi_bar(53, (unsigned)(y < 0 ? 0 : y), 55, (unsigned)(y + 6 > 101 ? 101 : y + 6));
64
+ tgi_bar(96, (unsigned)(y < 0 ? 0 : y), 98, (unsigned)(y + 6 > 101 ? 101 : y + 6));
65
+ }
66
+ /* grass rumble strips for extra colour texture */
67
+ tgi_setcolor(COLOR_LIGHTGREEN);
68
+ for (y = (int16_t)scroll - 8; y < 102; y += 16) {
69
+ tgi_bar(0, (unsigned)(y < 0 ? 0 : y), 6, (unsigned)(y + 6 > 101 ? 101 : y + 6));
70
+ tgi_bar(153, (unsigned)(y < 0 ? 0 : y), 159, (unsigned)(y + 6 > 101 ? 101 : y + 6));
71
+ }
47
72
 
48
73
  tgi_setcolor(COLOR_YELLOW);
49
74
  tgi_bar((unsigned)player.x - 4, (unsigned)player.y - 4, (unsigned)player.x + 4, (unsigned)player.y + 4);
@@ -54,6 +79,8 @@ void main(void) {
54
79
  tgi_updatedisplay();
55
80
  sfx_update();
56
81
 
82
+ scroll += 2; if (scroll >= 12) scroll -= 12; /* advance road dashes */
83
+
57
84
  if (game_over > 0) {
58
85
  game_over--;
59
86
  if (game_over == 0) {
@@ -50,8 +50,15 @@ static void spawn(void) {
50
50
  }
51
51
  }
52
52
 
53
+ /* Scrolling starfield: a handful of stars that drift down so the dark
54
+ * space field is never a flat single colour (would read as "blank"). */
55
+ #define N_STARS 24
56
+ static uint8_t star_x[N_STARS];
57
+ static uint8_t star_y[N_STARS];
58
+
53
59
  void main(void) {
54
60
  uint8_t joy, fire_now, i, j;
61
+ uint32_t srng = 0x1234;
55
62
 
56
63
  tgi_install(&lynx_160_102_16_tgi);
57
64
  tgi_init();
@@ -61,6 +68,12 @@ void main(void) {
61
68
  player.x = 76; player.y = 90; player.alive = 1;
62
69
  for (i = 0; i < MAX_BULLETS; i++) bullets[i].alive = 0;
63
70
  for (i = 0; i < MAX_ENEMIES; i++) enemies[i].alive = 0;
71
+ for (i = 0; i < N_STARS; i++) {
72
+ srng = srng * 1103515245u + 12345u;
73
+ star_x[i] = (uint8_t)((srng >> 16) % 160);
74
+ srng = srng * 1103515245u + 12345u;
75
+ star_y[i] = (uint8_t)((srng >> 16) % 102);
76
+ }
64
77
  spawn_timer = 0;
65
78
  prev_btn = 0;
66
79
 
@@ -76,10 +89,31 @@ void main(void) {
76
89
  * 3. DRAW every object.
77
90
  * 4. tgi_updatedisplay() to push the frame. */
78
91
  while (tgi_busy()) { }
79
- tgi_setcolor(COLOR_BLACK);
80
- tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* maxx/maxy = 159/101 = full screen */
81
92
 
82
- /* Render */
93
+ /* ── Background scene (drawn every frame; without it the dark space
94
+ * field is a near-flat single colour and the render-health audit
95
+ * flags the screen as blank). Layered bands keep any one colour well
96
+ * under the threshold:
97
+ * - deep-blue upper space
98
+ * - grey nebula band across the middle
99
+ * - green planet surface along the bottom
100
+ * - a drifting white/yellow starfield over the space. */
101
+ tgi_setcolor(COLOR_BLUE);
102
+ tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* base space field */
103
+ tgi_setcolor(COLOR_GREY);
104
+ tgi_bar(0, 34, 159, 60); /* nebula band */
105
+ tgi_setcolor(COLOR_GREEN);
106
+ tgi_bar(0, 84, 159, 101); /* planet surface */
107
+ tgi_setcolor(COLOR_LIGHTGREEN);
108
+ tgi_bar(0, 78, 159, 83); /* surface horizon */
109
+ /* starfield (bright specks; also drifts downward each frame) */
110
+ tgi_setcolor(COLOR_WHITE);
111
+ for (i = 0; i < N_STARS; i++) {
112
+ tgi_setpixel(star_x[i], star_y[i]);
113
+ tgi_setpixel(star_x[i], (star_y[i] + 1) % 102);
114
+ }
115
+
116
+ /* Render game objects on top */
83
117
  tgi_setcolor(COLOR_YELLOW);
84
118
  tgi_bar(player.x, player.y, player.x + 6, player.y + 6);
85
119
  tgi_setcolor(COLOR_WHITE);
@@ -93,6 +127,11 @@ void main(void) {
93
127
  tgi_updatedisplay();
94
128
  sfx_update();
95
129
 
130
+ /* drift the starfield downward */
131
+ for (i = 0; i < N_STARS; i++) {
132
+ if (star_y[i] >= 101) star_y[i] = 0; else star_y[i]++;
133
+ }
134
+
96
135
  /* Input + state */
97
136
  joy = joy_read(JOY_1);
98
137
  fire_now = JOY_BTN_1(joy) ? 1 : 0;
@@ -21,6 +21,7 @@ void main(void) {
21
21
  int16_t p1y = 40, p2y = 40, bx = 78, by = 48;
22
22
  int8_t bdx = 2, bdy = 1;
23
23
  uint8_t joy;
24
+ int16_t ny; /* loop var for the dashed centre net */
24
25
 
25
26
  tgi_install(&lynx_160_102_16_tgi);
26
27
  tgi_init();
@@ -33,8 +34,34 @@ void main(void) {
33
34
  * — drawing while the blitter is mid-flight loses the frame → black.
34
35
  * (Copied from the shmup scaffold, the LYNX-1 fix.) */
35
36
  while (tgi_busy()) { }
36
- tgi_setcolor(COLOR_BLACK);
37
- tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy());
37
+
38
+ /* ── Background scene (drawn every frame). Without it the court is a
39
+ * near-flat single colour and the render-health audit flags the
40
+ * screen as blank. A two-tone court with boards + net markings keeps
41
+ * several distinct colours well under the threshold:
42
+ * - green centre court
43
+ * - lighter-green end zones behind each paddle
44
+ * - dark-grey boards top and bottom
45
+ * - white boundary, dashed centre net + centre circle. */
46
+ tgi_setcolor(COLOR_GREEN);
47
+ tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* court grass */
48
+ tgi_setcolor(COLOR_LIGHTGREEN);
49
+ tgi_bar(0, COURT_TOP, 52, COURT_BOT - 1); /* left end zone */
50
+ tgi_bar(107, COURT_TOP, 159, COURT_BOT - 1); /* right end zone */
51
+ tgi_setcolor(COLOR_DARKGREY);
52
+ tgi_bar(0, 0, 159, COURT_TOP - 1); /* top boards */
53
+ tgi_bar(0, COURT_BOT, 159, 101); /* bottom boards */
54
+ /* white court boundary + dashed centre net + centre circle */
55
+ tgi_setcolor(COLOR_WHITE);
56
+ tgi_line(0, COURT_TOP, 159, COURT_TOP);
57
+ tgi_line(0, COURT_BOT, 159, COURT_BOT);
58
+ for (ny = COURT_TOP; ny < COURT_BOT; ny += 8)
59
+ tgi_bar(79, (unsigned)ny, 80, (unsigned)(ny + 3 > COURT_BOT ? COURT_BOT : ny + 3));
60
+ tgi_line(70, 40, 90, 40);
61
+ tgi_line(70, 60, 90, 60);
62
+ tgi_line(70, 40, 70, 60);
63
+ tgi_line(90, 40, 90, 60);
64
+
38
65
  tgi_setcolor(COLOR_WHITE);
39
66
  tgi_bar(PADDLE_X1, (unsigned)p1y, PADDLE_X1 + PADDLE_W - 1, (unsigned)(p1y + PADDLE_H - 1));
40
67
  tgi_bar(PADDLE_X2, (unsigned)p2y, PADDLE_X2 + PADDLE_W - 1, (unsigned)(p2y + PADDLE_H - 1));
@@ -0,0 +1,213 @@
1
+ /* ── platformer/main.c — MSX SIDE-SCROLLING platformer scaffold ──────
2
+ *
3
+ * Mirrors the SMS/GB/etc platformer scaffolds, translated to the MSX VDP
4
+ * via the romdev helper lib (msx_hw.h + msx_vdp.c).
5
+ *
6
+ * The world is 512 px wide (64 cells); the screen-2 name table is only 32
7
+ * cells (256 px) and wraps, so a world wider than one screen needs COLUMN
8
+ * STREAMING: each time the camera crosses an 8-px boundary we rewrite the
9
+ * name-table column about to scroll into view with the next world column's
10
+ * tiles. Screen 2 has no smooth-pixel-scroll register, so the camera moves
11
+ * in whole 8-px cells — the streaming gives a clean tile-by-tile scroll.
12
+ *
13
+ * Subpixel state (x/y in 1/16-pixel units) for fine gravity/acceleration;
14
+ * the player sprite draws in SCREEN space ((worldX>>4) - camX).
15
+ *
16
+ * Controls: joystick LEFT/RIGHT walks, trigger A jumps (only when grounded).
17
+ *
18
+ * Cartridge rule: INIT must never return — main() ends in for(;;).
19
+ */
20
+ #include "msx_hw.h"
21
+
22
+ /* ── interrupt-free vblank sync (poll VDP status S#0 bit 7) ────────────── */
23
+ __sfr __at 0x99 VDPSTATUS;
24
+ static void vsync(void) {
25
+ (void)VDPSTATUS;
26
+ while (!(VDPSTATUS & 0x80)) {
27
+ }
28
+ }
29
+ /* jump uses the BIOS GTTRIG wrapper (gttrig) provided by msx_hw.h. */
30
+
31
+ #define T_OPEN 0
32
+ #define T_WALL 1
33
+
34
+ #define WORLD_COLS 64
35
+ #define WORLD_W (WORLD_COLS * 8)
36
+ #define SCREEN_W 256
37
+ #define VIS_ROWS 24
38
+
39
+ /* background tile patterns (8x8) */
40
+ static const uint8_t TILE_OPEN[8] = {0,0,0,0,0,0,0,0};
41
+ static const uint8_t TILE_WALL[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
42
+
43
+ #define COL_OPEN 0x14 /* light-blue "open" (sky) on dark blue */
44
+ #define COL_WALL 0xE4 /* grey wall on dark blue */
45
+
46
+ /* player sprite (8x8) */
47
+ static const uint8_t SPR_PLAYER[8] = {0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C};
48
+ #define COL_PLAYER 9 /* red-ish */
49
+
50
+ typedef struct { int16_t x, y, w, h; } Rect;
51
+
52
+ /* platforms in WORLD coords, spread across the 512-px world */
53
+ static const Rect platforms[] = {
54
+ { 0, 176, 512, 16 }, /* floor spans the world */
55
+ { 32, 144, 56, 8 },
56
+ { 120, 144, 64, 8 },
57
+ { 200, 112, 48, 8 },
58
+ { 56, 96, 40, 8 },
59
+ { 288, 136, 64, 8 },
60
+ { 384, 104, 56, 8 },
61
+ { 440, 152, 48, 8 },
62
+ { 320, 72, 48, 8 }
63
+ };
64
+ #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
65
+
66
+ static int16_t px, py; /* player pos, 1/16-px units */
67
+ static int16_t vx, vy;
68
+ static int16_t camX; /* camera X in pixels (cell-aligned) */
69
+ static int16_t lastCamCol;
70
+
71
+ static void load_tiles(void) {
72
+ uint8_t third;
73
+ uint16_t poff;
74
+ for (third = 0; third < 3; third++) {
75
+ poff = (uint16_t)((uint16_t)third << 11);
76
+ msx_vram_write((uint16_t)(VRAM_PATTERN + poff + 0), TILE_OPEN, 8);
77
+ msx_vram_write((uint16_t)(VRAM_PATTERN + poff + 8), TILE_WALL, 8);
78
+ msx_fill_vram((uint16_t)(VRAM_COLOR + poff + 0), 8, COL_OPEN);
79
+ msx_fill_vram((uint16_t)(VRAM_COLOR + poff + 8), 8, COL_WALL);
80
+ }
81
+ }
82
+
83
+ static uint8_t cell_is_wall(int16_t col, uint8_t row) {
84
+ int16_t cx = (int16_t)(col << 3);
85
+ int16_t cy = (int16_t)((int16_t)row << 3);
86
+ uint8_t i;
87
+ const Rect *p;
88
+ for (i = 0; i < N_PLATFORMS; i++) {
89
+ p = &platforms[i];
90
+ if (cx + 8 > p->x && cx < p->x + p->w
91
+ && cy + 8 > p->y && cy < p->y + p->h) return 1;
92
+ }
93
+ return 0;
94
+ }
95
+
96
+ static uint8_t on_platform(int16_t ipx, int16_t ipy) {
97
+ uint8_t i;
98
+ const Rect *p;
99
+ for (i = 0; i < N_PLATFORMS; i++) {
100
+ p = &platforms[i];
101
+ if (ipy + 8 == p->y && ipx + 8 > p->x && ipx < p->x + p->w) return 1;
102
+ }
103
+ return 0;
104
+ }
105
+
106
+ /* write one world column into its wrapped name-table column */
107
+ static void paint_column(int16_t worldCol) {
108
+ uint8_t ntCol, row, tile;
109
+ uint16_t addr;
110
+ if (worldCol < 0 || worldCol >= WORLD_COLS) return;
111
+ ntCol = (uint8_t)(worldCol & 31);
112
+ for (row = 0; row < VIS_ROWS; row++) {
113
+ tile = cell_is_wall(worldCol, row) ? T_WALL : T_OPEN;
114
+ addr = (uint16_t)(VRAM_NAME + (uint16_t)row * 32 + ntCol);
115
+ msx_vram_write(addr, &tile, 1);
116
+ }
117
+ }
118
+
119
+ static void paint_initial(void) {
120
+ int16_t c;
121
+ for (c = 0; c < 32; c++) paint_column(c);
122
+ }
123
+
124
+ void main(void) {
125
+ const int16_t GRAVITY = 10;
126
+ const int16_t MOVE = 24;
127
+ const int16_t JUMP = -200;
128
+ const int16_t MAXFALL = 280;
129
+ uint8_t dir, trig, prev_trig, grounded, blip;
130
+
131
+ msx_set_screen2();
132
+ msx_clear_sprites();
133
+ load_tiles();
134
+ msx_fill_vram(VRAM_NAME, 32 * 24, T_OPEN);
135
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + 0), SPR_PLAYER, 8);
136
+
137
+ px = (int16_t)(16 << 4);
138
+ py = (int16_t)(64 << 4);
139
+ vx = 0; vy = 0;
140
+ camX = 0; lastCamCol = 0;
141
+ prev_trig = 0; blip = 0;
142
+
143
+ paint_initial();
144
+
145
+ for (;;) {
146
+ int16_t ipx, ipy, npy, sx, camCol;
147
+ int32_t np;
148
+ uint8_t i, landed;
149
+ const Rect *p;
150
+
151
+ vsync();
152
+
153
+ ipx = (int16_t)(px >> 4);
154
+ ipy = (int16_t)(py >> 4);
155
+
156
+ /* camera follows the player, centered, clamped, snapped to a cell */
157
+ camX = (int16_t)(ipx - (SCREEN_W / 2 - 4));
158
+ if (camX < 0) camX = 0;
159
+ if (camX > WORLD_W - SCREEN_W) camX = (int16_t)(WORLD_W - SCREEN_W);
160
+ camX = (int16_t)(camX & ~7);
161
+
162
+ camCol = (int16_t)(camX >> 3);
163
+ while (camCol > lastCamCol) { lastCamCol++; paint_column((int16_t)(lastCamCol + 31)); }
164
+ while (camCol < lastCamCol) { lastCamCol--; paint_column(lastCamCol); }
165
+
166
+ sx = (int16_t)(ipx - camX);
167
+ if (sx < 0) sx = 0;
168
+ if (sx > 248) sx = 248;
169
+ msx_set_sprite(0, (uint8_t)sx, (uint8_t)ipy, 0, COL_PLAYER);
170
+
171
+ dir = msx_read_joystick(1);
172
+ if (dir == STICK_CENTER) dir = msx_read_joystick(0);
173
+ trig = (uint8_t)(gttrig(1) || gttrig(2));
174
+
175
+ vx = 0;
176
+ if (dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL) vx = (int16_t)(-MOVE);
177
+ if (dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR) vx = MOVE;
178
+
179
+ grounded = on_platform(ipx, ipy);
180
+ if (trig && !prev_trig && grounded) { vy = JUMP; msx_psg_tone(0, 0x180, 12); blip = 6; }
181
+ prev_trig = trig;
182
+
183
+ vy = (int16_t)(vy + GRAVITY);
184
+ if (vy > MAXFALL) vy = MAXFALL;
185
+ if (grounded && vy > 0) vy = 0;
186
+
187
+ px = (int16_t)(px + vx);
188
+ if (px < 0) px = 0;
189
+ if (px > (int16_t)((WORLD_W - 8) << 4)) px = (int16_t)((WORLD_W - 8) << 4);
190
+
191
+ np = (int32_t)py + (int32_t)vy;
192
+ npy = (int16_t)(np >> 4);
193
+ if (vy > 0) {
194
+ landed = 0;
195
+ for (i = 0; i < N_PLATFORMS; i++) {
196
+ p = &platforms[i];
197
+ if (ipy + 8 <= p->y && npy + 8 >= p->y
198
+ && ipx + 8 > p->x && ipx < p->x + p->w) {
199
+ py = (int16_t)((p->y - 8) << 4);
200
+ vy = 0;
201
+ landed = 1;
202
+ break;
203
+ }
204
+ }
205
+ if (!landed) py = (int16_t)np;
206
+ } else {
207
+ py = (int16_t)np;
208
+ }
209
+ if (py > (int16_t)(192 << 4)) { py = (int16_t)(64 << 4); vy = 0; }
210
+
211
+ if (blip) { blip--; if (!blip) msx_psg_off(0); }
212
+ }
213
+ }