@henols/c64-re-tools 1.1.0 → 2.0.0-rc.3
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/LICENSE +21 -0
- package/README.md +138 -37
- package/distribution/package.ts +36 -0
- package/distribution/plugin.ts +120 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +73 -0
- package/skills/c64-assembler/scripts/assemble.ts +103 -0
- package/skills/c64-basic/SKILL.md +67 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +79 -0
- package/skills/c64-disk/scripts/disk.ts +65 -0
- package/skills/c64-emulator/SKILL.md +124 -0
- package/skills/c64-knowledge/SKILL.md +102 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
- package/skills/c64-memory-map/SKILL.md +76 -0
- package/skills/c64-memory-map/scripts/memmap.ts +81 -0
- package/skills/c64-memory-map/scripts/platform.ts +424 -0
- package/skills/c64-provenance/SKILL.md +76 -0
- package/skills/c64-provenance/scripts/compare.ts +92 -0
- package/skills/c64-provenance/scripts/provenance.ts +83 -0
- package/skills/c64-reverse-engineering/SKILL.md +90 -0
- package/skills/c64-static-analysis/SKILL.md +96 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
- package/skills/c64-static-analysis/scripts/findings.ts +73 -0
- package/skills/c64-testing/SKILL.md +79 -0
- package/skills/c64-testing/references/scenario-format.md +76 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/scenario.ts +473 -0
- package/skills/c64-testing/scripts/test.ts +55 -0
- package/skills/c64-unpacker/SKILL.md +82 -0
- package/skills/c64-unpacker/scripts/evidence.ts +135 -0
- package/skills/c64-unpacker/scripts/unpack.ts +216 -0
- package/src/c64.ts +281 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +122 -0
- package/src/cli/status.ts +34 -0
- package/src/host/main.ts +106 -0
- package/src/host/server.ts +376 -0
- package/src/host/tools/c1541.ts +255 -0
- package/src/host/tools/index.ts +31 -0
- package/src/host/tools/petcat.ts +70 -0
- package/src/host/tools/status.ts +30 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +761 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +108 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +402 -0
- package/src/host/vice/screen.ts +183 -0
- package/src/host/vice/session.ts +1316 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/connect.ts +238 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +100 -0
- package/src/host-client/vice-session.ts +254 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +128 -0
- package/src/knowledge/import.ts +344 -0
- package/src/knowledge/read.ts +191 -0
- package/src/knowledge/schema.ts +140 -0
- package/src/knowledge/write.ts +348 -0
- package/src/mcp/main.ts +106 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/tools/debug.ts +237 -0
- package/src/mcp/tools/execution.ts +116 -0
- package/src/mcp/tools/input.ts +69 -0
- package/src/mcp/tools/machine.ts +97 -0
- package/src/mcp/tools/media.ts +106 -0
- package/src/mcp/tools/memory.ts +203 -0
- package/src/mcp/tools/video.ts +260 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +141 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +157 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +242 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +101 -0
- package/src/native/types.ts +228 -0
- package/src/native/watchdog.ts +78 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +243 -0
- package/src/protocol/tools.ts +247 -0
- package/src/protocol/vice.ts +1235 -0
- package/src/script.ts +59 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +162 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Video and chip tools: c64_vicii, c64_sprite, c64_cia, c64_sid, c64_screen
|
|
2
|
+
// and c64_observe.
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { formatC64Address } from "../../c64.ts";
|
|
7
|
+
import { WireFailure } from "../../protocol/messages.ts";
|
|
8
|
+
import { CIA_SELECTIONS, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_OBSERVE_BYTES, MAX_OBSERVE_RANGES, SPACES, VICII_MODES, type ObserveParams } from "../../protocol/vice.ts";
|
|
9
|
+
import { AddressInput, AddressOutput, Byte, defineTool, HexData, MemoryViewInput, requireFields, SpaceInput, ToolOutput, TransientName } from "../server.ts";
|
|
10
|
+
|
|
11
|
+
const Color = z.number().int().min(0).max(15);
|
|
12
|
+
const NO_CHANGE = "This does not change the run state of the machine.";
|
|
13
|
+
|
|
14
|
+
export const c64Vicii = defineTool({
|
|
15
|
+
name: "c64_vicii",
|
|
16
|
+
title: "VIC-II state",
|
|
17
|
+
description:
|
|
18
|
+
"Get the state of the VIC-II video chip: the raster line, the graphics mode, the screen and graphics addresses, " +
|
|
19
|
+
`the scroll values and the colors. The addresses include the 16 KB video bank that CIA 2 selects. ${NO_CHANGE}`,
|
|
20
|
+
inputSchema: z.object({}).strict(),
|
|
21
|
+
outputSchema: z.object({
|
|
22
|
+
rasterLine: z.number().int().min(0),
|
|
23
|
+
mode: z.enum(VICII_MODES),
|
|
24
|
+
screenAddress: AddressOutput,
|
|
25
|
+
graphicsAddress: AddressOutput.describe("character set in text modes, bitmap in bitmap modes"),
|
|
26
|
+
scrollX: z.number().int().min(0).max(7),
|
|
27
|
+
scrollY: z.number().int().min(0).max(7),
|
|
28
|
+
borderColor: Color,
|
|
29
|
+
backgroundColors: z.array(Color).length(4),
|
|
30
|
+
}),
|
|
31
|
+
readOnly: true,
|
|
32
|
+
async run(_input, session) {
|
|
33
|
+
const state = await session.vicii();
|
|
34
|
+
return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const SpriteOutput = z.object({
|
|
39
|
+
index: z.number().int().min(0).max(7),
|
|
40
|
+
x: z.number().int().min(0),
|
|
41
|
+
y: z.number().int().min(0),
|
|
42
|
+
enabled: z.boolean(),
|
|
43
|
+
color: Color,
|
|
44
|
+
multicolor: z.boolean(),
|
|
45
|
+
expandX: z.boolean(),
|
|
46
|
+
expandY: z.boolean(),
|
|
47
|
+
behindBackground: z.boolean(),
|
|
48
|
+
dataAddress: AddressOutput,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const c64Sprite = defineTool({
|
|
52
|
+
name: "c64_sprite",
|
|
53
|
+
title: "Sprite state",
|
|
54
|
+
description:
|
|
55
|
+
"Get the state of sprites 0 to 7: position, enabled, color, multicolor, expansion, priority and the address of the sprite data. " +
|
|
56
|
+
`sprites selects which sprites to give. Without it, you get all eight. ${NO_CHANGE}`,
|
|
57
|
+
inputSchema: z
|
|
58
|
+
.object({ sprites: z.array(z.number().int().min(0).max(7)).min(1).max(8).optional().describe("sprite numbers 0 to 7, each once") })
|
|
59
|
+
.strict(),
|
|
60
|
+
outputSchema: z.object({ sprites: z.array(SpriteOutput) }),
|
|
61
|
+
readOnly: true,
|
|
62
|
+
async run(input, session) {
|
|
63
|
+
const result = await session.sprites(input.sprites ?? []);
|
|
64
|
+
return { sprites: result.sprites.map((sprite) => ({ ...sprite, dataAddress: formatC64Address(sprite.dataAddress) })) };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const CiaOutput = z.object({
|
|
69
|
+
id: z.union([z.literal(1), z.literal(2)]),
|
|
70
|
+
portA: Byte,
|
|
71
|
+
portB: Byte,
|
|
72
|
+
ddrA: Byte,
|
|
73
|
+
ddrB: Byte,
|
|
74
|
+
timerA: z.number().int().min(0).max(0xffff),
|
|
75
|
+
timerB: z.number().int().min(0).max(0xffff),
|
|
76
|
+
controlA: Byte,
|
|
77
|
+
controlB: Byte,
|
|
78
|
+
interruptStatus: Byte,
|
|
79
|
+
tod: z.object({ hours: z.number().int(), minutes: z.number().int(), seconds: z.number().int(), tenths: z.number().int() }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const c64Cia = defineTool({
|
|
83
|
+
name: "c64_cia",
|
|
84
|
+
title: "CIA state",
|
|
85
|
+
description:
|
|
86
|
+
"Get the state of CIA 1, CIA 2 or both. The state has the ports, the data direction registers and the current timer values. " +
|
|
87
|
+
`It also has the control registers, the interrupt flags and the time of day (hours 0 to 23). ${NO_CHANGE}`,
|
|
88
|
+
inputSchema: z.object({ cia: z.enum(CIA_SELECTIONS).default("both") }).strict(),
|
|
89
|
+
outputSchema: z.object({ chips: z.array(CiaOutput) }),
|
|
90
|
+
readOnly: true,
|
|
91
|
+
async run(input, session) {
|
|
92
|
+
return session.cia(input.cia);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const c64Sid = defineTool({
|
|
97
|
+
name: "c64_sid",
|
|
98
|
+
title: "SID state",
|
|
99
|
+
description:
|
|
100
|
+
"Get the SID sound chip registers as the program last wrote them: the three voices, the filter and the volume. " +
|
|
101
|
+
`This is register state, not an analysis of the sound. ${NO_CHANGE}`,
|
|
102
|
+
inputSchema: z.object({}).strict(),
|
|
103
|
+
outputSchema: z.object({
|
|
104
|
+
voices: z.array(
|
|
105
|
+
z.object({
|
|
106
|
+
index: z.number().int().min(1).max(3),
|
|
107
|
+
frequency: z.number().int(),
|
|
108
|
+
pulseWidth: z.number().int(),
|
|
109
|
+
control: Byte,
|
|
110
|
+
attackDecay: Byte,
|
|
111
|
+
sustainRelease: Byte,
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
filter: z.object({ cutoff: z.number().int(), resonance: z.number().int(), routing: z.number().int(), mode: z.number().int() }),
|
|
115
|
+
volume: z.number().int().min(0).max(15),
|
|
116
|
+
}),
|
|
117
|
+
readOnly: true,
|
|
118
|
+
async run(_input, session) {
|
|
119
|
+
return session.sid();
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const Rectangle = z
|
|
124
|
+
.object({ x: z.number().int().min(0), y: z.number().int().min(0), width: z.number().int().min(1), height: z.number().int().min(1) })
|
|
125
|
+
.strict();
|
|
126
|
+
|
|
127
|
+
export const c64Screen = defineTool({
|
|
128
|
+
name: "c64_screen",
|
|
129
|
+
title: "Capture and compare the C64 screen",
|
|
130
|
+
description:
|
|
131
|
+
"Action capture returns the last frame the C64 drew, with its borders, as a PNG image, plus its width and height in pixels. " +
|
|
132
|
+
"With baseline, the session also keeps that frame under the name. " +
|
|
133
|
+
"Action compare compares the current frame with a baseline, pixel by pixel. mask lists rectangles to ignore. " +
|
|
134
|
+
"match is true when the part of compared pixels that differ is at most maxMismatchRatio (default 0). bounds holds every different pixel. " +
|
|
135
|
+
"With includeDiff, compare also returns an image with the different pixels in magenta. " +
|
|
136
|
+
"Action list gives the baseline names. Action discard removes a baseline. Baselines stay until the session ends. " +
|
|
137
|
+
"The emulator draws frames while the CPU runs. After the CPU stops, the frame stays the same. " +
|
|
138
|
+
"This does not change the run state of the machine.",
|
|
139
|
+
inputSchema: z
|
|
140
|
+
.object({
|
|
141
|
+
action: z.enum(["capture", "compare", "list", "discard"]),
|
|
142
|
+
baseline: TransientName.optional().describe("capture: keep the frame under this name. compare and discard: the baseline to use"),
|
|
143
|
+
maxMismatchRatio: z.number().min(0).max(1).optional().describe("compare only: 0 to 1, default 0"),
|
|
144
|
+
mask: z.array(Rectangle).max(MAX_MASK_RECTANGLES).optional().describe("compare only: rectangles to ignore, in frame pixels"),
|
|
145
|
+
includeDiff: z.boolean().optional().describe("compare only: also return a difference image"),
|
|
146
|
+
})
|
|
147
|
+
.strict(),
|
|
148
|
+
outputSchema: z.object({
|
|
149
|
+
width: z.number().int().min(1).optional(),
|
|
150
|
+
height: z.number().int().min(1).optional(),
|
|
151
|
+
baseline: z.string().optional(),
|
|
152
|
+
match: z.boolean().optional(),
|
|
153
|
+
mismatchingPixels: z.number().int().min(0).optional(),
|
|
154
|
+
mismatchRatio: z.number().min(0).max(1).optional(),
|
|
155
|
+
bounds: Rectangle.optional(),
|
|
156
|
+
baselines: z.array(z.string()).optional(),
|
|
157
|
+
discarded: z.boolean().optional(),
|
|
158
|
+
}),
|
|
159
|
+
readOnly: false,
|
|
160
|
+
async run(input, session) {
|
|
161
|
+
const { action, ...fields } = input;
|
|
162
|
+
if (action === "capture") {
|
|
163
|
+
requireFields(action, fields, ["baseline"]);
|
|
164
|
+
const shot = await session.screenCapture(fields.baseline);
|
|
165
|
+
const result = { width: shot.width, height: shot.height, ...(shot.baseline === undefined ? {} : { baseline: shot.baseline }) };
|
|
166
|
+
return new ToolOutput(result, [{ type: "image", data: shot.png, mimeType: "image/png" }]);
|
|
167
|
+
}
|
|
168
|
+
if (action === "compare") {
|
|
169
|
+
requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff"], ["baseline"]);
|
|
170
|
+
const { diffPng, ...result } = await session.screenCompare({
|
|
171
|
+
baseline: fields.baseline!,
|
|
172
|
+
maxMismatchRatio: fields.maxMismatchRatio ?? 0,
|
|
173
|
+
mask: fields.mask ?? [],
|
|
174
|
+
includeDiff: fields.includeDiff ?? false,
|
|
175
|
+
});
|
|
176
|
+
return diffPng === undefined ? result : new ToolOutput(result, [{ type: "image", data: diffPng, mimeType: "image/png" }]);
|
|
177
|
+
}
|
|
178
|
+
if (action === "list") {
|
|
179
|
+
requireFields(action, fields, []);
|
|
180
|
+
return session.screenBaselines();
|
|
181
|
+
}
|
|
182
|
+
requireFields(action, fields, ["baseline"], ["baseline"]);
|
|
183
|
+
return session.screenDiscard(fields.baseline!);
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const FlagsOutput = z.object({ n: z.boolean(), v: z.boolean(), b: z.boolean(), d: z.boolean(), i: z.boolean(), z: z.boolean(), c: z.boolean() });
|
|
188
|
+
|
|
189
|
+
export const c64Observe = defineTool({
|
|
190
|
+
name: "c64_observe",
|
|
191
|
+
title: "Observe several things at one moment",
|
|
192
|
+
description:
|
|
193
|
+
"Get several observations from one moment: the CPU stops once, the tool reads all of them, and then the CPU continues if it was running. " +
|
|
194
|
+
"Ask for any of registers (a space), memory ranges, vicii, sprites, cia, sid, screen and timing (the raster position). " +
|
|
195
|
+
`Ask for at least one. Use at most ${MAX_OBSERVE_RANGES} memory ranges with at most ${MAX_OBSERVE_BYTES} bytes together. ` +
|
|
196
|
+
"The result has only the parts you asked for. With screen, an image comes with the result.",
|
|
197
|
+
inputSchema: z
|
|
198
|
+
.object({
|
|
199
|
+
registers: z.enum(SPACES).optional().describe("the space whose CPU registers to read"),
|
|
200
|
+
memory: z
|
|
201
|
+
.array(
|
|
202
|
+
z
|
|
203
|
+
.object({ address: AddressInput, size: z.number().int().min(1).max(MAX_MEMORY_READ), space: SpaceInput, view: MemoryViewInput })
|
|
204
|
+
.strict(),
|
|
205
|
+
)
|
|
206
|
+
.min(1)
|
|
207
|
+
.max(MAX_OBSERVE_RANGES)
|
|
208
|
+
.optional(),
|
|
209
|
+
vicii: z.boolean().optional(),
|
|
210
|
+
sprites: z.array(z.number().int().min(0).max(7)).min(1).max(8).optional().describe("sprite numbers 0 to 7, each once"),
|
|
211
|
+
cia: z.enum(CIA_SELECTIONS).optional(),
|
|
212
|
+
sid: z.boolean().optional(),
|
|
213
|
+
screen: z.boolean().optional(),
|
|
214
|
+
timing: z.boolean().optional(),
|
|
215
|
+
})
|
|
216
|
+
.strict(),
|
|
217
|
+
outputSchema: z.object({
|
|
218
|
+
registers: z.object({ pc: AddressOutput, a: Byte, x: Byte, y: Byte, sp: Byte, flags: FlagsOutput }).optional(),
|
|
219
|
+
memory: z.array(z.object({ address: AddressOutput, data: HexData })).optional(),
|
|
220
|
+
vicii: c64Vicii.outputSchema.optional(),
|
|
221
|
+
sprites: z.array(SpriteOutput).optional(),
|
|
222
|
+
cia: z.array(CiaOutput).optional(),
|
|
223
|
+
sid: c64Sid.outputSchema.optional(),
|
|
224
|
+
screen: z.object({ width: z.number().int(), height: z.number().int() }).optional(),
|
|
225
|
+
timing: z.object({ rasterLine: z.number().int(), rasterCycle: z.number().int() }).optional(),
|
|
226
|
+
}),
|
|
227
|
+
readOnly: true,
|
|
228
|
+
async run(input, session) {
|
|
229
|
+
const params: ObserveParams = {};
|
|
230
|
+
if (input.registers !== undefined) params.registers = input.registers;
|
|
231
|
+
if (input.memory !== undefined) params.memory = input.memory;
|
|
232
|
+
if (input.vicii === true) params.vicii = true;
|
|
233
|
+
if (input.sprites !== undefined) params.sprites = input.sprites;
|
|
234
|
+
if (input.cia !== undefined) params.cia = input.cia;
|
|
235
|
+
if (input.sid === true) params.sid = true;
|
|
236
|
+
if (input.screen === true) params.screen = true;
|
|
237
|
+
if (input.timing === true) params.timing = true;
|
|
238
|
+
if (Object.keys(params).length === 0) throw new WireFailure("invalid-input", "Ask for at least one observation.");
|
|
239
|
+
const observed = await session.observe(params);
|
|
240
|
+
const result: Record<string, unknown> = {};
|
|
241
|
+
if (observed.registers !== undefined) result.registers = { ...observed.registers, pc: formatC64Address(observed.registers.pc) };
|
|
242
|
+
if (observed.memory !== undefined) result.memory = observed.memory.map((range) => ({ ...range, address: formatC64Address(range.address) }));
|
|
243
|
+
if (observed.vicii !== undefined) {
|
|
244
|
+
result.vicii = {
|
|
245
|
+
...observed.vicii,
|
|
246
|
+
screenAddress: formatC64Address(observed.vicii.screenAddress),
|
|
247
|
+
graphicsAddress: formatC64Address(observed.vicii.graphicsAddress),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (observed.sprites !== undefined) result.sprites = observed.sprites.map((sprite) => ({ ...sprite, dataAddress: formatC64Address(sprite.dataAddress) }));
|
|
251
|
+
if (observed.cia !== undefined) result.cia = observed.cia;
|
|
252
|
+
if (observed.sid !== undefined) result.sid = observed.sid;
|
|
253
|
+
if (observed.timing !== undefined) result.timing = observed.timing;
|
|
254
|
+
if (observed.screen === undefined) return result;
|
|
255
|
+
result.screen = { width: observed.screen.width, height: observed.screen.height };
|
|
256
|
+
return new ToolOutput(result, [{ type: "image", data: observed.screen.png, mimeType: "image/png" }]);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
export const videoTools = [c64Vicii, c64Sprite, c64Cia, c64Sid, c64Screen, c64Observe];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The ACME adapter: argv from the typed request only, structured
|
|
2
|
+
// diagnostics and symbols, and a validated C64 PRG as the result.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { WireFailure } from "../protocol/messages.ts";
|
|
8
|
+
import { isRelativePath } from "../protocol/tools.ts";
|
|
9
|
+
import type { AcmeParams, AcmeResult, AssembledSymbol, Diagnostic } from "./types.ts";
|
|
10
|
+
import { Workspace } from "./staging.ts";
|
|
11
|
+
import { ACME, findTool } from "./discover.ts";
|
|
12
|
+
import { outputTail, runToolOrFail, type ToolContext } from "./run.ts";
|
|
13
|
+
|
|
14
|
+
const PROGRAM = "program.prg";
|
|
15
|
+
const SYMBOLS = "symbols.txt";
|
|
16
|
+
const TIMEOUT_MS = 60_000;
|
|
17
|
+
|
|
18
|
+
/** True for a relative path that stays inside the source root: no absolute path, no drive letter and no "..". */
|
|
19
|
+
const insideSourceRoot = (path: string) => isRelativePath(path) && !/^[A-Za-z]:/.test(path);
|
|
20
|
+
|
|
21
|
+
/** Refuses an entry source or include directory that ACME could read outside the source root, by name. */
|
|
22
|
+
export function checkAcmeParams(params: AcmeParams): void {
|
|
23
|
+
if (!insideSourceRoot(params.entrySource)) throw new WireFailure("invalid-input", `The entry source ${params.entrySource} is not a relative path inside the source root.`);
|
|
24
|
+
if (params.entrySource.startsWith("-")) throw new WireFailure("invalid-input", `The entry source ${params.entrySource} starts with a hyphen, which ACME reads as an option. Give the file a different name.`);
|
|
25
|
+
for (const directory of params.includeDirs) {
|
|
26
|
+
if (!insideSourceRoot(directory)) throw new WireFailure("invalid-input", `The include directory ${directory} is not a relative path inside the source root.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The argv for one assembly; every value comes from the request that checkAcmeParams accepted. */
|
|
31
|
+
export function acmeArguments(executable: string, params: AcmeParams, outputDirectory: string): string[] {
|
|
32
|
+
const argv = [executable, "--format", "cbm", "--cpu", "6510", "--maxerrors", "100", "-o", join(outputDirectory, PROGRAM), "--symbollist", join(outputDirectory, SYMBOLS)];
|
|
33
|
+
for (const directory of params.includeDirs) argv.push("-I", directory);
|
|
34
|
+
for (const [name, value] of Object.entries(params.defines)) argv.push(`-D${name}=${typeof value === "boolean" ? (value ? 1 : 0) : value}`);
|
|
35
|
+
if (params.setPc !== undefined) argv.push("--setpc", String(params.setPc));
|
|
36
|
+
argv.push(params.entrySource);
|
|
37
|
+
return argv;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DIAGNOSTIC = /^(Error|Warning|Serious error) - File (.+?), line (\d+)(?: \(.*?\))?: (.*)$/;
|
|
41
|
+
|
|
42
|
+
/** Parses ACME's "Error - File main.a, line 4 (Zone ...): message" lines. Files become source-root relative. */
|
|
43
|
+
export function parseDiagnostics(output: string, sourceRoot: string): Diagnostic[] {
|
|
44
|
+
const diagnostics: Diagnostic[] = [];
|
|
45
|
+
for (const line of output.split(/\r?\n/)) {
|
|
46
|
+
const match = DIAGNOSTIC.exec(line.trim());
|
|
47
|
+
if (match !== null) {
|
|
48
|
+
let file = match[2]!;
|
|
49
|
+
if (isAbsolute(file)) {
|
|
50
|
+
const inside = relative(sourceRoot, file);
|
|
51
|
+
file = inside.startsWith("..") || isAbsolute(inside) ? "(outside the source root)" : inside.split(sep).join("/");
|
|
52
|
+
}
|
|
53
|
+
diagnostics.push({ severity: match[1] === "Warning" ? "warning" : "error", file, line: Number(match[3]), message: match[4]! });
|
|
54
|
+
} else if (/^(Error|Serious error|Warning)\b/.test(line.trim())) {
|
|
55
|
+
diagnostics.push({ severity: line.trim().startsWith("Warning") ? "warning" : "error", message: line.trim() });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return diagnostics;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parses ACME's symbol list ("\tname\t= $2100\t; unused"). A value inside the
|
|
63
|
+
* assembled program is an address; any other value is a constant.
|
|
64
|
+
*/
|
|
65
|
+
export function parseSymbols(list: string, program: { start: number; end: number }): AssembledSymbol[] {
|
|
66
|
+
const symbols: AssembledSymbol[] = [];
|
|
67
|
+
for (const line of list.split(/\r?\n/)) {
|
|
68
|
+
const match = /^\s*([A-Za-z_][\w.]*)\s*=\s*\$([0-9a-fA-F]+)\s*(?:;\s*(.*?))?\s*$/.exec(line);
|
|
69
|
+
if (match === null) continue;
|
|
70
|
+
const name = match[1]!;
|
|
71
|
+
const value = Number.parseInt(match[2]!, 16);
|
|
72
|
+
const used = match[3] !== "unused";
|
|
73
|
+
symbols.push(value >= program.start && value <= program.end ? { name, kind: "address", address: value, used } : { name, kind: "constant", value, used });
|
|
74
|
+
}
|
|
75
|
+
// Code-point order: the same on every host, whatever its locale.
|
|
76
|
+
return symbols.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function assemble(
|
|
80
|
+
params: AcmeParams,
|
|
81
|
+
files: Buffer[],
|
|
82
|
+
context: ToolContext,
|
|
83
|
+
): Promise<{ result: AcmeResult; attachments?: Buffer[] }> {
|
|
84
|
+
checkAcmeParams(params);
|
|
85
|
+
const executable = findTool(ACME, context.env);
|
|
86
|
+
const workspace = Workspace.create(context.supervisor);
|
|
87
|
+
try {
|
|
88
|
+
const source = workspace.materialize("source", params.files, files);
|
|
89
|
+
// A fresh output directory, so no stale file can pass for this run's output.
|
|
90
|
+
const output = workspace.directory("out");
|
|
91
|
+
const run = await runToolOrFail("ACME", "The assembly", { argv: acmeArguments(executable, params, output), cwd: source, timeoutMs: TIMEOUT_MS }, context);
|
|
92
|
+
const diagnostics = parseDiagnostics(`${run.stdout}\n${run.stderr}`, source);
|
|
93
|
+
const programPath = join(output, PROGRAM);
|
|
94
|
+
if (run.code !== 0) {
|
|
95
|
+
if (diagnostics.every((diagnostic) => diagnostic.severity !== "error")) {
|
|
96
|
+
context.log?.(`ACME exited ${run.code} without a parsed error:\n${run.stdout}\n${run.stderr}`);
|
|
97
|
+
diagnostics.push({ severity: "error", message: `ACME stopped with exit code ${run.code} and gave no error in its usual form.${outputTail("ACME", run, workspace.root)}` });
|
|
98
|
+
}
|
|
99
|
+
return { result: { assembled: false, diagnostics } };
|
|
100
|
+
}
|
|
101
|
+
if (!existsSync(programPath)) throw new WireFailure("operation-failed", "ACME reported success but wrote no program.");
|
|
102
|
+
const program = readFileSync(programPath);
|
|
103
|
+
if (program.length < 3) throw new WireFailure("operation-failed", "ACME wrote a program without bytes.");
|
|
104
|
+
const start = program.readUInt16LE(0);
|
|
105
|
+
const bytes = program.length - 2;
|
|
106
|
+
if (start + bytes > 0x10000) throw new WireFailure("operation-failed", "ACME wrote a program that runs past $ffff.");
|
|
107
|
+
const end = start + bytes - 1;
|
|
108
|
+
const symbolList = existsSync(join(output, SYMBOLS)) ? readFileSync(join(output, SYMBOLS), "latin1") : "";
|
|
109
|
+
return {
|
|
110
|
+
result: { assembled: true, loadRange: { start, end, bytes }, symbols: parseSymbols(symbolList, { start, end }), diagnostics },
|
|
111
|
+
attachments: [program],
|
|
112
|
+
};
|
|
113
|
+
} finally {
|
|
114
|
+
workspace.remove();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// One discovery rule for every native tool: C64RT_<TOOL> names
|
|
2
|
+
// the executable, else the tool's usual name on PATH. A missing tool is
|
|
3
|
+
// refused by name with the remedy; nothing is ever installed.
|
|
4
|
+
|
|
5
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
6
|
+
import { isAbsolute, join, posix, win32 } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { WireFailure } from "../protocol/messages.ts";
|
|
9
|
+
|
|
10
|
+
export interface ToolSpec {
|
|
11
|
+
/** Human name, for messages. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Environment variable that can name the executable. */
|
|
14
|
+
envVar: string;
|
|
15
|
+
/** Executable names to look for on PATH, in order. */
|
|
16
|
+
binaries: string[];
|
|
17
|
+
/** Where the tool runs: the Host Runtime's machine, or the skill script's. */
|
|
18
|
+
where: "on the host" | "on this machine";
|
|
19
|
+
/** What the user does when the tool is missing. */
|
|
20
|
+
remedy: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ACME: ToolSpec = {
|
|
24
|
+
name: "ACME",
|
|
25
|
+
envVar: "C64RT_ACME",
|
|
26
|
+
binaries: ["acme"],
|
|
27
|
+
where: "on this machine",
|
|
28
|
+
remedy: "Install the ACME cross-assembler on the machine that runs the skill scripts so that acme is on PATH, or set C64RT_ACME to its full path.",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const C1541: ToolSpec = {
|
|
32
|
+
name: "c1541",
|
|
33
|
+
envVar: "C64RT_C1541",
|
|
34
|
+
binaries: ["c1541"],
|
|
35
|
+
where: "on the host",
|
|
36
|
+
remedy: "Install VICE 3.9 or later on the host (c1541 comes with it) so that c1541 is on PATH, or set C64RT_C1541 to its full path, then restart the Host Runtime.",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const PETCAT: ToolSpec = {
|
|
40
|
+
name: "petcat",
|
|
41
|
+
envVar: "C64RT_PETCAT",
|
|
42
|
+
binaries: ["petcat"],
|
|
43
|
+
where: "on the host",
|
|
44
|
+
remedy: "Install VICE 3.9 or later on the host (petcat comes with it) so that petcat is on PATH, or set C64RT_PETCAT to its full path, then restart the Host Runtime.",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const DXA: ToolSpec = {
|
|
48
|
+
name: "dxa",
|
|
49
|
+
envVar: "C64RT_DXA",
|
|
50
|
+
binaries: ["dxa"],
|
|
51
|
+
where: "on this machine",
|
|
52
|
+
remedy: "Install dxa (from the xa package, https://www.floodgap.com/retrotech/xa/) on the machine that runs the skill scripts so that dxa is on PATH, or set C64RT_DXA to its full path.",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const VICE: ToolSpec = {
|
|
56
|
+
name: "VICE (x64sc)",
|
|
57
|
+
envVar: "C64RT_VICE",
|
|
58
|
+
binaries: ["x64sc"],
|
|
59
|
+
where: "on the host",
|
|
60
|
+
remedy: "Install VICE 3.9 or later on the host so that x64sc is on PATH, or set C64RT_VICE to the full path of x64sc, then restart the Host Runtime.",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The oldest VICE that works: 3.7 has no monitor profiler, and its CPU
|
|
65
|
+
* history, until-return and c1541 chain output differ.
|
|
66
|
+
*/
|
|
67
|
+
export const MIN_VICE = { major: 3, minor: 9 } as const;
|
|
68
|
+
|
|
69
|
+
/** "x64sc (VICE 3.10)", "c1541 (VICE 3.9)" or "VICE 3.9.0": the major and minor version. */
|
|
70
|
+
export function viceVersionOf(text: string): { major: number; minor: number } | undefined {
|
|
71
|
+
const match = /VICE (\d+)\.(\d+)/.exec(text);
|
|
72
|
+
return match === null ? undefined : { major: Number(match[1]), minor: Number(match[2]) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isOlderThanMinimum(version: { major: number; minor: number }): boolean {
|
|
76
|
+
return version.major < MIN_VICE.major || (version.major === MIN_VICE.major && version.minor < MIN_VICE.minor);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A file this process may run. Windows has no execute permission (X_OK checks nothing there): a file is enough. */
|
|
80
|
+
export function isExecutableFile(path: string): boolean {
|
|
81
|
+
try {
|
|
82
|
+
if (!statSync(path).isFile()) return false;
|
|
83
|
+
if (process.platform !== "win32") accessSync(path, constants.X_OK);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The value of an environment variable. Windows ignores the case of the name,
|
|
92
|
+
* also in a copied environment, where PATH is often named "Path".
|
|
93
|
+
*/
|
|
94
|
+
export function environmentValue(env: NodeJS.ProcessEnv, name: string, platform: NodeJS.Platform = process.platform): string | undefined {
|
|
95
|
+
if (platform !== "win32" || env[name] !== undefined) return env[name];
|
|
96
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name.toUpperCase());
|
|
97
|
+
return key === undefined ? undefined : env[key];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The extensions that the supervisor can start on Windows: programs directly, batch files through cmd.exe. */
|
|
101
|
+
const STARTABLE = [".com", ".exe", ".bat", ".cmd"];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The suffixes that a name on PATH can have. On Windows, the PATHEXT
|
|
105
|
+
* extensions that the supervisor can start, in PATHEXT order, and never the
|
|
106
|
+
* bare name: Windows does not start a file without an extension. Elsewhere
|
|
107
|
+
* only the bare name.
|
|
108
|
+
*/
|
|
109
|
+
export function pathSuffixes(env: NodeJS.ProcessEnv, platform: NodeJS.Platform = process.platform): string[] {
|
|
110
|
+
if (platform !== "win32") return [""];
|
|
111
|
+
const listed = (environmentValue(env, "PATHEXT", platform) ?? "").split(";").map((suffix) => suffix.trim().toLowerCase());
|
|
112
|
+
const suffixes = listed.filter((suffix, index) => STARTABLE.includes(suffix) && listed.indexOf(suffix) === index);
|
|
113
|
+
return suffixes.length === 0 ? STARTABLE : suffixes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The first executable named `binary + suffix` in a PATH directory, in PATH order; undefined when there is none. */
|
|
117
|
+
export function findOnPath(binaries: readonly string[], env: NodeJS.ProcessEnv, suffixes: readonly string[] = pathSuffixes(env), platform: NodeJS.Platform = process.platform): string | undefined {
|
|
118
|
+
const delimiter = platform === "win32" ? win32.delimiter : posix.delimiter;
|
|
119
|
+
for (const dir of (environmentValue(env, "PATH", platform) ?? "").split(delimiter)) {
|
|
120
|
+
if (dir === "") continue;
|
|
121
|
+
for (const binary of binaries) {
|
|
122
|
+
for (const suffix of suffixes) {
|
|
123
|
+
const candidate = join(dir, binary + suffix);
|
|
124
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Finds a tool's executable. Throws WireFailure(installation-incomplete) naming the tool and the remedy. */
|
|
132
|
+
export function findTool(tool: ToolSpec, env: NodeJS.ProcessEnv = process.env): string {
|
|
133
|
+
const configured = env[tool.envVar];
|
|
134
|
+
if (configured !== undefined && configured !== "") {
|
|
135
|
+
if (isAbsolute(configured) && isExecutableFile(configured)) return configured;
|
|
136
|
+
throw new WireFailure("installation-incomplete", `${tool.envVar} ${tool.where} does not name an executable ${tool.name} file. ${tool.remedy}`);
|
|
137
|
+
}
|
|
138
|
+
const found = findOnPath(tool.binaries, env);
|
|
139
|
+
if (found !== undefined) return found;
|
|
140
|
+
throw new WireFailure("installation-incomplete", `${tool.name} is not installed ${tool.where}. ${tool.remedy}`);
|
|
141
|
+
}
|