@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.
Files changed (35) hide show
  1. package/README.md +61 -0
  2. package/bin/cli.mjs +226 -0
  3. package/package.json +53 -0
  4. package/skills/acme-build/SKILL.md +224 -0
  5. package/skills/acme-build/scripts/acme.mjs +263 -0
  6. package/skills/acme-build/template.a +39 -0
  7. package/skills/c64-memory-mapping/SKILL.md +199 -0
  8. package/skills/c64-memory-mapping/memmap.json +8800 -0
  9. package/skills/c64-memory-mapping/scripts/driver.mjs +553 -0
  10. package/skills/c64-program-recon/SKILL.md +172 -0
  11. package/skills/c64-program-recon/references/control-flow.md +174 -0
  12. package/skills/c64-program-recon/references/graphics.md +73 -0
  13. package/skills/c64-program-recon/references/observation-hazards.md +118 -0
  14. package/skills/c64-program-recon/references/reconstruction.md +128 -0
  15. package/skills/c64-program-recon/references/sound-and-input.md +68 -0
  16. package/skills/c64-program-recon/references/tool-selection.md +55 -0
  17. package/skills/c64-program-recon/scripts/derive.mjs +364 -0
  18. package/skills/c64-program-recon/templates/memory-map.template.md +62 -0
  19. package/skills/c64-provenance-diff/SKILL.md +257 -0
  20. package/skills/c64-provenance-diff/scripts/diff-images.mjs +981 -0
  21. package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +665 -0
  22. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +383 -0
  23. package/skills/c64-ram-capture/SKILL.md +306 -0
  24. package/skills/c64-ram-capture/scripts/compare.mjs +258 -0
  25. package/skills/c64-ram-capture/scripts/d64-parse.mjs +243 -0
  26. package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +243 -0
  27. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +317 -0
  28. package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +133 -0
  29. package/skills/c64-ram-capture/scripts/project-paths.mjs +81 -0
  30. package/skills/c64-ram-capture/scripts/releases.mjs +109 -0
  31. package/skills/c64-ram-capture/scripts/test-corpus.mjs +75 -0
  32. package/skills/c64-ram-capture/scripts/watch-loads.mjs +575 -0
  33. package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +339 -0
  34. package/skills/c64-ram-capture/templates/capture-record.template.md +59 -0
  35. package/skills/vice-wedge-triage/SKILL.md +149 -0
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: c64-program-recon
3
+ description: Work out how an unknown C64 program is structured at runtime — entry point, interrupt handlers, main loop, game states, graphics and sound — in a fixed order, before disassembling anything. Use when asked to reverse engineer a C64 game, find the main loop, entry point or IRQ handler, locate the player sprite, charset or music player, identify a game state machine, work out which memory regions are code versus data, or decide where to start on a depacked image.
4
+ ---
5
+
6
+ # Reconnaissance on an unknown C64 program
7
+
8
+ **Do not disassemble the whole program first.** The structure hangs off a small fixed set of
9
+ well-known addresses: read them in order and the answer falls out. Treated as a search problem it
10
+ costs an hour every session; written down it is minutes.
11
+
12
+ Build a network of confirmed facts. Once the vectors, the IRQ handler, the main loop and the major
13
+ tables are known, everything else classifies far more easily.
14
+
15
+ ```bash
16
+ D=.claude/skills/c64-program-recon/scripts/derive.mjs # from the repo root
17
+
18
+ node $D vectors dump.bin # $01 + six vectors, which pair is live
19
+ node $D vic --dd00 3E --d018 18 --d011 1B --d016 C8 # bank, screen, charset, mode
20
+ node $D sprites --dd00 3E --d018 18 --d015 0F --ptrs 20,21,22,23,FF,FF,FF,FF
21
+ ```
22
+
23
+ The script does only the arithmetic that a lookup table cannot — register bits to concrete
24
+ addresses — over values **you** fetched through `mcp__plugin_c64-re-tools_vice__*`. It contacts nothing.
25
+
26
+ ## The order
27
+
28
+ Each step is a read whose answer rules something out. Do not skip ahead: step 6 is cheap once the
29
+ handler is known, because that is where most chip writes happen.
30
+
31
+ | # | Question | Read | What the answer settles |
32
+ |---|---|---|---|
33
+ | 0 | Which of these bytes are even the game? | The manifest's buckets — `c64-provenance-diff` | Tracing a depacker's IRQ handler is wasted work. Scope before you trace |
34
+ | 1 | Where does execution start? | Post-depack: wherever the PC sits at the decrunch checkpoint. There is no BASIC stub to find. | The one address everything else hangs off |
35
+ | 2 | Which vector is live? | `$01`, then `$0314/$0315` **or** `$FFFE/$FFFF` | HIRAM (`$01` bit 1) decides. KERNAL out ⇒ the RAM vectors are meaningless |
36
+ | 3 | What drives the frame? | `$D01A`, `$D012`, `$DC0D` | `$DC0D` untouched ⇒ raster IRQ; programmed ⇒ the game runs its own timebase |
37
+ | 4 | Where is the main loop? | Checkpoint a suspected loop head, run one frame | Two shapes only: a real loop, or a two-instruction spin with the IRQ doing everything |
38
+ | 5 | Code or data? | What the PC actually visits across full coverage | A range never executed is data, whatever a tracer guessed |
39
+ | 6 | Where is the graphics? | `$DD00` → `$D018` → mode bits → `$D015` → VM+`$03F8` | Every displayed byte, computed. Nothing to search for |
40
+ | 7 | Where is the music? | Watch `$D404` | `init` runs once from main code; `play` runs once per frame from the IRQ |
41
+ | 8 | Where is the input? | Reads of `$DC00`/`$DC01` | Games poll the matrix directly and ignore the KERNAL buffer |
42
+
43
+ ## Step 0 in full: only the game is in scope
44
+
45
+ Every image taken from a cracked release carries three layers that are **not the game**, and no
46
+ original master exists to strip them for you:
47
+
48
+ | Layer | What it is | Dead when |
49
+ |---|---|---|
50
+ | **loader** | The custom raw-sector routine that bypasses the KERNAL | The payload is in RAM |
51
+ | **cruncher / depacker** | The decompression stub | It has run once |
52
+ | **cracktro** | The group's intro, scroller, credits, and the keypress gate that dismisses it | Dismissed |
53
+
54
+ **The rule: a byte with nothing to do with the game is out of scope — always, not case by case.**
55
+ It is not annotated, not reconstructed, and not traced. This is a standing default, not a
56
+ per-session decision.
57
+
58
+ **The evidence bar runs both ways, and this project has been burned in both directions.**
59
+
60
+ - Removal needs *positive* evidence of the bucket: a crack-credit vocabulary match, a
61
+ `RELEASES.json` `loader_ranges` entry earned from live disassembly, or a depacker stub provably
62
+ dead after first run. A bare printable-ASCII scan classified **the game's own title text** as
63
+ cracktro credit and would have shipped a confidently-wrong `CRACKER-PATCH` verdict.
64
+ - Absence of evidence records `UNKNOWN` and **keeps the bytes**. The
65
+ a real title-screen text divergence was found sitting in a region that is neither loader nor
66
+ cracktro — a cracker edit inside the game's own data. Stripping only the obvious intro screen
67
+ leaves crack residue behind.
68
+
69
+ `c64-provenance-diff` owns the machinery and the five kinds; do not re-derive them here. What
70
+ belongs here is the ordering: **bucket first, then trace only what survives.**
71
+
72
+ Then work **backwards from observable effects** rather than reading code sequentially — it is
73
+ consistently faster. Watch writes to the sprite coordinates to find movement; watch `$D018` to find
74
+ the room loader; watch VM+`$03F8` to find the animation driver. `vice_watch_add` finds *writers*,
75
+ and that is its real leverage.
76
+
77
+ Differential experiments close the loop: patch a routine to `RTS` and see what stops. If enemies
78
+ freeze and nothing else does, the routine's purpose is confirmed — far stronger evidence than
79
+ reading the listing.
80
+
81
+ ## Worked example — a real capture
82
+
83
+ ```
84
+ $ node $D vectors capture.bin
85
+ $01 = $40 %01000000
86
+ bit 0 LORAM = 0 BASIC ROM out (RAM at $A000-$BFFF)
87
+ bit 1 HIRAM = 0 KERNAL ROM out (RAM at $E000-$FFFF)
88
+ bit 2 CHAREN = 0 character ROM at $D000-$DFFF
89
+
90
+ LIVE VECTOR PAIR: $FFFE/$FFFF (KERNAL banked OUT — the hardware vectors are live)
91
+ CBM80 SIGNATURE: absent — nothing catches a reset here
92
+
93
+ ## KERNAL IRQ/BRK/NMI ($0314-$0319)
94
+ DORMANT: KERNAL ROM banked out — nothing maintains these. Read it, do not act on it.
95
+ vector value default status
96
+ $0314/$0315 $0101 $EA31 *** RETARGETED ***
97
+ CINV — KERNAL IRQ (RAM, indirect)
98
+
99
+ ## Hardware vectors ($FFFA-$FFFF) — live when the KERNAL is banked out
100
+ $FFFA/$FFFB $1116 -- no default
101
+ $FFFC/$FFFD $1116 -- no default
102
+ $FFFE/$FFFF $1103 -- no default
103
+
104
+ Non-default bytes in DORMANT blocks: 17. These are NOT diverted
105
+ vectors. …
106
+ ```
107
+
108
+ Read it as: the KERNAL is banked out, so the whole `$0300-$0333` range is **dormant** and `$0314`'s
109
+ `$0101` is residue, not a retargeted vector — ignore it. The live handler is `$1103`, and that is
110
+ where to arm the first checkpoint. `$FFFA` and `$FFFC` both holding `$1116` says the game installs
111
+ its own NMI *and* RESET handlers, at one shared address.
112
+
113
+ Both cracked releases give the same answers across all three of their captures, and `$1103` is the
114
+ same IRQ entry that phase-01 live work established independently (chain `$1103 → $1574 → $152C`).
115
+ The method reproduces a known-good result from a static image with no emulator running, and the
116
+ `$1116` pair is new — see `references/control-flow.md` § 2. **Confidence: HIGH** for steps 1-2.
117
+
118
+ ## Before you touch the emulator
119
+
120
+ Two hazards cost this project real sessions. Both are in `references/observation-hazards.md`; these
121
+ two lines are the part you cannot afford to load lazily.
122
+
123
+ - **Pause after every observation.** Agent think-time runs the emulator at full speed — 258 million
124
+ cycles (~262 emulated seconds) elapsed across a handful of reads with zero input sent, which was
125
+ enough to reach `GAME OVER` and to invalidate an earlier finding. Never leave the machine running
126
+ across a reasoning step.
127
+ - **When the machine looks frozen, enumerate your own checkpoints first.** An armed *stopping*
128
+ checkpoint on the live IRQ path reproduces the entire "dead emulator" signature — zero cycles,
129
+ `ping` still reporting `running`, an identical PC — because the machine genuinely never moved.
130
+ Two cheap reads settle it, and neither needs `vice_execution_run`.
131
+
132
+ ## Which skill does what
133
+
134
+ This one is the route between the stations. It does not restate what the others carry.
135
+
136
+ | Need | Go to |
137
+ |---|---|
138
+ | A verified 64K image, or comparing two captures | `c64-ram-capture` |
139
+ | What a specific address or bit means | `c64-memory-mapping` — `node … lookup '$D018'` |
140
+ | Assembling, or a first-pass dead listing | `acme-build` |
141
+ | Whether a byte is original or cracker-changed | `c64-provenance-diff` |
142
+ | The emulator stopped moving — wedged, self-trapped, or respawned | `vice-wedge-triage` |
143
+ | **Which address to read next, and what the answer rules out** | here |
144
+
145
+ ## References
146
+
147
+ | File | Covers |
148
+ |---|---|
149
+ | `references/control-flow.md` | Entry point, the six vectors, IRQ source, main-loop shapes, state machines |
150
+ | `references/graphics.md` | The VIC derivation chain, the char-ROM shadow trap, sprites, watch targets |
151
+ | `references/sound-and-input.md` | SID player vs `$D41B`-as-RNG vs digi; CIA#1 vs CIA#2 |
152
+ | `references/observation-hazards.md` | Every way a live read gives a wrong answer. **Read before driving.** |
153
+ | `references/tool-selection.md` | Which `mcp__plugin_c64-re-tools_vice__*` call answers which question, and what to delegate |
154
+ | `references/reconstruction.md` | Binary inclusion, behavioural-equivalence correctness bar, SMC labels, label vocabulary |
155
+ | `templates/memory-map.template.md` | Region map with per-row confidence grading |
156
+
157
+ Findings that make RE faster go in `.planning/RE-FINDINGS.md` **at the moment you find them**,
158
+ graded with `Evidence:` and `Confidence:`. Promote by re-logging with the new evidence, never by
159
+ editing a grade in place. File-changing work enters through a GSD command (`/gsd-quick`).
160
+
161
+ ## Troubleshooting
162
+
163
+ | Symptom | Fix |
164
+ |---|---|
165
+ | `$0314` holds something that is not a plausible address | Check HIRAM. With the KERNAL banked out the RAM vectors are uninitialised; read `$FFFE/$FFFF`. |
166
+ | Every graphics pointer is wrong, with no error | `$DD00` bits 0-1 are **inverted**. Re-derive the bank first; everything else hangs off it. |
167
+ | The charset at the computed address is garbage | CB may resolve into the char-ROM shadow (`$1000`/`$9000`, banks 0/2). `derive.mjs vic` flags it — there is no charset in RAM to extract. |
168
+ | A sprite decodes as noise | Check `$D015` first; a disabled sprite's registers are stale. Then check MCM — multicolor decoded as hires comes out twice as wide. |
169
+ | Computed mode is "INVALID — screen goes black" | You caught the registers mid-update inside a raster split. Re-read. |
170
+ | The emulator looks dead | Enumerate armed checkpoints before anything else. See hazard 2. |
171
+ | `vice_keyboard_type` does nothing | The game polls `$DC00`/`$DC01` directly. Use `vice_keyboard_matrix`. |
172
+ | Two captures of the same checkpoint differ | Expected. Full-64K identity is impossible in principle; use `c64-ram-capture`'s drift rules. |
@@ -0,0 +1,174 @@
1
+ # Control flow: entry point → vectors → IRQ source → main loop → structure
2
+
3
+ Source: `.planning/RE-FINDINGS.md` § Control-flow discovery method (2026-08-01, **MEDIUM**,
4
+ doc-derived) except where a line says otherwise. The vector-table step was confirmed against
5
+ this project's own captures on 2026-08-04 — see the bottom of this file.
6
+
7
+ ## 1. Entry point — three routes, and post-depack is a different question
8
+
9
+ | Situation | Where the entry point is |
10
+ |---|---|
11
+ | BASIC stub at `$0801` | A tokenized `SYS <addr>` line. The address is plain PETSCII digits in the stub, so it reads straight out of the load image with no interpretation. |
12
+ | Autostart / non-BASIC | The `.prg` load address (first two bytes), the RESET vector at `$FFFC/$FFFD`, or a cartridge CBM80 signature at `$8000`. |
13
+ | **Post-depack — this project's case** | Wherever the PC sits when the decrunch checkpoint fires. A different question with a different answer. |
14
+
15
+ For a depacked image there is no BASIC stub to find. Do not spend time looking for one.
16
+ `c64-ram-capture` § Find an entry point gives the live procedure: step in batches until PC and
17
+ SP settle into a repeating range across three consecutive batches.
18
+
19
+ ## 2. Vectors — sweep every block, and `$01` decides which are live
20
+
21
+ Six pairs is not "all vectors". `node derive.mjs vectors <image.bin>` sweeps all six blocks below;
22
+ it prints the IRQ/BRK/NMI and hardware blocks by default and takes `--all` for the rest.
23
+
24
+ | Block | Range | Why it matters |
25
+ |---|---|---|
26
+ | BASIC indirects | `$0300-$030B` | IERROR, IMAIN, ICRNCH, IQPLOP, IGONE, IEVAL — a program returning to a modified BASIC prompt hooks these |
27
+ | KERNAL IRQ/BRK/NMI | `$0314-$0319` | CINV `$EA31`, CBINV `$FE66`, NMINV `$FE47`. The per-frame handler, and the pair music players retarget |
28
+ | KERNAL I/O indirects | `$031A-$0333` | OPEN…SAVE. **`$0328` STOP and `$0330`/`$0332` LOAD/SAVE are where a cracker hooks** |
29
+ | Autostart / cartridge | `$8000` cold, `$8002` warm, `$8004` `CBM80` | The standard survive-a-reset trick. Without the signature the KERNAL ignores both words |
30
+ | BASIC ROM entry | `$A000/$A002` | Only meaningful with BASIC banked in — check `$01` LORAM first |
31
+ | Hardware vectors | `$FFFA-$FFFF` | NMI, RESET, IRQ/BRK. Live when the KERNAL is banked out |
32
+
33
+ **The hardware pairs are only live when the ROMs are banked out via `$01`.** The deciding bit is
34
+ HIRAM, `$01` bit 1 (RE-FINDINGS 2026-08-02). HIRAM = 1 ⇒ KERNAL ROM is in and `$0314/$0315` is
35
+ live. HIRAM = 0 ⇒ RAM at `$E000-$FFFF` and `$FFFE/$FFFF` is live.
36
+
37
+ Why LOAD and STOP earn their own callout on this project: both releases use custom raw-sector
38
+ loaders that bypass the KERNAL, so a diverted `$0330` is a provenance signal, and a diverted
39
+ `$0328` is anti-tamper. Neither was being looked for before 2026-08-04.
40
+
41
+ The second tell is the handler's own first instruction: the KERNAL's register-save preamble means
42
+ the KERNAL path is in use; a jump straight into game code means it is not.
43
+
44
+ ### A non-default value in a dormant block is not a hook
45
+
46
+ **A garbage-looking `$0314` is not a bug when HIRAM = 0.** With the KERNAL banked out, nothing
47
+ maintains the RAM vectors and they hold whatever was last there — usually the KERNAL's own
48
+ boot-time values, partly overwritten. `derive.mjs` labels each such block `DORMANT` and reports
49
+ its non-default bytes as *residue*, never as a divert, because reading a hook into residue is the
50
+ fastest way to a confidently-wrong structural claim.
51
+
52
+ A residue byte that **differs between two releases** is still worth something — but it is a
53
+ provenance question, not a structural one. Take it to `c64-provenance-diff`.
54
+
55
+ ### A target in a ROM window is unresolved until you read it twice
56
+
57
+ A vector target in `$A000-$BFFF`, `$D000-$DFFF` or `$E000-$FFFF` is *either* ROM *or* the RAM
58
+ underneath, decided by `$01` at the moment the vector is taken. `derive.mjs` marks these
59
+ `bank-ambiguous` and stops there, because a static image cannot settle it.
60
+
61
+ Live, it is one extra call: read the target with `mcp__plugin_c64-re-tools_vice__vice_memory_read`'s default bank,
62
+ then again with `bank: "ram"`, and compare (`vice_memory_banks` lists what is available). Agreeing
63
+ with stock ROM bytes ⇒ the vector genuinely lands in ROM and the KERNAL path is in use. Differing
64
+ ⇒ **the program has its own code hidden under ROM**, which is a large structural finding and is
65
+ otherwise not looked for at all. Record the stock bytes you compared against, so the check is
66
+ reproducible.
67
+
68
+ This supersedes the weaker "check `$01` and read the handler's preamble" tell, which silently
69
+ reads whichever bank happens to be mapped.
70
+
71
+ ### What the widened sweep found here — 2026-08-04
72
+
73
+ Run over all six committed captures of one title (two releases, runs 1-3 each):
74
+
75
+ | Vector | release-a | release-b | Reading |
76
+ |---|---|---|---|
77
+ | `$FFFE/$FFFF` IRQ | `$1103` | `$1103` | Known; matches the live-established handler chain |
78
+ | `$FFFA/$FFFB` NMI | `$1116` | `$1116` | **New.** The game installs its own NMI handler under KERNAL ROM |
79
+ | `$FFFC/$FFFD` RESET | `$1116` | `$1116` | **New.** RESET funnels to the *same* address as NMI |
80
+ | `$0328/$0329` ISTOP | `$F6FC` | `$F6ED` (stock) | **New, and a divergence.** Residue — the block is dormant |
81
+ | `$8004` `CBM80` | absent | absent | Nothing catches a reset via the cartridge route |
82
+
83
+ NMI and RESET sharing one entry is the shape of an anti-tamper trap: RESTORE and reset are the two
84
+ ways a user perturbs a running game, and both land in the same place. `$1116` is therefore the
85
+ address to checkpoint when the emulator is next available — press RESTORE with
86
+ `mcp__plugin_c64-re-tools_vice__vice_keyboard_restore` (it is *not* in the keyboard matrix, and NMI will not retrigger
87
+ until the line is released, so it is a press→release **edge**), then `vice_machine_reset` soft and
88
+ hard, and record where the PC actually lands.
89
+
90
+ **Evidence:** derived mechanically from six three-run-verified captures; every value identical
91
+ across all three runs of its release, so none of it is drift.
92
+ **Confidence:** HIGH for the values and for the cross-release divergence. The *interpretation* of
93
+ `$1116` as anti-tamper is **LOW** — unexercised, and the perturbation experiment above is exactly
94
+ what would settle it.
95
+
96
+ ## 3. IRQ source — two enable masks close the question
97
+
98
+ - **Raster IRQ** — `$D012` (raster compare), `$D011` bit 7 (raster bit 8), `$D01A` (IRQ enable
99
+ mask), `$D019` (latch, which the handler must acknowledge). **A handler that writes a new
100
+ `$D012` on its way out is a split raster chain**, and each such write is one more IRQ position
101
+ to enumerate.
102
+ - **CIA timer IRQ** — `$DC0D` (CIA#1, drives IRQ) and `$DD0D` (CIA#2, drives NMI), with
103
+ `$DC04-$DC07` / `$DD04-$DD07` for the periods.
104
+
105
+ A game that never touches `$DC0D` is on a raster IRQ. One that programs `$DC04-$DC07` and enables
106
+ timer A has its own timebase. Read the two enable registers before reading any handler code.
107
+
108
+ ## 4. Main loop — decide the shape before hunting
109
+
110
+ - **Real main loop** — an unconditional backward branch or `JMP` to a nearby earlier address that
111
+ never returns, usually preceded by a frame-sync wait: polling `$D012` for a fixed raster line, or
112
+ spinning on a flag the IRQ handler sets.
113
+ - **IRQ-does-everything** — the main loop is a two-instruction spin and all logic hangs off the
114
+ raster IRQ. Common enough that assuming the first shape wastes the search.
115
+
116
+ **The honest test is live, not static.** The address that repeats exactly once per frame in an
117
+ execution trace is the main loop, whatever the listing suggests.
118
+
119
+ ## 5. Four structural features a linear disassembler gets wrong
120
+
121
+ - **Jump tables and dispatch** — `JMP ($xxxx)` and `JSR` into an indexed table. This is where game
122
+ state machines live, and exactly what a linear decoder mis-decodes.
123
+ - **Self-modifying code** — writes into `$0800-$CFFF` from code that also executes there. Common
124
+ for animation frame pointers.
125
+ - **Zero page** — the game's hot variables. The highest-frequency ZP addresses in a trace are the
126
+ state worth naming first.
127
+ - **Code/data separation** — the project's standing rule (`.claude/CLAUDE.md` § Stack Patterns): a
128
+ range never hit as an instruction stream across full gameplay coverage is data, regardless of
129
+ what the tracer guessed. The provenance diff between the two cracked releases is the second check
130
+ on the same question.
131
+
132
+ ## Finding the state machine
133
+
134
+ Most games have one even when it is not explicit. Two shapes:
135
+
136
+ ```asm
137
+ LDA GameState ; indexed dispatch — the common shape, and the one
138
+ ASL ; a linear disassembler turns into nonsense
139
+ TAX
140
+ LDA StateTable,X
141
+ STA JumpVector+1
142
+ LDA StateTable+1,X
143
+ STA JumpVector+2
144
+ JumpVector:
145
+ JMP $FFFF ; operand is patched at runtime
146
+ ```
147
+
148
+ ```asm
149
+ LDA GameState ; compare chain — easier to read, easier to find
150
+ CMP #STATE_TITLE
151
+ BEQ TitleState
152
+ ```
153
+
154
+ Finding the state variable gives a high-level map of the whole program. A practical route: pause
155
+ at a title screen and again in gameplay, diff the two captures, and look for a single byte that
156
+ changed in zero page or low RAM. `vice_memory_compare` narrows this; `c64-ram-capture` § Compare
157
+ two captures gives the volatility rules that stop you chasing drift.
158
+
159
+ ## Verified against this project — 2026-08-04
160
+
161
+ Running `derive.mjs vectors` cold on both releases' `*-gameentry-run1.bin` returns `$01` = `$40`
162
+ (LORAM 0, HIRAM 0, CHAREN 0 — KERNAL and BASIC banked out), so the live pair is `$FFFE/$FFFF`,
163
+ holding **`$1103`**, while `$0314/$0315` holds `$0101` — nothing meaningful, exactly as the
164
+ dormant-block rule predicts.
165
+
166
+ `$1103` is the same IRQ-handler entry that phase-01 live work independently established, with the
167
+ raster-split chain `$1103 → $1574 → $152C` (RE-FINDINGS 2026-08-02, checkpoint-trap entry). The
168
+ method reproduces a known-good result from a static image with no emulator running. The widened
169
+ sweep's own results, including two facts this table never surfaced, are in §2 above.
170
+
171
+ **Evidence:** derived mechanically from three-run-verified captures, cross-checked against a live
172
+ finding recorded independently.
173
+ **Confidence:** HIGH for the vector-table step, the HIRAM rule, and the dormant-block rule. The
174
+ remaining steps in this file stay MEDIUM until exercised the same way.
@@ -0,0 +1,73 @@
1
+ # VIC-II: locating every displayed byte
2
+
3
+ Source: `.planning/RE-FINDINGS.md` § VIC-II discovery (2026-08-01, **MEDIUM**, doc-derived).
4
+
5
+ **Graphics data is computed, not searched.** Every pointer the VIC follows derives from two
6
+ registers plus a bank. Read the bank, read `$D018`, read the mode bits, read `$D015` and the
7
+ sprite pointer block, and you have located every byte of graphics on screen. Searching memory for
8
+ something the hardware will tell you the address of is the single largest time sink in graphics RE.
9
+
10
+ `node derive.mjs vic` and `node derive.mjs sprites` do the arithmetic. For what any individual bit
11
+ *means*, use `c64-memory-mapping` — it carries the full `$D018` and `$DD00` value tables, and this
12
+ file deliberately does not restate them.
13
+
14
+ ## The order, and why it is this order
15
+
16
+ 1. **`$DD00` bits 0-1 — the VIC bank. Read this first, every time.** The bits are **inverted**
17
+ (`%11` → bank 0 at `$0000`, `%00` → bank 3 at `$C000`). Every other pointer is relative to this
18
+ base, so getting it wrong corrupts the whole chain **silently, with no error to signal it**.
19
+ This is the most common source of a wrong answer in C64 graphics RE.
20
+ 2. **`$D018` — two pointers in one byte.** Bits 4-7 = VM (screen RAM = bank + VM × `$0400`);
21
+ bits 1-3 = CB (charset = bank + CB × `$0800`). In bitmap mode only bit 3 matters: which 8K half
22
+ of the bank the bitmap occupies, and the video matrix then holds colour pairs rather than
23
+ character codes.
24
+ 3. **The mode bits** — `$D011` bit 6 (ECM), `$D011` bit 5 (BMM), `$D016` bit 4 (MCM). These decide
25
+ what the bytes *mean*; decoding a multicolor sprite as hires produces garbage twice as wide as
26
+ it should be. ECM combined with BMM or MCM is invalid and blanks the screen — if you compute
27
+ that, you probably caught the registers mid-update inside a raster split, so re-read.
28
+ 4. **`$D015` — the sprite enable mask. Start here, not at the sprite data.** A disabled sprite's
29
+ other registers are stale and decoding them yields noise.
30
+ 5. **Sprite pointers at video matrix + `$03F8`**, 8 bytes. Each pointer × 64 = the sprite's data
31
+ address *within the current bank*. 63 bytes used of the 64 allocated.
32
+
33
+ ## The hazard that costs an hour
34
+
35
+ **The character ROM shadow.** The VIC sees character ROM at `$1000-$1FFF` (bank 0) and
36
+ `$9000-$9FFF` (bank 2) **regardless of the `$01` banking the CPU sees**. If CB resolves into
37
+ either window, the game is using ROM characters and there is no charset in RAM to extract.
38
+ `derive.mjs vic` flags this explicitly. Check it before dumping anything.
39
+
40
+ ## Colour is not banked
41
+
42
+ Colour RAM is fixed at `$D800-$DBFF` and does **not** move with the VIC bank. Only the low nybble
43
+ of each byte exists. `$D020` is the border, `$D021-$D024` backgrounds 0-3 (2 and 3 used only in
44
+ ECM). Looking for colour data at a bank-relative address finds something else entirely.
45
+
46
+ ## Two watch targets that find the two highest-value routines
47
+
48
+ `vice_watch_add` finds **writers**, and that is its real leverage in RE — it is under-used relative
49
+ to reading memory.
50
+
51
+ | Watch | Finds |
52
+ |---|---|
53
+ | `$D018` | The screen-setup routine. In a room- or level-based game this is usually the room loader — one of the highest-value routines to locate early. |
54
+ | video matrix + `$03F8` | The animation driver. Rewriting sprite pointers frame to frame is exactly what it does. |
55
+
56
+ ## Reading collision registers changes the game
57
+
58
+ `$D01E` (sprite-sprite) and `$D01F` (sprite-background) **clear when read**. Reading them while the
59
+ game runs steals the collision the game was about to act on — the worst class of observation bug,
60
+ because it discredits the capture without announcing itself. Prefer `vice_vicii_get_state`.
61
+
62
+ Whether the VICE monitor's own read is side-effect-free is **unverified** — treat it as
63
+ verify-don't-assume, not as a settled fact.
64
+
65
+ Many games do software collision anyway: look for coordinate subtraction, comparisons against
66
+ width and height, tile lookups, mask tables and bounding-box arithmetic before concluding the
67
+ hardware registers are what the game uses.
68
+
69
+ ## Sprite decoding
70
+
71
+ `vice_sprite_get` / `vice_sprite_inspect` do the pointer arithmetic and the multicolor bit-pair
72
+ unpacking. Verify what they return once against a hand-resolved pointer — `derive.mjs sprites`
73
+ gives you that hand resolution — then trust them.
@@ -0,0 +1,118 @@
1
+ # Hazards: how observing a running C64 gives you the wrong answer
2
+
3
+ The entries here are graded higher than the method files, because most were established live on
4
+ this project at real cost. A wrong answer from a register table is cheap; a wrong answer from a
5
+ machine that changed *because you looked at it* discredits a whole session without announcing
6
+ itself.
7
+
8
+ Full provenance in `.planning/RE-FINDINGS.md`. Log new hazards there at the moment you hit one.
9
+
10
+ ## 1. Agent think-time runs the emulator at full speed
11
+
12
+ **Evidence: live, 01-04 attempt 5. Confidence: HIGH (measured, then reproduced with a control).**
13
+
14
+ Between one screenshot and the next — a handful of memory/register reads, **zero input sent** —
15
+ a game ran through an interstitial to `GAME OVER`. `vice_cycles_stopwatch` read **258,504,308**
16
+ cycles since the previous action: roughly **262 seconds of emulated PAL time**, all of it real
17
+ unattended execution while the agent composed tool calls.
18
+
19
+ This invalidated a previously recorded gameplay-hazard conclusion. A "counter that depletes per
20
+ input event" was really a counter depleting during the agent's own reasoning latency, and the
21
+ earlier finding had no way to rule that out because it never paused either.
22
+
23
+ **The discipline:** call `vice_execution_pause` immediately after every observation that is not
24
+ immediately followed by a deliberate scripted input. Resume only for the bounded duration of that
25
+ input. **Never leave the machine running across a reasoning step.**
26
+
27
+ Two confirmations from the same session: a pause takes effect only once actually processed (the
28
+ counter climbed another ~20M cycles before settling), but once landed it holds solidly — two
29
+ consecutive stopwatch reads returned an identical value. And with the discipline held from the
30
+ first frame, a room previously judged impassable across six attempts was crossed on the next try.
31
+
32
+ ## 2. An armed stopping checkpoint can look exactly like a dead emulator
33
+
34
+ **Evidence: cross-read of three recorded incidents, 3/3 correlation. Confidence: MEDIUM — the
35
+ mechanism is consistent with every symptom and testable in minutes, but has not been reproduced.**
36
+
37
+ The common factor across three "silent stall" incidents was not an address. It was: **a stopping
38
+ exec checkpoint was armed, and execution was resumed.**
39
+
40
+ The signature is complete and self-consistent — the cycle bracket reads exactly `0`, `vice_ping`
41
+ answers `execution: "running"` because VICE's flag flips before the trap fires, and
42
+ `vice_registers_get` returns a byte-identical PC every time **because the machine genuinely never
43
+ moved.** The strongest single tell: in one incident `vice_checkpoint_list` reported the checkpoint
44
+ at `hit_count: 0` after multiple resume/poll cycles, on an IRQ-driven screen where its address
45
+ *must* execute every frame.
46
+
47
+ **`mcp__plugin_c64-re-tools_vice__vice_diagnose` now performs this check for you** (2026-08-04): it enumerates armed
48
+ checkpoints, reads the PC, resolves the live IRQ handler through `$01`, and returns a
49
+ `checkpoint_trap` verdict — with **no resume and no stopwatch call**, which matters because
50
+ `vice_execution_run` is this project's leading crash suspect. Call it before anything else. The
51
+ triage decision tree around it is `vice-wedge-triage`.
52
+
53
+ **Know the two shapes it matches, because a trap outside them reads as a wedge.** It fires when an
54
+ enabled stopping exec checkpoint sits *exactly* at the current PC, or at the resolved handler
55
+ entry with `hit_count` exactly `0`. This hazard's own evidence describes the PC as pinned "at or
56
+ just past" the checkpoint — *just past* is not *exactly at*. A checkpoint armed mid-handler rather
57
+ than at its entry, with a non-zero hit count, matches neither shape, falls through to the cycle
58
+ bracket, and comes back `wedged`. **The response to `wedged` is recycle, and recycling a healthy
59
+ instance is the exact loss both the tool and this hazard exist to prevent.** So when a `wedged`
60
+ verdict arrives with any checkpoint still armed, do the two reads by hand before recycling:
61
+ `vice_checkpoint_list`, then resolve the live handler (`$0314/$0315`, or `$FFFE/$FFFF` when `$01`
62
+ has the ROMs banked out). Filed as
63
+ `.planning/todos/pending/2026-08-04-vice-diagnose-checkpoint-trap-shapes-miss-mid-handler-arming.md`.
64
+
65
+ **Counter-evidence, and why this is MEDIUM:** in one incident, deleting the checkpoint did *not*
66
+ unfreeze the machine, and neither did a soft reset, a hard reset, nor a single step. A checkpoint
67
+ trap may be the *onset* without being the whole story. Do not assume delete-and-resume recovers it.
68
+ `vice_diagnose`'s own `checkpoint_trap` report states this too, and cites the same incident.
69
+
70
+ **This is not a reason to stop arming checkpoints on IRQ handlers** — that is core technique. It
71
+ is a reason to enumerate what you armed *first* whenever the machine looks frozen, before
72
+ concluding the emulator died.
73
+
74
+ ## 3. Registers that clear when you read them
75
+
76
+ `$D01E`, `$D01F` (VIC collisions) and `$DC0D`, `$DD0D` (CIA interrupt flags) clear on read.
77
+ Reading one while the game runs steals the event the game was about to service.
78
+
79
+ Prefer the whole-chip reads — `vice_vicii_get_state`, `vice_cia_get_state`, `vice_sid_get_state`
80
+ — over raw register reads. Whether the VICE monitor's own read path is side-effect-free is
81
+ **unverified**: treat it as verify-don't-assume rather than taking it on faith.
82
+
83
+ ## 4. The keyboard buffer is not how games read keys
84
+
85
+ **Evidence: live. Confidence: HIGH. Cost: an afternoon.**
86
+
87
+ Games and cracks poll the `$DC00`/`$DC01` matrix directly, bypassing the KERNAL buffer, so
88
+ `vice_keyboard_type` is invisible to them. Use `vice_keyboard_matrix`, and hold a key across a
89
+ gate by releasing it at the trigger checkpoint, never earlier.
90
+
91
+ ## 5. Most state reads pause the emulator and do not resume it
92
+
93
+ Read all state first, poll with `vice_ping` (the non-pausing poll), and resume **exactly once** at
94
+ the end. `vice_ping`'s `execution` field is **not** a liveness signal — see hazard 2 for why it
95
+ reports `running` on a machine that is not moving. The only trustworthy liveness test is a cycle
96
+ bracket.
97
+
98
+ ## 6. The machine can be replaced under you
99
+
100
+ A crash-and-respawn mid-session means the run is void — `c64-ram-capture` § Void a run gives the
101
+ procedure. A successful retry after an auto-restart may be talking to a blank machine.
102
+
103
+ **You do not poll for this, and no exposed tool reads the epoch.** The proxy compares it before and
104
+ *after* every forwarded call and raises a loud error naming both values, then re-baselines so the
105
+ session stays usable. The two recorded incidents surfaced exactly that way and self-healed on the
106
+ next call. Where an epoch value is needed for a record, it comes from that error text or from
107
+ `vice_diagnose`'s `restarted` report — there is no `vice_epoch_get`.
108
+
109
+ What is still yours to do: **treat every post-drift read as a fresh machine, never as a resume
110
+ point.** Reboot from `vice_disk_attach`. Two crashes in ~20 minutes of continuous live work is a
111
+ real measured rate, so plan a live session to tolerate re-deriving a boot sequence more than once.
112
+
113
+ ## 7. Full-64K byte-identity is impossible in principle
114
+
115
+ Never-written RAM drifts continuously, so two captures of the same checkpoint will not match
116
+ across all 64K. The recovery procedure is deterministic **for the program image**, not for 64K.
117
+ `c64-ram-capture` § Compare two captures carries the volatility regions and the drift
118
+ discriminator; use them rather than treating any difference as a divergence.
@@ -0,0 +1,128 @@
1
+ # Turning findings into ACME source
2
+
3
+ Do not wait until everything is understood. Stand up a buildable tree early, include the
4
+ unidentified bulk as binary, and replace regions with real source as they are confirmed. The tree
5
+ stays assemblable at every commit, so a regression has a small blast radius.
6
+
7
+ `acme-build` covers assembling; this file covers the shape of the source and what makes a
8
+ reconstruction correct.
9
+
10
+ ## Start with binary inclusion
11
+
12
+ ```asm
13
+ * = $4000
14
+ !binary "unknown_4000_5fff.bin"
15
+ ```
16
+
17
+ Then replace piece by piece, keeping the addresses fixed:
18
+
19
+ ```asm
20
+ * = $4200
21
+
22
+ UpdatePlayer:
23
+ ; reconstructed code
24
+ ```
25
+
26
+ A layout that keeps this workable:
27
+
28
+ ```
29
+ src/
30
+ main.a zeropage.a irq.a
31
+ input.a player.a enemies.a
32
+ collision.a graphics.a music.a
33
+ data/
34
+ sprites.a levels.a text.a
35
+ ```
36
+
37
+ ## Behavioural equivalence is this project's definition of correctness
38
+
39
+ **Correctness here means scripted replay plus comparison at checkpoints — nothing else**
40
+ (`.claude/CLAUDE.md` § Constraints). A reconstruction is right when driving it through the
41
+ checkpoint set produces the same observable behaviour as the original. **Anything not observable
42
+ at a checkpoint is not verified** — the constraint's own conclusion, and the reason checkpoint
43
+ design is part of the reconstruction work rather than something to bolt on afterwards.
44
+
45
+ **That bar is what buys you the freedom to rename routines, reorganise files, replace constants
46
+ with symbols and add macros** (`.planning/REQUIREMENTS.md` § Out of Scope makes the same
47
+ argument). **But** reorganising changes addresses, which breaks self-modifying code and
48
+ timing-sensitive raster routines. Replay through the checkpoint set after EACH reorganisation, not
49
+ at the end of several: one changed address per failing replay is a short diagnosis, ten is a
50
+ bisect.
51
+
52
+ A full 64K RAM capture is not a comparison surface: never-written RAM drifts continuously, so
53
+ full-64K identity is impossible in principle (`observation-hazards.md` § 7).
54
+
55
+ ## Self-modifying code needs explicit labels
56
+
57
+ A static disassembler sees `LDA $FFFF,X` and cannot know the operand is written at runtime:
58
+
59
+ ```asm
60
+ LDA SourceAddress
61
+ STA CopyLoop+1
62
+ LDA SourceAddress+1
63
+ STA CopyLoop+2
64
+ CopyLoop:
65
+ LDA $FFFF,X
66
+ STA $0400,X
67
+ ```
68
+
69
+ Name the patched location so the intent survives:
70
+
71
+ ```asm
72
+ CopySource = CopyLoop + 1
73
+ ```
74
+
75
+ Look for writes to the byte after an opcode, the two bytes after a `JMP`/`JSR`, branch operands
76
+ and immediate constants — `STA Routine+1`, `STX Routine+2`, `INC Routine+1`.
77
+
78
+ ## Label vocabulary: name from evidence, promote on confirmation
79
+
80
+ Provisional names that carry their own uncertainty beat confident names that turn out wrong:
81
+
82
+ | Prefix | Means |
83
+ |---|---|
84
+ | `ZP_` | unknown zero-page variable |
85
+ | `State_` | state variable |
86
+ | `Flag_` | boolean or bit field |
87
+ | `Counter_` | counter |
88
+ | `Ptr_` | pointer |
89
+ | `Table_` | lookup table |
90
+ | `IRQ_` | interrupt routine |
91
+ | `Maybe_` | plausible but unconfirmed |
92
+ | `Unknown_` | no reliable interpretation yet |
93
+
94
+ `Routine_43A2` → `Maybe_UpdatePlayer` → `UpdatePlayerPosition`, promoted only when behaviour is
95
+ confirmed. Avoid `AmazingCollisionRoutine`-style names entirely; they encode a guess as a fact.
96
+
97
+ Record the confidence next to the thing, in the same grammar the project uses everywhere else:
98
+
99
+ ```asm
100
+ ; Confirmed: decremented once per frame while the player is invulnerable.
101
+ ; Evidence: live, watch on $D015 during damage. Confidence: HIGH.
102
+ PlayerInvulnerabilityTimer:
103
+ !byte 0
104
+ ```
105
+
106
+ ## Data tables a linear disassembler destroys
107
+
108
+ The split low/high address table is the classic:
109
+
110
+ ```asm
111
+ AddressLo: !byte <Room0, <Room1, <Room2
112
+ AddressHi: !byte >Room0, >Room1, >Room2
113
+ ```
114
+
115
+ Without recognising the pair, a disassembler shows meaningless instructions. Other data tells:
116
+ referenced through indexed loads, repeated byte patterns, valid PETSCII or screen codes,
117
+ sprite-sized 64-byte blocks, values matching VIC-II coordinates.
118
+
119
+ Code tells: reachable through `JSR`/`JMP`/branches/vectors, **executes during tracing**, plausible
120
+ control flow. The project's standing rule settles ties — a range never hit as an instruction
121
+ stream across full gameplay coverage is data, whatever the tracer guessed.
122
+
123
+ ## Labels round-trip through VICE
124
+
125
+ ACME's `--vicelabels` output and regenerator2000's exported label files share one format, which
126
+ `vice_symbols_load` / `vice_symbols_lookup` consume. Labels therefore flow
127
+ disassembler → source → build → debugger without translation. `acme-build` emits the `.vs` file on
128
+ every build; load it after each one and your checkpoints carry real names.