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.
- package/AGENTS.md +61 -13
- package/CHANGELOG.md +289 -0
- package/README.md +1 -1
- 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/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/gb/templates/default.c +110 -16
- package/examples/gb/templates/platformer.c +25 -4
- package/examples/gb/templates/puzzle.c +32 -2
- package/examples/gb/templates/racing.c +72 -8
- package/examples/gb/templates/shmup.c +38 -1
- package/examples/gb/templates/sports.c +48 -1
- package/examples/gba/templates/gba_hello.c +29 -11
- 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/gbc/templates/default.c +103 -26
- package/examples/gbc/templates/platformer.c +25 -4
- package/examples/gbc/templates/puzzle.c +32 -2
- package/examples/gbc/templates/racing.c +85 -19
- package/examples/gbc/templates/shmup.c +34 -1
- package/examples/gbc/templates/sports.c +45 -1
- 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/gg/templates/default.c +56 -18
- package/examples/gg/templates/platformer.c +18 -12
- package/examples/gg/templates/puzzle.c +38 -7
- package/examples/gg/templates/racing.c +51 -5
- package/examples/gg/templates/shmup.c +47 -3
- package/examples/gg/templates/sports.c +46 -3
- package/examples/lynx/templates/default.c +39 -8
- 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/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/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/sms/main.c +35 -6
- 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/sports.c +43 -2
- package/examples/snes/templates/default.c +50 -28
- package/examples/snes/templates/platformer-data.asm +22 -0
- package/examples/snes/templates/platformer.c +16 -1
- package/examples/snes/templates/puzzle-data.asm +22 -0
- package/examples/snes/templates/puzzle.c +17 -1
- 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/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +122 -1
- package/src/host/callbacks.js +9 -1
- package/src/host/types.js +15 -8
- package/src/http/skill-doc.js +1 -1
- package/src/http/tool-registry.js +27 -2
- package/src/mcp/tools/cart-parts.js +75 -3
- package/src/mcp/tools/disasm-rebuild.js +507 -0
- package/src/mcp/tools/disasm.js +95 -6
- package/src/mcp/tools/frame.js +168 -3
- package/src/mcp/tools/index.js +4 -4
- package/src/mcp/tools/lifecycle.js +4 -2
- package/src/mcp/tools/project.js +54 -9
- package/src/mcp/tools/state.js +201 -14
- package/src/mcp/tools/toolchain.js +89 -4
- package/src/mcp/tools/watch-memory.js +125 -14
- package/src/platforms/c64/MENTAL_MODEL.md +45 -1
- package/src/platforms/c64/d64.js +281 -0
- package/src/platforms/gb/MENTAL_MODEL.md +10 -0
- package/src/platforms/msx/MENTAL_MODEL.md +10 -6
- package/src/platforms/nes/MENTAL_MODEL.md +63 -2
- package/src/platforms/pce/MENTAL_MODEL.md +9 -4
- package/src/platforms/pce/lib/c/pce_video.c +1 -1
- package/src/rom-id/identifier.js +15 -0
- package/src/toolchains/cc65/cc65.js +8 -1
- package/src/toolchains/cc65/ines.js +145 -0
- 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 -2
- package/src/toolchains/gba-c/gba-c.js +6 -1
- package/src/toolchains/genesis-c/genesis-c.js +10 -2
- package/src/toolchains/parse-errors.js +67 -5
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# NES CHR-ROM linker config preset for romdev (cc65-C with FIXED tile art).
|
|
2
|
+
#
|
|
3
|
+
# Use this when you write NES homebrew in C AND ship your pattern tables as a
|
|
4
|
+
# fixed 8KB CHR-ROM blob (vs chr-ram-runtime, which has the CPU upload tiles to
|
|
5
|
+
# CHR-RAM at runtime). The cartridge is NROM-256: 32KB PRG + 8KB CHR-ROM.
|
|
6
|
+
#
|
|
7
|
+
# DIFFERENCE FROM chr-ram-runtime.cfg:
|
|
8
|
+
# - Adds a ROM2 memory area (8KB CHR-ROM bank) + a CHARS segment. Put your
|
|
9
|
+
# tile data in `.segment "CHARS"` (e.g. `.incbin "tiles.chr"`); it lands in
|
|
10
|
+
# the CHR-ROM bank the PPU reads pattern tables from directly.
|
|
11
|
+
# - The companion crt0 (chr-rom.crt0.s) emits an iNES header with byte 5 = 1
|
|
12
|
+
# (one 8KB CHR-ROM bank) and does NOT clear CHR-RAM (there is none).
|
|
13
|
+
#
|
|
14
|
+
# To use this preset:
|
|
15
|
+
# 1. linkerConfig: "chr-rom" on build({output:'rom'}) (pulls in chr-rom.crt0.s)
|
|
16
|
+
# 2. Supply your tile art: a source with `.segment "CHARS"` + `.incbin
|
|
17
|
+
# "tiles.chr"`, plus the blob via binaryIncludePaths:{ "tiles.chr": path }.
|
|
18
|
+
# The CHARS segment must total exactly 8192 bytes (fill is on, so a short
|
|
19
|
+
# blob is padded; an over-long one errors).
|
|
20
|
+
# 3. For a DIFFERENT cart config (NROM-128, >8KB CHR, a mapper, vertical
|
|
21
|
+
# mirroring), don't use this preset — use the parametric `inesHeader`
|
|
22
|
+
# build option instead (it generates the header + .cfg for any NROM shape).
|
|
23
|
+
#
|
|
24
|
+
# This is the cc65-C-with-segments shape. To rebuild a COMMERCIAL game from its
|
|
25
|
+
# disassembly (one flat CODE blob with embedded vectors), use `inesHeader`.
|
|
26
|
+
|
|
27
|
+
SYMBOLS {
|
|
28
|
+
__STACKSIZE__: type = weak, value = $0300;
|
|
29
|
+
}
|
|
30
|
+
MEMORY {
|
|
31
|
+
ZP: file = "", start = $0002, size = $001A, type = rw, define = yes;
|
|
32
|
+
|
|
33
|
+
# iNES Cartridge Header — emitted by chr-rom.crt0.s (HEADER segment).
|
|
34
|
+
HEADER: file = %O, start = $0000, size = $0010, fill = yes;
|
|
35
|
+
|
|
36
|
+
# PRG-ROM: 2 × 16K = 32K (NROM-256).
|
|
37
|
+
ROM0: file = %O, start = $8000, size = $7FFA, fill = yes, define = yes;
|
|
38
|
+
|
|
39
|
+
# Hardware vectors at end of PRG.
|
|
40
|
+
ROMV: file = %O, start = $FFFA, size = $0006, fill = yes;
|
|
41
|
+
|
|
42
|
+
# CHR-ROM: one 8K bank. The PPU reads pattern tables straight from here —
|
|
43
|
+
# this is the whole point of the CHR-ROM preset.
|
|
44
|
+
ROM2: file = %O, start = $0000, size = $2000, fill = yes, define = yes;
|
|
45
|
+
|
|
46
|
+
SRAM: file = "", start = $0500, size = __STACKSIZE__, define = yes;
|
|
47
|
+
|
|
48
|
+
# BSS / DATA live in real RAM ($0300-$04FF, 512 bytes). NROM (mapper 0)
|
|
49
|
+
# with no battery has $6000-$7FFF UNMAPPED — don't put BSS there.
|
|
50
|
+
RAM: file = "", start = $0300, size = $0200, define = yes;
|
|
51
|
+
|
|
52
|
+
# Shadow OAM at $0200 (page 2). NMI handler DMAs it to PPU sprite RAM.
|
|
53
|
+
OAM: file = "", start = $0200, size = $0100;
|
|
54
|
+
}
|
|
55
|
+
SEGMENTS {
|
|
56
|
+
ZEROPAGE: load = ZP, type = zp;
|
|
57
|
+
HEADER: load = HEADER, type = ro;
|
|
58
|
+
STARTUP: load = ROM0, type = ro, define = yes;
|
|
59
|
+
LOWCODE: load = ROM0, type = ro, optional = yes;
|
|
60
|
+
ONCE: load = ROM0, type = ro, optional = yes;
|
|
61
|
+
CODE: load = ROM0, type = ro, define = yes;
|
|
62
|
+
RODATA: load = ROM0, type = ro, define = yes;
|
|
63
|
+
DATA: load = ROM0, run = RAM, type = rw, define = yes;
|
|
64
|
+
VECTORS: load = ROMV, type = rw;
|
|
65
|
+
CHARS: load = ROM2, type = ro;
|
|
66
|
+
BSS: load = RAM, type = bss, define = yes;
|
|
67
|
+
OAM: load = OAM, type = bss, optional = yes;
|
|
68
|
+
}
|
|
69
|
+
FEATURES {
|
|
70
|
+
CONDES: type = constructor,
|
|
71
|
+
label = __CONSTRUCTOR_TABLE__,
|
|
72
|
+
count = __CONSTRUCTOR_COUNT__,
|
|
73
|
+
segment = ONCE;
|
|
74
|
+
CONDES: type = destructor,
|
|
75
|
+
label = __DESTRUCTOR_TABLE__,
|
|
76
|
+
count = __DESTRUCTOR_COUNT__,
|
|
77
|
+
segment = RODATA;
|
|
78
|
+
CONDES: type = interruptor,
|
|
79
|
+
label = __INTERRUPTOR_TABLE__,
|
|
80
|
+
count = __INTERRUPTOR_COUNT__,
|
|
81
|
+
segment = RODATA,
|
|
82
|
+
import = __CALLIRQ__;
|
|
83
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
; crt0 for NES CHR-ROM mode + nes_runtime NMI handler.
|
|
2
|
+
;
|
|
3
|
+
; Companion to the linkerConfig:"chr-rom" preset. Identical to
|
|
4
|
+
; chr-ram-runtime.crt0.s EXCEPT:
|
|
5
|
+
; - the iNES header sets byte 5 = 1 (one 8KB CHR-ROM bank), and
|
|
6
|
+
; - there is NO CHR-RAM clear loop — pattern tables come from the CHR-ROM
|
|
7
|
+
; bank (the CHARS segment / ROM2 area) the PPU reads directly.
|
|
8
|
+
;
|
|
9
|
+
; The NMI handler is the canonical sprite-engine sequence (OAM DMA + VRAM-queue
|
|
10
|
+
; flush + scroll + PPUCTRL), same as the CHR-RAM runtime.
|
|
11
|
+
;
|
|
12
|
+
; Loaded silently by the linkerConfig:"chr-rom" preset.
|
|
13
|
+
|
|
14
|
+
.export _exit
|
|
15
|
+
.export __STARTUP__ : absolute = 1
|
|
16
|
+
.export start
|
|
17
|
+
.export nmi
|
|
18
|
+
.export irq
|
|
19
|
+
.export _shadow_oam
|
|
20
|
+
|
|
21
|
+
.import initlib, donelib, callmain
|
|
22
|
+
.import _main, zerobss, copydata
|
|
23
|
+
.import __RAM_START__, __RAM_SIZE__
|
|
24
|
+
.import __SRAM_START__, __SRAM_SIZE__
|
|
25
|
+
.import _vram_queue_flush
|
|
26
|
+
.import _scroll_x, _scroll_y, _ppuctrl_value, _nmi_counter
|
|
27
|
+
.importzp c_sp
|
|
28
|
+
|
|
29
|
+
; ------------------------------------------------------------------------
|
|
30
|
+
; 16-byte iNES header — CHR-ROM (byte 5 = 1 → one 8KB CHR-ROM bank).
|
|
31
|
+
|
|
32
|
+
.segment "HEADER"
|
|
33
|
+
.byte $4e, $45, $53, $1a ; "NES" + EOF
|
|
34
|
+
.byte 2 ; PRG-ROM banks (16K each) → 32K
|
|
35
|
+
.byte 1 ; CHR-ROM banks (8K each) → 8K CHR-ROM
|
|
36
|
+
.byte %00000001 ; flags6 — vertical mirroring
|
|
37
|
+
.byte %00000000 ; flags7 — mapper hi nybble
|
|
38
|
+
.byte 0, 0, 0, 0, 0, 0, 0, 0
|
|
39
|
+
|
|
40
|
+
; ------------------------------------------------------------------------
|
|
41
|
+
.segment "STARTUP"
|
|
42
|
+
|
|
43
|
+
start:
|
|
44
|
+
sei
|
|
45
|
+
cld
|
|
46
|
+
ldx #$ff
|
|
47
|
+
txs
|
|
48
|
+
|
|
49
|
+
; Disable everything that could fire during init.
|
|
50
|
+
lda #0
|
|
51
|
+
sta $2000 ; disable NMI
|
|
52
|
+
sta $2001 ; disable rendering
|
|
53
|
+
sta $4010 ; disable DMC IRQ
|
|
54
|
+
sta $4015 ; disable APU channels
|
|
55
|
+
bit $2002 ; clear vblank flag
|
|
56
|
+
|
|
57
|
+
; Wait two VBlanks before touching the PPU (standard NES init).
|
|
58
|
+
@vbl1: bit $2002
|
|
59
|
+
bpl @vbl1
|
|
60
|
+
@vbl2: bit $2002
|
|
61
|
+
bpl @vbl2
|
|
62
|
+
|
|
63
|
+
; Initialise shadow_oam to Y=$FF (off-screen) before anything else.
|
|
64
|
+
ldx #0
|
|
65
|
+
lda #$ff
|
|
66
|
+
@oam: sta _shadow_oam,x
|
|
67
|
+
inx
|
|
68
|
+
bne @oam
|
|
69
|
+
|
|
70
|
+
; NO CHR-RAM clear — pattern tables live in the CHR-ROM bank (CHARS).
|
|
71
|
+
; Just point PPUADDR at the palette ($3F00) ready for the caller.
|
|
72
|
+
bit $2002 ; reset PPUADDR latch
|
|
73
|
+
lda #$3F
|
|
74
|
+
sta $2006
|
|
75
|
+
lda #$00
|
|
76
|
+
sta $2006
|
|
77
|
+
|
|
78
|
+
; Clear BSS + copy DATA (cc65 conventions).
|
|
79
|
+
jsr zerobss
|
|
80
|
+
jsr copydata
|
|
81
|
+
|
|
82
|
+
; Set up cc65's C parameter stack pointer.
|
|
83
|
+
lda #<(__SRAM_START__ + __SRAM_SIZE__)
|
|
84
|
+
ldx #>(__SRAM_START__ + __SRAM_SIZE__)
|
|
85
|
+
sta c_sp
|
|
86
|
+
stx c_sp+1
|
|
87
|
+
|
|
88
|
+
jsr initlib
|
|
89
|
+
jsr callmain
|
|
90
|
+
|
|
91
|
+
_exit: jsr donelib
|
|
92
|
+
jmp start
|
|
93
|
+
|
|
94
|
+
; ------------------------------------------------------------------------
|
|
95
|
+
; NMI handler — runs every vblank when ppuctrl bit 7 is set.
|
|
96
|
+
|
|
97
|
+
.segment "STARTUP"
|
|
98
|
+
|
|
99
|
+
nmi:
|
|
100
|
+
pha
|
|
101
|
+
txa
|
|
102
|
+
pha
|
|
103
|
+
tya
|
|
104
|
+
pha
|
|
105
|
+
|
|
106
|
+
; OAM DMA: copy 256 bytes from $0200 to PPU OAM. Takes 513 cycles.
|
|
107
|
+
lda #$00
|
|
108
|
+
sta $2003 ; PPU OAMADDR = 0
|
|
109
|
+
lda #$02 ; high byte of $0200
|
|
110
|
+
sta $4014 ; PPU OAMDMA — kicks off the copy
|
|
111
|
+
|
|
112
|
+
; Flush the VRAM queue (nametable/palette writes game code stashed).
|
|
113
|
+
jsr _vram_queue_flush
|
|
114
|
+
|
|
115
|
+
; Reset PPUADDR to $2000 so the PPU doesn't sample random VRAM as BG.
|
|
116
|
+
bit $2002
|
|
117
|
+
lda #$20
|
|
118
|
+
sta $2006
|
|
119
|
+
lda #$00
|
|
120
|
+
sta $2006
|
|
121
|
+
|
|
122
|
+
; Set scroll (two PPUSCROLL writes: x then y).
|
|
123
|
+
lda _scroll_x
|
|
124
|
+
sta $2005
|
|
125
|
+
lda _scroll_y
|
|
126
|
+
sta $2005
|
|
127
|
+
|
|
128
|
+
; Re-enable NMI + base nametable + pattern-table bits via cached PPUCTRL.
|
|
129
|
+
lda _ppuctrl_value
|
|
130
|
+
sta $2000
|
|
131
|
+
|
|
132
|
+
; Tick the frame counter so ppu_wait_nmi can return.
|
|
133
|
+
inc _nmi_counter
|
|
134
|
+
|
|
135
|
+
pla
|
|
136
|
+
tay
|
|
137
|
+
pla
|
|
138
|
+
tax
|
|
139
|
+
pla
|
|
140
|
+
rti
|
|
141
|
+
|
|
142
|
+
irq: rti
|
|
143
|
+
|
|
144
|
+
; ------------------------------------------------------------------------
|
|
145
|
+
; Shadow OAM at $0200 — the NMI handler DMAs this to the PPU each frame.
|
|
146
|
+
.segment "OAM"
|
|
147
|
+
_shadow_oam: .res 256
|
|
148
|
+
|
|
149
|
+
; ------------------------------------------------------------------------
|
|
150
|
+
.segment "VECTORS"
|
|
151
|
+
.word nmi ; $FFFA
|
|
152
|
+
.word start ; $FFFC
|
|
153
|
+
.word irq ; $FFFE
|
|
@@ -67,7 +67,7 @@ function firstDiff(a, b) {
|
|
|
67
67
|
// m68k (genesis) → objdump → m68k-elf-as/ld/objcopy
|
|
68
68
|
// arm (gba) → objdump → arm-none-eabi-as/ld/objcopy
|
|
69
69
|
|
|
70
|
-
const CPU_FAMILY = {
|
|
70
|
+
export const CPU_FAMILY = {
|
|
71
71
|
nes: "6502", c64: "6502", atari2600: "6502", atari7800: "6502", lynx: "6502",
|
|
72
72
|
snes: "65816",
|
|
73
73
|
sms: "z80", gg: "z80", msx: "z80",
|
|
@@ -254,7 +254,15 @@ async function reassembleGnuNative(disasm, startAddress, original, tools, family
|
|
|
254
254
|
if (!pinned) { const n = codeIdx.find((i) => !forced.has(i)); if (n == null) break; forced.add(n); }
|
|
255
255
|
}
|
|
256
256
|
// Floor: clean all-`.byte` (proven byte-exact, no labels to perturb layout).
|
|
257
|
-
|
|
257
|
+
// Mirror build()'s `.org` for the no-link (z80/gbz80) path: without it,
|
|
258
|
+
// objcopy emits the section from file offset 0, and assemble()'s
|
|
259
|
+
// `bin.slice(startAddress, …)` then returns bytes that are `startAddress`-short
|
|
260
|
+
// (empty for a $4000/$8000-based region) — so any non-zero-org region (MSX
|
|
261
|
+
// $4010, GB bank1 $4000, …) silently fails the floor. The linked path sets the
|
|
262
|
+
// origin via the link script, so it must NOT carry a redundant `.org`.
|
|
263
|
+
const rows = [".section .text", ".global _start"];
|
|
264
|
+
if (tools.noLink) rows.push(`.org 0x${startAddress.toString(16)}`);
|
|
265
|
+
rows.push("_start:");
|
|
258
266
|
for (let i = 0; i < original.length; i += 16) rows.push("\t.byte " + Array.from(original.slice(i, i + 16)).map((b) => "0x" + b.toString(16).padStart(2, "0")).join(","));
|
|
259
267
|
const r = await assemble(rows.join("\n") + "\n");
|
|
260
268
|
const ok = r.ok && r.bytes.length === original.length && firstDiff(original, r.bytes) < 0;
|
|
@@ -92,7 +92,12 @@ export async function buildGbaC(args) {
|
|
|
92
92
|
// per-symbol `.bss.<name>`/`.data.<name>` line — that's what lets
|
|
93
93
|
// symbols({op:'resolve'}) turn a static C global's name into an address on GBA
|
|
94
94
|
// (same as SGDK does for Genesis). Pure metadata; no codegen change to what's kept.
|
|
95
|
-
|
|
95
|
+
// -Wall -Wextra so the agent SEES warnings (unused vars, implicit decls,
|
|
96
|
+
// sign-compare, etc.) — they're parsed into structured issues[]. Without these
|
|
97
|
+
// gcc is silent and agents build blind. -Wno-unused-parameter keeps the common
|
|
98
|
+
// intentional `(void)`-style scaffold params from being noise. Applied to USER
|
|
99
|
+
// .c only (the libtonc/maxmod SDK is a prebuilt seed, not recompiled here).
|
|
100
|
+
const cc1Options = args.cc1Options ?? ["-O2", "-mthumb", "-ffunction-sections", "-fdata-sections", "-Wall", "-Wextra", "-Wno-unused-parameter"];
|
|
96
101
|
const sources = normalizeGbaSources(args);
|
|
97
102
|
const binaryIncludes = args.binaryIncludes ?? {};
|
|
98
103
|
|
|
@@ -264,6 +264,12 @@ async function buildWithSgdk({ sources, headers, binaryIncludes, cc1Options, reb
|
|
|
264
264
|
"-fdata-sections",
|
|
265
265
|
...cc1Options,
|
|
266
266
|
];
|
|
267
|
+
// USER source gets warnings on so the agent SEES its bugs (unused vars,
|
|
268
|
+
// implicit decls, …) parsed into structured issues[]. The SGDK runtime is
|
|
269
|
+
// compiled WITHOUT these (sgdkCc1Options) — we can't fix SDK warnings and they'd
|
|
270
|
+
// bury the agent's own. -Wno-unused-parameter avoids the common `(void)hard`
|
|
271
|
+
// scaffold-param noise.
|
|
272
|
+
const userCc1Options = [...sgdkCc1Options, "-Wall", "-Wextra", "-Wno-unused-parameter"];
|
|
267
273
|
|
|
268
274
|
// ── Stage A: gather SGDK headers (visible to tcc via tcc-style flat mount) ──
|
|
269
275
|
// cc1's -iquote /work picks up sibling files mounted alongside main.c.
|
|
@@ -280,7 +286,7 @@ async function buildWithSgdk({ sources, headers, binaryIncludes, cc1Options, reb
|
|
|
280
286
|
const cc = await runCc1m68k({
|
|
281
287
|
source: sources[cName],
|
|
282
288
|
headers: tccHeaders,
|
|
283
|
-
options:
|
|
289
|
+
options: userCc1Options,
|
|
284
290
|
});
|
|
285
291
|
log += `--- cc1 (${cName}) ---\n` + (cc.log || "(ok)") + "\n";
|
|
286
292
|
if (cc.exitCode !== 0 || !cc.asmSource) {
|
|
@@ -469,12 +475,14 @@ async function buildMinimal(args) {
|
|
|
469
475
|
// ── Stage 1: compile each .c file via cc1 → .s ─────────────────
|
|
470
476
|
/** @type {Record<string, Uint8Array>} */
|
|
471
477
|
const userObjs = {};
|
|
478
|
+
// User .c gets warnings on (minimal path has no SDK to flood). See buildWithSgdk.
|
|
479
|
+
const userCc1Options = [...cc1Options, "-Wall", "-Wextra", "-Wno-unused-parameter"];
|
|
472
480
|
const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
|
|
473
481
|
for (const cName of cFiles) {
|
|
474
482
|
const cc = await runCc1m68k({
|
|
475
483
|
source: sources[cName],
|
|
476
484
|
headers,
|
|
477
|
-
options:
|
|
485
|
+
options: userCc1Options,
|
|
478
486
|
});
|
|
479
487
|
log += `--- cc1 (${cName}) ---\n` + (cc.log || "(ok)") + "\n";
|
|
480
488
|
if (cc.exitCode !== 0 || !cc.asmSource) {
|
|
@@ -41,10 +41,12 @@ export function parseBuildLog(log) {
|
|
|
41
41
|
} else if (/^vasm/.test(baseStage)) {
|
|
42
42
|
issues.push(...parseVasm(text));
|
|
43
43
|
} else if (/^sdcc$|^sdasz80$|^sdasgb$|^sdld$|^mcpp$/.test(baseStage)) {
|
|
44
|
-
// SDCC family: sdcc / sdasz80 / sdasgb / sdld / mcpp
|
|
45
|
-
// `file:line:
|
|
46
|
-
//
|
|
44
|
+
// SDCC family: sdcc / sdasz80 / sdasgb / sdld / mcpp. Some diagnostics use
|
|
45
|
+
// the cc65-style `file:line: Error: msg`; SDCC's frontend ALSO emits a
|
|
46
|
+
// keyword-less form — `main.c:2: syntax error: token -> ';' ; column 44`
|
|
47
|
+
// and `main.c:N: warning NNN: msg` — which parseCc65Like misses. Run both.
|
|
47
48
|
issues.push(...parseCc65Like(text, baseStage));
|
|
49
|
+
issues.push(...parseSdcc(text, baseStage));
|
|
48
50
|
} else if (/^wla|^wlalink|^wladx/.test(baseStage)) {
|
|
49
51
|
// SNES C path: wla-65816 assembler + wlalink linker. wlalink floods a
|
|
50
52
|
// symbol-table dump on failure — parseWla extracts just the diagnostics.
|
|
@@ -60,11 +62,18 @@ export function parseBuildLog(log) {
|
|
|
60
62
|
// Tag everything with the (possibly empty) actual stage name so an
|
|
61
63
|
// assembler error doesn't mistakenly report as "asar" on a non-SNES
|
|
62
64
|
// build.
|
|
65
|
+
// Try EVERY parser — some toolchains (vasm genesis-asm) emit no
|
|
66
|
+
// "--- stage ---" marker, so the whole log lands here unnamed; if we skip
|
|
67
|
+
// a parser the error is silently swallowed (issues[] empty on a real
|
|
68
|
+
// failure). Include vasm + sdcc + wla, which the old fallback omitted.
|
|
63
69
|
const tag = baseStage || "unknown";
|
|
64
70
|
issues.push(...parseCc65Like(text, tag));
|
|
71
|
+
issues.push(...parseSdcc(text, tag));
|
|
65
72
|
issues.push(...parseDasm(text));
|
|
66
73
|
issues.push(...parseAsar(text, tag));
|
|
67
74
|
issues.push(...parseRgbds(text, tag));
|
|
75
|
+
issues.push(...parseVasm(text));
|
|
76
|
+
issues.push(...parseWla(text, tag));
|
|
68
77
|
issues.push(...parseGnuToolchain(text, tag));
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -122,6 +131,57 @@ function parseCc65Like(text, stage) {
|
|
|
122
131
|
return out;
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
// SDCC's keyword-less diagnostics that parseCc65Like (which requires an explicit
|
|
135
|
+
// "Error:"/"Warning:" word) doesn't catch:
|
|
136
|
+
// /work/main.c:2: syntax error: token -> ';' ; column 44
|
|
137
|
+
// /work/main.c:7: warning 112: function 'foo' implicit declaration
|
|
138
|
+
// /work/main.c:9: error 20: undefined identifier 'x'
|
|
139
|
+
// We classify by the leading word after `file:line:` (syntax error / error / warning).
|
|
140
|
+
function parseSdcc(text, stage) {
|
|
141
|
+
const out = [];
|
|
142
|
+
const re = /^(?<file>[^\n:]+):(?<line>\d+):\s*(?<kind>syntax error|error(?:\s+\d+)?|warning(?:\s+\d+)?):?\s*(?<msg>.*)$/gm;
|
|
143
|
+
let m;
|
|
144
|
+
while ((m = re.exec(text))) {
|
|
145
|
+
const kind = m.groups.kind.toLowerCase();
|
|
146
|
+
// Skip the forms parseCc65Like already caught ("Error:" capitalized w/ colon)
|
|
147
|
+
// — this regex is case-insensitive on `error`/`warning`, but parseCc65Like
|
|
148
|
+
// only matches when a colon immediately follows the keyword AND it's
|
|
149
|
+
// capitalized; SDCC's lowercase keyword-less form is what we add here.
|
|
150
|
+
const severity = kind.startsWith("warning") ? "warning"
|
|
151
|
+
: (kind.startsWith("error") || kind === "syntax error") ? "error" : "info";
|
|
152
|
+
const message = kind === "syntax error"
|
|
153
|
+
? ("syntax error: " + m.groups.msg).trim().replace(/\s*;\s*$/, "")
|
|
154
|
+
: m.groups.msg.trim();
|
|
155
|
+
out.push({
|
|
156
|
+
severity,
|
|
157
|
+
file: m.groups.file,
|
|
158
|
+
line: parseInt(m.groups.line, 10),
|
|
159
|
+
message: message.replace(/\x1b\[[0-9;]*m/g, ""),
|
|
160
|
+
stage,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// sdld/ASlink linker diagnostics have NO file:line — they reference a symbol +
|
|
164
|
+
// module. The most common is an undefined symbol (a call to a function that was
|
|
165
|
+
// never defined/linked). Without parsing these the agent sees "build failed"
|
|
166
|
+
// with no reason in issues[] (the error lived only in the raw log).
|
|
167
|
+
// ?ASlink-Warning-Undefined Global '_foo' referenced by module '_main'
|
|
168
|
+
// ?ASlink-Error-...
|
|
169
|
+
const linkRe = /^\?ASlink-(?<sev>Warning|Error)-(?<msg>.+)$/gm;
|
|
170
|
+
let lm;
|
|
171
|
+
while ((lm = linkRe.exec(text))) {
|
|
172
|
+
const msg = lm.groups.msg.trim();
|
|
173
|
+
// An "Undefined Global" is effectively an error even though ASlink labels it
|
|
174
|
+
// a warning — the ROM won't run. Promote it so the agent treats it as fatal.
|
|
175
|
+
const isUndef = /undefined\s+global/i.test(msg);
|
|
176
|
+
out.push({
|
|
177
|
+
severity: lm.groups.sev === "Error" || isUndef ? "error" : "warning",
|
|
178
|
+
message: "linker: " + msg,
|
|
179
|
+
stage: "sdld",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
125
185
|
// dasm example:
|
|
126
186
|
// main.asm (1): error: Unknown Mnemonic 'is'.
|
|
127
187
|
function parseDasm(text) {
|
|
@@ -248,13 +308,15 @@ function parseWla(text, stage = "wla") {
|
|
|
248
308
|
// vasm example:
|
|
249
309
|
// error 22 in line 5 of "/work/main.s": ...
|
|
250
310
|
// warning 1003 in line 8 of "main.s": ...
|
|
311
|
+
// fatal error 13 in line 1 of "/work/main.s": could not open <x.bin> for input
|
|
312
|
+
// (← a MISSING incbin asset: the #1 thing an agent forgets to pass)
|
|
251
313
|
function parseVasm(text) {
|
|
252
314
|
const out = [];
|
|
253
|
-
const re = /^(?<sev>error|warning)\s+\d+\s+in\s+line\s+(?<line>\d+)\s+of\s+"(?<file>[^"]+)":\s*(?<msg>.+)$/gm;
|
|
315
|
+
const re = /^(?<sev>fatal error|error|warning)\s+\d+\s+in\s+line\s+(?<line>\d+)\s+of\s+"(?<file>[^"]+)":\s*(?<msg>.+)$/gm;
|
|
254
316
|
let m;
|
|
255
317
|
while ((m = re.exec(text))) {
|
|
256
318
|
out.push({
|
|
257
|
-
severity: m.groups.sev,
|
|
319
|
+
severity: m.groups.sev === "warning" ? "warning" : "error",
|
|
258
320
|
file: m.groups.file,
|
|
259
321
|
line: parseInt(m.groups.line, 10),
|
|
260
322
|
message: m.groups.msg.trim(),
|