@henols/c64-re-tools 0.1.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 +61 -0
- package/bin/cli.mjs +226 -0
- package/package.json +53 -0
- package/skills/acme-build/SKILL.md +224 -0
- package/skills/acme-build/scripts/acme.mjs +263 -0
- package/skills/acme-build/template.a +39 -0
- package/skills/c64-memory-mapping/SKILL.md +199 -0
- package/skills/c64-memory-mapping/memmap.json +8800 -0
- package/skills/c64-memory-mapping/scripts/driver.mjs +553 -0
- package/skills/c64-program-recon/SKILL.md +172 -0
- package/skills/c64-program-recon/references/control-flow.md +174 -0
- package/skills/c64-program-recon/references/graphics.md +73 -0
- package/skills/c64-program-recon/references/observation-hazards.md +118 -0
- package/skills/c64-program-recon/references/reconstruction.md +128 -0
- package/skills/c64-program-recon/references/sound-and-input.md +68 -0
- package/skills/c64-program-recon/references/tool-selection.md +55 -0
- package/skills/c64-program-recon/scripts/derive.mjs +364 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +62 -0
- package/skills/c64-provenance-diff/SKILL.md +257 -0
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +981 -0
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +665 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +383 -0
- package/skills/c64-ram-capture/SKILL.md +306 -0
- package/skills/c64-ram-capture/scripts/compare.mjs +258 -0
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +243 -0
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +243 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +317 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +133 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +81 -0
- package/skills/c64-ram-capture/scripts/releases.mjs +109 -0
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +75 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +575 -0
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +339 -0
- package/skills/c64-ram-capture/templates/capture-record.template.md +59 -0
- package/skills/vice-wedge-triage/SKILL.md +149 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SID and CIA: music, effects, the RNG, input, timing
|
|
2
|
+
|
|
3
|
+
Source: `.planning/RE-FINDINGS.md` § SID discovery and § CIA 6526 discovery (2026-08-01,
|
|
4
|
+
**MEDIUM**, doc-derived) except where marked. Per-register bit detail lives in
|
|
5
|
+
`c64-memory-mapping`; this file carries the idioms and the order.
|
|
6
|
+
|
|
7
|
+
## SID — separating the player from the game logic
|
|
8
|
+
|
|
9
|
+
Voice 1 at `$D400`, voice 2 at `$D407`, voice 3 at `$D40E`, seven bytes each: frequency lo/hi,
|
|
10
|
+
pulse width lo/hi, control (gate bit 0, then sync/ring/test and the waveform bits), attack/decay,
|
|
11
|
+
sustain/release. Then `$D415`-`$D418` for filter and volume. Write-only but for the last four:
|
|
12
|
+
`$D419`/`$D41A` paddles, `$D41B` voice 3 oscillator, `$D41C` voice 3 envelope.
|
|
13
|
+
|
|
14
|
+
**Watch `$D404` to land directly on the play routine.** Voice 1's control register gates on every
|
|
15
|
+
note, so a write watch there hits the player without reading the IRQ handler line by line.
|
|
16
|
+
Separating the two entry points follows immediately:
|
|
17
|
+
|
|
18
|
+
> `init` is called **once** from the main code. `play` is called **once per frame** from the IRQ.
|
|
19
|
+
|
|
20
|
+
Pulling the music driver out early removes a large amount of apparent complexity from everything
|
|
21
|
+
else — it is often the single biggest block of code that has nothing to do with gameplay.
|
|
22
|
+
|
|
23
|
+
### Two idioms worth recognising on sight
|
|
24
|
+
|
|
25
|
+
- **`$D41B` read is the random number generator, not audio.** Reading voice 3's oscillator is *the*
|
|
26
|
+
C64 RNG idiom. Code reading `$D41B` is almost never doing sound — it is enemy AI, spawn
|
|
27
|
+
placement, or a title-screen effect. Filing it as sound code sends the AI hunt in the wrong
|
|
28
|
+
direction. Corollary: `$D418` bit 7 (voice 3 disconnect) is often set **precisely because** voice
|
|
29
|
+
3 is the RNG rather than a voice.
|
|
30
|
+
- **`$D418` hammered alone at high frequency is 4-bit sample playback**, not music. It is a
|
|
31
|
+
separate subsystem from the player and usually runs off a **fast CIA timer** rather than the
|
|
32
|
+
frame IRQ — so finding it also explains a CIA timer you could not otherwise account for.
|
|
33
|
+
|
|
34
|
+
## CIA — two chips, identical layouts, different jobs
|
|
35
|
+
|
|
36
|
+
Confusing them is a frequent early error.
|
|
37
|
+
|
|
38
|
+
| | CIA#1 `$DC00` — keyboard, joysticks, **IRQ** | CIA#2 `$DD00` — VIC bank, serial, user port, **NMI** |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| Port A | `$DC00` keyboard **column** select; joystick port 2 | `$DD00` VIC bank (bits 0-1, inverted); serial ATN/CLK/DATA |
|
|
41
|
+
| Port B | `$DC01` keyboard **row** read; joystick port 1 | `$DD01` user port / RS-232 |
|
|
42
|
+
| Timers | `$DC04-$DC07`, control at `$DC0E`/`$DC0F` | `$DD04-$DD07`, `$DD0E`/`$DD0F` — these drive **NMI** |
|
|
43
|
+
| Interrupt | `$DC0D` | `$DD0D`, same bit layout |
|
|
44
|
+
|
|
45
|
+
**The timebase question closes in one read.** A game that never touches `$DC0D` is on a raster IRQ.
|
|
46
|
+
One that programs `$DC04-$DC07` and enables timer A runs its own timebase.
|
|
47
|
+
|
|
48
|
+
## Three CIA hazards
|
|
49
|
+
|
|
50
|
+
- **`$DD00` is dual-purpose.** The same register carries the VIC bank *and* the serial bus lines,
|
|
51
|
+
so a write during disk access also moves the VIC's view of memory unless the code masks
|
|
52
|
+
carefully. **Check the mask** before concluding a `$DD00` write is a bank switch — loader code
|
|
53
|
+
writing `$DD00` is usually talking to the drive.
|
|
54
|
+
- **`$DC0D`/`$DD0D` clear the interrupt flags on read**, the same shape as `$D01E`/`$D01F`. Reading
|
|
55
|
+
one steals an interrupt the game was about to service. Prefer `vice_cia_get_state`. The VICE
|
|
56
|
+
monitor's exact behaviour here is **unverified** — verify, don't assume.
|
|
57
|
+
- **Direct `$DC00`/`$DC01` polling is the norm, and it defeats `vice_keyboard_type`.**
|
|
58
|
+
**Evidence: live, established on this project during recovery work. Confidence: HIGH. Cost: an
|
|
59
|
+
afternoon.** Games and cracks bypass the KERNAL keyboard buffer and read the matrix directly.
|
|
60
|
+
Assume it until shown otherwise, and drive input with `vice_keyboard_matrix` or the joystick
|
|
61
|
+
tools instead.
|
|
62
|
+
|
|
63
|
+
## Finding input handling from the observable side
|
|
64
|
+
|
|
65
|
+
Watch reads of `$DC00`/`$DC01` to find the input routine, then trace forward to what it stores.
|
|
66
|
+
The joystick bits are active-low: bit 4 is fire, bits 0-3 up/down/left/right. A routine that reads
|
|
67
|
+
`$DC01`, masks one bit and branches is the input decoder; the variable it writes is the one to
|
|
68
|
+
name first.
|
|
@@ -0,0 +1,55 @@
|
|
|
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 from `.planning/RE-FINDINGS.md`'s tool-to-question entry (2026-08-01, doc-derived,
|
|
8
|
+
**Confidence: MEDIUM** — the mapping is reasoned from the tool surface and this project's own
|
|
9
|
+
usage, not measured). Individual rows that have since been exercised live are marked.
|
|
10
|
+
|
|
11
|
+
| Question | Call |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 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 |
|
|
14
|
+
| What does the handler at this vector do? | `vice_disassemble` — the emulator's own decoder, not a dead listing |
|
|
15
|
+
| Is this really the main loop? | `vice_checkpoint_add` + `vice_run_until` + `vice_registers_get` — fires once per frame ⇒ proven |
|
|
16
|
+
| What code writes this? | `vice_watch_add` — finds **writers**. Best targets: `$D018`, VM+`$03F8`, `$D404` |
|
|
17
|
+
| Whole-chip state without the read hazards | `vice_vicii_get_state` / `vice_sid_get_state` / `vice_cia_get_state` — **prefer these over raw register reads** |
|
|
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` and regenerator2000 output share this channel |
|
|
21
|
+
| Is the machine wedged, or did it stop itself? | `vice_diagnose` — five-state verdict with its evidence. **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 `.planning/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-changed? | the `c64-provenance-diff` skill |
|
|
31
|
+
| A verified 64K image, or comparing two captures | the `c64-ram-capture` skill |
|
|
32
|
+
| Fast first-pass listing | `toacme` — decodes data as instructions; never the deliverable |
|
|
33
|
+
| Traced disassembly with code/data separation | regenerator2000 — still MEDIUM per `STACK.md`; its first real run is its verification |
|
|
34
|
+
|
|
35
|
+
## Three traps in this table
|
|
36
|
+
|
|
37
|
+
**`vice_run_until` has no working timeout.** Its schema documents `cycles` as *"not yet
|
|
38
|
+
implemented"*, so a run to an address the program never reaches has nothing to bound it and looks
|
|
39
|
+
exactly like a wedged emulator. Prefer `vice_checkpoint_add` + a bounded poll when the address is
|
|
40
|
+
a hypothesis rather than a certainty. **Confidence: MEDIUM** — read off the schema, not reproduced.
|
|
41
|
+
|
|
42
|
+
**`vice_diagnose` leaves the machine paused.** When it measures a cycle bracket it resumes the
|
|
43
|
+
machine once or twice and then leaves it **paused** — resuming is your own next call. And a
|
|
44
|
+
`checkpoint_trap` verdict means the machine stopped *itself* at an armed checkpoint: it must
|
|
45
|
+
**not** be recycled, because recycling a self-inflicted stop destroys a healthy instance.
|
|
46
|
+
|
|
47
|
+
**Prefer the `*_get_state` calls, but know they pause.** They avoid the raw-register read
|
|
48
|
+
hazards, but most state reads pause the emulator. Read everything first, poll with `vice_ping`,
|
|
49
|
+
and resume exactly once at the end. See `observation-hazards.md`.
|
|
50
|
+
|
|
51
|
+
## The standing constraint
|
|
52
|
+
|
|
53
|
+
`mcp__plugin_c64-re-tools_vice__*` is the only route to the emulator. Any step of a method that would want a Node
|
|
54
|
+
process talking to VICE is dead on arrival and must be expressed as agent-performed tool calls —
|
|
55
|
+
the same rule that reduced `c64-ram-capture` to a procedure and deleted its original `scripts/`.
|
|
@@ -0,0 +1,364 @@
|
|
|
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));
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Memory map — <program name> @ <checkpoint where this was captured>
|
|
2
|
+
|
|
3
|
+
Capture: `<path to the 64K image>` · SHA-256 `<hash>`
|
|
4
|
+
`$01` = `<value>` · VIC bank `<n>` (`$DD00` = `<value>`) · video standard `<PAL/NTSC>`
|
|
5
|
+
Live vector pair: `<$0314/$0315 or $FFFE/$FFFF>` → `<handler>`
|
|
6
|
+
|
|
7
|
+
Every row carries a confidence. Do not promote a row by editing its grade — re-verify and restate
|
|
8
|
+
the evidence, so the record of when something stopped being a guess survives.
|
|
9
|
+
|
|
10
|
+
| Range | Contents | Confidence | Evidence |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| `$0000-$00FF` | Zero page — game variables | | |
|
|
13
|
+
| `$0100-$01FF` | Stack | CONFIRMED | hardware |
|
|
14
|
+
| `$0200-$03FF` | KERNAL work area / vectors | | |
|
|
15
|
+
| `$0400-$07E7` | Screen RAM (if VM resolves here) | | |
|
|
16
|
+
| `$0801-$` | | | |
|
|
17
|
+
| `$D800-$DBFF` | Colour RAM | CONFIRMED | hardware, not banked |
|
|
18
|
+
| `$E000-$FFFF` | RAM under KERNAL (HIRAM=0) or KERNAL ROM | | |
|
|
19
|
+
|
|
20
|
+
Confidence vocabulary — the project's HIGH / MEDIUM / LOW scale, applied to classification:
|
|
21
|
+
|
|
22
|
+
| Grade | Means |
|
|
23
|
+
|---|---|
|
|
24
|
+
| **confirmed code** | Executed during tracing, PC observed inside it |
|
|
25
|
+
| **probable code** | Reachable through a `JSR`/`JMP`/vector, not yet observed executing |
|
|
26
|
+
| **confirmed data** | Never hit as an instruction stream across full gameplay coverage |
|
|
27
|
+
| **probable data** | Indexed-load target, or matches a data shape (sprite blocks, PETSCII, address tables) |
|
|
28
|
+
| **unknown** | No reliable interpretation yet |
|
|
29
|
+
|
|
30
|
+
Do not force an unknown range through a disassembler and record the output as code. A linear
|
|
31
|
+
decode of data is silently wrong and contaminates everything downstream.
|
|
32
|
+
|
|
33
|
+
## Graphics chain
|
|
34
|
+
|
|
35
|
+
| What | Address | Derived from |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| VIC bank | | `$DD00` bits 0-1, inverted |
|
|
38
|
+
| Screen RAM (VM) | | `$D018` bits 4-7 |
|
|
39
|
+
| Charset / bitmap (CB) | | `$D018` bits 1-3 |
|
|
40
|
+
| Mode | | `$D011` bits 5-6, `$D016` bit 4 |
|
|
41
|
+
| Sprite pointers | | VM + `$03F8` |
|
|
42
|
+
|
|
43
|
+
Character ROM shadow checked: <yes/no — CB in `$1000`/`$9000` of bank 0/2 means no charset in RAM>
|
|
44
|
+
|
|
45
|
+
## Interrupts
|
|
46
|
+
|
|
47
|
+
| | Address | Notes |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Live IRQ handler | | |
|
|
50
|
+
| Raster positions | | one per `$D012` write on the way out of a handler |
|
|
51
|
+
| NMI handler | | `$0318/$0319` or `$FFFA/$FFFB` |
|
|
52
|
+
| Timebase | | `$DC0D` untouched ⇒ raster; programmed ⇒ own timebase |
|
|
53
|
+
|
|
54
|
+
## Routines
|
|
55
|
+
|
|
56
|
+
| Address | Provisional name | Confirmed by | Confidence |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| | `Maybe_` | | |
|
|
59
|
+
|
|
60
|
+
## Open questions
|
|
61
|
+
|
|
62
|
+
-
|