@henols/c64-re-tools 0.2.4 → 1.0.0
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/package.json +2 -2
- package/skills/acme-build/SKILL.md +22 -22
- package/skills/c64-disk-access/SKILL.md +49 -46
- package/skills/c64-memory-mapping/SKILL.md +71 -70
- package/skills/c64-petcat/SKILL.md +11 -11
- package/skills/c64-program-recon/SKILL.md +109 -105
- package/skills/c64-program-recon/references/control-flow.md +13 -13
- package/skills/c64-program-recon/references/graphics.md +6 -6
- package/skills/c64-program-recon/references/observation-hazards.md +7 -7
- package/skills/c64-program-recon/references/reconstruction.md +6 -6
- package/skills/c64-program-recon/references/sound-and-input.md +8 -8
- package/skills/c64-program-recon/references/tool-selection.md +11 -11
- package/skills/c64-program-recon/templates/memory-map.template.md +16 -12
- package/skills/c64-provenance-diff/SKILL.md +43 -43
- package/skills/c64-ram-capture/SKILL.md +96 -45
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +525 -0
- package/skills/c64-ram-capture/templates/capture-record.template.md +11 -11
- package/skills/c64-ram-capture/transients/README.md +11 -10
- package/skills/routine-queue-walker/SKILL.md +205 -176
- package/skills/vice-wedge-triage/SKILL.md +48 -48
|
@@ -5,12 +5,12 @@ description: Work out how an unknown C64 program is structured at runtime — en
|
|
|
5
5
|
|
|
6
6
|
# Reconnaissance on an unknown C64 program
|
|
7
7
|
|
|
8
|
-
**Do not disassemble the whole program first.** The structure hangs off a small
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
**Do not disassemble the whole program first.** The structure hangs off a small set of well-known
|
|
9
|
+
addresses: read them in order and the answer falls out. Treated as a search problem it costs an
|
|
10
|
+
hour every session. Written down, it is minutes.
|
|
11
11
|
|
|
12
|
-
Build a network of
|
|
13
|
-
|
|
12
|
+
Build a network of checked facts. Once you know the vectors, the IRQ handler, the main loop and
|
|
13
|
+
the major tables, everything else classifies far more easily.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
D=src/skills/c64-program-recon/scripts/derive.mjs # from the repo root
|
|
@@ -21,7 +21,7 @@ node $D sprites --dd00 3E --d018 18 --d015 0F --ptrs 20,21,22,23,FF,FF,FF,FF
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
The script does only the arithmetic that a lookup table cannot — register bits to concrete
|
|
24
|
-
addresses — over values **you**
|
|
24
|
+
addresses — over values **you** got through `mcp__plugin_c64-re-tools_vice__*`. It contacts nothing.
|
|
25
25
|
|
|
26
26
|
## Before disassembling anything
|
|
27
27
|
|
|
@@ -36,19 +36,19 @@ either one here:
|
|
|
36
36
|
|
|
37
37
|
## The order
|
|
38
38
|
|
|
39
|
-
Each step is a read whose answer rules something out. Do not skip ahead: step 6 is cheap once
|
|
40
|
-
|
|
39
|
+
Each step is a read whose answer rules something out. Do not skip ahead: step 6 is cheap once you
|
|
40
|
+
know the handler, because that is where most chip writes happen.
|
|
41
41
|
|
|
42
42
|
| # | Question | Read | What the answer settles |
|
|
43
43
|
|---|---|---|---|
|
|
44
|
-
| 0 | Which of these bytes are even the game? | The manifest's buckets — `c64-provenance-diff` | Tracing a depacker's IRQ handler
|
|
44
|
+
| 0 | Which of these bytes are even the game? | The manifest's buckets — `c64-provenance-diff` | Tracing a depacker's IRQ handler wastes work. Scope before you trace |
|
|
45
45
|
| 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 |
|
|
46
46
|
| 2 | Which vector is live? | `$01`, then `$0314/$0315` **or** `$FFFE/$FFFF` | HIRAM (`$01` bit 1) decides. KERNAL out ⇒ the RAM vectors are meaningless |
|
|
47
|
-
| 3 | What drives the frame? | `$D01A`, `$D012`, `$DC0D` | `$DC0D` untouched ⇒ raster IRQ
|
|
47
|
+
| 3 | What drives the frame? | `$D01A`, `$D012`, `$DC0D` | `$DC0D` untouched ⇒ raster IRQ. Programmed ⇒ the game runs its own timebase |
|
|
48
48
|
| 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 |
|
|
49
49
|
| 5 | Code or data? | What the PC actually visits across full coverage | A range never executed is data, whatever a tracer guessed |
|
|
50
|
-
| 6 | Where is the graphics? | `$DD00` → `$D018` → mode bits → `$D015` → VM+`$03F8` | Every
|
|
51
|
-
| 7 | Where is the music? | Watch `$D404` | `init` runs once from main code
|
|
50
|
+
| 6 | Where is the graphics? | `$DD00` → `$D018` → mode bits → `$D015` → VM+`$03F8` | Every byte shown on screen, computed. Nothing to search for |
|
|
51
|
+
| 7 | Where is the music? | Watch `$D404` | `init` runs once from main code. `play` runs once per frame from the IRQ |
|
|
52
52
|
| 8 | Where is the input? | Reads of `$DC00`/`$DC01` | Games poll the matrix directly and ignore the KERNAL buffer |
|
|
53
53
|
|
|
54
54
|
## Step 0 in full: only the game is in scope
|
|
@@ -72,21 +72,20 @@ per-session decision.
|
|
|
72
72
|
`RELEASES.json` `loader_ranges` entry earned from live disassembly, or a depacker stub provably
|
|
73
73
|
dead after first run. A bare printable-ASCII scan classified **the game's own title text** as
|
|
74
74
|
cracktro credit and would have shipped a confidently-wrong `CRACKER-PATCH` verdict.
|
|
75
|
-
- Absence of evidence records `UNKNOWN` and **keeps the bytes**.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
leaves crack residue behind.
|
|
75
|
+
- Absence of evidence records `UNKNOWN` and **keeps the bytes**. A real title-screen text
|
|
76
|
+
divergence was found sitting in a region that is neither loader nor cracktro — a cracker edit
|
|
77
|
+
inside the game's own data. Stripping only the obvious intro screen leaves crack residue behind.
|
|
79
78
|
|
|
80
|
-
`c64-provenance-diff` owns the machinery and the five kinds
|
|
79
|
+
`c64-provenance-diff` owns the machinery and the five kinds. Do not re-derive them here. What
|
|
81
80
|
belongs here is the ordering: **bucket first, then trace only what survives.**
|
|
82
81
|
|
|
83
82
|
Then work **backwards from observable effects** rather than reading code sequentially — it is
|
|
84
|
-
consistently faster. Watch writes to the sprite coordinates to find movement
|
|
85
|
-
the room loader
|
|
83
|
+
consistently faster. Watch writes to the sprite coordinates to find movement. Watch `$D018` to find
|
|
84
|
+
the room loader. Watch VM+`$03F8` to find the animation driver. `vice_watch_add` finds *writers*,
|
|
86
85
|
and that is its real leverage.
|
|
87
86
|
|
|
88
87
|
Differential experiments close the loop: patch a routine to `RTS` and see what stops. If enemies
|
|
89
|
-
freeze and nothing else does, the routine's purpose
|
|
88
|
+
freeze and nothing else does, that checks the routine's purpose — far stronger evidence than
|
|
90
89
|
reading the listing.
|
|
91
90
|
|
|
92
91
|
## Step 0.5: is it packed, and by what?
|
|
@@ -100,7 +99,7 @@ node src/skills/c64-program-recon/scripts/packer-finding.mjs game.prg # fro
|
|
|
100
99
|
node src/skills/c64-program-recon/scripts/packer-finding.mjs game.prg --entropy 7.83
|
|
101
100
|
```
|
|
102
101
|
|
|
103
|
-
Pass `--entropy` when you already have the number from `anno_get_binary_info
|
|
102
|
+
Pass `--entropy` when you already have the number from `anno_get_binary_info`. Otherwise the
|
|
104
103
|
script measures it from the file. It prints one JSON object. Read the `verdict`:
|
|
105
104
|
|
|
106
105
|
| Verdict | What it means | What to do |
|
|
@@ -113,7 +112,7 @@ script measures it from the file. It prints one JSON object. Read the `verdict`:
|
|
|
113
112
|
**A name is reported only when an external oracle stated one, and this project does not guess.**
|
|
114
113
|
No first-party route on this project's surface reports a packer name at all — that was
|
|
115
114
|
established four independent ways, and the acceptance bar a future first-party identifier would
|
|
116
|
-
have to clear was
|
|
115
|
+
have to clear was set at the same time. So there is no code path here that can write a packer
|
|
117
116
|
name from entropy, from a decompression address, or from a byte pattern. If you want a name and the finding does not give
|
|
118
117
|
you one, install an external identifier on the **host** and point `UNP64` or `UNP64_PATH` at it in
|
|
119
118
|
the environment the host broker process sees — do not infer it. `packer-finding.mjs` never spawns
|
|
@@ -127,7 +126,7 @@ only, and the configured path's file name must be the oracle binary's own name (
|
|
|
127
126
|
seam treats it as absent.
|
|
128
127
|
|
|
129
128
|
**The entropy gate answers packedness, not identity.** High entropy tells you the bytes are
|
|
130
|
-
compressed (or encrypted, or genuinely random)
|
|
129
|
+
compressed (or encrypted, or genuinely random). It does not tell you by what. And the way a packed
|
|
131
130
|
image is actually opened up here is the run-and-capture route — run the program under the emulator
|
|
132
131
|
and capture RAM at a checkpoint past the decrunch — not an in-place unpack, which would destroy
|
|
133
132
|
the comments, labels and blocks the project already holds.
|
|
@@ -198,21 +197,21 @@ an exactly-65536-byte flat capture, dispatched **by extension first**, never by
|
|
|
198
197
|
| `anno_set_data_type` | Classifying a block (`code`, `byte`, `address`, `petscii`, …) |
|
|
199
198
|
| `anno_add_scope` | Marking a handler's extent as a lexical scope |
|
|
200
199
|
| `anno_set_comment` | Recording the evidence — the carrier for the confidence grade below |
|
|
201
|
-
| `anno_batch_execute` | Bulk annotation, 5+ independent calls at once — a real memory map is dozens of labels
|
|
200
|
+
| `anno_batch_execute` | Bulk annotation, 5+ independent calls at once — a real memory map is dozens of labels, comments and block ranges. One batch is one open, commit, close instead of dozens. You name the store (and the image, when an inner call needs one) ONCE at the top level, and every inner call inherits it. A malformed payload, an empty `calls` array, an uncurated inner name at any depth, or an illegal label name refuses the **whole** batch by index. It executes nothing. Past that gate, execution runs to completion and each entry carries its own status. So an error entry inside a successful result means that one call did not work |
|
|
202
201
|
|
|
203
202
|
**Grade with the confidence prefix.** Lead every evidence comment with exactly one of these five
|
|
204
203
|
bracket tokens (quoted verbatim from `anno-confidence.ts`, the parser's own source of truth):
|
|
205
204
|
|
|
206
|
-
`[confirmed-code]` (confirmed code), `[probable-code]` (probable code), `[confirmed-data]`
|
|
207
|
-
(confirmed data), `[probable-data]` (probable data), `[unknown]` (unknown).
|
|
205
|
+
`[confirmed-code]` (`confirmed code`), `[probable-code]` (`probable code`), `[confirmed-data]`
|
|
206
|
+
(`confirmed data`), `[probable-data]` (`probable data`), `[unknown]` (`unknown`).
|
|
208
207
|
|
|
209
208
|
A typo in the bracket token — wrong case, an underscore, a plural, stray whitespace — **fails
|
|
210
|
-
loudly
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
loudly**. It does not silently degrade into an ungraded comment. Do not promote a row by editing
|
|
210
|
+
its grade in place. Re-check and restate the evidence with a fresh `anno_set_comment` call, so the
|
|
211
|
+
record of when something stopped being a guess survives.
|
|
213
212
|
|
|
214
213
|
**Query instead of re-deriving.** `anno_get_symbols`, `anno_get_comments` and `anno_get_blocks`
|
|
215
|
-
answer straight from the store
|
|
214
|
+
answer straight from the store. `anno_get_cross_references` and `anno_search` derive their answers
|
|
216
215
|
from the image bytes plus the store's typed ranges, so they take `image` too. `anno_search` searches
|
|
217
216
|
three corpora together — label names, comment text, and the instruction text rendered from every
|
|
218
217
|
range typed `code` — **byte-exact and case-sensitive**, with each corpus named in the answer
|
|
@@ -232,8 +231,8 @@ whole workflow exists to make cheap:
|
|
|
232
231
|
match exactly), every project enum with its variants, and every address-to-enum association.
|
|
233
232
|
|
|
234
233
|
`anno_get_address_details` composes everything known about ONE address — the labels bound there,
|
|
235
|
-
the comments there, the typed range covering it, and the cross-references reaching it. **The
|
|
236
|
-
|
|
234
|
+
the comments there, the typed range covering it, and the cross-references reaching it. **The call
|
|
235
|
+
discloses the composition:** the body carries `composed_client_side` and a `composed_from` list
|
|
237
236
|
naming all four sources, so a composition is never mistaken for something the store held whole.
|
|
238
237
|
|
|
239
238
|
### Take names to the running machine, and bring live findings back
|
|
@@ -241,8 +240,8 @@ naming all four sources, so a composition is never mistaken for something the st
|
|
|
241
240
|
**Dated withdrawal, 2026-08-29 — the `.lbl` round trip is WITHDRAWN, and as of 2026-08-31 no phase
|
|
242
241
|
currently owns its return.** The two CLI verbs that carried it, `export-lbl` and `import-lbl`, are
|
|
243
242
|
gone from this surface: both were delivery paths into the retired static analyser. This notice
|
|
244
|
-
previously forecast that a numbered phase would rebuild them alongside the ACME export route
|
|
245
|
-
forecast was **wrong and
|
|
243
|
+
previously forecast that a numbered phase would rebuild them alongside the ACME export route. That
|
|
244
|
+
forecast was **wrong, and this note fixes it here rather than removes it**. The phase that rebuilt the ACME
|
|
246
245
|
export route covered that route only — no requirement and no success criterion of it mentioned the
|
|
247
246
|
`.lbl` round trip — so the round trip still has no route and **no phase currently owns its return**.
|
|
248
247
|
Do not reach for these verbs here: they do not exist, and an invocation fails with an unknown-verb
|
|
@@ -251,14 +250,14 @@ error and no explanation of why.
|
|
|
251
250
|
The **loop itself is not withdrawn**, only its two automated legs, and the discipline it encodes is
|
|
252
251
|
what to keep doing by hand for as long as they stay gone:
|
|
253
252
|
|
|
254
|
-
1. **The store is the merge point, not your own notes.**
|
|
255
|
-
disassembling the running machine, a checkpoint hit —
|
|
256
|
-
`anno_set_label_name` *first*, before
|
|
253
|
+
1. **The store is the merge point, not your own notes.** Write a name discovered live —
|
|
254
|
+
disassembling the running machine, a checkpoint hit — into the store with
|
|
255
|
+
`anno_set_label_name` *first*, before you carry it anywhere else.
|
|
257
256
|
2. **`vice_symbols_load` REPLACES the machine's symbol table rather than merging into it.** Call it
|
|
258
257
|
**exactly once** per generated `.lbl` file. Loading an older file a second time, after the store
|
|
259
258
|
has moved on, silently discards the newer names.
|
|
260
259
|
3. **Regenerate whole, never patch incrementally.** The round trip regenerated the entire `.lbl`
|
|
261
|
-
from the store, and any rebuild of it must do the same
|
|
260
|
+
from the store, and any rebuild of it must do the same. A hand-written incremental patch
|
|
262
261
|
reintroduces exactly the drift the single merge point exists to prevent.
|
|
263
262
|
|
|
264
263
|
Two traps that survive the withdrawal and are part of the specification whoever eventually rebuilds
|
|
@@ -267,11 +266,11 @@ never appeared in the written file — and neither direction ever created a stor
|
|
|
267
266
|
|
|
268
267
|
`gen-enums` — turning register writes into named enum variants — is **withdrawn on the same terms,
|
|
269
268
|
and no phase currently owns its return either**. The same superseded forecast named a numbered phase
|
|
270
|
-
for it
|
|
271
|
-
the `memmap.json` bit table
|
|
272
|
-
by-hand route that stays open.
|
|
269
|
+
for it. That phase's requirements covered the ACME export oracle only. `c64-memory-mapping`
|
|
270
|
+
documents what `gen-enums` consumed — the `memmap.json` bit table — alongside the withdrawal and
|
|
271
|
+
the by-hand route that stays open.
|
|
273
272
|
|
|
274
|
-
**Generate the memory map
|
|
273
|
+
**Generate the memory map. Do not hand-author it.** Fill in the provenance sidecar (schema and a
|
|
275
274
|
filled example live in `templates/memory-map.template.md`), then:
|
|
276
275
|
|
|
277
276
|
```bash
|
|
@@ -279,58 +278,63 @@ npx -y @henols/vice-mcp anno render-memmap game.annostore --provenance sidecar.j
|
|
|
279
278
|
node <plugin-root>/src/mcp/vice/vice-proxy.ts anno render-memmap game.annostore --provenance sidecar.json
|
|
280
279
|
```
|
|
281
280
|
|
|
282
|
-
Add `--check` to detect drift.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
281
|
+
Add `--check` to detect drift. `--check` reports drift when, and only when, one of these changed:
|
|
282
|
+
|
|
283
|
+
- the rendered file itself (a hand edit)
|
|
284
|
+
- a store row (a range, a label, a comment, or a comment's confidence grade)
|
|
285
|
+
- the provenance sidecar's bytes
|
|
286
|
+
- the location of the store or the sidecar **relative to the workspace root**
|
|
287
|
+
- the renderer
|
|
288
|
+
|
|
289
|
+
**Relocating the checkout is not drift** — the same tree at a different absolute path renders the
|
|
290
|
+
same bytes, because the banner records workspace-relative locations. The rendered file carries a
|
|
291
|
+
generated-file banner. Treat it like every other generated artifact in this repo and never
|
|
292
|
+
hand-edit it.
|
|
289
293
|
|
|
290
294
|
**Dated correction, 2026-08-30 — the paragraph above used to name TWO drift causes, and a third
|
|
291
295
|
existed.** Before gap-closure round 2 the banner recorded the store and the sidecar by their
|
|
292
296
|
ABSOLUTE paths, so the checkout's own location was a silent third cause: an identical store,
|
|
293
297
|
sidecar and rendered file reported `drifted` the moment the tree sat at a different absolute path,
|
|
294
|
-
while `render-memmap` printed the same `render_digest` in both.
|
|
295
|
-
|
|
296
|
-
old two-cause wording
|
|
297
|
-
|
|
298
|
+
while `render-memmap` printed the same `render_digest` in both. Recording workspace-relative
|
|
299
|
+
locations removed that cause, so the cause set above is the one the shipped verb has. This note
|
|
300
|
+
supersedes the old two-cause wording rather than merely rewording it, because a reader meeting it
|
|
301
|
+
in history needs to know which claim was live when.
|
|
298
302
|
|
|
299
303
|
**One-time drift after upgrading, 2026-08-30.** A memory map rendered *before* that change reports
|
|
300
304
|
`drifted` on its first `--check` afterwards, exactly once, because the banner's recorded locations
|
|
301
305
|
changed from absolute to workspace-relative spellings. Re-run the generator and commit the new
|
|
302
306
|
banner. This repository has no committed rendered `memory-map.md` — only the template — so nothing
|
|
303
|
-
here regresses
|
|
307
|
+
here regresses. This sentence is meant for **consuming projects**, which do have one.
|
|
304
308
|
|
|
305
309
|
**This playbook itself has a generated twin, and it is not the one to edit.**
|
|
306
310
|
`installer/skills/c64-program-recon/` is a gitignored COPY of this directory, rebuilt from it by
|
|
307
311
|
`installer/scripts/sync-skills.mjs` on the installer package's `prepack` and by
|
|
308
|
-
`npm --prefix installer run sync-skills`. Edit THIS file
|
|
312
|
+
`npm --prefix installer run sync-skills`. Edit THIS file. Never edit the twin. A hand-edit there is
|
|
309
313
|
overwritten by the next sync and is not independently covered either — the gates that scan the
|
|
310
|
-
shipped tree run the sync before they scan it, so a change made only in the twin
|
|
311
|
-
|
|
314
|
+
shipped tree run the sync before they scan it, so the sync REMOVES a change made only in the twin
|
|
315
|
+
before anything ever measures it. A change made here SHIPS only once that sync has run.
|
|
312
316
|
|
|
313
317
|
**Dated correction, 2026-08-30 — `render-memmap` reads the annotation store directly, and the note
|
|
314
318
|
that used to stand here was WRONG when it shipped.** This verb was rebuilt over the annotation
|
|
315
319
|
store: its positional is an EXISTING `.annostore`, opened
|
|
316
|
-
with `mustExist` — an absent store
|
|
317
|
-
it reaches consults the retired external analyser. The pre-store project file the
|
|
318
|
-
has no producer left in this repository, so there is no route back to the old
|
|
319
|
-
note asserted in the PRESENT TENSE that this verb still read a project file
|
|
320
|
-
when it shipped, and
|
|
321
|
-
can tell which one to believe.
|
|
320
|
+
with `mustExist` — the call refuses an absent store by name rather than creating one — and nothing
|
|
321
|
+
on the path it reaches consults the retired external analyser. The pre-store project file the
|
|
322
|
+
earlier note named has no producer left in this repository, so there is no route back to the old
|
|
323
|
+
spelling. That earlier note asserted in the PRESENT TENSE that this verb still read a project file.
|
|
324
|
+
It was already false when it shipped, and this page REMOVES it here rather than amends it, so a
|
|
325
|
+
reader comparing two dated claims can tell which one to believe.
|
|
322
326
|
|
|
323
327
|
**Importing a Ghidra export, and the mechanical join that follows it.** When a Ghidra harness run
|
|
324
328
|
(a separate, host-side capability) has produced a transfer file, two calls land its findings in the
|
|
325
329
|
store — in this order, and each is one mechanical call, not an agent turn:
|
|
326
330
|
|
|
327
331
|
1. **`anno_import_ghidra_export`** reads the transfer file, writes one cross-reference row per
|
|
328
|
-
surviving reference, and
|
|
329
|
-
`referencesSeen`, `xrefsWritten`, `xrefsAlreadyPresent` (
|
|
330
|
-
double-
|
|
331
|
-
vocabulary, dropped and counted rather than guessed or refused.
|
|
332
|
-
digest-mismatched export
|
|
333
|
-
nothing.
|
|
332
|
+
surviving reference, and REMOVES the transfer file once every write has durably committed. It
|
|
333
|
+
reports `referencesSeen`, `xrefsWritten`, `xrefsAlreadyPresent` (the call deduplicates a
|
|
334
|
+
duplicate reference rather than double-counting it) and `kindsSeenNotImported` — reference kinds
|
|
335
|
+
outside this store's four-member vocabulary, dropped and counted rather than guessed or refused.
|
|
336
|
+
The call refuses a malformed, truncated or digest-mismatched export by name, naming the section
|
|
337
|
+
and the offending line, and writes nothing.
|
|
334
338
|
2. **`anno_join_memmap`** then reads every cross-reference target the store already holds, skips
|
|
335
339
|
addresses inside the program's own loaded image (those are code/data addresses, not hardware
|
|
336
340
|
features), and annotates everything else with the narrowest `c64-memory-mapping/memmap.json` entry
|
|
@@ -341,45 +345,45 @@ store — in this order, and each is one mechanical call, not an agent turn:
|
|
|
341
345
|
Both calls are **mechanical**: there is no agent invocation, no queue walk and no skill invocation
|
|
342
346
|
anywhere inside either one, checked structurally over the two modules' own source rather than
|
|
343
347
|
asserted in prose. Run the import call once per Ghidra export, then the join call once per updated
|
|
344
|
-
image
|
|
348
|
+
image. Neither call takes an agent turn to complete.
|
|
345
349
|
|
|
346
350
|
## Static disassembly
|
|
347
351
|
|
|
348
352
|
**Dated withdrawal 2026-08-29, dated return 2026-08-31 — whole-program ACME export was WITHDRAWN
|
|
349
|
-
and has come back as `anno export-asm`, behind a real-ACME byte-diff oracle.**
|
|
350
|
-
rather than
|
|
351
|
-
turned a `.prg` or a flat 64K image into ACME source offline and settled its own
|
|
352
|
-
transcript parser
|
|
353
|
-
and its correctness
|
|
354
|
-
against the input** — never by an exit code and never by a string match
|
|
355
|
-
output.
|
|
353
|
+
and has come back as `anno export-asm`, behind a real-ACME byte-diff oracle.** This page keeps the
|
|
354
|
+
notice rather than removing it, because the withdrawal explains the shape of what returned. The
|
|
355
|
+
removed verb turned a `.prg` or a flat 64K image into ACME source offline and settled its own
|
|
356
|
+
correctness with a transcript parser. What returned is not a rename of it. This project rebuilds it
|
|
357
|
+
over the **annotation store**, and settles its correctness by **assembling the output with a real
|
|
358
|
+
ACME and diffing the bytes against the input** — never by an exit code and never by a string match
|
|
359
|
+
on the exporter's own output.
|
|
356
360
|
|
|
357
361
|
```bash
|
|
358
362
|
npx -y @henols/vice-mcp anno export-asm game.prg --store game.annostore --out game-src
|
|
359
363
|
node <plugin-root>/src/mcp/vice/vice-proxy.ts anno export-asm game.prg --store game.annostore
|
|
360
364
|
```
|
|
361
365
|
|
|
362
|
-
`<image>` and `--store` are **two separate arguments and neither
|
|
366
|
+
`<image>` and `--store` are **two separate arguments and neither derives from the other**: the
|
|
363
367
|
image supplies the bytes, the store supplies the names, typed ranges and comments. `--out` names a
|
|
364
|
-
**directory** the whole export
|
|
365
|
-
|
|
366
|
-
|
|
368
|
+
**directory** for the whole export, defaulting to the image's basename stem beside the **store**
|
|
369
|
+
rather than beside the image, and the export refuses a non-empty destination rather than
|
|
370
|
+
overwriting it unless you pass `--force`. The directory holds a root file that sources the rest, one
|
|
367
371
|
file per annotation scope, and an `unscoped.a` for any block that lies inside no scope — open the
|
|
368
372
|
root file first to see how the tree fits together.
|
|
369
373
|
|
|
370
374
|
**It writes source and runs no assembler**, and says so in its own second output line
|
|
371
375
|
(`this file has NOT been assembled`). The real-ACME byte-diff is a **test-only** oracle in this
|
|
372
376
|
repository's test suite, absent from the published package and unreachable at runtime — so a clean
|
|
373
|
-
run is evidence that
|
|
374
|
-
statement of that split.
|
|
377
|
+
run is evidence that the export wrote source, not an assembler verdict. `acme-build` carries the
|
|
378
|
+
full statement of that split.
|
|
375
379
|
|
|
376
380
|
Two routes remain for reading a single routine, and they are the ones the rest of this playbook
|
|
377
381
|
already uses:
|
|
378
382
|
|
|
379
383
|
- **`anno_read_region`** and **`anno_disassemble`** render one routine or table at an **explicit**
|
|
380
384
|
inclusive range, decoded fresh from the image bytes on every call and written nowhere. That is
|
|
381
|
-
the static route, bounded on purpose: the combined byte count
|
|
382
|
-
(`ANNO_READ_REGION_MAX_BYTES`), and a wider request
|
|
385
|
+
the static route, bounded on purpose: these calls cap the combined byte count at **4096 bytes**
|
|
386
|
+
(`ANNO_READ_REGION_MAX_BYTES`), and they REFUSE a wider request by name rather than truncating it,
|
|
383
387
|
because a full-64K disassembly dumped into an agent's context is exactly the hazard the cap
|
|
384
388
|
exists to prevent.
|
|
385
389
|
- **`vice_disassemble`** is the live-RAM route this skill's own table above uses: it reads a
|
|
@@ -389,14 +393,14 @@ The two are complementary — reach for the static reads before the emulator is
|
|
|
389
393
|
`vice_disassemble` once you have a live checkpoint to decode from.
|
|
390
394
|
|
|
391
395
|
Extracting a program from a `.d64` image is a separate capability that this repository still does
|
|
392
|
-
not have, and —
|
|
393
|
-
export oracle — **no phase currently owns it**.
|
|
394
|
-
the image explicitly and refuse rather than guess, because a guess could analyse a cracktro
|
|
396
|
+
not have, and — fixing an earlier note that assigned it to the same numbered phase as the ACME
|
|
397
|
+
export oracle — **no phase currently owns it**. Whatever future tool builds it must name the file
|
|
398
|
+
inside the image explicitly and refuse rather than guess, because a guess could analyse a cracktro
|
|
395
399
|
or loader stub instead of the game.
|
|
396
400
|
|
|
397
401
|
## Before you touch the emulator
|
|
398
402
|
|
|
399
|
-
Two hazards cost this project real sessions. Both are in `references/observation-hazards.md
|
|
403
|
+
Two hazards cost this project real sessions. Both are in `references/observation-hazards.md`. These
|
|
400
404
|
two lines are the part you cannot afford to load lazily.
|
|
401
405
|
|
|
402
406
|
- **Pause after every observation.** Agent think-time runs the emulator at full speed — 258 million
|
|
@@ -433,7 +437,7 @@ symbols it touches, is the absorbed pair in `c64-memory-mapping`.
|
|
|
433
437
|
an Exomizer decrunch stub — and how genre informs a guess
|
|
434
438
|
(`check_collision` is a plausible routine in a shooter).
|
|
435
439
|
- With `may_contain_undocumented_opcodes: true`, expect `LAX`, `SAX`, `SLO`,
|
|
436
|
-
`DCP`, `ISC`. These are real instructions, not disassembly errors
|
|
440
|
+
`DCP`, `ISC`. These are real instructions, not disassembly errors. Do not
|
|
437
441
|
stop reading at one.
|
|
438
442
|
|
|
439
443
|
### 2. Bounds, from an explicit address
|
|
@@ -461,12 +465,12 @@ The combined byte count is capped at **4096 bytes** per call
|
|
|
461
465
|
(`ANNO_READ_REGION_MAX_BYTES`) and a request above it is refused by name
|
|
462
466
|
rather than silently truncated. A routine longer than that — rare, but real in
|
|
463
467
|
a decruncher or a level builder — is read as **consecutive ranges**. Read them
|
|
464
|
-
in order
|
|
468
|
+
in order. Do not raise the cap to swallow the whole program, because the cap is
|
|
465
469
|
what keeps a "read this routine" call from becoming a whole-program export.
|
|
466
470
|
|
|
467
471
|
Then read the flow, not just the instructions:
|
|
468
472
|
|
|
469
|
-
- Does it loop? Where does the loop
|
|
473
|
+
- Does it loop? Where does the loop stop?
|
|
470
474
|
- Does it call other routines, or ROM entry points?
|
|
471
475
|
- Does it touch hardware registers?
|
|
472
476
|
|
|
@@ -489,10 +493,10 @@ decisive than the body:
|
|
|
489
493
|
|
|
490
494
|
- Called from an init block → a setup routine, runs once.
|
|
491
495
|
- Called from the main loop → a per-frame update.
|
|
492
|
-
- Called from the IRQ → must be fast
|
|
496
|
+
- Called from the IRQ → must be fast. Likely a music tick or a raster update,
|
|
493
497
|
and its zero-page usage is IRQ-relative.
|
|
494
498
|
- **No callers at all** → not necessarily dead. It may be a dispatch target
|
|
495
|
-
reached through a jump table
|
|
499
|
+
reached through a jump table. Check the nearby data blocks for an address
|
|
496
500
|
table pointing at it.
|
|
497
501
|
|
|
498
502
|
### 5. What data it touches
|
|
@@ -567,8 +571,8 @@ documented earlier on this page.
|
|
|
567
571
|
| Symptom | What it actually is |
|
|
568
572
|
|---|---|
|
|
569
573
|
| No `RTS`/`JMP`/`RTI` at the apparent end | Deliberate fall-through. Check whether the next label is independently called. |
|
|
570
|
-
| `JMP some_routine` as the last instruction | A tail call. This routine ends there
|
|
571
|
-
| Several routines converging on one `RTS` | A shared epilogue. It belongs to none of them
|
|
574
|
+
| `JMP some_routine` as the last instruction | A tail call. This routine ends there. The target is a separate routine. |
|
|
575
|
+
| Several routines converging on one `RTS` | A shared epilogue. It belongs to none of them. Note it in each comment. |
|
|
572
576
|
| No callers, but the routine is clearly live | Reached through a jump table. Look for an address table pointing at it. |
|
|
573
577
|
| Disassembly appears to break mid-routine | Undocumented opcodes. Check the binary-info hint and keep reading. |
|
|
574
578
|
| Zero-page usage contradicts the main program's | The routine runs from the IRQ. Its context is IRQ-relative. |
|
|
@@ -581,8 +585,8 @@ reading tokenized bytes by hand: it wraps VICE's own `petcat` to produce the
|
|
|
581
585
|
readable listing and to report the handover address — or a *named decline*
|
|
582
586
|
(`entrypoint: null` with a reason) when the `SYS` argument is not a static
|
|
583
587
|
value, which is a resolved answer, never a guess to spend a disassembler on.
|
|
584
|
-
|
|
585
|
-
overlapped this skill's frontmatter
|
|
588
|
+
Fixed here: an earlier note claimed none of `c64-petcat`'s trigger phrases
|
|
589
|
+
overlapped this skill's frontmatter. That was true only because the skill did
|
|
586
590
|
not exist yet, and it is superseded now that it does.
|
|
587
591
|
|
|
588
592
|
The material below stays as **reference text only**, narrowed to what
|
|
@@ -599,7 +603,7 @@ layout below.
|
|
|
599
603
|
A tokenised BASIC program is a linked list in memory. Each line is:
|
|
600
604
|
|
|
601
605
|
1. **Bytes 0–1 — next-line pointer.** The address where the *next* line
|
|
602
|
-
|
|
606
|
+
starts, little-endian (`24 04` → `$0424`).
|
|
603
607
|
2. **Bytes 2–3 — line number**, 16-bit little-endian (`0A 00` → `10`).
|
|
604
608
|
3. **Bytes 4–N — the tokens**, running until a `$00` terminator.
|
|
605
609
|
4. **End of program** when a line's next-line pointer is `$00 $00`.
|
|
@@ -677,7 +681,7 @@ This one is the route between the stations. It does not restate what the others
|
|
|
677
681
|
|---|---|
|
|
678
682
|
| `references/control-flow.md` | Entry point, the six vectors, IRQ source, main-loop shapes, state machines |
|
|
679
683
|
| `references/graphics.md` | The VIC derivation chain, the char-ROM shadow trap, sprites, watch targets |
|
|
680
|
-
| `references/sound-and-input.md` | SID player vs `$D41B`-as-RNG vs digi
|
|
684
|
+
| `references/sound-and-input.md` | SID player vs `$D41B`-as-RNG vs digi. CIA#1 vs CIA#2 |
|
|
681
685
|
| `references/observation-hazards.md` | Every way a live read gives a wrong answer. **Read before driving.** |
|
|
682
686
|
| `references/tool-selection.md` | Which `mcp__plugin_c64-re-tools_vice__*` call answers which question, and what to delegate |
|
|
683
687
|
| `references/reconstruction.md` | Binary inclusion, behavioural-equivalence correctness bar, SMC labels, label vocabulary |
|
|
@@ -690,13 +694,13 @@ what was actually known when it was written.
|
|
|
690
694
|
|
|
691
695
|
## Troubleshooting
|
|
692
696
|
|
|
693
|
-
| Symptom |
|
|
697
|
+
| Symptom | Remedy |
|
|
694
698
|
|---|---|
|
|
695
|
-
| `$0314` holds something that is not a plausible address | Check HIRAM. With the KERNAL banked out the RAM vectors are uninitialised
|
|
696
|
-
| Every graphics pointer is wrong, with no error | `$DD00` bits 0-1 are **inverted**. Re-derive the bank first
|
|
699
|
+
| `$0314` holds something that is not a plausible address | Check HIRAM. With the KERNAL banked out the RAM vectors are uninitialised. Read `$FFFE/$FFFF`. |
|
|
700
|
+
| Every graphics pointer is wrong, with no error | `$DD00` bits 0-1 are **inverted**. Re-derive the bank first. Everything else hangs off it. |
|
|
697
701
|
| 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. |
|
|
698
|
-
| A sprite decodes as noise | Check `$D015` first
|
|
702
|
+
| 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. |
|
|
699
703
|
| Computed mode is "INVALID — screen goes black" | You caught the registers mid-update inside a raster split. Re-read. |
|
|
700
704
|
| The emulator looks dead | Enumerate armed checkpoints before anything else. See hazard 2. |
|
|
701
705
|
| `vice_keyboard_type` does nothing | The game polls `$DC00`/`$DC01` directly. `vice_keyboard_matrix` is **permanently unavailable** — see `references/observation-hazards.md` § 4 for the reason and the available alternative. |
|
|
702
|
-
| Two captures of the same checkpoint differ | Expected. Full-64K identity is impossible in principle
|
|
706
|
+
| Two captures of the same checkpoint differ | Expected. Full-64K identity is impossible in principle. Use `c64-ram-capture`'s drift rules. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Control flow: entry point → vectors → IRQ source → main loop → structure
|
|
2
2
|
|
|
3
|
-
Graded 2026-08-01, **MEDIUM**, doc-derived, except where a line says otherwise.
|
|
4
|
-
|
|
3
|
+
Graded 2026-08-01, **MEDIUM**, doc-derived, except where a line says otherwise. This project
|
|
4
|
+
checked the vector-table step against its own captures on 2026-08-04 — see the bottom of this file.
|
|
5
5
|
|
|
6
6
|
## 1. Entry point — three routes, and post-depack is a different question
|
|
7
7
|
|
|
@@ -17,8 +17,8 @@ SP settle into a repeating range across three consecutive batches.
|
|
|
17
17
|
|
|
18
18
|
## 2. Vectors — sweep every block, and `$01` decides which are live
|
|
19
19
|
|
|
20
|
-
Six pairs is not "all vectors". `node derive.mjs vectors <image.bin>` sweeps all six blocks below
|
|
21
|
-
|
|
20
|
+
Six pairs is not "all vectors". `node derive.mjs vectors <image.bin>` sweeps all six blocks below.
|
|
21
|
+
It prints the IRQ/BRK/NMI and hardware blocks by default and takes `--all` for the rest.
|
|
22
22
|
|
|
23
23
|
| Block | Range | Why it matters |
|
|
24
24
|
|---|---|---|
|
|
@@ -38,7 +38,7 @@ loaders that bypass the KERNAL, so a diverted `$0330` is a provenance signal, an
|
|
|
38
38
|
`$0328` is anti-tamper. Neither was being looked for before 2026-08-04.
|
|
39
39
|
|
|
40
40
|
The second tell is the handler's own first instruction: the KERNAL's register-save preamble means
|
|
41
|
-
the KERNAL path is in use
|
|
41
|
+
the KERNAL path is in use. A jump straight into game code means it is not.
|
|
42
42
|
|
|
43
43
|
### A non-default value in a dormant block is not a hook
|
|
44
44
|
|
|
@@ -73,7 +73,7 @@ Run over all six committed captures of one title (two releases, runs 1-3 each):
|
|
|
73
73
|
|
|
74
74
|
| Vector | release-a | release-b | Reading |
|
|
75
75
|
|---|---|---|---|
|
|
76
|
-
| `$FFFE/$FFFF` IRQ | `$1103` | `$1103` | Known
|
|
76
|
+
| `$FFFE/$FFFF` IRQ | `$1103` | `$1103` | Known. Matches the live-established handler chain |
|
|
77
77
|
| `$FFFA/$FFFB` NMI | `$1116` | `$1116` | **New.** The game installs its own NMI handler under KERNAL ROM |
|
|
78
78
|
| `$FFFC/$FFFD` RESET | `$1116` | `$1116` | **New.** RESET funnels to the *same* address as NMI |
|
|
79
79
|
| `$0328/$0329` ISTOP | `$F6FC` | `$F6ED` (stock) | **New, and a divergence.** Residue — the block is dormant |
|
|
@@ -84,13 +84,13 @@ ways a user perturbs a running game, and both land in the same place. `$1116` is
|
|
|
84
84
|
address to checkpoint when the emulator is next available. Testing the RESTORE half of that
|
|
85
85
|
experiment is not currently possible: **`vice_keyboard_restore` is permanently unavailable.** The
|
|
86
86
|
RESTORE key pulses the NMI line directly and is not part of the keyboard matrix, so `KEYBOARD_FEED`
|
|
87
|
-
(which only injects PETSCII text into the buffer) cannot produce it
|
|
87
|
+
(which only injects PETSCII text into the buffer) cannot produce it. Calling the tool returns an
|
|
88
88
|
error naming the reason, rather than pulsing RESTORE. No client-side substitute exists — see
|
|
89
89
|
`docs/stock-hard-losses.md`. The reset half of the experiment remains testable: arm the checkpoint,
|
|
90
90
|
call `vice_machine_reset` soft and hard, and record where the PC actually lands.
|
|
91
91
|
|
|
92
|
-
**Evidence:** derived mechanically from six three-run-verified captures
|
|
93
|
-
across all three runs of its release, so none of it is drift.
|
|
92
|
+
**Evidence:** derived mechanically from six three-run-verified captures. Every value stays
|
|
93
|
+
identical across all three runs of its release, so none of it is drift.
|
|
94
94
|
**Confidence:** HIGH for the values and for the cross-release divergence. The *interpretation* of
|
|
95
95
|
`$1116` as anti-tamper is **LOW** — unexercised, and the perturbation experiment above is exactly
|
|
96
96
|
what would settle it.
|
|
@@ -155,12 +155,12 @@ JumpVector:
|
|
|
155
155
|
|
|
156
156
|
Finding the state variable gives a high-level map of the whole program. A practical route: pause
|
|
157
157
|
at a title screen and again in gameplay, diff the two captures, and look for a single byte that
|
|
158
|
-
|
|
158
|
+
differs in zero page or low RAM. `vice_memory_compare` narrows this. `c64-ram-capture` § Compare
|
|
159
159
|
two captures gives the volatility rules that stop you chasing drift.
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
memory-only snapshot producer at all.
|
|
161
|
+
`vice_memory_compare` serves only `mode: 'ranges'`, which captures the two states at different
|
|
162
|
+
points in time and compares two live ranges. It refuses `mode: 'snapshot'` with an explanatory
|
|
163
|
+
message. There is no memory-only snapshot producer at all.
|
|
164
164
|
|
|
165
165
|
## Verified against this project — 2026-08-04
|
|
166
166
|
|
|
@@ -17,12 +17,12 @@ file deliberately does not restate them.
|
|
|
17
17
|
(`%11` → bank 0 at `$0000`, `%00` → bank 3 at `$C000`). Every other pointer is relative to this
|
|
18
18
|
base, so getting it wrong corrupts the whole chain **silently, with no error to signal it**.
|
|
19
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
|
-
|
|
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
22
|
of the bank the bitmap occupies, and the video matrix then holds colour pairs rather than
|
|
23
23
|
character codes.
|
|
24
24
|
3. **The mode bits** — `$D011` bit 6 (ECM), `$D011` bit 5 (BMM), `$D016` bit 4 (MCM). These decide
|
|
25
|
-
what the bytes *mean
|
|
25
|
+
what the bytes *mean*. Decoding a multicolor sprite as hires produces garbage twice as wide as
|
|
26
26
|
it should be. ECM combined with BMM or MCM is invalid and blanks the screen — if you compute
|
|
27
27
|
that, you probably caught the registers mid-update inside a raster split, so re-read.
|
|
28
28
|
4. **`$D015` — the sprite enable mask. Start here, not at the sprite data.** A disabled sprite's
|
|
@@ -60,7 +60,7 @@ game runs steals the collision the game was about to act on — the worst class
|
|
|
60
60
|
because it discredits the capture without announcing itself. Prefer `vice_vicii_get_state`.
|
|
61
61
|
|
|
62
62
|
Whether the VICE monitor's own read is side-effect-free is **unverified** — treat it as
|
|
63
|
-
|
|
63
|
+
check-don't-assume, not as a settled fact.
|
|
64
64
|
|
|
65
65
|
Many games do software collision anyway: look for coordinate subtraction, comparisons against
|
|
66
66
|
width and height, tile lookups, mask tables and bounding-box arithmetic before concluding the
|
|
@@ -69,10 +69,10 @@ hardware registers are what the game uses.
|
|
|
69
69
|
## Sprite decoding
|
|
70
70
|
|
|
71
71
|
`vice_sprite_get` / `vice_sprite_inspect` do the pointer arithmetic and the multicolor bit-pair
|
|
72
|
-
unpacking.
|
|
72
|
+
unpacking. Check what they return once against a hand-resolved pointer — `derive.mjs sprites`
|
|
73
73
|
gives you that hand resolution — then trust them.
|
|
74
74
|
|
|
75
|
-
On stock, `vice_vicii_get_state`'s `$D018` pointers
|
|
75
|
+
On stock, `vice_vicii_get_state`'s `$D018` pointers come back **bank-relative**.
|
|
76
76
|
`vice_sprite_get` resolves the absolute `screenBase` and per-sprite `dataAddress` for you.
|
|
77
77
|
`vice_sprite_inspect`'s ASCII grid is the sprite's native 24x21 (hi-res) or 12x21 (multicolour)
|
|
78
78
|
**data block** — it is **not** scaled by the `$D017`/`$D01D` expansion bits, so a sprite shown
|