@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0
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 +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +1 -1
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +2 -2
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -22,23 +22,23 @@ A PASS proves only the behavior that the scenario does and observes. It does not
|
|
|
22
22
|
The script starts one emulator through the host runtime. It runs the scenario on the original and then on the rebuild. Before each side, it does a hard reset of the emulator. The CPU then stops at the reset vector.
|
|
23
23
|
|
|
24
24
|
```
|
|
25
|
-
node <skill>/scripts/
|
|
26
|
-
node <skill>/scripts/
|
|
25
|
+
node <skill>/scripts/testing.ts test tests/moves-left.json --out .c64-re-tools/local/results/moves-left
|
|
26
|
+
node <skill>/scripts/testing.ts checklist tests/moves-left.json --item 'FIRE starts the game.'
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
`testing.ts test` gives `result`: PASS, FAIL or INCONCLUSIVE. 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
|
+
`testing.ts checklist` gives a Markdown checklist for a human tester in `text`.
|
|
32
32
|
|
|
33
33
|
### Runs
|
|
34
34
|
|
|
35
35
|
A run does steps on one program, with no comparison, and keeps the results as files. Use it for many steps. One run replaces many emulator calls, for example a visit to each room of a game.
|
|
36
36
|
|
|
37
37
|
```
|
|
38
|
-
node <skill>/scripts/
|
|
38
|
+
node <skill>/scripts/testing.ts run .c64-re-tools/local/runs/rooms/run.json --out .c64-re-tools/local/runs/rooms
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file.
|
|
41
|
+
The run file gives where to start (a program, or a snapshot file), the steps, and optional symbols. To go on from the live emulator, save its state with `c64_snapshot` action `save` and `out`, and start the run from that file. A run has the action steps of a scenario: `write`, `registers`, `joystick`, `type`, `frames` and `runUntil`. It also has these steps:
|
|
42
42
|
|
|
43
43
|
| Step | Effect |
|
|
44
44
|
|---|---|
|
|
@@ -47,47 +47,51 @@ The run file gives where to start (a program, or a snapshot file), the steps, an
|
|
|
47
47
|
| `{ "screen": "room3" }` | Write the screen to `room3.png`. |
|
|
48
48
|
| `{ "coverage": "room3" }` | Write the coverage map since the start or the last coverage step to `room3.bin`, then clear the map. Add the map to the knowledge with `coverage` of the c64-knowledge skill. |
|
|
49
49
|
| `{ "observe": "room3", "memory": ["$c100"], "registers": true }` | Record memory, registers, `vicii` or `sprites` in `results.json`. |
|
|
50
|
-
| `{ "trace": "d011", "points": [{ "kind": "write", "
|
|
50
|
+
| `{ "trace": "d011", "points": [{ "kind": "write", "start": "$d011" }], "frames": 100 }` | Record each hit of the points for 100 frames in `d011.json`: the instruction, its registers and the raster line and cycle. A point has the `kind` `exec`, `read` or `write`, a `start` and an optional `end`. `maxHits` limits the hits. |
|
|
51
51
|
| `{ "observe": "all3", "memory": "all" }` | Write all 64 KiB of RAM to `all3.bin`, also the RAM under the ROMs and the I/O area. |
|
|
52
52
|
|
|
53
53
|
A memory item can have `"view": "ram"`. Then it reads the RAM under the ROMs and the I/O area, for example code at `$e000` under the KERNAL. The default view `cpu` reads as the CPU sees memory. An item larger than 4096 bytes goes into its own `.bin` file, and `results.json` gives the file name.
|
|
54
54
|
|
|
55
|
-
See [references/scenario-format.md](references/scenario-format.md), section Runs.
|
|
55
|
+
See [references/scenario-format.md](references/scenario-format.md), section Runs. `result` is PASS when all steps ran, and FAIL when a step did not run. `reason` tells why. A long run can need more time than one shell call. Then run it in the background.
|
|
56
56
|
|
|
57
57
|
### Recordings
|
|
58
58
|
|
|
59
59
|
A recording keeps a session of the original program, for example a play through some rooms of a game. A replay gives the same input to a rebuild and compares the game state. Make the recordings early, before you change the program. A recording needs no names. Its coverage also helps the static analysis.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
`testing.ts record` and `testing.ts replay` run the replay core `c64run` on this machine, not the emulator of the host runtime. The replay core has no disk drive. The program must be a PRG file.
|
|
62
62
|
|
|
63
63
|
Ask the user to play first. Tell the user which parts of the program the recordings must have, for example each room, a fight and a lost life. Make a scripted recording only for a part that the sessions of the user do not have, or when the user cannot play.
|
|
64
64
|
|
|
65
65
|
```
|
|
66
|
-
node <skill>/scripts/
|
|
66
|
+
node <skill>/scripts/testing.ts record tests/record/start.json --out tests/recordings/play1 --play --joystick 1
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
- With `--play`, a person plays the program after the steps of the record file. The steps can start the program, for example a key for an intro.
|
|
70
70
|
- The script writes the address of a page to stderr. Give the address to the user. In a dev container, the port of the page (default 6465) must be forwarded.
|
|
71
|
-
-
|
|
72
|
-
-
|
|
71
|
+
- The page shows the screen after the steps. The C64 is frozen until the user presses Enter or clicks Start. Tell the user to read the screen first.
|
|
72
|
+
- Give `--joystick` the port that the program reads: port 2 is `$DC00`, and port 1 is `$DC01`. Find the port in the code or in the knowledge. If you do not know the port, use 2. Tell the user that F9 changes the port.
|
|
73
|
+
- The page has the full C64 keyboard and the joystick. F10 changes the arrow keys and space between the joystick and the C64 keys. A gamepad is always the joystick. See the key maps in [references/recording-format.md](references/recording-format.md).
|
|
74
|
+
- RESTORE (Page Up) is not a read. The recording keeps it as an event, and the replay presses it again at the same read.
|
|
75
|
+
- End (F12) stops the C64 and freezes the screen. Then the script writes the recording folder. Run the script in the background.
|
|
76
|
+
- Cancel (F11) stops the session. The script writes no folder and gives `recorded: false` with the reason. Then ask the user what to do. Do not make a scripted recording in its place.
|
|
73
77
|
|
|
74
|
-
A scripted recording has only steps. See [references/recording-format.md](references/recording-format.md). The steps are `frames`, `joystick`, `type`, `write`, `registers` and `runUntil` with `
|
|
78
|
+
A scripted recording has only steps. See [references/recording-format.md](references/recording-format.md). The steps are `frames`, `joystick`, `press`, `type`, `write`, `registers` and `runUntil` with an `address` or `memory` target. `press` pushes keys of the keyboard matrix. `type` puts at most 10 keys into the keyboard queue of the KERNAL.
|
|
75
79
|
|
|
76
80
|
```
|
|
77
|
-
node <skill>/scripts/
|
|
78
|
-
node <skill>/scripts/
|
|
81
|
+
node <skill>/scripts/testing.ts record tests/record/rooms.json --out tests/recordings/rooms
|
|
82
|
+
node <skill>/scripts/testing.ts replay tests/recordings/rooms --checkpoint main_loop --state room --state lives --pin random_ptr:2 --rebuild build/game.prg --symbols build/game.json
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
-
|
|
82
|
-
-
|
|
85
|
+
- `testing.ts record` writes the recording folder. `result` is PASS when it recorded the session, and FAIL when the session did not complete. `executed` gives the instructions in the coverage map.
|
|
86
|
+
- `testing.ts replay` first replays the recording on the original. The original must end the session with the recorded state. Then the script replays the recording on the rebuild and compares the state at each checkpoint.
|
|
83
87
|
- `--checkpoint` gives a routine, and `--state` gives each memory item to compare. Use the names that the knowledge has at the time of the replay. A `$` address is an address of the original. On the rebuild, it moves with the name before it.
|
|
84
|
-
-
|
|
85
|
-
- With `--out`, a FAIL writes the RAM of the two sides at that checkpoint. Compare the two files with `
|
|
86
|
-
- Without `--rebuild`,
|
|
88
|
+
- `result` of the replay is PASS, FAIL or INCONCLUSIVE. A FAIL gives the first checkpoint that is different and the state items that are different.
|
|
89
|
+
- With `--out`, a FAIL writes the RAM of the two sides at that checkpoint. Compare the two files with `testing.ts diff`.
|
|
90
|
+
- Without `--rebuild`, `testing.ts replay` only replays the recording on the original.
|
|
87
91
|
|
|
88
92
|
A checkpoint is one entry of a routine. Use a routine that the program executes one time in each pass of its game logic, for example the main loop. Do not use an interrupt handler.
|
|
89
93
|
|
|
90
|
-
The input is the reads of the joystick ports and the keyboard matrix. It is also the reads of the four SID registers that a program can read: the paddles, the noise of voice 3 and its envelope. It is also the reads of `$DE00`-`$DFFF`, where a C64 without a cartridge gives the last byte on the bus. On a replay, the n-th read of each instruction gets the value of the n-th read in the recording. Thus a rebuild with different timing gets the same input and the same SID random numbers. A `write`, `registers` or `runUntil` step is not input. The replay does
|
|
94
|
+
The input is the reads of the joystick ports and the keyboard matrix. It is also the reads of the four SID registers that a program can read: the paddles, the noise of voice 3 and its envelope. It is also the reads of `$DE00`-`$DFFF`, where a C64 without a cartridge gives the last byte on the bus. On a replay, the n-th read of each instruction gets the value of the n-th read in the recording. Thus a rebuild with different timing gets the same input and the same SID random numbers. A `press`, `joystick` or `frames` step is input. A `type`, `write`, `registers` or `runUntil` step is not input. The replay does that step again at the same read of the program.
|
|
91
95
|
|
|
92
96
|
A replay is deterministic. The replay core has no random source and no clock: the same recording gives the same session, to the cycle. Sometimes the recording has no value for a read of an input. Then the read gets a fixed value: `$FF` for the joystick, keyboard and paddle registers, `$00` for the others. `idleReads` in the result counts these reads on the rebuild. On the original, such a read makes the result INCONCLUSIVE.
|
|
93
97
|
|
|
@@ -104,24 +108,36 @@ Commit the recording folder with the scenarios. A recording is not knowledge.
|
|
|
104
108
|
### Differences
|
|
105
109
|
|
|
106
110
|
```
|
|
107
|
-
node <skill>/scripts/
|
|
108
|
-
node <skill>/scripts/
|
|
111
|
+
node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/before.vsf .c64-re-tools/local/runs/rooms/after.vsf
|
|
112
|
+
node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json:room0 .c64-re-tools/local/runs/rooms/results.json:room3
|
|
109
113
|
```
|
|
110
114
|
|
|
111
|
-
`
|
|
115
|
+
`testing.ts diff` tells what changed between two states of one kind. Use it in place of your own compare script.
|
|
112
116
|
|
|
113
117
|
- Two snapshot files: the script restores each one in its own emulator. It gives the changed ranges of the RAM and of the colour RAM, and the changed VIC-II, CIA, SID and CPU registers.
|
|
114
|
-
- Two memory files of a run: the changed ranges. For a file of less than 64 KiB, `--
|
|
118
|
+
- Two memory files of a run: the changed ranges. For a file of less than 64 KiB, `--address` gives the address of its first byte.
|
|
115
119
|
- Two observations of runs, also of one run: the changed memory items and registers.
|
|
116
120
|
|
|
117
121
|
`--gap N` joins ranges across N equal bytes. `--out` writes all ranges to a project file.
|
|
118
122
|
|
|
123
|
+
### Calling the script
|
|
124
|
+
|
|
125
|
+
- Call a script as `node <skill>/scripts/<script>.ts <command> [subjects] [options]`. The command comes first.
|
|
126
|
+
- `<script>.ts --help` lists the commands. `<script>.ts <command> --help` gives the subjects and the options of one command. The help is JSON: `{"text": "..."}`.
|
|
127
|
+
- An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
|
|
128
|
+
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
129
|
+
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
130
|
+
- The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
|
|
131
|
+
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
132
|
+
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
133
|
+
|
|
119
134
|
### Project folder
|
|
120
135
|
|
|
121
136
|
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
122
137
|
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
123
138
|
- Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
|
|
124
|
-
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence
|
|
139
|
+
- Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence test:<file>` or `--evidence file:<file>` of c64-knowledge. These two refuse a file in `.c64-re-tools/local/`.
|
|
140
|
+
- Do not move a coverage map after `coverage` of c64-knowledge imported it. `--evidence coverage:<map>` names the map by its path at the import. The knowledge keeps the coverage and the SHA-256 of the map, so the evidence stays valid.
|
|
125
141
|
- Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
|
|
126
142
|
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
127
143
|
|
|
@@ -129,9 +145,9 @@ node <skill>/scripts/diff.ts .c64-re-tools/local/runs/rooms/results.json:room0 .
|
|
|
129
145
|
|
|
130
146
|
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.
|
|
131
147
|
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.
|
|
132
|
-
3. Make each checkpoint a condition, for example `runUntil` at a routine. Do not wait for a time.
|
|
133
|
-
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 `
|
|
134
|
-
5. Run
|
|
148
|
+
3. Make each checkpoint a condition, for example a `runUntil` step with an `address` target at a routine. Do not wait for a time.
|
|
149
|
+
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 `assembler.ts build --project <folder> ... > <folder>/build/game.json`.
|
|
150
|
+
5. Run `testing.ts test`.
|
|
135
151
|
6. Read the result:
|
|
136
152
|
- `PASS`: all observations are equal, or the differences are in the allowed tolerance.
|
|
137
153
|
- `FAIL`: `differences` lists each difference with the checkpoint, the item, the original value and the rebuild value.
|
|
@@ -171,13 +187,13 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
|
|
|
171
187
|
- A FAIL with only a `screen` difference: add `vicii` or `sprites` to the observation to find which chip state is different.
|
|
172
188
|
- A program that uses random numbers: set the random seed with a `write` step on both sides. If that is not possible, observe only values that do not depend on the random numbers.
|
|
173
189
|
- The two sides start differently, for example the original after a cracked intro. Give the steps that only one side needs a `side`. Then make the memory the same with a `normalize` step. It writes the entry state of the capture in the knowledge.
|
|
174
|
-
- Frame counts drift by one frame between the sides: the sides got to the same address at different raster positions. Do not sync on an IRQ handler. Sync on a value that the IRQ changes, with `runUntil` `memory
|
|
190
|
+
- Frame counts drift by one frame between the sides: the sides got to the same address at different raster positions. Do not sync on an IRQ handler. Sync on a value that the IRQ changes, with a `runUntil` `memory` target. Or sync on an `address` target with a `raster` condition.
|
|
175
191
|
- INCONCLUSIVE, "does not reproduce": the original does not end the session with the recorded state. Record the session again with this `c64run`.
|
|
176
192
|
- INCONCLUSIVE, "never came to the checkpoint": the original did not execute the checkpoint routine in the session. Give another routine.
|
|
177
193
|
- INCONCLUSIVE, "is not the program of the recording": the program file changed after the recording. Record the session again.
|
|
178
|
-
- A record result with `recorded` false:
|
|
194
|
+
- A record result FAIL with `recorded` false: `reason` names the step that did not complete. Examples are a `runUntil` that did not reach its target, and a `type` step with too many keys. Change the step.
|
|
179
195
|
- A record result with `executed` 0: the program never read its input from RAM, so the coverage map is empty. The recording can still replay.
|
|
180
|
-
- A FAIL from a replay: compare the two RAM files of `--out` with `
|
|
196
|
+
- A FAIL from a replay: compare the two RAM files of `--out` with `testing.ts diff`. Then examine the first different state item with the c64-emulator skill.
|
|
181
197
|
- A program that loads more files from a disk: the replay core has no disk drive. Use a test scenario in the emulator.
|
|
182
198
|
- A byte that cannot be the same on both sides, for example a read-only collision register: use `exclude` with the reason. Do not use `exclude` to hide a real difference.
|
|
183
199
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Record file
|
|
4
4
|
|
|
5
|
-
A record file (for `
|
|
5
|
+
A record file (for `testing.ts record`) is one JSON object in a project file.
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
@@ -22,47 +22,76 @@ A record file (for `record.ts`) is one JSON object in a project file.
|
|
|
22
22
|
- `program`: a PRG file, relative to the project folder.
|
|
23
23
|
- `symbols`: optional. `knowledge` (default), `none`, or a JSON file: the output of the c64-assembler skill, or an object of names and `$` addresses. The steps use these names.
|
|
24
24
|
- `load`: optional. With `false` (default), the program must load at `$0801` with a BASIC start line, and the C64 types `RUN`. With `true`, the program goes into memory and the C64 stays at READY. Then a `registers` step with `pc` starts the program.
|
|
25
|
-
- `checkpoint`, `state` and `pin`: optional. The comparison that a replay uses when the replay gives none. `checkpoint` is the name or `$` address of a routine. Each entry of the routine is a checkpoint. `state` is 1 to 16 memory items that the checkpoints compare. `pin` is the random state in RAM. An item is a name, a `$` address, or `{ "
|
|
25
|
+
- `checkpoint`, `state` and `pin`: optional. The comparison that a replay uses when the replay gives none. `checkpoint` is the name or `$` address of a routine. Each entry of the routine is a checkpoint. `state` is 1 to 16 memory items that the checkpoints compare. `pin` is the random state in RAM. An item is a name, a `$` address, or `{ "address", "size" }` with a size from 1 to 4096. Give `checkpoint` and `state` together.
|
|
26
26
|
- `inputs`: optional. The I/O addresses (or ranges such as `$de00-$dfff`) whose reads are input. The default is `$dc00` and `$dc01`: the joystick ports and the keyboard matrix. It also has `$d419` to `$d41c` (the paddles, and the noise and envelope of SID voice 3), and `$de00-$dfff` (no device: the last byte on the bus). Add `$d012` or a CIA timer only for a program that reads a seed there and does not wait in a loop on it.
|
|
27
27
|
- `steps`: the steps.
|
|
28
28
|
|
|
29
29
|
## Steps
|
|
30
30
|
|
|
31
|
-
The steps have the form of the steps of a scenario.
|
|
31
|
+
The steps have the form of the steps of a scenario. Two steps behave differently:
|
|
32
|
+
|
|
33
|
+
- `type` puts at most 10 keys into the keyboard queue of the KERNAL. It is an event: a replay does it again.
|
|
34
|
+
- `press` pushes keys of the keyboard matrix. It is input. A scenario has no `press` step.
|
|
35
|
+
|
|
36
|
+
A record file has no `raster` target and no `condition`. `write` and `runUntil` have only the space `c64`.
|
|
32
37
|
|
|
33
38
|
| Step | Effect |
|
|
34
39
|
|---|---|
|
|
35
40
|
| `{ "frames": 300 }` | Run 300 frames (19656 cycles each). |
|
|
36
41
|
| `{ "joystick": { "port": 2, "direction": "left", "fire": true } }` | Set a joystick. It stays in this state until the next joystick step. |
|
|
37
|
-
| `{ "
|
|
38
|
-
| `{ "
|
|
42
|
+
| `{ "press": "RUN\n" }` | Press each key of the text on the keyboard matrix for 3 frames, then release it for 3 frames. |
|
|
43
|
+
| `{ "type": { "mode": "text", "text": "RUN\n" } }` | Put at most 10 keys into the keyboard queue of the KERNAL, after the keys that wait there. |
|
|
44
|
+
| `{ "write": { "address": "next_room", "data": "01" } }` | Write bytes into RAM. |
|
|
39
45
|
| `{ "registers": { "pc": "change_room", "a": 16 } }` | Set registers at the next instruction. `pc` continues the program at that address. |
|
|
40
|
-
| `{ "runUntil": { "
|
|
41
|
-
| `{ "runUntil": { "
|
|
46
|
+
| `{ "runUntil": { "target": { "kind": "address", "address": "main_loop" }, "timeoutFrames": 300 } }` | Run until the CPU gets to an address. |
|
|
47
|
+
| `{ "runUntil": { "target": { "kind": "memory", "address": "room", "operator": "eq", "value": 2 }, "timeoutFrames": 600 } }` | Run until a byte compares true with `value`. The script reads the byte at the end of each frame. |
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
A `press` step can press A to Z, 0 to 9, space, return (`\n`) and `+-.:@,*;=/!"#$%&'()[]<>?`. It presses a to z as A to Z.
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
The keyboard queue of the KERNAL holds 10 keys. If the keys of a `type` step and the keys that wait in the queue do not fit, the session stops. A program that reads the keyboard hardware does not see the keys of a `type` step. Use `press` for that program.
|
|
46
52
|
|
|
47
|
-
`joystick`, `
|
|
53
|
+
`joystick`, `press` and `frames` steps are input and time. The recording keeps their effect in the reads, so a replay does not do these steps again.
|
|
48
54
|
|
|
49
|
-
`write`, `registers` and `runUntil` steps are events. The recording keeps a point in the program for each event. The point is the next read at the input site with the most reads. That site is not in ROM. A replay does the event at the same read. An event that comes directly after another event runs at once after that event.
|
|
55
|
+
`type`, `write`, `registers` and `runUntil` steps are events. The recording keeps a point in the program for each event. The point is the next read at the input site with the most reads. That site is not in ROM. A replay does the event again at the same read. An event that comes directly after another event runs at once after that event.
|
|
50
56
|
|
|
51
57
|
## Play
|
|
52
58
|
|
|
53
|
-
With `--play`, `
|
|
59
|
+
With `--play`, `testing.ts record` does the steps, then a person plays. The page on `http://localhost:<port>/` (`--http-port`, default 6465) shows the screen at 50 frames each second. It has no sound.
|
|
60
|
+
|
|
61
|
+
| Button | Key | Effect |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Start | Enter, on the start screen | The C64 runs. Before Start, the page shows the screen after the steps, and the C64 is frozen. The time before Start is not in the recording. |
|
|
64
|
+
| End and save | F12 | The C64 stops at the frame on the page, and the screen stays frozen. The script writes the recording folder. |
|
|
65
|
+
| Cancel | F11 | The session stops before or during the play. The script writes no folder and gives `recorded: false`. |
|
|
66
|
+
| Port | F9 | The joystick goes to the other port. `--joystick` gives the first port (default 2). |
|
|
67
|
+
| Arrows | F10 | The arrow keys and space change between the joystick and the C64 keys. At the start, they are the joystick. |
|
|
68
|
+
| Keys | | The keyboard changes between the symbolic map (the default) and the positional map. |
|
|
69
|
+
|
|
70
|
+
The buttons are in one row above the screen.
|
|
54
71
|
|
|
55
72
|
| Page input | C64 input |
|
|
56
73
|
|---|---|
|
|
57
|
-
| Arrow keys
|
|
58
|
-
| A gamepad | The
|
|
59
|
-
|
|
|
74
|
+
| Arrow keys and space, when they are the joystick | The joystick: directions and fire. |
|
|
75
|
+
| A gamepad | The joystick: the left stick or the cross for directions, button 1 or 2 for fire. |
|
|
76
|
+
| Arrow keys and space, when they are C64 keys | CRSR up, down, left and right, and SPACE. |
|
|
77
|
+
| Escape, Shift+Escape | RUN/STOP, RUN |
|
|
78
|
+
| Page Up | RESTORE |
|
|
79
|
+
| Tab, left Alt | CTRL, C= |
|
|
80
|
+
| Home, Shift+Home | CLR/HOME, CLR |
|
|
81
|
+
| Backspace, Shift+Backspace | INST/DEL, INST |
|
|
82
|
+
| F1-F8 | F1-F8 |
|
|
83
|
+
|
|
84
|
+
In the symbolic map, a key gives the character on the PC key. a-z give the C64 letters, and A-Z give the letters with SHIFT. The digits and the symbols give the same characters. `£` gives the pound key, `^` gives the up arrow, and the backquote gives the left arrow.
|
|
85
|
+
|
|
86
|
+
In the positional map, a key gives the C64 key at the same place on the keyboard. Each SHIFT key is a C64 SHIFT key, also alone. The C64 keys `+ - @ * : ; =` are on the PC keys `- = [ ] ; ' \`. Insert gives the pound key, Delete gives the up arrow, and the backquote gives the left arrow. Use the positional map when the program reads SHIFT alone or a key with SHIFT.
|
|
87
|
+
|
|
88
|
+
Ctrl and Cmd are not C64 keys. They keep their function in the browser.
|
|
60
89
|
|
|
61
|
-
|
|
90
|
+
The page sends input only between Start and End. The input of the page goes into the reads, as the input of the steps does. RESTORE starts an NMI and is not a read. The recording keeps each RESTORE as an event without a step number. The replay presses RESTORE again at the same read of the program.
|
|
62
91
|
|
|
63
92
|
## Recording folder
|
|
64
93
|
|
|
65
|
-
`
|
|
94
|
+
`testing.ts record` writes these files into the `--out` folder:
|
|
66
95
|
|
|
67
96
|
| File | Content |
|
|
68
97
|
|---|---|
|
|
@@ -74,7 +103,7 @@ A site is the address of the instruction that reads. The values are hex. `<from>
|
|
|
74
103
|
|
|
75
104
|
## Replay
|
|
76
105
|
|
|
77
|
-
`
|
|
106
|
+
`testing.ts replay` replays a recording folder:
|
|
78
107
|
|
|
79
108
|
1. It replays the recording on the original program. The session must end with the recorded cycles and state hash. Else the result is INCONCLUSIVE. It also takes the state at each checkpoint of the original.
|
|
80
109
|
2. With `--rebuild`, it replays the recording on the rebuild. At each checkpoint, the rebuild first gets the pinned values of the original. Then the script compares the state. The cycles can be different.
|
|
@@ -89,7 +118,7 @@ The result is one JSON object:
|
|
|
89
118
|
- `side`: `original` for the check of the original only, `rebuild` with `--rebuild`.
|
|
90
119
|
- `checkpoint`: the routine of the comparison.
|
|
91
120
|
- `checkpoints` and `compared`: the checkpoints of the original, and the checkpoints of the rebuild that were the same.
|
|
92
|
-
- `firstDifference`: on a FAIL, the checkpoint (1 is the first), the cycle on each side, and each state item that is different
|
|
121
|
+
- `firstDifference`: on a FAIL, the checkpoint (1 is the first), the cycle on each side, and each state item that is different. An item has `address`, and the `original` and `rebuild` values as hex.
|
|
93
122
|
- `reason`: why the result is FAIL or INCONCLUSIVE.
|
|
94
123
|
- `movedSites`: the number of sites at a different address in the rebuild.
|
|
95
124
|
- `idleReads`: the input reads of the rebuild that the recording had no value for. Such a read is at a site that the recording does not have, or after its end. Each got the fixed value `$FF` (joystick, keyboard, paddles) or `$00`. An input site that the site map did not find shows here.
|
|
@@ -11,10 +11,10 @@ A scenario is one JSON object in a project file.
|
|
|
11
11
|
{ "frames": 150 },
|
|
12
12
|
{ "load": true },
|
|
13
13
|
{ "registers": { "pc": "start" } },
|
|
14
|
-
{ "runUntil": { "
|
|
14
|
+
{ "runUntil": { "target": { "kind": "address", "address": "main_loop" }, "timeoutFrames": 600 } },
|
|
15
15
|
{ "joystick": { "port": 2, "direction": "left" } },
|
|
16
16
|
{ "frames": 10 },
|
|
17
|
-
{ "observe": "moved", "memory": ["player_x", { "
|
|
17
|
+
{ "observe": "moved", "memory": ["player_x", { "address": "$0400", "size": 40 }], "screen": {} }
|
|
18
18
|
]
|
|
19
19
|
}
|
|
20
20
|
```
|
|
@@ -41,25 +41,30 @@ Before the first step of each side, the script does a hard reset. The CPU stops
|
|
|
41
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
|
-
| `{ "write": { "
|
|
45
|
-
| `{ "registers": { "pc": "start", "a": 0 } }` | Set registers: `pc`, `a`, `x`, `y`
|
|
46
|
-
| `{ "joystick": { "port": 2, "direction": "left", "fire": true } }` | Set a joystick. The joystick stays in this state until the next joystick step. |
|
|
47
|
-
| `{ "type": "RUN\n" }` |
|
|
48
|
-
| `{ "
|
|
49
|
-
| `{ "runUntil": { "
|
|
50
|
-
| `{ "runUntil": { "
|
|
51
|
-
| `{ "runUntil": { "
|
|
44
|
+
| `{ "write": { "address": "seed", "data": "0102" } }` | Write 1 to 4096 bytes into memory. Optional: `space` (`c64` or `drive8`, default `c64`) and `view` (`cpu` or `ram`, default `cpu`). |
|
|
45
|
+
| `{ "registers": { "pc": "start", "a": 0, "flags": { "i": true } } }` | Set registers: `pc`, `a`, `x`, `y`, `sp` and `flags` (`n`, `v`, `b`, `d`, `i`, `z`, `c`). Only `pc` can be a name. |
|
|
46
|
+
| `{ "joystick": { "port": 2, "direction": "left", "fire": true } }` | Set a joystick. `fire` is `false` by default. The joystick stays in this state until the next joystick step. |
|
|
47
|
+
| `{ "type": { "mode": "text", "text": "RUN\n" } }` | Put at most 1024 keys into the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. |
|
|
48
|
+
| `{ "type": { "mode": "petscii", "bytes": [147] } }` | Put PETSCII codes from 1 to 255 into the keyboard queue of the KERNAL. Use it for keys that mode `text` does not have. |
|
|
49
|
+
| `{ "runUntil": { "target": { "kind": "address", "address": "main_loop" }, "timeoutFrames": 600 } }` | Run until the CPU gets to an address. |
|
|
50
|
+
| `{ "runUntil": { "target": { "kind": "memory", "address": "game_state", "operator": "eq", "value": 2 }, "timeoutFrames": 600 } }` | Run until a byte in memory compares true with `value`. `operator` is `eq`, `ne`, `lt`, `lte`, `gt` or `gte`. |
|
|
51
|
+
| `{ "runUntil": { "target": { "kind": "raster", "line": 250 }, "timeoutFrames": 5 } }` | Run until the raster beam gets to a line (0 to 311 for PAL, 0 to 262 for NTSC). Optional: `cycle` from 0 to 64. |
|
|
52
|
+
| `{ "runUntil": { "target": { "kind": "address", "address": "irq_tick", "condition": { "kind": "raster", "line": 250 } }, "timeoutFrames": 600 } }` | Run until the CPU gets to an address while the condition is true. |
|
|
52
53
|
| `{ "snapshot": "snapshots/start.vsf" }` | Restore a snapshot file. Usually with `side`: the snapshot of one program is not a state of the other. |
|
|
53
54
|
| `{ "normalize": true }` | Write pages 0 to 3 and `$00`/`$01` from the entry state in the knowledge. The entry state comes from a capture of the c64-unpacker skill, which `image` of the c64-knowledge skill put in. Then both sides have the same zero page, stack and KERNAL work area. |
|
|
54
55
|
| `{ "observe": "moved", ... }` | Record a checkpoint. |
|
|
55
56
|
|
|
56
57
|
The `port` of a joystick is 1 or 2. The `direction` is `center`, `up`, `down`, `left`, `right`, `up-left`, `up-right`, `down-left` or `down-right`.
|
|
57
58
|
|
|
58
|
-
`
|
|
59
|
+
A `condition` has the `kind` `register`, `memory` or `raster`. A `register` condition has `register` (`a`, `x`, `y` or `sp`), `operator` and `value`. A `memory` condition has `address`, `operator` and `value`. A `raster` condition has `line` and an optional `cycle`.
|
|
60
|
+
|
|
61
|
+
`timeoutFrames` is from 1 to 30000, and 3000 by default. A `runUntil` that does not get to its target in `timeoutFrames` frames makes the result INCONCLUSIVE.
|
|
62
|
+
|
|
63
|
+
A scenario has no `press` step. Only a record file has `press` for keys of the keyboard matrix.
|
|
59
64
|
|
|
60
65
|
Each step except `observe` can have `"side": "original"` or `"side": "rebuild"`. Then only that side does the step. Use it when only one side needs a step, for example a key for the intro of the original. An `observe` step has no `side`, because it compares both sides.
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
Each side finds a name in its own symbols.
|
|
63
68
|
|
|
64
69
|
## Observations
|
|
65
70
|
|
|
@@ -67,7 +72,7 @@ An `observe` step has a checkpoint name. A scenario can have each name only one
|
|
|
67
72
|
|
|
68
73
|
| Key | What the script compares |
|
|
69
74
|
|---|---|
|
|
70
|
-
| `memory` | A list of names, `$` addresses, or objects. An object has `
|
|
75
|
+
| `memory` | A list of names, `$` addresses, or objects. An object has `address`, `size` (1 to 65536, default 1), `view` (`cpu` or `ram`, default `cpu`), `tolerance` (0 to 255, default 0) and `exclude`. The view `ram` reads the RAM under the ROMs and the I/O area. |
|
|
71
76
|
| `registers` | A list of register names: `pc`, `a`, `x`, `y`, `sp`, `flags`. |
|
|
72
77
|
| `screen` | The screen of the rebuild against the screen of the original. `maxMismatchRatio` (default 0) is the largest permitted part of different pixels. `mask` is a list of rectangles (`x`, `y`, `width`, `height`) that the comparison does not include. |
|
|
73
78
|
| `vicii` | The VIC-II state: mode, screen address, scroll and colors. |
|
|
@@ -75,13 +80,13 @@ An `observe` step has a checkpoint name. A scenario can have each name only one
|
|
|
75
80
|
|
|
76
81
|
A `tolerance` is the largest permitted difference for each byte. A `mask` has at most 64 rectangles. A `mask` must not cover all of the screen: PAL is 384×272 pixels and NTSC is 384×247 pixels. A scenario can observe the screen at most at 64 checkpoints.
|
|
77
82
|
|
|
78
|
-
`exclude` is a list of bytes in the observed range that the comparison does not include. Each has `
|
|
83
|
+
`exclude` is a list of bytes in the observed range that the comparison does not include. Each has `address` and a `reason`, for example `{ "address": "$d01e", "reason": "latched sprite collision, not writable" }`. The result lists each excluded byte with its `address` and `reason`.
|
|
79
84
|
|
|
80
85
|
A `pc` value is an address. Compare `pc` only when both programs have the code at the same address.
|
|
81
86
|
|
|
82
87
|
## Runs
|
|
83
88
|
|
|
84
|
-
A run file (for `
|
|
89
|
+
A run file (for `testing.ts run`) has:
|
|
85
90
|
|
|
86
91
|
- `name`: a short name.
|
|
87
92
|
- `start`: `{ "program": "game.prg" }` to autostart a program with a BASIC start line, `{ "program": "game.prg", "load": true }` to load it with the CPU stopped (set `pc` in a step), or `{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" }`.
|
|
@@ -89,9 +94,9 @@ A run file (for `run.ts`) has:
|
|
|
89
94
|
- `videoStandard`: optional, `pal` (default) or `ntsc`.
|
|
90
95
|
- `steps`: the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of a scenario, and the run steps `snapshot`, `save`, `screen`, `coverage`, `trace` and `observe`.
|
|
91
96
|
|
|
92
|
-
A `trace` step has a name, `points` (1 to 8), `frames` (1 to 3000) and an optional `maxHits` (at most 20000). A point has `kind` (`exec`, `read` or `write`), `
|
|
97
|
+
A `trace` step has a name, `points` (1 to 8), `frames` (1 to 3000) and an optional `maxHits` (at most 20000). A point has `kind` (`exec`, `read` or `write`), `start` and an optional `end`. The step writes `<name>.json` with each hit. `results.json` gives the file, the count of hits, the frames and why the trace ended. A trace that a breakpoint, a watchpoint or a JAM ends also ends the run.
|
|
93
98
|
|
|
94
|
-
In a run, a memory item has `
|
|
99
|
+
In a run, a memory item has `address`, `size` (1 to 65536) and `view`. An item larger than 4096 bytes goes into the file `<observe>-<address>.bin`, without the `$`. `"memory": "all"` writes all 64 KiB of RAM to `<observe>.bin`.
|
|
95
100
|
|
|
96
101
|
A name in `save`, `screen`, `coverage` and `observe` has 1 to 64 letters, digits, dots, underscores or hyphens. The files go into the `--out` directory, with `results.json`. It has the observations, the files and the steps done. If a `runUntil` step did not reach its target, it also tells why the run stopped.
|
|
97
102
|
|
|
@@ -102,12 +107,12 @@ Example: visit two rooms from the same start.
|
|
|
102
107
|
"name": "rooms",
|
|
103
108
|
"start": { "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
|
|
104
109
|
"steps": [
|
|
105
|
-
{ "write": { "
|
|
110
|
+
{ "write": { "address": "$0350", "data": "01" } },
|
|
106
111
|
{ "frames": 50 },
|
|
107
112
|
{ "screen": "room1" },
|
|
108
113
|
{ "coverage": "room1" },
|
|
109
114
|
{ "snapshot": ".c64-re-tools/local/snapshots/room0.vsf" },
|
|
110
|
-
{ "write": { "
|
|
115
|
+
{ "write": { "address": "$0350", "data": "02" } },
|
|
111
116
|
{ "frames": 50 },
|
|
112
117
|
{ "screen": "room2" },
|
|
113
118
|
{ "coverage": "room2" }
|
|
@@ -1,37 +1,15 @@
|
|
|
1
|
-
// The c64-testing
|
|
1
|
+
// The diff of c64-testing: what changed between two machine states.
|
|
2
2
|
// Two snapshot files are restored one after the other in an own emulator,
|
|
3
3
|
// which reads RAM, colour RAM and the chip and CPU registers with the readers
|
|
4
4
|
// that the runs use: VICE reads its own snapshot format. Two memory files of a
|
|
5
5
|
// run, or two observations of runs (also two of one run), compare as they are.
|
|
6
|
-
// Exit status: 0, or 2 for a bad call.
|
|
7
|
-
|
|
8
|
-
import { parseArgs } from "node:util";
|
|
9
6
|
|
|
10
7
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
11
8
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
12
|
-
import { readProjectFile,
|
|
9
|
+
import { readProjectFile, writeReported } from "#src/host-client/transfer.ts";
|
|
13
10
|
import { readMemoryInParts, ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { diffStates, type ChangedRange, type MachineState } from "./lib/diff.ts";
|
|
17
|
-
|
|
18
|
-
const USAGE = `diff.ts <a> <b> [--gap N] [--max-listed N] [--ntsc] [--at <address>] [--out <file>]
|
|
19
|
-
|
|
20
|
-
<a> and <b> are two of one kind, relative to the project folder:
|
|
21
|
-
snapshot.vsf the emulator restores each one and reads RAM, colour
|
|
22
|
-
RAM, the VIC-II, CIA, SID and CPU registers
|
|
23
|
-
memory.bin a memory file of a run (observe with memory "all" or
|
|
24
|
-
a size above 4096); --at gives the address of its
|
|
25
|
-
first byte when it is shorter than 64 KiB
|
|
26
|
-
results.json:<name> the observation <name> of a run; both can be in
|
|
27
|
-
one results.json
|
|
28
|
-
|
|
29
|
-
--gap N join changed ranges across at most N equal bytes (default 0)
|
|
30
|
-
--max-listed N list at most N ranges of each part (default 64)
|
|
31
|
-
--ntsc the snapshots are of an NTSC machine (default PAL)
|
|
32
|
-
--out <file> also write all ranges to a project file
|
|
33
|
-
|
|
34
|
-
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
11
|
+
import { listed, UsageError } from "#src/script.ts";
|
|
12
|
+
import { diffStates, type ChangedRange, type MachineState } from "./diff.ts";
|
|
35
13
|
|
|
36
14
|
type Kind = "snapshot" | "memory" | "observation";
|
|
37
15
|
|
|
@@ -91,54 +69,26 @@ async function snapshotState(client: ViceSessionClient, path: string): Promise<M
|
|
|
91
69
|
|
|
92
70
|
const show = (range: ChangedRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end), ...(range.a === undefined ? {} : { a: range.a, b: range.b }) });
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
args: argv,
|
|
97
|
-
strict: true,
|
|
98
|
-
allowPositionals: true,
|
|
99
|
-
options: { gap: { type: "string" }, "max-listed": { type: "string" }, ntsc: { type: "boolean" }, at: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } },
|
|
100
|
-
});
|
|
101
|
-
if (values.help) throw new UsageError("");
|
|
102
|
-
if (positionals.length !== 2) throw new UsageError("give two states to compare");
|
|
103
|
-
const [first, second] = positionals as [string, string];
|
|
72
|
+
/** The differences of two states of one kind: snapshots, memory files or observations of runs. */
|
|
73
|
+
export async function diffFiles(first: string, second: string, options: { gap: number; max: number; ntsc: boolean; at?: number; out?: string }): Promise<Record<string, unknown>> {
|
|
104
74
|
const kind = kindOf(first);
|
|
105
75
|
if (kindOf(second) !== kind) throw new UsageError("give two states of one kind: two snapshots, two memory files or two observations");
|
|
106
|
-
const
|
|
107
|
-
if (value === undefined) return fallback;
|
|
108
|
-
if (!/^\d+$/.test(value)) throw new UsageError(`${what} must be a whole number`);
|
|
109
|
-
return Number(value);
|
|
110
|
-
};
|
|
111
|
-
const gap = number(values.gap, 0, "--gap");
|
|
112
|
-
const max = number(values["max-listed"], 64, "--max-listed");
|
|
113
|
-
if (values.out !== undefined) {
|
|
114
|
-
try {
|
|
115
|
-
resolveProjectPath(values.out);
|
|
116
|
-
} catch {
|
|
117
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
76
|
+
const { gap, max } = options;
|
|
120
77
|
let a: MachineState;
|
|
121
78
|
let b: MachineState;
|
|
122
79
|
if (kind === "observation") {
|
|
123
80
|
a = observation(first);
|
|
124
81
|
b = observation(second);
|
|
125
82
|
} else if (kind === "memory") {
|
|
126
|
-
|
|
127
|
-
if (values.at !== undefined) {
|
|
128
|
-
try {
|
|
129
|
-
base = parseC64Address(values.at);
|
|
130
|
-
} catch {
|
|
131
|
-
throw new UsageError(`--at must be $ followed by four hex digits, not ${values.at}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
83
|
+
const base = options.at ?? 0;
|
|
134
84
|
const read = (path: string) => readProjectFile(path).bytes;
|
|
135
85
|
const [left, right] = [read(first), read(second)];
|
|
136
86
|
if (left.length !== right.length) throw new UsageError(`the memory files have ${left.length} and ${right.length} bytes; give two of one size`);
|
|
137
|
-
if (base + left.length > 0x10000) throw new UsageError("the memory file goes past $ffff from --
|
|
87
|
+
if (base + left.length > 0x10000) throw new UsageError("the memory file goes past $ffff from --address");
|
|
138
88
|
a = { memory: { base, bytes: left } };
|
|
139
89
|
b = { memory: { base, bytes: right } };
|
|
140
90
|
} else {
|
|
141
|
-
const client = await ViceSessionClient.open({ videoStandard:
|
|
91
|
+
const client = await ViceSessionClient.open({ videoStandard: options.ntsc ? "ntsc" : "pal" });
|
|
142
92
|
try {
|
|
143
93
|
a = await snapshotState(client, first);
|
|
144
94
|
b = await snapshotState(client, second);
|
|
@@ -147,21 +97,20 @@ async function run(argv: string[]): Promise<Record<string, unknown>> {
|
|
|
147
97
|
}
|
|
148
98
|
}
|
|
149
99
|
const diff = diffStates(a, b, gap);
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
100
|
+
const rest = options.out ?? "a larger --max-listed, or --out <file>";
|
|
101
|
+
const part = (item: { changedBytes: number; ranges: ChangedRange[] } | undefined) => (item === undefined ? undefined : { changedBytes: item.changedBytes, ...listed(item.ranges, max, show, rest) });
|
|
102
|
+
let written: ReturnType<typeof writeReported> | undefined;
|
|
103
|
+
if (options.out !== undefined) {
|
|
153
104
|
const all = { a: first, b: second, ...(diff.memory === undefined ? {} : { memory: diff.memory.ranges.map(show) }), ...(diff.colorRam === undefined ? {} : { colorRam: diff.colorRam.ranges.map(show) }), values: diff.values ?? [], observed: diff.observed ?? [] };
|
|
154
|
-
|
|
105
|
+
written = writeReported(options.out, `${JSON.stringify(all, null, 2)}\n`, { what: "--out" });
|
|
155
106
|
}
|
|
156
107
|
return {
|
|
157
108
|
a: first,
|
|
158
109
|
b: second,
|
|
159
110
|
...(diff.memory === undefined ? {} : { memory: part(diff.memory) }),
|
|
160
111
|
...(diff.colorRam === undefined ? {} : { colorRam: part(diff.colorRam) }),
|
|
161
|
-
...(diff.values === undefined ? {} : { registers: diff.values
|
|
162
|
-
...(diff.observed === undefined ? {} : { observed: diff.observed
|
|
163
|
-
...(
|
|
112
|
+
...(diff.values === undefined ? {} : { registers: listed(diff.values, max, undefined, rest) }),
|
|
113
|
+
...(diff.observed === undefined ? {} : { observed: listed(diff.observed, max, undefined, rest) }),
|
|
114
|
+
...(written === undefined ? {} : written),
|
|
164
115
|
};
|
|
165
116
|
}
|
|
166
|
-
|
|
167
|
-
await runScript(run, { usage: USAGE });
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// from snapshots that VICE restores, from memory files, or from the
|
|
4
4
|
// observations of runs; this file only compares.
|
|
5
5
|
|
|
6
|
+
import { Refusal } from "#src/errors.ts";
|
|
7
|
+
|
|
6
8
|
export interface MachineState {
|
|
7
9
|
/** Bytes from `base`: all 64 KiB of RAM, or the bytes of one memory file. */
|
|
8
10
|
memory?: { base: number; bytes: Uint8Array };
|
|
@@ -26,7 +28,7 @@ export interface StateDiff {
|
|
|
26
28
|
memory?: { changedBytes: number; ranges: ChangedRange[] };
|
|
27
29
|
colorRam?: { changedBytes: number; ranges: ChangedRange[] };
|
|
28
30
|
values?: Array<{ name: string; a: number; b: number }>;
|
|
29
|
-
observed?: Array<{
|
|
31
|
+
observed?: Array<{ address: string; a: string; b: string }>;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** Changed ranges of two byte arrays of one length, joined across gaps of at most `gap` equal bytes. */
|
|
@@ -52,7 +54,7 @@ export function changedRanges(a: Uint8Array, b: Uint8Array, base: number, gap =
|
|
|
52
54
|
export function diffStates(a: MachineState, b: MachineState, gap = 0): StateDiff {
|
|
53
55
|
const result: StateDiff = {};
|
|
54
56
|
if (a.memory !== undefined && b.memory !== undefined) {
|
|
55
|
-
if (a.memory.base !== b.memory.base || a.memory.bytes.length !== b.memory.bytes.length) throw new
|
|
57
|
+
if (a.memory.base !== b.memory.base || a.memory.bytes.length !== b.memory.bytes.length) throw new Refusal("operation-failed", "the two memory parts do not cover the same addresses");
|
|
56
58
|
result.memory = changedRanges(a.memory.bytes, b.memory.bytes, a.memory.base, gap);
|
|
57
59
|
}
|
|
58
60
|
if (a.colorRam !== undefined && b.colorRam !== undefined) result.colorRam = changedRanges(a.colorRam, b.colorRam, 0xd800, gap);
|
|
@@ -64,7 +66,7 @@ export function diffStates(a: MachineState, b: MachineState, gap = 0): StateDiff
|
|
|
64
66
|
if (a.observed !== undefined && b.observed !== undefined) {
|
|
65
67
|
result.observed = Object.keys(a.observed)
|
|
66
68
|
.filter((at) => b.observed![at] !== undefined && a.observed![at] !== b.observed![at])
|
|
67
|
-
.map((at) => ({ at, a: a.observed![at]!, b: b.observed![at]! }));
|
|
69
|
+
.map((at) => ({ address: at, a: a.observed![at]!, b: b.observed![at]! }));
|
|
68
70
|
}
|
|
69
71
|
return result;
|
|
70
72
|
}
|