romdevtools 0.16.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.
- package/AGENTS.md +75 -16
- package/CHANGELOG.md +316 -0
- package/examples/README.md +2 -0
- package/examples/atari2600/templates/platformer.asm +460 -0
- package/examples/atari2600/templates/racing.asm +463 -0
- package/examples/atari2600/templates/shmup.asm +386 -0
- package/examples/atari2600/templates/sports.asm +362 -0
- package/examples/atari7800/templates/default.c +49 -5
- package/examples/atari7800/templates/hello_sprite.c +48 -4
- package/examples/atari7800/templates/music_demo.c +47 -2
- package/examples/atari7800/templates/platformer.c +43 -4
- package/examples/atari7800/templates/puzzle.c +39 -4
- package/examples/atari7800/templates/racing.c +39 -4
- package/examples/atari7800/templates/shmup.c +40 -2
- package/examples/atari7800/templates/sports.c +36 -5
- package/examples/c64/templates/platformer.c +19 -5
- package/examples/c64/templates/puzzle.c +32 -2
- package/examples/c64/templates/shmup.c +28 -2
- package/examples/c64/templates/sports.c +30 -2
- package/examples/c64/templates/tile_engine.c +77 -27
- package/examples/gb/templates/default.c +110 -16
- package/examples/gb/templates/hello_sprite.c +15 -6
- package/examples/gb/templates/music_demo.c +36 -0
- package/examples/gb/templates/platformer.c +28 -6
- package/examples/gb/templates/puzzle.c +35 -4
- package/examples/gb/templates/racing.c +75 -10
- package/examples/gb/templates/shmup.c +41 -3
- package/examples/gb/templates/sports.c +51 -3
- package/examples/gb/templates/tile_engine.c +3 -2
- package/examples/gba/templates/gba_hello.c +29 -11
- package/examples/gba/templates/maxmod_demo.c +36 -2
- package/examples/gba/templates/platformer.c +3 -1
- package/examples/gba/templates/puzzle.c +15 -3
- package/examples/gba/templates/racing.c +65 -3
- package/examples/gba/templates/shmup.c +41 -4
- package/examples/gba/templates/sports.c +36 -2
- package/examples/gba/templates/tonc_hello.c +41 -5
- package/examples/gba/templates/tonc_hello_sprite.c +35 -1
- package/examples/gbc/templates/default.c +103 -26
- package/examples/gbc/templates/hello_sprite.c +12 -3
- package/examples/gbc/templates/music_demo.c +56 -12
- package/examples/gbc/templates/platformer.c +28 -6
- package/examples/gbc/templates/puzzle.c +35 -4
- package/examples/gbc/templates/racing.c +88 -21
- package/examples/gbc/templates/shmup.c +37 -3
- package/examples/gbc/templates/sports.c +48 -3
- package/examples/gbc/templates/tile_engine.c +3 -2
- package/examples/genesis/main.s +53 -1
- package/examples/genesis/templates/hello_sprite.c +25 -3
- package/examples/genesis/templates/puzzle.c +37 -3
- package/examples/genesis/templates/racing.c +44 -11
- package/examples/genesis/templates/sgdk_hello.c +34 -1
- package/examples/genesis/templates/shmup.c +31 -1
- package/examples/genesis/templates/shmup_2p.c +31 -0
- package/examples/genesis/templates/xgm2_demo.c +20 -0
- package/examples/gg/templates/default.c +56 -18
- package/examples/gg/templates/hello_sprite.c +25 -2
- package/examples/gg/templates/music_demo.c +24 -2
- package/examples/gg/templates/platformer.c +18 -12
- package/examples/gg/templates/puzzle.c +38 -7
- package/examples/gg/templates/racing.c +58 -9
- package/examples/gg/templates/shmup.c +47 -3
- package/examples/gg/templates/sports.c +57 -16
- package/examples/gg/templates/tile_engine.c +12 -6
- package/examples/lynx/templates/default.c +39 -8
- package/examples/lynx/templates/hello_sprite.c +15 -1
- package/examples/lynx/templates/music_demo.c +13 -1
- package/examples/lynx/templates/puzzle.c +28 -1
- package/examples/lynx/templates/racing.c +34 -7
- package/examples/lynx/templates/shmup.c +42 -3
- package/examples/lynx/templates/sports.c +29 -2
- package/examples/msx/platformer/main.c +213 -0
- package/examples/msx/puzzle/main.c +250 -0
- package/examples/msx/racing/main.c +249 -0
- package/examples/msx/shmup/main.c +288 -0
- package/examples/msx/sports/main.c +182 -0
- package/examples/nes/templates/default.c +67 -19
- package/examples/nes/templates/hello_sprite.c +35 -0
- package/examples/nes/templates/music_demo.c +40 -0
- package/examples/nes/templates/platformer.c +65 -6
- package/examples/nes/templates/puzzle.c +67 -6
- package/examples/nes/templates/racing.c +45 -13
- package/examples/nes/templates/shmup.c +51 -2
- package/examples/nes/templates/sports.c +51 -6
- package/examples/pce/catch_game/main.c +22 -3
- package/examples/pce/music_sfx/main.c +28 -1
- package/examples/pce/platformer/main.c +283 -0
- package/examples/pce/puzzle/main.c +304 -0
- package/examples/pce/racing/main.c +304 -0
- package/examples/pce/shmup/main.c +346 -0
- package/examples/pce/sports/main.c +254 -0
- package/examples/pce/sprite_move/main.c +7 -2
- package/examples/sms/main.c +35 -6
- package/examples/sms/templates/hello_sprite.c +29 -3
- package/examples/sms/templates/music_demo.c +18 -4
- package/examples/sms/templates/puzzle.c +34 -5
- package/examples/sms/templates/racing.c +39 -2
- package/examples/sms/templates/shmup.c +41 -2
- package/examples/sms/templates/shmup_2p.c +24 -1
- package/examples/sms/templates/sports.c +47 -4
- package/examples/snes/main.asm +108 -17
- package/examples/snes/templates/c-hello-data.asm +23 -0
- package/examples/snes/templates/c-hello.c +18 -1
- package/examples/snes/templates/default.c +50 -28
- package/examples/snes/templates/hello_sprite-data.asm +23 -0
- package/examples/snes/templates/hello_sprite.c +17 -1
- package/examples/snes/templates/music_demo-data.asm +23 -0
- package/examples/snes/templates/music_demo.c +22 -4
- package/examples/snes/templates/platformer-data.asm +22 -0
- package/examples/snes/templates/platformer.c +20 -2
- package/examples/snes/templates/puzzle-data.asm +22 -0
- package/examples/snes/templates/puzzle.c +21 -2
- package/examples/snes/templates/racing-data.asm +22 -0
- package/examples/snes/templates/racing.c +17 -1
- package/examples/snes/templates/shmup-data.asm +22 -0
- package/examples/snes/templates/shmup.c +20 -1
- package/examples/snes/templates/sports-data.asm +22 -0
- package/examples/snes/templates/sports.c +16 -1
- package/package.json +1 -1
- package/src/cheats/gamegenie.js +0 -1
- package/src/cli/smoke.js +1 -3
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +191 -16
- package/src/host/callbacks.js +9 -1
- package/src/host/chafa-render.js +2 -0
- package/src/host/dsp-state.js +2 -2
- package/src/host/gpgx-state.js +4 -0
- package/src/host/types.js +15 -8
- package/src/http/routes.js +1 -1
- package/src/http/tool-registry.js +26 -1
- package/src/mcp/server.js +1 -1
- package/src/mcp/state.js +36 -0
- package/src/mcp/tools/address-to-symbol.js +0 -1
- package/src/mcp/tools/art-loaders.js +1 -1
- package/src/mcp/tools/cart-parts.js +75 -4
- package/src/mcp/tools/classify-region.js +1 -1
- package/src/mcp/tools/diff-roms.js +1 -1
- package/src/mcp/tools/disasm-rebuild.js +507 -0
- package/src/mcp/tools/disasm.js +97 -9
- package/src/mcp/tools/find-references.js +1 -2
- package/src/mcp/tools/font-map.js +1 -1
- package/src/mcp/tools/frame.js +168 -3
- package/src/mcp/tools/index.js +0 -49
- package/src/mcp/tools/input-layout.js +0 -1
- package/src/mcp/tools/input.js +33 -3
- package/src/mcp/tools/lifecycle.js +18 -4
- package/src/mcp/tools/lospec.js +0 -19
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +4 -4
- package/src/mcp/tools/project.js +54 -11
- package/src/mcp/tools/reinject.js +0 -1
- package/src/mcp/tools/rom-id.js +2 -2
- package/src/mcp/tools/snippets.js +2 -2
- package/src/mcp/tools/sprite-pipeline.js +1 -2
- package/src/mcp/tools/state.js +201 -14
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +105 -12
- package/src/mcp/tools/watch-memory.js +137 -16
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
- package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
- package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/MENTAL_MODEL.md +45 -1
- package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/d64.js +280 -0
- package/src/platforms/c64/sid.js +0 -2
- package/src/platforms/common/metasprite-adapters.js +1 -1
- package/src/platforms/common/metasprite-codegen.js +3 -3
- package/src/platforms/common/registers.js +5 -3
- package/src/platforms/gb/MENTAL_MODEL.md +10 -0
- package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
- package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
- package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/msx/MENTAL_MODEL.md +10 -6
- package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/MENTAL_MODEL.md +63 -2
- package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/image-to-tilemap.js +3 -0
- package/src/platforms/nes/lib/asm/famitone2.s +5 -1
- package/src/platforms/pce/MENTAL_MODEL.md +9 -4
- package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
- package/src/platforms/pce/lib/c/pce_video.c +1 -1
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/brr.js +0 -2
- package/src/playtest/playtest.js +0 -7
- package/src/rom-id/identifier.js +15 -0
- package/src/toolchains/asar/asar.js +0 -9
- package/src/toolchains/assemble-snippet.js +30 -12
- package/src/toolchains/cc65/ines.js +145 -0
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
- package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
- package/src/toolchains/common/reassemble.js +10 -3
- package/src/toolchains/common/sdk-cache.js +1 -1
- package/src/toolchains/genesis-c/genesis-c.js +5 -3
- package/src/toolchains/index.js +27 -3
- package/src/toolchains/parse-errors.js +78 -1
- package/src/toolchains/sdcc/preflight-lint.js +5 -1
- package/src/toolchains/sdcc/sdcc.js +1 -1
- package/src/toolchains/sjasm/sjasm.js +1 -1
- package/src/toolchains/snes-c/snes-c.js +2 -2
- package/src/toolchains/vasm68k/vasm68k.js +2 -4
- package/src/toolchains/wladx/wladx.js +1 -1
|
@@ -145,6 +145,48 @@ stub that does a `SYS` to the C entry point.
|
|
|
145
145
|
For game ROMs you typically just let the user load the .prg into the
|
|
146
146
|
emulator and the rest takes care of itself.
|
|
147
147
|
|
|
148
|
+
## Disk images (.d64) — loading real games & distributing yours
|
|
149
|
+
|
|
150
|
+
A bare `.prg` is fine for dev iteration, but the real C64 world — the new
|
|
151
|
+
Commodore 64 Ultimate / C64C Ultimate FPGA hardware and the homebrew/demo
|
|
152
|
+
scene — ships and loads games as **`.d64` disk images** (and `.crt` carts /
|
|
153
|
+
`.tap` tapes). romdev handles them:
|
|
154
|
+
|
|
155
|
+
- **Load & run a disk/cart/tape:** `loadMedia({platform:'c64', path:'game.d64'})`
|
|
156
|
+
(also `.t64 .tap .crt .g64`). VICE attaches it to drive 8 and **autostarts**
|
|
157
|
+
it (equivalent to `LOAD"*",8,1 : RUN`) under warp — give it a few hundred
|
|
158
|
+
frames to finish the emulated 1541 load, then it's running. `mediaKind` in
|
|
159
|
+
status reports `disk` / `tape` / `cartridge` / `program` so you know what you
|
|
160
|
+
loaded.
|
|
161
|
+
- **Distribute YOUR game as a disk:** build your `.prg` as usual, then
|
|
162
|
+
`cart({op:'packDisk', prgPath:'game.prg'})` → an autostart-able `game.d64`
|
|
163
|
+
in the exact format the Ultimate hardware and the scene load. (`cart({op:
|
|
164
|
+
'extract', path:'x.d64'})` lists a disk's files; add `name:` to pull one out.)
|
|
165
|
+
|
|
166
|
+
## Disk SAVES (the C64 save medium)
|
|
167
|
+
|
|
168
|
+
The C64 has no battery SRAM — games save by **writing files to the floppy**. The
|
|
169
|
+
disk IS the save, so romdev exposes the LIVE mounted `.d64` for save/restore
|
|
170
|
+
(the C64 analogue of SRAM `exportSram`/`importSram`):
|
|
171
|
+
|
|
172
|
+
- **Snapshot the disk** (captures any files the game wrote): `state({op:
|
|
173
|
+
'exportDisk', path:'save.d64'})`. Re-load it later with `loadMedia` (autostarts)
|
|
174
|
+
or push it back into a running session with `state({op:'importDisk', path})`.
|
|
175
|
+
- **Inject a save file** a player made elsewhere, straight into the running disk:
|
|
176
|
+
`state({op:'putDiskFile', path:'progress.prg', name:'PROGRESS'})` writes one PRG
|
|
177
|
+
file via the drive. Read it back with `exportDisk` or `cart({op:'extract'})`.
|
|
178
|
+
|
|
179
|
+
These work on the standard 35-track 1541 `.d64` (174848 bytes).
|
|
180
|
+
|
|
181
|
+
**A game's OWN in-emulator `SAVE` works too.** When a running program does a
|
|
182
|
+
KERNAL `SAVE` to drive 8, VICE commits it into the live disk image (true-drive
|
|
183
|
+
GCR write-back) — so after the game saves, `state({op:'exportDisk', path})`
|
|
184
|
+
captures a `.d64` that includes the new file, and you can re-load it later to
|
|
185
|
+
resume. (The on-disk filename is stored in PETSCII; romdev's reader decodes it.)
|
|
186
|
+
So the normal flow is just: run the game, let it save, `exportDisk` to persist.
|
|
187
|
+
`putDiskFile`/`importDisk` are for *injecting* a save from outside (a save a
|
|
188
|
+
player made elsewhere), not a requirement for the game's own saves.
|
|
189
|
+
|
|
148
190
|
## Frame heartbeat
|
|
149
191
|
|
|
150
192
|
The C64 has no dedicated vblank interrupt by default. Two approaches:
|
|
@@ -166,7 +208,9 @@ When you call `build({output:'rom', platform:"c64", language:"c"})`:
|
|
|
166
208
|
3. ld65 links + the bundled c64.cfg → `.prg` with a 2-byte load-address
|
|
167
209
|
header.
|
|
168
210
|
|
|
169
|
-
Loadable via vice_x64 (`loadMedia`).
|
|
211
|
+
Loadable via vice_x64 (`loadMedia`). To ship it the way the scene/hardware
|
|
212
|
+
loads games, wrap the `.prg` into a `.d64`: `cart({op:'packDisk', prgPath})`
|
|
213
|
+
(see "Disk images" above).
|
|
170
214
|
|
|
171
215
|
## Horizontal scrolling (for side-scrollers)
|
|
172
216
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Commodore 64 — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
When something's broken. Read MENTAL_MODEL.md first
|
|
4
10
|
(via `platform({op:'doc', platform:"c64", name:"mental_model"})`).
|
|
5
11
|
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// C64 1541 disk image (.d64) codec — pure JS, no external tools.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: romdev builds C64 homebrew as a bare `.prg` (cc65's output),
|
|
4
|
+
// but the real C64 world — the new Commodore 64 Ultimate / C64C Ultimate FPGA
|
|
5
|
+
// hardware and the entire homebrew/demo scene — loads games as `.d64` disk
|
|
6
|
+
// images (and saves by writing files back INTO the disk). A `.prg` with no
|
|
7
|
+
// drive can't save and isn't how anything ships. This module is the bridge:
|
|
8
|
+
//
|
|
9
|
+
// prgToD64(prg, {name}) — pack a .prg into a fresh, autostart-able .d64
|
|
10
|
+
// readDirectory(d64) — list the files on a disk image
|
|
11
|
+
// extractFile(d64, name) — pull a file's bytes back out (post-save read)
|
|
12
|
+
//
|
|
13
|
+
// Format reference: the standard 35-track 1541 image (174848 bytes). 256-byte
|
|
14
|
+
// sectors, variable sectors per track. Track 18 holds the BAM (sector 0) and
|
|
15
|
+
// the directory (sectors 1+). Files are PETSCII-named, stored as linked sector
|
|
16
|
+
// chains where each sector's first two bytes are (nextTrack, nextSector) — or
|
|
17
|
+
// (0x00, lastByteIndex) on the final sector. This is the well-documented "D64"
|
|
18
|
+
// layout used by VICE's c1541 and every C64 emulator.
|
|
19
|
+
|
|
20
|
+
const SECTOR_SIZE = 256;
|
|
21
|
+
const NUM_TRACKS = 35;
|
|
22
|
+
const DIR_TRACK = 18;
|
|
23
|
+
const BAM_SECTOR = 0;
|
|
24
|
+
const DIR_START_SECTOR = 1;
|
|
25
|
+
|
|
26
|
+
// Sectors per track for a 35-track 1541 disk (zones 1-4).
|
|
27
|
+
// Tracks 1-17: 21, 18-24: 19, 25-30: 18, 31-35: 17.
|
|
28
|
+
const SECTORS_PER_TRACK = (() => {
|
|
29
|
+
const a = new Array(NUM_TRACKS + 1).fill(0); // 1-indexed
|
|
30
|
+
for (let t = 1; t <= NUM_TRACKS; t++) {
|
|
31
|
+
if (t <= 17) a[t] = 21;
|
|
32
|
+
else if (t <= 24) a[t] = 19;
|
|
33
|
+
else if (t <= 30) a[t] = 18;
|
|
34
|
+
else a[t] = 17;
|
|
35
|
+
}
|
|
36
|
+
return a;
|
|
37
|
+
})();
|
|
38
|
+
|
|
39
|
+
const TOTAL_SECTORS = (() => {
|
|
40
|
+
let n = 0;
|
|
41
|
+
for (let t = 1; t <= NUM_TRACKS; t++) n += SECTORS_PER_TRACK[t];
|
|
42
|
+
return n; // 683
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
const IMAGE_SIZE = TOTAL_SECTORS * SECTOR_SIZE; // 174848
|
|
46
|
+
|
|
47
|
+
/** Byte offset of (track, sector) within the flat image. track is 1-indexed. */
|
|
48
|
+
function offsetOf(track, sector) {
|
|
49
|
+
let off = 0;
|
|
50
|
+
for (let t = 1; t < track; t++) off += SECTORS_PER_TRACK[t] * SECTOR_SIZE;
|
|
51
|
+
return off + sector * SECTOR_SIZE;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Convert an ASCII string to PETSCII-ish bytes, padded/truncated to `len` with 0xA0 (shifted space). */
|
|
55
|
+
function petsciiName(name, len = 16) {
|
|
56
|
+
const out = new Uint8Array(len).fill(0xa0);
|
|
57
|
+
const s = String(name || "").toUpperCase();
|
|
58
|
+
for (let i = 0; i < len && i < s.length; i++) {
|
|
59
|
+
const c = s.charCodeAt(i);
|
|
60
|
+
// ASCII A-Z, 0-9, space, and common punctuation map ~1:1 to PETSCII for
|
|
61
|
+
// these ranges; anything exotic falls back to a space.
|
|
62
|
+
out[i] = c >= 0x20 && c <= 0x5f ? c : 0x20;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convert a PETSCII directory name (as stored on disk) back to a trimmed ASCII
|
|
69
|
+
* string. Filenames written by the C64 KERNAL SAVE use the DEFAULT uppercase
|
|
70
|
+
* charset, where letters A–Z are 0xC1–0xDA (high bit set), not 0x41–0x5A — so we
|
|
71
|
+
* must translate that range, otherwise an emulator-written "SCORE" reads as
|
|
72
|
+
* empty. (Our own prgToD64 writes plain 0x41–0x5A; both must decode.)
|
|
73
|
+
*/
|
|
74
|
+
function asciiFromPetscii(bytes) {
|
|
75
|
+
let s = "";
|
|
76
|
+
for (const b of bytes) {
|
|
77
|
+
if (b === 0xa0 || b === 0x00) break; // shifted-space pad / terminator
|
|
78
|
+
if (b >= 0xc1 && b <= 0xda) {
|
|
79
|
+
s += String.fromCharCode(b - 0x80); // PETSCII upper A–Z (0xC1..) → ASCII
|
|
80
|
+
} else if (b >= 0x20 && b <= 0x5f) {
|
|
81
|
+
s += String.fromCharCode(b); // plain ASCII / digits / punctuation
|
|
82
|
+
} else if (b >= 0x61 && b <= 0x7a) {
|
|
83
|
+
s += String.fromCharCode(b - 0x20); // PETSCII lower-as-upper → ASCII upper
|
|
84
|
+
}
|
|
85
|
+
// anything else (graphics chars etc.) is dropped from the readable name
|
|
86
|
+
}
|
|
87
|
+
return s.trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pack a `.prg` (2-byte little-endian load address + body) into a fresh,
|
|
92
|
+
* autostart-able 1541 `.d64` image. The file is written as a single PRG-type
|
|
93
|
+
* directory entry named `name` (default "GAME"). The disk is otherwise empty.
|
|
94
|
+
*
|
|
95
|
+
* @param {Uint8Array|Buffer} prg the raw .prg bytes (load addr + program)
|
|
96
|
+
* @param {object} [opts]
|
|
97
|
+
* @param {string} [opts.name] file name (PETSCII, ≤16 chars) — default "GAME"
|
|
98
|
+
* @param {string} [opts.diskName] disk label (≤16 chars) — default = name
|
|
99
|
+
* @param {string} [opts.diskId] 2-char disk id — default "RD"
|
|
100
|
+
* @returns {Uint8Array} a 174848-byte .d64 image
|
|
101
|
+
*/
|
|
102
|
+
export function prgToD64(prg, opts = {}) {
|
|
103
|
+
const body = prg instanceof Uint8Array ? prg : new Uint8Array(prg);
|
|
104
|
+
if (body.length < 2) throw new Error("prgToD64: .prg too small (need ≥2 bytes load address)");
|
|
105
|
+
const fileName = opts.name || "GAME";
|
|
106
|
+
const diskName = opts.diskName || fileName;
|
|
107
|
+
const diskId = (opts.diskId || "RD").slice(0, 2).padEnd(2, " ");
|
|
108
|
+
|
|
109
|
+
const img = new Uint8Array(IMAGE_SIZE);
|
|
110
|
+
|
|
111
|
+
// ---- Lay the file out as a linked sector chain ----------------------------
|
|
112
|
+
// Files conventionally start on track 1; we walk forward, skipping the
|
|
113
|
+
// directory track (18). 254 data bytes per sector (2 bytes are the link).
|
|
114
|
+
const dataPerSector = SECTOR_SIZE - 2;
|
|
115
|
+
const numSectors = Math.ceil(body.length / dataPerSector) || 1;
|
|
116
|
+
|
|
117
|
+
// Pick a sector list (track, sector) for the file, skipping the dir track.
|
|
118
|
+
const chain = [];
|
|
119
|
+
let track = 1;
|
|
120
|
+
let sector = 0;
|
|
121
|
+
for (let i = 0; i < numSectors; i++) {
|
|
122
|
+
// advance to a free (track,sector), skipping the directory track
|
|
123
|
+
while (track === DIR_TRACK || sector >= SECTORS_PER_TRACK[track]) {
|
|
124
|
+
if (sector >= SECTORS_PER_TRACK[track]) { track++; sector = 0; }
|
|
125
|
+
if (track === DIR_TRACK) { track++; sector = 0; }
|
|
126
|
+
if (track > NUM_TRACKS) throw new Error("prgToD64: file too large for a 35-track disk");
|
|
127
|
+
}
|
|
128
|
+
chain.push([track, sector]);
|
|
129
|
+
sector++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Write the chain.
|
|
133
|
+
for (let i = 0; i < chain.length; i++) {
|
|
134
|
+
const [t, s] = chain[i];
|
|
135
|
+
const base = offsetOf(t, s);
|
|
136
|
+
const isLast = i === chain.length - 1;
|
|
137
|
+
const sliceStart = i * dataPerSector;
|
|
138
|
+
const sliceEnd = Math.min(sliceStart + dataPerSector, body.length);
|
|
139
|
+
const chunk = body.subarray(sliceStart, sliceEnd);
|
|
140
|
+
if (isLast) {
|
|
141
|
+
img[base] = 0x00; // next track = 0 → end of file
|
|
142
|
+
img[base + 1] = chunk.length + 1; // bytes-used-in-this-sector index
|
|
143
|
+
} else {
|
|
144
|
+
const [nt, ns] = chain[i + 1];
|
|
145
|
+
img[base] = nt;
|
|
146
|
+
img[base + 1] = ns;
|
|
147
|
+
}
|
|
148
|
+
img.set(chunk, base + 2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const fileBlocks = chain.length;
|
|
152
|
+
|
|
153
|
+
// ---- BAM (track 18, sector 0) --------------------------------------------
|
|
154
|
+
const bam = offsetOf(DIR_TRACK, BAM_SECTOR);
|
|
155
|
+
img[bam + 0] = DIR_TRACK; // first directory track
|
|
156
|
+
img[bam + 1] = DIR_START_SECTOR; // first directory sector
|
|
157
|
+
img[bam + 2] = 0x41; // 'A' = 1541 disk format
|
|
158
|
+
img[bam + 3] = 0x00;
|
|
159
|
+
|
|
160
|
+
// Per-track free-sector bitmap: 4 bytes each for tracks 1..35 at +4.
|
|
161
|
+
// byte0 = free count, bytes1-3 = bitmap (bit set = sector free).
|
|
162
|
+
for (let t = 1; t <= NUM_TRACKS; t++) {
|
|
163
|
+
const e = bam + 4 + (t - 1) * 4;
|
|
164
|
+
const spt = SECTORS_PER_TRACK[t];
|
|
165
|
+
let freeMask = 0;
|
|
166
|
+
for (let s = 0; s < spt; s++) freeMask |= (1 << s);
|
|
167
|
+
// mark used: any sector in our file chain, plus track 18 sectors 0 & 1
|
|
168
|
+
let used = new Set();
|
|
169
|
+
if (t === DIR_TRACK) { used.add(BAM_SECTOR); used.add(DIR_START_SECTOR); }
|
|
170
|
+
for (const [ct, cs] of chain) if (ct === t) used.add(cs);
|
|
171
|
+
for (const s of used) freeMask &= ~(1 << s);
|
|
172
|
+
let freeCount = 0;
|
|
173
|
+
for (let s = 0; s < spt; s++) if (freeMask & (1 << s)) freeCount++;
|
|
174
|
+
img[e + 0] = freeCount;
|
|
175
|
+
img[e + 1] = freeMask & 0xff;
|
|
176
|
+
img[e + 2] = (freeMask >> 8) & 0xff;
|
|
177
|
+
img[e + 3] = (freeMask >> 16) & 0xff;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Disk name (+0x90, 16 bytes, 0xA0 padded), then id + dos type.
|
|
181
|
+
img.set(petsciiName(diskName, 16), bam + 0x90);
|
|
182
|
+
img[bam + 0xa0] = 0xa0;
|
|
183
|
+
img[bam + 0xa1] = 0xa0;
|
|
184
|
+
img[bam + 0xa2] = diskId.charCodeAt(0);
|
|
185
|
+
img[bam + 0xa3] = diskId.charCodeAt(1);
|
|
186
|
+
img[bam + 0xa4] = 0xa0;
|
|
187
|
+
img[bam + 0xa5] = 0x32; // '2' DOS version
|
|
188
|
+
img[bam + 0xa6] = 0x41; // 'A'
|
|
189
|
+
for (let i = 0xa7; i <= 0xaa; i++) img[bam + i] = 0xa0;
|
|
190
|
+
|
|
191
|
+
// ---- Directory entry (track 18, sector 1, first slot) --------------------
|
|
192
|
+
const dir = offsetOf(DIR_TRACK, DIR_START_SECTOR);
|
|
193
|
+
img[dir + 0] = 0x00; // next dir track = 0 (only one dir sector)
|
|
194
|
+
img[dir + 1] = 0xff; // next dir sector = 0xff (last in chain)
|
|
195
|
+
// entry: file type (0x82 = closed PRG), then first (track,sector)
|
|
196
|
+
img[dir + 2] = 0x82;
|
|
197
|
+
img[dir + 3] = chain[0][0];
|
|
198
|
+
img[dir + 4] = chain[0][1];
|
|
199
|
+
img.set(petsciiName(fileName, 16), dir + 5);
|
|
200
|
+
// bytes 0x15-0x1d: REL side info / unused for PRG → 0
|
|
201
|
+
img[dir + 0x1e] = fileBlocks & 0xff; // block count low
|
|
202
|
+
img[dir + 0x1f] = (fileBlocks >> 8) & 0xff; // block count high
|
|
203
|
+
|
|
204
|
+
return img;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Read the directory of a `.d64` image.
|
|
209
|
+
* @param {Uint8Array|Buffer} d64
|
|
210
|
+
* @returns {Array<{name:string, type:string, track:number, sector:number, blocks:number}>}
|
|
211
|
+
*/
|
|
212
|
+
export function readDirectory(d64) {
|
|
213
|
+
const img = d64 instanceof Uint8Array ? d64 : new Uint8Array(d64);
|
|
214
|
+
const TYPES = ["DEL", "SEQ", "PRG", "USR", "REL"];
|
|
215
|
+
const out = [];
|
|
216
|
+
let t = DIR_TRACK, s = DIR_START_SECTOR;
|
|
217
|
+
const seen = new Set();
|
|
218
|
+
while (t !== 0 && !seen.has(`${t},${s}`)) {
|
|
219
|
+
seen.add(`${t},${s}`);
|
|
220
|
+
const base = offsetOf(t, s);
|
|
221
|
+
const nextT = img[base + 0];
|
|
222
|
+
const nextS = img[base + 1];
|
|
223
|
+
// 8 entries per sector, 32 bytes each, first entry at +2 then every +32.
|
|
224
|
+
for (let e = 0; e < 8; e++) {
|
|
225
|
+
const entryBase = base + (e === 0 ? 2 : 2 + e * 32);
|
|
226
|
+
const typeByte = img[entryBase + 0];
|
|
227
|
+
if ((typeByte & 0x0f) === 0 && typeByte === 0) continue; // empty slot
|
|
228
|
+
const ft = img[entryBase + 0];
|
|
229
|
+
const ftrack = img[entryBase + 1];
|
|
230
|
+
const fsector = img[entryBase + 2];
|
|
231
|
+
const nameBytes = img.subarray(entryBase + 3, entryBase + 3 + 16);
|
|
232
|
+
const name = asciiFromPetscii(nameBytes);
|
|
233
|
+
if (!name) continue;
|
|
234
|
+
const blocks = img[entryBase + 0x1c] | (img[entryBase + 0x1d] << 8);
|
|
235
|
+
out.push({ name, type: TYPES[ft & 0x07] || "DEL", track: ftrack, sector: fsector, blocks });
|
|
236
|
+
}
|
|
237
|
+
t = nextT; s = nextS;
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Extract a file's raw bytes (including its 2-byte load address for PRG) from a
|
|
244
|
+
* `.d64`, by following its sector chain. If `name` is omitted, the first file
|
|
245
|
+
* is returned.
|
|
246
|
+
* @param {Uint8Array|Buffer} d64
|
|
247
|
+
* @param {string} [name]
|
|
248
|
+
* @returns {Uint8Array|null} file bytes, or null if not found
|
|
249
|
+
*/
|
|
250
|
+
export function extractFile(d64, name) {
|
|
251
|
+
const img = d64 instanceof Uint8Array ? d64 : new Uint8Array(d64);
|
|
252
|
+
const dir = readDirectory(img);
|
|
253
|
+
const entry = name
|
|
254
|
+
? dir.find((d) => d.name.toUpperCase() === String(name).toUpperCase())
|
|
255
|
+
: dir[0];
|
|
256
|
+
if (!entry) return null;
|
|
257
|
+
|
|
258
|
+
const bytes = [];
|
|
259
|
+
let t = entry.track, s = entry.sector;
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
while (t !== 0 && !seen.has(`${t},${s}`)) {
|
|
262
|
+
seen.add(`${t},${s}`);
|
|
263
|
+
const base = offsetOf(t, s);
|
|
264
|
+
const nextT = img[base + 0];
|
|
265
|
+
const nextS = img[base + 1];
|
|
266
|
+
if (nextT === 0) {
|
|
267
|
+
// last sector: nextS is the index of the last valid byte (+1 over data)
|
|
268
|
+
const used = nextS; // bytes 2..used hold data
|
|
269
|
+
for (let i = 2; i <= used && base + i < img.length; i++) bytes.push(img[base + i]);
|
|
270
|
+
break;
|
|
271
|
+
} else {
|
|
272
|
+
for (let i = 2; i < SECTOR_SIZE; i++) bytes.push(img[base + i]);
|
|
273
|
+
}
|
|
274
|
+
t = nextT; s = nextS;
|
|
275
|
+
}
|
|
276
|
+
return new Uint8Array(bytes);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const D64_IMAGE_SIZE = IMAGE_SIZE;
|
|
280
|
+
export const D64_DISK_EXTENSIONS = [".d64", ".d71", ".d81", ".g64", ".t64", ".tap", ".crt", ".p00"];
|
package/src/platforms/c64/sid.js
CHANGED
|
@@ -29,8 +29,6 @@
|
|
|
29
29
|
// $D41B voice 3 OSC3 readback
|
|
30
30
|
// $D41C voice 3 ENV3 readback
|
|
31
31
|
|
|
32
|
-
const WAVEFORMS = ["none", "triangle", "sawtooth", "tri+saw", "pulse", "tri+pulse", "saw+pulse", "tri+saw+pulse", "noise"];
|
|
33
|
-
|
|
34
32
|
function decodeControl(byte) {
|
|
35
33
|
// Decode the waveform field (top 4 bits) as a name where possible.
|
|
36
34
|
const wfBits = (byte >> 4) & 0x0F;
|
|
@@ -203,7 +203,7 @@ export async function gbAdapter(host, platform) {
|
|
|
203
203
|
// palette line (CRAM entries 16-31). Tile data base from VDP reg 6.
|
|
204
204
|
// =====================================================================
|
|
205
205
|
export async function smsAdapter(host, platform) {
|
|
206
|
-
const { decodeSmsTile, decodeSmsVdpRegs,
|
|
206
|
+
const { decodeSmsTile, decodeSmsVdpRegs, snapshotPalette } = await import("../sms/vdp.js");
|
|
207
207
|
const vramRegion = platform === "gg" ? "gg_vram" : "sms_vram";
|
|
208
208
|
const vram = host.readMemory(vramRegion, 0, 0x4000);
|
|
209
209
|
const regs = host.readMemory("sms_vdp_regs", 0, 16);
|
|
@@ -75,7 +75,7 @@ static u16 ${v}_draw(u16 firstSlot, s16 x, s16 y, u16 baseTile) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// ---- SNES (PVSnesLib oamSet-style) ----
|
|
78
|
-
function emitSnes(v, layout, tiles,
|
|
78
|
+
function emitSnes(v, layout, tiles, _palette) {
|
|
79
79
|
const pieces = layout.pieces.map((p) => {
|
|
80
80
|
// PVSnesLib oamSet: size 0=8x8/16x16 small/large per OBSEL — we expose
|
|
81
81
|
// wPx/hPx and let the user pick the OBSEL pair; flip bits in attr.
|
|
@@ -99,7 +99,7 @@ const unsigned short ${v}_piece_count = ${layout.pieces.length};
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// ---- NES (shadow-OAM bytes) ----
|
|
102
|
-
function emitNes(v, layout, tiles,
|
|
102
|
+
function emitNes(v, layout, tiles, _palette) {
|
|
103
103
|
// NES draw = write 4 OAM bytes per cell (y, tile, attr, x). We emit pieces
|
|
104
104
|
// as (x,y,tile,attr) so the user copies them into shadow OAM at their base.
|
|
105
105
|
const cells = [];
|
|
@@ -127,7 +127,7 @@ const unsigned char ${v}_cell_count = ${cells.length};
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// ---- GB/GBC (shadow-OAM bytes) ----
|
|
130
|
-
function emitGb(v, layout, tiles,
|
|
130
|
+
function emitGb(v, layout, tiles, _palette) {
|
|
131
131
|
const cells = [];
|
|
132
132
|
for (const p of layout.pieces) {
|
|
133
133
|
for (let r = 0; r < p.hTiles; r++) {
|
|
@@ -235,14 +235,16 @@ export const ATARI7800_REGISTERS = {
|
|
|
235
235
|
0x2E: "P3C1", 0x2F: "P3C2", 0x30: "P3C3",
|
|
236
236
|
0x32: "P4C1", 0x33: "P4C2", 0x34: "P4C3",
|
|
237
237
|
0x36: "P5C1", 0x37: "P5C2", 0x38: "P5C3",
|
|
238
|
-
0x3A: "P6C1", 0x3B: "P6C2",
|
|
238
|
+
0x3A: "P6C1", 0x3B: "P6C2",
|
|
239
|
+
// $3C is BOTH the MARIA control reg (CTRL) and P6C3 depending on the
|
|
240
|
+
// reference's naming convention — a JS object holds one value per key, so
|
|
241
|
+
// name it for both rather than silently dropping one.
|
|
242
|
+
0x3C: "CTRL/P6C3",
|
|
239
243
|
0x3E: "P7C1", 0x3F: "P7C2", // P7C3 lives at $40 in some refs
|
|
240
244
|
// DPP (display-list pointer) lives at $84/$85
|
|
241
245
|
0x84: "DPPH", 0x85: "DPPL",
|
|
242
246
|
0x87: "CHARBASE",
|
|
243
247
|
0x88: "OFFSET",
|
|
244
|
-
// MARIA control reg
|
|
245
|
-
0x3C: "CTRL", // overlaps with P6C3 — convention varies; tag both
|
|
246
248
|
// RIOT (6532) regs at $280
|
|
247
249
|
0x0280: "SWCHA", 0x0281: "SWACNT", 0x0282: "SWCHB", 0x0283: "SWBCNT",
|
|
248
250
|
0x0284: "INTIM",
|
|
@@ -4,6 +4,16 @@ One page. Read once before you write your first game. The
|
|
|
4
4
|
TROUBLESHOOTING.md alongside this file is for when something's broken;
|
|
5
5
|
this is the "what's going on" version.
|
|
6
6
|
|
|
7
|
+
## Blank screen? Verify rendering first (no vision needed)
|
|
8
|
+
|
|
9
|
+
Compiles clean but nothing on screen? Call **`frame({op:'verify', frames:60})`** —
|
|
10
|
+
one call fuses a framebuffer pixel scan with the live LCDC and returns
|
|
11
|
+
`{verified:true|false|null, issues[]}`. `renderDisabled` = LCD off (LCDC.7 clear);
|
|
12
|
+
`blankScreen`/`nearlyBlank` with LCD on = nothing in the BG map / OAM / palette
|
|
13
|
+
(check the footguns below + read `memory({op:'read', region:'gb_vram'})`);
|
|
14
|
+
`verified:null` = step a frame first. Zero image tokens, frame-0-guarded — use it
|
|
15
|
+
as the first move when a change "did nothing."
|
|
16
|
+
|
|
7
17
|
## Five silent-failure footguns to know before you start (R26 + R27)
|
|
8
18
|
|
|
9
19
|
If your ROM compiles cleanly but doesn't render — or sprites land in
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Game Boy / Game Boy Color — symptom → fix
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
Stuck? Find your symptom below; each entry has the 1-line diagnosis and
|
|
4
10
|
the MCP tool call that confirms it. **Run the diagnosis BEFORE you start
|
|
5
11
|
bisecting the C source** — most "GB doesn't render" bugs are one of these
|
|
@@ -127,10 +127,10 @@ void oam_dma_init_hram(void) {
|
|
|
127
127
|
0x20, 0xFD, /* jr nz, -3 ─┘ spin while a != 0 */
|
|
128
128
|
0xC9, /* ret */
|
|
129
129
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
/* Use the pointer-walk memcpy_vram (not an indexed dst[i]=src[i] loop):
|
|
131
|
+
* SDCC sm83 miscompiles the indexed form into a high-pointer like
|
|
132
|
+
* HRAM_DMA_STUB ($FF80). memcpy_vram does *d++=*s++, which is safe. */
|
|
133
|
+
memcpy_vram(HRAM_DMA_STUB, stub, sizeof(stub));
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/* OAM DMA — copy 160 bytes from `src` to OAM ($FE00-$FE9F) via the
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Game Boy Advance — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
When something's broken. Read MENTAL_MODEL.md first
|
|
4
10
|
(via `platform({op:'doc', platform:"gba", name:"mental_model"})`).
|
|
5
11
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Game Boy Color — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
Read MENTAL_MODEL.md first (`platform({op:'doc', platform:"gbc",
|
|
4
10
|
name:"mental_model"})`). Most DMG-era troubleshooting from GB applies
|
|
5
11
|
unchanged — including the **two SDCC sm83 codegen footguns below**, which are
|
|
@@ -127,10 +127,10 @@ void oam_dma_init_hram(void) {
|
|
|
127
127
|
0x20, 0xFD, /* jr nz, -3 ─┘ spin while a != 0 */
|
|
128
128
|
0xC9, /* ret */
|
|
129
129
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
/* Use the pointer-walk memcpy_vram (not an indexed dst[i]=src[i] loop):
|
|
131
|
+
* SDCC sm83 miscompiles the indexed form into a high-pointer like
|
|
132
|
+
* HRAM_DMA_STUB ($FF80). memcpy_vram does *d++=*s++, which is safe. */
|
|
133
|
+
memcpy_vram(HRAM_DMA_STUB, stub, sizeof(stub));
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/* OAM DMA — copy 160 bytes from `src` to OAM ($FE00-$FE9F) via the
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Sega Genesis / Mega Drive — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
When something's broken. Read MENTAL_MODEL.md first for the "what's
|
|
4
10
|
going on" version (via `platform({op:'doc', platform:"genesis", name:"mental_model"})`).
|
|
5
11
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Game Gear — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
When something's broken. Read MENTAL_MODEL.md first
|
|
4
10
|
(`platform({op:'doc', platform:"gg", name:"mental_model"})`).
|
|
5
11
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Atari Lynx — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
Read MENTAL_MODEL.md first (`platform({op:'doc', platform:"lynx",
|
|
4
10
|
name:"mental_model"})`).
|
|
5
11
|
|
|
@@ -12,12 +12,16 @@ romdev ships a **hardware helper library** (`src/platforms/msx/lib/c/`:
|
|
|
12
12
|
`msx_psg_tone()` in plain C. It uses DIRECT Z80 I/O ports (the reliable path —
|
|
13
13
|
NOT fragile inline-asm BIOS wrappers).
|
|
14
14
|
|
|
15
|
-
The fastest way to a working game: **`scaffold({op:'
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
The fastest way to a working game: **`scaffold({op:'game', platform: "msx", genre:
|
|
16
|
+
"shmup"})`** — or any of `platformer` / `puzzle` / `sports` / `racing`, the full
|
|
17
|
+
genre set. For a smaller starting point use **`scaffold({op:'project', platform:
|
|
18
|
+
"msx", template: "sprite_move"})`** (also `music_sfx`, `catch_game`). Either drops
|
|
19
|
+
a complete, *building* project — a verified playable example + the helper lib +
|
|
20
|
+
the cart crt0 + docs. Read the example's `main.c`, then change it. Examples live in
|
|
21
|
+
`examples/msx/`. The `platformer` scaffold column-streams the SCREEN 2 name table
|
|
22
|
+
for a tile-by-tile side-scroll. **Gotcha:** read joystick **port 1**
|
|
23
|
+
(`msx_read_joystick(1)`) — port 0 is the keyboard, which an emulator's gamepad
|
|
24
|
+
doesn't drive.
|
|
21
25
|
|
|
22
26
|
## CPU — Z80 (16-bit address space, paged in 16 KB slots)
|
|
23
27
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# MSX — troubleshooting (symptom → cause → fix)
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
Read this when something's broken. For the "how it works" overview, read
|
|
4
10
|
MENTAL_MODEL.md first.
|
|
5
11
|
|