@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.4

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 (75) hide show
  1. package/README.md +19 -59
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +5 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  5. package/skills/c64-assembler/scripts/compare.ts +84 -0
  6. package/skills/c64-basic/SKILL.md +1 -2
  7. package/skills/c64-disk/SKILL.md +20 -6
  8. package/skills/c64-disk/scripts/containers.ts +124 -0
  9. package/skills/c64-disk/scripts/disk.ts +59 -5
  10. package/skills/c64-emulator/SKILL.md +11 -4
  11. package/skills/c64-knowledge/SKILL.md +53 -3
  12. package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
  13. package/skills/c64-memory-map/SKILL.md +4 -1
  14. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  15. package/skills/c64-provenance/SKILL.md +37 -9
  16. package/skills/c64-provenance/scripts/compare.ts +60 -29
  17. package/skills/c64-provenance/scripts/provenance.ts +248 -19
  18. package/skills/c64-provenance/scripts/runtime.ts +179 -0
  19. package/skills/c64-provenance/scripts/separate.ts +132 -0
  20. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  21. package/skills/c64-source/SKILL.md +119 -0
  22. package/skills/c64-source/scripts/generate.ts +402 -0
  23. package/skills/c64-source/scripts/loader.ts +320 -0
  24. package/skills/c64-source/scripts/opcodes.ts +61 -0
  25. package/skills/c64-source/scripts/source.ts +558 -0
  26. package/skills/c64-static-analysis/SKILL.md +27 -4
  27. package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
  28. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  29. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  30. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  31. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  32. package/skills/c64-testing/SKILL.md +27 -3
  33. package/skills/c64-testing/references/scenario-format.md +43 -3
  34. package/skills/c64-testing/scripts/run.ts +189 -0
  35. package/skills/c64-testing/scripts/scenario.ts +153 -135
  36. package/skills/c64-testing/scripts/test.ts +16 -6
  37. package/skills/c64-unpacker/SKILL.md +65 -14
  38. package/skills/c64-unpacker/scripts/evidence.ts +86 -21
  39. package/skills/c64-unpacker/scripts/loads.ts +174 -0
  40. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  41. package/skills/c64-unpacker/scripts/unpack.ts +302 -48
  42. package/src/c64.ts +91 -0
  43. package/src/cli/status.ts +3 -1
  44. package/src/host/main.ts +10 -25
  45. package/src/host/server.ts +8 -40
  46. package/src/host/vice/adapter.ts +25 -10
  47. package/src/host/vice/session.ts +64 -20
  48. package/src/host-client/autostart.ts +45 -0
  49. package/src/host-client/connect.ts +4 -5
  50. package/src/host-client/coverage.ts +59 -0
  51. package/src/host-client/steps.ts +170 -0
  52. package/src/host-client/transfer.ts +29 -3
  53. package/src/host-client/vice-session.ts +42 -13
  54. package/src/knowledge/history.ts +40 -13
  55. package/src/knowledge/import.ts +72 -56
  56. package/src/knowledge/read.ts +203 -52
  57. package/src/knowledge/schema.ts +94 -12
  58. package/src/knowledge/write.ts +432 -123
  59. package/src/mcp/main.ts +1 -4
  60. package/src/mcp/output.ts +32 -0
  61. package/src/mcp/server.ts +4 -2
  62. package/src/mcp/tools/debug.ts +110 -24
  63. package/src/mcp/tools/input.ts +1 -0
  64. package/src/mcp/tools/media.ts +22 -6
  65. package/src/mcp/tools/memory.ts +57 -10
  66. package/src/mcp/tools/video.ts +9 -3
  67. package/src/native/discover.ts +5 -2
  68. package/src/native/ghidra/index.ts +19 -3
  69. package/src/native/ghidra/scripts/C64Export.java +33 -1
  70. package/src/native/status.ts +15 -3
  71. package/src/native/types.ts +30 -1
  72. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  73. package/src/protocol/messages.ts +17 -10
  74. package/src/protocol/vice.ts +72 -18
  75. package/src/script.ts +3 -2
package/README.md CHANGED
@@ -17,73 +17,40 @@ run ACME, DXA and Ghidra themselves, and keep project knowledge local in
17
17
  The design is in [`docs/redesign/`](docs/redesign/README.md), and
18
18
  [`docs/plan.md`](docs/plan.md) tracks the implementation.
19
19
 
20
- ## Install and start
20
+ ## Install
21
21
 
22
- Requirements:
22
+ You need Node.js 24 or newer, and VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files. ACME, DXA and Ghidra are needed only for the skills that use them: [docs/install-tools.md](docs/install-tools.md) has the steps for each system.
23
23
 
24
- - Node.js 24 or newer with npx.
25
- - On the machine with VICE: VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files.
26
- - Where the agent runs: ACME, DXA and Ghidra, for the skills that use them.
27
-
28
- Everything runs through npx. Nothing is installed globally.
29
-
30
- ### Release or prerelease
31
-
32
- | Channel | Version |
33
- | --- | --- |
34
- | `latest` | The current release. |
35
- | `next` | The current prerelease, for example `2.0.0-rc.1`. |
36
-
37
- Until 2.0.0 is released, `latest` is the old 1.1.0. Use `next` until then. In the commands below, replace `<channel>` with `latest` or `next`. Use the same channel on both machines.
38
-
39
- ### On one machine
40
-
41
- Start the Host Runtime in its own terminal. Ctrl+C stops it and every emulator:
24
+ Start the Host Runtime and leave it running:
42
25
 
43
26
  ```
44
- npx -y --package=@henols/c64-re-tools@<channel> c64-re-tools-host
27
+ npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host
45
28
  ```
46
29
 
47
- Install into the project, check it, and start the agent:
30
+ Install into your project and start the agent:
48
31
 
49
32
  ```
50
33
  cd my-c64-project
51
- npx -y @henols/c64-re-tools@<channel> install --target claude
52
- npx -y @henols/c64-re-tools@<channel> status
34
+ npx -y @henols/c64-re-tools@latest install --target claude
53
35
  claude
54
36
  ```
55
37
 
56
- ### In a dev container
57
-
58
- On the host, start the Host Runtime on the container bridge with a shared secret:
59
-
60
- ```
61
- export C64RT_HOST_TOKEN=$(openssl rand -hex 16)
62
- npx -y --package=@henols/c64-re-tools@<channel> c64-re-tools-host --listen 172.17.0.1
63
- ```
64
-
65
- In the container, set the same `C64RT_HOST_TOKEN`, then:
38
+ `npx -y @henols/c64-re-tools@latest status` shows what works.
66
39
 
67
- ```
68
- npx -y @henols/c64-re-tools@<channel> install --target claude
69
- npx -y @henols/c64-re-tools@<channel> status
70
- claude
71
- ```
40
+ Until 2.0.0 is released, `@latest` is the old 1.1.0. Use the prerelease instead. For the prerelease, a dev container and debugging, see [docs/debug-install.md](docs/debug-install.md).
72
41
 
73
- The container reaches the host as `host.docker.internal`. `status` must show the Host Runtime as reachable.
42
+ ### Dev container
74
43
 
75
- ### Update and remove
44
+ The agent runs in the container, VICE and the Host Runtime on the host. In `.devcontainer/devcontainer.json`:
76
45
 
46
+ ```json
47
+ "features": {
48
+ "https://github.com/henols/c64-re-tools/releases/latest/download/devcontainer-feature-c64-re-tools.tgz": { "target": "claude" }
49
+ },
50
+ "runArgs": ["--add-host=host.docker.internal:host-gateway"]
77
51
  ```
78
- npx -y @henols/c64-re-tools@<channel> update # the newest skills into the project
79
- npx -y @henols/c64-re-tools@<channel> uninstall
80
- ```
81
-
82
- Restart the Host Runtime after a new release. `--global` installs into your home directory.
83
52
 
84
- ### Debugging
85
-
86
- See [docs/debugging.md](docs/debugging.md) for the trace and the field test.
53
+ The Feature installs ACME, dxa, Java 21 and Ghidra, puts the toolkit into the project when the container is created, and updates it at each start. `target` is a harness or a comma list. `channel` is `latest` (default) or `next`. Debian and Ubuntu images only. `runArgs` is for Docker on Linux. Start the Host Runtime on the host as above.
87
54
 
88
55
  ### Settings
89
56
 
@@ -94,9 +61,8 @@ See [docs/debugging.md](docs/debugging.md) for the trace and the field test.
94
61
  | `C64RT_ACME`, `C64RT_DXA` | skills, CLI | Full path of the tool when it is not on `PATH`. |
95
62
  | `C64RT_GHIDRA` | skills, CLI | The Ghidra installation directory, when `analyzeHeadless` is not on `PATH`. |
96
63
  | `C64RT_HOST` | MCP, skills, CLI | `host:port` of the Host Runtime when it is not on 127.0.0.1, `host.docker.internal` or `host.containers.internal` at 6464. |
97
- | `C64RT_HOST_TOKEN` | host, MCP, skills, CLI | The shared secret for a Host Runtime that listens beyond loopback. |
98
64
  | `C64RT_VIDEO` | MCP | `pal` (default) or `ntsc`, fixed for the life of the MCP process. |
99
- | `C64RT_TRACE` | host, MCP, skills, CLI | A directory for a trace of what the programs do, for a person who debugs a run. See [docs/debugging.md](docs/debugging.md). Nothing of the trace reaches the agent. |
65
+ | `C64RT_TRACE` | host, MCP, skills, CLI | A directory for a trace of what the programs do, for a person who debugs a run. See [docs/debug-install.md](docs/debug-install.md). Nothing of the trace reaches the agent. |
100
66
 
101
67
  Ghidra runs with its own settings directory for each request. The toolkit
102
68
  brings its own NMOS 6510 language (all 256 opcodes); your Ghidra installation
@@ -138,6 +104,7 @@ The agent keeps what it learns about the program in `.c64-re-tools/knowledge.db`
138
104
  | c64-unpacker | Packing evidence, and capture of the unpacked program from the emulator. |
139
105
  | c64-memory-map | Platform meaning of addresses and register values. |
140
106
  | c64-provenance | Byte comparison of releases. |
107
+ | c64-source | ACME source from the image and the knowledge, checked byte by byte, and a loader for the entry state. |
141
108
  | c64-assembler | ACME builds with diagnostics and symbols. |
142
109
  | c64-testing | Original-against-rebuild scenarios with PASS, FAIL or INCONCLUSIVE. |
143
110
 
@@ -190,14 +157,7 @@ C64RT_LIVE_VICE=/usr/bin/x64sc C64RT_GHIDRA=/opt/ghidra pnpm test
190
157
 
191
158
  ACME, DXA, c1541 and petcat tests run when the tools are on `PATH`.
192
159
 
193
- To release, push a tag on a commit of `main`. The tag gives the version:
194
-
195
- ```
196
- git tag v2.0.0-rc.2 && git push origin v2.0.0-rc.2 # a prerelease, to npm next
197
- git tag v2.0.0 && git push origin v2.0.0 # a release, to npm latest
198
- ```
199
-
200
- CI checks and tests the tagged commit, publishes it to npm and makes a GitHub Release. The repository keeps the version `0.0.0-development`.
160
+ To release, push a tag. See [docs/releasing.md](docs/releasing.md).
201
161
 
202
162
  ## Layout
203
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0-rc.4",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
@@ -31,6 +31,7 @@ node <skill>/scripts/assemble.ts --source-root src --entry main.a --out build/ga
31
31
  | `--include lib` | An include directory, relative to the source root. Use it again for more directories. |
32
32
  | `--define DEBUG=1` | An assembler symbol: an integer, also as `$` hex, or `true` or `false`. Use it again for more symbols. |
33
33
  | `--set-pc '$0801'` | The start address when the source does not set one. |
34
+ | `--compare unpacked/image.json` | Check the program byte by byte against a reference. The reference is the `image.json` of the c64-unpacker skill, a PRG, or 64 KiB of memory. |
34
35
 
35
36
  The script copies the full source root. Put all files that the program includes under it. Version control directories stay out.
36
37
 
@@ -59,12 +60,14 @@ A symbol from the rebuilt source can give the address of a routine in the rebuil
59
60
 
60
61
  The PRG in the project at the output path, with its load range and symbols. Or the source diagnostics when the assembly failed.
61
62
 
62
- The exit status is 0 for a PRG. The exit status is 1 for `assembled: false` and for an error. The exit status is 2 for a wrong argument.
63
+ With `--compare`, `compare` gives `identical` and one check for each segment of the reference. A check is `identical`, `different` with the first different address, or `not covered` when the program does not hold the segment.
64
+
65
+ The exit status is 0 for a PRG. The exit status is 1 for `assembled: false`, for a `--compare` that is not identical, and for an error. The exit status is 2 for a wrong argument.
63
66
 
64
67
  ## Failure and conflicts
65
68
 
66
69
  - `assembled: false`: the source has errors. This is a normal result. Correct the source.
67
- - `installation-incomplete`: ACME is not installed on this machine. Tell the user the message.
70
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
68
71
  - `invalid-input`: an option or a path is wrong. Read the message and correct the call.
69
72
 
70
73
  ## Handoffs
@@ -12,6 +12,7 @@ import { localToolContext } from "#src/native/local.ts";
12
12
  import type { AcmeParams } from "#src/native/types.ts";
13
13
  import { resolveProjectPath } from "#src/project.ts";
14
14
  import { runScript, UsageError } from "#src/script.ts";
15
+ import { compareBuild } from "./compare.ts";
15
16
 
16
17
  const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [options]
17
18
 
@@ -21,6 +22,8 @@ const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [
21
22
  --include <dir> include directory relative to the source root (repeatable)
22
23
  --define NAME=VALUE assembler symbol: an integer ($hex allowed) or true/false (repeatable)
23
24
  --set-pc <address> start address when the source sets none, for example '$0801'
25
+ --compare <file> check the program byte by byte against an image.json of the
26
+ c64-unpacker skill (each segment), a PRG, or 64 KiB of memory
24
27
 
25
28
  The result is one JSON object. Addresses are $ and four hex digits.`;
26
29
 
@@ -45,6 +48,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
45
48
  include: { type: "string", multiple: true },
46
49
  define: { type: "string", multiple: true },
47
50
  "set-pc": { type: "string" },
51
+ compare: { type: "string" },
48
52
  help: { type: "boolean" },
49
53
  },
50
54
  });
@@ -77,6 +81,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
77
81
  mkdirSync(dirname(outPath), { recursive: true });
78
82
  writeFileSync(outPath, attachments![0]!);
79
83
  const range = result.loadRange!;
84
+ const compared = values.compare === undefined ? undefined : compareBuild(attachments![0]!, values.compare);
80
85
  return {
81
86
  output: {
82
87
  assembled: true,
@@ -88,8 +93,10 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
88
93
  : { name: symbol.name, kind: "constant", value: symbol.value, used: symbol.used },
89
94
  ),
90
95
  diagnostics: result.diagnostics,
96
+ ...(compared === undefined ? {} : { compare: compared }),
91
97
  },
92
- failed: false,
98
+ // A build that differs from its reference is a failed check.
99
+ failed: compared !== undefined && !compared.identical,
93
100
  };
94
101
  }
95
102
 
@@ -0,0 +1,84 @@
1
+ // The byte check of a build (D47): the assembled program against the image
2
+ // of the unpacker (image.json with image.bin), a reference PRG, or 64 KiB of
3
+ // memory, segment by segment.
4
+
5
+ import { posix } from "node:path";
6
+
7
+ import { formatC64Address, prgRange } from "#src/c64.ts";
8
+ import { readProjectFile } from "#src/host-client/transfer.ts";
9
+ import { UsageError } from "#src/script.ts";
10
+
11
+ export interface SegmentCheck {
12
+ start: string;
13
+ end: string;
14
+ result: "identical" | "different" | "not covered";
15
+ /** Bytes of the segment that the build does not hold. */
16
+ uncovered?: number;
17
+ differences?: number;
18
+ firstDifference?: { address: string; built: number; reference: number };
19
+ }
20
+
21
+ const FULL = 0x10000;
22
+
23
+ /** The reference as memory and the ranges to check in it. */
24
+ function reference(path: string, built: { start: number; end: number }): { memory: (address: number) => number | undefined; segments: Array<{ start: number; end: number }> } {
25
+ const { bytes } = readProjectFile(path);
26
+ if (path.endsWith(".json")) {
27
+ let manifest: { image?: unknown; segments?: unknown };
28
+ try {
29
+ manifest = JSON.parse(bytes.toString("utf8")) as typeof manifest;
30
+ } catch (error) {
31
+ throw new UsageError(`--compare ${path} is not JSON: ${(error as Error).message}`);
32
+ }
33
+ const segments = manifest.segments;
34
+ if (typeof manifest.image !== "string" || !Array.isArray(segments) || segments.length === 0) throw new UsageError(`--compare ${path} is not an image.json: it needs image and segments`);
35
+ const image = readProjectFile(posix.join(posix.dirname(path), manifest.image)).bytes;
36
+ if (image.length !== FULL) throw new UsageError(`the image of ${path} has ${image.length} bytes, not 65536`);
37
+ return {
38
+ memory: (address) => image[address],
39
+ segments: segments.map((segment: { start?: unknown; end?: unknown }) => {
40
+ const parse = (value: unknown) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : NaN);
41
+ const range = { start: parse(segment.start), end: parse(segment.end) };
42
+ if (!(range.start <= range.end)) throw new UsageError(`--compare ${path}: a segment needs start and end as $ addresses`);
43
+ return range;
44
+ }),
45
+ };
46
+ }
47
+ if (bytes.length === FULL && !path.endsWith(".prg")) return { memory: (address) => bytes[address], segments: [built] };
48
+ let range: ReturnType<typeof prgRange>;
49
+ try {
50
+ range = prgRange(bytes);
51
+ } catch (error) {
52
+ throw new UsageError(`--compare ${path}: ${(error as Error).message}`);
53
+ }
54
+ return { memory: (address) => (address >= range.start && address <= range.end ? range.body[address - range.start] : undefined), segments: [{ start: range.start, end: range.end }] };
55
+ }
56
+
57
+ /** Compares the assembled PRG with the reference, segment by segment. */
58
+ export function compareBuild(prg: Uint8Array, path: string): { against: string; identical: boolean; segments: SegmentCheck[] } {
59
+ const built = prgRange(prg);
60
+ const { memory, segments } = reference(path, built);
61
+ const checks = segments.map((segment): SegmentCheck => {
62
+ const from = Math.max(segment.start, built.start);
63
+ const to = Math.min(segment.end, built.end);
64
+ const shown = { start: formatC64Address(segment.start), end: formatC64Address(segment.end) };
65
+ if (from > to) return { ...shown, result: "not covered", uncovered: segment.end - segment.start + 1 };
66
+ let differences = 0;
67
+ let first: SegmentCheck["firstDifference"];
68
+ for (let address = from; address <= to; address++) {
69
+ const builtByte = built.body[address - built.start]!;
70
+ const referenceByte = memory(address);
71
+ if (referenceByte === undefined || builtByte === referenceByte) continue;
72
+ differences++;
73
+ first ??= { address: formatC64Address(address), built: builtByte, reference: referenceByte };
74
+ }
75
+ const uncovered = segment.end - segment.start + 1 - (to - from + 1);
76
+ return {
77
+ ...shown,
78
+ result: differences > 0 ? "different" : uncovered > 0 ? "not covered" : "identical",
79
+ ...(uncovered > 0 ? { uncovered } : {}),
80
+ ...(differences > 0 ? { differences, firstDifference: first! } : {}),
81
+ };
82
+ });
83
+ return { against: path, identical: checks.every((check) => check.result === "identical"), segments: checks };
84
+ }
@@ -56,8 +56,7 @@ The exit status is 0 for a listing. The exit status is 1 for `decoded: false` an
56
56
 
57
57
  - `decoded: false`: the file is not a BASIC program. This is a normal result. Usually the file is machine code. Give it to the c64-static-analysis skill.
58
58
  - An empty `handoffs` list: the BASIC does not start machine code with SYS or USR. Do not make up a handoff. Look for a POKE into a vector or a LOAD of another file in the listing.
59
- - `installation-incomplete`: petcat is not on the host. Tell the user the message.
60
- - `operation-failed` with a message about the host runtime: the host runtime does not run. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
59
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
61
60
  - `not-found` or `invalid-input`: the path or an argument is wrong. Read the message and correct the call.
62
61
 
63
62
  ## Handoffs
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: c64-disk
3
- description: Use this skill to examine a C64 disk image (D64, D71, D81 or G64) and to extract files from it into the project. It shows the directory, the BAM, the directory entry of a file and the sector chain of a file. Do not use it to run a program or to decode BASIC. Give an extracted file to the c64-basic, c64-unpacker or c64-static-analysis skill.
3
+ description: Use this skill to examine a C64 disk image (D64, D71, D81 or G64), a T64 tape file or a CRT cartridge file. It extracts files into the project. It shows the directory, the BAM, the directory entry of a file and the sector chain of a file. It writes each bank of a cartridge as a PRG. Do not use it to run a program or to decode BASIC. Give an extracted file to the c64-basic, c64-unpacker or c64-static-analysis skill.
4
4
  ---
5
5
 
6
6
  # c64-disk
7
7
 
8
8
  ## Purpose
9
9
 
10
- Examine a C64 disk image. Extract a file from the image into the project.
10
+ Examine a C64 disk image, a T64 tape file or a CRT cartridge file. Extract a file from it into the project.
11
11
 
12
12
  This skill does not change the disk image and does not change project knowledge.
13
13
 
@@ -16,10 +16,11 @@ This skill does not change the disk image and does not change project knowledge.
16
16
  - The disk image: a `.d64`, `.d71`, `.d81` or `.g64` file, relative to the project directory.
17
17
  - For one file: the file name as the directory shows it.
18
18
  - To extract a file: the output path, relative to the project directory.
19
+ - A T64 tape file or a CRT cartridge file, relative to the project directory.
19
20
 
20
21
  ## Tools / execution path
21
22
 
22
- The script sends the image to the host runtime. The host runs c1541 and sends the result back. The script writes an extracted file into the project.
23
+ The script sends a disk image to the host runtime. The host runs c1541 and sends the result back. The script writes an extracted file into the project. It reads T64 and CRT files itself.
23
24
 
24
25
  ```
25
26
  node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
@@ -32,6 +33,10 @@ node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
32
33
  | The type, size and first block of one file | `entry original/game.d64 GAME` |
33
34
  | The track and sector of each block of one file | `chain original/game.d64 GAME` |
34
35
  | Extract one file | `read original/game.d64 GAME --out extracted/game.prg` |
36
+ | The files of a tape | `directory original/game.t64` |
37
+ | Extract one file of a tape | `read original/game.t64 GAME --out extracted/game.prg` |
38
+ | The banks of a cartridge | `directory original/game.crt` |
39
+ | Write each bank of a cartridge as a PRG | `crt original/game.crt --out cart` |
35
40
 
36
41
  The script prints one JSON object.
37
42
 
@@ -55,6 +60,15 @@ Do not store:
55
60
 
56
61
  A downstream skill records the meaning of the extracted content.
57
62
 
63
+ ## Tapes and cartridges
64
+
65
+ The script reads T64 and CRT files itself. The host runtime is not necessary for them.
66
+
67
+ - T64: `directory` and `read`. Many T64 files have a wrong end address. Then the entry has `fixedSize: true`, and the size comes from the position of the next file.
68
+ - CRT: `directory` and `crt`. The script reads hardware types 0 (normal 8K, 16K or Ultimax), 5 (Ocean), 19 (Magic Desk) and 32 (EasyFlash). It refuses other types by name. `crt` writes `crt.json` and one PRG for each CHIP packet, for example `bank3-8000.prg`.
69
+ - A cartridge bank uses the same addresses as other banks. Analyze each bank PRG with c64-static-analysis and `--space bank3`, and record its knowledge in space `bank3`.
70
+ - TAP: a TAP file holds tape signals, and turbo loaders have their own formats. The script does not read it. Start it with the c64-emulator skill, or capture its parts with `loads` of the c64-unpacker skill.
71
+
58
72
  ## Result
59
73
 
60
74
  The directory, the BAM, an entry or a sector chain as JSON. Or the extracted file in the project at the output path.
@@ -65,8 +79,7 @@ The exit status is 0 for a result with the file. The exit status is 1 for `found
65
79
 
66
80
  - `found: false`: the image has no file with that name. This is a normal result. Read the directory again and compare the names.
67
81
  - `media-error`: the image has a defect or is not a disk image. The message tells the problem, for example a sector chain that loops. Tell the user. Do not change the image.
68
- - `installation-incomplete`: c1541 is not on the host. Tell the user the message.
69
- - `operation-failed` with a message about the host runtime: the host runtime does not run. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
82
+ - `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
70
83
  - `invalid-input`: an argument or a path is wrong. Read the message and correct the call.
71
84
 
72
85
  A file that is not `closed` was not written fully. Its data can be incomplete.
@@ -75,5 +88,6 @@ A file that is not `closed` was not written fully. Its data can be incomplete.
75
88
 
76
89
  - BASIC program → c64-basic
77
90
  - Packed program → c64-unpacker
78
- - Machine code → c64-static-analysis
91
+ - Machine code, also a cartridge bank → c64-static-analysis
92
+ - A TAP file → c64-unpacker
79
93
  - Run the program → c64-emulator
@@ -0,0 +1,124 @@
1
+ // Cartridge and tape containers that c64-disk reads locally (D54): no host
2
+ // tool reads them. A CRT file holds CHIP packets, each a bank of ROM at a
3
+ // load address; a T64 file holds a directory of PRG files. TAP files hold
4
+ // raw pulses and go through the emulator only.
5
+
6
+ import { formatC64Address, petsciiNameToText } from "#src/c64.ts";
7
+
8
+ /** A refusal of a container file: the caller turns it into a usage error. */
9
+ export class ContainerError extends Error {}
10
+
11
+ /** The CRT hardware types this toolkit reads, by type number (D54). */
12
+ export const CRT_TYPES: Record<number, string> = { 0: "normal", 5: "Ocean", 19: "Magic Desk", 32: "EasyFlash" };
13
+
14
+ export interface CrtChip {
15
+ bank: number;
16
+ address: number;
17
+ size: number;
18
+ /** 0 ROM, 1 RAM, 2 flash. */
19
+ chipType: number;
20
+ data: Uint8Array;
21
+ }
22
+
23
+ export interface Crt {
24
+ name: string;
25
+ type: number;
26
+ typeName: string;
27
+ /** The EXROM and GAME lines at start: 0 is active. */
28
+ exrom: number;
29
+ game: number;
30
+ /** For type 0: 8K, 16K or Ultimax by the EXROM and GAME lines. */
31
+ mode?: "8k" | "16k" | "ultimax";
32
+ chips: CrtChip[];
33
+ }
34
+
35
+ const SIGNATURE = "C64 CARTRIDGE ";
36
+
37
+ export function parseCrt(bytes: Uint8Array): Crt {
38
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
39
+ if (buffer.length < 0x40 || buffer.toString("latin1", 0, 16) !== SIGNATURE) throw new ContainerError("the file is not a CRT: it does not start with C64 CARTRIDGE");
40
+ const headerLength = buffer.readUInt32BE(0x10);
41
+ const type = buffer.readUInt16BE(0x16);
42
+ const exrom = buffer[0x18]!;
43
+ const game = buffer[0x19]!;
44
+ const typeName = CRT_TYPES[type];
45
+ if (typeName === undefined) {
46
+ throw new ContainerError(`the CRT has hardware type ${type}; this toolkit reads types ${Object.entries(CRT_TYPES).map(([number, name]) => `${number} (${name})`).join(", ")}`);
47
+ }
48
+ const name = buffer.toString("latin1", 0x20, 0x40).replace(/\0+$/, "");
49
+ const chips: CrtChip[] = [];
50
+ let offset = Math.max(headerLength, 0x40);
51
+ while (offset + 0x10 <= buffer.length) {
52
+ if (buffer.toString("latin1", offset, offset + 4) !== "CHIP") throw new ContainerError(`the CRT has no CHIP packet at offset ${offset}`);
53
+ const packetLength = buffer.readUInt32BE(offset + 4);
54
+ const chipType = buffer.readUInt16BE(offset + 8);
55
+ const bank = buffer.readUInt16BE(offset + 10);
56
+ const address = buffer.readUInt16BE(offset + 12);
57
+ const size = buffer.readUInt16BE(offset + 14);
58
+ if (packetLength < 0x10 || offset + packetLength > buffer.length) throw new ContainerError(`the CHIP packet at offset ${offset} runs past the end of the file`);
59
+ if (bank > 255) throw new ContainerError(`the CHIP packet at offset ${offset} has bank ${bank}; banks go up to 255`);
60
+ if (size > packetLength - 0x10 || address + size > 0x10000) throw new ContainerError(`the CHIP packet at offset ${offset} has a wrong size ${size} at ${formatC64Address(address)}`);
61
+ if (size > 0) chips.push({ bank, address, size, chipType, data: buffer.subarray(offset + 0x10, offset + 0x10 + size) });
62
+ offset += packetLength;
63
+ }
64
+ if (chips.length === 0) throw new ContainerError("the CRT has no CHIP packet with data");
65
+ const crt: Crt = { name, type, typeName, exrom, game, chips };
66
+ if (type === 0) crt.mode = exrom === 0 && game === 1 ? "8k" : exrom === 0 && game === 0 ? "16k" : "ultimax";
67
+ return crt;
68
+ }
69
+
70
+ export interface TapeEntry {
71
+ name: string;
72
+ nameBytes: number[];
73
+ /** The C64 file type byte of the entry. */
74
+ fileType: number;
75
+ start: number;
76
+ end: number;
77
+ bytes: number;
78
+ offset: number;
79
+ /** True when the end address in the directory was wrong and the size comes from the file layout. */
80
+ fixedSize?: boolean;
81
+ }
82
+
83
+ export interface Tape {
84
+ name: string;
85
+ entries: TapeEntry[];
86
+ }
87
+
88
+ export function parseT64(bytes: Uint8Array): Tape {
89
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
90
+ if (buffer.length < 0x40 || !buffer.toString("latin1", 0, 32).startsWith("C64")) throw new ContainerError("the file is not a T64: it does not start with C64");
91
+ const maxEntries = buffer.readUInt16LE(0x22);
92
+ const name = buffer.toString("latin1", 0x28, 0x40).replace(/[\0 ]+$/, "");
93
+ const raw: Array<Omit<TapeEntry, "bytes" | "end"> & { end: number }> = [];
94
+ for (let index = 0; index < maxEntries; index++) {
95
+ const at = 0x40 + index * 0x20;
96
+ if (at + 0x20 > buffer.length) break;
97
+ if (buffer[at] !== 1) continue;
98
+ const nameField = [...buffer.subarray(at + 0x10, at + 0x20)];
99
+ while (nameField.length > 0 && (nameField.at(-1) === 0x20 || nameField.at(-1) === 0xa0 || nameField.at(-1) === 0x00)) nameField.pop();
100
+ raw.push({
101
+ name: petsciiNameToText(Uint8Array.from(nameField)),
102
+ nameBytes: nameField,
103
+ fileType: buffer[at + 1]!,
104
+ start: buffer.readUInt16LE(at + 2),
105
+ end: buffer.readUInt16LE(at + 4),
106
+ offset: buffer.readUInt32LE(at + 8),
107
+ });
108
+ }
109
+ // Many T64 files have a wrong end address: the next file or the end of the file bounds each one.
110
+ const offsets = [...raw.map((entry) => entry.offset), buffer.length].sort((a, b) => a - b);
111
+ const entries = raw.map((entry): TapeEntry => {
112
+ if (entry.offset >= buffer.length) throw new ContainerError(`the T64 entry ${entry.name} starts past the end of the file`);
113
+ const available = offsets.find((offset) => offset > entry.offset)! - entry.offset;
114
+ const stated = entry.end - entry.start;
115
+ const bytes = stated > 0 && stated <= available ? stated : Math.min(available, 0x10000 - entry.start);
116
+ return { ...entry, end: entry.start + bytes - 1, bytes, ...(bytes === stated ? {} : { fixedSize: true }) };
117
+ });
118
+ return { name, entries };
119
+ }
120
+
121
+ /** The PRG of a tape entry: its start address, then its bytes. */
122
+ export function tapeFile(bytes: Uint8Array, entry: TapeEntry): Buffer {
123
+ return Buffer.concat([Buffer.from([entry.start & 0xff, entry.start >> 8]), Buffer.from(bytes.subarray(entry.offset, entry.offset + entry.bytes))]);
124
+ }
@@ -1,13 +1,17 @@
1
1
  // The c64-disk script: inspects a project disk image with c1541 through the
2
- // Host Runtime and extracts files into the project.
2
+ // Host Runtime and extracts files into the project. T64 tape files and CRT
3
+ // cartridge files are read locally (D54): no host tool reads them.
3
4
 
4
5
  import { mkdirSync, writeFileSync } from "node:fs";
5
6
  import { dirname } from "node:path";
6
7
  import { parseArgs } from "node:util";
7
8
 
9
+ import { formatC64Address } from "#src/c64.ts";
8
10
  import { inspectDisk } from "#src/host-client/tools.ts";
11
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
9
12
  import { resolveProjectPath } from "#src/project.ts";
10
13
  import { runScript, UsageError } from "#src/script.ts";
14
+ import { ContainerError, parseCrt, parseT64, tapeFile } from "./containers.ts";
11
15
 
12
16
  const USAGE = `disk.ts <action> <image> [name] [--out <file>]
13
17
 
@@ -16,12 +20,16 @@ const USAGE = `disk.ts <action> <image> [name] [--out <file>]
16
20
  entry <image> <name> the directory entry of one file
17
21
  chain <image> <name> the track and sector of each block of one file
18
22
  read <image> <name> --out <file> write the bytes of one file into the project
23
+ crt <file.crt> --out <dir> write each CHIP packet of a cartridge as a PRG
19
24
 
20
25
  <image> is a .d64, .d71, .d81 or .g64 file, relative to the project directory.
26
+ directory and read also take a .t64 tape file, and directory a .crt cartridge
27
+ file; the script reads those itself. A CRT can have hardware type 0 (normal
28
+ 8K, 16K or Ultimax), 5 (Ocean), 19 (Magic Desk) or 32 (EasyFlash).
21
29
  <name> is the file name as the directory shows it. Write a byte that has no
22
30
  character as {$xx}, for example {$c1}. The result is one JSON object.`;
23
31
 
24
- const ACTIONS = ["directory", "bam", "entry", "chain", "read"] as const;
32
+ const ACTIONS = ["directory", "bam", "entry", "chain", "read", "crt"] as const;
25
33
  type Action = (typeof ACTIONS)[number];
26
34
 
27
35
  async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
@@ -33,8 +41,8 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
33
41
  const named = action === "entry" || action === "chain" || action === "read";
34
42
  if (named && name === undefined) throw new UsageError(`${action} needs a file name`);
35
43
  if ((!named && name !== undefined) || extra.length > 0) throw new UsageError("too many arguments");
36
- if (action === "read" && values.out === undefined) throw new UsageError("read needs --out <file>");
37
- if (action !== "read" && values.out !== undefined) throw new UsageError("only read takes --out");
44
+ if ((action === "read" || action === "crt") && values.out === undefined) throw new UsageError(`${action} needs --out`);
45
+ if (action !== "read" && action !== "crt" && values.out !== undefined) throw new UsageError("only read and crt take --out");
38
46
  let outPath: string | undefined;
39
47
  if (values.out !== undefined) {
40
48
  try {
@@ -44,7 +52,17 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
44
52
  }
45
53
  }
46
54
 
47
- const result = await inspectDisk({ image, action: action as Action, ...(name === undefined ? {} : { name }) });
55
+ const kind = image.toLowerCase().endsWith(".t64") ? "t64" : image.toLowerCase().endsWith(".crt") ? "crt" : "disk";
56
+ if (action === "crt" && kind !== "crt") throw new UsageError("crt needs a .crt file");
57
+ if (kind !== "disk") {
58
+ try {
59
+ return local(kind, action as Action, image, name, values.out);
60
+ } catch (error) {
61
+ if (error instanceof ContainerError) throw new UsageError(`${image}: ${error.message}`);
62
+ throw error;
63
+ }
64
+ }
65
+ const result = await inspectDisk({ image, action: action as Exclude<Action, "crt">, ...(name === undefined ? {} : { name }) });
48
66
  if ("found" in result && !result.found) return { output: { found: false }, failed: true };
49
67
  if (result.action === "read" && result.found) {
50
68
  mkdirSync(dirname(outPath!), { recursive: true });
@@ -55,6 +73,42 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
55
73
  return { output, failed: false };
56
74
  }
57
75
 
76
+ /** A tape or cartridge file, read here (D54). */
77
+ function local(kind: "t64" | "crt", action: Action, image: string, name: string | undefined, out: string | undefined): { output: unknown; failed: boolean } {
78
+ const { bytes } = readProjectFile(image);
79
+ if (kind === "t64") {
80
+ if (action !== "directory" && action !== "read") throw new UsageError(`a T64 file has only directory and read, not ${action}`);
81
+ const tape = parseT64(bytes);
82
+ const shown = (entry: (typeof tape.entries)[number]) => ({
83
+ name: entry.name,
84
+ start: formatC64Address(entry.start),
85
+ end: formatC64Address(entry.end),
86
+ bytes: entry.bytes,
87
+ ...(entry.fixedSize ? { fixedSize: true } : {}),
88
+ });
89
+ if (action === "directory") return { output: { name: tape.name, entries: tape.entries.map(shown) }, failed: false };
90
+ const entry = tape.entries.find((candidate) => candidate.name.toLowerCase() === name!.toLowerCase());
91
+ if (entry === undefined) return { output: { found: false }, failed: true };
92
+ const file = tapeFile(bytes, entry);
93
+ writeProjectFile(out!, file, { what: "--out" });
94
+ return { output: { found: true, name: entry.name, bytes: file.length, output: out }, failed: false };
95
+ }
96
+ if (action !== "directory" && action !== "crt") throw new UsageError(`a CRT file has only directory and crt, not ${action}`);
97
+ const crt = parseCrt(bytes);
98
+ const chips = crt.chips.map((chip) => ({
99
+ bank: chip.bank,
100
+ address: formatC64Address(chip.address),
101
+ size: chip.size,
102
+ space: `bank${chip.bank}`,
103
+ file: `bank${chip.bank}-${chip.address.toString(16).padStart(4, "0")}.prg`,
104
+ }));
105
+ const output = { name: crt.name, type: crt.type, typeName: crt.typeName, exrom: crt.exrom, game: crt.game, ...(crt.mode === undefined ? {} : { mode: crt.mode }), chips };
106
+ if (action === "directory") return { output: { ...output, chips: chips.map(({ file: _file, ...chip }) => chip) }, failed: false };
107
+ crt.chips.forEach((chip, index) => writeProjectFile(`${out}/${chips[index]!.file}`, Buffer.concat([Buffer.from([chip.address & 0xff, chip.address >> 8]), chip.data]), { what: "--out" }));
108
+ writeProjectFile(`${out}/crt.json`, Buffer.from(`${JSON.stringify(output, null, 2)}\n`), { what: "--out" });
109
+ return { output: { ...output, out }, failed: false };
110
+ }
111
+
58
112
  await runScript(
59
113
  async () => {
60
114
  const { output, failed } = await run(process.argv.slice(2));