@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,129 @@
1
+ // Media tools: c64_autostart, c64_program_load, c64_disk_attach and
2
+ // c64_snapshot. Paths are project-relative; the host-client reads the file and
3
+ // the Host Runtime receives only its bytes.
4
+
5
+ import { z } from "zod";
6
+
7
+ import { formatC64Address } from "../../c64.ts";
8
+ import { readProjectFile } from "../../host-client/transfer.ts";
9
+ import { AUTOSTART_TYPES, DISK_TYPES, MAX_SNAPSHOTS, RUN_STATES } from "../../protocol/vice.ts";
10
+ import { OutInput, writeOut, WrittenOutput } from "../output.ts";
11
+ import { AddressInput, AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
12
+
13
+ const ProjectPath = z.string().min(1).describe("path relative to the project directory, for example build/game.prg");
14
+
15
+ const list = (types: readonly string[]) => types.map((type) => `.${type}`).join(", ");
16
+
17
+ export const c64Autostart = defineTool({
18
+ name: "c64_autostart",
19
+ title: "Autostart a program or image",
20
+ description:
21
+ `Start a program, or a disk, tape or cartridge image, from the project (${list(AUTOSTART_TYPES)}). ` +
22
+ "The C64 resets and loads the file. With run true (the default), it also runs the file. A disk image stays in drive 8. " +
23
+ "A cartridge stays in the C64 through every reset, as on a real C64. An autostart of a file that is not a cartridge detaches it first. " +
24
+ "Both joysticks are centred first: joystick port 1 uses the keyboard lines, and a held direction changes the RUN that the autostart types. " +
25
+ "Without index, a disk image loads like LOAD\"*\",8,1. index selects one directory entry by its number in the c64-disk directory list (0 is the first entry). " +
26
+ "The machine runs when this tool returns, and the load continues.",
27
+ inputSchema: z
28
+ .object({
29
+ path: ProjectPath,
30
+ index: z.number().int().min(0).max(0xfffe).optional().describe("directory entry of a disk image, 0 is the first entry. Without it, the first file loads as with LOAD\"*\""),
31
+ run: z.boolean().default(true).describe("true runs the program after it loads"),
32
+ })
33
+ .strict(),
34
+ outputSchema: z.object({
35
+ state: z.enum(RUN_STATES),
36
+ emulatorFlaw: z.string().optional().describe("for a tape image: a known flaw of this VICE version that stops a tape load"),
37
+ joysticksCentred: z.array(z.union([z.literal(1), z.literal(2)])).optional().describe("the joystick ports that held a direction or fire and were centred before the reset"),
38
+ }),
39
+ readOnly: false,
40
+ async run(input, session) {
41
+ // VICE numbers the directory entries from 1, and its 0 loads "*".
42
+ return session.autostart({ path: input.path, index: input.index === undefined ? 0 : input.index + 1, run: input.run });
43
+ },
44
+ });
45
+
46
+ export const c64ProgramLoad = defineTool({
47
+ name: "c64_program_load",
48
+ title: "Load a PRG into memory",
49
+ description:
50
+ "Load a PRG file from the project into memory, with no reset and no start. The CPU stops and stays stopped. " +
51
+ "The bytes go where the CPU writes them. Without address, the program goes to the load address in its first two bytes. " +
52
+ "With address, it goes there instead.",
53
+ inputSchema: z
54
+ .object({
55
+ path: ProjectPath,
56
+ address: AddressInput.optional().describe("load here instead of at the file's own load address"),
57
+ })
58
+ .strict(),
59
+ outputSchema: z.object({ state: z.enum(RUN_STATES), loadAddress: AddressOutput, size: z.number().int().min(1).max(0x10000) }),
60
+ readOnly: false,
61
+ async run(input, session) {
62
+ const result = await session.programLoad(input.address === undefined ? { path: input.path } : { path: input.path, address: input.address });
63
+ return { state: result.state, loadAddress: formatC64Address(result.loadAddress), size: result.size };
64
+ },
65
+ });
66
+
67
+ export const c64DiskAttach = defineTool({
68
+ name: "c64_disk_attach",
69
+ title: "Attach a disk image",
70
+ description:
71
+ `Put a disk image from the project into drive 8 (${list(DISK_TYPES)}). ` +
72
+ "Nothing loads or runs. To load from the disk, type LOAD on the C64 or use c64_autostart. " +
73
+ "This does not change the run state of the machine.",
74
+ inputSchema: z.object({ path: ProjectPath }).strict(),
75
+ outputSchema: z.object({ attached: z.boolean() }),
76
+ readOnly: false,
77
+ async run(input, session) {
78
+ return session.diskAttach(input);
79
+ },
80
+ });
81
+
82
+ export const c64Snapshot = defineTool({
83
+ name: "c64_snapshot",
84
+ title: "Machine snapshots",
85
+ description:
86
+ "Save and restore the whole machine state, attached disks included. Action save keeps a snapshot under a name you choose. " +
87
+ "With out, save also writes it to that project file as a snapshot file (.vsf), so a later session can use it. " +
88
+ "Action restore puts the machine back in that state and stops the CPU. Give name for a snapshot of this session, or file for a .vsf in the project. " +
89
+ "A file restores only in the emulator version and with the machine settings that saved it. The c64_timing stopwatch keeps its reading. " +
90
+ `Action list gives the names, and action discard removes one. A session keeps at most ${MAX_SNAPSHOTS} snapshots until it ends.`,
91
+ inputSchema: z
92
+ .object({
93
+ action: z.enum(["save", "restore", "list", "discard"]),
94
+ name: TransientName.optional().describe("save and discard, and restore without file: 1 to 64 letters, digits, dots, underscores or hyphens"),
95
+ out: OutInput.optional().describe("save only: also write the snapshot to this project file, for example snapshots/room0.vsf. An existing file is overwritten"),
96
+ file: ProjectPath.optional().describe("restore only: a .vsf file in the project, in place of name"),
97
+ })
98
+ .strict(),
99
+ outputSchema: z.object({
100
+ saved: z.boolean().optional(),
101
+ name: z.string().optional(),
102
+ restored: z.boolean().optional(),
103
+ state: z.enum(RUN_STATES).optional(),
104
+ snapshots: z.array(z.string()).optional(),
105
+ discarded: z.boolean().optional(),
106
+ ...WrittenOutput,
107
+ }),
108
+ readOnly: false,
109
+ async run(input, session) {
110
+ const { action, ...fields } = input;
111
+ if (action === "list") {
112
+ requireFields(action, fields, []);
113
+ return session.snapshot({ action });
114
+ }
115
+ if (action === "restore" && fields.file !== undefined) {
116
+ requireFields(action, fields, ["file"], ["file"]);
117
+ return session.snapshotRestore(readProjectFile(fields.file).bytes);
118
+ }
119
+ if (action === "save") {
120
+ requireFields(action, fields, ["name", "out"], ["name"]);
121
+ const saved = await session.snapshot({ action, name: fields.name! });
122
+ return fields.out === undefined ? saved : { ...saved, ...writeOut(fields.out, await session.snapshotExport(fields.name!)) };
123
+ }
124
+ requireFields(action, fields, ["name"], ["name"]);
125
+ return session.snapshot({ action, name: fields.name! });
126
+ },
127
+ });
128
+
129
+ export const mediaTools = [c64Autostart, c64ProgramLoad, c64DiskAttach, c64Snapshot];
@@ -0,0 +1,250 @@
1
+ // Memory and CPU tools: c64_memory_read, c64_memory_write, c64_memory_search,
2
+ // c64_memory_compare, c64_registers and c64_disassemble.
3
+
4
+ import { z } from "zod";
5
+
6
+ import { formatC64Address } from "../../c64.ts";
7
+ import { WireFailure } from "../../protocol/messages.ts";
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, writeOut, WrittenOutput } from "../output.ts";
10
+ import { AddressInput, AddressOutput, Byte, defineTool, HexData, HexDataInput, MemoryViewInput, requireFields, SpaceInput } from "../server.ts";
11
+
12
+ /** With out, a read or a disassembly can be larger than one answer of the session: it is read in parts (D39). */
13
+ const MAX_MEMORY_READ_OUT = 0x10000;
14
+ const MAX_DISASSEMBLE_OUT = 32768;
15
+
16
+ export const c64MemoryRead = defineTool({
17
+ name: "c64_memory_read",
18
+ title: "Read C64 memory",
19
+ description:
20
+ `Read 1 to ${MAX_MEMORY_READ} bytes of memory, starting at an address. ` +
21
+ "The range must not go past $ffff. Use space drive8 to read the memory of the 1541 disk drive. " +
22
+ `With out, it reads up to ${MAX_MEMORY_READ_OUT} bytes and writes them as raw bytes to that project file. The result then gives the file, the number of bytes and a SHA-256 in place of data. ` +
23
+ "This does not change the run state of the machine.",
24
+ inputSchema: z
25
+ .object({
26
+ address: AddressInput,
27
+ size: z.number().int().min(1).max(MAX_MEMORY_READ_OUT).describe(`number of bytes, 1 to ${MAX_MEMORY_READ}, or to ${MAX_MEMORY_READ_OUT} with out`),
28
+ space: SpaceInput,
29
+ view: MemoryViewInput,
30
+ out: OutInput.optional(),
31
+ })
32
+ .strict(),
33
+ outputSchema: z.object({ address: AddressOutput, size: z.number().int().min(1).max(MAX_MEMORY_READ_OUT), data: HexData.optional(), ...WrittenOutput }),
34
+ readOnly: false,
35
+ async run(input, session) {
36
+ const { out, ...read } = input;
37
+ if (out === undefined) {
38
+ if (read.size > MAX_MEMORY_READ) throw new WireFailure("invalid-input", `size can be at most ${MAX_MEMORY_READ} without out.`);
39
+ const result = await session.memoryRead(read);
40
+ return { address: formatC64Address(result.address), size: result.data.length / 2, data: result.data };
41
+ }
42
+ if (read.address + read.size > 0x10000) throw new WireFailure("invalid-input", "The range must not go past $ffff.");
43
+ const parts: Buffer[] = [];
44
+ for (let at = read.address; at < read.address + read.size; at += MAX_MEMORY_READ) {
45
+ const part = await session.memoryRead({ ...read, address: at, size: Math.min(MAX_MEMORY_READ, read.address + read.size - at) });
46
+ parts.push(Buffer.from(part.data, "hex"));
47
+ }
48
+ return { address: formatC64Address(read.address), size: read.size, ...writeOut(out, Buffer.concat(parts)) };
49
+ },
50
+ });
51
+
52
+ export const c64MemoryWrite = defineTool({
53
+ name: "c64_memory_write",
54
+ title: "Write C64 memory",
55
+ description:
56
+ `Write 1 to ${MAX_MEMORY_WRITE} bytes to memory, starting at an address. First stop the CPU with c64_execution action pause. ` +
57
+ "The range must not go past $ffff. With view cpu, the write goes where the CPU writes now. " +
58
+ "A write to a ROM address goes to the RAM under it, and a write to I/O goes to the chip. " +
59
+ "With view ram, the write goes to RAM (c64 only).",
60
+ inputSchema: z
61
+ .object({
62
+ address: AddressInput,
63
+ data: HexDataInput.describe("bytes as hex, two digits per byte, no separators, for example a9008d20d0"),
64
+ space: SpaceInput,
65
+ view: MemoryViewInput,
66
+ })
67
+ .strict(),
68
+ outputSchema: z.object({ address: AddressOutput, bytesWritten: z.number().int().min(1).max(MAX_MEMORY_WRITE) }),
69
+ readOnly: false,
70
+ async run(input, session) {
71
+ const result = await session.memoryWrite(input);
72
+ return { address: formatC64Address(result.address), bytesWritten: result.bytesWritten };
73
+ },
74
+ });
75
+
76
+ const Flags = z.object({
77
+ n: z.boolean(),
78
+ v: z.boolean(),
79
+ b: z.boolean(),
80
+ d: z.boolean(),
81
+ i: z.boolean(),
82
+ z: z.boolean(),
83
+ c: z.boolean(),
84
+ });
85
+
86
+ const RegisterValuesInput = z
87
+ .object({
88
+ pc: AddressInput.optional(),
89
+ a: Byte.optional(),
90
+ x: Byte.optional(),
91
+ y: Byte.optional(),
92
+ sp: Byte.optional(),
93
+ flags: Flags.partial().strict().optional().describe("any of n, v, b, d, i, z and c. Flags that you do not name keep their value"),
94
+ })
95
+ .strict();
96
+
97
+ export const c64Registers = defineTool({
98
+ name: "c64_registers",
99
+ title: "C64 CPU registers",
100
+ description:
101
+ "Get or set the CPU registers: pc, a, x, y, sp and the status flags n, v, b, d, i, z and c. " +
102
+ "Action get reads them. This does not change the run state of the machine. " +
103
+ "Action set writes the registers that values names and returns all registers. Stop the CPU before you use set. " +
104
+ "Use space drive8 for the CPU of the 1541 disk drive.",
105
+ inputSchema: z
106
+ .object({
107
+ action: z.enum(["get", "set"]),
108
+ space: SpaceInput,
109
+ values: RegisterValuesInput.optional().describe("set only: the registers to write"),
110
+ })
111
+ .strict(),
112
+ outputSchema: z.object({ pc: AddressOutput, a: Byte, x: Byte, y: Byte, sp: Byte, flags: Flags }),
113
+ readOnly: false,
114
+ async run(input, session) {
115
+ let registers;
116
+ if (input.action === "get") {
117
+ requireFields(input.action, { values: input.values }, []);
118
+ registers = await session.registersGet(input.space);
119
+ } else {
120
+ requireFields(input.action, { values: input.values }, ["values"], ["values"]);
121
+ const values: RegisterValues = {};
122
+ for (const key of ["pc", "a", "x", "y", "sp"] as const) {
123
+ const value = input.values?.[key];
124
+ if (value !== undefined) values[key] = value;
125
+ }
126
+ const flags = Object.fromEntries(Object.entries(input.values?.flags ?? {}).filter(([, value]) => value !== undefined));
127
+ if (Object.keys(flags).length > 0) values.flags = flags;
128
+ if (Object.keys(values).length === 0) {
129
+ throw new WireFailure("invalid-input", "values must name at least one register for action set.");
130
+ }
131
+ registers = await session.registersSet(input.space, values);
132
+ }
133
+ return { ...registers, pc: formatC64Address(registers.pc) };
134
+ },
135
+ });
136
+
137
+ /** "a9 ?? 8d 20 d0": hex bytes and ?? wildcards, separated by spaces. */
138
+ const SearchPattern = z
139
+ .string()
140
+ .regex(/^\s*(?:[0-9a-fA-F]{2}|\?\?)(?:\s+(?:[0-9a-fA-F]{2}|\?\?))*\s*$/, "must be hex bytes and ?? wildcards separated by spaces, for example a9 ?? 8d")
141
+ .transform((value) => value.trim().split(/\s+/).map((token) => (token === "??" ? null : Number.parseInt(token, 16))))
142
+ .describe("hex bytes and ?? (any byte), separated by spaces, for example a9 ?? 8d 20 d0");
143
+
144
+ export const c64MemorySearch = defineTool({
145
+ name: "c64_memory_search",
146
+ title: "Search C64 memory",
147
+ description:
148
+ `Find a byte pattern of 1 to ${MAX_SEARCH_PATTERN} bytes in memory from start to end. ?? matches any byte. ` +
149
+ `The result gives the start address of each match, at most maxResults (1 to ${MAX_SEARCH_RESULTS}, default 100). ` +
150
+ "This does not change the run state of the machine.",
151
+ inputSchema: z
152
+ .object({
153
+ start: AddressInput,
154
+ end: AddressInput.optional().describe("last address to search. Default $ffff"),
155
+ pattern: SearchPattern,
156
+ space: SpaceInput,
157
+ view: MemoryViewInput,
158
+ maxResults: z.number().int().min(1).max(MAX_SEARCH_RESULTS).default(100),
159
+ })
160
+ .strict(),
161
+ outputSchema: z.object({ matches: z.array(AddressOutput) }),
162
+ readOnly: true,
163
+ async run(input, session) {
164
+ if (input.pattern.length > MAX_SEARCH_PATTERN) {
165
+ throw new WireFailure("invalid-input", `pattern has ${input.pattern.length} bytes. The limit is ${MAX_SEARCH_PATTERN}.`);
166
+ }
167
+ if (input.pattern.every((token) => token === null)) throw new WireFailure("invalid-input", "pattern needs at least one byte that is not ??.");
168
+ const result = await session.memorySearch({ ...input, end: input.end ?? 0xffff });
169
+ return { matches: result.matches.map(formatC64Address) };
170
+ },
171
+ });
172
+
173
+ const Location = z
174
+ .object({ address: AddressInput, space: SpaceInput, view: MemoryViewInput })
175
+ .strict();
176
+
177
+ export const c64MemoryCompare = defineTool({
178
+ name: "c64_memory_compare",
179
+ title: "Compare C64 memory",
180
+ description:
181
+ `Compare two memory ranges of size bytes (1 to ${MAX_COMPARE_SIZE}). Each range has its own address, space and view. ` +
182
+ `The result tells if they are equal, how many bytes differ, and the first ${MAX_COMPARE_DIFFERENCES} differences with their offset. ` +
183
+ "This does not change the run state of the machine.",
184
+ inputSchema: z
185
+ .object({ left: Location, right: Location, size: z.number().int().min(1).max(MAX_COMPARE_SIZE) })
186
+ .strict(),
187
+ outputSchema: z.object({
188
+ equal: z.boolean(),
189
+ differentBytes: z.number().int().min(0),
190
+ firstDifferences: z.array(z.object({ offset: z.number().int().min(0), left: Byte, right: Byte })),
191
+ }),
192
+ readOnly: true,
193
+ async run(input, session) {
194
+ return session.memoryCompare(input);
195
+ },
196
+ });
197
+
198
+ export const c64Disassemble = defineTool({
199
+ name: "c64_disassemble",
200
+ title: "Disassemble live C64 memory",
201
+ description:
202
+ `Disassemble count instructions (1 to ${MAX_DISASSEMBLE}) from an address, as the bytes are in the live machine now. ` +
203
+ "Use it to read code that a program changed or unpacked at run time. It includes the undocumented opcodes. " +
204
+ `With out, it disassembles up to ${MAX_DISASSEMBLE_OUT} instructions, or to $ffff, and writes them as a listing to that project file. The result then gives the file in place of the instructions. ` +
205
+ "This does not change the run state of the machine.",
206
+ inputSchema: z
207
+ .object({
208
+ address: AddressInput,
209
+ count: z.number().int().min(1).max(MAX_DISASSEMBLE_OUT).describe(`1 to ${MAX_DISASSEMBLE}, or to ${MAX_DISASSEMBLE_OUT} with out`),
210
+ space: SpaceInput,
211
+ view: MemoryViewInput,
212
+ out: OutInput.optional(),
213
+ })
214
+ .strict(),
215
+ outputSchema: z.object({ instructions: z.array(z.object({ address: AddressOutput, bytes: HexData, text: z.string() })).optional(), count: z.number().int().min(0).optional(), ...WrittenOutput }),
216
+ readOnly: false,
217
+ async run(input, session) {
218
+ const { out, ...request } = input;
219
+ if (out === undefined) {
220
+ if (request.count > MAX_DISASSEMBLE) throw new WireFailure("invalid-input", `count can be at most ${MAX_DISASSEMBLE} without out.`);
221
+ const result = await session.disassemble(request);
222
+ return { instructions: result.instructions.map((instruction) => ({ ...instruction, address: formatC64Address(instruction.address) })) };
223
+ }
224
+ const lines: string[] = [];
225
+ let address = request.address;
226
+ // Ends at count, or where the disassembly would wrap past $ffff.
227
+ let wrapped = false;
228
+ while (!wrapped && lines.length < request.count && address <= 0xffff) {
229
+ const { instructions } = await session.disassemble({ ...request, address, count: Math.min(MAX_DISASSEMBLE, request.count - lines.length) });
230
+ if (instructions.length === 0) break;
231
+ for (const instruction of instructions) {
232
+ if (lines.length === request.count) break;
233
+ if (instruction.address < address) {
234
+ wrapped = true;
235
+ break;
236
+ }
237
+ lines.push(`${formatC64Address(instruction.address)} ${spaced(instruction.bytes).padEnd(9)} ${instruction.text}`);
238
+ address = instruction.address + instruction.bytes.length / 2;
239
+ }
240
+ }
241
+ return { count: lines.length, ...writeOut(out, `${lines.join("\n")}\n`) };
242
+ },
243
+ });
244
+
245
+ /** Hex bytes as pairs with spaces: "a90a" is "a9 0a". */
246
+ export function spaced(hex: string): string {
247
+ return (hex.match(/../g) ?? []).join(" ");
248
+ }
249
+
250
+ export const memoryTools = [c64MemoryRead, c64MemoryWrite, c64MemorySearch, c64MemoryCompare, c64Registers, c64Disassemble];
@@ -0,0 +1,271 @@
1
+ // Video and chip tools: c64_vicii, c64_sprite, c64_cia, c64_sid, c64_screen
2
+ // and c64_observe.
3
+
4
+ import { z } from "zod";
5
+
6
+ import { formatC64Address } from "../../c64.ts";
7
+ import { WireFailure } from "../../protocol/messages.ts";
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
+ import { OutInput, writeOut, WrittenOutput } from "../output.ts";
10
+ import { AddressInput, AddressOutput, Byte, defineTool, HexData, MemoryViewInput, requireFields, SpaceInput, ToolOutput, TransientName } from "../server.ts";
11
+
12
+ const Color = z.number().int().min(0).max(15);
13
+ const NO_CHANGE = "This does not change the run state of the machine.";
14
+
15
+ export const c64Vicii = defineTool({
16
+ name: "c64_vicii",
17
+ title: "VIC-II state",
18
+ description:
19
+ "Get the state of the VIC-II video chip: the raster line, the graphics mode, the screen and graphics addresses, " +
20
+ `the scroll values and the colors. The addresses include the 16 KB video bank that CIA 2 selects. ${NO_CHANGE}`,
21
+ inputSchema: z.object({}).strict(),
22
+ outputSchema: z.object({
23
+ rasterLine: z.number().int().min(0),
24
+ mode: z.enum(VICII_MODES),
25
+ screenAddress: AddressOutput,
26
+ graphicsAddress: AddressOutput.describe("character set in text modes, bitmap in bitmap modes"),
27
+ scrollX: z.number().int().min(0).max(7),
28
+ scrollY: z.number().int().min(0).max(7),
29
+ borderColor: Color,
30
+ backgroundColors: z.array(Color).length(4),
31
+ }),
32
+ readOnly: true,
33
+ async run(_input, session) {
34
+ const state = await session.vicii();
35
+ return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
36
+ },
37
+ });
38
+
39
+ const SpriteOutput = z.object({
40
+ index: z.number().int().min(0).max(7),
41
+ x: z.number().int().min(0),
42
+ y: z.number().int().min(0),
43
+ enabled: z.boolean(),
44
+ color: Color,
45
+ multicolor: z.boolean(),
46
+ expandX: z.boolean(),
47
+ expandY: z.boolean(),
48
+ behindBackground: z.boolean(),
49
+ dataAddress: AddressOutput,
50
+ });
51
+
52
+ export const c64Sprite = defineTool({
53
+ name: "c64_sprite",
54
+ title: "Sprite state",
55
+ description:
56
+ "Get the state of sprites 0 to 7: position, enabled, color, multicolor, expansion, priority and the address of the sprite data. " +
57
+ `sprites selects which sprites to give. Without it, you get all eight. ${NO_CHANGE}`,
58
+ inputSchema: z
59
+ .object({ sprites: z.array(z.number().int().min(0).max(7)).min(1).max(8).optional().describe("sprite numbers 0 to 7, each once") })
60
+ .strict(),
61
+ outputSchema: z.object({ sprites: z.array(SpriteOutput) }),
62
+ readOnly: true,
63
+ async run(input, session) {
64
+ const result = await session.sprites(input.sprites ?? []);
65
+ return { sprites: result.sprites.map((sprite) => ({ ...sprite, dataAddress: formatC64Address(sprite.dataAddress) })) };
66
+ },
67
+ });
68
+
69
+ const CiaOutput = z.object({
70
+ id: z.union([z.literal(1), z.literal(2)]),
71
+ portA: Byte,
72
+ portB: Byte,
73
+ ddrA: Byte,
74
+ ddrB: Byte,
75
+ timerA: z.number().int().min(0).max(0xffff),
76
+ timerB: z.number().int().min(0).max(0xffff),
77
+ latchA: z.number().int().min(0).max(0xffff).describe("the value that timer A loads when it underflows"),
78
+ latchB: z.number().int().min(0).max(0xffff).describe("the value that timer B loads when it underflows"),
79
+ controlA: Byte,
80
+ controlB: Byte,
81
+ interruptStatus: Byte,
82
+ interruptMask: z.number().int().min(0).max(0x1f).describe("the interrupt sources that are on, as bits 0-4 of the interrupt control register"),
83
+ tod: z.object({ hours: z.number().int(), minutes: z.number().int(), seconds: z.number().int(), tenths: z.number().int() }),
84
+ });
85
+
86
+ export const c64Cia = defineTool({
87
+ name: "c64_cia",
88
+ title: "CIA state",
89
+ description:
90
+ "Get the state of CIA 1, CIA 2 or both. The state has the ports, the data direction registers, the current timer values and the timer latches. " +
91
+ `It also has the control registers, the interrupt flags, the interrupt mask and the time of day (hours 0 to 23). ${NO_CHANGE}`,
92
+ inputSchema: z.object({ cia: z.enum(CIA_SELECTIONS).default("both") }).strict(),
93
+ outputSchema: z.object({ chips: z.array(CiaOutput) }),
94
+ readOnly: true,
95
+ async run(input, session) {
96
+ return session.cia(input.cia);
97
+ },
98
+ });
99
+
100
+ export const c64Sid = defineTool({
101
+ name: "c64_sid",
102
+ title: "SID state",
103
+ description:
104
+ "Get the SID sound chip registers as the program last wrote them: the three voices, the filter and the volume. " +
105
+ `This is register state, not an analysis of the sound. ${NO_CHANGE}`,
106
+ inputSchema: z.object({}).strict(),
107
+ outputSchema: z.object({
108
+ voices: z.array(
109
+ z.object({
110
+ index: z.number().int().min(1).max(3),
111
+ frequency: z.number().int(),
112
+ pulseWidth: z.number().int(),
113
+ control: Byte,
114
+ attackDecay: Byte,
115
+ sustainRelease: Byte,
116
+ }),
117
+ ),
118
+ filter: z.object({ cutoff: z.number().int(), resonance: z.number().int(), routing: z.number().int(), mode: z.number().int() }),
119
+ volume: z.number().int().min(0).max(15),
120
+ }),
121
+ readOnly: true,
122
+ async run(_input, session) {
123
+ return session.sid();
124
+ },
125
+ });
126
+
127
+ const Rectangle = z
128
+ .object({ x: z.number().int().min(0), y: z.number().int().min(0), width: z.number().int().min(1), height: z.number().int().min(1) })
129
+ .strict();
130
+
131
+ export const c64Screen = defineTool({
132
+ name: "c64_screen",
133
+ title: "Capture and compare the C64 screen",
134
+ description:
135
+ "Action capture returns the last frame the C64 drew, with its borders, as a PNG image, plus its width and height in pixels. " +
136
+ "With out, capture writes the PNG to that project file, and compare writes its difference image there. The result then has no image. " +
137
+ "With baseline, the session also keeps that frame under the name. " +
138
+ "Action compare compares the current frame with a baseline, pixel by pixel. mask lists rectangles to ignore. " +
139
+ "A mask that covers the whole frame is invalid input. comparedPixels is the number of pixels outside the mask. " +
140
+ "match is true when the part of compared pixels that differ is at most maxMismatchRatio (default 0). bounds holds every different pixel. " +
141
+ "With includeDiff, compare also returns an image with the different pixels in magenta. " +
142
+ "Action list gives the baseline names. Action discard removes a baseline. Baselines stay until the session ends. " +
143
+ "The emulator draws frames while the CPU runs. After the CPU stops, the frame stays the same. " +
144
+ "This does not change the run state of the machine.",
145
+ inputSchema: z
146
+ .object({
147
+ action: z.enum(["capture", "compare", "list", "discard"]),
148
+ baseline: TransientName.optional().describe("capture: keep the frame under this name. compare and discard: the baseline to use"),
149
+ maxMismatchRatio: z.number().min(0).max(1).optional().describe("compare only: 0 to 1, default 0"),
150
+ mask: z.array(Rectangle).max(MAX_MASK_RECTANGLES).optional().describe("compare only: rectangles to ignore, in frame pixels"),
151
+ includeDiff: z.boolean().optional().describe("compare only: also return a difference image"),
152
+ out: OutInput.optional(),
153
+ })
154
+ .strict(),
155
+ outputSchema: z.object({
156
+ width: z.number().int().min(1).optional(),
157
+ height: z.number().int().min(1).optional(),
158
+ baseline: z.string().optional(),
159
+ match: z.boolean().optional(),
160
+ mismatchingPixels: z.number().int().min(0).optional(),
161
+ comparedPixels: z.number().int().min(1).optional(),
162
+ mismatchRatio: z.number().min(0).max(1).optional(),
163
+ bounds: Rectangle.optional(),
164
+ baselines: z.array(z.string()).optional(),
165
+ discarded: z.boolean().optional(),
166
+ ...WrittenOutput,
167
+ }),
168
+ readOnly: false,
169
+ async run(input, session) {
170
+ const { action, ...fields } = input;
171
+ if (action === "capture") {
172
+ requireFields(action, fields, ["baseline", "out"]);
173
+ const shot = await session.screenCapture(fields.baseline);
174
+ const result = { width: shot.width, height: shot.height, ...(shot.baseline === undefined ? {} : { baseline: shot.baseline }) };
175
+ if (fields.out !== undefined) return { ...result, ...writeOut(fields.out, Buffer.from(shot.png, "base64")) };
176
+ return new ToolOutput(result, [{ type: "image", data: shot.png, mimeType: "image/png" }]);
177
+ }
178
+ if (action === "compare") {
179
+ requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff", "out"], ["baseline"]);
180
+ const { diffPng, ...result } = await session.screenCompare({
181
+ baseline: fields.baseline!,
182
+ maxMismatchRatio: fields.maxMismatchRatio ?? 0,
183
+ mask: fields.mask ?? [],
184
+ includeDiff: (fields.includeDiff ?? false) || fields.out !== undefined,
185
+ });
186
+ if (fields.out !== undefined && diffPng !== undefined) return { ...result, ...writeOut(fields.out, Buffer.from(diffPng, "base64")) };
187
+ return diffPng === undefined ? result : new ToolOutput(result, [{ type: "image", data: diffPng, mimeType: "image/png" }]);
188
+ }
189
+ if (action === "list") {
190
+ requireFields(action, fields, []);
191
+ return session.screenBaselines();
192
+ }
193
+ requireFields(action, fields, ["baseline"], ["baseline"]);
194
+ return session.screenDiscard(fields.baseline!);
195
+ },
196
+ });
197
+
198
+ const FlagsOutput = z.object({ n: z.boolean(), v: z.boolean(), b: z.boolean(), d: z.boolean(), i: z.boolean(), z: z.boolean(), c: z.boolean() });
199
+
200
+ export const c64Observe = defineTool({
201
+ name: "c64_observe",
202
+ title: "Observe several things at one moment",
203
+ description:
204
+ "Get several observations from one moment: the CPU stops once, the tool reads all of them, and then the CPU continues if it was running. " +
205
+ "Ask for any of registers (a space), memory ranges, vicii, sprites, cia, sid, screen and timing (the raster position). " +
206
+ `Ask for at least one. Use at most ${MAX_OBSERVE_RANGES} memory ranges with at most ${MAX_OBSERVE_BYTES} bytes together. ` +
207
+ "The result has only the parts you asked for. With screen, an image comes with the result.",
208
+ inputSchema: z
209
+ .object({
210
+ registers: z.enum(SPACES).optional().describe("the space whose CPU registers to read"),
211
+ memory: z
212
+ .array(
213
+ z
214
+ .object({ address: AddressInput, size: z.number().int().min(1).max(MAX_MEMORY_READ), space: SpaceInput, view: MemoryViewInput })
215
+ .strict(),
216
+ )
217
+ .min(1)
218
+ .max(MAX_OBSERVE_RANGES)
219
+ .optional(),
220
+ vicii: z.boolean().optional(),
221
+ sprites: z.array(z.number().int().min(0).max(7)).min(1).max(8).optional().describe("sprite numbers 0 to 7, each once"),
222
+ cia: z.enum(CIA_SELECTIONS).optional(),
223
+ sid: z.boolean().optional(),
224
+ screen: z.boolean().optional(),
225
+ timing: z.boolean().optional(),
226
+ })
227
+ .strict(),
228
+ outputSchema: z.object({
229
+ registers: z.object({ pc: AddressOutput, a: Byte, x: Byte, y: Byte, sp: Byte, flags: FlagsOutput }).optional(),
230
+ memory: z.array(z.object({ address: AddressOutput, data: HexData })).optional(),
231
+ vicii: c64Vicii.outputSchema.optional(),
232
+ sprites: z.array(SpriteOutput).optional(),
233
+ cia: z.array(CiaOutput).optional(),
234
+ sid: c64Sid.outputSchema.optional(),
235
+ screen: z.object({ width: z.number().int(), height: z.number().int() }).optional(),
236
+ timing: z.object({ rasterLine: z.number().int(), rasterCycle: z.number().int() }).optional(),
237
+ }),
238
+ readOnly: true,
239
+ async run(input, session) {
240
+ const params: ObserveParams = {};
241
+ if (input.registers !== undefined) params.registers = input.registers;
242
+ if (input.memory !== undefined) params.memory = input.memory;
243
+ if (input.vicii === true) params.vicii = true;
244
+ if (input.sprites !== undefined) params.sprites = input.sprites;
245
+ if (input.cia !== undefined) params.cia = input.cia;
246
+ if (input.sid === true) params.sid = true;
247
+ if (input.screen === true) params.screen = true;
248
+ if (input.timing === true) params.timing = true;
249
+ if (Object.keys(params).length === 0) throw new WireFailure("invalid-input", "Ask for at least one observation.");
250
+ const observed = await session.observe(params);
251
+ const result: Record<string, unknown> = {};
252
+ if (observed.registers !== undefined) result.registers = { ...observed.registers, pc: formatC64Address(observed.registers.pc) };
253
+ if (observed.memory !== undefined) result.memory = observed.memory.map((range) => ({ ...range, address: formatC64Address(range.address) }));
254
+ if (observed.vicii !== undefined) {
255
+ result.vicii = {
256
+ ...observed.vicii,
257
+ screenAddress: formatC64Address(observed.vicii.screenAddress),
258
+ graphicsAddress: formatC64Address(observed.vicii.graphicsAddress),
259
+ };
260
+ }
261
+ if (observed.sprites !== undefined) result.sprites = observed.sprites.map((sprite) => ({ ...sprite, dataAddress: formatC64Address(sprite.dataAddress) }));
262
+ if (observed.cia !== undefined) result.cia = observed.cia;
263
+ if (observed.sid !== undefined) result.sid = observed.sid;
264
+ if (observed.timing !== undefined) result.timing = observed.timing;
265
+ if (observed.screen === undefined) return result;
266
+ result.screen = { width: observed.screen.width, height: observed.screen.height };
267
+ return new ToolOutput(result, [{ type: "image", data: observed.screen.png, mimeType: "image/png" }]);
268
+ },
269
+ });
270
+
271
+ export const videoTools = [c64Vicii, c64Sprite, c64Cia, c64Sid, c64Screen, c64Observe];