@henols/c64-re-tools 2.1.1 → 2.1.3
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 +6 -3
- package/skills/c64-assembler/scripts/assembler.ts +19 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +1 -0
- package/skills/c64-disk/scripts/disk.ts +2 -2
- package/skills/c64-disk/scripts/lib/containers.ts +4 -4
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +2 -0
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +12 -6
- package/skills/c64-knowledge/scripts/knowledge.ts +41 -25
- package/skills/c64-knowledge/scripts/lib/listing.ts +16 -31
- package/skills/c64-memory-map/SKILL.md +3 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +23 -4
- package/skills/c64-provenance/SKILL.md +5 -3
- package/skills/c64-provenance/scripts/lib/library.ts +28 -5
- package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
- package/skills/c64-provenance/scripts/provenance.ts +22 -10
- package/skills/c64-source/SKILL.md +2 -1
- package/skills/c64-source/scripts/lib/source.ts +25 -33
- package/skills/c64-source/scripts/source.ts +13 -17
- package/skills/c64-static-analysis/SKILL.md +6 -3
- package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
- package/skills/c64-static-analysis/scripts/static-analysis.ts +31 -19
- package/skills/c64-testing/SKILL.md +9 -5
- package/skills/c64-testing/references/scenario-format.md +5 -3
- package/skills/c64-testing/scripts/lib/diff-files.ts +41 -10
- package/skills/c64-testing/scripts/lib/recording.ts +3 -3
- package/skills/c64-testing/scripts/lib/replay.ts +1 -1
- package/skills/c64-testing/scripts/lib/runs.ts +45 -9
- package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
- package/skills/c64-testing/scripts/testing.ts +9 -6
- package/skills/c64-unpacker/SKILL.md +10 -7
- package/skills/c64-unpacker/scripts/lib/loads.ts +15 -6
- package/skills/c64-unpacker/scripts/lib/machine.ts +61 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +21 -17
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +144 -11
- package/src/host-client/image.ts +27 -13
- package/src/host-client/steps.ts +41 -5
- package/src/host-client/tools.ts +4 -3
- package/src/host-client/transfer.ts +22 -29
- package/src/knowledge/history.ts +55 -2
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/program.ts +31 -2
- package/src/knowledge/read.ts +7 -4
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +86 -42
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +1 -1
- package/src/mcp/server.ts +9 -1
- package/src/mcp/tools/debug.ts +31 -4
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +5 -1
- package/src/mcp/tools/memory.ts +4 -3
- package/src/media.ts +57 -0
- package/src/opcodes.ts +81 -18
- package/src/platform.ts +14 -1
- package/src/project.ts +118 -3
- package/src/protocol/vice.ts +4 -1
- package/src/script.ts +118 -5
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
// 0-3, the chip registers read without side effects, and the vectors. The
|
|
3
3
|
// capture and loads commands of the unpacker share it.
|
|
4
4
|
|
|
5
|
+
import { readdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
5
8
|
import { formatC64Address } from "#src/c64.ts";
|
|
9
|
+
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
10
|
+
import { writeRefusal } from "#src/host-client/transfer.ts";
|
|
6
11
|
import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
12
|
+
import { projectRoot, resolveProjectPath } from "#src/project.ts";
|
|
7
13
|
|
|
8
14
|
type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
|
|
9
15
|
|
|
@@ -61,8 +67,34 @@ export function vectorsOf(state: MachineState, memory: Buffer) {
|
|
|
61
67
|
};
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
/** The
|
|
65
|
-
export function
|
|
70
|
+
/** The state of a clean machine at the READY prompt after a hard reset: what differsFromReset compares with. It resets the machine. */
|
|
71
|
+
export async function readyState(client: ViceSessionClient): Promise<MachineState> {
|
|
72
|
+
await resetToReady(client);
|
|
73
|
+
return machineState(client);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The bytes that differ from a clean machine: the CPU port, pages 0-3 from $0002, and the I/O areas. */
|
|
77
|
+
export function differsFrom(state: MachineState, clean: MachineState): Array<{ address: string; entry: number; reset: number }> {
|
|
78
|
+
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
79
|
+
const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
|
|
80
|
+
compare(0x0000, state.port, clean.port);
|
|
81
|
+
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
82
|
+
for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
|
|
83
|
+
return differs;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* entry_state.json, the one writer of capture and loads: the state at the
|
|
88
|
+
* entry, and what differs from `clean` (readyState). Gives the file and the
|
|
89
|
+
* count of differing bytes.
|
|
90
|
+
*/
|
|
91
|
+
export function entryStateFile(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, clean: MachineState): { bytes: Buffer; differsFromReset: number } {
|
|
92
|
+
const differs = differsFrom(state, clean);
|
|
93
|
+
return { bytes: json(entryState(registers, state, vectors, differs)), differsFromReset: differs.length };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The content of entry_state.json. */
|
|
97
|
+
function entryState(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, differsFromReset: Array<{ address: string; entry: number; reset: number }>) {
|
|
66
98
|
return {
|
|
67
99
|
registers: { pc: formatC64Address(registers.pc), a: registers.a, x: registers.x, y: registers.y, sp: registers.sp, flags: registers.flags },
|
|
68
100
|
cpuPort: { $00: state.port[0], $01: state.port[1] },
|
|
@@ -71,8 +103,34 @@ export function entryState(registers: Registers, state: MachineState, vectors: R
|
|
|
71
103
|
// The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them.
|
|
72
104
|
cia: state.cia,
|
|
73
105
|
vectors,
|
|
74
|
-
|
|
106
|
+
differsFromReset,
|
|
75
107
|
};
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
/** The names of the files that a capture writes for its content: a PRG for each segment, and the drive RAM. */
|
|
111
|
+
const CAPTURE_FILE = /^(?:segment-[0-9a-f]{4}-[0-9a-f]{4}\.prg|drive8\.prg)$/;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Removes from the capture folder `folder` (a project path) each file that an
|
|
115
|
+
* earlier capture wrote and this capture did not: a segment PRG of another
|
|
116
|
+
* range, or the drive RAM. image.json lists only the files of this capture.
|
|
117
|
+
* Every other file stays. Gives the removed files as project paths.
|
|
118
|
+
*/
|
|
119
|
+
export function removeStaleFiles(folder: string, written: readonly string[]): string[] {
|
|
120
|
+
const path = resolveProjectPath(folder, projectRoot());
|
|
121
|
+
const removed: string[] = [];
|
|
122
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
123
|
+
// A symbolic link is not a file of a capture: it stays.
|
|
124
|
+
if (!entry.isFile() || !CAPTURE_FILE.test(entry.name) || written.includes(entry.name)) continue;
|
|
125
|
+
const file = `${folder}/${entry.name}`;
|
|
126
|
+
try {
|
|
127
|
+
rmSync(join(path, entry.name));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw writeRefusal(error, file, "--out") ?? error;
|
|
130
|
+
}
|
|
131
|
+
removed.push(file);
|
|
132
|
+
}
|
|
133
|
+
return removed.sort();
|
|
134
|
+
}
|
|
135
|
+
|
|
78
136
|
export const json = (value: unknown) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -93,25 +93,33 @@ export function executedIn(map: Uint8Array, block: AddressRange): AddressRange[]
|
|
|
93
93
|
return ranges;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Pages 0-3 ($0000-$03ff) are in entry_state.json: a written block that is all in them is no segment. */
|
|
97
|
+
export const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
98
|
+
|
|
96
99
|
/**
|
|
97
100
|
* The segments of a capture: the blocks that the stages wrote, without loader
|
|
98
|
-
* leftovers
|
|
101
|
+
* leftovers and without blocks that are all in pages 0-3 (the entry state
|
|
102
|
+
* holds them), joined into ranges that do not overlap or touch. A block
|
|
99
103
|
* is a leftover when it holds the entry of an intermediate stage that started
|
|
100
104
|
* after the block's writer, and no write from that stage on replaced the code
|
|
101
105
|
* at that entry. Code that a stage ran before a later stage wrote over
|
|
102
106
|
* it never drops a block, and the block that holds the capture entry is always
|
|
103
|
-
* a segment.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
107
|
+
* a segment. A block that an intermediate stage wrote inside a leftover, for
|
|
108
|
+
* example a variable of the loader, is a leftover too. The filter comes before
|
|
109
|
+
* the join: a join first would drop the program with a leftover. `writtenBy` is
|
|
110
|
+
* the last stage that wrote into the range.
|
|
106
111
|
*/
|
|
107
112
|
export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly AddressRange[] }>, entry: number): Array<AddressRange & { writtenBy: number }> {
|
|
108
113
|
const last = stages.length - 1;
|
|
109
114
|
const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
|
|
110
115
|
const leftover = (block: AddressRange, writer: number) =>
|
|
111
116
|
!contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
const written = stages.flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })));
|
|
118
|
+
const leftovers = written.filter((block) => leftover(block, block.writtenBy));
|
|
119
|
+
const inLeftover = (block: AddressRange & { writtenBy: number }) =>
|
|
120
|
+
block.writtenBy < last && !contains(block, entry) && leftovers.some((left) => block.start >= left.start && block.end <= left.end);
|
|
121
|
+
const blocks = written
|
|
122
|
+
.filter((block) => !leftover(block, block.writtenBy) && !inLeftover(block) && (block.end >= FIRST_SEGMENT_ADDRESS || contains(block, entry)))
|
|
115
123
|
.sort((a, b) => a.start - b.start);
|
|
116
124
|
const segments: Array<AddressRange & { writtenBy: number }> = [];
|
|
117
125
|
for (const block of blocks) {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import { type AddressRange, COVERAGE, EXECUTE, formatC64Address, READ, WRITE } from "#src/c64.ts";
|
|
10
10
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
11
11
|
import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
12
|
-
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
13
12
|
import { readActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
14
13
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
15
14
|
import { readPrg } from "#src/host-client/image.ts";
|
|
@@ -20,7 +19,7 @@ import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, ty
|
|
|
20
19
|
import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
|
|
21
20
|
import { loads, readLoadSteps } from "./lib/loads.ts";
|
|
22
21
|
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
|
|
23
|
-
import {
|
|
22
|
+
import { entryStateFile, machineState, readRam, readyState, removeStaleFiles, vectorsOf } from "./lib/machine.ts";
|
|
24
23
|
|
|
25
24
|
/** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
|
|
26
25
|
const UNPACKED_BYTES = 256;
|
|
@@ -165,6 +164,13 @@ async function firstPcAfterBasic(client: ViceSessionClient, from: number, to: nu
|
|
|
165
164
|
return from > to ? undefined : firstPcIn(client, [{ start: from, end: to }]);
|
|
166
165
|
}
|
|
167
166
|
|
|
167
|
+
/**
|
|
168
|
+
* What a stage wrote: every written block, also a small one. --min-stage
|
|
169
|
+
* decides only where a stage starts; the wrote list and the capture segments
|
|
170
|
+
* keep each byte that a stage wrote.
|
|
171
|
+
*/
|
|
172
|
+
const stageWrote = (map: Uint8Array) => writtenBlocks(map, 1);
|
|
173
|
+
|
|
168
174
|
/** Frames between two looks at the memory map while stages are followed. */
|
|
169
175
|
const STAGE_CHUNK_FRAMES = 25;
|
|
170
176
|
const MAX_STAGES = 16;
|
|
@@ -238,7 +244,7 @@ async function follow(
|
|
|
238
244
|
let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as AddressRange[] };
|
|
239
245
|
const startStage = async (next: number, last: Uint8Array) => {
|
|
240
246
|
mergeMaps(stage.map, last);
|
|
241
|
-
found.push({ entry, firstFrame, frames: total - firstFrame, wrote:
|
|
247
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map), next });
|
|
242
248
|
entry = next;
|
|
243
249
|
firstFrame = total;
|
|
244
250
|
stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] };
|
|
@@ -428,7 +434,7 @@ async function follow(
|
|
|
428
434
|
else reached = await trackTo(options.until, left);
|
|
429
435
|
await client.snapshot({ action: "remove", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
430
436
|
mergeMaps(stage.map, await readCoverage(client));
|
|
431
|
-
found.push({ entry, firstFrame, frames: total - firstFrame, wrote:
|
|
437
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map) });
|
|
432
438
|
return { stages: found, frames: total, reached, ...(halted === undefined ? {} : { halted }) };
|
|
433
439
|
}
|
|
434
440
|
|
|
@@ -529,24 +535,21 @@ async function capture(
|
|
|
529
535
|
),
|
|
530
536
|
);
|
|
531
537
|
// A clean machine at the READY prompt shows what the loader and the stages changed.
|
|
532
|
-
await
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
compare(0x0000, state.port, clean.port);
|
|
537
|
-
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
538
|
-
for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
|
|
539
|
-
write("entry_state.json", json(entryState(registers, state, vectors, differs)));
|
|
538
|
+
const entry = entryStateFile(registers, state, vectors, await readyState(client));
|
|
539
|
+
write("entry_state.json", entry.bytes);
|
|
540
|
+
// A capture again into the folder: the segment PRGs of the earlier capture that image.json no longer lists go.
|
|
541
|
+
const removed = removeStaleFiles(options.out, files);
|
|
540
542
|
return {
|
|
541
543
|
output: {
|
|
542
544
|
captured: true,
|
|
543
545
|
pc: formatC64Address(registers.pc),
|
|
544
546
|
out: options.out,
|
|
545
547
|
files: written,
|
|
548
|
+
...(removed.length === 0 ? {} : { removed }),
|
|
546
549
|
segments: segments.map(showRange),
|
|
547
550
|
...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
|
|
548
551
|
stages: followed.stages.length,
|
|
549
|
-
differsFromReset:
|
|
552
|
+
differsFromReset: entry.differsFromReset,
|
|
550
553
|
},
|
|
551
554
|
failed: false,
|
|
552
555
|
};
|
|
@@ -569,7 +572,7 @@ const entryOf = (call: Call) => {
|
|
|
569
572
|
};
|
|
570
573
|
const stepsOf = (call: Call) => {
|
|
571
574
|
const path = call.string("steps");
|
|
572
|
-
return path === undefined ? [] : readActionSteps(path, "--steps");
|
|
575
|
+
return path === undefined ? [] : readActionSteps(path, "--steps", knowledgeAt());
|
|
573
576
|
};
|
|
574
577
|
|
|
575
578
|
const COMMANDS: CommandSpec[] = [
|
|
@@ -595,7 +598,8 @@ const COMMANDS: CommandSpec[] = [
|
|
|
595
598
|
run: async (call) => {
|
|
596
599
|
const steps = stepsOf(call);
|
|
597
600
|
// A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
|
|
598
|
-
|
|
601
|
+
const untracked = steps.findIndex((step) => "runUntil" in step && !trackedRunUntil(step));
|
|
602
|
+
if (untracked >= 0) throw new UsageError(`--steps ${call.string("steps")!}: step ${untracked + 1}: stages takes a runUntil step only with an address target without a condition. Use frames steps, or capture`);
|
|
599
603
|
return outcome(await stages(call.subject(0), { frames: call.count("frames")!, steps, minStage: call.count("min-stage")!, ...entryOf(call) }));
|
|
600
604
|
},
|
|
601
605
|
},
|
|
@@ -605,7 +609,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
605
609
|
subjects: [PROGRAM],
|
|
606
610
|
options: {
|
|
607
611
|
until: { kind: "address", value: "<address>", required: true, help: "the unpacked entry point" },
|
|
608
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
|
|
612
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
|
|
609
613
|
range: { ...OPTIONS.range, help: "one segment in place of the found ones" },
|
|
610
614
|
exclude: { kind: "range", value: "<start>-<end>", multiple: true, help: "a range to leave out of the segments, for example bytes that a crack left and the program never reads" },
|
|
611
615
|
"timeout-frames": { ...OPTIONS["timeout-frames"], max: 30_000, default: 3000, help: "stop the wait for --until after n frames (default 3000)" },
|
|
@@ -632,7 +636,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
632
636
|
subjects: [{ name: "<program>", help: "a PRG, or a disk, tape or cartridge image, relative to the project folder" }],
|
|
633
637
|
options: {
|
|
634
638
|
steps: { ...STEPS, required: true, help: "action steps and capture steps (JSON)" },
|
|
635
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
|
|
639
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
|
|
636
640
|
drive: { kind: "flag", help: "also write the 2 KiB drive RAM as drive8.prg, for knowledge space drive8" },
|
|
637
641
|
"min-block": { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "write a changed block of at least this many bytes (default 64)" },
|
|
638
642
|
},
|
package/src/host/vice/adapter.ts
CHANGED
|
@@ -59,7 +59,10 @@ export function conditionExpression(condition: Condition, space: Space): string
|
|
|
59
59
|
}
|
|
60
60
|
case "memory": {
|
|
61
61
|
if (condition.space !== "c64") {
|
|
62
|
-
throw new WireFailure(
|
|
62
|
+
throw new WireFailure(
|
|
63
|
+
"unsupported-in-space",
|
|
64
|
+
"A memory target or a memory condition tests only space c64 memory. For the drive, use a watchpoint in space drive8, or a c64_run_until address target in space drive8.",
|
|
65
|
+
);
|
|
63
66
|
}
|
|
64
67
|
return `(@${condition.view}:${formatC64Address(condition.address)} ${operator(condition.operator)} ${formatC64Byte(condition.value)})`;
|
|
65
68
|
}
|
package/src/host/vice/session.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
13
13
|
|
|
14
14
|
import { formatC64Address, instructionLength } from "../../c64.ts";
|
|
15
15
|
import { encodePng, type IndexedFrame } from "../../graphics.ts";
|
|
16
|
-
import { instructionText, operandAddress } from "../../opcodes.ts";
|
|
16
|
+
import { accessedValue, instructionText, operandAddress } from "../../opcodes.ts";
|
|
17
17
|
import type { ProcessSupervisor } from "../../native/processes.ts";
|
|
18
18
|
import { trace } from "../../trace.ts";
|
|
19
19
|
import type { ViceSessionFactory, ViceSessionHandle } from "../server.ts";
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
|
|
31
31
|
import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
|
|
32
32
|
import { compareFrames, differenceImage } from "./screen.ts";
|
|
33
|
+
import { isRefusal } from "../../errors.ts";
|
|
34
|
+
import { checkMedia, hasSignature } from "../../media.ts";
|
|
33
35
|
import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
|
|
34
36
|
import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
|
|
35
37
|
|
|
@@ -78,7 +80,6 @@ const WINDOW_CLOSED =
|
|
|
78
80
|
|
|
79
81
|
/** Emulator state that a move to another VICE cannot carry. */
|
|
80
82
|
/** The first bytes of every VICE snapshot file. */
|
|
81
|
-
const VSF_MAGIC = Buffer.from("VICE Snapshot File\x1a", "latin1");
|
|
82
83
|
const NOT_CARRIED = ["cpu history", "memory map", "profile", "keyboard input not yet typed"];
|
|
83
84
|
|
|
84
85
|
/** Starts another VICE for this session; `log` also gets that launch's lines. */
|
|
@@ -193,6 +194,28 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
193
194
|
* pc write, and reports the JAM only once.
|
|
194
195
|
*/
|
|
195
196
|
#jammed: number | undefined;
|
|
197
|
+
/**
|
|
198
|
+
* An autostart can still be loading: VICE goes on with it after a snapshot
|
|
199
|
+
* restore and writes the program over the restored memory. Only a reset stops it.
|
|
200
|
+
*/
|
|
201
|
+
#autostarting = false;
|
|
202
|
+
/** The C64 clock just after the last snapshot restore: VICE keeps the history of before it. */
|
|
203
|
+
#historySince: bigint | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* A breakpoint or watchpoint stopped a step, next or until-return early. VICE
|
|
206
|
+
* keeps the rest of that step and stops the machine once more when it ends.
|
|
207
|
+
* No monitor command ends it without running the CPU, so the session takes
|
|
208
|
+
* that one stop as the end of the step and lets the machine run on.
|
|
209
|
+
*/
|
|
210
|
+
#pendingStep = false;
|
|
211
|
+
/**
|
|
212
|
+
* The CPU stands at the stop of a reset without run, and nothing ran since. VICE
|
|
213
|
+
* (3.9, 3.10) stops answering at the restore of a snapshot saved here while the
|
|
214
|
+
* disk drive was busy, so a save here is refused.
|
|
215
|
+
*/
|
|
216
|
+
#atResetStop = false;
|
|
217
|
+
/** The CPU that the last run stopped in, when it was the drive: status then gives the drive's pc. */
|
|
218
|
+
#stopSpace: "c64" | "drive8" = "c64";
|
|
196
219
|
/** VICE records no CPU history after a JAM until the next reset. */
|
|
197
220
|
#historyStopped = false;
|
|
198
221
|
/** The CPU ran again after a JAM without a reset: the CPU history that VICE gives is old. */
|
|
@@ -396,6 +419,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
396
419
|
case "autostart": {
|
|
397
420
|
const { type, index, run } = params as ViceOperations["autostart"]["params"];
|
|
398
421
|
const bytes = onlyAttachment(attachments);
|
|
422
|
+
checkMedia(type, bytes);
|
|
399
423
|
if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
|
|
400
424
|
const file = this.#stage(bytes, type);
|
|
401
425
|
// VICE keeps a cartridge through every reset and autostart, as a real C64 does.
|
|
@@ -415,6 +439,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
415
439
|
}
|
|
416
440
|
await this.#runningCommand(() => this.#machine.autostart(file, index, run));
|
|
417
441
|
this.#clearJam();
|
|
442
|
+
this.#autostarting = true;
|
|
418
443
|
if (type === "crt") this.#cartridge = true;
|
|
419
444
|
this.#replaceStaged(stagedKind(type), file);
|
|
420
445
|
// A reset sets the CPU clock back, so the stopwatch starts again.
|
|
@@ -423,7 +448,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
423
448
|
}
|
|
424
449
|
case "diskAttach": {
|
|
425
450
|
const { type } = params as ViceOperations["diskAttach"]["params"];
|
|
426
|
-
const
|
|
451
|
+
const bytes = onlyAttachment(attachments);
|
|
452
|
+
checkMedia(type, bytes);
|
|
453
|
+
const file = this.#stage(bytes, type);
|
|
427
454
|
await this.#observe(() => this.#machine.attachDisk(file));
|
|
428
455
|
this.#replaceStaged("disk", file);
|
|
429
456
|
return { attached: true };
|
|
@@ -543,15 +570,23 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
543
570
|
// VICE 3.9 sends no JAM event: the opcode at the pc tells.
|
|
544
571
|
const stop = this.#lastStop ?? ((await this.#jamAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
|
|
545
572
|
if (stop !== undefined) Object.assign(status, stop);
|
|
573
|
+
// A point in the drive stopped it: the pc is the drive's, as advance-frames and run-until give it.
|
|
574
|
+
const point = status.pointId === undefined ? undefined : (status.stopReason === "watchpoint" ? this.#watchpoints : this.#breakpoints).get(status.pointId);
|
|
575
|
+
status.space = point?.space === "drive8" || this.#stopSpace === "drive8" ? "drive8" : "c64";
|
|
576
|
+
if (status.space === "drive8") status.pc = (await this.#machine.readRegisters("drive8")).pc;
|
|
546
577
|
}
|
|
547
578
|
return status;
|
|
548
579
|
}
|
|
549
580
|
|
|
550
581
|
async #execution(params: ExecutionParams): Promise<ExecutionResult> {
|
|
582
|
+
if (params.space === "drive8" && (params.action === "pause" || params.action === "resume")) {
|
|
583
|
+
throw new WireFailure("unsupported-in-space", `${params.action} stops or runs the whole machine, the computer and the disk drive together: use space c64.`);
|
|
584
|
+
}
|
|
551
585
|
if (params.action === "resume") {
|
|
552
586
|
if (this.#state === "stopped") {
|
|
553
587
|
await this.#refuseJammed("resume");
|
|
554
588
|
await this.#resume();
|
|
589
|
+
if (this.#pendingStep) await this.#endPendingStep();
|
|
555
590
|
}
|
|
556
591
|
return { state: "running" };
|
|
557
592
|
}
|
|
@@ -580,6 +615,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
580
615
|
throw new WireFailure("invalid-input", `The anchor is outside the frame: a ${this.#videoStandard} frame has lines 0-${raster.lines - 1} and cycles 0-${raster.cycles - 1}.`);
|
|
581
616
|
}
|
|
582
617
|
const outcome = await this.#runFrames(params.count, undefined, params.anchor);
|
|
618
|
+
this.#stopSpace = outcome.space;
|
|
583
619
|
return {
|
|
584
620
|
state: "stopped",
|
|
585
621
|
// A stop event carries the computer's PC; a stop in the drive reports the drive's.
|
|
@@ -592,6 +628,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
592
628
|
}
|
|
593
629
|
await this.#stop();
|
|
594
630
|
await this.#refuseJammed(params.action);
|
|
631
|
+
// A new step replaces the rest of an earlier one in VICE.
|
|
632
|
+
this.#pendingStep = false;
|
|
595
633
|
if (params.action === "until-return") {
|
|
596
634
|
const stopped = this.#nextStop(this.#untilReturnLimitMs);
|
|
597
635
|
await this.#resumingCommand(() => this.#machine.untilReturn());
|
|
@@ -603,18 +641,39 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
603
641
|
`The routine did not return within ${this.#untilReturnLimitMs / 1000} seconds. The machine is stopped at ${formatC64Address(await this.#pc())}.`,
|
|
604
642
|
);
|
|
605
643
|
}
|
|
606
|
-
return { state: "stopped", pc: stop.pc };
|
|
644
|
+
return { state: "stopped", pc: stop.pc, ...this.#stepStoppedByPoint() };
|
|
607
645
|
}
|
|
608
646
|
const count = params.count ?? 1;
|
|
647
|
+
this.#atResetStop = false;
|
|
609
648
|
try {
|
|
610
649
|
await this.#machine.step(count, params.action === "next", this.#stepLimitMs);
|
|
611
650
|
} catch (error) {
|
|
612
651
|
if (error instanceof MonitorTimeoutError) throw new StepTimeout(stepNotEnded(params.action, count, this.#stepLimitMs), error);
|
|
613
652
|
throw error;
|
|
614
653
|
}
|
|
615
|
-
// VICE answers a step only after the machine stopped again.
|
|
654
|
+
// VICE answers a step only after the machine stopped again; a ping reads the events of that stop (a hit point).
|
|
655
|
+
await this.#machine.stop();
|
|
616
656
|
this.#state = "stopped";
|
|
617
|
-
|
|
657
|
+
const point = this.#stepStoppedByPoint();
|
|
658
|
+
// A point stops the step early: VICE does not tell how many instructions ran.
|
|
659
|
+
return { state: "stopped", pc: await this.#pc(), ...(point.pointId === undefined ? { executed: count } : point) };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** The breakpoint or watchpoint that stopped a step, next or until-return early; VICE then keeps the rest of the step. */
|
|
663
|
+
#stepStoppedByPoint(): { stopReason?: PointStopReason; pointId?: number } {
|
|
664
|
+
const stop = this.#lastStop;
|
|
665
|
+
if (stop?.pointId === undefined) return {};
|
|
666
|
+
this.#pendingStep = true;
|
|
667
|
+
return { stopReason: stop.stopReason, pointId: stop.pointId };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** After a resume: waits for the stop that ends the rest of an earlier step, and runs on. */
|
|
671
|
+
async #endPendingStep(): Promise<void> {
|
|
672
|
+
const stop = await this.#nextStop(HALF_FRAME_LIMIT_MS);
|
|
673
|
+
if (stop === undefined) return;
|
|
674
|
+
this.#pendingStep = false;
|
|
675
|
+
// A user point or a JAM stopped the machine: it stays stopped and status tells why.
|
|
676
|
+
if (stop.hits.length === 0 && this.#lastStop === undefined) await this.#resume();
|
|
618
677
|
}
|
|
619
678
|
|
|
620
679
|
async #reset(params: { mode: "soft" | "hard"; run: boolean }): Promise<{ state: RunState; pc?: number }> {
|
|
@@ -639,6 +698,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
639
698
|
if (!reached) await this.#stop();
|
|
640
699
|
await this.#machine.deleteCheckpoint(checkpoint);
|
|
641
700
|
if (!reached) throw new WireFailure("operation-failed", "The C64 did not reach its reset vector after the reset.");
|
|
701
|
+
this.#atResetStop = true;
|
|
642
702
|
return { state: "stopped", pc: vector };
|
|
643
703
|
}
|
|
644
704
|
|
|
@@ -695,6 +755,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
695
755
|
}
|
|
696
756
|
if (end === "unknown") {
|
|
697
757
|
if (await this.#jamAt(stop.pc)) return { end: "jam", frames, pc: stop.pc, space: "c64" };
|
|
758
|
+
if (this.#pendingStep) {
|
|
759
|
+
// The rest of an earlier step ended: run on.
|
|
760
|
+
this.#pendingStep = false;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
698
763
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this run.");
|
|
699
764
|
}
|
|
700
765
|
// A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
|
|
@@ -763,6 +828,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
763
828
|
const point = this.#userPoint(stop.hits);
|
|
764
829
|
if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
|
|
765
830
|
if (await this.#jamAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
|
|
831
|
+
if (this.#pendingStep) {
|
|
832
|
+
this.#pendingStep = false;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
766
835
|
throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
|
|
767
836
|
}
|
|
768
837
|
} finally {
|
|
@@ -821,10 +890,22 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
821
890
|
frame,
|
|
822
891
|
rasterLine: Math.floor(at / cycles),
|
|
823
892
|
rasterCycle: at % cycles,
|
|
824
|
-
...(address === undefined ? {} : { address,
|
|
893
|
+
...(address === undefined ? {} : { address, ...(await this.#accessed(bytes[0]!, entry, address, byteAt)) }),
|
|
825
894
|
};
|
|
826
895
|
}
|
|
827
896
|
|
|
897
|
+
/**
|
|
898
|
+
* The byte of a read or write hit. A read of an I/O register after the
|
|
899
|
+
* access can give another byte than the access did (raster bits, cleared
|
|
900
|
+
* flags), so for $D000-$DFFF only a load or a store gives a value.
|
|
901
|
+
*/
|
|
902
|
+
async #accessed(opcode: number, before: { a: number; x: number; y: number }, address: number, byteAt: (address: number) => Promise<number>): Promise<{ value?: number }> {
|
|
903
|
+
const after = await this.#machine.readRegisters("c64");
|
|
904
|
+
const io = address >= 0xd000 && address <= 0xdfff;
|
|
905
|
+
const value = accessedValue(opcode, before, after, io ? undefined : await byteAt(address));
|
|
906
|
+
return value === undefined ? {} : { value };
|
|
907
|
+
}
|
|
908
|
+
|
|
828
909
|
/**
|
|
829
910
|
* Completes a stop that a drive 8 checkpoint caused: the computer runs to
|
|
830
911
|
* its next instruction boundary, where a computer checkpoint stops it, so
|
|
@@ -896,6 +977,9 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
896
977
|
|
|
897
978
|
/** A reset or an autostart starts a jammed CPU and the CPU history again. */
|
|
898
979
|
#clearJam(): void {
|
|
980
|
+
this.#autostarting = false;
|
|
981
|
+
// A reset sets the clock back to 0: the history from before it is of this run again.
|
|
982
|
+
this.#historySince = undefined;
|
|
899
983
|
this.#jammed = undefined;
|
|
900
984
|
this.#historyStopped = false;
|
|
901
985
|
this.#historyOld = false;
|
|
@@ -952,6 +1036,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
952
1036
|
const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
|
|
953
1037
|
// A stop event carries the computer's PC; a stop in the drive reports the drive's.
|
|
954
1038
|
const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
|
|
1039
|
+
this.#stopSpace = outcome.space;
|
|
955
1040
|
return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space, ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }) };
|
|
956
1041
|
} finally {
|
|
957
1042
|
await this.#machine.deleteCheckpoint(checkpoint);
|
|
@@ -1007,7 +1092,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1007
1092
|
|
|
1008
1093
|
#point<T>(registry: Map<number, T>, kind: string, id: number): T {
|
|
1009
1094
|
const point = registry.get(id);
|
|
1010
|
-
if (point === undefined)
|
|
1095
|
+
if (point === undefined) {
|
|
1096
|
+
const other = kind === "breakpoint" ? (this.#watchpoints.has(id) ? "watchpoint" : undefined) : this.#breakpoints.has(id) ? "breakpoint" : undefined;
|
|
1097
|
+
throw new WireFailure("not-found", `There is no ${kind} with id ${id}.${other === undefined ? "" : ` Id ${id} is a ${other}: use c64_${other}.`}`);
|
|
1098
|
+
}
|
|
1011
1099
|
return point;
|
|
1012
1100
|
}
|
|
1013
1101
|
|
|
@@ -1121,9 +1209,14 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1121
1209
|
);
|
|
1122
1210
|
}
|
|
1123
1211
|
return this.#observe(async () => {
|
|
1124
|
-
const
|
|
1125
|
-
if (params.space === "drive8")
|
|
1212
|
+
const all = await this.#machine.history(params.limit, params.space);
|
|
1213
|
+
if (params.space === "drive8") {
|
|
1214
|
+
// VICE records a wrong third byte for a 3-byte drive instruction ("JSR $00a6" for 20 a6 d5): the drive memory gives the bytes.
|
|
1215
|
+
const memory = await this.#machine.readAll("drive8");
|
|
1216
|
+
return { entries: all.map(({ clock: _clock, ...entry }) => driveEntry(entry, memory)) };
|
|
1217
|
+
}
|
|
1126
1218
|
const now = await this.#machine.clock();
|
|
1219
|
+
const raw = entriesSince(all, this.#historySince, now);
|
|
1127
1220
|
const position = await this.#machine.rasterPosition();
|
|
1128
1221
|
const { lines, cycles } = RASTER[this.#videoStandard];
|
|
1129
1222
|
const frame = BigInt(lines * cycles);
|
|
@@ -1197,6 +1290,13 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1197
1290
|
if (params.action === "list") return { snapshots: [...this.#snapshots.keys()] };
|
|
1198
1291
|
const existing = this.#snapshots.get(params.name);
|
|
1199
1292
|
if (params.action === "save") {
|
|
1293
|
+
if (this.#atResetStop) {
|
|
1294
|
+
throw new WireFailure(
|
|
1295
|
+
"operation-failed",
|
|
1296
|
+
"The CPU stands at the reset stop of c64_reset with run false. The emulator stops answering at the restore of a snapshot saved here when the disk drive was busy. " +
|
|
1297
|
+
"Step one instruction with c64_execution, then save the snapshot.",
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1200
1300
|
if (existing === undefined && this.#snapshots.size >= MAX_SNAPSHOTS) {
|
|
1201
1301
|
throw new WireFailure("limit-exceeded", `A session keeps at most ${MAX_SNAPSHOTS} snapshots. Remove one first with action remove.`);
|
|
1202
1302
|
}
|
|
@@ -1218,7 +1318,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1218
1318
|
async #restoreFrom(file: string): Promise<{ restored: true; state: "stopped" }> {
|
|
1219
1319
|
await this.#stop();
|
|
1220
1320
|
const elapsed = (await this.#machine.clock()) - this.#timingStart;
|
|
1321
|
+
// The restore replaces the whole machine state, so the reset that stops the autostart leaves no trace.
|
|
1322
|
+
if (this.#autostarting) await this.#reset({ mode: "soft", run: false });
|
|
1221
1323
|
await this.#machine.restoreSnapshot(file);
|
|
1324
|
+
this.#historySince = await this.#machine.clock();
|
|
1222
1325
|
// A restore starts a jammed CPU again, but VICE still records no CPU history.
|
|
1223
1326
|
this.#jammed = undefined;
|
|
1224
1327
|
if (this.#historyStopped) this.#historyOld = true;
|
|
@@ -1239,7 +1342,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1239
1342
|
}
|
|
1240
1343
|
const bytes = onlyAttachment(attachments);
|
|
1241
1344
|
// Checked before the machine stops: VICE gives no reason for a file that is no snapshot.
|
|
1242
|
-
if (!
|
|
1345
|
+
if (!hasSignature("vsf", bytes)) throw new WireFailure("invalid-input", "The file is not a VICE snapshot file (.vsf). Give a file that c64_snapshot save wrote.");
|
|
1243
1346
|
const file = this.#stage(bytes, "vsf");
|
|
1244
1347
|
// VICE resets the machine after a snapshot file that it cannot read: a backup keeps the state before the restore.
|
|
1245
1348
|
const backup = join(this.#workDir, "before-restore.vsf");
|
|
@@ -1425,6 +1528,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1425
1528
|
* arrive together, and those events must have the last word.
|
|
1426
1529
|
*/
|
|
1427
1530
|
async #resumingCommand(send: () => Promise<void>): Promise<void> {
|
|
1531
|
+
this.#atResetStop = false;
|
|
1532
|
+
this.#stopSpace = "c64";
|
|
1428
1533
|
this.#state = "running";
|
|
1429
1534
|
this.#lastStop = undefined;
|
|
1430
1535
|
try {
|
|
@@ -1534,6 +1639,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1534
1639
|
|
|
1535
1640
|
async #translate(error: unknown, runningBefore: boolean, halts: number): Promise<unknown> {
|
|
1536
1641
|
if (error instanceof WireFailure) return error;
|
|
1642
|
+
// A refusal of a shared module (for example the media check) goes on the wire as the host's own.
|
|
1643
|
+
if (isRefusal(error)) return new WireFailure(error.code, error.message);
|
|
1537
1644
|
if (this.#closed) return new WireFailure("machine-unavailable", "The emulator session is closed.");
|
|
1538
1645
|
if (error instanceof MonitorConnectionError || error instanceof TextMonitorError || error instanceof StepTimeout) {
|
|
1539
1646
|
this.#log(error.message);
|
|
@@ -1614,6 +1721,32 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
1614
1721
|
}
|
|
1615
1722
|
}
|
|
1616
1723
|
|
|
1724
|
+
/** A drive history entry with its bytes and text from drive memory, when the opcode there is still the one that ran. */
|
|
1725
|
+
export function driveEntry<T extends { address: number; bytes: string; text: string }>(entry: T, memory: Uint8Array): T {
|
|
1726
|
+
const opcode = Number.parseInt(entry.bytes.slice(0, 2), 16);
|
|
1727
|
+
if (memory[entry.address] !== opcode) return entry;
|
|
1728
|
+
const length = instructionLength(opcode);
|
|
1729
|
+
const bytes = Array.from({ length }, (_, offset) => memory[(entry.address + offset) & 0xffff]!);
|
|
1730
|
+
return { ...entry, bytes: Buffer.from(bytes).toString("hex"), text: instructionText(bytes, entry.address) };
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* The history entries (oldest first) that the CPU executed after a restore at
|
|
1735
|
+
* clock `since`, with the clock at `now`: the last entries, back to the first
|
|
1736
|
+
* one outside since to now or the first one whose clock does not go up to the
|
|
1737
|
+
* next. The others are of before the restore.
|
|
1738
|
+
*/
|
|
1739
|
+
export function entriesSince<T extends { clock: bigint }>(entries: readonly T[], since: bigint | undefined, now: bigint): T[] {
|
|
1740
|
+
if (since === undefined) return [...entries];
|
|
1741
|
+
let first = entries.length;
|
|
1742
|
+
while (first > 0) {
|
|
1743
|
+
const entry = entries[first - 1]!;
|
|
1744
|
+
if (entry.clock < since || entry.clock > now || (first < entries.length && entry.clock >= entries[first]!.clock)) break;
|
|
1745
|
+
first--;
|
|
1746
|
+
}
|
|
1747
|
+
return entries.slice(first);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1617
1750
|
/** The one attachment of an operation that takes a file (see attachmentCount); refuses any other count. */
|
|
1618
1751
|
function onlyAttachment(attachments: Buffer[]): Buffer {
|
|
1619
1752
|
const [bytes] = attachments;
|