@henols/c64-re-tools 2.0.0-rc.13 → 2.0.0-rc.15
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 -2
- package/skills/c64-assembler/scripts/assemble.ts +13 -5
- package/skills/c64-disk/SKILL.md +1 -1
- package/skills/c64-disk/scripts/disk.ts +1 -1
- package/skills/c64-emulator/SKILL.md +8 -4
- package/skills/c64-memory-map/SKILL.md +2 -2
- package/skills/c64-provenance/SKILL.md +1 -1
- package/skills/c64-provenance/scripts/provenance.ts +4 -4
- package/skills/c64-reverse-engineering/SKILL.md +2 -1
- package/skills/c64-source/SKILL.md +1 -1
- package/skills/c64-source/scripts/generate.ts +3 -3
- package/skills/c64-static-analysis/SKILL.md +4 -3
- package/skills/c64-static-analysis/scripts/analyze.ts +16 -10
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +20 -0
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +14 -3
- package/skills/c64-testing/SKILL.md +3 -2
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/checklist.ts +1 -1
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +11 -9
- package/skills/c64-testing/scripts/run.ts +4 -4
- package/skills/c64-testing/scripts/test.ts +1 -1
- package/skills/c64-unpacker/SKILL.md +4 -1
- package/skills/c64-unpacker/scripts/unpack.ts +4 -4
- package/src/c64.ts +10 -0
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +2 -1
- package/src/cli/status.ts +69 -2
- package/src/host/main.ts +14 -1
- package/src/host/vice/session.ts +71 -15
- package/src/host-client/steps.ts +7 -2
- package/src/host-client/vice-session.ts +5 -3
- package/src/mcp/server.ts +1 -0
- package/src/mcp/tools/debug.ts +22 -7
- package/src/mcp/tools/execution.ts +17 -3
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +1 -0
- package/src/mcp/tools/memory.ts +7 -2
- package/src/mcp/tools/video.ts +3 -4
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/protocol/vice.ts +32 -6
- /package/skills/c64-assembler/scripts/{compare.ts → lib/compare.ts} +0 -0
- /package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +0 -0
- /package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +0 -0
- /package/skills/c64-provenance/scripts/{library.ts → lib/library.ts} +0 -0
- /package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +0 -0
- /package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +0 -0
- /package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +0 -0
- /package/skills/c64-source/scripts/{opcodes.ts → lib/opcodes.ts} +0 -0
- /package/skills/c64-source/scripts/{source.ts → lib/source.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{macros.ts → lib/macros.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{pointers.ts → lib/pointers.ts} +0 -0
- /package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +0 -0
- /package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +0 -0
- /package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +0 -0
- /package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/c64-re-tools",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.15",
|
|
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",
|
|
@@ -32,10 +32,12 @@ node <skill>/scripts/assemble.ts --source-root src --entry main.a --out build/ga
|
|
|
32
32
|
| `--define DEBUG=1` | An assembler symbol: an integer, also as `$` hex, or `true` or `false`. Use it again for more symbols. |
|
|
33
33
|
| `--set-pc '$0801'` | The start address when the source does not set one. |
|
|
34
34
|
| `--compare unpacked/image.json` | Check the program byte by byte against a reference. The reference is the `image.json` of the c64-unpacker skill, a PRG, or 64 KiB of memory. |
|
|
35
|
+
| `--plain` | Write only the bytes of the program, without the 2-byte load address of a PRG. `loadRange` still tells where the bytes go. |
|
|
36
|
+
| `--all-symbols` | List all symbols. Without it, the list does not include the constants that the source does not use. `unusedConstants` gives their count. |
|
|
35
37
|
|
|
36
38
|
The script copies the full source root. Put all files that the program includes under it. Version control directories stay out.
|
|
37
39
|
|
|
38
|
-
The script prints one JSON object. The target CPU is the 6510
|
|
40
|
+
The script prints one JSON object. The target CPU is the 6510. The output is a C64 PRG, or the bytes only with `--plain`.
|
|
39
41
|
|
|
40
42
|
### Project folder
|
|
41
43
|
|
|
@@ -48,7 +50,10 @@ The script prints one JSON object. The target CPU is the 6510 and the output is
|
|
|
48
50
|
|
|
49
51
|
1. Find the source root and the entry source. The entry source sets its own start address with `* = $0801` or similar.
|
|
50
52
|
2. Run the script.
|
|
51
|
-
3. If `assembled` is `true`, the PRG is at `output`. `loadRange` tells where it loads. `symbols` gives each symbol
|
|
53
|
+
3. If `assembled` is `true`, the PRG is at `output`. `loadRange` tells where it loads. `symbols` gives each symbol:
|
|
54
|
+
- Kind `address` has a value in `loadRange`. It gives the `address`.
|
|
55
|
+
- Kind `constant` has a value outside `loadRange`. It gives the `value` as `$` and four or more hex digits. A label in a `!pseudopc` block can be a constant, because its value is the run address, not the load address.
|
|
56
|
+
- `used` is `false` when the source does not use the symbol.
|
|
52
57
|
4. If `assembled` is `false`, read `diagnostics`. Each one has a severity, a file, a line and a message. Correct the source and run the script again.
|
|
53
58
|
5. To run the program, give the PRG path to the c64-emulator skill.
|
|
54
59
|
|
|
@@ -12,7 +12,7 @@ import { localToolContext } from "#src/native/local.ts";
|
|
|
12
12
|
import type { AcmeParams } from "#src/native/types.ts";
|
|
13
13
|
import { resolveProjectPath } from "#src/project.ts";
|
|
14
14
|
import { runScript, UsageError } from "#src/script.ts";
|
|
15
|
-
import { compareBuild } from "./compare.ts";
|
|
15
|
+
import { compareBuild } from "./lib/compare.ts";
|
|
16
16
|
|
|
17
17
|
const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [options]
|
|
18
18
|
|
|
@@ -24,8 +24,10 @@ const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [
|
|
|
24
24
|
--set-pc <address> start address when the source sets none, for example '$0801'
|
|
25
25
|
--compare <file> check the program byte by byte against an image.json of the
|
|
26
26
|
c64-unpacker skill (each segment), a PRG, or 64 KiB of memory
|
|
27
|
+
--plain write the bytes only, without the 2-byte load address of a PRG
|
|
28
|
+
--all-symbols list also the constants that the source does not use
|
|
27
29
|
|
|
28
|
-
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
30
|
+
The result is one JSON object. Addresses and symbol values are $ and at least four hex digits.`;
|
|
29
31
|
|
|
30
32
|
function parseDefine(text: string): [string, number | boolean] {
|
|
31
33
|
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/.exec(text);
|
|
@@ -49,6 +51,8 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
49
51
|
define: { type: "string", multiple: true },
|
|
50
52
|
"set-pc": { type: "string" },
|
|
51
53
|
compare: { type: "string" },
|
|
54
|
+
plain: { type: "boolean" },
|
|
55
|
+
"all-symbols": { type: "boolean" },
|
|
52
56
|
help: { type: "boolean" },
|
|
53
57
|
},
|
|
54
58
|
});
|
|
@@ -79,19 +83,23 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
79
83
|
const { result, attachments } = await assemble(params, tree.contents, localToolContext());
|
|
80
84
|
if (!result.assembled) return { output: { assembled: false, diagnostics: result.diagnostics }, failed: true };
|
|
81
85
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
82
|
-
writeFileSync(outPath, attachments![0]!);
|
|
86
|
+
writeFileSync(outPath, values.plain ? attachments![0]!.subarray(2) : attachments![0]!);
|
|
83
87
|
const range = result.loadRange!;
|
|
84
88
|
const compared = values.compare === undefined ? undefined : compareBuild(attachments![0]!, values.compare);
|
|
89
|
+
// An include file of platform names gives many constants that the program does not use.
|
|
90
|
+
const listed = result.symbols!.filter((symbol) => values["all-symbols"] || symbol.kind === "address" || symbol.used);
|
|
85
91
|
return {
|
|
86
92
|
output: {
|
|
87
93
|
assembled: true,
|
|
88
94
|
output: out,
|
|
95
|
+
...(values.plain ? { plain: true } : {}),
|
|
89
96
|
loadRange: { start: formatC64Address(range.start), end: formatC64Address(range.end), bytes: range.bytes },
|
|
90
|
-
symbols:
|
|
97
|
+
symbols: listed.map((symbol) =>
|
|
91
98
|
symbol.kind === "address"
|
|
92
99
|
? { name: symbol.name, kind: "address", address: formatC64Address(symbol.address), used: symbol.used }
|
|
93
|
-
: { name: symbol.name, kind: "constant", value: symbol.value, used: symbol.used },
|
|
100
|
+
: { name: symbol.name, kind: "constant", value: `$${symbol.value.toString(16).padStart(4, "0")}`, used: symbol.used },
|
|
94
101
|
),
|
|
102
|
+
...(values["all-symbols"] ? {} : { unusedConstants: result.symbols!.length - listed.length }),
|
|
95
103
|
diagnostics: result.diagnostics,
|
|
96
104
|
...(compared === undefined ? {} : { compare: compared }),
|
|
97
105
|
},
|
package/skills/c64-disk/SKILL.md
CHANGED
|
@@ -40,7 +40,7 @@ node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
|
|
|
40
40
|
|
|
41
41
|
The script prints one JSON object.
|
|
42
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}'`.
|
|
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}'`. A name that starts with `-` needs `--` before it, after all options, for example `read d.d64 --out x.prg -- '-----'`.
|
|
44
44
|
|
|
45
45
|
### Project folder
|
|
46
46
|
|
|
@@ -11,7 +11,7 @@ import { inspectDisk } from "#src/host-client/tools.ts";
|
|
|
11
11
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
12
12
|
import { resolveProjectPath } from "#src/project.ts";
|
|
13
13
|
import { runScript, UsageError } from "#src/script.ts";
|
|
14
|
-
import { ContainerError, parseCrt, parseT64, tapeFile } from "./containers.ts";
|
|
14
|
+
import { ContainerError, parseCrt, parseT64, tapeFile } from "./lib/containers.ts";
|
|
15
15
|
|
|
16
16
|
const USAGE = `disk.ts <action> <image> [name] [--out <file>]
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ Use only the MCP tools. Each MCP session has its own C64. Nothing else can chang
|
|
|
25
25
|
|
|
26
26
|
| Need | Tool |
|
|
27
27
|
|---|---|
|
|
28
|
-
| Run state, video standard, warp mode, window | `c64_status` |
|
|
28
|
+
| Run state, video standard, warp mode, window, version of the MCP server | `c64_status` |
|
|
29
29
|
| Reset, power cycle | `c64_reset` |
|
|
30
30
|
| Fast emulation | `c64_warp` |
|
|
31
31
|
| Show the C64 to the user in a window, hide it again | `c64_window` |
|
|
@@ -48,10 +48,15 @@ Use only the MCP tools. Each MCP session has its own C64. Nothing else can chang
|
|
|
48
48
|
|
|
49
49
|
Each memory and register tool has a `space`. Use `c64` for the computer. Use `drive8` for the CPU of the 1541 disk drive. The memory `view` is `cpu` or `ram`. With `cpu`, you see what the CPU sees now, with ROM and I/O where the memory configuration puts them. With `ram`, you see the RAM under them.
|
|
50
50
|
|
|
51
|
-
For a large result, give `out` with a path in the project. `c64_memory_read`, `c64_disassemble`, `c64_cpu_history` and `c64_screen` then write the result to that file. The answer gives only the file, the number of bytes and a SHA-256. Use it for a memory dump of more than a few hundred bytes, a long listing, or a screen that the user wants to keep.
|
|
51
|
+
For a large result, give `out` with a path in the project. `c64_memory_read`, `c64_disassemble`, `c64_cpu_history` and `c64_screen` then write the result to that file. The answer gives only the file, the number of bytes and a SHA-256. Use it for a memory dump of more than a few hundred bytes, a long listing, or a screen that the user wants to keep. `c64_memmap` action `read` and `c64_snapshot` action `save` also take `out`.
|
|
52
52
|
|
|
53
53
|
`c64_memmap` keeps RAM, ROM and I/O apart. Use action `summary` to see which code ran. Use action `read` with `out` to keep a coverage run as a file, for example `coverage/attract.bin`. Clear the map before each run that you want to keep apart.
|
|
54
54
|
|
|
55
|
+
- Only the first byte of an instruction gets execute. Thus `read` without `out` gives a new range at each instruction of code. For code, use `summary`, or `read` with `out`.
|
|
56
|
+
- A read on a byte right after executed code, or at a return address, can be a dummy read of the 6502. The CPU reads such a byte but does not use it.
|
|
57
|
+
|
|
58
|
+
`c64_profile` gives the routines that used the most cycles since the session started, the last `clear` or the last `c64_window`. Use action `clear` before the part of the program that you want to measure. The profiler gives the cycles to the routine that a JSR called last. Code that the CPU enters with JMP or a branch counts for that routine. After `c64_window` or a snapshot restore, the profiler of VICE keeps the routines that ran before the restore. Then `c64_profile` can give the cycles of the program to such a routine, for example `$fd50`. Do not trust the routine addresses of `c64_profile` after a restore.
|
|
59
|
+
|
|
55
60
|
### Project folder
|
|
56
61
|
|
|
57
62
|
Paths (`path`, `file` and `out`) are relative to the project folder. A project folder holds `.c64-re-tools/`. The tools use the nearest project folder at or above the folder where the agent started. If that folder is not in a project and has projects below it, a tool with a path refuses and lists them. Then give `project` with the project folder, for example `"project": "game"`. Give the same folder as the `--project` of the skill scripts. A tool without a path does not need `project`.
|
|
@@ -62,7 +67,7 @@ Paths (`path`, `file` and `out`) are relative to the project folder. A project f
|
|
|
62
67
|
2. Get the machine state with `c64_status`.
|
|
63
68
|
3. Load the subject if it is not loaded. Use `c64_autostart` to start it as a user does. Use `c64_program_load` to put a PRG in memory with no start.
|
|
64
69
|
4. Set the stop condition. Use a breakpoint, a watchpoint or a `c64_run_until` target. Add a typed condition when the stop must occur only in one state. The `list` result shows the condition of each breakpoint and each watchpoint.
|
|
65
|
-
5. Run, step or give input. Prefer `c64_run_until` and `advance-frames` to waits. They stop at the same emulated time on every run.
|
|
70
|
+
5. Run, step or give input. Prefer `c64_run_until` and `advance-frames` to waits. They stop at the same emulated time on every run. To wait for a breakpoint or a watchpoint, use `advance-frames` with a large count. `stopReason` and `pointId` in the result tell what stopped the CPU. `c64_status` also gives them.
|
|
66
71
|
6. Read only the state that answers the question. To read several things from one moment, use `c64_observe`.
|
|
67
72
|
7. Interpret the evidence. Keep the observed facts apart from your conclusions.
|
|
68
73
|
|
|
@@ -76,7 +81,6 @@ Step, next and until-return work only in space `c64`. The emulator cannot stop t
|
|
|
76
81
|
|
|
77
82
|
`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:
|
|
78
83
|
|
|
79
|
-
- After `c64_window` or a snapshot restore, the profiler of VICE keeps the routines that ran before the restore. Then `c64_profile` can give the cycles of the program to such a routine, for example `$fd50`. Do not trust the routine addresses of `c64_profile` after a restore.
|
|
80
84
|
- 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.
|
|
81
85
|
- For one specific key, for example F1 or RUN/STOP, open the window and ask the user to press the key.
|
|
82
86
|
- Do not change the program to make it accept a key.
|
|
@@ -30,9 +30,9 @@ node <skill>/scripts/memmap.ts export --out src/hw.asm
|
|
|
30
30
|
| Command | Result |
|
|
31
31
|
|---|---|
|
|
32
32
|
| `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
|
|
33
|
-
| `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. |
|
|
33
|
+
| `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F`, `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`. |
|
|
34
34
|
| `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | All entries of that group. |
|
|
35
|
-
| `export --out <file>` | Writes all platform names to a project file as an ACME include: one
|
|
35
|
+
| `export --out <file>` | Writes all platform names to a project file as an ACME include: one line for each name, with its meaning as a comment. A zero-page name is `NAME = $xx`. Other names are `NAME = $xxxx`. |
|
|
36
36
|
|
|
37
37
|
Write an address or a value as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
|
|
38
38
|
|
|
@@ -78,7 +78,7 @@ With `--run`, a PRG starts with autostart and RUN. An image starts from its byte
|
|
|
78
78
|
Use `separate` when you have only one release. It gives evidence, not a decision:
|
|
79
79
|
|
|
80
80
|
- `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.
|
|
81
|
-
- `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs.
|
|
81
|
+
- `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs. A `read` byte right after executed code, or at a return address, can be a dummy read of the 6502 that the program does not use.
|
|
82
82
|
- `codeRegions.total`: the number of code regions in the knowledge.
|
|
83
83
|
- `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.
|
|
84
84
|
- `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. A `generated` range can have gaps of two bytes or less: these are operand bytes.
|
|
@@ -38,10 +38,10 @@ import { CODE_REGION_TYPES, type Origin } from "#src/knowledge/schema.ts";
|
|
|
38
38
|
import { platformNames } from "#src/platform.ts";
|
|
39
39
|
import { projectRoot } from "#src/project.ts";
|
|
40
40
|
import { runScript, UsageError } from "#src/script.ts";
|
|
41
|
-
import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./compare.ts";
|
|
42
|
-
import { findRoutine, libraryFolder, LibraryError, readLibrary, routineFrom, saveRoutine, suggest } from "./library.ts";
|
|
43
|
-
import { watchRelease, type ImageState, type Start } from "./runtime.ts";
|
|
44
|
-
import { separate } from "./separate.ts";
|
|
41
|
+
import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./lib/compare.ts";
|
|
42
|
+
import { findRoutine, libraryFolder, LibraryError, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
|
|
43
|
+
import { watchRelease, type ImageState, type Start } from "./lib/runtime.ts";
|
|
44
|
+
import { separate } from "./lib/separate.ts";
|
|
45
45
|
|
|
46
46
|
const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
|
|
47
47
|
provenance.ts separate <release> [--coverage <file> ...] [--out <file>] [--max-listed <n>]
|
|
@@ -78,7 +78,7 @@ Do the stages in this sequence. Go to the next stage only when its exit check is
|
|
|
78
78
|
| 4. Unpack | c64-unpacker: examine the program. Capture a packed program into `unpacked/`. | `image.json` and `entry_state.json` exist, or the evidence shows no packer. |
|
|
79
79
|
| 5. Run | c64-emulator and the run of c64-testing: run each part that the scope names. Write a coverage file for each part into `coverage/`. | Each part that the scope names has a coverage file. |
|
|
80
80
|
| 6. Analyze | c64-static-analysis with `--coverage` for each coverage file. | No coverage conflict is open. |
|
|
81
|
-
| 7. Name | c64-provenance: if the workspace has a library, find its routines with `match` and apply the batch. Then c64-knowledge and c64-memory-map: name each routine in the scope and give it a comment. Record the modules. | Each routine
|
|
81
|
+
| 7. Name | c64-provenance: if the workspace has a library, find its routines with `match` and apply the batch. Then c64-knowledge and c64-memory-map: name each routine in the scope and give it a comment. Record the modules. When the scope has hundreds of routines, ask the user which routines to name. The source gives the other routines an `l_xxxx` label. | Each routine that the user selected has a name and a comment. |
|
|
82
82
|
| 8. Generate | c64-source with `--coverage` into `src/`. For an unpacked program, add `--loader`. | `check.identical` is true. For an unpacked program, `loader.verified` is true. |
|
|
83
83
|
| 9. Build and test | c64-assembler with `--compare` into `build/`. c64-testing scenarios in `tests/`. | The criteria of stage 1 are true. |
|
|
84
84
|
| 10. Report | Write `report.md`. | The report has all its sections. |
|
|
@@ -148,6 +148,7 @@ A clear answer for the scope, with the durable conclusions in the project knowle
|
|
|
148
148
|
## Failure and conflicts
|
|
149
149
|
|
|
150
150
|
- A skill reports `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the whole task. Give the message to the user and wait for the user. Do not continue with other skills. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
151
|
+
- A tool gives a wrong result, and the exit check of a stage cannot be true. Tell the user the defect and the evidence, and wait for the user. Do not make a workaround, for example a change to a generated file. When the user accepts an exception, write the defect and the exception in the report.
|
|
151
152
|
- If the evidence does not support a conclusion, keep the unknown and get more evidence. Do not make up a meaning.
|
|
152
153
|
- Before you replace current knowledge from the user or the LLM, read its history and the evidence for it.
|
|
153
154
|
- A conflict from the static analysis: find the correct answer at run time with the c64-emulator skill.
|
|
@@ -102,7 +102,7 @@ The script writes a macro use only when the use gives the bytes of the image. Fi
|
|
|
102
102
|
|
|
103
103
|
The copy table, the copier and the jump to the entry use labels, not numbers. `loader_from_xxxx` and `loader_end_xxxx` are around each packed segment, and `loader_to_xxxx` is its first address in place. The jump goes to the label of the entry. Thus a segment can change its length, and the loader still copies it and jumps to the entry that moved. The display values, the raster line and the CIA values stay numbers. They are the entry state, not addresses of the program.
|
|
104
104
|
|
|
105
|
-
The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the SID, the time of day of the CIAs, the CIA interrupt flags or the interrupt registers of the VIC-II. `loader.limits` names each CIA value that the loader cannot set as it was at the entry. A program that reads the SID voice 3 for random numbers can still differ in a scenario. A raster sync does not change a timer or the SID. Then observe after the program sets up its own random numbers.
|
|
105
|
+
The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the SID, the time of day of the CIAs, the CIA interrupt flags or the interrupt registers of the VIC-II. `loader.limits` names each CIA value that the loader cannot set as it was at the entry. A program that reads the SID voice 3 for random numbers can still differ in a scenario. A raster sync does not change a timer or the SID. Then observe after the program sets up its own random numbers. To sync the original and the rebuild in a test, see the sync rules of the c64-testing skill.
|
|
106
106
|
|
|
107
107
|
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, the registers, the display registers of the VIC-II, the VIC bank and the raster line with the entry state there. It also compares the CIA timers, latches and interrupt masks. `loader.verified: true` tells that all of them are equal. An interrupt can run right at the entry, as in the original program.
|
|
108
108
|
|
|
@@ -30,9 +30,9 @@ import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/nativ
|
|
|
30
30
|
import { describeAddress, platformNames } from "#src/platform.ts";
|
|
31
31
|
import { resolveProjectPath } from "#src/project.ts";
|
|
32
32
|
import { runScript, UsageError } from "#src/script.ts";
|
|
33
|
-
import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
34
|
-
import { OPCODES } from "./opcodes.ts";
|
|
35
|
-
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Range, type Rendered } from "./source.ts";
|
|
33
|
+
import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
|
|
34
|
+
import { OPCODES } from "./lib/opcodes.ts";
|
|
35
|
+
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Range, type Rendered } from "./lib/source.ts";
|
|
36
36
|
|
|
37
37
|
const USAGE = `generate.ts <image> --out <dir> [options]
|
|
38
38
|
|
|
@@ -51,8 +51,9 @@ More seeds come from evidence:
|
|
|
51
51
|
|
|
52
52
|
- The start of each `code` region and each entry of a `jumps` table from the user or the LLM is an entry point.
|
|
53
53
|
- With `--coverage`, each executed instruction that starts a flow is an entry point. A flow starts after a gap, or after RTS, RTI, JMP or BRK. Then the analyzers find code that only a vector, an RTS table or an interrupt reaches.
|
|
54
|
+
- With `--coverage`, some RAM under a ROM is data for the analysis. At these addresses, the runs used the ROM and did not read or execute the RAM. Then a call into the KERNAL does not decode the RAM bytes under it as code. A knowledge routine or an `--entry` in such a range keeps its code. Ghidra does not follow BRK, because the vector at `$fffe` is in ROM unless the program removes the KERNAL.
|
|
54
55
|
|
|
55
|
-
The analyzers take at most 1024 entry points. Knowledge routines and `--entry` come first, then code regions and tables, then coverage. `coverageSeeds` gives how many entry points the coverage gave and how many the analysis used.
|
|
56
|
+
The analyzers take at most 1024 entry points. Knowledge routines and `--entry` come first, then code regions and tables, then coverage. `coverageSeeds` gives how many entry points the coverage gave and how many the analysis used. `romOnlyRanges` and `romOnlyUsed` give the same counts for the data ranges under ROM.
|
|
56
57
|
|
|
57
58
|
### Project folder
|
|
58
59
|
|
|
@@ -73,10 +74,10 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
73
74
|
- Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
|
|
74
75
|
- If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
|
|
75
76
|
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
76
|
-
- `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 (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
|
|
77
|
+
- `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). It also finds the `CBM80` header of a cartridge bank at `$8000` (`cartridgeHeaders`). The two start vectors of the header become entry points. The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. A loader that copies relocated code is also such a part. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
|
|
77
78
|
- `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair that `(zp),y` uses. Its `target` is in the image or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
|
|
78
79
|
- `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--macro-batch`, `batch` gives the file, the count of writes and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
|
|
79
|
-
- 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.
|
|
80
|
+
- 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. For many conflicts, write all classifications in one `apply` batch of the c64-knowledge skill.
|
|
80
81
|
6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
|
|
81
82
|
|
|
82
83
|
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.
|
|
@@ -23,12 +23,12 @@ import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
|
|
|
23
23
|
import { platformNames } from "#src/platform.ts";
|
|
24
24
|
import { resolveProjectPath } from "#src/project.ts";
|
|
25
25
|
import { runScript, UsageError } from "#src/script.ts";
|
|
26
|
-
import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps, type ImageByte } from "./coverage.ts";
|
|
27
|
-
import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
|
|
28
|
-
import { findIdioms } from "./idioms.ts";
|
|
29
|
-
import { instructionList } from "./instructions.ts";
|
|
30
|
-
import { CATALOGUE, findMacroUses, type MacroCandidate } from "./macros.ts";
|
|
31
|
-
import { findPointers, type PointerCandidate } from "./pointers.ts";
|
|
26
|
+
import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps, romOnlyRanges, type ImageByte } from "./lib/coverage.ts";
|
|
27
|
+
import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./lib/findings.ts";
|
|
28
|
+
import { findIdioms } from "./lib/idioms.ts";
|
|
29
|
+
import { instructionList } from "./lib/instructions.ts";
|
|
30
|
+
import { CATALOGUE, findMacroUses, type MacroCandidate } from "./lib/macros.ts";
|
|
31
|
+
import { findPointers, type PointerCandidate } from "./lib/pointers.ts";
|
|
32
32
|
|
|
33
33
|
const USAGE = `analyze.ts <image> [options]
|
|
34
34
|
|
|
@@ -270,10 +270,16 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
270
270
|
const seeds = seedsFromKnowledge(reader, entries, space);
|
|
271
271
|
// Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound (D44).
|
|
272
272
|
const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, byteAt);
|
|
273
|
-
const
|
|
273
|
+
const known = [...seeds.entryPoints, ...codeEntries(reader, byteAt, space)];
|
|
274
|
+
const ordered = [...new Set([...known, ...idioms.entryPoints, ...fromCoverage])];
|
|
274
275
|
seeds.entryPoints = ordered.slice(0, GHIDRA_LIMITS.entryPoints).sort((a, b) => a - b);
|
|
275
|
-
// Idiom tables are data for this run too; semantic data ranges come first.
|
|
276
|
-
|
|
276
|
+
// Idiom tables and the RAM under ROM that the runs never used are data for this run too; semantic data ranges come first.
|
|
277
|
+
// A range with an entry point of the knowledge or --entry in it is left out: that code stays code.
|
|
278
|
+
const fromRom = coverage === undefined ? [] : romOnlyRanges(coverage, byteAt).filter((range) => !known.some((entry) => entry >= range.start && entry <= range.end));
|
|
279
|
+
const dataRanges = [...seeds.dataRanges];
|
|
280
|
+
for (const range of [...idioms.dataRanges, ...fromRom]) if (!dataRanges.some((seed) => range.start <= seed.end && range.end >= seed.start)) dataRanges.push(range);
|
|
281
|
+
seeds.dataRanges = dataRanges.slice(0, GHIDRA_LIMITS.dataRanges);
|
|
282
|
+
const romUsed = fromRom.filter((range) => seeds.dataRanges.includes(range)).length;
|
|
277
283
|
const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry)).length;
|
|
278
284
|
// Platform names are C64 names: drive code and banks get none.
|
|
279
285
|
const platform = space === DEFAULT_SPACE ? platformNames() : [];
|
|
@@ -315,7 +321,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
315
321
|
const conflicts = coverageConflicts(db, coverage);
|
|
316
322
|
db?.close();
|
|
317
323
|
return {
|
|
318
|
-
coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed },
|
|
324
|
+
coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed, romOnlyRanges: fromRom.length, romOnlyUsed: romUsed },
|
|
319
325
|
coverageConflicts: conflicts.map(({ region, executed }) => ({
|
|
320
326
|
range: { start: formatC64Address(region.start), end: formatC64Address(region.end) },
|
|
321
327
|
knowledge: { type: region.type, origin: region.origin },
|
|
@@ -49,6 +49,26 @@ export function coverageEntries(map: Uint8Array, image: ImageByte): number[] {
|
|
|
49
49
|
return entries;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
|
|
53
|
+
const RAM_USE = COVERAGE.ramRead | COVERAGE.ramExecute;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The image bytes under ROM that the runs never used: the CPU read or ran the
|
|
57
|
+
* ROM there, and never read or ran the RAM (R2-07). A JSR $FFE4 or an IRQ
|
|
58
|
+
* vector at $FFFE then leads into ROM, not into the leftover RAM bytes of a
|
|
59
|
+
* flat image, so the ranges are data for the analysis run.
|
|
60
|
+
*/
|
|
61
|
+
export function romOnlyRanges(map: Uint8Array, image: ImageByte): Array<{ start: number; end: number }> {
|
|
62
|
+
const ranges: Array<{ start: number; end: number }> = [];
|
|
63
|
+
for (let address = 0; address < FULL_MAP; address++) {
|
|
64
|
+
if ((map[address]! & ROM_ACCESS) === 0 || (map[address]! & RAM_USE) !== 0 || image(address) === undefined) continue;
|
|
65
|
+
const last = ranges.at(-1);
|
|
66
|
+
if (last !== undefined && last.end === address - 1) last.end = address;
|
|
67
|
+
else ranges.push({ start: address, end: address });
|
|
68
|
+
}
|
|
69
|
+
return ranges;
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
/** Entry points from semantic knowledge beyond routines: the start of each code region, and each entry of a JMP table with its target. */
|
|
53
73
|
export function codeEntries(db: DatabaseSync | undefined, image: ImageByte, space: Space = DEFAULT_SPACE): number[] {
|
|
54
74
|
const entries: number[] = [];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// 6502 idioms that hide code from a disassembler (D44): RTS dispatch tables,
|
|
2
2
|
// JMP tables and BIT skips are found by their exact instruction patterns and
|
|
3
3
|
// become heuristic findings and entry points; DOP padding and self-modified
|
|
4
|
-
// opcodes are only reported.
|
|
4
|
+
// opcodes are only reported. The CBM80 header of a cartridge bank at $8000
|
|
5
|
+
// gives its two start vectors as entry points (F-29). A self-modified opcode is one that one run wrote
|
|
5
6
|
// and executed, in either order, since a coverage file holds no order (R3-04):
|
|
6
7
|
// a copier's writes in one run and their execution in another do not count (F-16). A table's length is not known statically: entries
|
|
7
8
|
// count while each target is a plausible instruction in the program.
|
|
@@ -17,6 +18,8 @@ const LDA_INDEXED = new Set([0xbd, 0xb9]);
|
|
|
17
18
|
const JAMS = new Set([0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
|
|
18
19
|
/** Undocumented NOPs with an operand byte (DOP): padding between instructions hides the real ones. */
|
|
19
20
|
const DOPS = new Set([0x04, 0x14, 0x34, 0x44, 0x54, 0x64, 0x74, 0x80, 0x82, 0x89, 0xc2, 0xd4, 0xe2, 0xf4]);
|
|
21
|
+
/** "CBM80" in PETSCII at $8004: the KERNAL then starts the cartridge through the vectors at $8000 (cold) and $8002 (warm). */
|
|
22
|
+
const CBM80 = [0xc3, 0xc2, 0xcd, 0x38, 0x30];
|
|
20
23
|
|
|
21
24
|
export interface Idioms {
|
|
22
25
|
findings: NormalizedFindings;
|
|
@@ -24,7 +27,7 @@ export interface Idioms {
|
|
|
24
27
|
entryPoints: number[];
|
|
25
28
|
/** Table ranges for the same run: they are data. */
|
|
26
29
|
dataRanges: Array<{ start: number; end: number }>;
|
|
27
|
-
counts: { rtsTables: number; jumpTables: number; bitSkips: number };
|
|
30
|
+
counts: { cartridgeHeaders: number; rtsTables: number; jumpTables: number; bitSkips: number };
|
|
28
31
|
/** Seen in the coverage, not written: the LLM decides. All of them: the result lists the first ones. */
|
|
29
32
|
candidates: { dop: number[]; selfModified: number[] };
|
|
30
33
|
}
|
|
@@ -47,7 +50,7 @@ export function findIdioms(byteAt: ImageByte, range: { start: number; end: numbe
|
|
|
47
50
|
const regions: NormalizedFindings["regions"] = [];
|
|
48
51
|
const references: NormalizedFindings["references"] = [];
|
|
49
52
|
const entryPoints = new Set<number>();
|
|
50
|
-
const counts = { rtsTables: 0, jumpTables: 0, bitSkips: 0 };
|
|
53
|
+
const counts = { cartridgeHeaders: 0, rtsTables: 0, jumpTables: 0, bitSkips: 0 };
|
|
51
54
|
const taken = (start: number, end: number) => regions.some((region) => start <= region.end && end >= region.start);
|
|
52
55
|
const addRegion = (start: number, end: number, type: NormalizedFindings["regions"][number]["type"]) => {
|
|
53
56
|
if (start < range.start || end > range.end || taken(start, end)) return false;
|
|
@@ -55,6 +58,14 @@ export function findIdioms(byteAt: ImageByte, range: { start: number; end: numbe
|
|
|
55
58
|
return true;
|
|
56
59
|
};
|
|
57
60
|
|
|
61
|
+
if (range.start <= 0x8000 && range.end >= 0x8008 && CBM80.every((byte, index) => byteAt(0x8004 + index) === byte)) {
|
|
62
|
+
const vectors = [word(byteAt, 0x8000)!, word(byteAt, 0x8002)!].filter((target) => plausible(byteAt, target, range));
|
|
63
|
+
if (addRegion(0x8000, 0x8003, "addresses") && addRegion(0x8004, 0x8008, "petscii")) {
|
|
64
|
+
counts.cartridgeHeaders++;
|
|
65
|
+
for (const target of vectors) entryPoints.add(target);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
for (let at = range.start; at + 8 <= range.end; at++) {
|
|
59
70
|
// RTS dispatch: LDA high,X / PHA / LDA low,X / PHA / RTS.
|
|
60
71
|
if (LDA_INDEXED.has(byteAt(at)!) && byteAt(at + 3) === 0x48 && LDA_INDEXED.has(byteAt(at + 4)!) && byteAt(at + 7) === 0x48 && byteAt(at + 8) === 0x60) {
|
|
@@ -26,7 +26,7 @@ node <skill>/scripts/test.ts tests/moves-left.json --out results/moves-left
|
|
|
26
26
|
node <skill>/scripts/checklist.ts tests/moves-left.json --item 'FIRE starts the game.'
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
The test script prints one JSON object. The exit status is 0 for PASS, 1 for FAIL, 3 for INCONCLUSIVE and 2 for a bad call. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json` with each byte that differs. `files` in the result names them.
|
|
29
|
+
The test script prints one JSON object. The exit status is 0 for PASS, 1 for FAIL, 3 for INCONCLUSIVE and 2 for a bad call. With `--out`, a FAIL writes files into that directory: both screens and the difference image for each screen difference, and `memory.json` with each byte that differs. `files` in the result names them. A PASS writes no files. To keep the screens of a PASS, use a run (below).
|
|
30
30
|
|
|
31
31
|
The checklist script prints a Markdown checklist for a human tester.
|
|
32
32
|
|
|
@@ -62,11 +62,12 @@ See [references/scenario-format.md](references/scenario-format.md), section Runs
|
|
|
62
62
|
1. Write the scenario file. Start the steps with a frame count, for example `{ "frames": 150 }`. Each side starts at the reset vector, and the C64 needs these frames to start.
|
|
63
63
|
2. Use symbol names, for example `player_x`, not fixed addresses. Each side finds the name in its own symbols. Use a `$` address only when both programs use the same address.
|
|
64
64
|
3. Make each checkpoint a condition, for example `runUntil` at a routine. Do not wait for a time.
|
|
65
|
-
4. Save the c64-assembler output of the rebuild to the symbol file, for example `build/game.json` in the project folder. From the folder above the project, write `assemble.ts --project <folder> ... > <folder>/build/game.json`.
|
|
65
|
+
4. Save the c64-assembler output of the rebuild to the symbol file, for example `build/game.json` in the project folder. Give `--all-symbols`, so that the file also has the constants that the source does not use. From the folder above the project, write `assemble.ts --project <folder> ... > <folder>/build/game.json`.
|
|
66
66
|
5. Run the test script.
|
|
67
67
|
6. Read the result:
|
|
68
68
|
- `PASS`: all observations are equal, or the differences are in the allowed tolerance.
|
|
69
69
|
- `FAIL`: `differences` lists each difference with the checkpoint, the item, the original value and the rebuild value.
|
|
70
|
+
- Each value has one form. The pc and each address are `$xxxx`. Memory is lowercase hex. Other register values and chip values are decimal numbers.
|
|
70
71
|
- `INCONCLUSIVE`: `reason` tells why the script could not compare. This is not a failure of the program.
|
|
71
72
|
7. For a FAIL, examine the first difference with the c64-emulator skill.
|
|
72
73
|
|
|
@@ -38,7 +38,7 @@ Before the first step of each side, the script does a hard reset. The CPU stops
|
|
|
38
38
|
| Step | Effect |
|
|
39
39
|
|---|---|
|
|
40
40
|
| `{ "reset": "hard" }` | Reset the C64 (`hard` or `soft`). The CPU stops at the reset vector. |
|
|
41
|
-
| `{ "frames": 150 }` | Run for an exact number of frames, from 1 to
|
|
41
|
+
| `{ "frames": 150 }` | Run for an exact number of frames, from 1 to 30000. A breakpoint or a watchpoint stops it before that. Use it after a reset to start the C64. |
|
|
42
42
|
| `{ "load": true }` | Load the program of the side into memory. The CPU stays stopped. |
|
|
43
43
|
| `{ "autostart": true }` | Load the program of the side, type `RUN` and wait until the program runs. The program must have a BASIC start line. The keys of a later `type` step go to the program. |
|
|
44
44
|
| `{ "write": { "at": "seed", "bytes": [1, 2] } }` | Write bytes into memory: 1 to 4096 numbers from 0 to 255. |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
|
|
7
7
|
import { runScript, UsageError } from "#src/script.ts";
|
|
8
|
-
import { readScenario, ScenarioError, type Scenario } from "./scenario.ts";
|
|
8
|
+
import { readScenario, ScenarioError, type Scenario } from "./lib/scenario.ts";
|
|
9
9
|
|
|
10
10
|
const USAGE = `checklist.ts <scenario.json> [<scenario.json> ...] [--item <text> ...]
|
|
11
11
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Observations name semantic symbols that resolve per side, comparisons are
|
|
4
4
|
// explicit, and the result is PASS, FAIL or INCONCLUSIVE.
|
|
5
5
|
|
|
6
|
-
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
6
|
+
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
7
7
|
import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
|
|
8
8
|
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { ACTION_STEP_KEYS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
@@ -248,11 +248,12 @@ export function loadSymbols(subject: Subject): Map<string, number> {
|
|
|
248
248
|
// c64-assembler output, or a plain name → "$xxxx" map. A constant in the
|
|
249
249
|
// assembler output counts too: RAM variables such as "counter = $c100" lie
|
|
250
250
|
// outside the program, so the assembler reports them as constants.
|
|
251
|
-
const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?:
|
|
251
|
+
const assembled = (value as { symbols?: Array<{ name: string; address?: string; value?: string }> }).symbols;
|
|
252
252
|
const entries: Array<[string, unknown]> = Array.isArray(assembled)
|
|
253
253
|
? assembled.flatMap((symbol): Array<[string, unknown]> => {
|
|
254
254
|
if (symbol.address !== undefined) return [[symbol.name, symbol.address]];
|
|
255
|
-
|
|
255
|
+
// A constant above $ffff has more digits and is no address.
|
|
256
|
+
if (typeof symbol.value === "string" && /^\$[0-9a-f]{4}$/.test(symbol.value)) return [[symbol.name, symbol.value]];
|
|
256
257
|
return [];
|
|
257
258
|
})
|
|
258
259
|
: Object.entries(value as Record<string, unknown>);
|
|
@@ -358,14 +359,14 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
358
359
|
}
|
|
359
360
|
if (step.registers !== undefined) {
|
|
360
361
|
const registers = await client.registersGet("c64");
|
|
361
|
-
for (const name of step.registers) observation.registers[name] = registers[name];
|
|
362
|
+
for (const name of step.registers) observation.registers[name] = name === "pc" ? formatC64Address(registers.pc) : registers[name];
|
|
362
363
|
}
|
|
363
364
|
if (step.vicii) {
|
|
364
365
|
// The raster line changes from one moment to the next; the comparison leaves it out.
|
|
365
366
|
const { rasterLine: _rasterLine, ...state } = await client.vicii();
|
|
366
|
-
observation.vicii = state;
|
|
367
|
+
observation.vicii = formatVicii(state);
|
|
367
368
|
}
|
|
368
|
-
if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites;
|
|
369
|
+
if (step.sprites !== undefined) observation.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
|
|
369
370
|
if (step.screen !== undefined) {
|
|
370
371
|
const baseline = baselineName(checkpoint, step.observe);
|
|
371
372
|
if (side === "original") {
|
|
@@ -413,7 +414,7 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
413
414
|
const other = right.memory[index]!;
|
|
414
415
|
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
415
416
|
if (item.bytes.some((byte, at) => !skipped.has(at) && Math.abs(byte - other.bytes[at]!) > item.tolerance)) {
|
|
416
|
-
const show = (bytes: number[]) =>
|
|
417
|
+
const show = (bytes: number[]) => Buffer.from(bytes).toString("hex");
|
|
417
418
|
differences.push({ checkpoint, what: `memory ${item.label}`, original: show(item.bytes), rebuild: show(other.bytes) });
|
|
418
419
|
}
|
|
419
420
|
});
|
|
@@ -439,7 +440,8 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
439
440
|
writeProjectFile(`${out}/${name}`, bytes, { what: "--out" });
|
|
440
441
|
files.push(name);
|
|
441
442
|
};
|
|
442
|
-
const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original:
|
|
443
|
+
const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: string; rebuild: string }> }> = [];
|
|
444
|
+
const hex = (byte: number) => byte.toString(16).padStart(2, "0");
|
|
443
445
|
for (const difference of differences) {
|
|
444
446
|
const left = original.get(difference.checkpoint)!;
|
|
445
447
|
const right = rebuild.get(difference.checkpoint)!;
|
|
@@ -454,7 +456,7 @@ function writeFailFiles(out: string, original: Map<string, Observation>, rebuild
|
|
|
454
456
|
const item = left.memory[index]!;
|
|
455
457
|
const other = right.memory[index]!;
|
|
456
458
|
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
457
|
-
const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: byte, rebuild: other.bytes[offset]! }] : []));
|
|
459
|
+
const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: hex(byte), rebuild: hex(other.bytes[offset]!) }] : []));
|
|
458
460
|
memory.push({ checkpoint: difference.checkpoint, what: difference.what, bytes });
|
|
459
461
|
}
|
|
460
462
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
|
|
9
|
-
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
9
|
+
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
10
10
|
import { runProgram } from "#src/host-client/autostart.ts";
|
|
11
11
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
12
12
|
import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
@@ -15,7 +15,7 @@ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts"
|
|
|
15
15
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
16
16
|
import { resolveProjectPath } from "#src/project.ts";
|
|
17
17
|
import { runScript, UsageError } from "#src/script.ts";
|
|
18
|
-
import { loadSymbols } from "./scenario.ts";
|
|
18
|
+
import { loadSymbols } from "./lib/scenario.ts";
|
|
19
19
|
|
|
20
20
|
const USAGE = `run.ts <run.json> --out <dir>
|
|
21
21
|
|
|
@@ -149,8 +149,8 @@ async function runFile(path: string, out: string): Promise<{ output: Record<stri
|
|
|
149
149
|
const registers = await client.registersGet("c64");
|
|
150
150
|
seen.registers = { ...registers, pc: formatC64Address(registers.pc) };
|
|
151
151
|
}
|
|
152
|
-
if (step.vicii === true) seen.vicii = await client.vicii();
|
|
153
|
-
if (step.sprites !== undefined) seen.sprites = (await client.sprites(step.sprites)).sprites;
|
|
152
|
+
if (step.vicii === true) seen.vicii = formatVicii(await client.vicii());
|
|
153
|
+
if (step.sprites !== undefined) seen.sprites = (await client.sprites(step.sprites)).sprites.map(formatSprite);
|
|
154
154
|
observations[step.observe] = seen;
|
|
155
155
|
} else {
|
|
156
156
|
const notReached = await runActionStep(client, step, at);
|
|
@@ -8,7 +8,7 @@ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
|
8
8
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
9
9
|
import { resolveProjectPath } from "#src/project.ts";
|
|
10
10
|
import { runScript, UsageError } from "#src/script.ts";
|
|
11
|
-
import { readScenario, runScenario, ScenarioError, type ScenarioResult } from "./scenario.ts";
|
|
11
|
+
import { readScenario, runScenario, ScenarioError, type ScenarioResult } from "./lib/scenario.ts";
|
|
12
12
|
|
|
13
13
|
const USAGE = `test.ts <scenario.json> [--out <dir>]
|
|
14
14
|
|
|
@@ -49,6 +49,8 @@ A steps file has the steps `write`, `registers`, `joystick`, `type`, `frames` an
|
|
|
49
49
|
[{ "frames": 50 }, { "type": " " }]
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
A `frames` step runs 1 to 30000 frames. A `runUntil` step has `timeoutFrames` from 1 to 30000.
|
|
53
|
+
|
|
52
54
|
A `type` step puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. A step cannot press one specific key of the keyboard matrix. A wait for any key that reads all keyboard columns can end without a key.
|
|
53
55
|
|
|
54
56
|
### Project folder
|
|
@@ -66,7 +68,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
|
|
|
66
68
|
- `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.
|
|
67
69
|
|
|
68
70
|
Also read the evidence that the script finds in the file. The script never names a packer from it:
|
|
69
|
-
- `basicText`: the BASIC lines
|
|
71
|
+
- `basicText`: raw evidence from the BASIC lines, not a listing. A byte that is not a printable character shows as `{$xx}`. A packer or a crack often puts its name there. For a listing, use the c64-basic skill.
|
|
70
72
|
- `strings`: readable text near the start of the code.
|
|
71
73
|
- `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
|
|
72
74
|
- `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
|
|
@@ -79,6 +81,7 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
|
|
|
79
81
|
- Or use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
|
|
80
82
|
- Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
|
|
81
83
|
- Disassemble the code at the address with the c64-emulator skill. Make sure that it is the start of the program. The start of a program often sets the stack pointer, the interrupts and the vectors.
|
|
84
|
+
- 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.
|
|
82
85
|
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
83
86
|
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
84
87
|
- `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.
|