@henols/c64-re-tools 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +7 -3
- package/skills/c64-assembler/scripts/assembler.ts +29 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +3 -2
- package/skills/c64-disk/scripts/disk.ts +26 -12
- package/skills/c64-disk/scripts/lib/containers.ts +10 -11
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +5 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +9 -2
- package/skills/c64-knowledge/SKILL.md +9 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +2 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
- package/skills/c64-provenance/SKILL.md +6 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +14 -10
- package/skills/c64-source/SKILL.md +3 -0
- package/skills/c64-source/scripts/lib/source.ts +9 -31
- package/skills/c64-source/scripts/source.ts +14 -10
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
- package/skills/c64-testing/SKILL.md +14 -3
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +50 -15
- package/skills/c64-testing/scripts/lib/replay.ts +12 -1
- package/skills/c64-testing/scripts/lib/runs.ts +84 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +20 -7
- package/skills/c64-unpacker/SKILL.md +17 -10
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +148 -13
- package/src/host-client/image.ts +40 -8
- package/src/host-client/steps.ts +145 -9
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +33 -18
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +74 -4
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +32 -18
- package/src/knowledge/read.ts +46 -0
- package/src/knowledge/schema.ts +4 -2
- package/src/knowledge/text.ts +2 -0
- package/src/knowledge/write.ts +58 -16
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +20 -4
- package/src/mcp/tools/debug.ts +36 -9
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +11 -6
- package/src/mcp/tools/video.ts +5 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +111 -3
- package/src/protocol/vice.ts +8 -2
- package/src/script.ts +55 -3
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// checklist gives the Markdown list for a human tester.
|
|
6
6
|
|
|
7
7
|
import { COVERAGE } from "#src/c64.ts";
|
|
8
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
8
9
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
10
|
+
import { readPrg } from "#src/host-client/image.ts";
|
|
9
11
|
import { writeReported } from "#src/host-client/transfer.ts";
|
|
10
12
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
11
13
|
import { C64RunSession, PAL_FRAME_CYCLES } from "#src/native/c64run.ts";
|
|
@@ -18,7 +20,7 @@ import { play } from "./lib/play.ts";
|
|
|
18
20
|
import { readRecordFile, record, RECORDING_FILES } from "./lib/recording.ts";
|
|
19
21
|
import { replay } from "./lib/replay.ts";
|
|
20
22
|
import { runFile } from "./lib/runs.ts";
|
|
21
|
-
import { readScenario, runScenario, type Scenario, type ScenarioResult } from "./lib/scenario.ts";
|
|
23
|
+
import { namesOf, readScenario, runScenario, scenarioNames, type Scenario, type ScenarioResult } from "./lib/scenario.ts";
|
|
22
24
|
|
|
23
25
|
/** The default port of the --play page: next to the Host Runtime's 6464. */
|
|
24
26
|
const DEFAULT_HTTP_PORT = 6465;
|
|
@@ -35,6 +37,8 @@ const ALWAYS = [
|
|
|
35
37
|
async function test(call: Call): Promise<ScenarioResult> {
|
|
36
38
|
const scenario = readScenario(call.subject(0));
|
|
37
39
|
const out = call.string("out");
|
|
40
|
+
// Every file of the scenario is checked before the emulator starts.
|
|
41
|
+
const names = scenarioNames(scenario);
|
|
38
42
|
let client: ViceSessionClient;
|
|
39
43
|
try {
|
|
40
44
|
client = await ViceSessionClient.open({ videoStandard: scenario.videoStandard ?? "pal" });
|
|
@@ -43,7 +47,7 @@ async function test(call: Call): Promise<ScenarioResult> {
|
|
|
43
47
|
return { result: "INCONCLUSIVE", scenario: scenario.name, reason: `no emulator: ${error.message}`, checkpoints: [], differences: [] };
|
|
44
48
|
}
|
|
45
49
|
try {
|
|
46
|
-
return await runScenario(client, scenario, out === undefined ? {} : { out });
|
|
50
|
+
return await runScenario(client, scenario, { names, ...(out === undefined ? {} : { out }) });
|
|
47
51
|
} finally {
|
|
48
52
|
await client.close();
|
|
49
53
|
}
|
|
@@ -56,6 +60,15 @@ async function recordCommand(call: Call): Promise<Record<string, unknown>> {
|
|
|
56
60
|
const httpPort = call.count("http-port") ?? DEFAULT_HTTP_PORT;
|
|
57
61
|
const out = call.string("out")!;
|
|
58
62
|
const file = readRecordFile(call.subject(0));
|
|
63
|
+
// The program and the symbols are checked before the replay core starts.
|
|
64
|
+
for (const [field, check] of [["program", () => readPrg(file.program)], ["symbols", () => namesOf(file.symbols)]] as const) {
|
|
65
|
+
try {
|
|
66
|
+
check();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (isRefusal(error)) throw new Refusal(error.code, `record file ${call.subject(0)}: ${field}: ${error.message}`);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
59
72
|
const context = localToolContext();
|
|
60
73
|
const core = await C64RunSession.open(context);
|
|
61
74
|
const workspace = Workspace.create(context.supervisor, "c64-re-tools-record-");
|
|
@@ -110,14 +123,14 @@ const COMMANDS: CommandSpec[] = [
|
|
|
110
123
|
name: "test",
|
|
111
124
|
summary: "Runs a scenario on the original and then on the rebuild in one emulator. result is PASS, FAIL with the differences, or INCONCLUSIVE with the reason.",
|
|
112
125
|
subjects: [{ name: "<scenario.json>", help: "a scenario file, relative to the project folder. See references/scenario-format.md" }],
|
|
113
|
-
options: { out: { ...OPTIONS.out, value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json with each differing byte into this folder" } },
|
|
126
|
+
options: { out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json with each differing byte into this folder" } },
|
|
114
127
|
run: test,
|
|
115
128
|
},
|
|
116
129
|
{
|
|
117
130
|
name: "run",
|
|
118
131
|
summary: "Runs the steps of a run file in its own emulator and writes what they ask for into --out: screens, coverage maps, snapshot files, traces, and results.json with the observations. result is FAIL when a step did not run.",
|
|
119
132
|
subjects: [{ name: "<run.json>", help: "a run file, relative to the project folder. See references/scenario-format.md, section Runs" }],
|
|
120
|
-
options: { out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the files of the steps and results.json" } },
|
|
133
|
+
options: { out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the files of the steps and results.json" } },
|
|
121
134
|
run: async (call) => {
|
|
122
135
|
const { output, failed } = await runFile(call.subject(0), call.string("out")!);
|
|
123
136
|
return { result: failed ? "FAIL" : "PASS", ...output };
|
|
@@ -128,7 +141,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
128
141
|
summary: "Plays the steps of a record file on the original in the replay core (c64run), and writes the session as a recording folder: recording.json, reads.txt and coverage.bin. result is FAIL when the session did not complete.",
|
|
129
142
|
subjects: [{ name: "<record.json>", help: "a record file, relative to the project folder. See references/recording-format.md" }],
|
|
130
143
|
options: {
|
|
131
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the recording folder, for example tests/recordings/<name>" },
|
|
144
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the recording folder, for example tests/recordings/<name>" },
|
|
132
145
|
play: { kind: "flag", help: "a person plays after the steps: the script prints the address of a page on stderr. The page shows the start screen frozen until Start (Enter). End (F12) saves the recording, Cancel (F11) ends without one" },
|
|
133
146
|
joystick: { kind: "count", value: "<1|2>", min: 1, max: 2, help: "with --play: the joystick port that the program reads, at the start (default 2). F9 on the page changes it" },
|
|
134
147
|
"http-port": { kind: "count", value: "<n>", min: 0, max: 65535, help: `with --play: the port of the page (default ${DEFAULT_HTTP_PORT})` },
|
|
@@ -145,7 +158,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
145
158
|
pin: { kind: "text", value: "<item>", multiple: true, help: `random state in RAM, in the form of --state: at each checkpoint the rebuild gets the original's values there` },
|
|
146
159
|
rebuild: { kind: "path", value: "<prg>", help: "the rebuild to compare with the original" },
|
|
147
160
|
symbols: { ...OPTIONS.symbols, help: "the names of the rebuild: the c64-assembler output (default: the symbols of the recording)" },
|
|
148
|
-
out: { ...OPTIONS.out, value: "<dir>", help: "with a FAIL, write the RAM of both sides at the first different checkpoint into this folder" },
|
|
161
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write the RAM of both sides at the first different checkpoint into this folder" },
|
|
149
162
|
},
|
|
150
163
|
notes: "A recording without names takes the names of the knowledge for the original. A $ address is an address of the original: on the rebuild it moves with the name before it.",
|
|
151
164
|
run: (call) => {
|
|
@@ -165,7 +178,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
165
178
|
"max-listed": OPTIONS["max-listed"],
|
|
166
179
|
ntsc: { kind: "flag", help: "the snapshots are of an NTSC machine (default PAL)" },
|
|
167
180
|
address: { kind: "address", value: "<address>", help: "the address of the first byte of a memory file that is shorter than 64 KiB" },
|
|
168
|
-
out: { ...OPTIONS.out, value: "<file>", help: "also write all ranges to this file" },
|
|
181
|
+
out: { ...OPTIONS.out, output: "file", value: "<file>", help: "also write all ranges to this file" },
|
|
169
182
|
},
|
|
170
183
|
run: (call) => {
|
|
171
184
|
const [address, out] = [call.address("address"), call.string("out")];
|
|
@@ -13,7 +13,7 @@ This skill does not change project knowledge.
|
|
|
13
13
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
16
|
-
- The program: a PRG file, relative to the project folder.
|
|
16
|
+
- The program: a PRG file, relative to the project folder. `inspect`, `trace`, `stages` and `capture` refuse a disk, tape or cartridge image. For such an image, use `loads`, or get the PRG with `disk.ts read` of the c64-disk skill.
|
|
17
17
|
- For a capture: the address where the unpacked program starts, and the directory for the result.
|
|
18
18
|
|
|
19
19
|
## Tools / execution path
|
|
@@ -26,25 +26,26 @@ node <skill>/scripts/unpacker.ts capture original/game.prg --until '$c000' --out
|
|
|
26
26
|
node <skill>/scripts/unpacker.ts loads original/game.d64 --steps .c64-re-tools/local/parts.json --out .c64-re-tools/local/parts --drive
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`inspect` examines the file locally. `trace`, `stages`, `capture` and `loads` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). `capture` stops the program when the CPU gets to the `--until` address. In `stages` and `capture`, a `frames` step and a `runUntil` step to an address without a condition count as run time. `stages` refuses a different `runUntil` step.
|
|
29
|
+
`inspect` examines the file locally. `trace`, `stages`, `capture` and `loads` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). `wrote` lists each block that the stage wrote, also a small block. `capture` stops the program when the CPU gets to the `--until` address. In `stages` and `capture`, a `frames` step and a `runUntil` step to an address without a condition count as run time. `stages` refuses a different `runUntil` step.
|
|
30
30
|
|
|
31
31
|
| Option | Meaning |
|
|
32
32
|
|---|---|
|
|
33
33
|
| `--until '$c000'` | `capture`: the address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
|
|
34
34
|
| `--out .c64-re-tools/local/capture` | `capture` and `loads`: the folder for the result, relative to the project folder. |
|
|
35
|
-
| `--range '$c000-$cfff'` | `capture` only: write this range as the one segment. Without it, the segments are the blocks that the stages wrote. |
|
|
36
|
-
| `--exclude '$a000-$a9ff'` | `capture` only: remove this range from the segments. You can give it more than once. `image.json` lists the ranges in `excluded`. The
|
|
35
|
+
| `--range '$c000-$cfff'` | `capture` only: write this range as the one segment. Without it, the segments are the blocks that the stages wrote. The range must hold the `--until` address. |
|
|
36
|
+
| `--exclude '$a000-$a9ff'` | `capture` only: remove this range from the segments. You can give it more than once. `image.json` lists the ranges in `excluded`. The range must not hold the `--until` address. |
|
|
37
37
|
| `--timeout-frames 3000` | `capture` only: how many frames the program can run before the capture stops (default 3000). |
|
|
38
38
|
| `--frames 3000` | `trace` and `stages`: how many frames the program runs (default 3000). |
|
|
39
39
|
| `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
|
|
40
40
|
| `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. |
|
|
41
|
+
| `--max-listed 50` | `trace` only: the most ranges that `writtenThenExecuted` lists (default 50). |
|
|
41
42
|
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
42
43
|
| `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
|
|
43
|
-
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
44
|
+
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). It does not change `wrote` or the segments. |
|
|
44
45
|
|
|
45
|
-
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script refuses an option that the command does not use.
|
|
46
|
+
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script refuses an option that the command does not use. It refuses a wrong `--range` or `--exclude` before the emulator starts.
|
|
46
47
|
|
|
47
|
-
A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. For example, wait 50 frames and type a space:
|
|
48
|
+
A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. The script refuses an unknown name before the emulator starts. The message gives the file and the step. For example, wait 50 frames and type a space:
|
|
48
49
|
|
|
49
50
|
```json
|
|
50
51
|
[{ "frames": 50 }, { "type": { "mode": "text", "text": " " } }]
|
|
@@ -59,6 +60,8 @@ A steps file is a JSON list of steps. Each step has the input of the MCP tool of
|
|
|
59
60
|
|
|
60
61
|
A program that reads the keyboard matrix does not see the keys of a `type` step. These scripts refuse a `press` step. A wait for any key that reads all keyboard columns can end without a key.
|
|
61
62
|
|
|
63
|
+
The scripts refuse a step in the form of version 2.0, for example `{ "runUntil": { "at": "main_loop" }, "timeoutFrames": 300 }`. The message names the old field and gives the step to write.
|
|
64
|
+
|
|
62
65
|
### Calling the script
|
|
63
66
|
|
|
64
67
|
- Call a script as `node <skill>/scripts/<script>.ts <command> [subjects] [options]`. The command comes first.
|
|
@@ -66,6 +69,7 @@ A program that reads the keyboard matrix does not see the keys of a `type` step.
|
|
|
66
69
|
- An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
|
|
67
70
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
68
71
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
72
|
+
- The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
|
|
69
73
|
- The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
|
|
70
74
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
71
75
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -92,7 +96,7 @@ A program that reads the keyboard matrix does not see the keys of a `type` step.
|
|
|
92
96
|
- `strings`: readable text near the start of the code.
|
|
93
97
|
- `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
|
|
94
98
|
- `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
|
|
95
|
-
2. Run `trace`. It gives the evidence from the run. Read `packing`:
|
|
99
|
+
2. Run `trace`. It gives the evidence from the run. `writtenThenExecuted` lists the ranges that the program wrote and then executed. Read `packing`:
|
|
96
100
|
- `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is code that runs after the unpacking. It is not always the entry point of the program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
|
|
97
101
|
- `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
|
|
98
102
|
- `unlikely`: the program executed no code that it wrote. If the program waits for a key or a joystick, it can move code after that input. Then use the c64-emulator skill.
|
|
@@ -104,7 +108,7 @@ A program that reads the keyboard matrix does not see the keys of a `type` step.
|
|
|
104
108
|
- A program can be relocated and not packed. Then a short copier moves the code and jumps to it. The moved code is the last stage. Make sure that the `entry` of the last stage is not code that the program writes later. An example is a routine that the program makes at run time. Disassemble the entry to make sure.
|
|
105
109
|
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
106
110
|
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
107
|
-
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
111
|
+
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote, also a small block. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. A block that a stage before the program wrote in such a block is also not a segment, for example a variable of a loader. A block that is fully in pages 0 to 3 is not a segment, because `entry_state.json` holds these pages. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
108
112
|
- One PRG for each segment.
|
|
109
113
|
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
110
114
|
5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it. Then capture again with `--exclude` for that range. Thus the source and the loader do not include the range.
|
|
@@ -147,11 +151,14 @@ The packing evidence, the stages, or the unpacked program with its image, segmen
|
|
|
147
151
|
|
|
148
152
|
`trace`, `stages`, `capture` and `loads` give `result: "PASS"` or `result: "FAIL"`. A FAIL has `reason`. `capture` gives the written files in `files`, each as `{out, bytes, sha256}`.
|
|
149
153
|
|
|
150
|
-
|
|
154
|
+
`capture` gives `differsFromReset`: the count of bytes that are not as a clean C64 has them at READY.
|
|
155
|
+
|
|
156
|
+
For `loads`: `captures`, with the name, the PC, the segments, `differsFromReset`, the files and the drive file of each capture. Each `entry_state.json` of `loads` has `differsFromReset` as the one of `capture` has. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
|
|
151
157
|
|
|
152
158
|
## Failure and conflicts
|
|
153
159
|
|
|
154
160
|
- `traced: false` or `followed: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
|
|
161
|
+
- `traced: false` or `followed: false` with `stopReason`: the CPU stopped before the frames ran, for example at a JAM instruction. `pc` gives the address. Make sure that `--entry` is the start of the code that the file holds at that time, and that the steps are correct.
|
|
155
162
|
- `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a `type` step.
|
|
156
163
|
- A step did not reach its target: the reason names the step. Change the step or give it more frames.
|
|
157
164
|
- `loaded: false` and no capture has a segment: the program did not load a part, or it did not start. `keyWait: true` on a capture tells that the C64 waited at READY. or at an INPUT statement. For a tape image, read `emulatorFlaw`. A known VICE flaw can stop a tape load. Tell the user to install the latest VICE.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Local evidence of packing for the c64-unpacker script: where BASIC hands
|
|
2
2
|
// over and how compressed the bytes look. It never names a packer.
|
|
3
3
|
|
|
4
|
-
import { basicLineText, findHandoffs, formatC64Address, instructionLength, parseTokenized } from "#src/c64.ts";
|
|
4
|
+
import { basicLineText, findHandoffs, formatC64Address, instructionLength, parseTokenized, prgRange } from "#src/c64.ts";
|
|
5
5
|
import { Refusal } from "#src/errors.ts";
|
|
6
6
|
|
|
7
7
|
/** A 256-byte block above this many bits per byte looks compressed. */
|
|
@@ -111,9 +111,8 @@ function startSignals(program: Uint8Array, load: number, end: number, sys: numbe
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function inspect(program: Uint8Array): PackingEvidence {
|
|
114
|
-
|
|
115
|
-
const load
|
|
116
|
-
const end = load + program.length - 3;
|
|
114
|
+
// A PRG ends at $ffff or before: a longer file (a disk image) is refused.
|
|
115
|
+
const { start: load, end } = prgRange(program);
|
|
117
116
|
const basic = basicStart(program);
|
|
118
117
|
const stub = basic?.sys;
|
|
119
118
|
// Without BASIC, all bytes count; with it, only the bytes after the BASIC text.
|
|
@@ -132,7 +131,7 @@ export function inspect(program: Uint8Array): PackingEvidence {
|
|
|
132
131
|
const packing = byEntropy === "unlikely" && startMoves ? "unclear" : byEntropy;
|
|
133
132
|
const sorted = [...blocks].sort((a, b) => a - b);
|
|
134
133
|
return {
|
|
135
|
-
loadRange: { start: formatC64Address(load), end: formatC64Address(
|
|
134
|
+
loadRange: { start: formatC64Address(load), end: formatC64Address(end) },
|
|
136
135
|
bytes: program.length - 2,
|
|
137
136
|
basicStart: stub === undefined ? null : stub.address === undefined ? { line: stub.line, sys: null, expression: stub.expression! } : { line: stub.line, sys: formatC64Address(stub.address) },
|
|
138
137
|
entropy: {
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
8
8
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
|
-
import { ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkStepNames, isObject, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
11
11
|
import { knowledgeAt } from "#src/knowledge/names.ts";
|
|
12
12
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
13
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
14
14
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
15
15
|
import { UsageError } from "#src/script.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { entryStateFile, json, machineState, readRam, readyState, vectorsOf } from "./machine.ts";
|
|
17
|
+
import { FIRST_SEGMENT_ADDRESS } from "./stages.ts";
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
export type LoadStep = ActionStep | { capture: string };
|
|
@@ -22,8 +23,6 @@ export type LoadStep = ActionStep | { capture: string };
|
|
|
22
23
|
const NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
23
24
|
/** The drive RAM of a 1541. */
|
|
24
25
|
const DRIVE_RAM = 0x0800;
|
|
25
|
-
/** Pages 0-3 belong to the entry state, not to the segments. */
|
|
26
|
-
const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
27
26
|
/** The KERNAL LOAD entry in the jump table. */
|
|
28
27
|
const KERNAL_LOAD = 0xffd5;
|
|
29
28
|
/** The KERNAL loop that waits for a key at READY. or at an INPUT statement. */
|
|
@@ -54,9 +53,13 @@ export function readLoadSteps(path: string): LoadStep[] {
|
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
56
55
|
const kinds = Object.keys(step).filter((key) => (ACTION_STEP_KINDS as readonly string[]).includes(key));
|
|
57
|
-
if (kinds.length
|
|
56
|
+
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
57
|
+
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...VICE_STEP_KINDS, "capture"].join(", ")}`);
|
|
58
58
|
// The step with its defaults filled in, as the shared schema of the action steps gives it.
|
|
59
|
-
|
|
59
|
+
const checked = checkActionStep(kinds[0] as ActionStepKind, step, at, fail);
|
|
60
|
+
// Its names resolve now: an unknown name stops the call before the emulator starts.
|
|
61
|
+
checkStepNames([checked], knowledgeAt(), `--steps ${path}`, index + 1);
|
|
62
|
+
(value as unknown[])[index] = checked;
|
|
60
63
|
});
|
|
61
64
|
if (names.size === 0) fail("the steps have no capture step");
|
|
62
65
|
return value as LoadStep[];
|
|
@@ -96,6 +99,8 @@ const hex4 = (value: number) => value.toString(16).padStart(4, "0");
|
|
|
96
99
|
export async function loads(program: string, options: { out: string; steps: LoadStep[]; drive: boolean; minBlock: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
97
100
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
98
101
|
try {
|
|
102
|
+
// A clean machine at the READY prompt shows what the program changed at each capture. The run goes on after a capture, so it is read first.
|
|
103
|
+
const clean = await readyState(client);
|
|
99
104
|
await client.reset({ mode: "hard", run: false });
|
|
100
105
|
let previous: Buffer;
|
|
101
106
|
/** A PRG that autostart put into memory without the CPU: its bytes count as written in the first capture. */
|
|
@@ -183,7 +188,8 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
183
188
|
...(drive === undefined ? {} : { drive }),
|
|
184
189
|
}),
|
|
185
190
|
);
|
|
186
|
-
|
|
191
|
+
const entry = entryStateFile(registers, state, vectors, clean);
|
|
192
|
+
write("entry_state.json", entry.bytes);
|
|
187
193
|
const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
|
|
188
194
|
captures.push({
|
|
189
195
|
capture: step.capture,
|
|
@@ -192,6 +198,7 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
192
198
|
pc: formatC64Address(registers.pc),
|
|
193
199
|
...(keyWait ? { keyWait: true } : {}),
|
|
194
200
|
segments: segments.map(showRange),
|
|
201
|
+
differsFromReset: entry.differsFromReset,
|
|
195
202
|
...(small === 0 ? {} : { smallChanges: small }),
|
|
196
203
|
...(drive === undefined ? {} : { drive: drive.file }),
|
|
197
204
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// capture and loads commands of the unpacker share it.
|
|
4
4
|
|
|
5
5
|
import { formatC64Address } from "#src/c64.ts";
|
|
6
|
+
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
6
7
|
import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
7
8
|
|
|
8
9
|
type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
|
|
@@ -61,8 +62,34 @@ export function vectorsOf(state: MachineState, memory: Buffer) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
/** The
|
|
65
|
-
export function
|
|
65
|
+
/** The state of a clean machine at the READY prompt after a hard reset: what differsFromReset compares with. It resets the machine. */
|
|
66
|
+
export async function readyState(client: ViceSessionClient): Promise<MachineState> {
|
|
67
|
+
await resetToReady(client);
|
|
68
|
+
return machineState(client);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The bytes that differ from a clean machine: the CPU port, pages 0-3 from $0002, and the I/O areas. */
|
|
72
|
+
export function differsFrom(state: MachineState, clean: MachineState): Array<{ address: string; entry: number; reset: number }> {
|
|
73
|
+
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
74
|
+
const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
|
|
75
|
+
compare(0x0000, state.port, clean.port);
|
|
76
|
+
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
77
|
+
for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
|
|
78
|
+
return differs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* entry_state.json, the one writer of capture and loads: the state at the
|
|
83
|
+
* entry, and what differs from `clean` (readyState). Gives the file and the
|
|
84
|
+
* count of differing bytes.
|
|
85
|
+
*/
|
|
86
|
+
export function entryStateFile(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, clean: MachineState): { bytes: Buffer; differsFromReset: number } {
|
|
87
|
+
const differs = differsFrom(state, clean);
|
|
88
|
+
return { bytes: json(entryState(registers, state, vectors, differs)), differsFromReset: differs.length };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The content of entry_state.json. */
|
|
92
|
+
function entryState(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, differsFromReset: Array<{ address: string; entry: number; reset: number }>) {
|
|
66
93
|
return {
|
|
67
94
|
registers: { pc: formatC64Address(registers.pc), a: registers.a, x: registers.x, y: registers.y, sp: registers.sp, flags: registers.flags },
|
|
68
95
|
cpuPort: { $00: state.port[0], $01: state.port[1] },
|
|
@@ -71,7 +98,7 @@ export function entryState(registers: Registers, state: MachineState, vectors: R
|
|
|
71
98
|
// The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them.
|
|
72
99
|
cia: state.cia,
|
|
73
100
|
vectors,
|
|
74
|
-
|
|
101
|
+
differsFromReset,
|
|
75
102
|
};
|
|
76
103
|
}
|
|
77
104
|
|
|
@@ -93,25 +93,33 @@ export function executedIn(map: Uint8Array, block: AddressRange): AddressRange[]
|
|
|
93
93
|
return ranges;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Pages 0-3 ($0000-$03ff) are in entry_state.json: a written block that is all in them is no segment. */
|
|
97
|
+
export const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
98
|
+
|
|
96
99
|
/**
|
|
97
100
|
* The segments of a capture: the blocks that the stages wrote, without loader
|
|
98
|
-
* leftovers
|
|
101
|
+
* leftovers and without blocks that are all in pages 0-3 (the entry state
|
|
102
|
+
* holds them), joined into ranges that do not overlap or touch. A block
|
|
99
103
|
* is a leftover when it holds the entry of an intermediate stage that started
|
|
100
104
|
* after the block's writer, and no write from that stage on replaced the code
|
|
101
105
|
* at that entry. Code that a stage ran before a later stage wrote over
|
|
102
106
|
* it never drops a block, and the block that holds the capture entry is always
|
|
103
|
-
* a segment.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
107
|
+
* a segment. A block that an intermediate stage wrote inside a leftover, for
|
|
108
|
+
* example a variable of the loader, is a leftover too. The filter comes before
|
|
109
|
+
* the join: a join first would drop the program with a leftover. `writtenBy` is
|
|
110
|
+
* the last stage that wrote into the range.
|
|
106
111
|
*/
|
|
107
112
|
export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly AddressRange[] }>, entry: number): Array<AddressRange & { writtenBy: number }> {
|
|
108
113
|
const last = stages.length - 1;
|
|
109
114
|
const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
|
|
110
115
|
const leftover = (block: AddressRange, writer: number) =>
|
|
111
116
|
!contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
const written = stages.flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })));
|
|
118
|
+
const leftovers = written.filter((block) => leftover(block, block.writtenBy));
|
|
119
|
+
const inLeftover = (block: AddressRange & { writtenBy: number }) =>
|
|
120
|
+
block.writtenBy < last && !contains(block, entry) && leftovers.some((left) => block.start >= left.start && block.end <= left.end);
|
|
121
|
+
const blocks = written
|
|
122
|
+
.filter((block) => !leftover(block, block.writtenBy) && !inLeftover(block) && (block.end >= FIRST_SEGMENT_ADDRESS || contains(block, entry)))
|
|
115
123
|
.sort((a, b) => a.start - b.start);
|
|
116
124
|
const segments: Array<AddressRange & { writtenBy: number }> = [];
|
|
117
125
|
for (const block of blocks) {
|