@henols/c64-re-tools 1.1.0 → 2.0.0-rc.10
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/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The c64-assembler script: assembles project source with ACME, which it runs
|
|
2
|
+
// itself, and writes the program into the project.
|
|
3
|
+
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
9
|
+
import { readProjectTree } from "#src/host-client/transfer.ts";
|
|
10
|
+
import { assemble } from "#src/native/acme.ts";
|
|
11
|
+
import { localToolContext } from "#src/native/local.ts";
|
|
12
|
+
import type { AcmeParams } from "#src/native/types.ts";
|
|
13
|
+
import { resolveProjectPath } from "#src/project.ts";
|
|
14
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
15
|
+
import { compareBuild } from "./compare.ts";
|
|
16
|
+
|
|
17
|
+
const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [options]
|
|
18
|
+
|
|
19
|
+
--source-root <dir> project directory to assemble from; all of it is sent
|
|
20
|
+
--entry <file> the main source file, relative to the source root
|
|
21
|
+
--out <file.prg> where to write the program, relative to the project
|
|
22
|
+
--include <dir> include directory relative to the source root (repeatable)
|
|
23
|
+
--define NAME=VALUE assembler symbol: an integer ($hex allowed) or true/false (repeatable)
|
|
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
|
|
27
|
+
|
|
28
|
+
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
29
|
+
|
|
30
|
+
function parseDefine(text: string): [string, number | boolean] {
|
|
31
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/.exec(text);
|
|
32
|
+
if (match === null) throw new UsageError(`--define needs NAME=VALUE, not ${text}`);
|
|
33
|
+
const value = match[2]!;
|
|
34
|
+
if (value === "true" || value === "false") return [match[1]!, value === "true"];
|
|
35
|
+
if (/^\$[0-9a-fA-F]+$/.test(value)) return [match[1]!, Number.parseInt(value.slice(1), 16)];
|
|
36
|
+
if (/^-?\d+$/.test(value)) return [match[1]!, Number(value)];
|
|
37
|
+
throw new UsageError(`--define ${match[1]} needs an integer or true/false, not ${value}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
|
|
41
|
+
const { values } = parseArgs({
|
|
42
|
+
args: argv,
|
|
43
|
+
strict: true,
|
|
44
|
+
options: {
|
|
45
|
+
"source-root": { type: "string" },
|
|
46
|
+
entry: { type: "string" },
|
|
47
|
+
out: { type: "string" },
|
|
48
|
+
include: { type: "string", multiple: true },
|
|
49
|
+
define: { type: "string", multiple: true },
|
|
50
|
+
"set-pc": { type: "string" },
|
|
51
|
+
compare: { type: "string" },
|
|
52
|
+
help: { type: "boolean" },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (values.help) throw new UsageError("");
|
|
56
|
+
const sourceRoot = values["source-root"];
|
|
57
|
+
const entry = values.entry;
|
|
58
|
+
const out = values.out;
|
|
59
|
+
if (sourceRoot === undefined || entry === undefined || out === undefined) throw new UsageError("--source-root, --entry and --out are necessary");
|
|
60
|
+
let outPath: string;
|
|
61
|
+
try {
|
|
62
|
+
outPath = resolveProjectPath(out);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${out}`);
|
|
65
|
+
}
|
|
66
|
+
const defines = Object.fromEntries((values.define ?? []).map(parseDefine));
|
|
67
|
+
let setPc: number | undefined;
|
|
68
|
+
if (values["set-pc"] !== undefined) {
|
|
69
|
+
try {
|
|
70
|
+
setPc = parseC64Address(values["set-pc"]);
|
|
71
|
+
} catch {
|
|
72
|
+
throw new UsageError(`--set-pc must be $ followed by four hex digits, not ${values["set-pc"]}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const tree = readProjectTree(sourceRoot);
|
|
76
|
+
if (!tree.files.some((file) => file.path === entry)) throw new UsageError(`--entry ${entry} is not a file in ${sourceRoot}`);
|
|
77
|
+
const params: AcmeParams = { files: tree.files, entrySource: entry, includeDirs: values.include ?? [], defines, ...(setPc === undefined ? {} : { setPc }) };
|
|
78
|
+
|
|
79
|
+
const { result, attachments } = await assemble(params, tree.contents, localToolContext());
|
|
80
|
+
if (!result.assembled) return { output: { assembled: false, diagnostics: result.diagnostics }, failed: true };
|
|
81
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
82
|
+
writeFileSync(outPath, attachments![0]!);
|
|
83
|
+
const range = result.loadRange!;
|
|
84
|
+
const compared = values.compare === undefined ? undefined : compareBuild(attachments![0]!, values.compare);
|
|
85
|
+
return {
|
|
86
|
+
output: {
|
|
87
|
+
assembled: true,
|
|
88
|
+
output: out,
|
|
89
|
+
loadRange: { start: formatC64Address(range.start), end: formatC64Address(range.end), bytes: range.bytes },
|
|
90
|
+
symbols: result.symbols!.map((symbol) =>
|
|
91
|
+
symbol.kind === "address"
|
|
92
|
+
? { name: symbol.name, kind: "address", address: formatC64Address(symbol.address), used: symbol.used }
|
|
93
|
+
: { name: symbol.name, kind: "constant", value: symbol.value, used: symbol.used },
|
|
94
|
+
),
|
|
95
|
+
diagnostics: result.diagnostics,
|
|
96
|
+
...(compared === undefined ? {} : { compare: compared }),
|
|
97
|
+
},
|
|
98
|
+
// A build that differs from its reference is a failed check.
|
|
99
|
+
failed: compared !== undefined && !compared.identical,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await runScript(
|
|
104
|
+
async () => {
|
|
105
|
+
const { output, failed } = await run(process.argv.slice(2));
|
|
106
|
+
if (failed) process.exitCode = 1;
|
|
107
|
+
return output;
|
|
108
|
+
},
|
|
109
|
+
{ usage: USAGE },
|
|
110
|
+
);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-basic
|
|
3
|
+
description: Use this skill to decode a C64 BASIC V2 program (PRG) into a readable listing. It also finds where the BASIC gives control to machine code with SYS or USR. Use it on the start file of a game or of a demo. Do not use it on machine code or to run the program. Give the handoff address to the c64-static-analysis or c64-emulator skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# c64-basic
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Decode a C64 BASIC V2 program. Find the handoff from BASIC to machine code.
|
|
11
|
+
|
|
12
|
+
This skill does not run the program and does not change project knowledge.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- The program: a PRG file, relative to the project directory.
|
|
17
|
+
|
|
18
|
+
## Tools / execution path
|
|
19
|
+
|
|
20
|
+
The script sends the program to the host runtime. The host decodes it with petcat and finds the handoffs.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
node <skill>/scripts/basic.ts extracted/game.prg
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The script prints one JSON object. Addresses are `$` and four hex digits.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
1. Run the script on the program.
|
|
31
|
+
2. Read `listing`. Each line starts with its BASIC line number. Keywords show in lower case. Control characters in strings show in braces, for example `{clr}`.
|
|
32
|
+
3. Read `handoffs`. Each handoff has a `kind` (`sys` or `usr`) and the BASIC `line`.
|
|
33
|
+
- A `sys` handoff with an `address`: BASIC calls the machine code at that address. The script calculates a SYS argument with numbers, π, `+`, `-`, `*`, `/` and brackets. For example, `SYS π*656` calls `$080c`.
|
|
34
|
+
- A handoff with `computed: true`: the address comes from a calculation at run time, for example with `PEEK`. A `sys` handoff gives its argument in `expression`. Use the c64-emulator skill to find the address.
|
|
35
|
+
4. Compare the address with `loadRange` and `basicEnd`. From `basicEnd` to the end of `loadRange`, the file holds bytes that are not BASIC. Usually this is the machine code that the handoff calls. Some programs have no end marker (two zero bytes) after the last line. The machine code starts directly after the last line. Then `basicEnd` is the first byte of that machine code.
|
|
36
|
+
|
|
37
|
+
The address ranges are for a load to the load address of the file. A load without `,1` puts a BASIC program at `$0801`. Most loaders have `$0801` as their load address.
|
|
38
|
+
|
|
39
|
+
## Knowledge
|
|
40
|
+
|
|
41
|
+
Automatic write:
|
|
42
|
+
- none.
|
|
43
|
+
|
|
44
|
+
Do not store:
|
|
45
|
+
- the listing.
|
|
46
|
+
|
|
47
|
+
The entry point of the machine code can be durable knowledge. The c64-reverse-engineering skill decides that and records it with the c64-knowledge skill.
|
|
48
|
+
|
|
49
|
+
## Result
|
|
50
|
+
|
|
51
|
+
The listing and the handoff addresses. Or `decoded: false` with a reason when the file is not a BASIC program.
|
|
52
|
+
|
|
53
|
+
The exit status is 0 for a listing. The exit status is 1 for `decoded: false` and for an error. The exit status is 2 for a wrong argument.
|
|
54
|
+
|
|
55
|
+
## Failure and conflicts
|
|
56
|
+
|
|
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. A memory dump also gives `decoded: false`: its first two bytes are not a load address, and its bytes go past `$ffff`.
|
|
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`, or `machine-unavailable` with 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.
|
|
60
|
+
- `not-found` or `invalid-input`: the path or an argument is wrong. Read the message and correct the call.
|
|
61
|
+
|
|
62
|
+
## Handoffs
|
|
63
|
+
|
|
64
|
+
- Machine code at the handoff address → c64-static-analysis
|
|
65
|
+
- Run the program to the handoff → c64-emulator
|
|
66
|
+
- Record the entry point → c64-knowledge
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The c64-basic script: decodes a project C64 BASIC V2 program with petcat
|
|
2
|
+
// through the Host Runtime and reports its machine-code handoffs.
|
|
3
|
+
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { formatC64Address } from "#src/c64.ts";
|
|
8
|
+
import { decodeBasic } from "#src/host-client/tools.ts";
|
|
9
|
+
import { resolveProjectPath } from "#src/project.ts";
|
|
10
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
11
|
+
|
|
12
|
+
const USAGE = `basic.ts <program>
|
|
13
|
+
|
|
14
|
+
<program> is a PRG file relative to the project directory. The result is one
|
|
15
|
+
JSON object. Addresses are $ and four hex digits.`;
|
|
16
|
+
|
|
17
|
+
async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
|
|
18
|
+
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { help: { type: "boolean" } } });
|
|
19
|
+
if (values.help) throw new UsageError("");
|
|
20
|
+
if (positionals.length !== 1) throw new UsageError("give one program");
|
|
21
|
+
const program = positionals[0]!;
|
|
22
|
+
const result = await decodeBasic({ program });
|
|
23
|
+
if (!result.decoded) return { output: { decoded: false, reason: result.reason }, failed: true };
|
|
24
|
+
const size = statSync(resolveProjectPath(program)).size - 2;
|
|
25
|
+
const end = result.loadAddress + size - 1;
|
|
26
|
+
return {
|
|
27
|
+
output: {
|
|
28
|
+
decoded: true,
|
|
29
|
+
loadRange: { start: formatC64Address(result.loadAddress), end: formatC64Address(end) },
|
|
30
|
+
// From here to the end of the load range, the file holds no BASIC (often machine code).
|
|
31
|
+
basicEnd: result.basicEnd <= end ? formatC64Address(result.basicEnd) : null,
|
|
32
|
+
listing: result.listing,
|
|
33
|
+
handoffs: result.handoffs.map((handoff) => ("address" in handoff ? { ...handoff, address: formatC64Address(handoff.address) } : handoff)),
|
|
34
|
+
},
|
|
35
|
+
failed: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await runScript(
|
|
40
|
+
async () => {
|
|
41
|
+
const { output, failed } = await run(process.argv.slice(2));
|
|
42
|
+
if (failed) process.exitCode = 1;
|
|
43
|
+
return output;
|
|
44
|
+
},
|
|
45
|
+
{ usage: USAGE },
|
|
46
|
+
);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-disk
|
|
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
|
+
---
|
|
5
|
+
|
|
6
|
+
# c64-disk
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Examine a C64 disk image, a T64 tape file or a CRT cartridge file. Extract a file from it into the project.
|
|
11
|
+
|
|
12
|
+
This skill does not change the disk image and does not change project knowledge.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- The disk image: a `.d64`, `.d71`, `.d81` or `.g64` file, relative to the project directory.
|
|
17
|
+
- For one file: the file name as the directory shows it.
|
|
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.
|
|
20
|
+
|
|
21
|
+
## Tools / execution path
|
|
22
|
+
|
|
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.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Need | Command |
|
|
30
|
+
|---|---|
|
|
31
|
+
| The disk name, the free blocks and the files | `directory original/game.d64` |
|
|
32
|
+
| The free and used sectors of each track, and the used sectors that no file uses | `bam original/game.d64` |
|
|
33
|
+
| The type, size and first block of one file | `entry original/game.d64 GAME` |
|
|
34
|
+
| The track and sector of each block of one file | `chain original/game.d64 GAME` |
|
|
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` |
|
|
40
|
+
|
|
41
|
+
The script prints one JSON object.
|
|
42
|
+
|
|
43
|
+
File names show the characters that the C64 shows in a directory list. A byte that has no character shows as `{$xx}`, for example `{$c1}`. Write the name in the same form. The letter case of a name is not important. Put the name in single quotes in a shell, for example `'GAME {$c1}'`.
|
|
44
|
+
|
|
45
|
+
## Workflow
|
|
46
|
+
|
|
47
|
+
1. Run `directory`. Find the file in `entries`. Each entry has a `name`, a `type`, a size in `blocks` and the `closed` and `locked` flags.
|
|
48
|
+
2. Run `read` with an output path to extract the file. A PRG file keeps its 2-byte load address at the start.
|
|
49
|
+
3. Run `entry` or `chain` only when the disk structure is important, for example for a loader that reads sectors directly.
|
|
50
|
+
4. Run `bam` only to find used sectors that no file uses. `unreferenced` lists them. The list does not include the header, the directory and the BAM blocks. These sectors can hold data that a loader reads directly.
|
|
51
|
+
|
|
52
|
+
## Knowledge
|
|
53
|
+
|
|
54
|
+
Automatic write:
|
|
55
|
+
- none.
|
|
56
|
+
|
|
57
|
+
Do not store:
|
|
58
|
+
- the directory list
|
|
59
|
+
- sector chains.
|
|
60
|
+
|
|
61
|
+
A downstream skill records the meaning of the extracted content.
|
|
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
|
+
|
|
72
|
+
## Result
|
|
73
|
+
|
|
74
|
+
The directory, the BAM, an entry or a sector chain as JSON. Or the extracted file in the project at the output path.
|
|
75
|
+
|
|
76
|
+
The exit status is 0 for a result with the file. The exit status is 1 for `found: false` and for an error. The exit status is 2 for a wrong argument.
|
|
77
|
+
|
|
78
|
+
## Failure and conflicts
|
|
79
|
+
|
|
80
|
+
- `found: false`: the image has no file with that name. This is a normal result. Read the directory again and compare the names.
|
|
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.
|
|
82
|
+
- `installation-incomplete`, or `machine-unavailable` with 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.
|
|
83
|
+
- `invalid-input`: an argument or a path is wrong. Read the message and correct the call.
|
|
84
|
+
|
|
85
|
+
A file that is not `closed` was not written fully. Its data can be incomplete.
|
|
86
|
+
|
|
87
|
+
## Handoffs
|
|
88
|
+
|
|
89
|
+
- BASIC program → c64-basic
|
|
90
|
+
- Packed program → c64-unpacker
|
|
91
|
+
- Machine code, also a cartridge bank → c64-static-analysis
|
|
92
|
+
- A TAP file → c64-unpacker
|
|
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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The c64-disk script: inspects a project disk image with c1541 through the
|
|
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.
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
|
|
9
|
+
import { formatC64Address } from "#src/c64.ts";
|
|
10
|
+
import { inspectDisk } from "#src/host-client/tools.ts";
|
|
11
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
12
|
+
import { resolveProjectPath } from "#src/project.ts";
|
|
13
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
14
|
+
import { ContainerError, parseCrt, parseT64, tapeFile } from "./containers.ts";
|
|
15
|
+
|
|
16
|
+
const USAGE = `disk.ts <action> <image> [name] [--out <file>]
|
|
17
|
+
|
|
18
|
+
directory <image> disk name, id, free blocks and the files
|
|
19
|
+
bam <image> free and used sectors of each track, and the
|
|
20
|
+
used sectors that no file holds
|
|
21
|
+
entry <image> <name> the directory entry of one file
|
|
22
|
+
chain <image> <name> the track and sector of each block of one file
|
|
23
|
+
read <image> <name> --out <file> write the bytes of one file into the project
|
|
24
|
+
crt <file.crt> --out <dir> write each CHIP packet of a cartridge as a PRG
|
|
25
|
+
|
|
26
|
+
<image> is a .d64, .d71, .d81 or .g64 file, relative to the project directory.
|
|
27
|
+
directory and read also take a .t64 tape file, and directory a .crt cartridge
|
|
28
|
+
file; the script reads those itself. A CRT can have hardware type 0 (normal
|
|
29
|
+
8K, 16K or Ultimax), 5 (Ocean), 19 (Magic Desk) or 32 (EasyFlash).
|
|
30
|
+
<name> is the file name as the directory shows it. Write a byte that has no
|
|
31
|
+
character as {$xx}, for example {$c1}. The result is one JSON object.`;
|
|
32
|
+
|
|
33
|
+
const ACTIONS = ["directory", "bam", "entry", "chain", "read", "crt"] as const;
|
|
34
|
+
type Action = (typeof ACTIONS)[number];
|
|
35
|
+
|
|
36
|
+
async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
|
|
37
|
+
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { out: { type: "string" }, help: { type: "boolean" } } });
|
|
38
|
+
if (values.help) throw new UsageError("");
|
|
39
|
+
const [action, image, name, ...extra] = positionals;
|
|
40
|
+
if (!ACTIONS.includes(action as Action)) throw new UsageError(`the action must be one of ${ACTIONS.join(", ")}`);
|
|
41
|
+
if (image === undefined) throw new UsageError("the disk image is necessary");
|
|
42
|
+
const named = action === "entry" || action === "chain" || action === "read";
|
|
43
|
+
if (named && name === undefined) throw new UsageError(`${action} needs a file name`);
|
|
44
|
+
if ((!named && name !== undefined) || extra.length > 0) throw new UsageError("too many arguments");
|
|
45
|
+
if ((action === "read" || action === "crt") && values.out === undefined) throw new UsageError(`${action} needs --out`);
|
|
46
|
+
if (action !== "read" && action !== "crt" && values.out !== undefined) throw new UsageError("only read and crt take --out");
|
|
47
|
+
let outPath: string | undefined;
|
|
48
|
+
if (values.out !== undefined) {
|
|
49
|
+
try {
|
|
50
|
+
outPath = resolveProjectPath(values.out);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const kind = image.toLowerCase().endsWith(".t64") ? "t64" : image.toLowerCase().endsWith(".crt") ? "crt" : "disk";
|
|
57
|
+
if (action === "crt" && kind !== "crt") throw new UsageError("crt needs a .crt file");
|
|
58
|
+
if (kind !== "disk") {
|
|
59
|
+
try {
|
|
60
|
+
return local(kind, action as Action, image, name, values.out);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof ContainerError) throw new UsageError(`${image}: ${error.message}`);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const result = await inspectDisk({ image, action: action as Exclude<Action, "crt">, ...(name === undefined ? {} : { name }) });
|
|
67
|
+
if ("found" in result && !result.found) return { output: { found: false }, failed: true };
|
|
68
|
+
if (result.action === "read" && result.found) {
|
|
69
|
+
mkdirSync(dirname(outPath!), { recursive: true });
|
|
70
|
+
writeFileSync(outPath!, result.data!);
|
|
71
|
+
return { output: { found: true, name: result.name, bytes: result.bytes, output: values.out }, failed: false };
|
|
72
|
+
}
|
|
73
|
+
const { action: _action, ...output } = result;
|
|
74
|
+
return { output, failed: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A tape or cartridge file, read here (D54). */
|
|
78
|
+
function local(kind: "t64" | "crt", action: Action, image: string, name: string | undefined, out: string | undefined): { output: unknown; failed: boolean } {
|
|
79
|
+
const { bytes } = readProjectFile(image);
|
|
80
|
+
if (kind === "t64") {
|
|
81
|
+
if (action !== "directory" && action !== "read") throw new UsageError(`a T64 file has only directory and read, not ${action}`);
|
|
82
|
+
const tape = parseT64(bytes);
|
|
83
|
+
const shown = (entry: (typeof tape.entries)[number]) => ({
|
|
84
|
+
name: entry.name,
|
|
85
|
+
start: formatC64Address(entry.start),
|
|
86
|
+
end: formatC64Address(entry.end),
|
|
87
|
+
bytes: entry.bytes,
|
|
88
|
+
...(entry.fixedSize ? { fixedSize: true } : {}),
|
|
89
|
+
});
|
|
90
|
+
if (action === "directory") return { output: { name: tape.name, entries: tape.entries.map(shown) }, failed: false };
|
|
91
|
+
const entry = tape.entries.find((candidate) => candidate.name.toLowerCase() === name!.toLowerCase());
|
|
92
|
+
if (entry === undefined) return { output: { found: false }, failed: true };
|
|
93
|
+
const file = tapeFile(bytes, entry);
|
|
94
|
+
writeProjectFile(out!, file, { what: "--out" });
|
|
95
|
+
return { output: { found: true, name: entry.name, bytes: file.length, output: out }, failed: false };
|
|
96
|
+
}
|
|
97
|
+
if (action !== "directory" && action !== "crt") throw new UsageError(`a CRT file has only directory and crt, not ${action}`);
|
|
98
|
+
const crt = parseCrt(bytes);
|
|
99
|
+
const chips = crt.chips.map((chip) => ({
|
|
100
|
+
bank: chip.bank,
|
|
101
|
+
address: formatC64Address(chip.address),
|
|
102
|
+
size: chip.size,
|
|
103
|
+
space: `bank${chip.bank}`,
|
|
104
|
+
file: `bank${chip.bank}-${chip.address.toString(16).padStart(4, "0")}.prg`,
|
|
105
|
+
}));
|
|
106
|
+
const output = { name: crt.name, type: crt.type, typeName: crt.typeName, exrom: crt.exrom, game: crt.game, ...(crt.mode === undefined ? {} : { mode: crt.mode }), chips };
|
|
107
|
+
if (action === "directory") return { output: { ...output, chips: chips.map(({ file: _file, ...chip }) => chip) }, failed: false };
|
|
108
|
+
crt.chips.forEach((chip, index) => writeProjectFile(`${out}/${chips[index]!.file}`, Buffer.concat([Buffer.from([chip.address & 0xff, chip.address >> 8]), chip.data]), { what: "--out" }));
|
|
109
|
+
writeProjectFile(`${out}/crt.json`, Buffer.from(`${JSON.stringify(output, null, 2)}\n`), { what: "--out" });
|
|
110
|
+
return { output: { ...output, out }, failed: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await runScript(
|
|
114
|
+
async () => {
|
|
115
|
+
const { output, failed } = await run(process.argv.slice(2));
|
|
116
|
+
if (failed) process.exitCode = 1;
|
|
117
|
+
return output;
|
|
118
|
+
},
|
|
119
|
+
{ usage: USAGE },
|
|
120
|
+
);
|