@henols/c64-re-tools 1.0.0 → 2.0.0-rc.2
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/LICENSE +21 -0
- package/README.md +190 -37
- package/distribution/package.ts +36 -0
- package/distribution/plugin.ts +120 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +73 -0
- package/skills/c64-assembler/scripts/assemble.ts +103 -0
- package/skills/c64-basic/SKILL.md +67 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +79 -0
- package/skills/c64-disk/scripts/disk.ts +65 -0
- package/skills/c64-emulator/SKILL.md +124 -0
- package/skills/c64-knowledge/SKILL.md +102 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
- package/skills/c64-memory-map/SKILL.md +76 -0
- package/skills/c64-memory-map/scripts/memmap.ts +81 -0
- package/skills/c64-memory-map/scripts/platform.ts +424 -0
- package/skills/c64-provenance/SKILL.md +76 -0
- package/skills/c64-provenance/scripts/compare.ts +92 -0
- package/skills/c64-provenance/scripts/provenance.ts +83 -0
- package/skills/c64-reverse-engineering/SKILL.md +90 -0
- package/skills/c64-static-analysis/SKILL.md +96 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
- package/skills/c64-static-analysis/scripts/findings.ts +73 -0
- package/skills/c64-testing/SKILL.md +79 -0
- package/skills/c64-testing/references/scenario-format.md +76 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/scenario.ts +473 -0
- package/skills/c64-testing/scripts/test.ts +55 -0
- package/skills/c64-unpacker/SKILL.md +82 -0
- package/skills/c64-unpacker/scripts/evidence.ts +135 -0
- package/skills/c64-unpacker/scripts/unpack.ts +216 -0
- package/src/c64.ts +281 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +122 -0
- package/src/cli/status.ts +34 -0
- package/src/host/main.ts +121 -0
- package/src/host/server.ts +409 -0
- package/src/host/tools/c1541.ts +255 -0
- package/src/host/tools/index.ts +31 -0
- package/src/host/tools/petcat.ts +70 -0
- package/src/host/tools/status.ts +30 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +761 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +108 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +402 -0
- package/src/host/vice/screen.ts +183 -0
- package/src/host/vice/session.ts +1316 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +100 -0
- package/src/host-client/vice-session.ts +254 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +128 -0
- package/src/knowledge/import.ts +344 -0
- package/src/knowledge/read.ts +191 -0
- package/src/knowledge/schema.ts +140 -0
- package/src/knowledge/write.ts +348 -0
- package/src/mcp/main.ts +109 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/tools/debug.ts +237 -0
- package/src/mcp/tools/execution.ts +116 -0
- package/src/mcp/tools/input.ts +69 -0
- package/src/mcp/tools/machine.ts +97 -0
- package/src/mcp/tools/media.ts +106 -0
- package/src/mcp/tools/memory.ts +203 -0
- package/src/mcp/tools/video.ts +260 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +141 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +157 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +242 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +101 -0
- package/src/native/types.ts +228 -0
- package/src/native/watchdog.ts +78 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +252 -0
- package/src/protocol/tools.ts +247 -0
- package/src/protocol/vice.ts +1235 -0
- package/src/script.ts +59 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +162 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Which `mcp__plugin_c64-re-tools_vice__*` call answers which question
|
|
2
|
-
|
|
3
|
-
**This is not a restatement of the tool surface.** You already hold typed schemas for every
|
|
4
|
-
call. Read parameters off those. What the schemas cannot tell you is *which call to reach for
|
|
5
|
-
first*, and that ordering is the whole value here.
|
|
6
|
-
|
|
7
|
-
Curated 2026-08-01, doc-derived, **Confidence: MEDIUM** — the mapping is reasoned from the tool surface and this project's own
|
|
8
|
-
usage, not measured). Individual rows that have since been exercised live are marked.
|
|
9
|
-
|
|
10
|
-
| Question | Call |
|
|
11
|
-
|---|---|
|
|
12
|
-
| Vectors, `$01`, `$D011`/`$D012`/`$D018`/`$D019`/`$D01A`, `$DC0D`/`$DD0D`, `$DD00` | `vice_memory_read` — highest-value first move. Answers most vector questions in one or two calls |
|
|
13
|
-
| What does the handler at this vector do? | `vice_disassemble` — the emulator's own decoder, not a dead listing |
|
|
14
|
-
| Is this really the main loop? | `vice_checkpoint_add` + `vice_run_until` + `vice_registers_get` — fires once per frame ⇒ proven |
|
|
15
|
-
| What code writes this? | `vice_watch_add` — finds **writers**. Best targets: `$D018`, VM+`$03F8`, `$D404` |
|
|
16
|
-
| Whole-chip VIC-II/CIA state without the read hazards | `vice_vicii_get_state` / `vice_cia_get_state` — prefer these over raw register reads |
|
|
17
|
-
| Whole-chip SID state without the read hazards | `vice_sid_get_state` — **permanently unavailable**: SID `$D400-$D418` is write-only in hardware and the binary monitor has no SID command, so read-back cannot be recovered. Writes to those addresses still work fine over the memory-set primitive. See `docs/stock-hard-losses.md` |
|
|
18
|
-
| Decode sprite data | `vice_sprite_get` / `vice_sprite_inspect` |
|
|
19
|
-
| Find a known byte pattern | `vice_memory_search` |
|
|
20
|
-
| Carry labels across sessions | `vice_symbols_load` / `vice_symbols_lookup` — ACME `--vicelabels` emits the format they consume. The annotation store's own export into that format is **withdrawn as of 2026-08-29, and no phase currently owns its return**. An earlier forecast named a numbered phase for it, and that forecast is superseded now |
|
|
21
|
-
| Is the machine wedged, or did it stop itself? | `vice_diagnose` — five-state verdict with its evidence (one state is `monitor_held_elsewhere`, because this monitor serves exactly one client at a time). **Reachable and proxy-intercepted as of 2026-08-04** (verified live). Triage tree: `vice-wedge-triage` |
|
|
22
|
-
| Replace a wedged instance | `vice_recycle` — destructive, requires a `reason`, and that reason is written into `.c64-re-tools/incidents/` **before** anything is killed. The reason *is* the evidence record |
|
|
23
|
-
| Read the restart epoch | **No tool does.** The proxy compares it around every forwarded call and raises drift itself. A value comes from that error or from `vice_diagnose` |
|
|
24
|
-
|
|
25
|
-
## Delegate rather than restate
|
|
26
|
-
|
|
27
|
-
| Question | Go to |
|
|
28
|
-
|---|---|
|
|
29
|
-
| What does address X mean? | the `c64-memory-mapping` skill — `node … lookup '$D018'`. **Do not restate its tables.** |
|
|
30
|
-
| Is this byte original or cracker-patched? | the `c64-provenance-diff` skill |
|
|
31
|
-
| A verified 64K image, or comparing two captures | the `c64-ram-capture` skill |
|
|
32
|
-
| Whole-program static disassembly with code/data separation | **`anno export-asm`** — withdrawn 2026-08-29, returned 2026-08-31. It settles correctness by assembling the output with a real ACME and diffing the bytes against the input. That oracle is test-only, so the verb itself writes source and runs no assembler. For a single routine, read one explicit range at a time with `anno_read_region` / `anno_disassemble` (4096-byte cap per call, refused rather than truncated above it). Record what you checked with `anno_set_data_type` |
|
|
33
|
-
|
|
34
|
-
## Runtime evidence versus the byte-derived guess
|
|
35
|
-
|
|
36
|
-
| Question | Call |
|
|
37
|
-
|---|---|
|
|
38
|
-
| Where the store's byte-derived block table (`anno_set_data_type`'s own ranges) disagrees with what the emulator was actually observed executing | **`anno evid-disagreements`** (also `anno_evid_disagreements`) — joins the typed ranges against the runtime-observed rows an `anno_evid_ingest` call already wrote. It reports disagreements first, agreement as a count only, and a never-observed count. A never-observed count is explicitly NOT evidence the address holds data |
|
|
39
|
-
| What evidence a store already holds, without re-running the program | `anno_evid_runs` — every run identity's observation count beside its denominator |
|
|
40
|
-
| Reset one run identity's evidence for a fresh re-measurement | `anno_evid_reset` — clears only that run identity's rows. Pair it with `vice_memmap_zap` on the emulator side |
|
|
41
|
-
|
|
42
|
-
## What blocks this program's code from being moved
|
|
43
|
-
|
|
44
|
-
| Question | Call |
|
|
45
|
-
|---|---|
|
|
46
|
-
| Which constructions block relocating, rebasing or stripping part of this program | **`anno hazard-report`** (also `anno_hazard_report`) — enumerates movement-hazard findings derived from decoded bytes alone. For example, a store or read-modify-write instruction may land its literal target on another instruction's opcode or operand byte. A later pass may then run different code or read a different value there. Each finding carries its own detection mechanism and a detection-strength token. It reports and touches NOTHING. It never relocates, strips or rebases anything, and it never emits a flag a caller could act on as an automatic relocation |
|
|
47
|
-
|
|
48
|
-
**A region this report does not flag is undecided or unflagged, never certified safe to move.** Every checked
|
|
49
|
-
region reports one of exactly three outcomes, and only one of them means a construction was actually found
|
|
50
|
-
there — the other two both mean "nothing this report knows how to look for fired here," which is a fact about
|
|
51
|
-
the detectors, not a guarantee about the bytes. A store through a runtime-computed pointer into the code range
|
|
52
|
-
is a known, named miss: this report cannot see it, and its absence from the findings is not evidence that no
|
|
53
|
-
such construction exists. Use this alongside, never instead of, the runtime-evidence and provenance-diff
|
|
54
|
-
routes above and in the sibling skills — a region with no finding still needs a human decision before it is
|
|
55
|
-
moved.
|
|
56
|
-
|
|
57
|
-
## Three traps in this table
|
|
58
|
-
|
|
59
|
-
**`vice_run_until`'s `timeout_ms` bounds the wait.** `timeout_ms` (default 30000, ceiling 600000)
|
|
60
|
-
bounds a run to an address the program never reaches, and a timed-out answer says the machine is
|
|
61
|
-
left stopped rather than looking like a wedged emulator. Prefer `vice_checkpoint_add` + a bounded
|
|
62
|
-
poll when the address is a hypothesis rather than a certainty — see `vice-wedge-triage` for the
|
|
63
|
-
full triage judgement and its live evidence. Do not restate it here.
|
|
64
|
-
|
|
65
|
-
**`vice_diagnose` leaves the machine paused.** When it measures a cycle bracket it resumes the
|
|
66
|
-
machine once or twice and then leaves it **paused** — resuming is your own next call. And a
|
|
67
|
-
`checkpoint_trap` verdict means the machine stopped *itself* at an armed checkpoint: it must
|
|
68
|
-
**not** be recycled, because recycling a self-inflicted stop destroys a healthy instance.
|
|
69
|
-
|
|
70
|
-
**Prefer the `*_get_state` calls, but know they pause.** They avoid the raw-register read
|
|
71
|
-
hazards, but most state reads pause the emulator. Read everything first, poll with `vice_ping`,
|
|
72
|
-
and resume exactly once at the end. See `observation-hazards.md`.
|
|
73
|
-
|
|
74
|
-
## The standing constraint
|
|
75
|
-
|
|
76
|
-
`mcp__plugin_c64-re-tools_vice__*` is the only route to the emulator. Any step of a method that would want a Node
|
|
77
|
-
process talking to VICE is dead on arrival and must be expressed as agent-performed tool calls —
|
|
78
|
-
the same rule that reduced `c64-ram-capture` to a procedure and deleted its original `scripts/`.
|
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
|
|
4
|
-
// Pure derivation over values the agent already fetched. Contacts nothing.
|
|
5
|
-
//
|
|
6
|
-
// The mcp__plugin_c64-re-tools_vice__* tools are the only route to the emulator (.claude/CLAUDE.md
|
|
7
|
-
// § Emulator Access). This script therefore takes register values as arguments
|
|
8
|
-
// and RAM as a file, and performs only the arithmetic that a lookup table
|
|
9
|
-
// cannot: register bits -> concrete addresses.
|
|
10
|
-
|
|
11
|
-
const HEX = /^(?:\$|0x)?([0-9a-f]+)h?$/i;
|
|
12
|
-
|
|
13
|
-
function parseNum(s, what) {
|
|
14
|
-
if (s === undefined || s === null) return undefined;
|
|
15
|
-
const t = String(s).trim();
|
|
16
|
-
if (/^%[01]+$/.test(t)) return parseInt(t.slice(1), 2);
|
|
17
|
-
const m = HEX.exec(t);
|
|
18
|
-
// A bare decimal is only decimal when it cannot be hex; callers copy values
|
|
19
|
-
// out of register dumps, which are hex. Require an explicit marker for decimal.
|
|
20
|
-
if (/^\d+$/.test(t) && !/^\$|^0x/i.test(t)) return parseInt(t, 16);
|
|
21
|
-
if (m) return parseInt(m[1], 16);
|
|
22
|
-
throw new Error(`cannot parse ${what}: ${s}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const hex = (n, w = 4) => '$' + n.toString(16).toUpperCase().padStart(w, '0');
|
|
26
|
-
const bin8 = (n) => '%' + n.toString(2).padStart(8, '0');
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------- VIC banking
|
|
29
|
-
|
|
30
|
-
// $DD00 bits 0-1 select the bank, INVERTED. The single most common source of a
|
|
31
|
-
// wrong answer in C64 graphics RE: every other pointer hangs off this base.
|
|
32
|
-
function bankOf(dd00) {
|
|
33
|
-
const sel = dd00 & 3;
|
|
34
|
-
const bank = 3 - sel;
|
|
35
|
-
return { bank, base: bank * 0x4000, sel };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// The VIC sees character ROM at $1000-$1FFF of banks 0 and 2, regardless of the
|
|
39
|
-
// $01 banking the CPU sees. If CB lands there, no charset exists in RAM.
|
|
40
|
-
function charRomShadow(bank, cb) {
|
|
41
|
-
return (bank === 0 || bank === 2) && (cb === 2 || cb === 3);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function modeOf(d011, d016) {
|
|
45
|
-
const ecm = (d011 >> 6) & 1, bmm = (d011 >> 5) & 1, mcm = (d016 >> 4) & 1;
|
|
46
|
-
const names = {
|
|
47
|
-
'000': 'standard text', '001': 'multicolor text',
|
|
48
|
-
'010': 'standard bitmap', '011': 'multicolor bitmap',
|
|
49
|
-
'100': 'extended background text',
|
|
50
|
-
};
|
|
51
|
-
const key = `${ecm}${bmm}${mcm}`;
|
|
52
|
-
return { ecm, bmm, mcm, name: names[key] ?? 'INVALID — screen goes black', key };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function vic({ dd00, d018, d011, d016 }) {
|
|
56
|
-
const { bank, base, sel } = bankOf(dd00);
|
|
57
|
-
const vm = (d018 >> 4) & 0x0f;
|
|
58
|
-
const cb = (d018 >> 1) & 0x07;
|
|
59
|
-
const mode = modeOf(d011, d016);
|
|
60
|
-
const screen = base + vm * 0x0400;
|
|
61
|
-
const out = [];
|
|
62
|
-
|
|
63
|
-
out.push(`$DD00 = ${hex(dd00, 2)} ${bin8(dd00)}`);
|
|
64
|
-
out.push(` bits 0-1 = %${sel.toString(2).padStart(2, '0')} (inverted) -> VIC bank ${bank}, base ${hex(base)}`);
|
|
65
|
-
out.push('');
|
|
66
|
-
out.push(`$D018 = ${hex(d018, 2)} ${bin8(d018)}`);
|
|
67
|
-
out.push(` VM bits 4-7 = ${vm.toString().padStart(2)} -> screen RAM ${hex(screen)}-${hex(screen + 0x3e7)}`);
|
|
68
|
-
|
|
69
|
-
if (mode.bmm) {
|
|
70
|
-
// In bitmap mode only bit 3 of $D018 matters: which 8K half of the bank.
|
|
71
|
-
const half = (cb & 4) ? 0x2000 : 0x0000;
|
|
72
|
-
const bmp = base + half;
|
|
73
|
-
out.push(` CB bit 3 = ${(cb & 4) ? 1 : 0} -> bitmap ${hex(bmp)}-${hex(bmp + 0x1f3f)} (8000 bytes)`);
|
|
74
|
-
out.push('');
|
|
75
|
-
out.push(`mode: ${mode.name} (ECM=${mode.ecm} BMM=${mode.bmm} MCM=${mode.mcm})`);
|
|
76
|
-
out.push(` video matrix at ${hex(screen)} holds COLOUR PAIRS, not character codes`);
|
|
77
|
-
} else {
|
|
78
|
-
const chr = base + cb * 0x0800;
|
|
79
|
-
if (charRomShadow(bank, cb)) {
|
|
80
|
-
out.push(` CB bits 1-3 = ${cb} -> character ROM SHADOW at ${hex(chr)}`);
|
|
81
|
-
out.push('');
|
|
82
|
-
out.push(' *** CHARACTER ROM, NOT RAM ***');
|
|
83
|
-
out.push(' The VIC sees char ROM at $1000-$1FFF in banks 0 and 2 whatever $01 says.');
|
|
84
|
-
out.push(' This game uses ROM characters here. There is no charset in RAM to extract.');
|
|
85
|
-
} else {
|
|
86
|
-
out.push(` CB bits 1-3 = ${cb} -> charset ${hex(chr)}-${hex(chr + 0x7ff)} (256 chars)`);
|
|
87
|
-
}
|
|
88
|
-
out.push('');
|
|
89
|
-
out.push(`mode: ${mode.name} (ECM=${mode.ecm} BMM=${mode.bmm} MCM=${mode.mcm})`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (mode.name.startsWith('INVALID')) {
|
|
93
|
-
out.push(' This bit combination blanks the screen. Re-read the registers — you');
|
|
94
|
-
out.push(' probably caught them mid-update inside a raster split.');
|
|
95
|
-
}
|
|
96
|
-
if (mode.mcm) out.push(' multicolor: bit PAIRS, half horizontal resolution');
|
|
97
|
-
|
|
98
|
-
out.push('');
|
|
99
|
-
out.push(`sprite pointers: ${hex(screen + 0x3f8)}-${hex(screen + 0x3ff)} (screen + $03F8, 8 bytes)`);
|
|
100
|
-
out.push(`colour RAM: $D800-$DBFF (fixed; does NOT move with the VIC bank, low nybble only)`);
|
|
101
|
-
return out.join('\n');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------- sprites
|
|
105
|
-
|
|
106
|
-
function sprites({ dd00, d018, d015, ptrs }) {
|
|
107
|
-
const { bank, base } = bankOf(dd00);
|
|
108
|
-
const vm = (d018 >> 4) & 0x0f;
|
|
109
|
-
const screen = base + vm * 0x0400;
|
|
110
|
-
const out = [];
|
|
111
|
-
out.push(`VIC bank ${bank} (${hex(base)}), screen ${hex(screen)}, pointer block ${hex(screen + 0x3f8)}`);
|
|
112
|
-
out.push(`$D015 = ${hex(d015, 2)} ${bin8(d015)}`);
|
|
113
|
-
out.push('');
|
|
114
|
-
out.push('spr enabled ptr data address note');
|
|
115
|
-
for (let i = 0; i < 8; i++) {
|
|
116
|
-
const on = (d015 >> i) & 1;
|
|
117
|
-
const p = ptrs[i];
|
|
118
|
-
const addr = p === undefined ? undefined : base + p * 64;
|
|
119
|
-
const note = on ? '' : 'DISABLED — other registers are stale, do not decode';
|
|
120
|
-
out.push(
|
|
121
|
-
` ${i} ${on ? 'yes' : 'no '} ` +
|
|
122
|
-
`${p === undefined ? ' -- ' : hex(p, 2).padEnd(4)} ` +
|
|
123
|
-
`${addr === undefined ? ' --------- ' : `${hex(addr)}-${hex(addr + 62)}`} ${note}`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
out.push('');
|
|
127
|
-
out.push('63 bytes used of each 64-byte block. $D010 carries X bit 8 for X>255.');
|
|
128
|
-
return out.join('\n');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------- vectors
|
|
132
|
-
|
|
133
|
-
// Every indirection a C64 program can be sitting behind, grouped by block.
|
|
134
|
-
// Defaults are the stock KERNAL/BASIC values; `null` means the location is not
|
|
135
|
-
// a KERNAL-maintained vector, so "retargeted" is not a meaningful verdict there.
|
|
136
|
-
// `hook` marks a vector a cracker has a specific reason to divert — those are
|
|
137
|
-
// read as provenance signals, not merely as structure.
|
|
138
|
-
const VECTOR_BLOCKS = [
|
|
139
|
-
['BASIC indirects ($0300-$030B) — only meaningful with BASIC banked in', [
|
|
140
|
-
['$0300/$0301', 0x0300, 0xe38b, 'IERROR — print BASIC error message'],
|
|
141
|
-
['$0302/$0303', 0x0302, 0xa483, 'IMAIN — BASIC warm start / input loop'],
|
|
142
|
-
['$0304/$0305', 0x0304, 0xa57c, 'ICRNCH — tokenise a BASIC line'],
|
|
143
|
-
['$0306/$0307', 0x0306, 0xa71a, 'IQPLOP — list a tokenised line'],
|
|
144
|
-
['$0308/$0309', 0x0308, 0xa7e4, 'IGONE — execute next BASIC token'],
|
|
145
|
-
['$030A/$030B', 0x030a, 0xae86, 'IEVAL — evaluate an expression'],
|
|
146
|
-
]],
|
|
147
|
-
['KERNAL IRQ/BRK/NMI ($0314-$0319)', [
|
|
148
|
-
['$0314/$0315', 0x0314, 0xea31, 'CINV — KERNAL IRQ (RAM, indirect)'],
|
|
149
|
-
['$0316/$0317', 0x0316, 0xfe66, 'CBINV — BRK'],
|
|
150
|
-
['$0318/$0319', 0x0318, 0xfe47, 'NMINV — NMI (music players retarget this)'],
|
|
151
|
-
]],
|
|
152
|
-
['KERNAL I/O indirects ($031A-$0333)', [
|
|
153
|
-
['$031A/$031B', 0x031a, 0xf34a, 'IOPEN'],
|
|
154
|
-
['$031C/$031D', 0x031c, 0xf291, 'ICLOSE'],
|
|
155
|
-
['$031E/$031F', 0x031e, 0xf20e, 'ICHKIN'],
|
|
156
|
-
['$0320/$0321', 0x0320, 0xf250, 'ICKOUT'],
|
|
157
|
-
['$0322/$0323', 0x0322, 0xf333, 'ICLRCH'],
|
|
158
|
-
['$0324/$0325', 0x0324, 0xf157, 'IBASIN'],
|
|
159
|
-
['$0326/$0327', 0x0326, 0xf1ca, 'IBSOUT'],
|
|
160
|
-
['$0328/$0329', 0x0328, 0xf6ed, 'ISTOP — STOP key check', 'hook'],
|
|
161
|
-
['$032A/$032B', 0x032a, 0xf13e, 'IGETIN'],
|
|
162
|
-
['$032C/$032D', 0x032c, 0xf32f, 'ICLALL'],
|
|
163
|
-
['$032E/$032F', 0x032e, 0xfe66, 'USRCMD — unused by the KERNAL'],
|
|
164
|
-
['$0330/$0331', 0x0330, 0xf4a5, 'ILOAD — LOAD', 'hook'],
|
|
165
|
-
['$0332/$0333', 0x0332, 0xf5ed, 'ISAVE — SAVE', 'hook'],
|
|
166
|
-
]],
|
|
167
|
-
['Autostart / cartridge block ($8000-$8008)', [
|
|
168
|
-
['$8000/$8001', 0x8000, null, 'cartridge cold-start entry'],
|
|
169
|
-
['$8002/$8003', 0x8002, null, 'cartridge NMI entry'],
|
|
170
|
-
]],
|
|
171
|
-
['BASIC ROM entry ($A000-$A003) — check $01 LORAM first', [
|
|
172
|
-
['$A000/$A001', 0xa000, null, 'BASIC cold-start ($E394 in stock ROM)'],
|
|
173
|
-
['$A002/$A003', 0xa002, null, 'BASIC warm-start ($E37B in stock ROM)'],
|
|
174
|
-
]],
|
|
175
|
-
['Hardware vectors ($FFFA-$FFFF) — live when the KERNAL is banked out', [
|
|
176
|
-
['$FFFA/$FFFB', 0xfffa, null, 'hardware NMI'],
|
|
177
|
-
['$FFFC/$FFFD', 0xfffc, null, 'hardware RESET'],
|
|
178
|
-
['$FFFE/$FFFF', 0xfffe, null, 'hardware IRQ/BRK'],
|
|
179
|
-
]],
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
// $8004-$8008 holds "CBM80" in PETSCII when a cartridge/autostart block is
|
|
183
|
-
// present, and the KERNAL only honours $8000/$8002 when it does. Without the
|
|
184
|
-
// signature those two words are just whatever is in RAM there.
|
|
185
|
-
const CBM80 = [0xc3, 0xc2, 0xcd, 0x38, 0x30];
|
|
186
|
-
|
|
187
|
-
// A target inside a ROM window is ambiguous from a static image alone: the byte
|
|
188
|
-
// range it names is either ROM or the RAM underneath, decided by $01 at the
|
|
189
|
-
// moment the vector is taken. Resolving it needs two live reads (see below).
|
|
190
|
-
const ROM_WINDOWS = [
|
|
191
|
-
[0xa000, 0xbfff, 'BASIC ROM window'],
|
|
192
|
-
[0xd000, 0xdfff, 'I/O — or char ROM, or RAM'],
|
|
193
|
-
[0xe000, 0xffff, 'KERNAL ROM window'],
|
|
194
|
-
];
|
|
195
|
-
|
|
196
|
-
function romWindowOf(addr) {
|
|
197
|
-
for (const [lo, hi, label] of ROM_WINDOWS) if (addr >= lo && addr <= hi) return label;
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function decodePort(v) {
|
|
202
|
-
const loram = v & 1, hiram = (v >> 1) & 1, charen = (v >> 2) & 1;
|
|
203
|
-
return { loram, hiram, charen };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Blocks the CPU cannot currently be dispatching through, given $01. Their
|
|
207
|
-
// bytes are real, but nothing maintains them, so "retargeted" says nothing.
|
|
208
|
-
function dormantReason(title, { loram, hiram }, cbm80) {
|
|
209
|
-
if (title.startsWith('BASIC indirects') && !loram) return 'BASIC ROM banked out — nothing maintains these';
|
|
210
|
-
if (title.startsWith('BASIC ROM entry') && !loram) return 'BASIC ROM banked out — this is RAM';
|
|
211
|
-
if (title.startsWith('KERNAL') && !hiram) return 'KERNAL ROM banked out — nothing maintains these';
|
|
212
|
-
if (title.startsWith('Autostart') && !cbm80) return 'no CBM80 signature — the KERNAL ignores these words';
|
|
213
|
-
if (title.startsWith('Hardware') && hiram) return 'KERNAL ROM banked in — the ROM vectors, not the program\'s';
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function vectorRow(buf, [name, addr, def, meaning, hook]) {
|
|
218
|
-
const v = buf[addr] | (buf[addr + 1] << 8);
|
|
219
|
-
const window = romWindowOf(v);
|
|
220
|
-
let status;
|
|
221
|
-
if (def === null) status = 'no default';
|
|
222
|
-
else if (v === def) status = 'default';
|
|
223
|
-
else status = '*** RETARGETED ***';
|
|
224
|
-
return { name, addr, def, meaning, hook: hook === 'hook', value: v, status, window };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function renderRows(rows) {
|
|
228
|
-
const out = ['vector value default status'];
|
|
229
|
-
for (const r of rows) {
|
|
230
|
-
out.push(`${r.name} ${hex(r.value)} ${r.def === null ? ' -- ' : hex(r.def) + ' '} ${r.status}`);
|
|
231
|
-
out.push(` ${r.meaning}${r.window ? ` [target in ${r.window} — bank-ambiguous]` : ''}`);
|
|
232
|
-
}
|
|
233
|
-
return out;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function vectors(buf, portOverride, showAll) {
|
|
237
|
-
const out = [];
|
|
238
|
-
const port = portOverride !== undefined ? portOverride : buf[0x0001];
|
|
239
|
-
const banking = decodePort(port);
|
|
240
|
-
const { loram, hiram, charen } = banking;
|
|
241
|
-
const cbm80 = CBM80.every((b, i) => buf[0x8004 + i] === b);
|
|
242
|
-
|
|
243
|
-
out.push(`$01 = ${hex(port, 2)} ${bin8(port)}`);
|
|
244
|
-
out.push(` bit 0 LORAM = ${loram} BASIC ROM ${loram ? 'in' : 'out (RAM at $A000-$BFFF)'}`);
|
|
245
|
-
out.push(` bit 1 HIRAM = ${hiram} KERNAL ROM ${hiram ? 'in' : 'out (RAM at $E000-$FFFF)'}`);
|
|
246
|
-
out.push(` bit 2 CHAREN = ${charen} ${charen ? 'I/O at $D000-$DFFF' : 'character ROM at $D000-$DFFF'}`);
|
|
247
|
-
out.push('');
|
|
248
|
-
out.push(`LIVE VECTOR PAIR: ${hiram ? '$0314/$0315 (KERNAL path — the RAM vectors are live)'
|
|
249
|
-
: '$FFFE/$FFFF (KERNAL banked OUT — the hardware vectors are live)'}`);
|
|
250
|
-
out.push(`CBM80 SIGNATURE: ${cbm80
|
|
251
|
-
? 'PRESENT at $8004 — $8000/$8002 survive a reset and the KERNAL honours them'
|
|
252
|
-
: 'absent — nothing catches a reset here'}`);
|
|
253
|
-
|
|
254
|
-
const hooks = []; // live block, hook site, diverted — an actual signal
|
|
255
|
-
const retargeted = []; // live block, diverted, not a hook site
|
|
256
|
-
const residue = []; // dormant block, non-default — leftover bytes, NOT a divert
|
|
257
|
-
|
|
258
|
-
for (const [title, block] of VECTOR_BLOCKS) {
|
|
259
|
-
const dormant = dormantReason(title, banking, cbm80);
|
|
260
|
-
const rows = block.map((v) => vectorRow(buf, v));
|
|
261
|
-
for (const r of rows) {
|
|
262
|
-
if (r.status !== '*** RETARGETED ***') continue;
|
|
263
|
-
if (dormant) residue.push(r);
|
|
264
|
-
else if (r.hook) hooks.push(r);
|
|
265
|
-
else retargeted.push(r);
|
|
266
|
-
}
|
|
267
|
-
const spine = title.startsWith('KERNAL IRQ') || title.startsWith('Hardware');
|
|
268
|
-
if (!showAll && !spine) continue;
|
|
269
|
-
out.push('');
|
|
270
|
-
out.push(`## ${title}`);
|
|
271
|
-
if (dormant) out.push(` DORMANT: ${dormant}. Read it, do not act on it.`);
|
|
272
|
-
out.push(...renderRows(rows));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (hooks.length) {
|
|
276
|
-
out.push('');
|
|
277
|
-
out.push('*** CRACKER-HOOK SITES DIVERTED ***');
|
|
278
|
-
for (const r of hooks) out.push(` ${r.name} -> ${hex(r.value)} ${r.meaning}`);
|
|
279
|
-
out.push(' A diverted LOAD/SAVE is a custom loader bypassing the KERNAL; a diverted');
|
|
280
|
-
out.push(' STOP is anti-tamper. Both are provenance signals — see c64-provenance-diff.');
|
|
281
|
-
}
|
|
282
|
-
if (retargeted.length && !showAll) {
|
|
283
|
-
out.push('');
|
|
284
|
-
out.push('Other retargeted vectors in LIVE blocks (--all for the full table):');
|
|
285
|
-
for (const r of retargeted) out.push(` ${r.name} -> ${hex(r.value)} ${r.meaning}`);
|
|
286
|
-
}
|
|
287
|
-
if (residue.length) {
|
|
288
|
-
out.push('');
|
|
289
|
-
out.push(`Non-default bytes in DORMANT blocks: ${residue.length}. These are NOT diverted`);
|
|
290
|
-
out.push('vectors. Nothing maintains a block whose ROM is banked out, so the bytes are');
|
|
291
|
-
out.push('whatever was last written there — usually the KERNAL\'s own boot-time values,');
|
|
292
|
-
out.push('partly overwritten. Do not read a hook into them. A byte here that DIFFERS');
|
|
293
|
-
out.push('BETWEEN TWO RELEASES is a provenance question, not a structural one — take it');
|
|
294
|
-
out.push('to c64-provenance-diff, which can prove whether a cracker wrote it.');
|
|
295
|
-
if (showAll) for (const r of residue) out.push(` ${r.name} -> ${hex(r.value)} (default ${hex(r.def)}) ${r.meaning}`);
|
|
296
|
-
else out.push(' --all lists them.');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
out.push('');
|
|
300
|
-
out.push('A retargeted $0314 is the per-frame handler. Confirm it live: the handler');
|
|
301
|
-
out.push('that runs exactly once per frame is the one that matters, whatever the');
|
|
302
|
-
out.push('listing suggests. $FFFA-$FFFF read out of a RAM capture are the RAM bytes');
|
|
303
|
-
out.push('under KERNAL ROM — which is exactly what runs when HIRAM = 0.');
|
|
304
|
-
out.push('');
|
|
305
|
-
out.push('A target marked bank-ambiguous is NOT resolved by this image. Read it twice');
|
|
306
|
-
out.push('live — mcp__plugin_c64-re-tools_vice__vice_memory_read with the default bank, then again with');
|
|
307
|
-
out.push('bank:"ram" — and compare. Differing from stock ROM at that address means the');
|
|
308
|
-
out.push('program has its own code hidden under ROM.');
|
|
309
|
-
return out.join('\n');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// -------------------------------------------------------------------------- cli
|
|
313
|
-
|
|
314
|
-
function flag(argv, name) {
|
|
315
|
-
const i = argv.indexOf(`--${name}`);
|
|
316
|
-
return i === -1 ? undefined : argv[i + 1];
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const USAGE = `usage:
|
|
320
|
-
derive.mjs vic --dd00 3E --d018 18 --d011 1B --d016 C8
|
|
321
|
-
derive.mjs sprites --dd00 3E --d018 18 --d015 FF --ptrs 20,21,22,23,24,25,26,27
|
|
322
|
-
derive.mjs vectors <image.bin> [--port 35] [--all]
|
|
323
|
-
|
|
324
|
-
Values are hex by default ($3E, 0x3E, 3E all work); %00111110 for binary.
|
|
325
|
-
Register values come from mcp__plugin_c64-re-tools_vice__vice_vicii_get_state / vice_memory_read.
|
|
326
|
-
<image.bin> is a 65536-byte capture (see the c64-ram-capture skill).`;
|
|
327
|
-
|
|
328
|
-
function main(argv) {
|
|
329
|
-
const verb = argv[0];
|
|
330
|
-
try {
|
|
331
|
-
if (verb === 'vic') {
|
|
332
|
-
const g = (n, d) => {
|
|
333
|
-
const raw = flag(argv, n);
|
|
334
|
-
if (raw === undefined && d === undefined) throw new Error(`missing --${n}`);
|
|
335
|
-
return raw === undefined ? d : parseNum(raw, `--${n}`);
|
|
336
|
-
};
|
|
337
|
-
console.log(vic({ dd00: g('dd00'), d018: g('d018'), d011: g('d011', 0x1b), d016: g('d016', 0xc8) }));
|
|
338
|
-
} else if (verb === 'sprites') {
|
|
339
|
-
const raw = flag(argv, 'ptrs');
|
|
340
|
-
const ptrs = raw === undefined ? [] : raw.split(',').map((s) => parseNum(s, '--ptrs'));
|
|
341
|
-
console.log(sprites({
|
|
342
|
-
dd00: parseNum(flag(argv, 'dd00'), '--dd00'),
|
|
343
|
-
d018: parseNum(flag(argv, 'd018'), '--d018'),
|
|
344
|
-
d015: parseNum(flag(argv, 'd015') ?? 'FF', '--d015'),
|
|
345
|
-
ptrs,
|
|
346
|
-
}));
|
|
347
|
-
} else if (verb === 'vectors') {
|
|
348
|
-
const path = argv[1];
|
|
349
|
-
if (!path || path.startsWith('--')) throw new Error('vectors needs an image path');
|
|
350
|
-
const buf = readFileSync(path);
|
|
351
|
-
if (buf.length !== 65536) throw new Error(`expected a 65536-byte image, got ${buf.length}`);
|
|
352
|
-
const p = flag(argv, 'port');
|
|
353
|
-
console.log(vectors(buf, p === undefined ? undefined : parseNum(p, '--port'), argv.includes('--all')));
|
|
354
|
-
} else {
|
|
355
|
-
console.log(USAGE);
|
|
356
|
-
process.exit(verb === undefined || verb === '--help' || verb === '-h' ? 0 : 2);
|
|
357
|
-
}
|
|
358
|
-
} catch (e) {
|
|
359
|
-
console.error(`error: ${e.message}`);
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
main(process.argv.slice(2));
|