@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.4
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/README.md +19 -59
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +5 -2
- package/skills/c64-assembler/scripts/assemble.ts +8 -1
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +1 -2
- package/skills/c64-disk/SKILL.md +20 -6
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +59 -5
- package/skills/c64-emulator/SKILL.md +11 -4
- package/skills/c64-knowledge/SKILL.md +53 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
- package/skills/c64-memory-map/SKILL.md +4 -1
- package/skills/c64-memory-map/scripts/memmap.ts +14 -3
- package/skills/c64-provenance/SKILL.md +37 -9
- package/skills/c64-provenance/scripts/compare.ts +60 -29
- package/skills/c64-provenance/scripts/provenance.ts +248 -19
- package/skills/c64-provenance/scripts/runtime.ts +179 -0
- package/skills/c64-provenance/scripts/separate.ts +132 -0
- package/skills/c64-reverse-engineering/SKILL.md +65 -11
- package/skills/c64-source/SKILL.md +119 -0
- package/skills/c64-source/scripts/generate.ts +402 -0
- package/skills/c64-source/scripts/loader.ts +320 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +558 -0
- package/skills/c64-static-analysis/SKILL.md +27 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +6 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +27 -3
- package/skills/c64-testing/references/scenario-format.md +43 -3
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +153 -135
- package/skills/c64-testing/scripts/test.ts +16 -6
- package/skills/c64-unpacker/SKILL.md +65 -14
- package/skills/c64-unpacker/scripts/evidence.ts +86 -21
- package/skills/c64-unpacker/scripts/loads.ts +174 -0
- package/skills/c64-unpacker/scripts/machine.ts +68 -0
- package/skills/c64-unpacker/scripts/unpack.ts +302 -48
- package/src/c64.ts +91 -0
- package/src/cli/status.ts +3 -1
- package/src/host/main.ts +10 -25
- package/src/host/server.ts +8 -40
- package/src/host/vice/adapter.ts +25 -10
- package/src/host/vice/session.ts +64 -20
- package/src/host-client/autostart.ts +45 -0
- package/src/host-client/connect.ts +4 -5
- package/src/host-client/coverage.ts +59 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/transfer.ts +29 -3
- package/src/host-client/vice-session.ts +42 -13
- package/src/knowledge/history.ts +40 -13
- package/src/knowledge/import.ts +72 -56
- package/src/knowledge/read.ts +203 -52
- package/src/knowledge/schema.ts +94 -12
- package/src/knowledge/write.ts +432 -123
- package/src/mcp/main.ts +1 -4
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +4 -2
- package/src/mcp/tools/debug.ts +110 -24
- package/src/mcp/tools/input.ts +1 -0
- package/src/mcp/tools/media.ts +22 -6
- package/src/mcp/tools/memory.ts +57 -10
- package/src/mcp/tools/video.ts +9 -3
- package/src/native/discover.ts +5 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +30 -1
- package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/vice.ts +72 -18
- package/src/script.ts +3 -2
|
@@ -3,19 +3,11 @@
|
|
|
3
3
|
// Observations name semantic symbols that resolve per side, comparisons are
|
|
4
4
|
// explicit, and the result is PASS, FAIL or INCONCLUSIVE.
|
|
5
5
|
|
|
6
|
-
import { formatC64Address, parseC64Address
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
MAX_KEYBOARD_BYTES,
|
|
12
|
-
MAX_MASK_RECTANGLES,
|
|
13
|
-
MAX_MEMORY_READ,
|
|
14
|
-
MAX_MEMORY_WRITE,
|
|
15
|
-
MAX_TIMEOUT_FRAMES,
|
|
16
|
-
WireFailure,
|
|
17
|
-
} from "#src/host-client/tools.ts";
|
|
18
|
-
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
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";
|
|
8
|
+
import { LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, runProgram } from "#src/host-client/autostart.ts";
|
|
9
|
+
import { ACTION_STEP_KEYS, checkActionStep, checkLocation, isInteger, isObject, runActionStep, type ActionStep, type Location } from "#src/host-client/steps.ts";
|
|
10
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
19
11
|
import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
20
12
|
import { openForRead } from "#src/knowledge/database.ts";
|
|
21
13
|
import { listSymbols } from "#src/knowledge/read.ts";
|
|
@@ -26,7 +18,7 @@ export type Side = "original" | "rebuild";
|
|
|
26
18
|
export const SIDES: Side[] = ["original", "rebuild"];
|
|
27
19
|
|
|
28
20
|
/** A "$xxxx" address or a symbol name that each side resolves on its own. */
|
|
29
|
-
export type Location
|
|
21
|
+
export type { Location };
|
|
30
22
|
|
|
31
23
|
export interface MemoryObservation {
|
|
32
24
|
at: Location;
|
|
@@ -34,6 +26,8 @@ export interface MemoryObservation {
|
|
|
34
26
|
size?: number;
|
|
35
27
|
/** The largest allowed difference per byte; 0 when not given. */
|
|
36
28
|
tolerance?: number;
|
|
29
|
+
/** Bytes in the range that the comparison leaves out, each with the reason (D46). */
|
|
30
|
+
exclude?: Array<{ at: Location; reason: string }>;
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
export interface ScreenPolicy {
|
|
@@ -43,18 +37,18 @@ export interface ScreenPolicy {
|
|
|
43
37
|
mask?: Array<{ x: number; y: number; width: number; height: number }>;
|
|
44
38
|
}
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
type StepBody =
|
|
47
41
|
| { reset: "hard" | "soft" }
|
|
48
42
|
| { load: true }
|
|
49
43
|
| { autostart: true }
|
|
50
|
-
| {
|
|
51
|
-
| {
|
|
52
|
-
|
|
|
53
|
-
| { type: string }
|
|
54
|
-
| { frames: number }
|
|
55
|
-
| { runUntil: { at: Location } | { memory: { at: Location; equals: number } }; timeoutFrames: number }
|
|
44
|
+
| { snapshot: string }
|
|
45
|
+
| { normalize: string }
|
|
46
|
+
| ActionStep
|
|
56
47
|
| { observe: string; memory?: Array<Location | MemoryObservation>; registers?: ObservedRegister[]; screen?: ScreenPolicy; vicii?: boolean; sprites?: number[] };
|
|
57
48
|
|
|
49
|
+
/** A step; with side, only that side does it (D46). An observe step has no side: it compares both. */
|
|
50
|
+
export type Step = StepBody & { side?: Side };
|
|
51
|
+
|
|
58
52
|
export interface Subject {
|
|
59
53
|
program: string;
|
|
60
54
|
/** "knowledge" for knowledge.db, a project JSON file (c64-assembler output or a name → "$xxxx" map), or "none". */
|
|
@@ -85,26 +79,23 @@ export interface ScenarioResult {
|
|
|
85
79
|
reason?: string;
|
|
86
80
|
checkpoints: string[];
|
|
87
81
|
differences: Difference[];
|
|
82
|
+
/** The bytes that the comparison left out, with their reasons (D46). */
|
|
83
|
+
excluded?: Array<{ checkpoint: string; at: Location; reason: string }>;
|
|
84
|
+
/** The files that a FAIL wrote with --out. */
|
|
85
|
+
files?: string[];
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
const STEP_KINDS = ["reset", "load", "autostart", "write", "registers", "joystick", "type", "frames", "runUntil", "observe"];
|
|
88
|
+
const STEP_KINDS = ["reset", "load", "autostart", "snapshot", "normalize", "write", "registers", "joystick", "type", "frames", "runUntil", "observe"];
|
|
91
89
|
|
|
92
90
|
/** The keys each kind of step can have. */
|
|
93
91
|
const STEP_KEYS: Record<string, readonly string[]> = {
|
|
94
|
-
|
|
92
|
+
...ACTION_STEP_KEYS,
|
|
95
93
|
observe: ["observe", "memory", "registers", "screen", "vicii", "sprites"],
|
|
96
94
|
};
|
|
97
95
|
|
|
98
96
|
/** The registers an observe step can compare. */
|
|
99
97
|
export const OBSERVED_REGISTERS = ["pc", "a", "x", "y", "sp", "flags"] as const;
|
|
100
98
|
type ObservedRegister = (typeof OBSERVED_REGISTERS)[number];
|
|
101
|
-
/** The registers a registers step can set. */
|
|
102
|
-
const SET_REGISTERS = ["pc", "a", "x", "y", "sp"];
|
|
103
|
-
|
|
104
|
-
type JoystickDirection = (typeof JOYSTICK_DIRECTIONS)[number];
|
|
105
|
-
|
|
106
|
-
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107
|
-
const isInteger = (value: unknown, min: number, max: number): value is number => Number.isInteger(value) && (value as number) >= min && (value as number) <= max;
|
|
108
99
|
|
|
109
100
|
/** Reads and checks a scenario file. Throws ScenarioError naming the first problem. */
|
|
110
101
|
export function readScenario(path: string): Scenario {
|
|
@@ -135,8 +126,9 @@ export function readScenario(path: string): Scenario {
|
|
|
135
126
|
const kinds = "observe" in step ? ["observe"] : Object.keys(step).filter((key) => STEP_KINDS.includes(key));
|
|
136
127
|
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${STEP_KINDS.join(", ")}`);
|
|
137
128
|
const kind = kinds[0]!;
|
|
138
|
-
const allowed = STEP_KEYS[kind] ?? [kind];
|
|
129
|
+
const allowed = [...(STEP_KEYS[kind] ?? [kind]), ...(kind === "observe" ? [] : ["side"])];
|
|
139
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
|
+
if (step.side !== undefined && !(SIDES as unknown[]).includes(step.side)) fail(`${at}: side must be original or rebuild`);
|
|
140
132
|
checkStep(kind, step, at, fail);
|
|
141
133
|
if (kind === "observe") {
|
|
142
134
|
if (labels.has(step.observe as string)) fail(`${at} needs a new checkpoint name`);
|
|
@@ -149,18 +141,6 @@ export function readScenario(path: string): Scenario {
|
|
|
149
141
|
return scenario;
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
/** Checks a "$xxxx" address or a symbol name. */
|
|
153
|
-
function checkLocation(value: unknown, what: string, fail: (message: string) => never): void {
|
|
154
|
-
if (typeof value !== "string" || value === "") fail(`${what} must be a $ address or a symbol name`);
|
|
155
|
-
if ((value as string).startsWith("$")) {
|
|
156
|
-
try {
|
|
157
|
-
parseC64Address(value as string);
|
|
158
|
-
} catch {
|
|
159
|
-
fail(`${what} must be $ followed by four hex digits, not ${value as string}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
144
|
/** Checks the value of one step against its kind. */
|
|
165
145
|
function checkStep(kind: string, step: Record<string, unknown>, at: string, fail: (message: string) => never): void {
|
|
166
146
|
switch (kind) {
|
|
@@ -171,60 +151,19 @@ function checkStep(kind: string, step: Record<string, unknown>, at: string, fail
|
|
|
171
151
|
case "autostart":
|
|
172
152
|
if (step[kind] !== true) fail(`${at}: ${kind} must be true`);
|
|
173
153
|
return;
|
|
174
|
-
case "
|
|
175
|
-
|
|
176
|
-
if (!isObject(write) || Object.keys(write).some((key) => key !== "at" && key !== "bytes")) return fail(`${at}: write needs at and bytes, and nothing else`);
|
|
177
|
-
checkLocation(write.at, `${at}: write.at`, fail);
|
|
178
|
-
if (!Array.isArray(write.bytes) || write.bytes.length === 0 || write.bytes.length > MAX_MEMORY_WRITE || !write.bytes.every((byte) => isInteger(byte, 0, 0xff))) {
|
|
179
|
-
fail(`${at}: write.bytes must be a list of 1 to ${MAX_MEMORY_WRITE} numbers from 0 to 255`);
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
case "registers": {
|
|
184
|
-
const registers = step.registers;
|
|
185
|
-
if (!isObject(registers) || Object.keys(registers).length === 0) return fail(`${at}: registers must name at least one of ${SET_REGISTERS.join(", ")}`);
|
|
186
|
-
for (const [name, value] of Object.entries(registers)) {
|
|
187
|
-
if (!SET_REGISTERS.includes(name)) fail(`${at}: registers can set only ${SET_REGISTERS.join(", ")}, not ${name}`);
|
|
188
|
-
if (typeof value === "string") checkLocation(value, `${at}: registers.${name}`, fail);
|
|
189
|
-
else if (!isInteger(value, 0, name === "pc" ? 0xffff : 0xff)) fail(`${at}: registers.${name} must be a symbol name, a $ address or a number from 0 to ${name === "pc" ? 65535 : 255}`);
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
case "joystick": {
|
|
194
|
-
const joystick = step.joystick;
|
|
195
|
-
if (!isObject(joystick) || Object.keys(joystick).some((key) => !["port", "direction", "fire"].includes(key))) return fail(`${at}: joystick needs port and direction, and fire is optional`);
|
|
196
|
-
if (joystick.port !== 1 && joystick.port !== 2) fail(`${at}: joystick.port must be 1 or 2`);
|
|
197
|
-
if (!(JOYSTICK_DIRECTIONS as readonly unknown[]).includes(joystick.direction)) fail(`${at}: joystick.direction must be one of ${JOYSTICK_DIRECTIONS.join(", ")}`);
|
|
198
|
-
if (joystick.fire !== undefined && typeof joystick.fire !== "boolean") fail(`${at}: joystick.fire must be true or false`);
|
|
154
|
+
case "snapshot":
|
|
155
|
+
if (typeof step.snapshot !== "string" || step.snapshot === "") fail(`${at}: snapshot needs a .vsf file in the project`);
|
|
199
156
|
return;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (typeof step.type !== "string" || step.type === "") return fail(`${at}: type needs text`);
|
|
203
|
-
let bytes: Uint8Array;
|
|
204
|
-
try {
|
|
205
|
-
bytes = textToPetscii(step.type);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
return fail(`${at}: ${(error as Error).message}`);
|
|
208
|
-
}
|
|
209
|
-
if (bytes.length > MAX_KEYBOARD_BYTES) fail(`${at}: type can have at most ${MAX_KEYBOARD_BYTES} keys`);
|
|
157
|
+
case "normalize":
|
|
158
|
+
if (typeof step.normalize !== "string" || step.normalize === "") fail(`${at}: normalize needs an entry state file in the project`);
|
|
210
159
|
return;
|
|
211
|
-
|
|
160
|
+
case "write":
|
|
161
|
+
case "registers":
|
|
162
|
+
case "joystick":
|
|
163
|
+
case "type":
|
|
212
164
|
case "frames":
|
|
213
|
-
|
|
214
|
-
return;
|
|
215
|
-
case "runUntil": {
|
|
216
|
-
const target = step.runUntil;
|
|
217
|
-
if (!isObject(target) || Object.keys(target).length !== 1 || !("at" in target || "memory" in target)) return fail(`${at}: runUntil needs at or memory`);
|
|
218
|
-
if ("at" in target) checkLocation(target.at, `${at}: runUntil.at`, fail);
|
|
219
|
-
else {
|
|
220
|
-
const memory = target.memory;
|
|
221
|
-
if (!isObject(memory) || Object.keys(memory).some((key) => key !== "at" && key !== "equals")) return fail(`${at}: runUntil.memory needs at and equals, and nothing else`);
|
|
222
|
-
checkLocation(memory.at, `${at}: runUntil.memory.at`, fail);
|
|
223
|
-
if (!isInteger(memory.equals, 0, 0xff)) fail(`${at}: runUntil.memory.equals must be a number from 0 to 255`);
|
|
224
|
-
}
|
|
225
|
-
if (!isInteger(step.timeoutFrames, 1, MAX_TIMEOUT_FRAMES)) fail(`${at} needs timeoutFrames from 1 to ${MAX_TIMEOUT_FRAMES}`);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
165
|
+
case "runUntil":
|
|
166
|
+
return checkActionStep(kind, step, at, fail);
|
|
228
167
|
default:
|
|
229
168
|
checkObserve(step, at, fail);
|
|
230
169
|
}
|
|
@@ -239,7 +178,15 @@ function checkObserve(step: Record<string, unknown>, at: string, fail: (message:
|
|
|
239
178
|
checkLocation(item, `${at}: memory`, fail);
|
|
240
179
|
continue;
|
|
241
180
|
}
|
|
242
|
-
if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "tolerance"].includes(key))) return fail(`${at}: a memory object has at, size and
|
|
181
|
+
if (!isObject(item) || Object.keys(item).some((key) => !["at", "size", "tolerance", "exclude"].includes(key))) return fail(`${at}: a memory object has at, size, tolerance and exclude, and nothing else`);
|
|
182
|
+
if (item.exclude !== undefined) {
|
|
183
|
+
if (!Array.isArray(item.exclude) || item.exclude.length === 0) fail(`${at}: memory.exclude must be a list of objects with at and reason`);
|
|
184
|
+
for (const excluded of item.exclude as unknown[]) {
|
|
185
|
+
if (!isObject(excluded) || Object.keys(excluded).some((key) => key !== "at" && key !== "reason")) return fail(`${at}: an exclude has at and reason, and nothing else`);
|
|
186
|
+
checkLocation(excluded.at, `${at}: memory.exclude.at`, fail);
|
|
187
|
+
if (typeof excluded.reason !== "string" || excluded.reason.trim() === "") fail(`${at}: an exclude needs a reason: why this byte may differ`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
243
190
|
checkLocation(item.at, `${at}: memory.at`, fail);
|
|
244
191
|
if (item.size !== undefined && !isInteger(item.size, 1, MAX_MEMORY_READ)) fail(`${at}: memory.size must be a number from 1 to ${MAX_MEMORY_READ}`);
|
|
245
192
|
if (item.tolerance !== undefined && !isInteger(item.tolerance, 0, 0xff)) fail(`${at}: memory.tolerance must be a number from 0 to 255`);
|
|
@@ -334,11 +281,34 @@ export function resolve(location: Location, symbols: Map<string, number>, side:
|
|
|
334
281
|
}
|
|
335
282
|
|
|
336
283
|
interface Observation {
|
|
337
|
-
memory: Array<{ label: string; bytes: number[]; tolerance: number }>;
|
|
284
|
+
memory: Array<{ label: string; address: number; bytes: number[]; tolerance: number; excluded: Array<{ offset: number; at: Location; reason: string }> }>;
|
|
338
285
|
registers: Partial<Record<ObservedRegister, unknown>>;
|
|
339
286
|
screen?: { match: boolean; mismatchingPixels: number; mismatchRatio: number; bounds?: unknown };
|
|
340
287
|
vicii?: unknown;
|
|
341
288
|
sprites?: unknown;
|
|
289
|
+
/** PNG images for the files of a FAIL (D46): the side's screen, and on the rebuild the difference image. */
|
|
290
|
+
png?: string;
|
|
291
|
+
diffPng?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Writes pages 0-3 and $00/$01 from an entry state file (D43, D46): both sides then start from the same RAM. */
|
|
295
|
+
async function normalize(client: ViceSessionClient, path: string): Promise<void> {
|
|
296
|
+
let state: { pages?: Record<string, string>; cpuPort?: Record<string, number> };
|
|
297
|
+
try {
|
|
298
|
+
state = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as typeof state;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof WireFailure) throw error;
|
|
301
|
+
throw new ScenarioError(`${path} is not an entry state file: ${(error as Error).message}`);
|
|
302
|
+
}
|
|
303
|
+
const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => state.pages?.[page]);
|
|
304
|
+
const port = [state.cpuPort?.$00, state.cpuPort?.$01];
|
|
305
|
+
if (pages.some((page) => typeof page !== "string" || !/^[0-9a-f]{512}$/.test(page)) || port.some((value) => !isInteger(value, 0, 0xff))) {
|
|
306
|
+
throw new ScenarioError(`${path} is not an entry state file: it needs pages $0000 to $0300 and cpuPort $00 and $01`);
|
|
307
|
+
}
|
|
308
|
+
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
309
|
+
// Pages 0 to 3 in RAM from $0002, then the CPU port: the port bits switch what the CPU sees.
|
|
310
|
+
await client.memoryWrite({ address: 0x0002, data: pages.join("").slice(4), space: "c64", view: "ram" });
|
|
311
|
+
await client.memoryWrite({ address: 0x0000, data: Buffer.from(port as number[]).toString("hex"), space: "c64", view: "cpu" });
|
|
342
312
|
}
|
|
343
313
|
|
|
344
314
|
/**
|
|
@@ -350,27 +320,39 @@ export function baselineName(checkpoint: number, label: string): string {
|
|
|
350
320
|
return `test-${checkpoint}-${label}`.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
351
321
|
}
|
|
352
322
|
|
|
353
|
-
|
|
323
|
+
/** Autostart that returns only when the program runs (runProgram), or INCONCLUSIVE with the reason. */
|
|
324
|
+
async function startProgram(client: ViceSessionClient, program: string, side: Side): Promise<void> {
|
|
325
|
+
const failed = await runProgram(client, program, readProjectFile(program).bytes);
|
|
326
|
+
if (failed === "not-loaded") throw new Inconclusive(`the ${side} program was not in memory after ${LOAD_LIMIT_FRAMES} frames of autostart`);
|
|
327
|
+
if (failed === "not-started") throw new Inconclusive(`the ${side} program did not start within ${RUN_LIMIT_FRAMES} frames of RUN; autostart runs only a program with a BASIC start line`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side, symbols: Map<string, number>, files: boolean): Promise<Map<string, Observation>> {
|
|
354
331
|
const subject = scenario[side];
|
|
355
332
|
const observations = new Map<string, Observation>();
|
|
356
333
|
const at = (location: Location) => resolve(location, symbols, side);
|
|
357
|
-
const stop = async () => {
|
|
358
|
-
if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
|
|
359
|
-
};
|
|
360
334
|
// Each side starts from a hard reset, stopped at the reset vector, with the control ports released.
|
|
361
335
|
await client.reset({ mode: "hard", run: false });
|
|
362
336
|
await client.joystick({ port: 1, direction: "center", fire: false });
|
|
363
337
|
await client.joystick({ port: 2, direction: "center", fire: false });
|
|
364
338
|
let checkpoint = 0;
|
|
365
339
|
for (const step of scenario.steps) {
|
|
340
|
+
if (step.side !== undefined && step.side !== side) continue;
|
|
366
341
|
// Observe first: an observe step may also carry "registers" or "memory".
|
|
367
342
|
if ("observe" in step) {
|
|
368
343
|
checkpoint++;
|
|
369
344
|
const observation: Observation = { memory: [], registers: {} };
|
|
370
345
|
for (const item of step.memory ?? []) {
|
|
371
|
-
const spec = typeof item === "string" ? { at: item } : item;
|
|
372
|
-
const
|
|
373
|
-
|
|
346
|
+
const spec: MemoryObservation = typeof item === "string" ? { at: item } : item;
|
|
347
|
+
const address = at(spec.at);
|
|
348
|
+
const size = spec.size ?? 1;
|
|
349
|
+
const read = await client.memoryRead({ address, size, space: "c64", view: "cpu" });
|
|
350
|
+
const excluded = (spec.exclude ?? []).map((exclude) => {
|
|
351
|
+
const offset = at(exclude.at) - address;
|
|
352
|
+
if (offset < 0 || offset >= size) throw new ScenarioError(`the exclude ${exclude.at} lies outside the observed ${spec.at} (${size} bytes)`);
|
|
353
|
+
return { offset, at: exclude.at, reason: exclude.reason };
|
|
354
|
+
});
|
|
355
|
+
observation.memory.push({ label: size > 1 ? `${spec.at} (${size} bytes)` : spec.at, address, bytes: [...Buffer.from(read.data, "hex")], tolerance: spec.tolerance ?? 0, excluded });
|
|
374
356
|
}
|
|
375
357
|
if (step.registers !== undefined) {
|
|
376
358
|
const registers = await client.registersGet("c64");
|
|
@@ -385,10 +367,15 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
385
367
|
if (step.screen !== undefined) {
|
|
386
368
|
const baseline = baselineName(checkpoint, step.observe);
|
|
387
369
|
if (side === "original") {
|
|
388
|
-
await client.screenCapture(baseline);
|
|
370
|
+
const shot = await client.screenCapture(baseline);
|
|
371
|
+
if (files) observation.png = shot.png;
|
|
389
372
|
} else {
|
|
390
|
-
const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff:
|
|
373
|
+
const compared = await client.screenCompare({ baseline, maxMismatchRatio: step.screen.maxMismatchRatio ?? 0, mask: step.screen.mask ?? [], includeDiff: files });
|
|
391
374
|
observation.screen = { match: compared.match, mismatchingPixels: compared.mismatchingPixels, mismatchRatio: compared.mismatchRatio, ...(compared.bounds === undefined ? {} : { bounds: compared.bounds }) };
|
|
375
|
+
if (files) {
|
|
376
|
+
observation.png = (await client.screenCapture()).png;
|
|
377
|
+
if (compared.diffPng !== undefined) observation.diffPng = compared.diffPng;
|
|
378
|
+
}
|
|
392
379
|
await client.screenDiscard(baseline);
|
|
393
380
|
}
|
|
394
381
|
}
|
|
@@ -399,30 +386,16 @@ async function runSide(client: ViceSessionClient, scenario: Scenario, side: Side
|
|
|
399
386
|
} else if ("load" in step) {
|
|
400
387
|
await client.programLoad({ path: subject.program });
|
|
401
388
|
} else if ("autostart" in step) {
|
|
402
|
-
await client
|
|
403
|
-
} else if ("
|
|
404
|
-
await
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
await client.joystick({ port: step.joystick.port, direction: step.joystick.direction, fire: step.joystick.fire ?? false });
|
|
413
|
-
} else if ("type" in step) {
|
|
414
|
-
await client.keyboard(textToPetscii(step.type));
|
|
415
|
-
} else if ("frames" in step) {
|
|
416
|
-
await stop();
|
|
417
|
-
await client.execution({ action: "advance-frames", count: step.frames, space: "c64" });
|
|
418
|
-
} else if ("runUntil" in step) {
|
|
419
|
-
const target =
|
|
420
|
-
"at" in step.runUntil
|
|
421
|
-
? { kind: "address" as const, address: at(step.runUntil.at), space: "c64" as const }
|
|
422
|
-
: { kind: "memory" as const, address: at(step.runUntil.memory.at), operator: "eq" as const, value: step.runUntil.memory.equals, space: "c64" as const, view: "cpu" as const };
|
|
423
|
-
const reached = await client.runUntil({ target, timeoutFrames: step.timeoutFrames });
|
|
424
|
-
if (!reached.reached) {
|
|
425
|
-
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.timeoutFrames} frames (it stopped by ${reached.stopReason} at ${formatC64Address(reached.pc)})`);
|
|
389
|
+
await startProgram(client, subject.program, side);
|
|
390
|
+
} else if ("snapshot" in step) {
|
|
391
|
+
await client.snapshotRestore(readProjectFile(step.snapshot).bytes);
|
|
392
|
+
} else if ("normalize" in step) {
|
|
393
|
+
await normalize(client, step.normalize);
|
|
394
|
+
} else {
|
|
395
|
+
const { side: _side, ...action } = step;
|
|
396
|
+
const notReached = await runActionStep(client, action as ActionStep, at);
|
|
397
|
+
if (notReached !== undefined && "runUntil" in step) {
|
|
398
|
+
throw new Inconclusive(`the ${side} side did not reach the checkpoint before ${step.timeoutFrames} frames (it stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)})`);
|
|
426
399
|
}
|
|
427
400
|
}
|
|
428
401
|
}
|
|
@@ -436,7 +409,8 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
436
409
|
const right = rebuild.get(checkpoint)!;
|
|
437
410
|
left.memory.forEach((item, index) => {
|
|
438
411
|
const other = right.memory[index]!;
|
|
439
|
-
|
|
412
|
+
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
413
|
+
if (item.bytes.some((byte, at) => !skipped.has(at) && Math.abs(byte - other.bytes[at]!) > item.tolerance)) {
|
|
440
414
|
const show = (bytes: number[]) => (bytes.length === 1 ? bytes[0] : Buffer.from(bytes).toString("hex"));
|
|
441
415
|
differences.push({ checkpoint, what: `memory ${item.label}`, original: show(item.bytes), rebuild: show(other.bytes) });
|
|
442
416
|
}
|
|
@@ -453,13 +427,46 @@ export function compare(original: Map<string, Observation>, rebuild: Map<string,
|
|
|
453
427
|
return differences;
|
|
454
428
|
}
|
|
455
429
|
|
|
456
|
-
/**
|
|
457
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Writes the files of a FAIL into `out` (D46): for a screen difference both
|
|
432
|
+
* screens and the difference image, and memory.json with each differing byte.
|
|
433
|
+
*/
|
|
434
|
+
function writeFailFiles(out: string, original: Map<string, Observation>, rebuild: Map<string, Observation>, differences: Difference[]): string[] {
|
|
435
|
+
const files: string[] = [];
|
|
436
|
+
const write = (name: string, bytes: Uint8Array) => {
|
|
437
|
+
writeProjectFile(`${out}/${name}`, bytes, { what: "--out" });
|
|
438
|
+
files.push(name);
|
|
439
|
+
};
|
|
440
|
+
const memory: Array<{ checkpoint: string; what: string; bytes: Array<{ address: string; original: number; rebuild: number }> }> = [];
|
|
441
|
+
for (const difference of differences) {
|
|
442
|
+
const left = original.get(difference.checkpoint)!;
|
|
443
|
+
const right = rebuild.get(difference.checkpoint)!;
|
|
444
|
+
const name = difference.checkpoint.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
445
|
+
if (difference.what === "screen") {
|
|
446
|
+
if (left.png !== undefined) write(`${name}-original.png`, Buffer.from(left.png, "base64"));
|
|
447
|
+
if (right.png !== undefined) write(`${name}-rebuild.png`, Buffer.from(right.png, "base64"));
|
|
448
|
+
if (right.diffPng !== undefined) write(`${name}-diff.png`, Buffer.from(right.diffPng, "base64"));
|
|
449
|
+
}
|
|
450
|
+
if (difference.what.startsWith("memory ")) {
|
|
451
|
+
const index = left.memory.findIndex((item) => `memory ${item.label}` === difference.what);
|
|
452
|
+
const item = left.memory[index]!;
|
|
453
|
+
const other = right.memory[index]!;
|
|
454
|
+
const skipped = new Set(item.excluded.map((excluded) => excluded.offset));
|
|
455
|
+
const bytes = item.bytes.flatMap((byte, offset) => (!skipped.has(offset) && Math.abs(byte - other.bytes[offset]!) > item.tolerance ? [{ address: formatC64Address(item.address + offset), original: byte, rebuild: other.bytes[offset]! }] : []));
|
|
456
|
+
memory.push({ checkpoint: difference.checkpoint, what: difference.what, bytes });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (memory.length > 0) write("memory.json", Buffer.from(`${JSON.stringify(memory, null, 2)}\n`));
|
|
460
|
+
return files;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Runs the scenario on the original, then on the rebuild, in one VICE session. With out, a FAIL leaves files there. */
|
|
464
|
+
export async function runScenario(client: ViceSessionClient, scenario: Scenario, options: { out?: string } = {}): Promise<ScenarioResult> {
|
|
458
465
|
const checkpoints = scenario.steps.flatMap((step) => ("observe" in step ? [step.observe] : []));
|
|
459
466
|
const observed = new Map<Side, Map<string, Observation>>();
|
|
460
467
|
for (const side of SIDES) {
|
|
461
468
|
try {
|
|
462
|
-
observed.set(side, await runSide(client, scenario, side, loadSymbols(scenario[side])));
|
|
469
|
+
observed.set(side, await runSide(client, scenario, side, loadSymbols(scenario[side]), options.out !== undefined));
|
|
463
470
|
} catch (error) {
|
|
464
471
|
if (error instanceof Inconclusive) return { result: "INCONCLUSIVE", scenario: scenario.name, reason: error.message, checkpoints, differences: [] };
|
|
465
472
|
if (error instanceof WireFailure) {
|
|
@@ -468,6 +475,17 @@ export async function runScenario(client: ViceSessionClient, scenario: Scenario)
|
|
|
468
475
|
throw error;
|
|
469
476
|
}
|
|
470
477
|
}
|
|
471
|
-
const
|
|
472
|
-
|
|
478
|
+
const original = observed.get("original")!;
|
|
479
|
+
const rebuild = observed.get("rebuild")!;
|
|
480
|
+
const differences = compare(original, rebuild);
|
|
481
|
+
const excluded = [...original].flatMap(([checkpoint, observation]) => observation.memory.flatMap((item) => item.excluded.map((excluded) => ({ checkpoint, at: excluded.at, reason: excluded.reason }))));
|
|
482
|
+
const files = differences.length > 0 && options.out !== undefined ? writeFailFiles(options.out, original, rebuild, differences) : [];
|
|
483
|
+
return {
|
|
484
|
+
result: differences.length === 0 ? "PASS" : "FAIL",
|
|
485
|
+
scenario: scenario.name,
|
|
486
|
+
checkpoints,
|
|
487
|
+
differences,
|
|
488
|
+
...(excluded.length === 0 ? {} : { excluded }),
|
|
489
|
+
...(files.length === 0 ? {} : { files }),
|
|
490
|
+
};
|
|
473
491
|
}
|
|
@@ -6,22 +6,32 @@ import { parseArgs } from "node:util";
|
|
|
6
6
|
|
|
7
7
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
8
8
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
9
|
+
import { resolveProjectPath } from "#src/project.ts";
|
|
9
10
|
import { runScript, UsageError } from "#src/script.ts";
|
|
10
11
|
import { readScenario, runScenario, ScenarioError, type ScenarioResult } from "./scenario.ts";
|
|
11
12
|
|
|
12
|
-
const USAGE = `test.ts <scenario.json>
|
|
13
|
+
const USAGE = `test.ts <scenario.json> [--out <dir>]
|
|
13
14
|
|
|
14
15
|
<scenario.json> is a scenario file relative to the project directory. See
|
|
15
|
-
references/scenario-format.md.
|
|
16
|
+
references/scenario-format.md. With --out, a FAIL writes both screens, the
|
|
17
|
+
difference image and memory.json with each differing byte into <dir>. The
|
|
18
|
+
result is one JSON object.`;
|
|
16
19
|
|
|
17
20
|
const EXIT = { PASS: 0, FAIL: 1, INCONCLUSIVE: 3 } as const;
|
|
18
21
|
|
|
19
22
|
async function run(argv: string[]): Promise<ScenarioResult> {
|
|
20
|
-
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { help: { type: "boolean" } } });
|
|
23
|
+
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { out: { type: "string" }, help: { type: "boolean" } } });
|
|
21
24
|
if (values.help) throw new UsageError("");
|
|
22
25
|
if (positionals.length !== 1) throw new UsageError("give one scenario file");
|
|
26
|
+
if (values.out !== undefined) {
|
|
27
|
+
try {
|
|
28
|
+
resolveProjectPath(values.out);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
23
33
|
try {
|
|
24
|
-
return await runScenarioFile(positionals[0]
|
|
34
|
+
return await runScenarioFile(positionals[0]!, values.out);
|
|
25
35
|
} catch (error) {
|
|
26
36
|
// A scenario or symbol file that is not usable is a bad call.
|
|
27
37
|
if (error instanceof ScenarioError) throw new UsageError(error.message);
|
|
@@ -29,7 +39,7 @@ async function run(argv: string[]): Promise<ScenarioResult> {
|
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
async function runScenarioFile(path: string): Promise<ScenarioResult> {
|
|
42
|
+
async function runScenarioFile(path: string, out: string | undefined): Promise<ScenarioResult> {
|
|
33
43
|
const scenario = readScenario(path);
|
|
34
44
|
let client: ViceSessionClient;
|
|
35
45
|
try {
|
|
@@ -39,7 +49,7 @@ async function runScenarioFile(path: string): Promise<ScenarioResult> {
|
|
|
39
49
|
return { result: "INCONCLUSIVE", scenario: scenario.name, reason: `no emulator: ${error.message}`, checkpoints: [], differences: [] };
|
|
40
50
|
}
|
|
41
51
|
try {
|
|
42
|
-
return await runScenario(client, scenario);
|
|
52
|
+
return await runScenario(client, scenario, out === undefined ? {} : { out });
|
|
43
53
|
} finally {
|
|
44
54
|
await client.close();
|
|
45
55
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: c64-unpacker
|
|
3
|
-
description: Use this skill to find out if a packer or a cruncher compressed a C64 program, and to get the unpacked program for analysis. It shows evidence of packing without a guess of the packer name. It runs the program in an emulator until the unpacked code starts, then writes the memory into the project. Do not use it to analyze the unpacked code.
|
|
3
|
+
description: Use this skill to find out if a packer or a cruncher compressed a C64 program, and to get the unpacked program for analysis. It shows evidence of packing without a guess of the packer name. It runs the program in an emulator until the unpacked code starts, then writes the memory into the project. It also captures each part of a multi-load program and the drive RAM. Do not use it to analyze the unpacked code.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# c64-unpacker
|
|
@@ -14,17 +14,19 @@ This skill does not change project knowledge.
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
16
16
|
- The program: a PRG file, relative to the project directory.
|
|
17
|
-
- For a capture: the address where the unpacked program starts, and
|
|
17
|
+
- For a capture: the address where the unpacked program starts, and the directory for the result.
|
|
18
18
|
|
|
19
19
|
## Tools / execution path
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
node <skill>/scripts/unpack.ts inspect original/game.prg
|
|
23
23
|
node <skill>/scripts/unpack.ts trace original/game.prg
|
|
24
|
-
node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out unpacked
|
|
24
|
+
node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out unpacked
|
|
25
|
+
node <skill>/scripts/unpack.ts stages original/game.prg --steps input.json
|
|
26
|
+
node <skill>/scripts/unpack.ts loads original/game.d64 --steps parts.json --out parts --drive
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
`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.
|
|
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 counts as run time, and a `runUntil` step is not possible.
|
|
28
30
|
|
|
29
31
|
| Option | Meaning |
|
|
30
32
|
|---|---|
|
|
@@ -34,25 +36,71 @@ node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out u
|
|
|
34
36
|
| `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
|
|
35
37
|
| `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
|
|
36
38
|
| `--entry '$c000'` | `trace` only: start the program at this address. Without it, `trace` types RUN. A program without a BASIC start line 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. |
|
|
40
|
+
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
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
|
+
| `--min-stage 64` | `stages` only: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
37
43
|
|
|
38
44
|
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object.
|
|
39
45
|
|
|
46
|
+
A steps file has the steps `write`, `registers`, `joystick`, `type`, `frames` and `runUntil` of the c64-testing skill. Addresses are `$` and four hex digits. For example, wait 50 frames and press the space bar:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
[{ "frames": 50 }, { "type": " " }]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A `type` step puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware does not see them. A step cannot press one specific key of the keyboard matrix. A wait for any key that reads all keyboard columns can end without a key.
|
|
53
|
+
|
|
40
54
|
## Workflow
|
|
41
55
|
|
|
42
56
|
1. Run `inspect`. Read `basicStart` and `packing`:
|
|
43
57
|
- `likely`: most of the bytes look compressed. Packed data has this property.
|
|
44
58
|
- `unlikely`: the bytes look like code and data. A simple cruncher can also give this result.
|
|
45
|
-
- `unclear`: the file is short, or the blocks do not give clear evidence.
|
|
59
|
+
- `unclear`: the file is short, or the blocks do not give clear evidence. Or the code at the SYS address decodes or moves bytes outside the program.
|
|
60
|
+
|
|
61
|
+
Also read the evidence that the script finds in the file. The script never names a packer from it:
|
|
62
|
+
- `basicText`: the BASIC lines as text. A packer or a crack often puts its name there.
|
|
63
|
+
- `strings`: readable text near the start of the code.
|
|
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
|
+
- `decodeLoops`: EOR instructions in loops that store with an index. This is usually decryption or decoding.
|
|
46
66
|
2. Run `trace`. It gives the evidence from the run. Read `packing`:
|
|
47
67
|
- `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the code that runs now. Its `start` is usually the entry point of the unpacked program.
|
|
48
68
|
- `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
|
|
49
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.
|
|
50
70
|
3. Find the address where the unpacked program starts:
|
|
51
|
-
-
|
|
71
|
+
- Run `stages`. The `entry` of the last stage is usually the start of the program.
|
|
72
|
+
- Or use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
|
|
52
73
|
- Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
|
|
53
|
-
4. Run `capture` with that address
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
75
|
+
- `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. Blocks with the code of an earlier stage are not segments. `writtenBy` tells which stage wrote the segment.
|
|
77
|
+
- 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
|
+
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
|
+
6. Give `image.bin` to the c64-static-analysis skill with its `--flat64k` option, or give it the segment PRGs.
|
|
81
|
+
7. Use `--range` only when you know the area of the program. Then the capture writes that one segment.
|
|
82
|
+
|
|
83
|
+
## Multi-load programs
|
|
84
|
+
|
|
85
|
+
A multi-load program loads more parts from the disk while it runs, for example one part for each level. Use `loads` to capture each part. The program can be a PRG, or a disk, tape or cartridge image that VICE starts.
|
|
86
|
+
|
|
87
|
+
1. Write a steps file. Put a `capture` step after each point where a part is in memory, for example:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
[
|
|
91
|
+
{ "runUntil": { "at": "$080d" }, "timeoutFrames": 6000 },
|
|
92
|
+
{ "capture": "main" },
|
|
93
|
+
{ "type": " " },
|
|
94
|
+
{ "runUntil": { "at": "$c000" }, "timeoutFrames": 6000 },
|
|
95
|
+
{ "capture": "level1" }
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. Run `loads` with the steps file and a directory in `--out`. Add `--drive` when the program has its own drive code, for example a fast loader.
|
|
100
|
+
3. Each capture writes `<out>/<name>/` with `image.bin`, `image.json`, `entry_state.json` and a PRG for each segment. A segment is a block of RAM from `$0400` that the program changed after the capture before. The first capture also has the program itself.
|
|
101
|
+
4. With `--drive`, `drive8.prg` is the drive RAM at `$0000`-`$07ff`. Analyze it with c64-static-analysis and `--space drive8`, and record its knowledge in space `drive8`.
|
|
102
|
+
|
|
103
|
+
VICE records no memory access map for the drive CPU. Thus no tool gives coverage of drive code.
|
|
56
104
|
|
|
57
105
|
## Knowledge
|
|
58
106
|
|
|
@@ -63,18 +111,21 @@ Do not record the unpacker in the project knowledge, unless the user asks to exa
|
|
|
63
111
|
|
|
64
112
|
## Result
|
|
65
113
|
|
|
66
|
-
The packing evidence, or the unpacked program
|
|
114
|
+
The packing evidence, the stages, or the unpacked program with its image, segments and entry state in a directory of the project.
|
|
115
|
+
|
|
116
|
+
For `loads`: `captures`, with the name, the PC, the segments and the drive file of each capture.
|
|
67
117
|
|
|
68
|
-
The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false` and for an error. The exit status is 2 for a wrong argument.
|
|
118
|
+
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.
|
|
69
119
|
|
|
70
120
|
## Failure and conflicts
|
|
71
121
|
|
|
72
122
|
- `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.
|
|
73
|
-
- `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.
|
|
123
|
+
- `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
|
+
- A step did not reach its target: the reason names the step. Change the step or give it more frames.
|
|
74
125
|
- Do not name a packer from weak evidence. The script never names one.
|
|
75
|
-
- Some programs unpack in more than one stage. Make sure that
|
|
126
|
+
- 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.
|
|
76
127
|
- `not-found` or `invalid-input`: a path or an argument is wrong. Read the message.
|
|
77
|
-
- `
|
|
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.
|
|
78
129
|
|
|
79
130
|
## Handoffs
|
|
80
131
|
|