@henols/c64-re-tools 2.0.0-rc.9 → 2.0.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/README.md +97 -116
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +18 -4
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +1 -1
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
package/src/host/main.ts
CHANGED
|
@@ -7,6 +7,7 @@ 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
9
|
import { viceFlaws, type ViceVersion } from "../native/discover.ts";
|
|
10
|
+
import { inspect } from "./tools/c1541.ts";
|
|
10
11
|
import { createToolDispatcher } from "./tools/index.ts";
|
|
11
12
|
import { checkViceStarts, findVice } from "./vice/process.ts";
|
|
12
13
|
import { viceSessionFactory } from "./vice/session.ts";
|
|
@@ -34,8 +35,9 @@ Options:
|
|
|
34
35
|
directory, with a copy of each VICE log.
|
|
35
36
|
--help Show this help.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
The host always logs its start and stop, connections, failures, and each call
|
|
39
|
+
from a skill script (c1541, petcat) with its outcome. --log-console adds the
|
|
40
|
+
calls of the MCP server and the tool runs. C64RT_TRACE does not apply to the host.`;
|
|
39
41
|
|
|
40
42
|
function log(line: string): void {
|
|
41
43
|
process.stderr.write(`c64-re-tools-host: ${line}\n`);
|
|
@@ -76,6 +78,9 @@ async function main(): Promise<number> {
|
|
|
76
78
|
return 2;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// First, so that every start, also a refused one, tells which runtime runs.
|
|
82
|
+
log(`${PACKAGE.name} ${PACKAGE.version} (Node.js ${process.version}, ${process.platform} ${process.arch})`);
|
|
83
|
+
|
|
79
84
|
// Without an emulator the runtime is useless: refuse to start, by name and with the remedy.
|
|
80
85
|
try {
|
|
81
86
|
findVice(process.env);
|
|
@@ -101,7 +106,7 @@ async function main(): Promise<number> {
|
|
|
101
106
|
}
|
|
102
107
|
log(`VICE ${vice.major}.${vice.minor}${vice.revision === undefined ? "" : ` r${vice.revision}`} starts`);
|
|
103
108
|
const flaws = viceFlaws(vice);
|
|
104
|
-
// A known VICE flaw is named, not worked around
|
|
109
|
+
// A known VICE flaw is named, not worked around.
|
|
105
110
|
for (const flaw of flaws) log(`Known VICE flaw: ${flaw}`);
|
|
106
111
|
|
|
107
112
|
let server;
|
|
@@ -110,7 +115,16 @@ async function main(): Promise<number> {
|
|
|
110
115
|
host: "0.0.0.0",
|
|
111
116
|
port,
|
|
112
117
|
log,
|
|
113
|
-
createViceSession: viceSessionFactory({
|
|
118
|
+
createViceSession: viceSessionFactory({
|
|
119
|
+
supervisor,
|
|
120
|
+
log,
|
|
121
|
+
flaws,
|
|
122
|
+
// c1541 lists the disk, so an autostart index selects a program.
|
|
123
|
+
directory: async (imageType, image) => {
|
|
124
|
+
const { result } = await inspect({ action: "directory", imageType }, image, { supervisor, signal: new AbortController().signal, log });
|
|
125
|
+
return result.action === "directory" ? result.entries : [];
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
114
128
|
tools: createToolDispatcher({ supervisor, log, vice }),
|
|
115
129
|
});
|
|
116
130
|
} catch (error) {
|
package/src/host/server.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { encodeFrame, MessageReader } from "../protocol/framing.ts";
|
|
|
4
4
|
import { checkHello, isOneOf, HEARTBEAT_TIMEOUT_MS, parseClientMessage, ProtocolError, type HostMessage, type ReceivedRequest, type VideoStandard, type WireError, WireFailure } from "../protocol/messages.ts";
|
|
5
5
|
import { TOOL_OPERATIONS, validateToolParams, type ToolOperation, type ToolOperations } from "../protocol/tools.ts";
|
|
6
6
|
import { attachmentCount, VICE_OPERATIONS, validateViceParams, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
|
|
7
|
-
import { elapsedMs, trace } from "../trace.ts";
|
|
7
|
+
import { elapsedMs, summarize, trace } from "../trace.ts";
|
|
8
8
|
|
|
9
9
|
/** One live VICE session, owned by one connection. */
|
|
10
10
|
export interface ViceSessionHandle {
|
|
@@ -234,8 +234,15 @@ class Connection {
|
|
|
234
234
|
|
|
235
235
|
async #answer(request: ReceivedRequest, attachments: Buffer[]): Promise<void> {
|
|
236
236
|
const started = performance.now();
|
|
237
|
-
const record = (outcome: Record<string, unknown>) =>
|
|
238
|
-
|
|
237
|
+
const record = (outcome: Record<string, unknown>) => {
|
|
238
|
+
const ms = elapsedMs(started);
|
|
239
|
+
trace().event("request", { connection: this.#id, op: request.op, id: request.id, params: request.params, attachments: attachments.length, ms, ...outcome });
|
|
240
|
+
// A call from a skill script (c1541, petcat, status) is always logged; the many calls of a VICE session only in the trace.
|
|
241
|
+
if (this.#isTool) {
|
|
242
|
+
const what = outcome.ok === true ? "ok" : `${String(outcome.code)}: ${String(outcome.message)}`;
|
|
243
|
+
this.#log(`${request.op} ${JSON.stringify(summarize(request.params) ?? {})}: ${what} (${ms} ms)`);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
239
246
|
try {
|
|
240
247
|
let result: unknown;
|
|
241
248
|
let replyAttachments: Buffer[] = [];
|
package/src/host/tools/petcat.ts
CHANGED
|
@@ -18,7 +18,7 @@ const TIMEOUT_MS = 30_000;
|
|
|
18
18
|
const LISTING = "listing.txt";
|
|
19
19
|
/**
|
|
20
20
|
* petcat's listing lines (" 10 sys2061"), as number and text. petcat
|
|
21
|
-
* prints π ($ff) as `~`, which is no PETSCII character: the text has
|
|
21
|
+
* prints π ($ff) as `~`, which is no PETSCII character: the text has π.
|
|
22
22
|
*/
|
|
23
23
|
export function parseListing(listing: string): Array<{ number: number; text: string }> {
|
|
24
24
|
const lines: Array<{ number: number; text: string }> = [];
|
package/src/host/tools/status.ts
CHANGED
|
@@ -38,7 +38,7 @@ export async function hostStatus(context: ToolContext, started?: ViceVersion): P
|
|
|
38
38
|
);
|
|
39
39
|
const tools = started === undefined ? probed : [{ name: VICE.name, found: true, path: findVice(context.env ?? process.env), version: versionText(started), runs: true }, ...probed];
|
|
40
40
|
const checked = tools.map((status, index) => requireMinimum(status, specs[index]!));
|
|
41
|
-
// VICE runs with its known flaws; status names them
|
|
41
|
+
// VICE runs with its known flaws; status names them.
|
|
42
42
|
const vice = checked[0]!;
|
|
43
43
|
const version = vice.runs ? viceVersionOf(vice.version ?? "") : undefined;
|
|
44
44
|
const flaws = version === undefined ? [] : viceFlaws(version);
|
package/src/host/vice/adapter.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
5
5
|
import { type Comparison, type Condition, type BacktraceFrame, type HistoryEntry, type Instruction, type MemmapRange, type ProfileEntry, type JoystickState, type MemoryView, type RegisterValues, type Registers, type Space } from "../../protocol/vice.ts";
|
|
6
6
|
import { COVERAGE, formatC64Address, formatC64Byte } from "../../c64.ts";
|
|
7
|
-
import {
|
|
7
|
+
import type { IndexedFrame } from "../../graphics.ts";
|
|
8
|
+
import { decodeDisplay, decodePalette, visibleFrame } from "./screen.ts";
|
|
8
9
|
import type { TextMonitor } from "./text-monitor.ts";
|
|
9
10
|
import type { CiaHidden } from "./chips.ts";
|
|
10
11
|
import {
|
|
@@ -260,7 +261,7 @@ function unknownForm(what: string, lines: string[]): WireFailure {
|
|
|
260
261
|
return new WireFailure("operation-failed", `VICE printed the ${what} in a form that c64-re-tools does not read: ${JSON.stringify(lines.slice(0, 3))}`);
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
/** The coverage byte
|
|
264
|
+
/** The coverage byte of one memmap row's I/O, ROM and RAM columns, each "rwx" with "-" for no access. */
|
|
264
265
|
export function memmapFlags(io: string, rom: string, ram: string): number {
|
|
265
266
|
let flags = 0;
|
|
266
267
|
if (ram[0] === "r") flags |= COVERAGE.ramRead;
|
|
@@ -434,7 +435,7 @@ export class ViceAdapter {
|
|
|
434
435
|
|
|
435
436
|
/**
|
|
436
437
|
* The timer latches and the interrupt mask of the CIA at `base`, from VICE's
|
|
437
|
-
* text-monitor dump: no register read gives them
|
|
438
|
+
* text-monitor dump: no register read gives them.
|
|
438
439
|
*/
|
|
439
440
|
async ciaHidden(base: 0xdc00 | 0xdd00): Promise<CiaHidden> {
|
|
440
441
|
const answer = await this.#computerText(`io ${hex4(base)}`);
|
package/src/host/vice/process.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// - autostart of a PRG needs `-autostartprgmode 1` (inject into RAM);
|
|
13
13
|
// - the joyport command reaches the C64 only through the "Joyport I/O
|
|
14
14
|
// simulation" device (37). In VICE 3.10 and older it holds bits 5-7 of
|
|
15
|
-
// both CIA 1 ports low (VICE bug #2202, a known flaw
|
|
15
|
+
// both CIA 1 ports low (VICE bug #2202, a known flaw);
|
|
16
16
|
// - with the default JAM action a jammed CPU hangs silently; -jamaction 2
|
|
17
17
|
// enters the monitor instead;
|
|
18
18
|
// - the monitor's profiler prints numbers in the host locale, so VICE runs
|
|
@@ -97,7 +97,7 @@ export function viceArguments(options: {
|
|
|
97
97
|
// Autostart injects a PRG into RAM; the default mode copies it to a disk image first.
|
|
98
98
|
"-autostartprgmode",
|
|
99
99
|
"1",
|
|
100
|
-
// VICE's default waits a random time before an autostart types its LOAD, so two runs of one file differ
|
|
100
|
+
// VICE's default waits a random time before an autostart types its LOAD, so two runs of one file differ.
|
|
101
101
|
"+autostart-delay-random",
|
|
102
102
|
// The same RAM after every power-up: a hard reset is a power cycle, and VICE's default can flip
|
|
103
103
|
// random bits, so two runs from one start state could differ.
|
package/src/host/vice/screen.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
// Canonical emulator-frame capture: VICE's own rendered VIC-II frame, never a
|
|
2
2
|
// desktop screenshot; the frame comparison and difference image for baselines.
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import type { IndexedFrame } from "../../graphics.ts";
|
|
6
5
|
import type { VideoStandard } from "../../protocol/messages.ts";
|
|
7
6
|
import { FRAME_SIZE } from "../../protocol/vice.ts";
|
|
8
7
|
|
|
9
|
-
export interface IndexedFrame {
|
|
10
|
-
width: number;
|
|
11
|
-
height: number;
|
|
12
|
-
/** One palette index per pixel, row by row. */
|
|
13
|
-
pixels: Uint8Array;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
8
|
export interface RawDisplay {
|
|
17
9
|
/** Full buffer size, blanking included. */
|
|
18
10
|
bufferWidth: number;
|
|
@@ -83,39 +75,6 @@ export function decodePalette(body: Buffer): Array<[number, number, number]> {
|
|
|
83
75
|
return colours;
|
|
84
76
|
}
|
|
85
77
|
|
|
86
|
-
function chunk(type: string, data: Uint8Array): Buffer {
|
|
87
|
-
const length = Buffer.alloc(4);
|
|
88
|
-
length.writeUInt32BE(data.length, 0);
|
|
89
|
-
const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
|
|
90
|
-
const crc = Buffer.alloc(4);
|
|
91
|
-
crc.writeUInt32BE(crc32(body), 0);
|
|
92
|
-
return Buffer.concat([length, body, crc]);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Encodes an indexed frame as an 8-bit palette PNG. */
|
|
96
|
-
export function encodePng(frame: IndexedFrame, palette: ReadonlyArray<readonly [number, number, number]>): Buffer {
|
|
97
|
-
for (const index of frame.pixels) {
|
|
98
|
-
if (index >= palette.length) throw new Error(`pixel colour ${index} is not in the ${palette.length}-colour palette`);
|
|
99
|
-
}
|
|
100
|
-
const header = Buffer.alloc(13);
|
|
101
|
-
header.writeUInt32BE(frame.width, 0);
|
|
102
|
-
header.writeUInt32BE(frame.height, 4);
|
|
103
|
-
header[8] = 8; // bit depth
|
|
104
|
-
header[9] = 3; // indexed colour
|
|
105
|
-
const rows = Buffer.alloc((frame.width + 1) * frame.height);
|
|
106
|
-
for (let row = 0; row < frame.height; row++) {
|
|
107
|
-
// Filter byte 0 (none), then the row.
|
|
108
|
-
rows.set(frame.pixels.subarray(row * frame.width, (row + 1) * frame.width), row * (frame.width + 1) + 1);
|
|
109
|
-
}
|
|
110
|
-
return Buffer.concat([
|
|
111
|
-
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
112
|
-
chunk("IHDR", header),
|
|
113
|
-
chunk("PLTE", Buffer.from(palette.flat())),
|
|
114
|
-
chunk("IDAT", deflateSync(rows)),
|
|
115
|
-
chunk("IEND", new Uint8Array(0)),
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
78
|
export interface FrameDifference {
|
|
120
79
|
mismatchingPixels: number;
|
|
121
80
|
/** Pixels compared: the frame minus the masked ones. */
|
package/src/host/vice/session.ts
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
// session starts headless and can move the machine into a VICE with a window
|
|
4
4
|
// and back; breakpoints, watchpoints and the rest go along.
|
|
5
5
|
|
|
6
|
-
import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { isOneOf, WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
+
import { DISK_IMAGE_TYPES, type DiskFile, type DiskImageType } from "../../protocol/tools.ts";
|
|
8
|
+
import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type TraceHit, type TraceKind, type TraceResult, type PointStopReason, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
|
|
8
9
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
10
|
import { tmpdir } from "node:os";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
12
13
|
|
|
13
|
-
import { formatC64Address } from "../../c64.ts";
|
|
14
|
+
import { formatC64Address, instructionLength } from "../../c64.ts";
|
|
15
|
+
import { encodePng, type IndexedFrame } from "../../graphics.ts";
|
|
16
|
+
import { instructionText, operandAddress } from "../../opcodes.ts";
|
|
14
17
|
import type { ProcessSupervisor } from "../../native/processes.ts";
|
|
15
18
|
import { trace } from "../../trace.ts";
|
|
16
19
|
import type { ViceSessionFactory, ViceSessionHandle } from "../server.ts";
|
|
@@ -26,7 +29,7 @@ import {
|
|
|
26
29
|
} from "./adapter.ts";
|
|
27
30
|
import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
|
|
28
31
|
import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
|
|
29
|
-
import { compareFrames, differenceImage
|
|
32
|
+
import { compareFrames, differenceImage } from "./screen.ts";
|
|
30
33
|
import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
|
|
31
34
|
import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
|
|
32
35
|
|
|
@@ -121,10 +124,14 @@ export interface SessionOptions {
|
|
|
121
124
|
launch?: ViceLauncher;
|
|
122
125
|
/** Makes the host remove a path if it dies; returns the release. */
|
|
123
126
|
ownPath?: (path: string) => () => void;
|
|
124
|
-
/** The known flaws of this VICE version
|
|
127
|
+
/** The known flaws of this VICE version; keyboard and joystick results name them. */
|
|
125
128
|
flaws?: readonly string[];
|
|
129
|
+
/** Lists the entries of a disk image, so an autostart index is checked first. Without it, VICE gets the index unchecked. */
|
|
130
|
+
directory?: DiskDirectory;
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
export type DiskDirectory = (type: DiskImageType, image: Buffer) => Promise<DiskFile[]>;
|
|
134
|
+
|
|
128
135
|
/**
|
|
129
136
|
* Where VICE keeps a staged file: an autostarted disk stays in drive 8, a tape
|
|
130
137
|
* in the datasette and a cartridge in the port, until the same place gets
|
|
@@ -151,6 +158,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
151
158
|
readonly #untilReturnLimitMs: number;
|
|
152
159
|
readonly #stepLimitMs: number;
|
|
153
160
|
readonly #launch: ViceLauncher | undefined;
|
|
161
|
+
readonly #directory: DiskDirectory | undefined;
|
|
154
162
|
/** Session files that outlive one VICE: snapshots, staged media, handovers. */
|
|
155
163
|
readonly #workDir: string;
|
|
156
164
|
readonly #releaseWorkDir: () => void;
|
|
@@ -177,6 +185,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
177
185
|
#driveStop = false;
|
|
178
186
|
/** Counts stops caused by checkpoints or a CPU JAM rather than by a monitor command. */
|
|
179
187
|
#haltCount = 0;
|
|
188
|
+
/** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
|
|
189
|
+
#lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
|
|
180
190
|
#queue: Promise<unknown> = Promise.resolve();
|
|
181
191
|
#closed = false;
|
|
182
192
|
#lost = false;
|
|
@@ -218,6 +228,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
218
228
|
this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
|
|
219
229
|
this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
|
|
220
230
|
this.#launch = options.launch;
|
|
231
|
+
this.#directory = options.directory;
|
|
221
232
|
this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
|
|
222
233
|
this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
|
|
223
234
|
this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
|
|
@@ -374,14 +385,16 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
374
385
|
return this.#programLoad(params as ViceOperations["programLoad"]["params"], onlyAttachment(attachments));
|
|
375
386
|
case "autostart": {
|
|
376
387
|
const { type, index, run } = params as ViceOperations["autostart"]["params"];
|
|
377
|
-
const
|
|
388
|
+
const bytes = onlyAttachment(attachments);
|
|
389
|
+
if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
|
|
390
|
+
const file = this.#stage(bytes, type);
|
|
378
391
|
// VICE keeps a cartridge through every reset and autostart, as a real C64 does.
|
|
379
392
|
if (type !== "crt" && this.#cartridge) {
|
|
380
393
|
await this.#observe(() => this.#machine.detachCartridge());
|
|
381
394
|
this.#cartridge = false;
|
|
382
395
|
this.#replaceStaged("cartridge", undefined);
|
|
383
396
|
}
|
|
384
|
-
// Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types
|
|
397
|
+
// Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types.
|
|
385
398
|
const centred: Array<1 | 2> = [];
|
|
386
399
|
for (const [port, joystick] of this.#joysticks) {
|
|
387
400
|
if (joystick.direction === "center" && !joystick.fire) continue;
|
|
@@ -445,6 +458,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
445
458
|
return this.#snapshotFile(params as ViceOperations["snapshotFile"]["params"], attachments, reply);
|
|
446
459
|
case "cpuHistory":
|
|
447
460
|
return this.#cpuHistory(params as ViceOperations["cpuHistory"]["params"]);
|
|
461
|
+
case "trace":
|
|
462
|
+
return this.#trace(params as ViceOperations["trace"]["params"]);
|
|
448
463
|
case "backtrace": {
|
|
449
464
|
const { depth, space } = params as ViceOperations["backtrace"]["params"];
|
|
450
465
|
return this.#observe(async () => {
|
|
@@ -462,8 +477,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
462
477
|
return { cycles: (now - this.#timingStart).toString() };
|
|
463
478
|
}
|
|
464
479
|
case "profile": {
|
|
465
|
-
const
|
|
466
|
-
|
|
480
|
+
const request = params as ViceOperations["profile"]["params"];
|
|
481
|
+
if (request.action === "clear") {
|
|
482
|
+
await this.#observe(() => this.#machine.startProfiler());
|
|
483
|
+
return { cleared: true };
|
|
484
|
+
}
|
|
485
|
+
return { entries: await this.#observe(() => this.#machine.profile(request.limit)) };
|
|
467
486
|
}
|
|
468
487
|
case "memmap": {
|
|
469
488
|
const request = params as ViceOperations["memmap"]["params"];
|
|
@@ -508,7 +527,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
508
527
|
const warp = await this.#observe(() => this.#machine.warp());
|
|
509
528
|
// Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
|
|
510
529
|
const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
|
|
511
|
-
if (status.state === "stopped")
|
|
530
|
+
if (status.state === "stopped") {
|
|
531
|
+
status.pc = await this.#pc();
|
|
532
|
+
// VICE 3.9 sends no JAM event: the opcode at the pc tells.
|
|
533
|
+
const stop = this.#lastStop ?? ((await this.#jammedAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
|
|
534
|
+
if (stop !== undefined) Object.assign(status, stop);
|
|
535
|
+
}
|
|
512
536
|
return status;
|
|
513
537
|
}
|
|
514
538
|
|
|
@@ -539,7 +563,13 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
539
563
|
throw new WireFailure("invalid-input", `The anchor is outside the frame: a ${this.#videoStandard} frame has lines 0-${raster.lines - 1} and cycles 0-${raster.cycles - 1}.`);
|
|
540
564
|
}
|
|
541
565
|
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
542
|
-
return {
|
|
566
|
+
return {
|
|
567
|
+
state: "stopped",
|
|
568
|
+
pc: outcome.pc,
|
|
569
|
+
advancedFrames: outcome.frames,
|
|
570
|
+
stopReason: outcome.end === "target" ? "frames" : outcome.end,
|
|
571
|
+
...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
|
|
572
|
+
};
|
|
543
573
|
}
|
|
544
574
|
await this.#stop();
|
|
545
575
|
if (params.action === "until-return") {
|
|
@@ -600,7 +630,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
600
630
|
* checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
|
|
601
631
|
* The machine is stopped afterwards.
|
|
602
632
|
*/
|
|
603
|
-
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
|
|
633
|
+
async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8"; pointId?: number }> {
|
|
604
634
|
await this.#stop();
|
|
605
635
|
const lines = RASTER[this.#videoStandard].lines;
|
|
606
636
|
// A frame ends where the raster passes the anchor; without one, where this run starts.
|
|
@@ -642,13 +672,127 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
642
672
|
}
|
|
643
673
|
// A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
|
|
644
674
|
const driveStop = this.#driveStop;
|
|
645
|
-
|
|
675
|
+
const pointId = end === "target" ? undefined : this.#userPoint(stop.hits)?.pointId;
|
|
676
|
+
return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64", ...(pointId === undefined ? {} : { pointId }) };
|
|
646
677
|
}
|
|
647
678
|
} finally {
|
|
648
679
|
await this.#machine.deleteCheckpoint(clock);
|
|
649
680
|
}
|
|
650
681
|
}
|
|
651
682
|
|
|
683
|
+
/**
|
|
684
|
+
* A trace: runs the machine for up to `frames` frames, counted as
|
|
685
|
+
* #runFrames counts them, with a stopping checkpoint for each point. At each
|
|
686
|
+
* hit the session records the instruction, its registers and the raster
|
|
687
|
+
* position, and runs on. It ends at the frame limit, at `maxHits` hits, or
|
|
688
|
+
* where a user point or a JAM stops the program. VICE also reports the hits
|
|
689
|
+
* of a checkpoint that does not stop, but without the PC, the registers or
|
|
690
|
+
* the raster position, so each point stops.
|
|
691
|
+
*/
|
|
692
|
+
async #trace(params: ViceOperations["trace"]["params"]): Promise<TraceResult> {
|
|
693
|
+
await this.#stop();
|
|
694
|
+
const lines = RASTER[this.#videoStandard].lines;
|
|
695
|
+
const start = await this.#machine.rasterPosition();
|
|
696
|
+
const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
|
|
697
|
+
const ending = rasterWindowExpression(start.line, start.cycle, lines);
|
|
698
|
+
const operation: Record<TraceKind, number> = { exec: 0x04, read: 0x01, write: 0x02 };
|
|
699
|
+
const points: number[] = [];
|
|
700
|
+
const hits: TraceHit[] = [];
|
|
701
|
+
let frames = 0;
|
|
702
|
+
const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
703
|
+
try {
|
|
704
|
+
for (const point of params.points) points.push(await this.#machine.addCheckpoint({ start: point.start, end: point.end, operation: operation[point.kind], space: "c64" }));
|
|
705
|
+
await this.#machine.setCondition(clock, arming);
|
|
706
|
+
let armingPhase = true;
|
|
707
|
+
for (;;) {
|
|
708
|
+
const next = this.#nextStop(HALF_FRAME_LIMIT_MS);
|
|
709
|
+
await this.#resume();
|
|
710
|
+
const stop = await next;
|
|
711
|
+
if (stop === undefined) {
|
|
712
|
+
await this.#stop();
|
|
713
|
+
throw new WireFailure("operation-failed", "The emulator did not reach the next frame in time.");
|
|
714
|
+
}
|
|
715
|
+
const traced = points.flatMap((checkpoint, index) => (stop.hits.includes(checkpoint) ? [index] : []));
|
|
716
|
+
for (const index of traced) {
|
|
717
|
+
hits.push(await this.#traceHit(index, params.points[index]!.kind, frames));
|
|
718
|
+
if (hits.length >= params.maxHits) return { hits, frames, end: "hits", pc: stop.pc };
|
|
719
|
+
}
|
|
720
|
+
if (stop.hits.includes(clock)) {
|
|
721
|
+
if (armingPhase) await this.#machine.setCondition(clock, ending);
|
|
722
|
+
else {
|
|
723
|
+
frames++;
|
|
724
|
+
if (frames >= params.frames) return { hits, frames, end: "frames", pc: stop.pc };
|
|
725
|
+
await this.#machine.setCondition(clock, arming);
|
|
726
|
+
}
|
|
727
|
+
armingPhase = !armingPhase;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (traced.length > 0) continue;
|
|
731
|
+
const point = this.#userPoint(stop.hits);
|
|
732
|
+
if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
|
|
733
|
+
if (await this.#jammedAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
|
|
734
|
+
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
|
|
735
|
+
}
|
|
736
|
+
} finally {
|
|
737
|
+
await this.#machine.deleteCheckpoint(clock);
|
|
738
|
+
for (const checkpoint of points) await this.#machine.deleteCheckpoint(checkpoint);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* One hit of a trace point. An exec point stops before its instruction: the
|
|
744
|
+
* registers and the raster position are the ones of now. A read or write
|
|
745
|
+
* point stops after the instruction that made the access: the last entry of
|
|
746
|
+
* the CPU history is that instruction, with its registers and its clock.
|
|
747
|
+
*/
|
|
748
|
+
async #traceHit(point: number, kind: TraceKind, frame: number): Promise<TraceHit> {
|
|
749
|
+
const position = await this.#machine.rasterPosition();
|
|
750
|
+
const byteAt = async (address: number) => Number.parseInt(await this.#machine.readMemory({ address, size: 1, space: "c64", view: "cpu" }), 16);
|
|
751
|
+
if (kind === "exec") {
|
|
752
|
+
const registers = await this.#machine.readRegisters("c64");
|
|
753
|
+
const bytes = [...Buffer.from(await this.#machine.readMemory({ address: registers.pc, size: 3, space: "c64", view: "cpu" }), "hex")];
|
|
754
|
+
const length = instructionLength(bytes[0]!);
|
|
755
|
+
return {
|
|
756
|
+
point,
|
|
757
|
+
pc: registers.pc,
|
|
758
|
+
bytes: Buffer.from(bytes.slice(0, length)).toString("hex"),
|
|
759
|
+
text: instructionText(bytes, registers.pc),
|
|
760
|
+
a: registers.a,
|
|
761
|
+
x: registers.x,
|
|
762
|
+
y: registers.y,
|
|
763
|
+
sp: registers.sp,
|
|
764
|
+
frame,
|
|
765
|
+
rasterLine: position.line,
|
|
766
|
+
rasterCycle: position.cycle,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const [entry] = await this.#machine.history(1, "c64");
|
|
770
|
+
if (entry === undefined) throw new WireFailure("operation-failed", "The emulator gave no CPU history at a trace hit.");
|
|
771
|
+
const now = await this.#machine.clock();
|
|
772
|
+
const { lines, cycles } = RASTER[this.#videoStandard];
|
|
773
|
+
const span = BigInt(lines * cycles);
|
|
774
|
+
const at = Number((((BigInt(position.line * cycles + position.cycle) - (now - entry.clock)) % span) + span) % span);
|
|
775
|
+
const bytes = [...Buffer.from(entry.bytes, "hex")];
|
|
776
|
+
// A zero-page pointer is read now: the instruction did not change it.
|
|
777
|
+
const pointer = new Map<number, number>();
|
|
778
|
+
for (const address of [bytes[1] ?? 0, ((bytes[1] ?? 0) + 1) & 0xff, ((bytes[1] ?? 0) + entry.x) & 0xff, ((bytes[1] ?? 0) + entry.x + 1) & 0xff]) pointer.set(address, await byteAt(address));
|
|
779
|
+
const address = operandAddress(bytes, entry.x, entry.y, (zp) => pointer.get(zp) ?? 0);
|
|
780
|
+
return {
|
|
781
|
+
point,
|
|
782
|
+
pc: entry.address,
|
|
783
|
+
bytes: entry.bytes,
|
|
784
|
+
text: instructionText(bytes, entry.address),
|
|
785
|
+
a: entry.a,
|
|
786
|
+
x: entry.x,
|
|
787
|
+
y: entry.y,
|
|
788
|
+
sp: entry.sp,
|
|
789
|
+
frame,
|
|
790
|
+
rasterLine: Math.floor(at / cycles),
|
|
791
|
+
rasterCycle: at % cycles,
|
|
792
|
+
...(address === undefined ? {} : { address, value: await byteAt(address) }),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
652
796
|
/**
|
|
653
797
|
* Completes a stop that a drive 8 checkpoint caused: the computer runs to
|
|
654
798
|
* its next instruction boundary, where a computer checkpoint stops it, so
|
|
@@ -659,6 +803,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
659
803
|
async #completeDriveStop(): Promise<number | undefined> {
|
|
660
804
|
if (!this.#driveStop) return undefined;
|
|
661
805
|
this.#driveStop = false;
|
|
806
|
+
// The drive checkpoint stopped the machine; the stop at the boundary keeps that reason.
|
|
807
|
+
const reason = this.#lastStop;
|
|
662
808
|
const boundary = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
|
|
663
809
|
try {
|
|
664
810
|
for (let attempt = 0; attempt < DRIVE_STOP_TRIES; attempt++) {
|
|
@@ -669,6 +815,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
669
815
|
// The drive can hit its checkpoint again before the computer gets to the next instruction.
|
|
670
816
|
if (stop.hits.includes(boundary)) {
|
|
671
817
|
this.#driveStop = false;
|
|
818
|
+
this.#lastStop = reason;
|
|
672
819
|
trace().event("vice.drive-stop", { pc: stop.pc, resumes: attempt + 1 });
|
|
673
820
|
return stop.pc;
|
|
674
821
|
}
|
|
@@ -683,14 +830,20 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
683
830
|
/** Names what caused a stop during a frame-clocked run. */
|
|
684
831
|
#stopCause(stop: Stop, clock: number, target: number | undefined): Exclude<RunEnd, "frames" | "jam"> | "clock" | "unknown" {
|
|
685
832
|
if (target !== undefined && stop.hits.includes(target)) return "target";
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
if (isPoint(this.#breakpoints)) return "breakpoint";
|
|
689
|
-
if (isPoint(this.#watchpoints)) return "watchpoint";
|
|
833
|
+
const point = this.#userPoint(stop.hits);
|
|
834
|
+
if (point !== undefined) return point.stopReason;
|
|
690
835
|
if (stop.hits.includes(clock)) return "clock";
|
|
691
836
|
return "unknown";
|
|
692
837
|
}
|
|
693
838
|
|
|
839
|
+
/** The user breakpoint, else the user watchpoint, among the hit VICE checkpoints. */
|
|
840
|
+
#userPoint(hits: number[]): { stopReason: "breakpoint" | "watchpoint"; pointId: number } | undefined {
|
|
841
|
+
for (const [stopReason, registry] of [["breakpoint", this.#breakpoints], ["watchpoint", this.#watchpoints]] as const) {
|
|
842
|
+
for (const [pointId, point] of registry) if (hits.includes(point.checkpoint)) return { stopReason, pointId };
|
|
843
|
+
}
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
|
|
694
847
|
async #jammedAt(pc: number): Promise<boolean> {
|
|
695
848
|
const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
|
|
696
849
|
return JAM_OPCODES.has(Number.parseInt(opcode, 16));
|
|
@@ -734,9 +887,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
734
887
|
if (expression !== undefined) await this.#machine.setCondition(checkpoint, expression);
|
|
735
888
|
const outcome = await this.#runFrames(params.timeoutFrames, { checkpoint, deferred });
|
|
736
889
|
const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
|
|
737
|
-
// A stop event carries the computer's PC; a stop in the drive reports the drive's
|
|
890
|
+
// A stop event carries the computer's PC; a stop in the drive reports the drive's.
|
|
738
891
|
const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
|
|
739
|
-
return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space };
|
|
892
|
+
return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space, ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }) };
|
|
740
893
|
} finally {
|
|
741
894
|
await this.#machine.deleteCheckpoint(checkpoint);
|
|
742
895
|
}
|
|
@@ -801,7 +954,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
801
954
|
if (params.action === "list") return { breakpoints: [...this.#breakpoints.values()].map(publicView) };
|
|
802
955
|
if (params.action === "remove") return this.#removePoint(this.#breakpoints, "breakpoint", params.id);
|
|
803
956
|
if (params.action !== "add") return publicView(await this.#changePoint(this.#breakpoints, "breakpoint", params));
|
|
804
|
-
// A breakpoint over a range stops at the first instruction executed in it: exec, not a watch
|
|
957
|
+
// A breakpoint over a range stops at the first instruction executed in it: exec, not a watch.
|
|
805
958
|
const added = await this.#addPoint({
|
|
806
959
|
...(params.size === undefined ? pointRange({ address: params.address }) : { start: params.address, end: params.address + params.size - 1, operation: 0x04 }),
|
|
807
960
|
space: params.space,
|
|
@@ -1002,7 +1155,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1002
1155
|
return { restored: true, state: "stopped" };
|
|
1003
1156
|
}
|
|
1004
1157
|
|
|
1005
|
-
/** A snapshot as bytes
|
|
1158
|
+
/** A snapshot as bytes: export a named one into `reply`, or restore from the attached .vsf. */
|
|
1006
1159
|
async #snapshotFile(params: ViceOperations["snapshotFile"]["params"], attachments: Buffer[], reply: Buffer[]): Promise<ViceOperations["snapshotFile"]["result"]> {
|
|
1007
1160
|
if (params.action === "export") {
|
|
1008
1161
|
const file = this.#snapshots.get(params.name);
|
|
@@ -1012,7 +1165,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1012
1165
|
return { exported: true, bytes: bytes.length };
|
|
1013
1166
|
}
|
|
1014
1167
|
const bytes = onlyAttachment(attachments);
|
|
1015
|
-
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot
|
|
1168
|
+
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot.
|
|
1016
1169
|
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.");
|
|
1017
1170
|
const file = this.#stage(bytes, "vsf");
|
|
1018
1171
|
try {
|
|
@@ -1185,6 +1338,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1185
1338
|
*/
|
|
1186
1339
|
async #resumingCommand(send: () => Promise<void>): Promise<void> {
|
|
1187
1340
|
this.#state = "running";
|
|
1341
|
+
this.#lastStop = undefined;
|
|
1188
1342
|
try {
|
|
1189
1343
|
await send();
|
|
1190
1344
|
} catch (error) {
|
|
@@ -1360,10 +1514,12 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1360
1514
|
const stop: Stop = { pc: decodeProgramCounter(event.body), hits: this.#checkpointHits };
|
|
1361
1515
|
if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
|
|
1362
1516
|
if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
|
|
1517
|
+
this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
|
|
1363
1518
|
this.#checkpointHits = [];
|
|
1364
1519
|
this.#state = "stopped";
|
|
1365
1520
|
this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
|
|
1366
1521
|
} else if (event.type === ResponseType.resumed) {
|
|
1522
|
+
this.#lastStop = undefined;
|
|
1367
1523
|
this.#state = "running";
|
|
1368
1524
|
}
|
|
1369
1525
|
}
|
|
@@ -1400,6 +1556,17 @@ async function setPoint(
|
|
|
1400
1556
|
return checkpoint;
|
|
1401
1557
|
}
|
|
1402
1558
|
|
|
1559
|
+
/**
|
|
1560
|
+
* Refuses an autostart index that selects no program: VICE numbers the
|
|
1561
|
+
* entries from 1, and the caller from 0, as the c64-disk directory list does.
|
|
1562
|
+
* A DEL entry holds no program, and an index past the last entry selects nothing.
|
|
1563
|
+
*/
|
|
1564
|
+
function checkDirectoryEntry(entries: DiskFile[], index: number): void {
|
|
1565
|
+
const entry = entries[index - 1];
|
|
1566
|
+
if (entry === undefined) throw new WireFailure("invalid-input", `The disk has ${entries.length} directory entries, so index ${index - 1} selects no entry. The c64-disk directory list gives the index of each entry.`);
|
|
1567
|
+
if (entry.type === "del") throw new WireFailure("invalid-input", `Directory entry ${index - 1} (${entry.name}) is a DEL entry and holds no program. Give the index of a PRG entry.`);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1403
1570
|
/** The production factory: launch a headless VICE per session through the host's supervisor. */
|
|
1404
1571
|
export function viceSessionFactory(options: {
|
|
1405
1572
|
supervisor: ProcessSupervisor;
|
|
@@ -1407,6 +1574,7 @@ export function viceSessionFactory(options: {
|
|
|
1407
1574
|
log?: (line: string) => void;
|
|
1408
1575
|
/** The known flaws of the VICE version that the host checked at start. */
|
|
1409
1576
|
flaws?: readonly string[];
|
|
1577
|
+
directory?: DiskDirectory;
|
|
1410
1578
|
}): ViceSessionFactory {
|
|
1411
1579
|
return async ({ videoStandard, log = options.log ?? (() => {}) }) => {
|
|
1412
1580
|
const launch: ViceLauncher = (mode, launchLog) =>
|
|
@@ -1421,6 +1589,11 @@ export function viceSessionFactory(options: {
|
|
|
1421
1589
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
1422
1590
|
});
|
|
1423
1591
|
const vice = await launch("headless", () => {});
|
|
1424
|
-
return ViceSession.start(vice, videoStandard, log, {
|
|
1592
|
+
return ViceSession.start(vice, videoStandard, log, {
|
|
1593
|
+
launch,
|
|
1594
|
+
ownPath: (path) => options.supervisor.ownPath(path),
|
|
1595
|
+
...(options.flaws === undefined ? {} : { flaws: options.flaws }),
|
|
1596
|
+
...(options.directory === undefined ? {} : { directory: options.directory }),
|
|
1597
|
+
});
|
|
1425
1598
|
};
|
|
1426
1599
|
}
|
|
@@ -42,7 +42,7 @@ export async function runProgram(client: ViceSessionClient, path: string, prg: U
|
|
|
42
42
|
/**
|
|
43
43
|
* Types RUN at the READY prompt and stops where BASIC starts the first line,
|
|
44
44
|
* before its SYS. Every tool that starts a loaded program this way reaches
|
|
45
|
-
* its entry at the same raster line and timer values
|
|
45
|
+
* its entry at the same raster line and timer values. False when BASIC
|
|
46
46
|
* runs no line within RUN_LIMIT_FRAMES.
|
|
47
47
|
*/
|
|
48
48
|
export async function typeRun(client: ViceSessionClient): Promise<boolean> {
|
|
@@ -37,7 +37,7 @@ export function hostEndpoints(env: NodeJS.ProcessEnv = process.env): Endpoint[]
|
|
|
37
37
|
return ["127.0.0.1", "host.docker.internal", "host.containers.internal"].map((host) => ({ host, port: DEFAULT_HOST_PORT }));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** A host that does not run or cannot be reached is machine-unavailable for every role
|
|
40
|
+
/** A host that does not run or cannot be reached is machine-unavailable for every role. */
|
|
41
41
|
function unreachable(): WireFailure {
|
|
42
42
|
return new WireFailure(
|
|
43
43
|
"machine-unavailable",
|