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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +3 -1
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-basic/SKILL.md +3 -3
  6. package/skills/c64-emulator/SKILL.md +2 -2
  7. package/skills/c64-knowledge/SKILL.md +17 -3
  8. package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
  9. package/skills/c64-memory-map/scripts/memmap.ts +1 -1
  10. package/skills/c64-provenance/SKILL.md +3 -2
  11. package/skills/c64-provenance/scripts/provenance.ts +4 -2
  12. package/skills/c64-provenance/scripts/runtime.ts +14 -2
  13. package/skills/c64-source/SKILL.md +4 -4
  14. package/skills/c64-source/scripts/generate.ts +57 -14
  15. package/skills/c64-source/scripts/loader.ts +5 -1
  16. package/skills/c64-source/scripts/source.ts +3 -1
  17. package/skills/c64-static-analysis/SKILL.md +2 -2
  18. package/skills/c64-static-analysis/scripts/analyze.ts +8 -4
  19. package/skills/c64-unpacker/SKILL.md +5 -5
  20. package/skills/c64-unpacker/scripts/evidence.ts +29 -22
  21. package/skills/c64-unpacker/scripts/loads.ts +30 -7
  22. package/skills/c64-unpacker/scripts/stages.ts +67 -0
  23. package/skills/c64-unpacker/scripts/unpack.ts +167 -47
  24. package/src/c64.ts +37 -12
  25. package/src/cli/main.ts +7 -4
  26. package/src/cli/status.ts +9 -7
  27. package/src/host/main.ts +8 -3
  28. package/src/host/tools/petcat.ts +5 -1
  29. package/src/host/tools/status.ts +8 -2
  30. package/src/host/vice/process.ts +10 -5
  31. package/src/host/vice/session.ts +26 -13
  32. package/src/host-client/connect.ts +2 -1
  33. package/src/host-client/coverage.ts +0 -5
  34. package/src/host-client/vice-session.ts +37 -4
  35. package/src/knowledge/history.ts +8 -0
  36. package/src/knowledge/import.ts +34 -3
  37. package/src/knowledge/write.ts +2 -1
  38. package/src/mcp/main.ts +43 -15
  39. package/src/mcp/server.ts +9 -2
  40. package/src/mcp/tools/execution.ts +3 -2
  41. package/src/mcp/tools/input.ts +6 -4
  42. package/src/mcp/tools/media.ts +4 -3
  43. package/src/native/discover.ts +38 -4
  44. package/src/package.ts +21 -0
  45. package/src/protocol/tools.ts +7 -3
  46. package/src/protocol/vice.ts +14 -6
package/README.md CHANGED
@@ -19,7 +19,7 @@ The design is in [`docs/redesign/`](docs/redesign/README.md), and
19
19
 
20
20
  ## Install
21
21
 
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.
22
+ You need Node.js 24 or newer, and VICE 3.9 or later (`x64sc`, `c1541`, `petcat`) with its ROM files; use the latest VICE release. `status` and the host name the known flaws of an older VICE. 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
24
  Start the Host Runtime and leave it running:
25
25
 
@@ -50,6 +50,8 @@ The agent runs in the container, VICE and the Host Runtime on the host. In `.dev
50
50
  "runArgs": ["--add-host=host.docker.internal:host-gateway"]
51
51
  ```
52
52
 
53
+ Until 2.0.0 is released, the `latest` URL has no Feature. Use the prerelease: `https://github.com/henols/c64-re-tools/releases/download/v2.0.0-rc.4/devcontainer-feature-c64-re-tools.tgz` with `{ "target": "claude", "channel": "next" }`.
54
+
53
55
  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.
54
56
 
55
57
  ### Settings
@@ -2,22 +2,7 @@
2
2
  // only reads package.json, so a caller that needs the declaration does not
3
3
  // build the plugin.
4
4
 
5
- import { readFileSync } from "node:fs";
6
- import { join } from "node:path";
7
-
8
- /**
9
- * The npm dist-tag that a version is published under: a prerelease
10
- * (2.0.0-rc.1) goes to "next", a release to "latest".
11
- */
12
- export function channelOf(version: string): "latest" | "next" {
13
- return version.includes("-") ? "next" : "latest";
14
- }
15
-
16
- /** The name, version and dist-tag of this npm package, from its package.json. */
17
- export const PACKAGE = (() => {
18
- const metadata = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { name: string; version: string };
19
- return { name: metadata.name, version: metadata.version, tag: channelOf(metadata.version) };
20
- })();
5
+ import { PACKAGE } from "../src/package.ts";
21
6
 
22
7
  /**
23
8
  * The agent starts the MCP server through npx with the newest version of the
@@ -13,7 +13,8 @@ import { dirname, join, relative, resolve, sep } from "node:path";
13
13
 
14
14
  import { definePlugin, defineSkill, type Skill } from "@jalco/ap-sdk";
15
15
 
16
- import { MCP_SERVER, PACKAGE } from "./package.ts";
16
+ import { PACKAGE } from "../src/package.ts";
17
+ import { MCP_SERVER } from "./package.ts";
17
18
 
18
19
  const root = resolve(import.meta.dirname, "..");
19
20
  const SOURCE = join(root, "src");
@@ -27,9 +28,10 @@ const SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|\bnew URL\(\s*)(["'])((?:\.\.?
27
28
 
28
29
  /**
29
30
  * The package.json of an installed skill: ES modules, #src/* resolved inside
30
- * the skill, and the version of c64-re-tools that the copy comes from.
31
+ * the skill, and the name and version of c64-re-tools that the copy comes
32
+ * from, which src/package.ts reads.
31
33
  */
32
- export const SKILL_PACKAGE = `${JSON.stringify({ type: "module", version: PACKAGE.version, imports: { "#src/*": "./src/*" } }, null, 2)}\n`;
34
+ export const SKILL_PACKAGE = `${JSON.stringify({ name: PACKAGE.name, type: "module", version: PACKAGE.version, imports: { "#src/*": "./src/*" } }, null, 2)}\n`;
33
35
 
34
36
  /** The scripts that a SKILL.md runs, by file name. */
35
37
  export function scriptsOf(skillMarkdown: string): string[] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.4",
3
+ "version": "2.0.0-rc.5",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
@@ -30,9 +30,9 @@ The script prints one JSON object. Addresses are `$` and four hex digits.
30
30
  1. Run the script on the program.
31
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
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.
34
- - A handoff with `computed: true`: the address comes from a calculation at run time. 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.
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
36
 
37
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
38
 
@@ -70,7 +70,7 @@ Step, next and until-return work only in space `c64`. The emulator cannot stop t
70
70
 
71
71
  `c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this. The emulator has no command that presses one key of the keyboard matrix. For such a program:
72
72
 
73
- - A wait for any key that reads all keyboard columns at one time can end without a key. Joystick port 1 uses the same lines as the keyboard.
73
+ - VICE 3.10 and older have a known flaw: bits 5-7 of `$DC00` and `$DC01` are always 0. Then `emulatorFlaw` in the result of `c64_keyboard` and `c64_joystick` gives the flaw. With this flaw, a wait for any key that reads all keyboard columns at one time ends without a key. A wait for one specific key, or a compare of the full byte of a joystick port, never ends. Tell the user about the flaw. The next VICE release and the VICE nightly builds do not have it. Joystick port 1 uses the same lines as the keyboard.
74
74
  - For one specific key, for example F1 or RUN/STOP, open the window and ask the user to press the key.
75
75
  - Do not change the program to make it accept a key.
76
76
 
@@ -114,7 +114,7 @@ If you cannot get the machine into the necessary state, say which state you coul
114
114
 
115
115
  - `machine-state-lost` and the message says that the VICE window closed: the window closed before `c64_window` action `close`. The emulator has no window again. The machine state is the same as at the time when the window opened. The breakpoints and the watchpoints stay. Do not use results from the window time. Read the machine state again, then continue.
116
116
  - `machine-unavailable` and the message says that the VICE window closed: the session starts the emulator again. Wait a few seconds. Then do the operation again.
117
- - `machine-state-lost` with a different message: the emulator stopped and its state is gone. Do not continue with results from before. Tell the user to restart the MCP server.
117
+ - `machine-state-lost` with a different message: the connection to the host runtime was lost, and the machine state is gone. Do not continue with results from before. The next call starts a new machine when the host runtime runs. Load the program again, and set the breakpoints and the watchpoints again. If the next call gives `machine-unavailable` with a message that the host runtime does not run, obey the rule for that message.
118
118
  - `machine-unavailable` and the message names a pause or a dialog in the emulator window: this occurs only while the window is open. Do not send more operations. Tell the user to resume the emulator (Pause, Alt+P) or to close the dialog in the VICE window. Then do the operation again. The machine state does not change.
119
119
  - `machine-running`: stop the CPU first, then try again.
120
120
  - `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. Do not try to repair the installation.
@@ -76,7 +76,7 @@ Region types:
76
76
  Read:
77
77
 
78
78
  1. Use `at` for the addresses in the question before other work.
79
- 2. Use `history` when the current knowledge is surprising or disputed.
79
+ 2. Use `history` when the current knowledge is surprising or disputed. Each row gives the `reason` and the `evidence` of the revision that wrote it.
80
80
 
81
81
  Write:
82
82
 
@@ -97,11 +97,25 @@ For more than a few writes, put them in one JSON file and use `apply`. The file
97
97
  ]
98
98
  ```
99
99
 
100
- The ops are `rename`, `remove-symbol`, `classify`, `unclassify`, `comment`, `uncomment`, `reference`, `unreference`, `module` and `unmodule`. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
100
+ The ops and their fields:
101
+
102
+ | op | Fields |
103
+ |---|---|
104
+ | `rename` | `address`, `name`, `kind` (optional) |
105
+ | `remove-symbol` | `address` |
106
+ | `classify` | `start`, `end`, `type`, `element` (optional) |
107
+ | `unclassify` | `start`, `end` |
108
+ | `comment` | `address`, `placement` (`line` or `side`), `text` |
109
+ | `uncomment` | `address`, `placement` |
110
+ | `reference`, `unreference` | `from`, `to`, `kind` |
111
+ | `module` | `start`, `file`, `title` (optional) |
112
+ | `unmodule` | `start` |
113
+
114
+ A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
101
115
 
102
116
  ## Modules
103
117
 
104
- A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. Modules have a history, and `revert` restores them.
118
+ A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
105
119
 
106
120
  ## Removals
107
121
 
@@ -9,7 +9,7 @@ import { parseArgs } from "node:util";
9
9
  import { formatC64Address, parseC64Address } from "#src/c64.ts";
10
10
  import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
11
11
  import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
12
- import { historyAt, historyNamed, revision, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
12
+ import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
13
13
  import {
14
14
  at,
15
15
  currentRevision,
@@ -281,7 +281,10 @@ function run(argv: string[]): unknown {
281
281
  case "spaces":
282
282
  return { spaces: listSpaces(db) };
283
283
  case "history":
284
- return { history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map(historyEntry) };
284
+ // Each row with the reason and the evidence of the revision that wrote it (F-26).
285
+ return {
286
+ history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
287
+ };
285
288
  case "revisions": {
286
289
  const options: { limit?: number; before?: number } = {};
287
290
  if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
@@ -79,7 +79,7 @@ function run(argv: string[]): unknown {
79
79
  case "export": {
80
80
  if (rest.length !== 0 || values.out === undefined) throw new UsageError("export takes --out <file> and nothing else");
81
81
  const names = platformNames();
82
- const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${hex(item.address)}`.padEnd(24) + `; ${item.meaning}`)];
82
+ const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${hex(item.address, item.address < 0x100 ? 2 : 4)}`.padEnd(24) + `; ${item.meaning}`)];
83
83
  const bytes = Buffer.from(`${lines.join("\n")}\n`, "utf8");
84
84
  writeProjectFile(values.out, bytes, { what: "--out" });
85
85
  return { out: values.out, names: names.length, bytes: bytes.length };
@@ -48,10 +48,11 @@ With `--run`, a PRG starts with autostart and RUN. An image starts from its byte
48
48
  2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with `--shift`.
49
49
  3. Read `differences`. Each one has an address range, the bytes of each release, and `groups`: the releases that have the same bytes there.
50
50
  4. To know how the program uses the differences, run the script again with `--run` and the frames that get the program to the part you examine. Read `runtime` of each difference:
51
- - `first`: the first access in that release: `read`, `write`, `execute` or `none`. An instruction that reads and then writes the same byte, for example INC, counts as `read`.
51
+ - `first`: the first access in that release: `read`, `write`, `execute` or `none`. An instruction that reads and then writes the same byte, for example INC, counts as `read`. The operand bytes of an executed instruction count as `execute`, for example the value of `LDA #$03`.
52
52
  - `changed`: `true` when the bytes at the end of the run are not the bytes of the release.
53
53
  - A range that each release writes before it reads is probably runtime state, not a difference of the releases.
54
54
  - A range with `none` in each release is not used in this run. Run more frames or other steps before you decide.
55
+ - `run.skipped`: the releases have no differences, so no release ran.
55
56
  5. Examine each important difference:
56
57
  - Disassemble the bytes of each release with the c64-emulator skill, or analyze them with the c64-static-analysis skill.
57
58
  - Find what the change does, for example a trainer, a removed copy protection, or a changed text.
@@ -62,7 +63,7 @@ With `--run`, a PRG starts with autostart and RUN. An image starts from its byte
62
63
 
63
64
  Use `separate` when you have only one release. It gives evidence, not a decision:
64
65
 
65
- - `ranges`: the image in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. In `image.json`, the last stage is the program. An earlier stage is a depacker, an intro or a loader.
66
+ - `ranges`: the image in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. In `image.json`, the last stage is the program. An earlier stage is a depacker, an intro or a loader. Only an image from c64-unpacker `capture` has stages. For an image from `loads`, `noStages` tells this.
66
67
  - `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs.
67
68
  - `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
68
69
  - `outside`: RAM outside the image that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write.
@@ -252,7 +252,9 @@ async function run(argv: string[]): Promise<unknown> {
252
252
  })),
253
253
  ...(comparison.moreDifferences ? { moreDifferences: true } : {}),
254
254
  ...(suggestions.length > 0 ? { suggestedShifts: suggestions } : {}),
255
- ...(frames === undefined ? {} : { run: { frames, steps: steps.length, releases: runs } }),
255
+ ...(frames === undefined
256
+ ? {}
257
+ : { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
256
258
  };
257
259
  }
258
260
 
@@ -293,7 +295,7 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
293
295
  release: path,
294
296
  ...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
295
297
  ...(lastStage === undefined
296
- ? {}
298
+ ? { noStages: "image.json has no stage log. Only a capture of c64-unpacker writes one. A capture of loads holds one part of a program and has none." }
297
299
  : {
298
300
  stages: lastStage + 1,
299
301
  bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
@@ -4,7 +4,7 @@
4
4
  // Watchpoints and range breakpoints stop the run at an access; the coverage
5
5
  // map then tells which range and which access it was.
6
6
 
7
- import { COVERAGE, formatC64Address } from "#src/c64.ts";
7
+ import { COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
8
8
  import { runProgram } from "#src/host-client/autostart.ts";
9
9
  import { readCoverage } from "#src/host-client/coverage.ts";
10
10
  import { runActionStep, type ActionStep } from "#src/host-client/steps.ts";
@@ -108,7 +108,12 @@ export async function watchRelease(
108
108
  }
109
109
  await client.memmap({ action: "clear" });
110
110
 
111
- /** After a stop: the ranges that the coverage shows accessed get their first access, and lose their points. */
111
+ /**
112
+ * After a stop: the ranges that the coverage shows accessed get their first
113
+ * access, and lose their points. VICE marks only the opcode byte of an
114
+ * executed instruction, so an instruction that starts up to two bytes before
115
+ * a range, or in it, executes its operand bytes too (F-27).
116
+ */
112
117
  const settle = async (): Promise<boolean> => {
113
118
  const map = await readCoverage(client);
114
119
  let found = false;
@@ -116,6 +121,13 @@ export async function watchRelease(
116
121
  const range = ranges[index]!;
117
122
  let flags = 0;
118
123
  for (let address = range.start; address <= range.end; address++) flags |= map[address]!;
124
+ const from = Math.max(0, range.start - 2);
125
+ if ((flags & EXECUTE) === 0 && map.subarray(from, range.start).some((byte) => (byte & COVERAGE.ramExecute) !== 0)) {
126
+ const code = Buffer.from((await client.memoryRead({ address: from, size: range.start - from, space: "c64", view: "ram" })).data, "hex");
127
+ for (let address = from; address < range.start; address++) {
128
+ if ((map[address]! & COVERAGE.ramExecute) !== 0 && address + instructionLength(code[address - from]!) - 1 >= range.start) flags |= COVERAGE.ramExecute;
129
+ }
130
+ }
119
131
  if (accessOf(flags) === "none") continue;
120
132
  first[index] = accessOf(flags);
121
133
  for (const point of ids) {
@@ -74,7 +74,7 @@ Do not edit the generated files. The next run writes over them.
74
74
 
75
75
  The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the VIC, SID and CIA registers. They stay as the KERNAL set them.
76
76
 
77
- The script simulates the copies first. Then it runs `loader.prg` in the emulator to the entry. It compares the memory, $00/$01 and the registers with the entry state. `loader.verified: true` tells that all of them are equal.
77
+ The script simulates the copies first. Then it runs `loader.prg` in the emulator to its jump to the entry. It compares the memory, $00/$01 and the registers with the entry state there. `loader.verified: true` tells that all of them are equal. An interrupt can run right at the entry, as in the original program.
78
78
 
79
79
  ## Knowledge
80
80
 
@@ -98,7 +98,7 @@ One JSON object:
98
98
  - `unusedModules`: modules that start outside the image.
99
99
  - `notExecuted`: with `--coverage`, the count of instructions that no run executed, and their ranges. Put these ranges in the report.
100
100
  - `check`: `identical`, and the result for each segment. A different segment gives its first different address.
101
- - `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`.
101
+ - `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`. When the script cannot make the loader, `loader` has `verified: false` and a `reason`. The loader includes the source. Thus a source that does not assemble to the image also stops the loader.
102
102
 
103
103
  The exit status is 0 when the source is identical and the emulator check of the loader passes. The exit status is 1 for a failed check and for an error. The exit status is 2 for a wrong argument.
104
104
 
@@ -107,8 +107,8 @@ The exit status is 0 when the source is identical and the emulator check of the
107
107
  - `check.diagnostics`: ACME did not assemble the source. A name in the knowledge can be wrong for ACME. Read the diagnostics and change the name with c64-knowledge.
108
108
  - `check.identical: false`: the script has a defect. Give the result to the user.
109
109
  - `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
110
- - `invalid-input` for the loader: no copy order or no free pages exist. The message gives the ranges. Capture the image again with fewer segments, or write the loader by hand.
111
- - `loader.verified: false`: `differences` gives the bytes and registers that are not equal.
110
+ - `loader.verified: false` with a `reason`: the script did not make the loader. The script still writes and checks the source. When no copy order or no free pages exist, or the loader needs too many copies, the reason gives the ranges. Capture the image again with fewer segments, or write the loader by hand.
111
+ - `loader.verified: false` with `differences`: the loader ran, and `differences` gives the bytes and registers that are not equal.
112
112
  - `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 disassembler or emulator script.
113
113
 
114
114
  ## Handoffs
@@ -5,7 +5,8 @@
5
5
  // checks it in the emulator. Exit status: 0 when every check passes, 1 when
6
6
  // one fails, 2 for a bad call.
7
7
 
8
- import { posix } from "node:path";
8
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
9
+ import { join, posix } from "node:path";
9
10
  import { parseArgs } from "node:util";
10
11
 
11
12
  import { formatC64Address, parseC64Address, prgRange, textToPetscii } from "#src/c64.ts";
@@ -20,11 +21,11 @@ import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowle
20
21
  import { assemble } from "#src/native/acme.ts";
21
22
  import { analyze } from "#src/native/ghidra/analyze.ts";
22
23
  import { localToolContext } from "#src/native/local.ts";
23
- import { GHIDRA_LIMITS, type Diagnostic } from "#src/native/types.ts";
24
+ import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
24
25
  import { platformNames } from "#src/platform.ts";
25
26
  import { resolveProjectPath } from "#src/project.ts";
26
27
  import { runScript, UsageError } from "#src/script.ts";
27
- import { planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
28
+ import { LOADER_JUMP, planLoader, readEntryState, simulate, type EntryState, type LoaderPlan } from "./loader.ts";
28
29
  import { OPCODES } from "./opcodes.ts";
29
30
  import { instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
30
31
 
@@ -148,7 +149,7 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
148
149
  }
149
150
 
150
151
  /** Assembles files given as text with ACME; the program or the diagnostics. */
151
- async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; diagnostics: Diagnostic[] }> {
152
+ async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
152
153
  const contents = files.map((file) => Buffer.from(file.text, "utf8"));
153
154
  const { result, attachments } = await assemble(
154
155
  { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines: {} },
@@ -156,7 +157,7 @@ async function assembleFiles(files: Array<{ name: string; text: string }>, entry
156
157
  localToolContext(),
157
158
  );
158
159
  if (!result.assembled) return { diagnostics: result.diagnostics };
159
- return { program: attachments![0]!, start: result.loadRange!.start, diagnostics: result.diagnostics };
160
+ return { program: attachments![0]!, start: result.loadRange!.start, ...(result.symbols === undefined ? {} : { symbols: result.symbols }), diagnostics: result.diagnostics };
160
161
  }
161
162
 
162
163
  /**
@@ -238,7 +239,13 @@ async function readRam(client: ViceSessionClient, start: number, size: number):
238
239
  }
239
240
 
240
241
  /** Runs the loader in the emulator to the entry and compares memory, $00/$01 and the registers with the entry state. */
241
- async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: Image, state: EntryState): Promise<{ verified: boolean; differences: string[] }> {
242
+ /**
243
+ * Runs the loader in the emulator to its jump to the entry and compares the
244
+ * machine with the entry state there. At the jump, every byte and register is
245
+ * set and no interrupt has run since the PLP: the 6502 takes one only after
246
+ * the JMP, as the original program can take one right at its entry (F-32).
247
+ */
248
+ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: Image, state: EntryState, jump: number): Promise<{ verified: boolean; differences: string[] }> {
242
249
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
243
250
  try {
244
251
  await client.reset({ mode: "hard", run: false });
@@ -246,9 +253,12 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
246
253
  await client.keyboard(textToPetscii("RUN\n"));
247
254
  const copier = await client.runUntil({ target: { kind: "address", address: plan.copier, space: "c64" }, timeoutFrames: RUN_LIMIT_FRAMES });
248
255
  if (!copier.reached) return { verified: false, differences: [`The loader did not get to its copier at ${formatC64Address(plan.copier)}; it stopped at ${formatC64Address(copier.pc)}.`] };
249
- const entry = await client.runUntil({ target: { kind: "address", address: state.pc, space: "c64" }, timeoutFrames: LOADER_FRAMES });
250
- if (!entry.reached) return { verified: false, differences: [`The loader did not get to the entry ${formatC64Address(state.pc)}; it stopped at ${formatC64Address(entry.pc)}.`] };
256
+ const atJump = await client.runUntil({ target: { kind: "address", address: jump, space: "c64" }, timeoutFrames: LOADER_FRAMES });
257
+ if (!atJump.reached) return { verified: false, differences: [`The loader did not get to its jump to the entry at ${formatC64Address(jump)}; it stopped at ${formatC64Address(atJump.pc)}.`] };
251
258
  const memory = await readRam(client, 0x0000, 0x10000);
259
+ if (memory[jump] !== 0x4c || (memory[jump + 1]! | (memory[jump + 2]! << 8)) !== state.pc) {
260
+ return { verified: false, differences: [`The instruction at ${formatC64Address(jump)} is not JMP ${formatC64Address(state.pc)}.`] };
261
+ }
252
262
  const port = Buffer.from((await client.memoryRead({ address: 0x0000, size: 2, space: "c64", view: "cpu" })).data, "hex");
253
263
  const registers = await client.registersGet("c64");
254
264
  const differences: string[] = [];
@@ -272,6 +282,22 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
272
282
  }
273
283
  }
274
284
 
285
+ /** The first words of every file that the script writes. */
286
+ const GENERATED_MARK = "; Generated by c64-source";
287
+
288
+ /** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg; other files stay. */
289
+ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
290
+ const directory = resolveProjectPath(out);
291
+ if (!existsSync(directory)) return;
292
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
293
+ if (!entry.isFile()) continue;
294
+ const name = entry.name;
295
+ const path = join(directory, name);
296
+ if (name === "loader.prg") rmSync(path, { force: true });
297
+ else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
298
+ }
299
+ }
300
+
275
301
  async function run(argv: string[]): Promise<{ output: Record<string, unknown>; failed: boolean }> {
276
302
  const { values, positionals } = parseArgs({
277
303
  args: argv,
@@ -325,11 +351,21 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
325
351
  const rendered = render(laid, asBytes, header);
326
352
  const files = [...rendered.files];
327
353
  let loader: LoaderPlan | undefined;
354
+ // A loader that cannot be planned does not stop the source: the source is written and checked, and the result says why.
355
+ let loaderRefused: string | undefined;
328
356
  if (state !== undefined) {
329
357
  const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
330
- loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
331
- files.push({ name: "loader.asm", text: loader.text });
358
+ try {
359
+ loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
360
+ files.push({ name: "loader.asm", text: loader.text });
361
+ } catch (error) {
362
+ if (!(error instanceof WireFailure)) throw error;
363
+ loaderRefused = error.message;
364
+ }
332
365
  }
366
+ // Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again (F-25).
367
+ for (const [index, file] of files.entries()) if (!file.text.startsWith(GENERATED_MARK)) files[index] = { ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` };
368
+ removeOldFiles(values.out, new Set(files.map((file) => file.name)));
333
369
  for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
334
370
 
335
371
  const output: Record<string, unknown> = {
@@ -354,16 +390,20 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
354
390
  }),
355
391
  ...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
356
392
  };
393
+ // The loader includes the source, so it is built only from a source that is identical to the image.
394
+ const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
357
395
  const built = await assembleFiles(files, "main.asm");
358
396
  if (built.program === undefined) {
359
- return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
397
+ return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
360
398
  }
361
399
  const checked = check(built.program, built.start!, image);
362
400
  output.check = checked;
363
- if (!checked.identical || loader === undefined || state === undefined) return { output, failed: !checked.identical };
401
+ if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
402
+ if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
403
+ if (loader === undefined || state === undefined) return { output, failed: false };
364
404
 
365
405
  const loaderBuilt = await assembleFiles(files, "loader.asm");
366
- if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
406
+ if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
367
407
  const program = loaderBuilt.program;
368
408
  // The simulation: the loader in memory as it loads, then every copy in its order, byte by byte.
369
409
  const memory = new Int16Array(0x10000).fill(-1);
@@ -376,12 +416,15 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
376
416
  if (loaderBuilt.start !== 0x0801 || loaderBuilt.start + program.length - 3 !== loader.end || wrongPacking !== undefined) {
377
417
  throw new WireFailure("operation-failed", `The assembled loader does not have the planned layout${wrongPacking === undefined ? "" : ` (${wrongPacking.what})`}. This is a defect of the c64-source script.`);
378
418
  }
419
+ const jumpSymbol = loaderBuilt.symbols?.find((symbol) => symbol.name === LOADER_JUMP);
420
+ if (jumpSymbol === undefined) throw new WireFailure("operation-failed", `The assembled loader has no ${LOADER_JUMP} label. This is a defect of the c64-source script.`);
421
+ const jump = jumpSymbol.kind === "address" ? jumpSymbol.address : jumpSymbol.value;
379
422
  const wrong = simulate(memory, loader.copies, expected);
380
423
  if (wrong !== undefined) throw new WireFailure("operation-failed", `The simulated loader writes a wrong byte at ${formatC64Address(wrong)}. This is a defect of the c64-source script.`);
381
424
  const loaderPath = `${values.out}/loader.prg`;
382
425
  writeProjectFile(loaderPath, program, { what: "--out" });
383
426
  (output.files as string[]).push("loader.prg");
384
- const ran = await runLoader(loaderPath, program, loader, image, state);
427
+ const ran = await runLoader(loaderPath, program, loader, image, state, jump);
385
428
  output.loader = {
386
429
  program: loaderPath,
387
430
  copies: loader.copies.map((copy) => ({ what: copy.what, to: `${formatC64Address(copy.target.start)}-${formatC64Address(copy.target.end)}` })),
@@ -88,6 +88,9 @@ export interface LoaderPlan {
88
88
  text: string;
89
89
  }
90
90
 
91
+ /** The label of the loader's jump to the entry, in ACME's symbol list. */
92
+ export const LOADER_JUMP = "c64_source_loader_jump";
93
+
91
94
  const LOAD = 0x0801;
92
95
  const STUB = 0x080d;
93
96
  /** sei, ldx, the 2-page move loop and jmp. */
@@ -295,7 +298,8 @@ export function planLoader(input: LoaderInput): LoaderPlan {
295
298
  ` ldx #${hex2(state.x)}`,
296
299
  ` ldy #${hex2(state.y)}`,
297
300
  " plp",
298
- ` jmp ${hex4(input.entry)}`,
301
+ // A global label: the check of the loader stops here, before an interrupt can run (F-32).
302
+ `${LOADER_JUMP} jmp ${hex4(input.entry)}`,
299
303
  ".count !word 0",
300
304
  ".index !byte 0",
301
305
  ".back !byte 0",
@@ -93,6 +93,8 @@ export interface Layout {
93
93
 
94
94
  const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
95
95
  const hex4 = (value: number) => formatC64Address(value);
96
+ /** ACME reads leading zeros as a 16-bit value, so a zero-page name gets two digits. */
97
+ const constantValue = (address: number) => (address < 0x100 ? hex2(address) : hex4(address));
96
98
  const signed = (byte: number) => (byte < 0x80 ? byte : byte - 0x100);
97
99
 
98
100
  function operandValue(address: number, opcode: number, byteAt: (address: number) => number): number | undefined {
@@ -545,7 +547,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
545
547
  result.files.push({ name: piece.file, text: `${out.join("\n")}\n` });
546
548
  }
547
549
 
548
- const constants = ["; Names outside the image.", ...layout.constants.map((address) => `${layout.names.get(address)} = ${hex4(address)}`)];
550
+ const constants = ["; Names outside the image.", ...layout.constants.map((address) => `${layout.names.get(address)} = ${constantValue(address)}`)];
549
551
  result.files.push({ name: "constants.asm", text: `${constants.join("\n")}\n` });
550
552
  const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"'];
551
553
  for (const piece of layout.pieces) {
@@ -66,7 +66,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
66
66
  - If a list is not complete, `rest` gives the c64-knowledge command that lists all of them.
67
67
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
68
68
  - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding and opcodes that the program wrote and then executed. The script does not write those. Look at them and decide.
69
- - With `--coverage`, `coverageConflicts` lists the analyzer data regions where the coverage shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
69
+ - With `--coverage`, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
70
70
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
71
71
 
72
72
  The script records only structure: generated names, code and data regions, and references from Ghidra. The listing and the decompiled code are only for the current task. The script does not record them in the knowledge.
@@ -78,7 +78,7 @@ Automatic write:
78
78
  - names that DXA generated (labels) and routine entry points with names that Ghidra generated
79
79
  - references from Ghidra: call, jump, read and write.
80
80
 
81
- The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other.
81
+ The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other, with one exception. With `--coverage`, a code finding replaces a DXA or Ghidra data region or data name where a run executed an instruction start. The revision keeps the coverage files as evidence. Thus run DXA first, then Ghidra with all coverage files.
82
82
 
83
83
  A `heuristic` region replaces the part of a DXA or Ghidra region where it lies, because an idiom is specific evidence about those bytes. DXA and Ghidra never replace a `heuristic` region. If they find code there, the result shows a conflict. A heuristic table can be too long or too short, because the script cannot know its length. Correct it with the c64-knowledge skill. Your region replaces it, and a removal holds.
84
84
 
@@ -3,11 +3,12 @@
3
3
  // current knowledge, and imports the structural findings into
4
4
  // .c64-re-tools/knowledge.db.
5
5
 
6
+ import { createHash } from "node:crypto";
6
7
  import { mkdirSync, writeFileSync } from "node:fs";
7
8
  import { dirname } from "node:path";
8
9
  import { parseArgs } from "node:util";
9
10
 
10
- import { formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
11
+ import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
11
12
  import { WireFailure } from "#src/host-client/tools.ts";
12
13
  import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
13
14
  import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
@@ -93,7 +94,7 @@ function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<stri
93
94
  * changed since the seeds were read. A refused import creates no knowledge.db.
94
95
  * The revision records the image hash and the analyzer version.
95
96
  */
96
- function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion">) {
97
+ function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
97
98
  let imported: ImportResult;
98
99
  try {
99
100
  imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
@@ -159,6 +160,9 @@ async function run(argv: string[]): Promise<unknown> {
159
160
  const bytes = readProjectFile(image).bytes;
160
161
  const coverage = readCoverageFiles(values.coverage ?? []);
161
162
  const byteAt = imageReader(bytes, imageKind);
163
+ // With coverage, a code finding over an executed instruction start replaces the data of the other analyzer (D58), and the files are the evidence.
164
+ const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
165
+ const coverageEvidence = coverage === undefined ? {} : { evidence: (values.coverage ?? []).map((path) => ({ kind: "coverage" as const, ref: path, sha256: createHash("sha256").update(readProjectFile(path).bytes).digest("hex") })) };
162
166
 
163
167
  let loaded: { start: number; end: number };
164
168
  try {
@@ -212,7 +216,7 @@ async function run(argv: string[]): Promise<unknown> {
212
216
  const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
213
217
  const listing = attachments[0]!.toString("utf8");
214
218
  // The listing is written only after the import: a refused import leaves no new file.
215
- const imported = record({ ...dxaFindings(result), space }, revision, `DXA analysis of ${image}`, runProvenance(bytes, result.toolVersion));
219
+ const imported = record({ ...dxaFindings(result), space, ...executed }, revision, `DXA analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
216
220
  if (listingPath !== undefined) {
217
221
  try {
218
222
  mkdirSync(dirname(listingPath), { recursive: true });
@@ -237,7 +241,7 @@ async function run(argv: string[]): Promise<unknown> {
237
241
  }
238
242
 
239
243
  const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
240
- const imported = record({ ...ghidraFindings(result), space }, revision, `Ghidra analysis of ${image}`, runProvenance(bytes, result.toolVersion));
244
+ const imported = record({ ...ghidraFindings(result), space, ...executed }, revision, `Ghidra analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
241
245
  const found = idiomsResult();
242
246
  let instructions: { out: string; count: number } | undefined;
243
247
  if (values.instructions !== undefined) {
@@ -35,13 +35,13 @@ node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out
35
35
  | `--range '$c000' '$cfff'` | Write this range as a PRG. Without it, the script writes all 64 KiB of RAM as a flat file. |
36
36
  | `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
37
37
  | `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
38
- | `--entry '$c000'` | `trace` only: start the program at this address. Without it, `trace` types RUN. A program without a BASIC start line needs it. |
38
+ | `--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. |
39
39
  | `--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. |
40
40
  | `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
41
41
  | `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
42
- | `--min-stage 64` | `stages` only: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
42
+ | `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
43
43
 
44
- The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object.
44
+ The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object. The script refuses an option that the command does not use.
45
45
 
46
46
  A steps file has the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of the c64-testing skill. Addresses are `$` and four hex digits. For example, wait 50 frames and press the space bar:
47
47
 
@@ -53,7 +53,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
53
53
 
54
54
  ## Workflow
55
55
 
56
- 1. Run `inspect`. Read `basicStart` and `packing`:
56
+ 1. Run `inspect`. Read `basicStart` and `packing`. `basicStart` gives the first SYS line and its address. When the SYS argument needs values at run time, `sys` is null and `expression` gives the argument. Then `trace`, `stages` and `capture` stop at the first instruction after the BASIC text:
57
57
  - `likely`: most of the bytes look compressed. Packed data has this property.
58
58
  - `unlikely`: the bytes look like code and data. A simple cruncher can also give this result.
59
59
  - `unclear`: the file is short, or the blocks do not give clear evidence. Or the code at the SYS address decodes or moves bytes outside the program.
@@ -64,7 +64,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
64
64
  - `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
65
65
  - `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
66
66
  2. Run `trace`. It gives the evidence from the run. Read `packing`:
67
- - `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the code that runs now. Its `start` is usually the entry point of the unpacked program.
67
+ - `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 usually the entry point of the unpacked program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
68
68
  - `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
69
69
  - `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.
70
70
  3. Find the address where the unpacked program starts: