@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,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.
|