@henols/c64-re-tools 2.0.0-rc.4 → 2.0.0-rc.5
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/README.md +3 -1
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-basic/SKILL.md +3 -3
- package/skills/c64-emulator/SKILL.md +2 -2
- package/skills/c64-knowledge/SKILL.md +17 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-memory-map/scripts/memmap.ts +1 -1
- package/skills/c64-provenance/SKILL.md +3 -2
- package/skills/c64-provenance/scripts/provenance.ts +4 -2
- package/skills/c64-provenance/scripts/runtime.ts +14 -2
- package/skills/c64-source/SKILL.md +4 -4
- package/skills/c64-source/scripts/generate.ts +57 -14
- package/skills/c64-source/scripts/loader.ts +5 -1
- package/skills/c64-source/scripts/source.ts +3 -1
- package/skills/c64-static-analysis/SKILL.md +2 -2
- package/skills/c64-static-analysis/scripts/analyze.ts +8 -4
- package/skills/c64-unpacker/SKILL.md +5 -5
- package/skills/c64-unpacker/scripts/evidence.ts +29 -22
- package/skills/c64-unpacker/scripts/loads.ts +30 -7
- package/skills/c64-unpacker/scripts/stages.ts +67 -0
- package/skills/c64-unpacker/scripts/unpack.ts +167 -47
- package/src/c64.ts +37 -12
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +9 -7
- package/src/host/main.ts +8 -3
- package/src/host/tools/petcat.ts +5 -1
- package/src/host/tools/status.ts +8 -2
- package/src/host/vice/process.ts +10 -5
- package/src/host/vice/session.ts +26 -13
- package/src/host-client/connect.ts +2 -1
- package/src/host-client/coverage.ts +0 -5
- package/src/host-client/vice-session.ts +37 -4
- package/src/knowledge/history.ts +8 -0
- package/src/knowledge/import.ts +34 -3
- package/src/knowledge/write.ts +2 -1
- package/src/mcp/main.ts +43 -15
- package/src/mcp/server.ts +9 -2
- package/src/mcp/tools/execution.ts +3 -2
- package/src/mcp/tools/input.ts +6 -4
- package/src/mcp/tools/media.ts +4 -3
- package/src/native/discover.ts +38 -4
- package/src/package.ts +21 -0
- package/src/protocol/tools.ts +7 -3
- package/src/protocol/vice.ts +14 -6
package/src/c64.ts
CHANGED
|
@@ -128,7 +128,7 @@ export function textToPetsciiName(text: string): Uint8Array {
|
|
|
128
128
|
/** BASIC refuses higher line numbers when a line is entered. */
|
|
129
129
|
const MAX_LINE_NUMBER = 63999;
|
|
130
130
|
|
|
131
|
-
const TOKEN = { data: 0x83, rem: 0x8f, sys: 0x9e, usr: 0xb7, plus: 0xaa, minus: 0xab, times: 0xac, divide: 0xad } as const;
|
|
131
|
+
const TOKEN = { data: 0x83, rem: 0x8f, sys: 0x9e, usr: 0xb7, plus: 0xaa, minus: 0xab, times: 0xac, divide: 0xad, pi: 0xff } as const;
|
|
132
132
|
const QUOTE = 0x22;
|
|
133
133
|
const COLON = 0x3a;
|
|
134
134
|
|
|
@@ -138,15 +138,27 @@ export interface TokenizedLine {
|
|
|
138
138
|
tokens: Uint8Array;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/**
|
|
142
|
-
|
|
141
|
+
/**
|
|
142
|
+
* A machine-code handoff in BASIC program text: SYS with a constant address, a
|
|
143
|
+
* SYS whose argument needs run-time values (with the argument as text), or a USR.
|
|
144
|
+
*/
|
|
145
|
+
export type BasicHandoff =
|
|
146
|
+
| { kind: "sys"; line: number; address: number }
|
|
147
|
+
| { kind: "sys"; line: number; computed: true; expression: string }
|
|
148
|
+
| { kind: "usr"; line: number; computed: true };
|
|
143
149
|
|
|
144
|
-
|
|
150
|
+
/**
|
|
151
|
+
* `basicEnd` is the first address that holds no BASIC; `textBytes` is the
|
|
152
|
+
* number of file bytes (load address included) before the end of the BASIC text.
|
|
153
|
+
*/
|
|
154
|
+
export type TokenizedProgram = { loadAddress: number; basicEnd: number; textBytes: number; lines: TokenizedLine[] } | { reason: string };
|
|
145
155
|
|
|
146
156
|
/**
|
|
147
157
|
* Splits a PRG into BASIC lines the way BASIC relinks a loaded program: each
|
|
148
158
|
* line ends at a zero byte, and a link whose high byte is zero ends the
|
|
149
|
-
* program. The stored links are not trusted.
|
|
159
|
+
* program. The stored links are not trusted. A real end marker is two zero
|
|
160
|
+
* bytes; when only the high byte is zero, the "marker" is the first machine
|
|
161
|
+
* code after the BASIC text (SYS lines that jump right behind themselves).
|
|
150
162
|
*/
|
|
151
163
|
export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
152
164
|
if (program.length < 4) return { reason: "The file is too short for a BASIC program." };
|
|
@@ -157,12 +169,13 @@ export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
|
157
169
|
for (;;) {
|
|
158
170
|
// A file that ends right after a line leaves the end marker to the zeros after it in memory;
|
|
159
171
|
// basicEnd is the address after that marker, as when the file holds it.
|
|
160
|
-
if (at === program.length && lines.length > 0) return { loadAddress, basicEnd: loadAddress + at, lines };
|
|
172
|
+
if (at === program.length && lines.length > 0) return { loadAddress, basicEnd: loadAddress + at, textBytes: at, lines };
|
|
161
173
|
if (at + 1 >= program.length) return { reason: "The BASIC program has no end marker." };
|
|
162
174
|
if (program[at + 1] === 0) {
|
|
163
175
|
// An end marker before any line, with more bytes after it, is how machine code at a non-BASIC address often starts.
|
|
164
176
|
if (lines.length === 0 && at + 2 < program.length) return { reason: "The file holds no BASIC line before other bytes, so it is no BASIC program." };
|
|
165
|
-
|
|
177
|
+
const marker = program[at] === 0;
|
|
178
|
+
return { loadAddress, basicEnd: loadAddress + (marker ? at : at - 2), textBytes: at, lines };
|
|
166
179
|
}
|
|
167
180
|
if (at + 4 > program.length) return { reason: "The last BASIC line is cut off." };
|
|
168
181
|
const number = program[at + 2]! | (program[at + 3]! << 8);
|
|
@@ -188,13 +201,20 @@ const KEYWORDS = [
|
|
|
188
201
|
* as it is, and {$xx} for bytes that are no character. For evidence, not a listing.
|
|
189
202
|
*/
|
|
190
203
|
export function basicLineText(line: TokenizedLine): string {
|
|
191
|
-
|
|
204
|
+
return `${line.number} ${basicText(line.tokens)}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Tokenized BASIC bytes as text, as for basicLineText; pi is π. */
|
|
208
|
+
function basicText(tokens: Uint8Array): string {
|
|
209
|
+
let text = "";
|
|
192
210
|
let literal = false;
|
|
193
|
-
for (const byte of
|
|
211
|
+
for (const byte of tokens) {
|
|
194
212
|
if (byte === QUOTE) literal = !literal;
|
|
195
213
|
if (!literal && byte >= 0x80 && byte < 0x80 + KEYWORDS.length) {
|
|
196
214
|
text += KEYWORDS[byte - 0x80];
|
|
197
215
|
if (byte === TOKEN.rem) literal = true;
|
|
216
|
+
} else if (!literal && byte === TOKEN.pi) {
|
|
217
|
+
text += "π";
|
|
198
218
|
} else {
|
|
199
219
|
text += petsciiNameToText(Uint8Array.of(byte));
|
|
200
220
|
}
|
|
@@ -203,7 +223,7 @@ export function basicLineText(line: TokenizedLine): string {
|
|
|
203
223
|
}
|
|
204
224
|
|
|
205
225
|
/**
|
|
206
|
-
* Evaluates a SYS argument that holds only numbers, + - * / and brackets.
|
|
226
|
+
* Evaluates a SYS argument that holds only numbers, π, + - * / and brackets.
|
|
207
227
|
* Returns undefined for anything that needs run-time values.
|
|
208
228
|
*/
|
|
209
229
|
export function constantExpression(bytes: Uint8Array): number | undefined {
|
|
@@ -242,6 +262,10 @@ export function constantExpression(bytes: Uint8Array): number | undefined {
|
|
|
242
262
|
if (value === undefined || tokens[at++] !== 0x29) return undefined;
|
|
243
263
|
return value;
|
|
244
264
|
}
|
|
265
|
+
if (peek() === TOKEN.pi) {
|
|
266
|
+
at++;
|
|
267
|
+
return Math.PI;
|
|
268
|
+
}
|
|
245
269
|
return number();
|
|
246
270
|
};
|
|
247
271
|
const product = (): number | undefined => {
|
|
@@ -297,8 +321,9 @@ export function findHandoffs(lines: TokenizedLine[]): BasicHandoff[] {
|
|
|
297
321
|
// The argument ends at the statement end or at a comma (extra SYS parameters).
|
|
298
322
|
let end = at + 1;
|
|
299
323
|
while (end < tokens.length && tokens[end] !== COLON && tokens[end] !== 0x2c) end++;
|
|
300
|
-
const
|
|
301
|
-
|
|
324
|
+
const argument = tokens.subarray(at + 1, end);
|
|
325
|
+
const value = constantExpression(argument);
|
|
326
|
+
if (value === undefined) handoffs.push({ kind: "sys", line: number, computed: true, expression: basicText(argument).trim() });
|
|
302
327
|
// BASIC drops the fraction; outside 0-65535 SYS stops with ILLEGAL QUANTITY and jumps nowhere.
|
|
303
328
|
else if (Math.trunc(value) >= 0 && Math.trunc(value) <= 0xffff) handoffs.push({ kind: "sys", line: number, address: Math.trunc(value) });
|
|
304
329
|
at = end - 1;
|
package/src/cli/main.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
10
|
-
import { PACKAGE } from "
|
|
10
|
+
import { PACKAGE } from "../package.ts";
|
|
11
11
|
import { localToolContext } from "../native/local.ts";
|
|
12
12
|
import { localToolStatus } from "../native/status.ts";
|
|
13
13
|
import { WireFailure } from "../protocol/messages.ts";
|
|
@@ -51,9 +51,12 @@ async function status(): Promise<number> {
|
|
|
51
51
|
console.log(`Package: ${join(here, "..", "..")}`);
|
|
52
52
|
console.log(`Node: ${process.version} (${process.execPath})`);
|
|
53
53
|
// The skill scripts run these tools here, on this machine.
|
|
54
|
-
|
|
55
|
-
for (const line of
|
|
56
|
-
|
|
54
|
+
const local = await localToolStatus(localToolContext());
|
|
55
|
+
for (const line of toolLines("Tools here:", local)) console.log(line);
|
|
56
|
+
const host = await hostStatusLines();
|
|
57
|
+
for (const line of host.lines) console.log(line);
|
|
58
|
+
// A script or a CI check can tell a ready setup from a broken one.
|
|
59
|
+
return host.ready && local.every((tool) => tool.runs) ? 0 : 1;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
async function main(argv: string[]): Promise<number> {
|
package/src/cli/status.ts
CHANGED
|
@@ -5,32 +5,34 @@ import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
|
|
|
5
5
|
import { STOP_AND_ASK } from "../protocol/messages.ts";
|
|
6
6
|
import type { ToolStatus } from "../protocol/tools.ts";
|
|
7
7
|
|
|
8
|
-
/** One line per tool under `heading`: its version, or why it does not run. */
|
|
8
|
+
/** One line per tool under `heading`: its version, or why it does not run; then each known flaw of it. */
|
|
9
9
|
export function toolLines(heading: string, tools: readonly ToolStatus[]): string[] {
|
|
10
10
|
return [
|
|
11
11
|
heading,
|
|
12
|
-
...tools.
|
|
12
|
+
...tools.flatMap((tool) => [
|
|
13
13
|
tool.runs
|
|
14
14
|
? ` ${tool.name}: ${tool.version} (${tool.path})`
|
|
15
15
|
: tool.found
|
|
16
16
|
? ` ${tool.name}: found at ${tool.path} but it does not run. ${tool.problem}`
|
|
17
17
|
: ` ${tool.name}: missing. ${tool.problem}`,
|
|
18
|
-
),
|
|
18
|
+
...(tool.flaws ?? []).map((flaw) => ` Known flaw: ${flaw}`),
|
|
19
|
+
]),
|
|
19
20
|
];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Whether the Host Runtime answers, and the tools it has. VICE and its tools
|
|
24
25
|
* live on the host; it finds and runs each one. One request shows both that
|
|
25
|
-
* the host answers and what it has.
|
|
26
|
+
* the host answers and what it has. `ready` is true when the host answers and
|
|
27
|
+
* every tool on it runs.
|
|
26
28
|
*/
|
|
27
|
-
export async function hostStatusLines(options: ToolCallOptions = {}): Promise<string[]> {
|
|
29
|
+
export async function hostStatusLines(options: ToolCallOptions = {}): Promise<{ lines: string[]; ready: boolean }> {
|
|
28
30
|
let tools: ToolStatus[];
|
|
29
31
|
try {
|
|
30
32
|
tools = await hostTools(options);
|
|
31
33
|
} catch (error) {
|
|
32
34
|
// status is read by a person: the instruction for an agent (D37) stays out.
|
|
33
|
-
return [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`];
|
|
35
|
+
return { lines: [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`], ready: false };
|
|
34
36
|
}
|
|
35
|
-
return ["Host Runtime: reachable", ...toolLines("Tools on the host:", tools)];
|
|
37
|
+
return { lines: ["Host Runtime: reachable", ...toolLines("Tools on the host:", tools)], ready: tools.every((tool) => tool.runs) };
|
|
36
38
|
}
|
package/src/host/main.ts
CHANGED
|
@@ -6,14 +6,16 @@ import { DEFAULT_HOST_PORT } from "../protocol/messages.ts";
|
|
|
6
6
|
import { ProcessSupervisor } from "../native/processes.ts";
|
|
7
7
|
import { startTrace, trace } from "../trace.ts";
|
|
8
8
|
import { ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
|
|
9
|
+
import { viceFlaws } from "../native/discover.ts";
|
|
9
10
|
import { createToolDispatcher } from "./tools/index.ts";
|
|
10
11
|
import { checkViceStarts, findVice } from "./vice/process.ts";
|
|
11
12
|
import { viceSessionFactory } from "./vice/session.ts";
|
|
13
|
+
import { PACKAGE } from "../package.ts";
|
|
12
14
|
|
|
13
15
|
const HELP = `c64-re-tools-host
|
|
14
16
|
|
|
15
17
|
Usage:
|
|
16
|
-
npx -y --package
|
|
18
|
+
npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host [--port <port>]
|
|
17
19
|
c64-re-tools-host --help
|
|
18
20
|
|
|
19
21
|
Runs the c64-re-tools Host Runtime in the foreground. It first starts VICE
|
|
@@ -68,14 +70,17 @@ async function main(): Promise<number> {
|
|
|
68
70
|
supervisor.startWatchdog("host");
|
|
69
71
|
|
|
70
72
|
// Finding x64sc is not enough: VICE without its ROM files exits at once.
|
|
73
|
+
let flaws: string[];
|
|
71
74
|
try {
|
|
72
|
-
await checkViceStarts({ supervisor });
|
|
75
|
+
flaws = viceFlaws(await checkViceStarts({ supervisor }));
|
|
73
76
|
} catch (error) {
|
|
74
77
|
process.stderr.write(`${(error as Error).message}\n`);
|
|
75
78
|
await supervisor.stopAll();
|
|
76
79
|
return 1;
|
|
77
80
|
}
|
|
78
81
|
log("VICE starts");
|
|
82
|
+
// A known VICE flaw is named, not worked around (D56).
|
|
83
|
+
for (const flaw of flaws) log(`Known VICE flaw: ${flaw}`);
|
|
79
84
|
|
|
80
85
|
let server;
|
|
81
86
|
try {
|
|
@@ -83,7 +88,7 @@ async function main(): Promise<number> {
|
|
|
83
88
|
host: "0.0.0.0",
|
|
84
89
|
port,
|
|
85
90
|
log,
|
|
86
|
-
createViceSession: viceSessionFactory({ supervisor, log }),
|
|
91
|
+
createViceSession: viceSessionFactory({ supervisor, log, flaws }),
|
|
87
92
|
tools: createToolDispatcher({ supervisor, log }),
|
|
88
93
|
});
|
|
89
94
|
} catch (error) {
|
package/src/host/tools/petcat.ts
CHANGED
|
@@ -36,9 +36,13 @@ export async function decode(
|
|
|
36
36
|
if ("reason" in tokenized) return { result: { decoded: false, reason: tokenized.reason } };
|
|
37
37
|
const executable = findTool(PETCAT, context.env);
|
|
38
38
|
await requireMinimumVersion(PETCAT, executable, context);
|
|
39
|
+
// petcat gets the BASIC text only, closed with a real end marker: after a
|
|
40
|
+
// link that only ends BASIC (machine code right after the last line), petcat
|
|
41
|
+
// prints a comment line where a listing line belongs.
|
|
42
|
+
const text = Buffer.concat([program.subarray(0, tokenized.textBytes), Buffer.of(0, 0)]);
|
|
39
43
|
const workspace = Workspace.create(context.supervisor);
|
|
40
44
|
try {
|
|
41
|
-
workspace.materialize("input", [{ path: "program.prg", size:
|
|
45
|
+
workspace.materialize("input", [{ path: "program.prg", size: text.length }], [text]);
|
|
42
46
|
const listingPath = join(workspace.directory("out"), LISTING);
|
|
43
47
|
const run = await runToolOrFail(
|
|
44
48
|
"petcat",
|
package/src/host/tools/status.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// VICE older than MIN_VICE is reported as not working, as its operations refuse it.
|
|
4
4
|
|
|
5
5
|
import type { ToolStatus } from "../../protocol/tools.ts";
|
|
6
|
-
import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceVersionOf, type ToolSpec } from "../../native/discover.ts";
|
|
6
|
+
import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceFlaws, viceVersionOf, type ToolSpec } from "../../native/discover.ts";
|
|
7
7
|
import type { ToolContext } from "../../native/run.ts";
|
|
8
8
|
import { probeTools } from "../../native/status.ts";
|
|
9
9
|
import { findVice } from "../vice/process.ts";
|
|
@@ -26,5 +26,11 @@ export async function hostStatus(context: ToolContext): Promise<{ result: { tool
|
|
|
26
26
|
],
|
|
27
27
|
context,
|
|
28
28
|
);
|
|
29
|
-
|
|
29
|
+
const checked = tools.map((status, index) => requireMinimum(status, specs[index]!));
|
|
30
|
+
// VICE runs with its known flaws; status names them (D56).
|
|
31
|
+
const vice = checked[0]!;
|
|
32
|
+
const version = vice.runs ? viceVersionOf(vice.version ?? "") : undefined;
|
|
33
|
+
const flaws = version === undefined ? [] : viceFlaws(version);
|
|
34
|
+
if (flaws.length > 0) checked[0] = { ...vice, flaws };
|
|
35
|
+
return { result: { tools: checked } };
|
|
30
36
|
}
|
package/src/host/vice/process.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// operations only it offers; both act on the same machine;
|
|
10
10
|
// - autostart of a PRG needs `-autostartprgmode 1` (inject into RAM);
|
|
11
11
|
// - the joyport command reaches the C64 only through the "Joyport I/O
|
|
12
|
-
// simulation" device (37)
|
|
12
|
+
// simulation" device (37). In VICE 3.10 and older it holds bits 5-7 of
|
|
13
|
+
// both CIA 1 ports low (VICE bug #2202, a known flaw named by D56);
|
|
13
14
|
// - with the default JAM action a jammed CPU hangs silently; -jamaction 2
|
|
14
15
|
// enters the monitor instead;
|
|
15
16
|
// - the monitor's profiler prints numbers in the host locale, so VICE runs
|
|
@@ -29,7 +30,7 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
29
30
|
|
|
30
31
|
import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
31
32
|
import { elapsedMs, trace } from "../../trace.ts";
|
|
32
|
-
import { findTool, isOlderThanMinimum, MIN_VICE, VICE } from "../../native/discover.ts";
|
|
33
|
+
import { findTool, isOlderThanMinimum, MIN_VICE, VICE, type ViceVersion } from "../../native/discover.ts";
|
|
33
34
|
import type { ExitStatus, ProcessSupervisor, SupervisedProcess } from "../../native/processes.ts";
|
|
34
35
|
import { BinaryMonitor, Command, decodeMemory, memoryGetBody } from "./binary-monitor.ts";
|
|
35
36
|
import { TextMonitor } from "./text-monitor.ts";
|
|
@@ -200,9 +201,10 @@ export function startErrorLines(output: string[]): string[] {
|
|
|
200
201
|
/**
|
|
201
202
|
* Starts VICE once, as a session does, and stops it again. Finding
|
|
202
203
|
* x64sc is not enough: without its ROM files VICE exits at once. Throws
|
|
203
|
-
* WireFailure installation-incomplete with VICE's own error lines.
|
|
204
|
+
* WireFailure installation-incomplete with VICE's own error lines. Returns
|
|
205
|
+
* the VICE version.
|
|
204
206
|
*/
|
|
205
|
-
export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandard" | "log">): Promise<
|
|
207
|
+
export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandard" | "log">): Promise<ViceVersion> {
|
|
206
208
|
const binary = findVice(options.env ?? process.env);
|
|
207
209
|
const output: string[] = [];
|
|
208
210
|
let vice: ViceProcess;
|
|
@@ -231,13 +233,16 @@ export async function checkViceStarts(options: Omit<LaunchOptions, "videoStandar
|
|
|
231
233
|
} finally {
|
|
232
234
|
await vice.stop();
|
|
233
235
|
}
|
|
234
|
-
|
|
236
|
+
// The SVN revision is 0 in a release build.
|
|
237
|
+
const revision = info.length >= 10 ? info.readUInt32LE(6) : 0;
|
|
238
|
+
const version = { major: info[1] ?? 0, minor: info[2] ?? 0, ...(revision === 0 ? {} : { revision }) };
|
|
235
239
|
if (isOlderThanMinimum(version)) {
|
|
236
240
|
throw new WireFailure(
|
|
237
241
|
"installation-incomplete",
|
|
238
242
|
`VICE (${binary}) is version ${version.major}.${version.minor}, and c64-re-tools needs VICE ${MIN_VICE.major}.${MIN_VICE.minor} or later. ${VICE.remedy}`,
|
|
239
243
|
);
|
|
240
244
|
}
|
|
245
|
+
return version;
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
async function launchOnce(options: LaunchOptions): Promise<ViceProcess> {
|
package/src/host/vice/session.ts
CHANGED
|
@@ -74,6 +74,8 @@ const WINDOW_CLOSED =
|
|
|
74
74
|
"Breakpoints and watchpoints are kept. Check the machine state before you continue.";
|
|
75
75
|
|
|
76
76
|
/** Emulator state that a move to another VICE cannot carry. */
|
|
77
|
+
/** The first bytes of every VICE snapshot file. */
|
|
78
|
+
const VSF_MAGIC = Buffer.from("VICE Snapshot File\x1a", "latin1");
|
|
77
79
|
const NOT_CARRIED = ["cpu history", "memory map", "profile", "keyboard input not yet typed"];
|
|
78
80
|
|
|
79
81
|
/** Starts another VICE for this session; `log` also gets that launch's lines. */
|
|
@@ -119,6 +121,8 @@ export interface SessionOptions {
|
|
|
119
121
|
launch?: ViceLauncher;
|
|
120
122
|
/** Makes the host remove a path if it dies; returns the release. */
|
|
121
123
|
ownPath?: (path: string) => () => void;
|
|
124
|
+
/** The known flaws of this VICE version (D56); keyboard and joystick results name them. */
|
|
125
|
+
flaws?: readonly string[];
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
export class ViceSession implements ViceSessionHandle {
|
|
@@ -128,6 +132,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
128
132
|
#mode: ViceMode = "headless";
|
|
129
133
|
readonly #videoStandard: VideoStandard;
|
|
130
134
|
readonly #log: (line: string) => void;
|
|
135
|
+
/** The known VICE flaws that keyboard and joystick input meet, as one text, or undefined. */
|
|
136
|
+
readonly #inputFlaw: string | undefined;
|
|
131
137
|
readonly #untilReturnLimitMs: number;
|
|
132
138
|
readonly #stepLimitMs: number;
|
|
133
139
|
readonly #launch: ViceLauncher | undefined;
|
|
@@ -196,6 +202,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
196
202
|
this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
|
|
197
203
|
this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
|
|
198
204
|
this.#launch = options.launch;
|
|
205
|
+
this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
|
|
199
206
|
this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
|
|
200
207
|
this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
|
|
201
208
|
this.#watch(vice);
|
|
@@ -268,7 +275,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
268
275
|
session.#timingStart = await session.#adapter.clock();
|
|
269
276
|
// c64_profile reports what ran since the session started.
|
|
270
277
|
await session.#adapter.startProfiler();
|
|
271
|
-
//
|
|
278
|
+
// In VICE 3.10 and older the control-port lines start all pressed; release them before the C64 reads them.
|
|
272
279
|
for (const joystick of session.#joysticks.values()) await session.#adapter.setJoystick(joystick);
|
|
273
280
|
}),
|
|
274
281
|
);
|
|
@@ -368,7 +375,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
368
375
|
case "keyboard": {
|
|
369
376
|
const bytes = Buffer.from((params as ViceOperations["keyboard"]["params"]).data, "hex");
|
|
370
377
|
await this.#observe(() => this.#machine.feedKeyboard(bytes));
|
|
371
|
-
return { queuedBytes: bytes.length };
|
|
378
|
+
return { queuedBytes: bytes.length, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
|
|
372
379
|
}
|
|
373
380
|
case "breakpoint":
|
|
374
381
|
return this.#breakpoint(params as BreakpointParams);
|
|
@@ -450,7 +457,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
450
457
|
const joystick = params as JoystickState;
|
|
451
458
|
await this.#observe(() => this.#machine.setJoystick(joystick));
|
|
452
459
|
this.#joysticks.set(joystick.port, joystick);
|
|
453
|
-
return joystick;
|
|
460
|
+
return { ...joystick, ...(this.#inputFlaw === undefined ? {} : { emulatorFlaw: this.#inputFlaw }) };
|
|
454
461
|
}
|
|
455
462
|
case "warp": {
|
|
456
463
|
const { enabled } = params as ViceOperations["warp"]["params"];
|
|
@@ -553,7 +560,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
553
560
|
* checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
|
|
554
561
|
* The machine is stopped afterwards.
|
|
555
562
|
*/
|
|
556
|
-
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number }> {
|
|
563
|
+
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
|
|
557
564
|
await this.#stop();
|
|
558
565
|
const lines = RASTER[this.#videoStandard].lines;
|
|
559
566
|
const start = await this.#machine.rasterPosition();
|
|
@@ -582,18 +589,19 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
582
589
|
}
|
|
583
590
|
} else {
|
|
584
591
|
frames++;
|
|
585
|
-
if (frames >= limit) return { end: "frames", frames, pc: stop.pc };
|
|
592
|
+
if (frames >= limit) return { end: "frames", frames, pc: stop.pc, space: "c64" };
|
|
586
593
|
await this.#machine.setCondition(clock, arming);
|
|
587
594
|
}
|
|
588
595
|
armingPhase = !armingPhase;
|
|
589
596
|
continue;
|
|
590
597
|
}
|
|
591
598
|
if (end === "unknown") {
|
|
592
|
-
if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc };
|
|
599
|
+
if (await this.#jammedAt(stop.pc)) return { end: "jam", frames, pc: stop.pc, space: "c64" };
|
|
593
600
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this run.");
|
|
594
601
|
}
|
|
595
|
-
// A drive stop is completed before the result names the computer's pc.
|
|
596
|
-
|
|
602
|
+
// A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
|
|
603
|
+
const driveStop = this.#driveStop;
|
|
604
|
+
return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64" };
|
|
597
605
|
}
|
|
598
606
|
} finally {
|
|
599
607
|
await this.#machine.deleteCheckpoint(clock);
|
|
@@ -685,9 +693,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
685
693
|
if (expression !== undefined) await this.#machine.setCondition(checkpoint, expression);
|
|
686
694
|
const outcome = await this.#runFrames(params.timeoutFrames, { checkpoint, deferred });
|
|
687
695
|
const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
|
|
688
|
-
// A stop event carries the computer's PC; a drive
|
|
689
|
-
const pc =
|
|
690
|
-
return { reached: stopReason === "target", stopReason, state: "stopped", pc };
|
|
696
|
+
// A stop event carries the computer's PC; a stop in the drive reports the drive's (F-31).
|
|
697
|
+
const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
|
|
698
|
+
return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space };
|
|
691
699
|
} finally {
|
|
692
700
|
await this.#machine.deleteCheckpoint(checkpoint);
|
|
693
701
|
}
|
|
@@ -952,7 +960,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
952
960
|
reply.push(bytes);
|
|
953
961
|
return { exported: true, bytes: bytes.length };
|
|
954
962
|
}
|
|
955
|
-
const
|
|
963
|
+
const bytes = onlyAttachment(attachments);
|
|
964
|
+
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot (F-07).
|
|
965
|
+
if (!bytes.subarray(0, VSF_MAGIC.length).equals(VSF_MAGIC)) throw new WireFailure("invalid-input", "The file is not a VICE snapshot file (.vsf). Give a file that c64_snapshot save wrote.");
|
|
966
|
+
const file = this.#stage(bytes, "vsf");
|
|
956
967
|
try {
|
|
957
968
|
return await this.#restoreFrom(file);
|
|
958
969
|
} catch (error) {
|
|
@@ -1340,6 +1351,8 @@ export function viceSessionFactory(options: {
|
|
|
1340
1351
|
supervisor: ProcessSupervisor;
|
|
1341
1352
|
env?: NodeJS.ProcessEnv;
|
|
1342
1353
|
log?: (line: string) => void;
|
|
1354
|
+
/** The known flaws of the VICE version that the host checked at start. */
|
|
1355
|
+
flaws?: readonly string[];
|
|
1343
1356
|
}): ViceSessionFactory {
|
|
1344
1357
|
const log = options.log ?? (() => {});
|
|
1345
1358
|
return async ({ videoStandard }) => {
|
|
@@ -1355,6 +1368,6 @@ export function viceSessionFactory(options: {
|
|
|
1355
1368
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
1356
1369
|
});
|
|
1357
1370
|
const vice = await launch("headless", () => {});
|
|
1358
|
-
return ViceSession.start(vice, videoStandard, log, { launch, ownPath: (path) => options.supervisor.ownPath(path) });
|
|
1371
|
+
return ViceSession.start(vice, videoStandard, log, { launch, ownPath: (path) => options.supervisor.ownPath(path), ...(options.flaws === undefined ? {} : { flaws: options.flaws }) });
|
|
1359
1372
|
};
|
|
1360
1373
|
}
|
|
@@ -5,6 +5,7 @@ import { connect as connectTcp, type Socket } from "node:net";
|
|
|
5
5
|
import { encodeFrame, MessageReader } from "../protocol/framing.ts";
|
|
6
6
|
import { elapsedMs, trace } from "../trace.ts";
|
|
7
7
|
import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, withStopAndAsk, type ErrorCode, type Hello, type HostMessage, type Ping, type Request, type Role, type VideoStandard } from "../protocol/messages.ts";
|
|
8
|
+
import { PACKAGE } from "../package.ts";
|
|
8
9
|
|
|
9
10
|
export interface Endpoint {
|
|
10
11
|
host: string;
|
|
@@ -41,7 +42,7 @@ function unreachable(role: Role): WireFailure {
|
|
|
41
42
|
return new WireFailure(
|
|
42
43
|
code,
|
|
43
44
|
withStopAndAsk(
|
|
44
|
-
|
|
45
|
+
`The c64-re-tools host runtime is not running or cannot be reached. Start it on the host (npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host) and try again.`,
|
|
45
46
|
),
|
|
46
47
|
);
|
|
47
48
|
}
|
|
@@ -52,8 +52,3 @@ export function writtenBlocks(map: Uint8Array, minimum: number): CodeRange[] {
|
|
|
52
52
|
return blocks;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** True when the CPU executed an instruction in RAM somewhere in the range. */
|
|
56
|
-
export function executedIn(map: Uint8Array, range: CodeRange): boolean {
|
|
57
|
-
for (let address = range.start; address <= range.end; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0) return true;
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
@@ -7,9 +7,20 @@ import { HostConnection } from "./connect.ts";
|
|
|
7
7
|
import { readProjectFile } from "./transfer.ts";
|
|
8
8
|
|
|
9
9
|
const HOST_LOST =
|
|
10
|
-
"The connection to the c64-re-tools host runtime was lost
|
|
11
|
-
"
|
|
10
|
+
"The connection to the c64-re-tools host runtime was lost. The emulator and its machine state are gone: memory, registers, breakpoints, snapshots and the loaded program. " +
|
|
11
|
+
"The next call starts a new machine when the Host Runtime (c64-re-tools-host) runs. Load the program again and set the breakpoints again.";
|
|
12
12
|
const SESSION_CLOSED = "The C64 session is closed because the c64-re-tools MCP server stops.";
|
|
13
|
+
const LOST_BEFORE_ANSWER = "The connection to the c64-re-tools host runtime was lost before the emulator answered. No machine state was lost. Do the call again.";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The loss of a session that never answered (D57): it had no machine state,
|
|
17
|
+
* so the caller can do the call again on a new session.
|
|
18
|
+
*/
|
|
19
|
+
export class LostBeforeAnswer extends WireFailure {
|
|
20
|
+
constructor() {
|
|
21
|
+
super("machine-unavailable", LOST_BEFORE_ANSWER);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
interface Pending {
|
|
15
26
|
op: ViceOperation;
|
|
@@ -24,6 +35,8 @@ export class ViceSessionClient {
|
|
|
24
35
|
readonly #pending = new Map<number, Pending>();
|
|
25
36
|
#nextId = 1;
|
|
26
37
|
#lost: WireFailure | undefined;
|
|
38
|
+
#answered = false;
|
|
39
|
+
#lossReported = false;
|
|
27
40
|
|
|
28
41
|
private constructor(connection: HostConnection) {
|
|
29
42
|
this.#connection = connection;
|
|
@@ -33,6 +46,7 @@ export class ViceSessionClient {
|
|
|
33
46
|
const pending = this.#pending.get(message.id);
|
|
34
47
|
if (pending === undefined) return; // not ours; ignore
|
|
35
48
|
this.#pending.delete(message.id);
|
|
49
|
+
this.#answered = true;
|
|
36
50
|
if ("error" in message) {
|
|
37
51
|
pending.reject(new WireFailure(message.error.code, message.error.message));
|
|
38
52
|
return;
|
|
@@ -50,7 +64,8 @@ export class ViceSessionClient {
|
|
|
50
64
|
}
|
|
51
65
|
});
|
|
52
66
|
this.closed = connection.closed.then(() => {
|
|
53
|
-
this.#lost ??= new WireFailure("machine-state-lost", HOST_LOST);
|
|
67
|
+
this.#lost ??= this.#answered ? new WireFailure("machine-state-lost", HOST_LOST) : new LostBeforeAnswer();
|
|
68
|
+
this.#lossReported = this.#pending.size > 0;
|
|
54
69
|
for (const pending of this.#pending.values()) pending.reject(this.#lost);
|
|
55
70
|
this.#pending.clear();
|
|
56
71
|
});
|
|
@@ -246,6 +261,21 @@ export class ViceSessionClient {
|
|
|
246
261
|
return this.#request("window", { action });
|
|
247
262
|
}
|
|
248
263
|
|
|
264
|
+
/** True when the connection to the host is gone, whatever the reason. */
|
|
265
|
+
get lost(): boolean {
|
|
266
|
+
return this.#lost !== undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** True when the emulator answered a request: then the session had machine state to lose (D57). */
|
|
270
|
+
get answered(): boolean {
|
|
271
|
+
return this.#answered;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The loss of a lost session, when no request got it; undefined while the session lives (D57). */
|
|
275
|
+
get unreportedLoss(): WireFailure | undefined {
|
|
276
|
+
return this.#lost?.code === "machine-state-lost" && !this.#lossReported ? this.#lost : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
249
279
|
/** Ends the session; the host stops its emulator. Open and later requests fail as closed. */
|
|
250
280
|
async close(): Promise<void> {
|
|
251
281
|
this.#lost ??= new WireFailure("machine-unavailable", SESSION_CLOSED);
|
|
@@ -267,7 +297,10 @@ export class ViceSessionClient {
|
|
|
267
297
|
params: ViceOperations[O]["params"],
|
|
268
298
|
attachments: readonly Uint8Array[] = [],
|
|
269
299
|
): Promise<{ result: ViceOperations[O]["result"]; attachments: Buffer[] }> {
|
|
270
|
-
if (this.#lost !== undefined)
|
|
300
|
+
if (this.#lost !== undefined) {
|
|
301
|
+
this.#lossReported = true;
|
|
302
|
+
return Promise.reject(this.#lost);
|
|
303
|
+
}
|
|
271
304
|
const id = this.#nextId;
|
|
272
305
|
this.#nextId = this.#nextId >= 0xffff_ffff ? 1 : this.#nextId + 1;
|
|
273
306
|
return new Promise((resolve, reject) => {
|
package/src/knowledge/history.ts
CHANGED
|
@@ -103,6 +103,14 @@ export function historyNamed(db: DatabaseSync | undefined, name: string, space:
|
|
|
103
103
|
.sort(bySequence);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
|
|
107
|
+
export function revisionNote(db: DatabaseSync | undefined, id: number): { reason?: string; evidence?: Evidence[] } {
|
|
108
|
+
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
109
|
+
if (db === undefined || raw === undefined) return {};
|
|
110
|
+
const { description, evidence } = withEvidence(db, toRevision(raw));
|
|
111
|
+
return { ...(description === undefined ? {} : { reason: description }), ...(evidence === undefined ? {} : { evidence }) };
|
|
112
|
+
}
|
|
113
|
+
|
|
106
114
|
/** The revision with its evidence, when it has any. */
|
|
107
115
|
function withEvidence(db: DatabaseSync, revision: Revision): Revision {
|
|
108
116
|
const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
|