@henols/c64-re-tools 2.0.0-rc.7 → 2.0.0-rc.9
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/package.json +1 -1
- package/skills/c64-basic/SKILL.md +1 -1
- package/skills/c64-basic/scripts/basic.ts +1 -1
- package/skills/c64-emulator/SKILL.md +2 -1
- package/skills/c64-knowledge/SKILL.md +1 -1
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-provenance/SKILL.md +4 -1
- package/skills/c64-provenance/scripts/compare.ts +9 -6
- package/skills/c64-provenance/scripts/provenance.ts +13 -7
- package/skills/c64-source/SKILL.md +6 -5
- package/skills/c64-source/scripts/generate.ts +19 -0
- package/skills/c64-source/scripts/loader.ts +172 -7
- package/skills/c64-source/scripts/source.ts +7 -3
- package/skills/c64-static-analysis/SKILL.md +1 -1
- package/skills/c64-static-analysis/scripts/idioms.ts +2 -2
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/scenario.ts +7 -5
- package/skills/c64-unpacker/SKILL.md +2 -2
- package/skills/c64-unpacker/scripts/loads.ts +4 -3
- package/skills/c64-unpacker/scripts/machine.ts +13 -1
- package/skills/c64-unpacker/scripts/unpack.ts +40 -13
- package/src/c64.ts +6 -1
- package/src/host/main.ts +27 -6
- package/src/host/vice/adapter.ts +14 -0
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/screen.ts +3 -2
- package/src/host/vice/session.ts +23 -7
- package/src/host-client/autostart.ts +11 -1
- package/src/host-client/tools.ts +1 -1
- package/src/mcp/tools/debug.ts +6 -3
- package/src/mcp/tools/machine.ts +2 -2
- package/src/mcp/tools/media.ts +2 -0
- package/src/mcp/tools/video.ts +5 -2
- package/src/protocol/vice.ts +50 -8
- package/src/trace.ts +55 -23
package/src/protocol/vice.ts
CHANGED
|
@@ -168,9 +168,14 @@ export interface CiaState {
|
|
|
168
168
|
ddrB: number;
|
|
169
169
|
timerA: number;
|
|
170
170
|
timerB: number;
|
|
171
|
+
/** The values that a timer loads at an underflow: write-only on the chip, from VICE's dump (D63). */
|
|
172
|
+
latchA: number;
|
|
173
|
+
latchB: number;
|
|
171
174
|
controlA: number;
|
|
172
175
|
controlB: number;
|
|
173
176
|
interruptStatus: number;
|
|
177
|
+
/** The interrupt sources that are on: bits 0-4 of a write to $xd0d with bit 7 set. */
|
|
178
|
+
interruptMask: number;
|
|
174
179
|
tod: { hours: number; minutes: number; seconds: number; tenths: number };
|
|
175
180
|
}
|
|
176
181
|
|
|
@@ -256,6 +261,24 @@ export interface Rectangle {
|
|
|
256
261
|
height: number;
|
|
257
262
|
}
|
|
258
263
|
|
|
264
|
+
/** The visible frame with normal borders that a screen capture holds, in pixels. */
|
|
265
|
+
export const FRAME_SIZE: Record<VideoStandard, { width: number; height: number }> = {
|
|
266
|
+
pal: { width: 384, height: 272 },
|
|
267
|
+
ntsc: { width: 384, height: 247 },
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** True when the mask rectangles leave no pixel of the frame to compare. */
|
|
271
|
+
export function maskCoversFrame(mask: readonly Rectangle[], standard: VideoStandard): boolean {
|
|
272
|
+
const { width, height } = FRAME_SIZE[standard];
|
|
273
|
+
const masked = new Uint8Array(width * height);
|
|
274
|
+
for (const rectangle of mask) {
|
|
275
|
+
for (let y = rectangle.y; y < Math.min(height, rectangle.y + rectangle.height); y++) {
|
|
276
|
+
if (rectangle.x < width) masked.fill(1, y * width + rectangle.x, y * width + Math.min(width, rectangle.x + rectangle.width));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return !masked.includes(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
259
282
|
export interface ScreenComparison {
|
|
260
283
|
match: boolean;
|
|
261
284
|
mismatchingPixels: number;
|
|
@@ -312,8 +335,8 @@ export interface MachineStatus {
|
|
|
312
335
|
window: boolean;
|
|
313
336
|
/** Present only while the C64 CPU is stopped. */
|
|
314
337
|
pc?: number;
|
|
315
|
-
/**
|
|
316
|
-
cartridge
|
|
338
|
+
/** True while a cartridge from an autostart is attached (D64). */
|
|
339
|
+
cartridge: boolean;
|
|
317
340
|
}
|
|
318
341
|
|
|
319
342
|
export interface MemoryReadParams {
|
|
@@ -370,6 +393,12 @@ export interface ExecutionParams {
|
|
|
370
393
|
/** step and next: defaults to 1; advance-frames: required. */
|
|
371
394
|
count?: number;
|
|
372
395
|
space: Space;
|
|
396
|
+
/**
|
|
397
|
+
* advance-frames only: the raster position that ends a frame, instead of the
|
|
398
|
+
* position where this call starts. Runs that stop between frames then keep
|
|
399
|
+
* the frames of one timeline (D62).
|
|
400
|
+
*/
|
|
401
|
+
anchor?: { line: number; cycle: number };
|
|
373
402
|
}
|
|
374
403
|
|
|
375
404
|
export interface ExecutionResult {
|
|
@@ -421,8 +450,11 @@ export interface ViceOperations {
|
|
|
421
450
|
/** Attachment: the PRG bytes, load address first. */
|
|
422
451
|
programLoad: { params: { address?: number }; result: { state: RunState; loadAddress: number; size: number } };
|
|
423
452
|
/** Attachment: the program or image bytes; `type` is the file's extension. `index` is VICE's file number: 0 loads "*", 1 is the first directory entry. */
|
|
424
|
-
/**
|
|
425
|
-
|
|
453
|
+
/**
|
|
454
|
+
* `emulatorFlaw`: for a tape image, a known flaw of the host's VICE version that a tape load meets (D56).
|
|
455
|
+
* `joysticksCentred`: the control ports that held a direction or fire and were centred first (R3-05).
|
|
456
|
+
*/
|
|
457
|
+
autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState; emulatorFlaw?: string; joysticksCentred?: Array<1 | 2> } };
|
|
426
458
|
/** Attachment: the disk image bytes; `type` is the file's extension. */
|
|
427
459
|
diskAttach: { params: { type: string }; result: { attached: boolean } };
|
|
428
460
|
/** pc is the reset vector when run is false: the CPU stops there. */
|
|
@@ -748,7 +780,7 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
748
780
|
return { space: params.space, values: result } as ViceOperations[O]["params"];
|
|
749
781
|
}
|
|
750
782
|
case "execution": {
|
|
751
|
-
onlyFields(params, ["action", "space", "count"]);
|
|
783
|
+
onlyFields(params, ["action", "space", "count", "anchor"]);
|
|
752
784
|
if (!isOneOf(EXECUTION_ACTIONS, params.action)) invalid(`action must be one of ${EXECUTION_ACTIONS.join(", ")}`);
|
|
753
785
|
if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
|
|
754
786
|
const counted = params.action === "step" || params.action === "next" || params.action === "advance-frames";
|
|
@@ -760,6 +792,13 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
760
792
|
}
|
|
761
793
|
const result: ExecutionParams = { action: params.action, space: params.space };
|
|
762
794
|
if (counted) result.count = (params.count as number | undefined) ?? 1;
|
|
795
|
+
if (params.anchor !== undefined) {
|
|
796
|
+
if (params.action !== "advance-frames") invalid(`anchor is not used with action ${params.action}`);
|
|
797
|
+
const anchor = params.anchor;
|
|
798
|
+
if (!isObject(anchor) || !isInteger(anchor.line, 0, 511) || !isInteger(anchor.cycle, 0, 127)) invalid("anchor needs line from 0 to 511 and cycle from 0 to 127");
|
|
799
|
+
onlyFields(anchor as Record<string, unknown>, ["line", "cycle"]);
|
|
800
|
+
result.anchor = { line: anchor.line as number, cycle: anchor.cycle as number };
|
|
801
|
+
}
|
|
763
802
|
return result as ViceOperations[O]["params"];
|
|
764
803
|
}
|
|
765
804
|
case "runUntil": {
|
|
@@ -972,7 +1011,7 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
972
1011
|
typeof value.warp === "boolean" &&
|
|
973
1012
|
typeof value.window === "boolean" &&
|
|
974
1013
|
(value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
|
|
975
|
-
|
|
1014
|
+
typeof value.cartridge === "boolean";
|
|
976
1015
|
if (!ok) throw new ProtocolError("status result is malformed");
|
|
977
1016
|
break;
|
|
978
1017
|
}
|
|
@@ -1059,6 +1098,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1059
1098
|
if (!isOneOf(RUN_STATES, value.state)) throw new ProtocolError(`${op} result is malformed`);
|
|
1060
1099
|
if (op === "reset" && value.pc !== undefined && !isInteger(value.pc, 0, 0xffff)) throw new ProtocolError("reset result is malformed");
|
|
1061
1100
|
if (op === "autostart" && !(value.emulatorFlaw === undefined || typeof value.emulatorFlaw === "string")) throw new ProtocolError("autostart result is malformed");
|
|
1101
|
+
if (op === "autostart" && !(value.joysticksCentred === undefined || (Array.isArray(value.joysticksCentred) && value.joysticksCentred.every((port) => port === 1 || port === 2)))) {
|
|
1102
|
+
throw new ProtocolError("autostart result is malformed");
|
|
1103
|
+
}
|
|
1062
1104
|
break;
|
|
1063
1105
|
}
|
|
1064
1106
|
case "programLoad": {
|
|
@@ -1235,8 +1277,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1235
1277
|
isObject(chip) &&
|
|
1236
1278
|
(chip.id === 1 || chip.id === 2) &&
|
|
1237
1279
|
(["portA", "portB", "ddrA", "ddrB", "controlA", "controlB", "interruptStatus"] as const).every((name) => isInteger(chip[name], 0, 0xff)) &&
|
|
1238
|
-
isInteger(chip.
|
|
1239
|
-
isInteger(chip
|
|
1280
|
+
isInteger(chip.interruptMask, 0, 0x1f) &&
|
|
1281
|
+
(["timerA", "timerB", "latchA", "latchB"] as const).every((name) => isInteger(chip[name], 0, 0xffff)) &&
|
|
1240
1282
|
isObject(chip.tod) &&
|
|
1241
1283
|
isInteger(chip.tod.hours, 0, 23) &&
|
|
1242
1284
|
isInteger(chip.tod.minutes, 0, 99) &&
|
package/src/trace.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// An opt-in trace for the person who debugs a run. With C64RT_TRACE=<directory>
|
|
2
|
-
// in the environment, each process appends its events as
|
|
3
|
-
// of its own in that directory
|
|
4
|
-
//
|
|
2
|
+
// in the environment (the host: --log-dir), each process appends its events as
|
|
3
|
+
// JSON lines to a file of its own in that directory. The host can also echo
|
|
4
|
+
// each event to the console (--log-console). Off, every call does nothing. The
|
|
5
|
+
// trace is for people: nothing of it reaches the agent, and no result depends on it.
|
|
5
6
|
|
|
6
7
|
import { appendFileSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
@@ -66,14 +67,16 @@ export class Trace {
|
|
|
66
67
|
readonly kind: TraceKind;
|
|
67
68
|
/** The directory of the trace, or undefined when the trace is off. */
|
|
68
69
|
readonly dir: string | undefined;
|
|
69
|
-
/** The file this trace appends to, or undefined when the trace
|
|
70
|
+
/** The file this trace appends to, or undefined when the trace writes no file. */
|
|
70
71
|
readonly file: string | undefined;
|
|
72
|
+
readonly #echo: ((line: string) => void) | undefined;
|
|
71
73
|
#broken = false;
|
|
72
74
|
|
|
73
|
-
private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined) {
|
|
75
|
+
private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined, echo?: (line: string) => void) {
|
|
74
76
|
this.kind = kind;
|
|
75
77
|
this.dir = dir;
|
|
76
78
|
this.file = file;
|
|
79
|
+
this.#echo = echo;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/** A trace that records nothing. */
|
|
@@ -81,31 +84,46 @@ export class Trace {
|
|
|
81
84
|
return new Trace(kind, undefined, undefined);
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/** A trace that writes no file and gives each event to `echo` as one line. */
|
|
88
|
+
static echoing(kind: TraceKind, echo: (line: string) => void): Trace {
|
|
89
|
+
return new Trace(kind, undefined, undefined, echo);
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
/**
|
|
85
93
|
* A trace into `dir`, created when it is missing. The file is
|
|
86
94
|
* `<kind>-<CET time>-<pid>.jsonl`, and its first event is `trace.start`.
|
|
87
95
|
* Throws when the directory cannot be used.
|
|
88
96
|
*/
|
|
89
|
-
static open(kind: TraceKind, dir: string, now = new Date()): Trace {
|
|
97
|
+
static open(kind: TraceKind, dir: string, now = new Date(), echo?: (line: string) => void): Trace {
|
|
90
98
|
mkdirSync(dir, { recursive: true });
|
|
91
|
-
const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`));
|
|
99
|
+
const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`), echo);
|
|
92
100
|
trace.event("trace.start", { version: runningVersion(), pid: process.pid, argv: process.argv.slice(1), cwd: process.cwd(), node: process.version, platform: process.platform });
|
|
93
101
|
return trace;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
get on(): boolean {
|
|
105
|
+
return this.#writes || this.#echo !== undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get #writes(): boolean {
|
|
97
109
|
return this.file !== undefined && !this.#broken;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Records one event: appends it to the file, and gives it to the echo as
|
|
114
|
+
* "<event> <fields as JSON>". The echo skips `log` events, which the program
|
|
115
|
+
* prints itself. A write that fails stops the file; nothing throws.
|
|
116
|
+
*/
|
|
101
117
|
event(name: string, fields: TraceFields = {}): void {
|
|
102
118
|
if (!this.on) return;
|
|
103
|
-
const
|
|
119
|
+
const summary: Record<string, unknown> = {};
|
|
104
120
|
for (const [key, value] of Object.entries(fields)) {
|
|
105
|
-
if (!RESERVED.has(key) && value !== undefined)
|
|
121
|
+
if (!RESERVED.has(key) && value !== undefined) summary[key] = summarize(value);
|
|
106
122
|
}
|
|
123
|
+
if (this.#echo !== undefined && name !== "log") this.#echo(`${name} ${JSON.stringify(summary)}`);
|
|
124
|
+
if (!this.#writes) return;
|
|
107
125
|
try {
|
|
108
|
-
appendFileSync(this.file!, `${JSON.stringify(
|
|
126
|
+
appendFileSync(this.file!, `${JSON.stringify({ t: isoCet(), kind: this.kind, event: name, ...summary })}\n`);
|
|
109
127
|
} catch {
|
|
110
128
|
this.#broken = true;
|
|
111
129
|
}
|
|
@@ -121,7 +139,7 @@ export class Trace {
|
|
|
121
139
|
* trace, as `<kind>-<pid>-<name>`. A copy that fails is recorded, not thrown.
|
|
122
140
|
*/
|
|
123
141
|
keep(source: string, name: string): void {
|
|
124
|
-
if (!this
|
|
142
|
+
if (!this.#writes) return;
|
|
125
143
|
const target = join(this.dir!, `${this.kind}-${process.pid}-${name}`);
|
|
126
144
|
try {
|
|
127
145
|
copyFileSync(source, target);
|
|
@@ -135,23 +153,37 @@ export class Trace {
|
|
|
135
153
|
// Off until a program calls startTrace; the kind is a placeholder until then.
|
|
136
154
|
let current = Trace.off("script");
|
|
137
155
|
|
|
156
|
+
/** Where the trace of a process goes: a directory for its file, and an echo for each event. */
|
|
157
|
+
export interface TraceTarget {
|
|
158
|
+
/** The directory, and how the user gave it (`--log-dir <path>`, for example) for a warning. */
|
|
159
|
+
dir?: { path: string; given: string };
|
|
160
|
+
echo?: (line: string) => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The target that C64RT_TRACE in `env` gives: its directory, or nothing. */
|
|
164
|
+
export function traceTargetFromEnv(env: NodeJS.ProcessEnv = process.env): TraceTarget {
|
|
165
|
+
const path = env[TRACE_VARIABLE];
|
|
166
|
+
return path === undefined || path === "" ? {} : { dir: { path, given: `${TRACE_VARIABLE}=${path}` } };
|
|
167
|
+
}
|
|
168
|
+
|
|
138
169
|
/**
|
|
139
|
-
* Switches the trace of this process on
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* start of a program.
|
|
170
|
+
* Switches the trace of this process on for `target` (by default what
|
|
171
|
+
* C64RT_TRACE in `env` gives), and off when the target is empty. A directory
|
|
172
|
+
* that cannot be used writes no file and reports that through `warn`; an echo
|
|
173
|
+
* stays on. Called once, at the start of a program.
|
|
143
174
|
*/
|
|
144
|
-
export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; warn?: (line: string) => void } = {}): Trace {
|
|
145
|
-
const dir =
|
|
146
|
-
|
|
147
|
-
|
|
175
|
+
export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; target?: TraceTarget; warn?: (line: string) => void } = {}): Trace {
|
|
176
|
+
const { dir, echo } = options.target ?? traceTargetFromEnv(options.env);
|
|
177
|
+
const noFile = () => (echo === undefined ? Trace.off(kind) : Trace.echoing(kind, echo));
|
|
178
|
+
if (dir === undefined) {
|
|
179
|
+
current = noFile();
|
|
148
180
|
return current;
|
|
149
181
|
}
|
|
150
182
|
try {
|
|
151
|
-
current = Trace.open(kind, dir);
|
|
183
|
+
current = Trace.open(kind, dir.path, new Date(), echo);
|
|
152
184
|
} catch (error) {
|
|
153
|
-
current =
|
|
154
|
-
options.warn?.(`${
|
|
185
|
+
current = noFile();
|
|
186
|
+
options.warn?.(`${dir.given} cannot be used (${error instanceof Error ? error.message : String(error)}); no trace file is written.`);
|
|
155
187
|
}
|
|
156
188
|
return current;
|
|
157
189
|
}
|