@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.
- package/README.md +19 -59
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +5 -2
- package/skills/c64-assembler/scripts/assemble.ts +8 -1
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +1 -2
- package/skills/c64-disk/SKILL.md +20 -6
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +59 -5
- package/skills/c64-emulator/SKILL.md +11 -4
- package/skills/c64-knowledge/SKILL.md +53 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
- package/skills/c64-memory-map/SKILL.md +4 -1
- package/skills/c64-memory-map/scripts/memmap.ts +14 -3
- package/skills/c64-provenance/SKILL.md +37 -9
- package/skills/c64-provenance/scripts/compare.ts +60 -29
- package/skills/c64-provenance/scripts/provenance.ts +248 -19
- package/skills/c64-provenance/scripts/runtime.ts +179 -0
- package/skills/c64-provenance/scripts/separate.ts +132 -0
- package/skills/c64-reverse-engineering/SKILL.md +65 -11
- package/skills/c64-source/SKILL.md +119 -0
- package/skills/c64-source/scripts/generate.ts +402 -0
- package/skills/c64-source/scripts/loader.ts +320 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +558 -0
- package/skills/c64-static-analysis/SKILL.md +27 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +6 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +27 -3
- package/skills/c64-testing/references/scenario-format.md +43 -3
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +153 -135
- package/skills/c64-testing/scripts/test.ts +16 -6
- package/skills/c64-unpacker/SKILL.md +65 -14
- package/skills/c64-unpacker/scripts/evidence.ts +86 -21
- package/skills/c64-unpacker/scripts/loads.ts +174 -0
- package/skills/c64-unpacker/scripts/machine.ts +68 -0
- package/skills/c64-unpacker/scripts/unpack.ts +302 -48
- package/src/c64.ts +91 -0
- package/src/cli/status.ts +3 -1
- package/src/host/main.ts +10 -25
- package/src/host/server.ts +8 -40
- package/src/host/vice/adapter.ts +25 -10
- package/src/host/vice/session.ts +64 -20
- package/src/host-client/autostart.ts +45 -0
- package/src/host-client/connect.ts +4 -5
- package/src/host-client/coverage.ts +59 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/transfer.ts +29 -3
- package/src/host-client/vice-session.ts +42 -13
- package/src/knowledge/history.ts +40 -13
- package/src/knowledge/import.ts +72 -56
- package/src/knowledge/read.ts +203 -52
- package/src/knowledge/schema.ts +94 -12
- package/src/knowledge/write.ts +432 -123
- package/src/mcp/main.ts +1 -4
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +4 -2
- package/src/mcp/tools/debug.ts +110 -24
- package/src/mcp/tools/input.ts +1 -0
- package/src/mcp/tools/media.ts +22 -6
- package/src/mcp/tools/memory.ts +57 -10
- package/src/mcp/tools/video.ts +9 -3
- package/src/native/discover.ts +5 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +30 -1
- package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/vice.ts +72 -18
- 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
|
|
20
|
+
## Install
|
|
21
21
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
27
|
+
npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host
|
|
45
28
|
```
|
|
46
29
|
|
|
47
|
-
Install into
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
### Dev container
|
|
74
43
|
|
|
75
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
package/skills/c64-disk/SKILL.md
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
37
|
-
if (action !== "read" && values.out !== undefined) throw new UsageError("only read
|
|
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
|
|
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));
|