@henols/c64-re-tools 2.1.0 → 2.1.2

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 (73) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +7 -3
  3. package/skills/c64-assembler/scripts/assembler.ts +29 -11
  4. package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
  5. package/skills/c64-basic/SKILL.md +1 -0
  6. package/skills/c64-disk/SKILL.md +3 -2
  7. package/skills/c64-disk/scripts/disk.ts +26 -12
  8. package/skills/c64-disk/scripts/lib/containers.ts +10 -11
  9. package/skills/c64-docs/SKILL.md +2 -1
  10. package/skills/c64-docs/scripts/docs.ts +1 -1
  11. package/skills/c64-docs/scripts/lib/links.ts +5 -3
  12. package/skills/c64-docs/scripts/lib/reference.ts +2 -2
  13. package/skills/c64-emulator/SKILL.md +9 -2
  14. package/skills/c64-knowledge/SKILL.md +9 -5
  15. package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
  16. package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
  17. package/skills/c64-memory-map/SKILL.md +2 -1
  18. package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
  19. package/skills/c64-provenance/SKILL.md +6 -4
  20. package/skills/c64-provenance/scripts/lib/library.ts +18 -6
  21. package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
  22. package/skills/c64-provenance/scripts/provenance.ts +14 -10
  23. package/skills/c64-source/SKILL.md +3 -0
  24. package/skills/c64-source/scripts/lib/source.ts +9 -31
  25. package/skills/c64-source/scripts/source.ts +14 -10
  26. package/skills/c64-static-analysis/SKILL.md +2 -0
  27. package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
  28. package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
  29. package/skills/c64-testing/SKILL.md +14 -3
  30. package/skills/c64-testing/references/recording-format.md +2 -0
  31. package/skills/c64-testing/references/scenario-format.md +6 -0
  32. package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
  33. package/skills/c64-testing/scripts/lib/diff.ts +3 -3
  34. package/skills/c64-testing/scripts/lib/recording.ts +50 -15
  35. package/skills/c64-testing/scripts/lib/replay.ts +12 -1
  36. package/skills/c64-testing/scripts/lib/runs.ts +84 -4
  37. package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
  38. package/skills/c64-testing/scripts/testing.ts +20 -7
  39. package/skills/c64-unpacker/SKILL.md +17 -10
  40. package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
  41. package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
  42. package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
  43. package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
  44. package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
  45. package/src/host/vice/adapter.ts +4 -1
  46. package/src/host/vice/session.ts +148 -13
  47. package/src/host-client/image.ts +40 -8
  48. package/src/host-client/steps.ts +145 -9
  49. package/src/host-client/tools.ts +1 -1
  50. package/src/host-client/transfer.ts +33 -18
  51. package/src/knowledge/expression.ts +1 -1
  52. package/src/knowledge/history.ts +74 -4
  53. package/src/knowledge/import.ts +79 -7
  54. package/src/knowledge/names.ts +2 -1
  55. package/src/knowledge/program.ts +32 -18
  56. package/src/knowledge/read.ts +46 -0
  57. package/src/knowledge/schema.ts +4 -2
  58. package/src/knowledge/text.ts +2 -0
  59. package/src/knowledge/write.ts +58 -16
  60. package/src/mcp/inputs.ts +6 -3
  61. package/src/mcp/output.ts +5 -0
  62. package/src/mcp/server.ts +20 -4
  63. package/src/mcp/tools/debug.ts +36 -9
  64. package/src/mcp/tools/execution.ts +18 -5
  65. package/src/mcp/tools/machine.ts +3 -1
  66. package/src/mcp/tools/media.ts +14 -2
  67. package/src/mcp/tools/memory.ts +11 -6
  68. package/src/mcp/tools/video.ts +5 -3
  69. package/src/media.ts +42 -0
  70. package/src/opcodes.ts +51 -18
  71. package/src/project.ts +111 -3
  72. package/src/protocol/vice.ts +8 -2
  73. package/src/script.ts +55 -3
@@ -8,6 +8,7 @@ import { type AddressRange, formatC64Address, instructionLength, SPACE_PATTERN,
8
8
  import { isoCet } from "../time.ts";
9
9
  import { CONFLICT_RULES, conflicts, conflictTable, type Conflict, type ConflictRow } from "./conflicts.ts";
10
10
  import { transaction } from "./database.ts";
11
+ import { labelProblem } from "../opcodes.ts";
11
12
  import { checkExpression, evaluate, identifiers, operandText, replaceIdentifiers } from "./expression.ts";
12
13
  import {
13
14
  COMMENT_COLUMNS,
@@ -32,6 +33,7 @@ import {
32
33
  referencesFrom,
33
34
  REGION_COLUMNS,
34
35
  regionsOverlapping,
36
+ coverageRuns,
35
37
  resolveRevision,
36
38
  SUPPRESSION_COLUMNS,
37
39
  suppressionsOverlapping,
@@ -49,6 +51,7 @@ import {
49
51
  toSuppression,
50
52
  toSymbol,
51
53
  type CommentRow,
54
+ type CoverageRun,
52
55
  type MacroRow,
53
56
  type MacroUseRow,
54
57
  type ModuleRow,
@@ -59,7 +62,7 @@ import {
59
62
  type SuppressionRow,
60
63
  type SymbolRow,
61
64
  } from "./read.ts";
62
- import { COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
65
+ import { CODE_REGION_TYPES, COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
63
66
  import { newRevisionId, rowId, sqlName } from "./text.ts";
64
67
 
65
68
  /** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
@@ -136,6 +139,10 @@ export interface ChangeContext {
136
139
  inputHash?: string;
137
140
  toolVersion?: string;
138
141
  evidence?: Evidence[];
142
+ /** A revert: the revision whose knowledge it restores. */
143
+ restores?: string;
144
+ /** A revert of one space: the space whose knowledge it restores. Without it, a revert restores every space. */
145
+ restoresSpace?: Space;
139
146
  }
140
147
 
141
148
  /**
@@ -177,8 +184,8 @@ export class Change {
177
184
  const now = new Date();
178
185
  const id = newRevisionId(currentRevision(this.#db), now);
179
186
  this.#db
180
- .prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description) VALUES (?, ?, ?, ?, ?, ?, ?)")
181
- .run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null);
187
+ .prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description, restored_revision, restored_space) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
188
+ .run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null, this.#context.restores ?? null, this.#context.restoresSpace ?? null);
182
189
  this.revision = id;
183
190
  const evidence = this.#db.prepare("INSERT INTO evidence (revision, kind, ref, sha256, space) VALUES (?, ?, ?, ?, ?)");
184
191
  for (const item of this.#context.evidence ?? []) evidence.run(id, item.kind, item.ref, item.sha256 ?? null, item.space ?? null);
@@ -321,7 +328,7 @@ function checkEvidence(evidence: Evidence[] | undefined): void {
321
328
  }
322
329
 
323
330
  /** Runs semantic writes in one transaction, as one revision. */
324
- export function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
331
+ export function write<T>(db: DatabaseSync, context: WriteContext & Pick<ChangeContext, "restores">, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
325
332
  if (context.origin !== "user" && context.origin !== "llm") {
326
333
  throw new Refusal("invalid-input", "A semantic write comes from the user or the LLM.");
327
334
  }
@@ -365,9 +372,8 @@ export function checkSpace(request: InSpace): void {
365
372
  function checkSymbol(request: SymbolRequest): void {
366
373
  checkSpace(request);
367
374
  checkAddress(request.address);
368
- if (!SYMBOL_NAME.test(request.name)) {
369
- throw new Refusal("invalid-input", "A symbol name is a letter or underscore, then letters, digits or underscores (at most 64), with an optional leading dot.");
370
- }
375
+ const problem = labelProblem(request.name);
376
+ if (problem !== undefined) throw new Refusal("invalid-input", problem);
371
377
  if (request.kind !== undefined) checkOneOf(SYMBOL_KINDS, request.kind, "kind");
372
378
  }
373
379
 
@@ -572,6 +578,32 @@ function checkOperand(request: OperandRequest): void {
572
578
 
573
579
  const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${value.toString(16).padStart(size === 0x100 ? 2 : 4, "0")}`);
574
580
 
581
+ /**
582
+ * Refuses an address that is not the start of an instruction. The code
583
+ * regions of the knowledge give the instructions: they are decoded from the
584
+ * start of the region, as the listing and the source decode them.
585
+ */
586
+ function checkInstructionStart(db: DatabaseSync, space: Space, image: OperandImage, address: number): void {
587
+ const where = formatC64Address(address);
588
+ const code = regionsOverlapping(db, address, address, space).find((region) => CODE_REGION_TYPES.includes(region.type));
589
+ if (code === undefined) {
590
+ throw new Refusal("invalid-input", `${where} is in no code region, so the knowledge does not know an instruction that starts there. Classify the code that holds the instruction first (classify <range> code), then write the operand.`);
591
+ }
592
+ let start = code.start;
593
+ let next = code.start;
594
+ while (next < address) {
595
+ const opcode = image.byteAt(next);
596
+ if (opcode === undefined) {
597
+ throw new Refusal("invalid-input", `The code region ${formatC64Address(code.start)}-${formatC64Address(code.end)} has no program byte at ${formatC64Address(next)}, so its instructions up to ${where} are not known. Correct the region (classify), then write the operand.`);
598
+ }
599
+ start = next;
600
+ next += instructionLength(opcode);
601
+ }
602
+ if (next !== address) {
603
+ throw new Refusal("invalid-input", `${where} is not the start of an instruction. It is an operand byte of the instruction at ${formatC64Address(start)}, decoded from the start of the code region at ${formatC64Address(code.start)}. Give ${formatC64Address(start)}.`);
604
+ }
605
+ }
606
+
575
607
  /**
576
608
  * Refuses an expression whose value is not the operand in the image, and
577
609
  * returns the addresses of its names. Symbols of the knowledge come first, then
@@ -581,6 +613,7 @@ function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage,
581
613
  const where = formatC64Address(request.address);
582
614
  const opcode = image.byteAt(request.address);
583
615
  if (opcode === undefined) throw new Refusal("invalid-input", `${where} is outside the image.`);
616
+ checkInstructionStart(db, space, image, request.address);
584
617
  const length = instructionLength(opcode);
585
618
  if (length === 1) throw new Refusal("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
586
619
  if ((opcode & 0x1f) === 0x10) throw new Refusal("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
@@ -1168,16 +1201,25 @@ const KEYS = {
1168
1201
 
1169
1202
  /**
1170
1203
  * Makes the current knowledge equal to what it was at `revision`, as a new
1171
- * revision. History is never erased; restored rows keep their origin.
1204
+ * revision. With `space`, only the rows of that space change; without it,
1205
+ * every space changes. History is never erased; restored rows keep their origin.
1172
1206
  */
1173
- export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string }): WriteResult<{ restoredRevision: string }> {
1174
- return write(db, context, "revert", (change) => {
1175
- const restored = resolveRevision(db, request.revision);
1176
- const at = "valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)";
1207
+ export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string; space?: Space }): WriteResult<{ restoredRevision: string }> {
1208
+ checkSpace(request);
1209
+ const restored = resolveRevision(db, request.revision);
1210
+ const only = request.space;
1211
+ return write(db, { ...context, restores: restored, ...(only === undefined ? {} : { restoresSpace: only }) }, "revert", (change) => {
1212
+ // The coverage maps that the knowledge has follow the revert, also when no coverage range changes.
1213
+ const maps = (runs: CoverageRun[]) => JSON.stringify(runs.filter((run) => only === undefined || run.space === only).map((run) => [run.name, run.sha256, run.space, run.revision]));
1214
+ if (maps(coverageRuns(db, restored)) !== maps(coverageRuns(db))) change.id();
1215
+ const inSpace = only === undefined ? "" : " AND space = ?";
1216
+ const spaceArgs = only === undefined ? [] : [only];
1217
+ const at = `valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)${inSpace}`;
1218
+ const current = `valid_to_revision IS NULL${inSpace}`;
1177
1219
  for (const [table, spec] of Object.entries(KEYS)) {
1178
1220
  const name = table === "references" ? '"references"' : table;
1179
- const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
1180
- const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE valid_to_revision IS NULL`).all() as Array<Record<string, unknown>>;
1221
+ const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored, ...spaceArgs) as Array<Record<string, unknown>>;
1222
+ const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${current}`).all(...spaceArgs) as Array<Record<string, unknown>>;
1181
1223
  const key = (row: Record<string, unknown>) => (spec.key as (row: unknown) => string)(spec.map(row as never));
1182
1224
  const thenKeys = new Set(then.map(key));
1183
1225
  const nowKeys = new Set(now.map(key));
@@ -1219,8 +1261,8 @@ export function revert(db: DatabaseSync, context: WriteContext, request: { revis
1219
1261
  }
1220
1262
  // The program: a row is the same when all its values are the same.
1221
1263
  for (const [table, columns] of PROGRAM_TABLES) {
1222
- const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
1223
- const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE valid_to_revision IS NULL`).all() as Array<Record<string, unknown>>;
1264
+ const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored, ...spaceArgs) as Array<Record<string, unknown>>;
1265
+ const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${current}`).all(...spaceArgs) as Array<Record<string, unknown>>;
1224
1266
  const key = (row: Record<string, unknown>) => JSON.stringify(columns.split(", ").map((column) => row[column]));
1225
1267
  const thenKeys = new Set(then.map(key));
1226
1268
  const nowKeys = new Set(now.map(key));
package/src/mcp/inputs.ts CHANGED
@@ -33,12 +33,15 @@ export const AddressInput = z
33
33
  .transform((value) => c64AddressOf(value)!)
34
34
  .describe("C64 address: $ and 1 to 4 hex digits, $0000 to $ffff");
35
35
 
36
+ /** The space of a memory target or condition: the emulator tests only C64 memory. */
37
+ const MemorySpaceInput = SpaceInput.describe("c64 only: the emulator tests only C64 memory. For the drive, use a watchpoint or an address target in space drive8");
38
+
36
39
  const rasterLine = z.number().int().min(0).max(MAX_RASTER_LINE).describe("raster line: PAL 0-311, NTSC 0-262");
37
40
 
38
41
  const condition = z
39
42
  .discriminatedUnion("kind", [
40
43
  z.object({ kind: z.literal("register"), register: z.enum(CONDITION_REGISTERS), operator: z.enum(COMPARISONS), value: Byte }).strict(),
41
- z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: SpaceInput, view: MemoryViewInput }).strict(),
44
+ z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: MemorySpaceInput, view: MemoryViewInput }).strict(),
42
45
  z.object({ kind: z.literal("raster"), line: rasterLine, cycle: z.number().int().min(0).max(64).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. True from this cycle on") }).strict(),
43
46
  ])
44
47
  .describe("register: a CPU register compared to a byte. memory: a byte in memory compared to a byte. raster: the raster position");
@@ -50,7 +53,7 @@ export const TOOL_INPUTS = {
50
53
 
51
54
  runTarget: z.discriminatedUnion("kind", [
52
55
  z.object({ kind: z.literal("address"), address: AddressInput, space: SpaceInput, condition: condition.optional().describe("stop at the address only when this is true") }).strict(),
53
- z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: SpaceInput, view: MemoryViewInput }).strict(),
56
+ z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: MemorySpaceInput, view: MemoryViewInput }).strict(),
54
57
  z.object({ kind: z.literal("raster"), line: rasterLine, cycle: z.number().int().min(0).max(64).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. Default 0") }).strict(),
55
58
  ]),
56
59
 
@@ -82,7 +85,7 @@ export const TOOL_INPUTS = {
82
85
 
83
86
  joystick: z
84
87
  .object({
85
- port: z.union([z.literal(1), z.literal(2)]).describe("control port 1 or 2"),
88
+ port: z.union([z.literal(1), z.literal(2)], { error: "port must be the number 1 or 2" }).describe("control port 1 or 2"),
86
89
  direction: z.enum(JOYSTICK_DIRECTIONS),
87
90
  fire: z.boolean().default(false),
88
91
  })
package/src/mcp/output.ts CHANGED
@@ -22,6 +22,11 @@ export const WrittenOutput = {
22
22
  sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
23
23
  };
24
24
 
25
+ /** Lines as file text: each line ends with a newline, and no lines give an empty file. */
26
+ export function linesText(lines: readonly string[]): string {
27
+ return lines.map((line) => `${line}\n`).join("");
28
+ }
29
+
25
30
  /** Writes `data` to the file `path` of the project that `project` names (or the project lookup finds), and gives what the result shows in its place. */
26
31
  export function writeOut(path: string, data: Uint8Array | string, project: string | undefined): Written {
27
32
  return writeReported(path, data, { what: "out", root: projectFolder(project) });
package/src/mcp/server.ts CHANGED
@@ -91,6 +91,17 @@ export function requireFields(action: string, given: Record<string, unknown>, al
91
91
  }
92
92
  }
93
93
 
94
+ /**
95
+ * Checks the fields that go with out: project only with out, and the
96
+ * fields of `notWithOut` only without it.
97
+ */
98
+ export function requireOut(given: { out?: unknown; project?: unknown } & Record<string, unknown>, notWithOut: readonly string[] = []): void {
99
+ if (given.out === undefined && given.project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
100
+ for (const field of notWithOut) {
101
+ if (given.out !== undefined && given[field] !== undefined) throw new WireFailure("invalid-input", `${field} is not used with out.`);
102
+ }
103
+ }
104
+
94
105
  /** A tool result that carries content blocks (for example an image) beside its structured result. */
95
106
  export class ToolOutput<T> {
96
107
  readonly structured: T;
@@ -163,12 +174,17 @@ function errorResult(error: WireError): CallToolResult {
163
174
  return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: { code: error.code, message: shownMessage(error) } }) }] };
164
175
  }
165
176
 
166
- /** "address: Invalid string: must match ..." style messages, one sentence per problem. */
167
- function describeIssues(error: z.ZodError): string {
177
+ /**
178
+ * "address: Invalid string: must match ..." style messages, one sentence per problem.
179
+ * An unknown field of the tool input also gets the fields that the tool takes.
180
+ */
181
+ function describeIssues(error: z.ZodError, input?: z.ZodObject): string {
168
182
  return error.issues
169
183
  .map((issue) => {
170
184
  const where = issue.path.length === 0 ? "input" : issue.path.join(".");
171
- return `${where}: ${issue.message}`;
185
+ const names = input === undefined ? [] : Object.keys(input.shape);
186
+ const fields = issue.code !== "unrecognized_keys" || issue.path.length !== 0 || input === undefined ? "" : names.length === 0 ? ". The tool takes no fields" : `. The fields are ${names.join(", ")}`;
187
+ return `${where}: ${issue.message}${fields}`;
172
188
  })
173
189
  .join(". ");
174
190
  }
@@ -211,7 +227,7 @@ async function runCall(
211
227
  const tool = tools.get(name);
212
228
  if (tool === undefined) return errorResult({ code: "invalid-input", message: `There is no tool named ${name}.` });
213
229
  const parsed = tool.inputSchema.safeParse(args ?? {});
214
- if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error) });
230
+ if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error, tool.inputSchema) });
215
231
  try {
216
232
  let output: unknown;
217
233
  try {
@@ -7,9 +7,9 @@ import { COVERAGE, executedRamCode, EXECUTE, formatC64Address, READ, WRITE, type
7
7
  import { WireFailure } from "../../protocol/messages.ts";
8
8
  import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, TRACE_ENDS, MAX_MEMMAP_RANGES, MAX_MEMORY_READ, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
9
9
  import { readCoverage } from "../../host-client/coverage.ts";
10
- import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
10
+ import { linesText, OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
11
11
  import { AddressInput, Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
12
- import { AddressOutput, ConditionOutput, defineTool, HexData, requireFields, showCondition, toCondition } from "../server.ts";
12
+ import { AddressOutput, ConditionOutput, defineTool, HexData, requireFields, requireOut, showCondition, toCondition } from "../server.ts";
13
13
  import { spaced } from "./memory.ts";
14
14
 
15
15
  /** No default here: a filled-in default would count as a field the other actions refuse. */
@@ -169,7 +169,7 @@ export const c64CpuHistory = defineTool({
169
169
  const raster = entry.rasterLine === undefined ? "" : ` line ${entry.rasterLine} cycle ${entry.rasterCycle ?? 0}`;
170
170
  return `${formatC64Address(entry.address)} ${spaced(entry.bytes).padEnd(9)} ${entry.text.padEnd(16)} a=${byte(entry.a)} x=${byte(entry.x)} y=${byte(entry.y)} sp=${byte(entry.sp)}${raster}`;
171
171
  });
172
- return { count: entries.length, ...writeOut(out, `${lines.join("\n")}\n`, project) };
172
+ return { count: entries.length, ...writeOut(out, linesText(lines), project) };
173
173
  },
174
174
  });
175
175
 
@@ -184,6 +184,7 @@ export const c64Trace = defineTool({
184
184
  "A point is exec (an instruction at an address from start to end), read or write (an access from start to end). " +
185
185
  `Give 1 to ${MAX_TRACE_POINTS} points. ` +
186
186
  "Each hit gives the instruction (pc, bytes, text) and the registers a, x, y and sp before it. " +
187
+ "point in a hit is the number of its point in points, 0 for the first. " +
187
188
  "It also gives the frame from 0, and the raster line and cycle where the instruction started. " +
188
189
  "A read or write hit also gives the address that the instruction used, and the byte there after the access. " +
189
190
  `The trace ends after frames frames (1 to ${MAX_TRACE_FRAMES}), at maxHits hits (at most ${MAX_TRACE_HITS}), or at a breakpoint, a watchpoint or a JAM. ` +
@@ -233,7 +234,7 @@ export const c64Trace = defineTool({
233
234
  const shown = traced.hits.map(({ address, ...hit }) => ({ ...hit, pc: formatC64Address(hit.pc), ...(address === undefined ? {} : { address: formatC64Address(address) }) }));
234
235
  const summary = { end: traced.end, frames: traced.frames, pc: formatC64Address(traced.pc), count: traced.hits.length, hits: shown.slice(0, TRACE_LISTED) };
235
236
  if (out === undefined) return summary;
236
- return { ...summary, ...writeOut(out, `${shown.map((hit) => JSON.stringify(hit)).join("\n")}\n`, project) };
237
+ return { ...summary, ...writeOut(out, linesText(shown.map((hit) => JSON.stringify(hit))), project) };
237
238
  },
238
239
  });
239
240
 
@@ -338,7 +339,9 @@ export const c64Memmap = defineTool({
338
339
  "A read on a byte right after executed code, or at a return address, can be a dummy read that the CPU does not use. " +
339
340
  "Action summary gives the counts for RAM, ROM and I/O. It also gives the executed code in RAM with its operand bytes, and the RAM that was written and executed. " +
340
341
  `Each list has at most ${MAX_SUMMARY_RANGES} ranges. ` +
341
- "Action clear sets all access records to zero.",
342
+ "Action clear sets all access records to zero. " +
343
+ "The emulator decides RAM, ROM or I/O from one bit of $01. At $A000-$BFFF it gives ROM when bit 0 is set, also when bit 1 is clear and the CPU used RAM. " +
344
+ "Every access at $D000-$DFFF is I/O, also when $01 shows RAM or the character ROM there. emulatorFlaw in the result tells when ROM at $A000-$BFFF can be RAM.",
342
345
  inputSchema: z
343
346
  .object({
344
347
  action: z.enum(["read", "summary", "clear"]),
@@ -361,6 +364,7 @@ export const c64Memmap = defineTool({
361
364
  writtenAndExecuted: z.array(RangeOutput).optional(),
362
365
  writtenAndExecutedTruncated: z.boolean().optional(),
363
366
  cleared: z.boolean().optional(),
367
+ emulatorFlaw: z.string().optional().describe("a known flaw of VICE that can make the ROM and I/O classes wrong in this result"),
364
368
  ...WrittenOutput,
365
369
  }),
366
370
  readOnly: false,
@@ -390,13 +394,15 @@ export const c64Memmap = defineTool({
390
394
  ...(code.length > MAX_SUMMARY_RANGES ? { codeTruncated: true } : {}),
391
395
  writtenAndExecuted: showRanges(written),
392
396
  ...(written.length > MAX_SUMMARY_RANGES ? { writtenAndExecutedTruncated: true } : {}),
397
+ ...basicRomFlaw(romInBasicArea(map, start)),
393
398
  };
394
399
  }
400
+ requireOut(fields, ["maxRanges"]);
401
+ requireFields("read", fields, ["start", "end", "maxRanges", "out", "project"]);
395
402
  if (fields.out !== undefined) {
396
- requireFields("read", fields, ["start", "end", "out", "project"]);
397
- return writeOut(fields.out, await readCoverage(session, start, end), fields.project);
403
+ const map = await readCoverage(session, start, end);
404
+ return { ...writeOut(fields.out, map, fields.project), ...basicRomFlaw(romInBasicArea(map, start)) };
398
405
  }
399
- requireFields("read", fields, ["start", "end", "maxRanges"]);
400
406
  const result = await session.memmap({ action: "read", start, end, maxRanges: fields.maxRanges ?? 256 });
401
407
  const ranges = result.ranges.map((range) => ({
402
408
  start: formatC64Address(range.start),
@@ -406,8 +412,29 @@ export const c64Memmap = defineTool({
406
412
  write: (range.flags & WRITE) !== 0,
407
413
  in: sources(range.flags),
408
414
  }));
409
- return result.next === undefined ? { ranges } : { ranges, truncated: true, next: formatC64Address(result.next) };
415
+ const flaw = basicRomFlaw(result.ranges.some((range) => range.start <= 0xbfff && range.end >= 0xa000 && (range.flags & ROM_ACCESS) !== 0));
416
+ return result.next === undefined ? { ranges, ...flaw } : { ranges, truncated: true, next: formatC64Address(result.next), ...flaw };
410
417
  },
411
418
  });
412
419
 
420
+ const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
421
+
422
+ /** True when the map from `start` has a ROM access at $A000-$BFFF. */
423
+ function romInBasicArea(map: Uint8Array, start: number): boolean {
424
+ for (let address = Math.max(start, 0xa000); address <= Math.min(start + map.length - 1, 0xbfff); address++) {
425
+ if ((map[address - start]! & ROM_ACCESS) !== 0) return true;
426
+ }
427
+ return false;
428
+ }
429
+
430
+ /** VICE tests only bit 0 of $01 for $A000-$BFFF; the C64 shows BASIC ROM there only when bits 0 and 1 are set. */
431
+ function basicRomFlaw(present: boolean): { emulatorFlaw?: string } {
432
+ if (!present) return {};
433
+ return {
434
+ emulatorFlaw:
435
+ "The emulator gives ROM for an access at $A000-$BFFF when bit 0 of $01 is set, also when bit 1 is clear and the CPU used the RAM there. " +
436
+ "Read $01 of the program at that time before you take rom at $A000-$BFFF as the BASIC ROM.",
437
+ };
438
+ }
439
+
413
440
  export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Trace, c64Backtrace, c64Profile, c64Memmap, c64Timing];
@@ -3,9 +3,10 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { formatC64Address } from "../../c64.ts";
6
+ import { isRefusal } from "../../errors.ts";
6
7
  import { EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_HISTORY, MAX_TIMEOUT_FRAMES, POINT_STOP_REASONS, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
7
8
  import { Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
8
- import { AddressOutput, defineTool, toCondition, ToolOutput } from "../server.ts";
9
+ import { AddressOutput, defineTool, toCondition, ToolOutput, type ViceSessionApi } from "../server.ts";
9
10
  import { c64Observe } from "./video.ts";
10
11
 
11
12
  export const c64Execution = defineTool({
@@ -19,6 +20,8 @@ export const c64Execution = defineTool({
19
20
  "advance-frames runs exactly count video frames (count is necessary). It stops at the same raster position, count frames later. " +
20
21
  "A breakpoint, a watchpoint or a JAM opcode can stop it before that. advancedFrames tells how many frames ran. " +
21
22
  "stopReason tells why it stopped: frames (all frames ran), breakpoint, watchpoint or jam. pointId gives the id of the breakpoint or watchpoint. " +
23
+ "space tells which CPU stopped: c64, or drive8 for a breakpoint or watchpoint in the disk drive. pc is the address in that space. " +
24
+ "A CPU that a JAM stopped runs again only after c64_reset, a snapshot restore or c64_autostart. " +
22
25
  "To wait for a breakpoint or watchpoint after resume, use advance-frames with a large count. " +
23
26
  "step, next, until-return and advance-frames first stop a running CPU. Use count only with step, next and advance-frames. " +
24
27
  "step, next and until-return work only in space c64. For the drive, use c64_run_until with an address in space drive8.",
@@ -42,6 +45,7 @@ export const c64Execution = defineTool({
42
45
  advancedFrames: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("frames advance-frames ran"),
43
46
  stopReason: z.enum(["frames", ...POINT_STOP_REASONS]).optional().describe("advance-frames: why it stopped"),
44
47
  pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped advance-frames"),
48
+ space: z.enum(SPACES).optional().describe("advance-frames: the CPU that stopped, c64 or drive8. pc is the address in that space"),
45
49
  }),
46
50
  readOnly: false,
47
51
  async run(input, session) {
@@ -54,6 +58,7 @@ export const c64Execution = defineTool({
54
58
  ...(result.advancedFrames === undefined ? {} : { advancedFrames: result.advancedFrames }),
55
59
  ...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),
56
60
  ...(result.pointId === undefined ? {} : { pointId: result.pointId }),
61
+ ...(result.space === undefined ? {} : { space: result.space }),
57
62
  };
58
63
  },
59
64
  });
@@ -93,6 +98,7 @@ export const c64RunUntil = defineTool({
93
98
  history: z
94
99
  .array(z.object({ address: AddressOutput, bytes: z.string(), text: z.string(), a: Byte, x: Byte, y: Byte, sp: Byte, rasterLine: z.number().int().min(0).optional(), rasterCycle: z.number().int().min(0).optional() }))
95
100
  .optional(),
101
+ historyMissing: z.string().optional().describe("why the result has no history, for example an old history after a JAM"),
96
102
  }),
97
103
  readOnly: false,
98
104
  async run(input, session) {
@@ -108,10 +114,7 @@ export const c64RunUntil = defineTool({
108
114
  }
109
115
  const result = await session.runUntil({ target, timeoutFrames: input.timeoutFrames });
110
116
  const answer = { ...result, pc: formatC64Address(result.pc) };
111
- const history =
112
- input.history === undefined
113
- ? {}
114
- : { history: (await session.cpuHistory({ limit: input.history, space: result.space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
117
+ const history = input.history === undefined ? {} : await runHistory(session, input.history, result.space);
115
118
  if (input.observe === undefined) return { ...answer, ...history };
116
119
  // The observation at the stop: an image of the screen comes with the result as c64_observe gives it.
117
120
  const observed = await c64Observe.run(input.observe, session);
@@ -120,4 +123,14 @@ export const c64RunUntil = defineTool({
120
123
  },
121
124
  });
122
125
 
126
+ /** The history at the stop; the run result stays when the session refuses the history. */
127
+ async function runHistory(session: ViceSessionApi, limit: number, space: "c64" | "drive8") {
128
+ try {
129
+ return { history: (await session.cpuHistory({ limit, space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
130
+ } catch (error) {
131
+ if (isRefusal(error) && error.code === "operation-failed") return { historyMissing: error.message };
132
+ throw error;
133
+ }
134
+ }
135
+
123
136
  export const executionTools = [c64Execution, c64RunUntil];
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  import { formatC64Address } from "../../c64.ts";
6
6
  import { PACKAGE } from "../../package.ts";
7
7
  import { VIDEO_STANDARDS } from "../../protocol/messages.ts";
8
- import { POINT_STOP_REASONS, RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
8
+ import { POINT_STOP_REASONS, RESET_MODES, RUN_STATES, SPACES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
9
9
  import { AddressOutput, defineTool } from "../server.ts";
10
10
 
11
11
  export const c64Status = defineTool({
@@ -27,6 +27,7 @@ export const c64Status = defineTool({
27
27
  pc: AddressOutput.optional().describe("program counter. Only when the CPU is stopped"),
28
28
  stopReason: z.enum(POINT_STOP_REASONS).optional().describe("what stopped the CPU. Only when a breakpoint, a watchpoint or a JAM opcode stopped it"),
29
29
  pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
30
+ space: z.enum(SPACES).optional().describe("with pc: c64, or drive8 when a breakpoint or watchpoint in the disk drive stopped the CPU. pc is the address in that space"),
30
31
  cartridge: z.boolean().describe("true while a cartridge from c64_autostart is attached"),
31
32
  version: z.string().describe("the c64-re-tools version of this MCP server"),
32
33
  }),
@@ -43,6 +44,7 @@ export const c64Status = defineTool({
43
44
  ...(status.pc === undefined ? {} : { pc: formatC64Address(status.pc) }),
44
45
  ...(status.stopReason === undefined ? {} : { stopReason: status.stopReason }),
45
46
  ...(status.pointId === undefined ? {} : { pointId: status.pointId }),
47
+ ...(status.space === undefined ? {} : { space: status.space }),
46
48
  };
47
49
  },
48
50
  });
@@ -2,14 +2,17 @@
2
2
  // c64_snapshot. Paths are relative to the project folder; the host-client
3
3
  // reads the file and the Host Runtime receives only its bytes.
4
4
 
5
+ import { extname } from "node:path";
6
+
5
7
  import { z } from "zod";
6
8
 
7
9
  import { formatC64Address } from "../../c64.ts";
8
10
  import { projectFolder, readProjectFile } from "../../host-client/transfer.ts";
11
+ import { WireFailure } from "../../protocol/messages.ts";
9
12
  import { AUTOSTART_TYPES, DISK_TYPES, MAX_SNAPSHOTS, RUN_STATES } from "../../protocol/vice.ts";
10
13
  import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
11
14
  import { AddressInput } from "../inputs.ts";
12
- import { AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
15
+ import { AddressOutput, defineTool, requireFields, requireOut, TransientName } from "../server.ts";
13
16
 
14
17
  const ProjectPath = z.string().min(1).describe("path relative to the project folder, for example build/game.prg");
15
18
 
@@ -63,6 +66,10 @@ export const c64ProgramLoad = defineTool({
63
66
  outputSchema: z.object({ state: z.enum(RUN_STATES), loadAddress: AddressOutput, size: z.number().int().min(1).max(0x10000) }),
64
67
  readOnly: false,
65
68
  async run(input, session) {
69
+ const type = extname(input.path).slice(1).toLowerCase();
70
+ if (type !== "prg" && (AUTOSTART_TYPES as readonly string[]).includes(type)) {
71
+ throw new WireFailure("invalid-input", `${input.path} is a .${type} image, not a PRG. Use c64_autostart, or c64_disk_attach for a disk image.`);
72
+ }
66
73
  const root = projectFolder(input.project);
67
74
  const result = await session.programLoad(input.address === undefined ? { path: input.path, root } : { path: input.path, address: input.address, root });
68
75
  return { state: result.state, loadAddress: formatC64Address(result.loadAddress), size: result.size };
@@ -118,12 +125,17 @@ export const c64Snapshot = defineTool({
118
125
  requireFields(action, fields, []);
119
126
  return session.snapshot({ action });
120
127
  }
128
+ if (action === "restore") {
129
+ if (fields.name !== undefined && fields.file !== undefined) throw new WireFailure("invalid-input", "Action restore takes name or file, not both.");
130
+ if (fields.name === undefined && fields.file === undefined) throw new WireFailure("invalid-input", "Action restore needs name (a snapshot of this session) or file (a .vsf in the project).");
131
+ }
121
132
  if (action === "restore" && fields.file !== undefined) {
122
133
  requireFields(action, fields, ["file", "project"], ["file"]);
123
134
  return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project) }).bytes);
124
135
  }
125
136
  if (action === "save") {
126
- requireFields(action, fields, fields.out === undefined ? ["name"] : ["name", "out", "project"], ["name"]);
137
+ requireOut(fields);
138
+ requireFields(action, fields, ["name", "out", "project"], ["name"]);
127
139
  const saved = await session.snapshot({ action, name: fields.name! });
128
140
  return fields.out === undefined ? saved : { ...saved, ...writeOut(fields.out, await session.snapshotExport(fields.name!), fields.project) };
129
141
  }
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
  import { formatC64Address } from "../../c64.ts";
7
7
  import { WireFailure } from "../../protocol/messages.ts";
8
8
  import { MAX_COMPARE_DIFFERENCES, MAX_COMPARE_SIZE, MAX_DISASSEMBLE, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_SEARCH_PATTERN, MAX_SEARCH_RESULTS, type RegisterValues } from "../../protocol/vice.ts";
9
- import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
9
+ import { linesText, OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
10
10
  import { AddressInput, Byte, MemoryViewInput, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
11
11
  import { AddressOutput, defineTool, HexData, requireFields } from "../server.ts";
12
12
 
@@ -123,7 +123,7 @@ export const c64MemorySearch = defineTool({
123
123
  title: "Search C64 memory",
124
124
  description:
125
125
  `Find a byte pattern of 1 to ${MAX_SEARCH_PATTERN} bytes in memory from start to end. ?? matches any byte. ` +
126
- `The result gives the start address of each match, at most maxResults (1 to ${MAX_SEARCH_RESULTS}, default 100). ` +
126
+ `The result gives the start address of each match, at most maxResults (1 to ${MAX_SEARCH_RESULTS}, default 100). truncated: true tells that more matches can follow. ` +
127
127
  "This does not change the run state of the machine.",
128
128
  inputSchema: z
129
129
  .object({
@@ -135,15 +135,20 @@ export const c64MemorySearch = defineTool({
135
135
  maxResults: z.number().int().min(1).max(MAX_SEARCH_RESULTS).default(100),
136
136
  })
137
137
  .strict(),
138
- outputSchema: z.object({ matches: z.array(AddressOutput) }),
138
+ outputSchema: z.object({ matches: z.array(AddressOutput), truncated: z.boolean().optional().describe("true when the result has maxResults matches: more can follow. Search again from the address after the last match") }),
139
139
  readOnly: true,
140
140
  async run(input, session) {
141
141
  if (input.pattern.length > MAX_SEARCH_PATTERN) {
142
142
  throw new WireFailure("invalid-input", `pattern has ${input.pattern.length} bytes. The limit is ${MAX_SEARCH_PATTERN}.`);
143
143
  }
144
144
  if (input.pattern.every((token) => token === null)) throw new WireFailure("invalid-input", "pattern needs at least one byte that is not ??.");
145
- const result = await session.memorySearch({ ...input, end: input.end ?? 0xffff });
146
- return { matches: result.matches.map(formatC64Address) };
145
+ const end = input.end ?? 0xffff;
146
+ if (end < input.start) throw new WireFailure("invalid-input", "end is lower than start.");
147
+ // VICE refuses a range shorter than the pattern. Such a range has no match.
148
+ if (end - input.start + 1 < input.pattern.length) return { matches: [] };
149
+ const result = await session.memorySearch({ ...input, end });
150
+ const matches = result.matches.map(formatC64Address);
151
+ return matches.length >= input.maxResults ? { matches, truncated: true } : { matches };
147
152
  },
148
153
  });
149
154
 
@@ -220,7 +225,7 @@ export const c64Disassemble = defineTool({
220
225
  address = instruction.address + instruction.bytes.length / 2;
221
226
  }
222
227
  }
223
- return { count: lines.length, ...writeOut(out, `${lines.join("\n")}\n`, project) };
228
+ return { count: lines.length, ...writeOut(out, linesText(lines), project) };
224
229
  },
225
230
  });
226
231
 
@@ -8,7 +8,7 @@ import { WireFailure } from "../../protocol/messages.ts";
8
8
  import { CIA_SELECTIONS, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_OBSERVE_BYTES, MAX_OBSERVE_RANGES, SPACES, VICII_MODES, type ObserveParams } from "../../protocol/vice.ts";
9
9
  import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
10
10
  import { AddressInput, Byte, MemoryViewInput, SpaceInput } from "../inputs.ts";
11
- import { AddressOutput, defineTool, HexData, requireFields, ToolOutput, TransientName } from "../server.ts";
11
+ import { AddressOutput, defineTool, HexData, requireFields, requireOut, ToolOutput, TransientName } from "../server.ts";
12
12
 
13
13
  const Color = z.number().int().min(0).max(15);
14
14
  const NO_CHANGE = "This does not change the run state of the machine.";
@@ -170,14 +170,16 @@ export const c64Screen = defineTool({
170
170
  async run(input, session) {
171
171
  const { action, ...fields } = input;
172
172
  if (action === "capture") {
173
- requireFields(action, fields, fields.out === undefined ? ["baseline"] : ["baseline", "out", "project"]);
173
+ requireOut(fields);
174
+ requireFields(action, fields, ["baseline", "out", "project"]);
174
175
  const shot = await session.screenCapture(fields.baseline);
175
176
  const result = { width: shot.width, height: shot.height, ...(shot.baseline === undefined ? {} : { baseline: shot.baseline }) };
176
177
  if (fields.out !== undefined) return { ...result, ...writeOut(fields.out, Buffer.from(shot.png, "base64"), fields.project) };
177
178
  return new ToolOutput(result, [{ type: "image", data: shot.png, mimeType: "image/png" }]);
178
179
  }
179
180
  if (action === "compare") {
180
- requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff", ...(fields.out === undefined ? [] : ["out", "project"])], ["baseline"]);
181
+ requireOut(fields);
182
+ requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff", "out", "project"], ["baseline"]);
181
183
  const { diffPng, ...result } = await session.screenCompare({
182
184
  baseline: fields.baseline!,
183
185
  maxMismatchRatio: fields.maxMismatchRatio ?? 0,
package/src/media.ts ADDED
@@ -0,0 +1,42 @@
1
+ // Media file signatures and sizes. The host checks a file with them before
2
+ // VICE gets it: VICE takes a file that is not of its type in another way, and
3
+ // a CRT or T64 that it cannot read loads as a PRG with a successful autostart.
4
+
5
+ import { Refusal } from "./errors.ts";
6
+
7
+ /** The signature at the start of each media type that has one. */
8
+ const SIGNATURES: Readonly<Record<string, readonly string[]>> = {
9
+ crt: ["C64 CARTRIDGE "],
10
+ t64: ["C64"],
11
+ tap: ["C64-TAPE-RAW"],
12
+ g64: ["GCR-1541"],
13
+ p00: ["C64File"],
14
+ };
15
+
16
+ /** True when `bytes` start with a signature of `type`, or `type` has none. */
17
+ export function hasSignature(type: string, bytes: Uint8Array): boolean {
18
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
19
+ const signatures = SIGNATURES[type];
20
+ return signatures === undefined || signatures.some((signature) => buffer.toString("latin1", 0, signature.length) === signature);
21
+ }
22
+
23
+ /** The file sizes that VICE reads for each disk image type without a signature. */
24
+ const SIZES: Readonly<Record<string, readonly number[]>> = {
25
+ d64: [174848, 175531, 196608, 197376, 205312, 206114],
26
+ d71: [349696, 351062],
27
+ d81: [819200, 822400],
28
+ };
29
+
30
+ /** The x64 header: 64 bytes that start with these four, then a d64 image. */
31
+ const X64_MAGIC = Buffer.from([0x43, 0x15, 0x41, 0x64]);
32
+
33
+ /** Refuses a file whose bytes are not of `type` (its extension), with media-error. */
34
+ export function checkMedia(type: string, bytes: Buffer): void {
35
+ const refuse = (why: string) => {
36
+ throw new Refusal("media-error", `The file is not a valid .${type} file: ${why}. Give a file of that type.`);
37
+ };
38
+ if (!hasSignature(type, bytes)) refuse(`it does not start with ${SIGNATURES[type]!.map((signature) => `"${signature.trimEnd()}"`).join(" or ")}`);
39
+ const sizes = SIZES[type];
40
+ if (sizes !== undefined && !sizes.includes(bytes.length)) refuse(`it has ${bytes.length} bytes, and a .${type} image has ${sizes.join(", ")}`);
41
+ if (type === "x64" && (!bytes.subarray(0, 4).equals(X64_MAGIC) || !SIZES.d64!.includes(bytes.length - 64))) refuse("it has no x64 header before a d64 image");
42
+ }