@henols/c64-re-tools 2.0.0 → 2.1.0-rc.1

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.
@@ -3,7 +3,8 @@
3
3
  // release is a PRG or an unpacker image.json. With --run, each release
4
4
  // runs in its own emulator, and each listed difference gets its first access
5
5
  // and whether the run changed it. The separate command gives evidence from
6
- // the program in the knowledge. The routine and match commands keep
6
+ // the program in the knowledge, and the leftovers command lists what its
7
+ // unused bytes hold: text, BASIC lines, fills and copies. The routine and match commands keep
7
8
  // routines that several programs share in a library folder and find them in the
8
9
  // program (D71).
9
10
 
@@ -42,10 +43,12 @@ import { runScript, UsageError } from "#src/script.ts";
42
43
  import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./lib/compare.ts";
43
44
  import { findRoutine, libraryFolder, LibraryError, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
44
45
  import { watchRelease, type ImageState, type Start } from "./lib/runtime.ts";
46
+ import { findLeftovers } from "./lib/leftovers.ts";
45
47
  import { separate } from "./lib/separate.ts";
46
48
 
47
49
  const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
48
50
  provenance.ts separate [--out <file>] [--max-listed <n>]
51
+ provenance.ts leftovers [--out <file>] [--min-text <n>] [--max-listed <n>]
49
52
  provenance.ts routine --library <folder> (--module <address> | --start <address> --end <address>)
50
53
  [--name <name>] [--title <text>] [--replace]
51
54
  provenance.ts match --library <folder> [--out <file>] [--max-listed <n>]
@@ -63,6 +66,13 @@ and jump references in knowledge reach each code region from the entry, and what
63
66
  the program reads or executes outside its segments. --out writes all ranges to a file;
64
67
  the result lists at most --max-listed of each list (default 50) and its count.
65
68
 
69
+ leftovers lists what the program in the knowledge holds besides its working
70
+ code: text in PETSCII, screen codes, ASCII or with bit 7 set (Apple II) and
71
+ assembler source (at least --min-text characters, default 8), BASIC lines away
72
+ from the BASIC start, fills, untouched bytes outside code, exact copies of
73
+ earlier bytes, and code regions that no call or jump reaches and no run executed. Each finding has the stages and the accesses of
74
+ its bytes. --out writes all findings to a file.
75
+
66
76
  routine saves a routine of the program in the knowledge to a library: a plain folder of routine
67
77
  files, beside the projects (--library, relative to the working directory, as
68
78
  --project). It takes a module of the knowledge, or --start and --end. Its
@@ -195,6 +205,7 @@ async function run(argv: string[]): Promise<unknown> {
195
205
  steps: { type: "string" },
196
206
  out: { type: "string" },
197
207
  "max-listed": { type: "string" },
208
+ "min-text": { type: "string" },
198
209
  library: { type: "string" },
199
210
  module: { type: "string" },
200
211
  start: { type: "string" },
@@ -216,6 +227,11 @@ async function run(argv: string[]): Promise<unknown> {
216
227
  if (positionals.length !== 1) throw new UsageError("separate takes no release: it reads the program from the knowledge");
217
228
  return separateCommand(values.out, values["max-listed"]);
218
229
  }
230
+ if (positionals[0] === "leftovers") {
231
+ only("leftovers", ["out", "min-text", "max-listed"]);
232
+ if (positionals.length !== 1) throw new UsageError("leftovers takes no release: it reads the program from the knowledge");
233
+ return leftoversCommand(values.out, values["min-text"], values["max-listed"]);
234
+ }
219
235
  if (positionals[0] === "routine") {
220
236
  only("routine", ["library", "module", "start", "end", "name", "title", "replace"]);
221
237
  if (positionals.length !== 1) throw new UsageError("routine takes no image: it reads the program from the knowledge");
@@ -347,10 +363,8 @@ function stagesOf(stages: Array<Record<string, unknown>> | undefined): Array<{ e
347
363
  }));
348
364
  }
349
365
 
350
- /** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
351
- function separateCommand(out: string | undefined, maxListed: string | undefined): unknown {
352
- const max = maxListed ?? "50";
353
- if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
366
+ /** The program in the knowledge with its stages and coverage, and the separation of its bytes. */
367
+ function programEvidence() {
354
368
  const db = openForRead();
355
369
  let program: ReturnType<typeof requireProgram>;
356
370
  let coverage: Uint8Array | undefined;
@@ -365,7 +379,6 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
365
379
  db?.close();
366
380
  }
367
381
  const stages = stagesOf(program.stages);
368
- const loaded = { entry: program.entry, stages };
369
382
  const found = separate({
370
383
  segments: program.segments,
371
384
  memory: program.memory,
@@ -375,6 +388,17 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
375
388
  codeRegions: regions,
376
389
  references,
377
390
  });
391
+ return { program, stages, coverage, found };
392
+ }
393
+
394
+ const NO_STAGES = "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none.";
395
+
396
+ /** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
397
+ function separateCommand(out: string | undefined, maxListed: string | undefined): unknown {
398
+ const max = maxListed ?? "50";
399
+ if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
400
+ const { program, stages, coverage, found } = programEvidence();
401
+ const loaded = { entry: program.entry, stages };
378
402
  const show = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
379
403
  const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
380
404
  const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
@@ -386,7 +410,7 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
386
410
  return {
387
411
  ...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
388
412
  ...(lastStage === undefined
389
- ? { noStages: "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none." }
413
+ ? { noStages: NO_STAGES }
390
414
  : {
391
415
  stages: lastStage + 1,
392
416
  bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
@@ -403,6 +427,54 @@ function separateCommand(out: string | undefined, maxListed: string | undefined)
403
427
  };
404
428
  }
405
429
 
430
+ /** The longest text that the result shows; --out has all of it. */
431
+ const SHOWN_TEXT = 120;
432
+
433
+ /** leftovers: what the program in the knowledge holds besides its working code, with all findings in --out and the first ones in the result. */
434
+ function leftoversCommand(out: string | undefined, minText: string | undefined, maxListed: string | undefined): unknown {
435
+ const max = maxListed ?? "50";
436
+ if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
437
+ const min = minText ?? "8";
438
+ if (!/^\d{1,3}$/.test(min) || Number(min) < 4) throw new UsageError(`--min-text must be a whole number of 4 or more, not ${min}`);
439
+ const { program, stages, coverage, found } = programEvidence();
440
+ const leftovers = findLeftovers({
441
+ segments: program.segments,
442
+ memory: program.memory,
443
+ separation: found,
444
+ ...(program.segments.some((segment) => segment.start === 0x0801) ? { basicStart: 0x0801 } : {}),
445
+ minText: Number(min),
446
+ });
447
+ const show = <T extends Range & { stages?: number[]; access?: string[] }>({ start, end, stages: wrote, access, ...rest }: T) => ({
448
+ start: formatC64Address(start),
449
+ end: formatC64Address(end),
450
+ ...rest,
451
+ ...(wrote === undefined ? {} : { stages: wrote }),
452
+ ...(access === undefined ? {} : { access }),
453
+ });
454
+ const all = {
455
+ text: leftovers.text.map(show),
456
+ basic: leftovers.basic.map(({ linkedAt, ...rest }) => ({ ...show(rest), linkedAt: formatC64Address(linkedAt) })),
457
+ fill: leftovers.fill.map(({ pattern, ...rest }) => ({ ...show(rest), pattern: hexBytes(pattern) })),
458
+ residue: leftovers.residue.map((range) => ({ ...show(range), bytes: hexBytes([...program.memory.subarray(range.start, Math.min(range.end, range.start + 15) + 1)]) + (range.end - range.start >= 16 ? " ..." : "") })),
459
+ copies: leftovers.copies.map(({ copyOf, ...rest }) => ({ ...show(rest), copyOf: formatC64Address(copyOf) })),
460
+ deadCode: leftovers.deadCode.map(show),
461
+ };
462
+ const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
463
+ if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify(all, null, 1)}\n`), { what: "--out" });
464
+ return {
465
+ ...(program.entry === undefined ? { noEntry: "The program in the knowledge has no entry, so deadCode is empty." } : { entry: formatC64Address(program.entry) }),
466
+ ...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
467
+ ...(coverage === undefined ? { noCoverage: "The knowledge has no coverage, so residue is empty and no finding has access. Add coverage with coverage of the c64-knowledge skill." } : {}),
468
+ text: listed(all.text.map((item) => (item.text.length > SHOWN_TEXT ? { ...item, text: `${item.text.slice(0, SHOWN_TEXT)} ...`, length: item.text.length } : item))),
469
+ basic: listed(all.basic),
470
+ fill: listed(all.fill),
471
+ residue: listed(all.residue),
472
+ copies: listed(all.copies),
473
+ deadCode: listed(all.deadCode),
474
+ ...(out === undefined ? {} : { out }),
475
+ };
476
+ }
477
+
406
478
  const SEMANTIC: readonly Origin[] = ["user", "llm"];
407
479
 
408
480
  const platform = () => new Map(platformNames().map((entry) => [entry.name, entry.address]));
@@ -63,6 +63,7 @@ Select the path for the scope:
63
63
  | The routines, code, data and references of the program | c64-static-analysis |
64
64
  | The platform meaning of an address or a register | c64-memory-map |
65
65
  | Differences between releases of the program | c64-provenance |
66
+ | Leftovers of the development, a crack or a recrack: texts, old copies, unused bytes, dead code | c64-provenance `leftovers` |
66
67
  | A routine that another program of the workspace has, for example a music driver | c64-provenance, with the library |
67
68
  | Source code from the knowledge | c64-source |
68
69
  | Source code to build | c64-assembler |
@@ -78,19 +79,19 @@ Do the stages in this sequence. Go to the next stage only when its exit check is
78
79
 
79
80
  | Stage | Work | Exit check |
80
81
  |---|---|---|
81
- | 1. Acceptance | Ask the user for the criteria of the rebuild. Offer these: identical source, a verified loader, passed c64-testing scenarios, and a list of the code that no run executed. Also offer documentation of the flows, tables, formats and graphics. | The user gave the criteria. |
82
+ | 1. Acceptance | Ask the user for the criteria of the rebuild. Offer these: identical source, a verified loader, passed c64-testing scenarios, passed replays of the recordings, and a list of the code that no run executed. Also offer documentation of the flows, tables, formats and graphics. | The user gave the criteria. |
82
83
  | 2. Get | c64-disk: list the disk. Extract the start file into `original/`. | The start file is in the project. |
83
84
  | 3. Start | c64-basic: find the SYS address of the BASIC start. | You have the entry address. |
84
85
  | 4. Unpack | c64-unpacker: examine the program. Capture a packed program into `.c64-re-tools/local/`. Then c64-knowledge: put the capture, or the PRG of a program without a packer, into the knowledge with `image`. | `program` of c64-knowledge shows the segments and the entry. |
85
- | 5. Run | c64-emulator and the run of c64-testing: run each part that the scope names. Write a coverage map for each part into `.c64-re-tools/local/`. Then c64-knowledge: add the maps with `coverage`. | `program` of c64-knowledge lists a coverage map for each part that the scope names. |
86
+ | 5. Run | First record: ask the user to play each part that the scope names, with `record.ts --play` of c64-testing. Tell the user which parts. Record a part yourself only when the sessions of the user do not have it. Put the recordings in `tests/recordings/`. Then c64-emulator and the run of c64-testing for the parts that a recording cannot have. Write a coverage map for each part into `.c64-re-tools/local/`. Then c64-knowledge: add the maps and the `coverage.bin` of each recording with `coverage`. | `program` of c64-knowledge lists a coverage map for each part that the scope names. |
86
87
  | 6. Analyze | c64-static-analysis. It reads the program and the coverage from the knowledge. | No coverage conflict is open. |
87
88
  | 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. |
88
89
  | 8. Generate | c64-source into `src/`. For an unpacked program, add `--loader`. | `check.identical` is true. For an unpacked program, `loader.verified` is true. |
89
- | 9. Build and test | c64-assembler with `--compare` into `build/`. c64-testing scenarios in `tests/`. | The criteria of stage 1 are true. |
90
+ | 9. Build and test | c64-assembler with `--compare` into `build/`. c64-testing scenarios in `tests/`. Replay each recording on the rebuild with `replay.ts`: give the checkpoint routine, the state and the random state (`--pin`) with the names of stage 7. | The criteria of stage 1 are true. |
90
91
  | 10. Document | When the criteria ask for documentation: c64-docs into `README.md` and `docs/`. | `link.ts --check` of c64-docs gives no problem. |
91
92
  | 11. Hand over | Find each file that you made outside `.c64-re-tools/local/` and that is not a deliverable. Move it into `.c64-re-tools/local/`. Then give the answer (below). | Each file that you made outside `.c64-re-tools/local/` is a deliverable. |
92
93
 
93
- In stage 5, if you cannot get to a part of the program, ask the user how to get to it. Do stages 7 and 8 again until the source is clear. Each run of c64-source writes the source again.
94
+ In stage 5, make the recordings before you change the program. A recording needs no names, and its coverage gives the analysis of stage 6 more code. If you cannot get to a part of the program, ask the user how to get to it. Do stages 7 and 8 again until the source is clear. Each run of c64-source writes the source again.
94
95
 
95
96
  After stage 7, save each routine that other programs can have to the library with c64-provenance `routine`. Examples are a music driver, a decruncher and a loader. Ask the user before you make a library.
96
97
 
@@ -100,6 +101,7 @@ After stage 7, save each routine that other programs can have to the library wit
100
101
  - the generated source
101
102
  - the built program and the loader
102
103
  - the c64-testing scenarios
104
+ - the c64-testing recordings in `tests/recordings/`
103
105
  - the documentation in `README.md` and `docs/`, when the criteria ask for it
104
106
  - the runs in `runs/` that the knowledge gives as evidence
105
107
  - the scripts in `tools/` for the formats of this program, when you wrote them.
@@ -124,7 +126,7 @@ If the project has no layout, use this one. No script checks it. Each script tak
124
126
  original/ the disk image or the program as you got it
125
127
  src/ the generated source from c64-source
126
128
  build/ programs from c64-assembler
127
- tests/ c64-testing scenarios
129
+ tests/ c64-testing scenarios, and the recordings in tests/recordings/
128
130
  runs/ the runs that the knowledge gives as evidence
129
131
  docs/ the documentation from c64-docs, with the reference in docs/reference/
130
132
  tools/ scripts for the formats of this program only
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: c64-testing
3
- description: Use this skill to test if a rebuilt or changed C64 program behaves as the original. It runs one scenario on the original and then on the rebuild in an emulator. It compares memory, registers, chip state and the screen with explicit rules, and gives PASS, FAIL or INCONCLUSIVE. It also makes a checklist for a human playtest. Use its run command to explore a program with many steps, for example to visit each room or level. A run keeps screens, coverage maps and snapshots as files. Do not use it to find the cause of a difference.
3
+ description: Use this skill to test if a rebuilt or changed C64 program behaves as the original. It runs one scenario on the original and then on the rebuild in an emulator. It compares memory, registers, chip state and the screen with explicit rules, and gives PASS, FAIL or INCONCLUSIVE. It also makes a checklist for a human playtest. Use its run command to explore a program with many steps, for example to visit each room or level. A run keeps screens, coverage maps and snapshots as files. Use its record and replay commands to keep a long play session of the original and replay it on each rebuild. Do not use it to find the cause of a difference.
4
4
  ---
5
5
 
6
6
  # c64-testing
@@ -54,6 +54,53 @@ A memory item can have `"view": "ram"`. Then it reads the RAM under the ROMs and
54
54
 
55
55
  See [references/scenario-format.md](references/scenario-format.md), section Runs. The script prints one JSON object. The exit status is 0 when all steps ran, 1 when a step did not reach its target, and 2 for a bad call. A long run can need more time than one shell call. Then run it in the background.
56
56
 
57
+ ### Recordings
58
+
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
+
61
+ The record and replay scripts 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
+
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
+
65
+ ```
66
+ node <skill>/scripts/record.ts tests/record/start.json --out tests/recordings/play1 --play --joystick 1
67
+ ```
68
+
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
+ - 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
+ - On the page, the arrow keys, space and a gamepad move the joystick of `--joystick` (default 2). The other keys go to the C64 keyboard.
72
+ - The script runs until the user clicks Stop on the page. Run it in the background. Then it writes the recording folder.
73
+
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 `at` or `memory`.
75
+
76
+ ```
77
+ node <skill>/scripts/record.ts tests/record/rooms.json --out tests/recordings/rooms
78
+ node <skill>/scripts/replay.ts tests/recordings/rooms --checkpoint main_loop --state room --state lives --pin random_ptr:2 --rebuild build/game.prg --symbols build/game.json
79
+ ```
80
+
81
+ - The record script writes the recording folder. The exit status is 0 when it recorded the session, 1 when the session did not complete, and 2 for a bad call. `executed` gives the instructions in the coverage map.
82
+ - The replay script 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
+ - `--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
+ - The replay script gives PASS, FAIL or INCONCLUSIVE, with the exit status 0, 1 or 3. A FAIL gives the first checkpoint that is different and the state items that are different.
85
+ - With `--out`, a FAIL writes the RAM of the two sides at that checkpoint. Compare the two files with `diff.ts`.
86
+ - Without `--rebuild`, the replay script only replays the recording on the original.
87
+
88
+ 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
+
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 the step again at the same read of the program.
91
+
92
+ 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
+
94
+ Control each source of random numbers:
95
+
96
+ - A program that reads a seed from the SID gets the recorded value. You do not need to do more.
97
+ - A program that reads a seed from the raster line or a CIA timer: add the address to `inputs` of the record file. Do this only when the program does not wait in a loop on that address.
98
+ - A program that keeps its random state in RAM: give that state with `--pin`. An example is a pointer into a random table that moves each frame. At each checkpoint, the rebuild gets the values of the original there. A FAIL where an enemy or an event is different, but the input is the same, often shows that a pin is missing.
99
+
100
+ Put a `runUntil` step with the checkpoint routine before a `registers` step. A `registers` step that sets the pc during an interrupt stops the program.
101
+
102
+ Commit the recording folder with the scenarios. A recording is not knowledge.
103
+
57
104
  ### Differences
58
105
 
59
106
  ```
@@ -100,10 +147,15 @@ Read:
100
147
  Automatic write:
101
148
  - none.
102
149
 
150
+ Write with c64-knowledge:
151
+ - the coverage of a recording: `knowledge.ts coverage tests/recordings/<name>/coverage.bin`.
152
+ - a finding that a recording shows, with `--evidence test:tests/recordings/<name>/recording.json`.
153
+
103
154
  Do not store:
104
155
  - test results
105
156
  - screenshots
106
- - playtest results.
157
+ - playtest results
158
+ - recordings.
107
159
 
108
160
  ## Result
109
161
 
@@ -120,6 +172,13 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
120
172
  - 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.
121
173
  - 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.
122
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`, or on an address and a raster line together.
175
+ - INCONCLUSIVE, "does not reproduce": the original does not end the session with the recorded state. Record the session again with this `c64run`.
176
+ - INCONCLUSIVE, "never came to the checkpoint": the original did not execute the checkpoint routine in the session. Give another routine.
177
+ - 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: a step did not reach its target. Change the step.
179
+ - 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 `diff.ts`. Then examine the first different state item with the c64-emulator skill.
181
+ - A program that loads more files from a disk: the replay core has no disk drive. Use a test scenario in the emulator.
123
182
  - 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.
124
183
 
125
184
  Do not add a mask or a tolerance only to make a test pass. A tolerance must have a reason in the behavior of the program.
@@ -0,0 +1,96 @@
1
+ # Recording format
2
+
3
+ ## Record file
4
+
5
+ A record file (for `record.ts`) is one JSON object in a project file.
6
+
7
+ ```json
8
+ {
9
+ "name": "first rooms",
10
+ "program": "original/game.prg",
11
+ "steps": [
12
+ { "frames": 300 },
13
+ { "joystick": { "port": 2, "direction": "center", "fire": true } },
14
+ { "frames": 30 },
15
+ { "joystick": { "port": 2, "direction": "right" } },
16
+ { "frames": 500 }
17
+ ]
18
+ }
19
+ ```
20
+
21
+ - `name`: a short text.
22
+ - `program`: a PRG file, relative to the project folder.
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
+ - `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 `{ "at", "size" }` with a size from 1 to 4096. Give `checkpoint` and `state` together.
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
+ - `steps`: the steps.
28
+
29
+ ## Steps
30
+
31
+ The steps have the form of the steps of a scenario.
32
+
33
+ | Step | Effect |
34
+ |---|---|
35
+ | `{ "frames": 300 }` | Run 300 frames (19656 cycles each). |
36
+ | `{ "joystick": { "port": 2, "direction": "left", "fire": true } }` | Set a joystick. It stays in this state until the next joystick step. |
37
+ | `{ "type": "RUN\n" }` | Press each key of the text on the keyboard matrix, for 3 frames, then release it for 3 frames. |
38
+ | `{ "write": { "at": "next_room", "bytes": [1] } }` | Write bytes into RAM. |
39
+ | `{ "registers": { "pc": "change_room", "a": 16 } }` | Set registers at the next instruction. `pc` continues the program at that address. |
40
+ | `{ "runUntil": { "at": "main_loop" }, "timeoutFrames": 300 }` | Run until the CPU gets to an address. |
41
+ | `{ "runUntil": { "memory": { "at": "room", "equals": 2 } }, "timeoutFrames": 600 }` | Run until a byte has a value. The script reads the byte at the end of each frame. |
42
+
43
+ The replay core has no `runUntil` with `raster`.
44
+
45
+ A `type` step can press A to Z, 0 to 9, space, return (`\n`) and `+-.:@,*;=/!"#$%&'()[]<>?`. It types a to z as A to Z.
46
+
47
+ `joystick`, `type` 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
+
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.
50
+
51
+ ## Play
52
+
53
+ With `--play`, `record.ts` 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.
54
+
55
+ | Page input | C64 input |
56
+ |---|---|
57
+ | Arrow keys, space | The joystick of `--joystick` (default 2): directions and fire. |
58
+ | A gamepad | The same joystick: the left stick or the cross for directions, button 1 or 2 for fire. |
59
+ | A-Z, 0-9, the symbols of a `type` step, Return, Backspace (DEL), Escape (RUN/STOP), F1-F8 | The C64 keyboard. |
60
+
61
+ Stop on the page ends the session, and the script writes the recording folder. The input of the page goes into the reads, as the input of the steps does.
62
+
63
+ ## Recording folder
64
+
65
+ `record.ts` writes these files into the `--out` folder:
66
+
67
+ | File | Content |
68
+ |---|---|
69
+ | `recording.json` | The program and its SHA-256, the symbols, and the comparison of the record file. Also the inputs, the events, the input sites, the cycles of the session and the state hash at its end. |
70
+ | `reads.txt` | One line `<site> <addr> <from> <value>` for each change of the value at a site. From the read with the index `<from>`, the reads at `<site>` from `<addr>` get `<value>`. One line `end <site> <addr> <count>` for each site gives its number of reads. |
71
+ | `coverage.bin` | The coverage map of the session: 65536 bytes, one for each address, in the format of the coverage maps of the emulator. It starts at the first input read of code in RAM, thus after a loader or a relocation. |
72
+
73
+ A site is the address of the instruction that reads. The values are hex. `<from>` and `<count>` are decimal.
74
+
75
+ ## Replay
76
+
77
+ `replay.ts` replays a recording folder:
78
+
79
+ 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
+ 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.
81
+
82
+ The comparison comes from `--checkpoint`, `--state` and `--pin`, or else from the record file. A recording without names (`symbols` is `none`) takes the names of the knowledge at the time of the replay.
83
+
84
+ For the rebuild, the script finds each name with the names of the rebuild (`--symbols`). A `$` address is an address of the original. It moves with the name before it in the original: the script finds the same name in the rebuild and adds the same offset. Code moves with a code label, and data with any name. A site in ROM does not move. Give a state item a name, or the state moves with the code before it.
85
+
86
+ The result is one JSON object:
87
+
88
+ - `result`: PASS, FAIL or INCONCLUSIVE.
89
+ - `side`: `original` for the check of the original only, `rebuild` with `--rebuild`.
90
+ - `checkpoint`: the routine of the comparison.
91
+ - `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, as hex.
93
+ - `reason`: why the result is FAIL or INCONCLUSIVE.
94
+ - `movedSites`: the number of sites at a different address in the rebuild.
95
+ - `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.
96
+ - `files`: with `--out`, the RAM of each side at the first different checkpoint, `original-<n>.bin` and `rebuild-<n>.bin`.
@@ -0,0 +1,144 @@
1
+ // A person plays the program in the replay core: a page on localhost shows
2
+ // the screen and sends the keyboard and gamepad. The core runs one frame each
3
+ // 20 ms. The reads of the program keep the input, so the recording needs
4
+ // nothing from the page. Stop on the page ends the session.
5
+
6
+ import { createServer, type ServerResponse } from "node:http";
7
+
8
+ import { PAL_FRAME_CYCLES, type C64RunSession } from "#src/native/c64run.ts";
9
+
10
+ const FRAME_MS = 20;
11
+
12
+ export interface PlayOptions {
13
+ /** The TCP port of the page; 0 takes a free one. */
14
+ httpPort: number;
15
+ /** The control port that the arrow keys, space and the gamepad move. */
16
+ joystick: 1 | 2;
17
+ /** Called with the address of the page when it listens. */
18
+ ready: (url: string) => void;
19
+ }
20
+
21
+ /** The C64 keys of the page: KeyboardEvent.key → the key code of the replay core. */
22
+ const KEYS: Record<string, number> = { Enter: 0x0d, Backspace: 0x01, F1: 0xf1, F2: 0xf2, F3: 0xf3, F4: 0xf4, F5: 0xf5, F6: 0xf6, F7: 0xf7, F8: 0xf8, Escape: 0x03 };
23
+
24
+ const PAGE = String.raw`<!doctype html>
25
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
26
+ <title>c64-testing record</title>
27
+ <style>
28
+ body{margin:0;background:#111;color:#ddd;font:14px system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px}
29
+ canvas{image-rendering:pixelated;width:min(784px,100%);aspect-ratio:392/272;background:#000}
30
+ button{font:inherit;padding:6px 16px}
31
+ </style></head><body>
32
+ <canvas id="screen" width="392" height="272"></canvas>
33
+ <div>Joystick: arrow keys and space, or a gamepad. Other keys go to the C64 keyboard. <span id="state">connecting</span></div>
34
+ <button id="stop">Stop and save the recording</button>
35
+ <script>
36
+ const canvas = document.getElementById("screen"), context = canvas.getContext("2d"), state = document.getElementById("state");
37
+ let palette = [], image = context.createImageData(392, 272), frames = 0;
38
+ const source = new EventSource("frames");
39
+ source.addEventListener("palette", (event) => { palette = event.data.split(" ").map((hex) => [0, 2, 4].map((at) => parseInt(hex.slice(at, at + 2), 16))); });
40
+ source.onmessage = (event) => {
41
+ const [width, height, data] = event.data.split(" ");
42
+ if (image.width != width) { canvas.width = width; canvas.height = height; image = context.createImageData(+width, +height); }
43
+ const bytes = atob(data);
44
+ for (let at = 0; at < bytes.length; at++) { const rgb = palette[bytes.charCodeAt(at)] || [0, 0, 0]; image.data.set(rgb, at * 4); image.data[at * 4 + 3] = 255; }
45
+ context.putImageData(image, 0, 0);
46
+ state.textContent = "frame " + ++frames;
47
+ };
48
+ source.onerror = () => { state.textContent = "the session ended"; };
49
+ const held = new Set();
50
+ const bits = { ArrowUp: 1, ArrowDown: 2, ArrowLeft: 4, ArrowRight: 8, " ": 16 };
51
+ let sent = -1;
52
+ function send(path) { fetch(path, { method: "POST" }); }
53
+ function joystick() {
54
+ let mask = 0;
55
+ for (const key of held) mask |= bits[key] || 0;
56
+ for (const pad of navigator.getGamepads ? navigator.getGamepads() : []) {
57
+ if (!pad) continue;
58
+ const [x, y] = pad.axes;
59
+ if (y < -0.5 || pad.buttons[12]?.pressed) mask |= 1;
60
+ if (y > 0.5 || pad.buttons[13]?.pressed) mask |= 2;
61
+ if (x < -0.5 || pad.buttons[14]?.pressed) mask |= 4;
62
+ if (x > 0.5 || pad.buttons[15]?.pressed) mask |= 8;
63
+ if (pad.buttons[0]?.pressed || pad.buttons[1]?.pressed) mask |= 16;
64
+ }
65
+ if (mask !== sent) { sent = mask; send("joystick?mask=" + mask); }
66
+ requestAnimationFrame(joystick);
67
+ }
68
+ requestAnimationFrame(joystick);
69
+ function key(event, down) {
70
+ if (event.key in bits) { event.preventDefault(); down ? held.add(event.key) : held.delete(event.key); return; }
71
+ const name = event.key.length === 1 ? event.key.toUpperCase() : event.key;
72
+ if (name.length === 1 || /^(Enter|Backspace|Escape|F[1-8])$/.test(name)) { event.preventDefault(); if (!event.repeat) send("key?name=" + encodeURIComponent(name) + "&down=" + (down ? 1 : 0)); }
73
+ }
74
+ addEventListener("keydown", (event) => key(event, true));
75
+ addEventListener("keyup", (event) => key(event, false));
76
+ document.getElementById("stop").onclick = () => { send("stop"); state.textContent = "saving"; };
77
+ </script></body></html>`;
78
+
79
+ /** The key code of a key name from the page, or undefined for a key that the C64 does not have. */
80
+ function keyCode(name: string): number | undefined {
81
+ if (name in KEYS) return KEYS[name];
82
+ return name.length === 1 && /[A-Z0-9 +\-.:@,*;=/!"#$%&'()[\]<>?]/.test(name) ? name.charCodeAt(0) : undefined;
83
+ }
84
+
85
+ /** Runs the core in real time with the page as its joystick and keyboard, until Stop. */
86
+ export async function play(core: C64RunSession, options: PlayOptions): Promise<void> {
87
+ const palette = (await core.command("palette")).split(" ").slice(1).join(" ");
88
+ const viewers = new Set<ServerResponse>();
89
+ const pending: string[] = [];
90
+ let mask = 0;
91
+ let stop: () => void = () => {};
92
+ const stopped = new Promise<void>((resolve) => (stop = resolve));
93
+ const server = createServer((request, response) => {
94
+ const url = new URL(request.url ?? "/", "http://localhost");
95
+ if (request.method === "GET" && url.pathname === "/") {
96
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(PAGE);
97
+ } else if (request.method === "GET" && url.pathname === "/frames") {
98
+ response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store" });
99
+ response.write(`event: palette\ndata: ${palette}\n\n`);
100
+ viewers.add(response);
101
+ request.on("close", () => viewers.delete(response));
102
+ } else if (request.method === "POST" && url.pathname === "/joystick") {
103
+ mask = Number(url.searchParams.get("mask")) & 0x1f;
104
+ response.writeHead(204).end();
105
+ } else if (request.method === "POST" && url.pathname === "/key") {
106
+ const code = keyCode(url.searchParams.get("name") ?? "");
107
+ if (code !== undefined) pending.push(`key ${code.toString(16)} ${url.searchParams.get("down") === "1" ? "down" : "up"}`);
108
+ response.writeHead(204).end();
109
+ } else if (request.method === "POST" && url.pathname === "/stop") {
110
+ response.writeHead(204).end();
111
+ stop();
112
+ } else {
113
+ response.writeHead(404).end();
114
+ }
115
+ });
116
+ await new Promise<void>((resolve) => server.listen(options.httpPort, "127.0.0.1", resolve));
117
+ const address = server.address();
118
+ options.ready(`http://localhost:${typeof address === "object" && address !== null ? address.port : options.httpPort}/`);
119
+ let running = true;
120
+ void stopped.then(() => (running = false));
121
+ let tick = (await core.status("status")).tick;
122
+ let sent = -1;
123
+ try {
124
+ while (running) {
125
+ const started = Date.now();
126
+ if (mask !== sent) {
127
+ await core.command(options.joystick === 1 ? `joy ${mask.toString(16)} 0` : `joy 0 ${mask.toString(16)}`);
128
+ sent = mask;
129
+ }
130
+ while (pending.length > 0) await core.command(pending.shift()!);
131
+ tick += PAL_FRAME_CYCLES;
132
+ await core.status(`run ${tick}`);
133
+ if (viewers.size > 0) {
134
+ const frame = (await core.command("frame")).slice(3);
135
+ for (const viewer of viewers) viewer.write(`data: ${frame}\n\n`);
136
+ }
137
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, FRAME_MS - (Date.now() - started))));
138
+ }
139
+ } finally {
140
+ await core.command("joy 0 0");
141
+ for (const viewer of viewers) viewer.end();
142
+ await new Promise((resolve) => server.close(resolve));
143
+ }
144
+ }