@henols/c64-re-tools 2.0.0-rc.6 → 2.0.0-rc.8
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 +1 -1
- package/skills/c64-basic/SKILL.md +2 -2
- package/skills/c64-basic/scripts/basic.ts +1 -1
- package/skills/c64-disk/SKILL.md +3 -3
- package/skills/c64-disk/scripts/disk.ts +2 -1
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +2 -2
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-provenance/SKILL.md +10 -5
- package/skills/c64-provenance/scripts/compare.ts +35 -6
- package/skills/c64-provenance/scripts/provenance.ts +35 -15
- package/skills/c64-provenance/scripts/separate.ts +12 -4
- package/skills/c64-reverse-engineering/SKILL.md +2 -2
- package/skills/c64-source/SKILL.md +18 -11
- package/skills/c64-source/scripts/generate.ts +55 -10
- package/skills/c64-source/scripts/loader.ts +253 -6
- package/skills/c64-source/scripts/source.ts +46 -11
- package/skills/c64-static-analysis/SKILL.md +4 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
- package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
- package/skills/c64-testing/SKILL.md +1 -1
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/scenario.ts +9 -7
- package/skills/c64-unpacker/SKILL.md +11 -9
- package/skills/c64-unpacker/scripts/loads.ts +16 -4
- package/skills/c64-unpacker/scripts/machine.ts +13 -1
- package/skills/c64-unpacker/scripts/stages.ts +66 -3
- package/skills/c64-unpacker/scripts/unpack.ts +135 -42
- package/src/c64.ts +19 -5
- package/src/cli/status.ts +26 -3
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/petcat.ts +5 -2
- package/src/host/tools/status.ts +6 -3
- package/src/host/vice/adapter.ts +37 -9
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +7 -2
- package/src/host/vice/screen.ts +3 -2
- package/src/host/vice/session.ts +68 -15
- package/src/host-client/autostart.ts +11 -1
- package/src/host-client/connect.ts +10 -10
- package/src/host-client/coverage.ts +19 -10
- package/src/host-client/tools.ts +6 -6
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/schema.ts +7 -0
- package/src/knowledge/write.ts +5 -0
- package/src/mcp/tools/debug.ts +9 -5
- package/src/mcp/tools/machine.ts +4 -1
- package/src/mcp/tools/media.ts +7 -1
- package/src/mcp/tools/video.ts +7 -2
- package/src/native/discover.ts +2 -1
- package/src/platform.ts +3 -2
- package/src/protocol/tools.ts +19 -4
- package/src/protocol/vice.ts +56 -5
|
@@ -9,6 +9,7 @@ import { dirname } from "node:path";
|
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
|
|
11
11
|
import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
|
|
12
|
+
import { joinCoverage } from "#src/host-client/coverage.ts";
|
|
12
13
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
13
14
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
14
15
|
import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
|
|
@@ -21,7 +22,7 @@ import { currentRevision } from "#src/knowledge/read.ts";
|
|
|
21
22
|
import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
|
|
22
23
|
import { resolveProjectPath } from "#src/project.ts";
|
|
23
24
|
import { runScript, UsageError } from "#src/script.ts";
|
|
24
|
-
import { codeEntries, coverageConflicts, coverageEntries, imageReader,
|
|
25
|
+
import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps } from "./coverage.ts";
|
|
25
26
|
import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
|
|
26
27
|
import { findIdioms } from "./idioms.ts";
|
|
27
28
|
import { instructionList } from "./instructions.ts";
|
|
@@ -38,8 +39,9 @@ const USAGE = `analyze.ts <image> [options]
|
|
|
38
39
|
mode, operand, target, executed) to this project file as JSON
|
|
39
40
|
--coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): its
|
|
40
41
|
executed code is seeded, and analyzer data over it is reported
|
|
41
|
-
--max-listed <n> list at most n labels or
|
|
42
|
-
the counts are always given, and knowledge has
|
|
42
|
+
--max-listed <n> list at most n labels, functions or candidates (default 50,
|
|
43
|
+
0 for none); the counts are always given, and knowledge has
|
|
44
|
+
all labels and functions
|
|
43
45
|
--space <space> the knowledge space of the image: c64 (the default), drive8
|
|
44
46
|
for 1541 drive code, or bank0 to bank255 for a cartridge bank
|
|
45
47
|
|
|
@@ -158,7 +160,8 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
158
160
|
if (space !== DEFAULT_SPACE && values.coverage !== undefined) throw new UsageError("--coverage works only in space c64");
|
|
159
161
|
const imageKind = values.flat64k ? "flat64k" : "prg";
|
|
160
162
|
const bytes = readProjectFile(image).bytes;
|
|
161
|
-
const
|
|
163
|
+
const runs = readCoverageMaps(values.coverage ?? []);
|
|
164
|
+
const coverage = runs.length === 0 ? undefined : joinCoverage(runs);
|
|
162
165
|
const byteAt = imageReader(bytes, imageKind);
|
|
163
166
|
// With coverage, a code finding over an executed instruction start replaces the data of the other analyzer (D58), and the files are the evidence.
|
|
164
167
|
const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
|
|
@@ -170,7 +173,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
170
173
|
} catch (error) {
|
|
171
174
|
throw new UsageError(`${image}: ${(error as Error).message}`);
|
|
172
175
|
}
|
|
173
|
-
const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end },
|
|
176
|
+
const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, runs);
|
|
174
177
|
|
|
175
178
|
const reader = openForRead();
|
|
176
179
|
const revision = currentRevision(reader);
|
|
@@ -192,7 +195,11 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
192
195
|
revision: imported.revision,
|
|
193
196
|
changes: { regions: imported.regions, references: imported.references },
|
|
194
197
|
conflicts: imported.conflicts.map((conflict) => showConflict(conflict, "heuristic")),
|
|
195
|
-
|
|
198
|
+
// Candidates are not knowledge: a larger --max-listed gives more of them.
|
|
199
|
+
candidates: {
|
|
200
|
+
dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
|
|
201
|
+
selfModified: listed(idioms.candidates.selfModified, max, formatC64Address, "a larger --max-listed"),
|
|
202
|
+
},
|
|
196
203
|
},
|
|
197
204
|
};
|
|
198
205
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { DatabaseSync } from "node:sqlite";
|
|
6
6
|
|
|
7
7
|
import { COVERAGE, instructionLength } from "#src/c64.ts";
|
|
8
|
-
import { FULL_COVERAGE,
|
|
8
|
+
import { FULL_COVERAGE, readCoverageMaps as readMaps } from "#src/host-client/coverage.ts";
|
|
9
9
|
import { listRegions, type RegionRow } from "#src/knowledge/read.ts";
|
|
10
10
|
import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
11
11
|
import { UsageError } from "#src/script.ts";
|
|
@@ -15,9 +15,9 @@ const FULL_MAP = FULL_COVERAGE;
|
|
|
15
15
|
const FLOW_ENDS = new Set([0x00, 0x40, 0x4c, 0x60, 0x6c]);
|
|
16
16
|
const ANALYZER_ORIGINS = new Set(["dxa", "ghidra", "heuristic"]);
|
|
17
17
|
|
|
18
|
-
/** Reads coverage files
|
|
19
|
-
export function
|
|
20
|
-
return
|
|
18
|
+
/** Reads coverage files, one map per file (a run). */
|
|
19
|
+
export function readCoverageMaps(paths: string[]): Uint8Array[] {
|
|
20
|
+
return readMaps(paths, (message) => {
|
|
21
21
|
throw new UsageError(message);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// 6502 idioms that hide code from a disassembler (D44): RTS dispatch tables,
|
|
2
2
|
// JMP tables and BIT skips are found by their exact instruction patterns and
|
|
3
3
|
// become heuristic findings and entry points; DOP padding and self-modified
|
|
4
|
-
// opcodes are only reported. A
|
|
5
|
-
//
|
|
4
|
+
// opcodes are only reported. A self-modified opcode is one that one run wrote
|
|
5
|
+
// and executed, in either order, since a coverage file holds no order (R3-04):
|
|
6
|
+
// a copier's writes in one run and their execution in another do not count (F-16). A table's length is not known statically: entries
|
|
7
|
+
// count while each target is a plausible instruction in the program.
|
|
6
8
|
|
|
7
9
|
import { COVERAGE } from "#src/c64.ts";
|
|
10
|
+
import { joinCoverage } from "#src/host-client/coverage.ts";
|
|
8
11
|
import type { NormalizedFindings } from "#src/knowledge/import.ts";
|
|
9
12
|
import type { ImageByte } from "./coverage.ts";
|
|
10
13
|
|
|
@@ -22,7 +25,7 @@ export interface Idioms {
|
|
|
22
25
|
/** Table ranges for the same run: they are data. */
|
|
23
26
|
dataRanges: Array<{ start: number; end: number }>;
|
|
24
27
|
counts: { rtsTables: number; jumpTables: number; bitSkips: number };
|
|
25
|
-
/** Seen in the coverage, not written: the LLM decides. */
|
|
28
|
+
/** Seen in the coverage, not written: the LLM decides. All of them: the result lists the first ones. */
|
|
26
29
|
candidates: { dop: number[]; selfModified: number[] };
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -38,7 +41,9 @@ const word = (byteAt: ImageByte, address: number) => {
|
|
|
38
41
|
return low === undefined || high === undefined ? undefined : low | (high << 8);
|
|
39
42
|
};
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
/** `runs` are the coverage maps, one for each file. */
|
|
45
|
+
export function findIdioms(byteAt: ImageByte, range: { start: number; end: number }, runs: readonly Uint8Array[] = []): Idioms {
|
|
46
|
+
const coverage = runs.length === 0 ? undefined : joinCoverage(runs);
|
|
42
47
|
const regions: NormalizedFindings["regions"] = [];
|
|
43
48
|
const references: NormalizedFindings["references"] = [];
|
|
44
49
|
const entryPoints = new Set<number>();
|
|
@@ -113,14 +118,14 @@ export function findIdioms(byteAt: ImageByte, range: { start: number; end: numbe
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
// Candidates from the coverage: DOP padding that ran, and opcodes that
|
|
121
|
+
// Candidates from the coverage: DOP padding that ran, and opcodes that one run wrote and executed.
|
|
117
122
|
const candidates = { dop: [] as number[], selfModified: [] as number[] };
|
|
118
123
|
if (coverage !== undefined) {
|
|
124
|
+
const both = COVERAGE.ramWrite | COVERAGE.ramExecute;
|
|
119
125
|
for (let address = range.start; address <= range.end; address++) {
|
|
120
|
-
|
|
121
|
-
if ((
|
|
122
|
-
if (
|
|
123
|
-
if ((flags & COVERAGE.ramWrite) !== 0 && candidates.selfModified.length < MAX_TABLE) candidates.selfModified.push(address);
|
|
126
|
+
if ((coverage[address]! & COVERAGE.ramExecute) === 0) continue;
|
|
127
|
+
if (DOPS.has(byteAt(address)!)) candidates.dop.push(address);
|
|
128
|
+
if (runs.some((run) => (run[address]! & both) === both)) candidates.selfModified.push(address);
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
|
|
@@ -86,7 +86,7 @@ PASS, FAIL or INCONCLUSIVE, the checkpoints and the smallest differences that ex
|
|
|
86
86
|
- INCONCLUSIVE, "did not reach the checkpoint": one side did not reach the checkpoint in the frame limit. Find the reason with the c64-emulator skill. Do not report a FAIL.
|
|
87
87
|
- INCONCLUSIVE, "has no symbol": one side has no symbol with that name. Add the name to the knowledge or to the rebuild source.
|
|
88
88
|
- INCONCLUSIVE, "no emulator" or "could not run": the host runtime or the emulator failed. Tell the user the message.
|
|
89
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
89
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
90
90
|
- A FAIL with only a `screen` difference: add `vicii` or `sprites` to the observation to find which chip state is different.
|
|
91
91
|
- 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.
|
|
92
92
|
- 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 from the entry state of the c64-unpacker skill.
|
|
@@ -73,7 +73,7 @@ An `observe` step has a checkpoint name. A scenario can have each name only one
|
|
|
73
73
|
| `vicii` | The VIC-II state: mode, screen address, scroll and colors. |
|
|
74
74
|
| `sprites` | A list of sprite numbers from 0 to 7. The script compares their position, color and flags. |
|
|
75
75
|
|
|
76
|
-
A `tolerance` is the largest permitted difference for each byte. A `mask` has at most 64 rectangles. A scenario can observe the screen at most at 64 checkpoints.
|
|
76
|
+
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
77
|
|
|
78
78
|
`exclude` is a list of bytes in the observed range that the comparison does not include. Each has `at` and a `reason`, for example `{ "at": "$d01e", "reason": "latched sprite collision, not writable" }`. The result lists each excluded byte with its reason.
|
|
79
79
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// explicit, and the result is PASS, FAIL or INCONCLUSIVE.
|
|
5
5
|
|
|
6
6
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
7
|
-
import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
|
|
7
|
+
import { MAX_BASELINES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, maskCoversFrame, WireFailure } from "#src/host-client/tools.ts";
|
|
8
8
|
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { ACTION_STEP_KEYS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
10
10
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
@@ -129,7 +129,7 @@ export function readScenario(path: string): Scenario {
|
|
|
129
129
|
const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
|
|
130
130
|
for (const key of Object.keys(step)) if (!allowed.includes(key)) fail(`${at} has the key ${key}, which a ${kind} step does not have`);
|
|
131
131
|
if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
|
|
132
|
-
checkStep(kind, step, at, fail);
|
|
132
|
+
checkStep(kind, step, at, fail, scenario.videoStandard ?? "pal");
|
|
133
133
|
if (kind === "observe") {
|
|
134
134
|
if (labels.has(step.observe as string)) fail(`${at} needs a new checkpoint name`);
|
|
135
135
|
labels.add(step.observe as string);
|
|
@@ -142,7 +142,7 @@ export function readScenario(path: string): Scenario {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/** Checks the value of one step against its kind. */
|
|
145
|
-
function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
145
|
+
function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
|
|
146
146
|
switch (kind) {
|
|
147
147
|
case "reset":
|
|
148
148
|
if (step.reset !== "hard" && step.reset !== "soft") fail(`${at}: reset must be hard or soft`);
|
|
@@ -165,11 +165,11 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
|
|
|
165
165
|
case "runUntil":
|
|
166
166
|
return checkActionStep(kind, step, at, fail);
|
|
167
167
|
default:
|
|
168
|
-
checkObserve(step, at, fail);
|
|
168
|
+
checkObserve(step, at, fail, videoStandard);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
172
|
+
function checkObserve(step: Record<string, unknown>, at: string, fail: (message: string) => never, videoStandard: "pal" | "ntsc"): void {
|
|
173
173
|
if (typeof step.observe !== "string" || step.observe === "") fail(`${at} needs a new checkpoint name`);
|
|
174
174
|
if (step.memory !== undefined) {
|
|
175
175
|
if (!Array.isArray(step.memory) || step.memory.length === 0) fail(`${at}: memory must be a list of names, $ addresses or objects with at`);
|
|
@@ -216,6 +216,8 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
216
216
|
isInteger(rectangle.height, 1, 4096);
|
|
217
217
|
if (!ok) fail(`${at}: a screen.mask rectangle needs whole numbers x and y from 0, and width and height from 1`);
|
|
218
218
|
}
|
|
219
|
+
// The emulator refuses such a compare too, but only after the original side ran (b2/03).
|
|
220
|
+
if (maskCoversFrame(screen.mask as Array<{ x: number; y: number; width: number; height: number }>, videoStandard)) fail(`${at}: screen.mask covers the whole screen, so nothing is compared`);
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
if (step.vicii !== undefined && typeof step.vicii !== "boolean") fail(`${at}: vicii must be true or false`);
|
|
@@ -283,7 +285,7 @@ export function resolve(location: Location, symbols: Map<string, number>, side:
|
|
|
283
285
|
interface Observation {
|
|
284
286
|
memory: Array<{ label: string; address: number; bytes: number[]; tolerance: number; excluded: Array<{ offset: number; at: Location; reason: string }> }>;
|
|
285
287
|
registers: Partial<Record<ObservedRegister, unknown>>;
|
|
286
|
-
screen?: { match: boolean; mismatchingPixels: number; mismatchRatio: number; bounds?: unknown };
|
|
288
|
+
screen?: { match: boolean; mismatchingPixels: number; comparedPixels: number; mismatchRatio: number; bounds?: unknown };
|
|
287
289
|
vicii?: unknown;
|
|
288
290
|
sprites?: unknown;
|
|
289
291
|
/** PNG images for the files of a FAIL (D46): the side's screen, and on the rebuild the difference image. */
|
|
@@ -371,7 +373,7 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
371
373
|
if (files) observation.png = shot.png;
|
|
372
374
|
} else {
|
|
373
375
|
const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff: files });
|
|
374
|
-
observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
|
|
376
|
+
observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, comparedPixels: compared.comparedPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
|
|
375
377
|
if (files) {
|
|
376
378
|
observation.png = (await client.screenCapture()).png;
|
|
377
379
|
if (compared.diffPng !== undefined) observation.diffPng = compared.diffPng;
|
|
@@ -26,17 +26,17 @@ node <skill>/scripts/unpack.ts stages original/game.prg --steps input.json
|
|
|
26
26
|
node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out parts --drive
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). In `stages`, a `frames` step
|
|
29
|
+
`inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). In `stages` and `capture`, a `frames` step and a `runUntil` step with only `at` count as run time. `stages` refuses a `runUntil` step with `raster` or `memory`.
|
|
30
30
|
|
|
31
31
|
| Option | Meaning |
|
|
32
32
|
|---|---|
|
|
33
|
-
| `--until '$c000'` | The address where the unpacked program starts. |
|
|
33
|
+
| `--until '$c000'` | The address where the unpacked program starts. The capture stops there after the steps. To capture a later arrival at the address, add a `runUntil` step to an address after the first arrival. |
|
|
34
34
|
| `--out unpacked/game.prg` | Where to write the result, relative to the project directory. |
|
|
35
35
|
| `--range '$c000' '$cfff'` | Write this range as a PRG. Without it, the script writes all 64 KiB of RAM as a flat file. |
|
|
36
36
|
| `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
|
|
37
37
|
| `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
|
|
38
38
|
| `--entry '$c000'` | `trace`, `stages` and `capture`: start the program at this address. Without it, the script types RUN. A file that is not a BASIC program at `$0801` needs it. |
|
|
39
|
-
| `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. |
|
|
39
|
+
| `--steps input.json` | Input for the program, right after it starts: a JSON list of steps. Use it when an intro waits for a key or a menu waits for a choice. The scripts start the program as a c64-testing scenario does. Thus the same steps in a scenario get the program to the entry at the same time. |
|
|
40
40
|
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
41
41
|
| `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
|
|
42
42
|
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
@@ -64,18 +64,19 @@ A `type` step puts keys in the keyboard queue of the KERNAL. A program that read
|
|
|
64
64
|
- `start`: the code at the SYS address. `jumpsOutside` and `writesOutside` give an address outside the loaded program, where the code jumps or writes.
|
|
65
65
|
- `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
|
|
66
66
|
2. Run `trace`. It gives the evidence from the run. Read `packing`:
|
|
67
|
-
- `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is
|
|
67
|
+
- `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the written code that runs at the end of the trace. When the trace ends in ROM code, for example in an interrupt, it is the written code that runs next. Its `start` is code that runs after the unpacking. It is not always the entry point of the program. When no written code runs in 50 more frames, the result has no `runningRange` and no `suggestedUntil`.
|
|
68
68
|
- `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
|
|
69
69
|
- `unlikely`: the program executed no code that it wrote. If the program waits for a key or a joystick, it can move code after that input. Then use the c64-emulator skill.
|
|
70
70
|
3. Find the address where the unpacked program starts:
|
|
71
|
-
- Run `stages`. The `entry` of the last stage is usually the start of the program.
|
|
71
|
+
- Run `stages`. The `entry` of the last stage is usually the start of the program. A routine that a stage writes and calls is part of that stage when it returns to the stage.
|
|
72
72
|
- Or use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
|
|
73
73
|
- Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
|
|
74
|
+
- Disassemble the code at the address with the c64-emulator skill. Make sure that it is the start of the program. The start of a program often sets the stack pointer, the interrupts and the vectors.
|
|
74
75
|
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
75
76
|
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
76
|
-
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote.
|
|
77
|
+
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
77
78
|
- One PRG for each segment.
|
|
78
|
-
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
79
|
+
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
79
80
|
5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it.
|
|
80
81
|
6. Give `image.bin` to the c64-static-analysis skill with its `--flat64k` option, or give it the segment PRGs.
|
|
81
82
|
7. Use `--range` only when you know the area of the program. Then the capture writes that one segment.
|
|
@@ -113,7 +114,7 @@ Do not record the unpacker in the project knowledge, unless the user asks to exa
|
|
|
113
114
|
|
|
114
115
|
The packing evidence, the stages, or the unpacked program with its image, segments and entry state in a directory of the project.
|
|
115
116
|
|
|
116
|
-
For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture.
|
|
117
|
+
For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
|
|
117
118
|
|
|
118
119
|
The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false`, for `loaded: false` and for an error. The exit status is 2 for a wrong argument.
|
|
119
120
|
|
|
@@ -122,10 +123,11 @@ The exit status is 0 for a result. The exit status is 1 for `traced: false`, for
|
|
|
122
123
|
- `traced: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
|
|
123
124
|
- `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames. If it stopped in a key wait, give `--steps` with a key.
|
|
124
125
|
- A step did not reach its target: the reason names the step. Change the step or give it more frames.
|
|
126
|
+
- `loaded: false` and no capture has a segment: the program did not load a part, or it did not start. `keyWait: true` on a capture tells that the C64 waited at READY. or at an INPUT statement. For a tape image, read `emulatorFlaw`. A known VICE flaw can stop a tape load. Tell the user to install the latest VICE.
|
|
125
127
|
- Do not name a packer from weak evidence. The script never names one.
|
|
126
128
|
- Some programs unpack in more than one stage. Run `stages` to find each stage. Capture at the entry of the last stage. Make sure that it is the application and not a second unpacker.
|
|
127
129
|
- `not-found` or `invalid-input`: a path or an argument is wrong. Read the message.
|
|
128
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
130
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
129
131
|
|
|
130
132
|
## Handoffs
|
|
131
133
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// image with its entry state, and with --drive the drive RAM as a PRG for
|
|
5
5
|
// knowledge space drive8. No tool guesses where a load ends: the steps decide.
|
|
6
6
|
|
|
7
|
-
import { COVERAGE, formatC64Address, parseC64Address
|
|
8
|
-
import { autostartLoaded, LOAD_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
7
|
+
import { COVERAGE, formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
8
|
+
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
10
|
import { ACTION_STEP_KEYS, ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
11
11
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
@@ -24,6 +24,8 @@ const DRIVE_RAM = 0x0800;
|
|
|
24
24
|
const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
25
25
|
/** The KERNAL LOAD entry in the jump table. */
|
|
26
26
|
const KERNAL_LOAD = 0xffd5;
|
|
27
|
+
/** The KERNAL loop that waits for a key at READY. or at an INPUT statement. */
|
|
28
|
+
const KERNAL_KEY_WAIT = { start: 0xe5cd, end: 0xe5d5 };
|
|
27
29
|
|
|
28
30
|
/** Reads and checks the steps file of loads: action steps with $ addresses, and capture steps with new names. */
|
|
29
31
|
export function readLoadSteps(path: string): LoadStep[] {
|
|
@@ -100,6 +102,8 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
100
102
|
let loadedProgram: Buffer | undefined;
|
|
101
103
|
/** False while the map holds the writes of the KERNAL RAM test. */
|
|
102
104
|
let writesKnown = true;
|
|
105
|
+
/** A known flaw of the host's VICE version that a tape load meets (D56). */
|
|
106
|
+
let emulatorFlaw: string | undefined;
|
|
103
107
|
if (program.toLowerCase().endsWith(".prg")) {
|
|
104
108
|
const { bytes } = readProjectFile(program);
|
|
105
109
|
if (!(await autostartLoaded(client, program, bytes))) return { output: { loaded: false, reason: `The program was not in memory after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
|
|
@@ -109,12 +113,13 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
109
113
|
previous.fill(0, load, Math.min(0x10000, load + bytes.length - 2));
|
|
110
114
|
loadedProgram = bytes;
|
|
111
115
|
await client.memmap({ action: "clear" });
|
|
112
|
-
|
|
116
|
+
// RUN as c64-testing types it: the steps start at the same time as a scenario's (D62).
|
|
117
|
+
if (!(await typeRun(client))) return { output: { loaded: false, reason: `After RUN, BASIC ran no program line in ${RUN_LIMIT_FRAMES} frames.` }, failed: true };
|
|
113
118
|
} else {
|
|
114
119
|
// A disk, tape or cartridge: VICE loads and starts it. The RAM test of the KERNAL puts back what it writes.
|
|
115
120
|
previous = await readRam(client, 0x0000, 0x10000);
|
|
116
121
|
await client.memmap({ action: "clear" });
|
|
117
|
-
await client.autostart({ path: program, index: 0, run: true });
|
|
122
|
+
({ emulatorFlaw } = await client.autostart({ path: program, index: 0, run: true }));
|
|
118
123
|
writesKnown = false;
|
|
119
124
|
if (!program.toLowerCase().endsWith(".crt")) {
|
|
120
125
|
// The map starts again where the KERNAL LOAD starts, after the RAM test: then it holds only what the program wrote.
|
|
@@ -178,10 +183,12 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
178
183
|
}),
|
|
179
184
|
);
|
|
180
185
|
write("entry_state.json", json(entryState(registers, state, vectors)));
|
|
186
|
+
const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
|
|
181
187
|
captures.push({
|
|
182
188
|
capture: step.capture,
|
|
183
189
|
out: directory,
|
|
184
190
|
pc: formatC64Address(registers.pc),
|
|
191
|
+
...(keyWait ? { keyWait: true } : {}),
|
|
185
192
|
segments: segments.map(showRange),
|
|
186
193
|
...(small === 0 ? {} : { smallChanges: small }),
|
|
187
194
|
...(drive === undefined ? {} : { drive: drive.file }),
|
|
@@ -190,6 +197,11 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
190
197
|
await client.memmap({ action: "clear" });
|
|
191
198
|
writesKnown = true;
|
|
192
199
|
}
|
|
200
|
+
if (captures.length > 0 && captures.every((capture) => (capture.segments as unknown[]).length === 0)) {
|
|
201
|
+
const waits = captures.some((capture) => capture.keyWait === true) ? " The C64 waited for a key at READY. or at an INPUT statement, so the load ended or did not start." : "";
|
|
202
|
+
const reason = `No capture has a segment: the program changed no block of RAM from ${formatC64Address(FIRST_SEGMENT_ADDRESS)} on.${waits}`;
|
|
203
|
+
return { output: { loaded: false, reason, captures, ...(emulatorFlaw === undefined ? {} : { emulatorFlaw }) }, failed: true };
|
|
204
|
+
}
|
|
193
205
|
return { output: { loaded: true, captures }, failed: false };
|
|
194
206
|
} finally {
|
|
195
207
|
await client.close();
|
|
@@ -14,10 +14,19 @@ export const IO_AREAS = { vic: [0xd000, 0x2f], sid: [0xd400, 0x1d], cia1: [0xdc0
|
|
|
14
14
|
/** Frames from a hard reset to the READY prompt, with margin. */
|
|
15
15
|
export const READY_FRAMES = 200;
|
|
16
16
|
|
|
17
|
+
/** What no register read of a CIA gives (D63). */
|
|
18
|
+
export interface CiaHidden {
|
|
19
|
+
latchA: number;
|
|
20
|
+
latchB: number;
|
|
21
|
+
interruptMask: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface MachineState {
|
|
18
25
|
port: Buffer;
|
|
19
26
|
pages: Buffer;
|
|
20
27
|
io: Record<keyof typeof IO_AREAS, Buffer>;
|
|
28
|
+
/** The timer latches and the interrupt mask of each CIA. */
|
|
29
|
+
cia: { cia1: CiaHidden; cia2: CiaHidden };
|
|
21
30
|
romVectors: Buffer;
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -36,8 +45,9 @@ export async function machineState(client: ViceSessionClient): Promise<MachineSt
|
|
|
36
45
|
const pages = await readRam(client, 0x0000, 0x400);
|
|
37
46
|
const io = {} as Record<keyof typeof IO_AREAS, Buffer>;
|
|
38
47
|
for (const [name, [address, size]] of Object.entries(IO_AREAS)) io[name as keyof typeof IO_AREAS] = Buffer.from((await client.ioRead(address, size)).data, "hex");
|
|
48
|
+
const [cia1, cia2] = (await client.cia("both")).chips.map(({ latchA, latchB, interruptMask }) => ({ latchA, latchB, interruptMask }));
|
|
39
49
|
const romVectors = Buffer.from((await client.memoryRead({ address: 0xfffa, size: 6, space: "c64", view: "cpu" })).data, "hex");
|
|
40
|
-
return { port, pages, io, romVectors };
|
|
50
|
+
return { port, pages, io, cia: { cia1: cia1!, cia2: cia2! }, romVectors };
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
const word = (bytes: Buffer, offset: number) => formatC64Address(bytes[offset]! | (bytes[offset + 1]! << 8));
|
|
@@ -60,6 +70,8 @@ export function entryState(registers: Registers, state: MachineState, vectors: R
|
|
|
60
70
|
cpuPort: { $00: state.port[0], $01: state.port[1] },
|
|
61
71
|
pages: Object.fromEntries([0, 1, 2, 3].map((page) => [formatC64Address(page << 8), state.pages.subarray(page << 8, (page + 1) << 8).toString("hex")])),
|
|
62
72
|
io: Object.fromEntries(Object.entries(IO_AREAS).map(([name, [address]]) => [name, { from: formatC64Address(address), bytes: state.io[name as keyof typeof IO_AREAS].toString("hex") }])),
|
|
73
|
+
// The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them (D63).
|
|
74
|
+
cia: state.cia,
|
|
63
75
|
vectors,
|
|
64
76
|
...(differsFromReset === undefined ? {} : { differsFromReset }),
|
|
65
77
|
};
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// starts only where the CPU executes an opcode byte that the current stage
|
|
5
5
|
// wrote before the CPU got there. Code that the stage executed before it wrote
|
|
6
6
|
// it is the stage's own code: it stops the run once, and then a write watch
|
|
7
|
-
// over it arms it again.
|
|
7
|
+
// over it arms it again. Written code that the stage calls, and that returns
|
|
8
|
+
// to the stage, is the stage's own code too (D59): a packer hands off by JMP,
|
|
9
|
+
// RTS or RTI and never runs its own code again.
|
|
8
10
|
|
|
9
11
|
import { COVERAGE } from "#src/c64.ts";
|
|
10
12
|
import { writtenBlocks } from "#src/host-client/coverage.ts";
|
|
@@ -26,12 +28,13 @@ export function mergeMaps(into: Uint8Array, from: Uint8Array): void {
|
|
|
26
28
|
* code that can start a stage: every executed address except own code that
|
|
27
29
|
* the stage has not written.
|
|
28
30
|
*/
|
|
29
|
-
export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number): Range[] {
|
|
31
|
+
export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: ReadonlySet<number>, minimum: number, called: readonly Range[] = []): Range[] {
|
|
30
32
|
const combined = Uint8Array.from(stage);
|
|
31
33
|
mergeMaps(combined, chunk);
|
|
32
34
|
return writtenBlocks(combined, minimum).filter((block) => {
|
|
33
35
|
for (let address = block.start; address <= block.end; address++) {
|
|
34
36
|
if ((chunk[address]! & COVERAGE.ramExecute) === 0) continue;
|
|
37
|
+
if (inRanges(called, address)) continue;
|
|
35
38
|
if (own.has(address) && (combined[address]! & COVERAGE.ramWrite) === 0) continue;
|
|
36
39
|
return true;
|
|
37
40
|
}
|
|
@@ -44,7 +47,7 @@ export function stageCandidates(stage: Uint8Array, chunk: Uint8Array, own: Reado
|
|
|
44
47
|
* them except own code that is not written yet, and write watches over that
|
|
45
48
|
* own code.
|
|
46
49
|
*/
|
|
47
|
-
export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean): { exec: Range[]; watch: Range[] } {
|
|
50
|
+
export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>, written: (address: number) => boolean, called: readonly Range[] = []): { exec: Range[]; watch: Range[] } {
|
|
48
51
|
const exec: Range[] = [];
|
|
49
52
|
const watch: Range[] = [];
|
|
50
53
|
const add = (list: Range[], address: number) => {
|
|
@@ -54,6 +57,7 @@ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>,
|
|
|
54
57
|
};
|
|
55
58
|
for (const block of blocks) {
|
|
56
59
|
for (let address = block.start; address <= block.end; address++) {
|
|
60
|
+
if (inRanges(called, address)) continue;
|
|
57
61
|
if (own.has(address) && !written(address)) add(watch, address);
|
|
58
62
|
else add(exec, address);
|
|
59
63
|
}
|
|
@@ -65,3 +69,62 @@ export function stagePoints(blocks: readonly Range[], own: ReadonlySet<number>,
|
|
|
65
69
|
export function inRanges(ranges: readonly Range[], address: number): boolean {
|
|
66
70
|
return ranges.some((range) => contains(range, address));
|
|
67
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Where a call returns to the stage (D59): the return address on top of the
|
|
75
|
+
* stack, when a JSR that the stage executed pushed it. `stack` is page 1.
|
|
76
|
+
* Returns the address of the instruction after that JSR.
|
|
77
|
+
*/
|
|
78
|
+
export function callReturn(stack: Uint8Array, sp: number, byteAt: (address: number) => number, executed: (address: number) => boolean): number | undefined {
|
|
79
|
+
const pushed = stack[(sp + 1) & 0xff]! | (stack[(sp + 2) & 0xff]! << 8);
|
|
80
|
+
const jsr = (pushed - 2) & 0xffff;
|
|
81
|
+
if (byteAt(jsr) !== 0x20 || !executed(jsr)) return undefined;
|
|
82
|
+
return (pushed + 1) & 0xffff;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The code that ran in a block: its executed opcode bytes with the two bytes
|
|
87
|
+
* after each, which can be operands, joined and cut at the block's end.
|
|
88
|
+
*/
|
|
89
|
+
export function executedIn(map: Uint8Array, block: Range): Range[] {
|
|
90
|
+
const ranges: Range[] = [];
|
|
91
|
+
for (let address = block.start; address <= block.end; address++) {
|
|
92
|
+
if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
|
|
93
|
+
const end = Math.min(block.end, address + 2);
|
|
94
|
+
const last = ranges[ranges.length - 1];
|
|
95
|
+
if (last !== undefined && address <= last.end + 1) last.end = Math.max(last.end, end);
|
|
96
|
+
else ranges.push({ start: address, end });
|
|
97
|
+
}
|
|
98
|
+
return ranges;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The segments of a capture: the blocks that the stages wrote, without loader
|
|
103
|
+
* leftovers, joined into ranges that do not overlap or touch (F-26). A block
|
|
104
|
+
* is a leftover when it holds the entry of an intermediate stage that started
|
|
105
|
+
* after the block's writer, and no write from that stage on replaced the code
|
|
106
|
+
* at that entry (R2-01). Code that a stage ran before a later stage wrote over
|
|
107
|
+
* it never drops a block, and the block that holds the capture entry is always
|
|
108
|
+
* a segment. The filter comes before the join: a join first would drop the
|
|
109
|
+
* program with a leftover. `writtenBy` is the last stage that wrote into the
|
|
110
|
+
* range.
|
|
111
|
+
*/
|
|
112
|
+
export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly Range[] }>, entry: number): Array<Range & { writtenBy: number }> {
|
|
113
|
+
const last = stages.length - 1;
|
|
114
|
+
const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
|
|
115
|
+
const leftover = (block: Range, writer: number) =>
|
|
116
|
+
!contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
|
|
117
|
+
const blocks = stages
|
|
118
|
+
.flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })))
|
|
119
|
+
.filter((block) => !leftover(block, block.writtenBy))
|
|
120
|
+
.sort((a, b) => a.start - b.start);
|
|
121
|
+
const segments: Array<Range & { writtenBy: number }> = [];
|
|
122
|
+
for (const block of blocks) {
|
|
123
|
+
const previous = segments[segments.length - 1];
|
|
124
|
+
if (previous !== undefined && block.start <= previous.end + 1) {
|
|
125
|
+
previous.end = Math.max(previous.end, block.end);
|
|
126
|
+
previous.writtenBy = Math.max(previous.writtenBy, block.writtenBy);
|
|
127
|
+
} else segments.push({ ...block });
|
|
128
|
+
}
|
|
129
|
+
return segments;
|
|
130
|
+
}
|