@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.
Files changed (36) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/skills/c64-basic/SKILL.md +1 -1
  4. package/skills/c64-basic/scripts/basic.ts +1 -1
  5. package/skills/c64-emulator/SKILL.md +2 -1
  6. package/skills/c64-knowledge/SKILL.md +1 -1
  7. package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
  8. package/skills/c64-provenance/SKILL.md +4 -1
  9. package/skills/c64-provenance/scripts/compare.ts +9 -6
  10. package/skills/c64-provenance/scripts/provenance.ts +13 -7
  11. package/skills/c64-source/SKILL.md +6 -5
  12. package/skills/c64-source/scripts/generate.ts +19 -0
  13. package/skills/c64-source/scripts/loader.ts +172 -7
  14. package/skills/c64-source/scripts/source.ts +7 -3
  15. package/skills/c64-static-analysis/SKILL.md +1 -1
  16. package/skills/c64-static-analysis/scripts/idioms.ts +2 -2
  17. package/skills/c64-testing/references/scenario-format.md +1 -1
  18. package/skills/c64-testing/scripts/scenario.ts +7 -5
  19. package/skills/c64-unpacker/SKILL.md +2 -2
  20. package/skills/c64-unpacker/scripts/loads.ts +4 -3
  21. package/skills/c64-unpacker/scripts/machine.ts +13 -1
  22. package/skills/c64-unpacker/scripts/unpack.ts +40 -13
  23. package/src/c64.ts +6 -1
  24. package/src/host/main.ts +27 -6
  25. package/src/host/vice/adapter.ts +14 -0
  26. package/src/host/vice/chips.ts +11 -1
  27. package/src/host/vice/screen.ts +3 -2
  28. package/src/host/vice/session.ts +23 -7
  29. package/src/host-client/autostart.ts +11 -1
  30. package/src/host-client/tools.ts +1 -1
  31. package/src/mcp/tools/debug.ts +6 -3
  32. package/src/mcp/tools/machine.ts +2 -2
  33. package/src/mcp/tools/media.ts +2 -0
  34. package/src/mcp/tools/video.ts +5 -2
  35. package/src/protocol/vice.ts +50 -8
  36. package/src/trace.ts +55 -23
@@ -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
- /** Present, true, while a cartridge from an autostart is attached. */
316
- cartridge?: true;
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
- /** `emulatorFlaw`: for a tape image, a known flaw of the host's VICE version that a tape load meets (D56). */
425
- autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState; emulatorFlaw?: string } };
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
- (value.cartridge === undefined || value.cartridge === true);
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.timerA, 0, 0xffff) &&
1239
- isInteger(chip.timerB, 0, 0xffff) &&
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 JSON lines to a file
3
- // of its own in that directory; unset, every call does nothing. The trace is
4
- // for people: nothing of it reaches the agent, and no result depends on it.
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 is off. */
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
- /** Appends one event. A write that fails turns the trace off; nothing throws. */
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 line: Record<string, unknown> = { t: isoCet(), kind: this.kind, event: name };
119
+ const summary: Record<string, unknown> = {};
104
120
  for (const [key, value] of Object.entries(fields)) {
105
- if (!RESERVED.has(key) && value !== undefined) line[key] = summarize(value);
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(line)}\n`);
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.on) return;
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 when `env` (the environment by
140
- * default) has C64RT_TRACE, and off otherwise. A directory that cannot be used
141
- * leaves the trace off and reports that through `warn`. Called once, at the
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 = (options.env ?? process.env)[TRACE_VARIABLE];
146
- if (dir === undefined || dir === "") {
147
- current = Trace.off(kind);
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 = Trace.off(kind);
154
- options.warn?.(`${TRACE_VARIABLE}=${dir} cannot be used (${error instanceof Error ? error.message : String(error)}); the trace is off.`);
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
  }