@henols/c64-re-tools 0.2.1 → 0.2.3
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 +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +83 -33
- package/skills/acme-build/scripts/acme.mjs +159 -64
- package/skills/acme-build/template.a +1 -1
- package/skills/c64-disk-access/SKILL.md +156 -0
- package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
- package/skills/c64-memory-mapping/SKILL.md +419 -23
- package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
- package/skills/c64-petcat/SKILL.md +87 -0
- package/skills/c64-petcat/scripts/petcat.mjs +221 -0
- package/skills/c64-program-recon/SKILL.md +497 -92
- package/skills/c64-program-recon/references/control-flow.md +12 -15
- package/skills/c64-program-recon/references/graphics.md +1 -1
- package/skills/c64-program-recon/references/observation-hazards.md +18 -16
- package/skills/c64-program-recon/references/reconstruction.md +11 -6
- package/skills/c64-program-recon/references/sound-and-input.md +6 -8
- package/skills/c64-program-recon/references/tool-selection.md +37 -18
- package/skills/c64-program-recon/scripts/packer-finding.mjs +709 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +27 -13
- package/skills/c64-provenance-diff/SKILL.md +43 -8
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +9 -6
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +20 -8
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +147 -46
- package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +19 -13
- package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
- package/skills/c64-ram-capture/transients/README.md +136 -0
- package/skills/routine-queue-walker/SKILL.md +365 -0
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
- package/skills/vice-wedge-triage/SKILL.md +104 -97
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: c64-memory-mapping
|
|
3
|
-
description: Look up what any C64 address means and turn raw 6502 disassembly into documented assembly, by resolving every address against the C64 memory map, KERNAL ROM routine list, canonical assembler symbols, and per-bit VIC-II/SID/CIA register tables. Use when asked to annotate or comment assembly, document a disassembly listing, or look up an address like $D020, $EA24 or $FFD2.
|
|
3
|
+
description: Look up what any C64 address means and turn raw 6502 disassembly into documented assembly, by resolving every address against the C64 memory map, KERNAL ROM routine list, canonical assembler symbols, and per-bit VIC-II/SID/CIA register tables. Use when asked to annotate or comment assembly against the published memory map, document a disassembly listing by resolving every address it touches, or look up an address like $D020, $EA24 or $FFD2.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# C64 memory mapping & annotated disassembly
|
|
@@ -9,7 +9,7 @@ Look up what a C64 address means, and document a 6502 listing by resolving every
|
|
|
9
9
|
address it touches. One script does both, offline, anywhere Node ≥18 runs:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
D
|
|
12
|
+
D=src/skills/c64-memory-mapping/scripts/driver.mjs # relative to the repo root
|
|
13
13
|
|
|
14
14
|
node $D lookup '$D011' '$FFD2' # what lives at an address
|
|
15
15
|
node $D annotate --file game.asm # document a listing or .asm file
|
|
@@ -182,44 +182,440 @@ combined (driver.mjs:262, 268) — so a partially-reachable or partially-changed
|
|
|
182
182
|
source set can silently replace good tracked data with less of it. Rebuild, then
|
|
183
183
|
run `git diff --stat` on `memmap.json` before accepting the result, and
|
|
184
184
|
`git checkout` the file if the diff is not explainable as the correction you
|
|
185
|
-
were expecting.
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
were expecting. **`memmap` mutates tracked files** — unlike `lookup` and
|
|
186
|
+
`annotate`, which are read-only — so run it deliberately, on a clean working
|
|
187
|
+
tree, and review the diff before you commit it.
|
|
188
188
|
|
|
189
189
|
## Feeding the enum generator
|
|
190
190
|
|
|
191
191
|
`memmap.json`'s structured `bits` entries are the source of the curated register bit-name table used
|
|
192
|
-
to generate program-specific enums for
|
|
192
|
+
to generate program-specific enums for this project's annotation store: register
|
|
193
193
|
writes disassemble as `lda #D011_YSCROLL3_ROW25_SCREENON_TEXT` instead of a bare `#$1b`. The
|
|
194
|
-
generator is
|
|
195
|
-
|
|
196
|
-
`node
|
|
194
|
+
generator is `src/mcp/vice/anno-regbits-gen.ts`; its committed output is
|
|
195
|
+
`src/mcp/vice/anno-regbits.json`; and that output is **digest-pinned** to `memmap.json` — a
|
|
196
|
+
`node anno-regbits-gen.ts` run compares its own fresh build against the committed file, and CI fails
|
|
197
197
|
if `memmap.json` changed without a re-run.
|
|
198
198
|
|
|
199
199
|
**The honest gap:** only 29 of this file's 959 entries carry a structured `bits` array. `$D015`,
|
|
200
200
|
`$D017`, `$D01A` and `$D01B`–`$D01D` — the sprite-plane bitmask registers a real game writes
|
|
201
201
|
constantly — are **not** among those 29, so the enum generator supplies them from its own curated
|
|
202
|
-
override table (`OVERRIDES` in `
|
|
202
|
+
override table (`OVERRIDES` in `anno-regbits-gen.ts`), not from this file. Widening `memmap.json`'s
|
|
203
203
|
`io` parser (or repairing the OCR damage already present in some `bits` prose, e.g. a letter `O` for
|
|
204
204
|
the digit `0`) so those registers get a real structured entry here is separate work belonging to this
|
|
205
205
|
skill, not the generator.
|
|
206
206
|
|
|
207
|
-
**Installing those bit names into a
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
**Installing those bit names into a program's own disassembly — dated withdrawal, 2026-08-29.** The
|
|
208
|
+
table above only builds `anno-regbits.json`. Turning a specific program's register *writes* into
|
|
209
|
+
named enum variants was the `gen-enums` CLI verb, and **that verb is WITHDRAWN from this surface, and
|
|
210
|
+
no phase currently owns its return.** This notice previously forecast that it would come
|
|
211
|
+
back as a rebuild over the annotation store alongside the ACME export route; the ACME export route
|
|
212
|
+
did come back on 2026-08-31, but the phase that rebuilt it covered that route only — no requirement
|
|
213
|
+
and no success criterion of it mentioned `gen-enums`. That forecast is corrected here rather than
|
|
214
|
+
deleted. Do not invoke the verb — it does not exist, and the invocation fails with an unknown-verb
|
|
215
|
+
error and nothing to explain it.
|
|
216
|
+
|
|
217
|
+
What it did, so the rebuild has a specification and so a reader knows what is missing: it read the
|
|
218
|
+
program's own disassembly, created one enum variant per DISTINCT value actually written at each
|
|
219
|
+
matching immediate-load address (named from the curated table above), and printed
|
|
220
|
+
total/paired/unpaired register-store counts plus a per-enum variant count.
|
|
221
|
+
|
|
222
|
+
**The by-hand route is open in the meantime, and it is the same one this skill already documents
|
|
223
|
+
for any other enum.** `anno_create_project_enum` defines the variants and `anno_apply_enum_usage`
|
|
224
|
+
binds one to the accessing instruction's address — see "Name it" and "Document it" below. That is
|
|
225
|
+
manual where `gen-enums` was bulk, but it writes exactly the same rows into the store.
|
|
226
|
+
|
|
227
|
+
## Reference material for annotating
|
|
228
|
+
|
|
229
|
+
Two deeper jobs feed the annotate/lookup job above, rather than standing apart from it: knowing
|
|
230
|
+
what a whole region actually is (code, or one of eight kinds of data) before it can be annotated
|
|
231
|
+
at all, and knowing what one of a program's own addresses represents when no published table can
|
|
232
|
+
say. Both exist to serve a documented listing; neither is invoked as a job on its own.
|
|
233
|
+
|
|
234
|
+
### Classifying every region of an annotation project
|
|
235
|
+
|
|
236
|
+
Everything above answers *what does this published address mean*. This section
|
|
237
|
+
answers a different question over the same map: **given a loaded binary in an
|
|
238
|
+
annotation project, what is each region of it — code, or one of eight kinds of
|
|
239
|
+
data?**
|
|
240
|
+
|
|
241
|
+
**This is the static answer, taken from bytes on disk.** `c64-program-recon`
|
|
242
|
+
answers the *live* code-versus-data question — its step 5 is "what the PC
|
|
243
|
+
actually visits across full coverage", and an execution trace beats every
|
|
244
|
+
static heuristic on this page. Run the live pass when you have a running
|
|
245
|
+
machine; run this one when all you have is a file, and treat a later trace as
|
|
246
|
+
the thing that overrules it.
|
|
247
|
+
|
|
248
|
+
When a binary is first loaded, the auto-analyzer traces reachable code from the
|
|
249
|
+
entry point and marks it **Code**. Everything else is **Undefined** — not
|
|
250
|
+
"data", just unexplored. The job here is to walk the Undefined regions, work
|
|
251
|
+
out what each one actually is, and set it.
|
|
252
|
+
|
|
253
|
+
#### The one mistake that matters more than the rest
|
|
254
|
+
|
|
255
|
+
**Never disassemble a region without concrete proof that it executes.** Random
|
|
256
|
+
data routinely disassembles into plausible-looking instruction sequences, and
|
|
257
|
+
that is *not* evidence of code — it is a property of the 6502's dense opcode
|
|
258
|
+
map. A region earns the Code type only when at least one of these holds:
|
|
259
|
+
|
|
260
|
+
- **It is a `JSR`/`JMP` target.** Already-analysed code contains `JSR $addr` or
|
|
261
|
+
`JMP $addr` landing in it. Check with `anno_get_cross_references` — it names
|
|
262
|
+
the `image` as well as the `store`, takes a REQUIRED `max_results`, and unions
|
|
263
|
+
three sources on every call without caching any of them: the instructions
|
|
264
|
+
decoded fresh out of every range typed `code`, the typed split ADDRESS tables
|
|
265
|
+
(the `_address` forms produce cross-references and the `_word` forms do not),
|
|
266
|
+
and the stored rows, which exist because a computed dispatch cannot be
|
|
267
|
+
recovered from bytes at all.
|
|
268
|
+
- **It is a branch target** of an already-analysed `BNE`/`BEQ`/`BCC`/`BCS`/
|
|
269
|
+
`BPL`/`BMI`/`BVC`/`BVS`.
|
|
270
|
+
- **It is a vector or handler**: its address appears in a vector table
|
|
271
|
+
(`$FFFA`–`$FFFF`, `$0314`–`$0319`), in an `address` or split-address block,
|
|
272
|
+
or in a jump table reached by `JMP ($addr)`.
|
|
273
|
+
- **A human says so explicitly.**
|
|
274
|
+
|
|
275
|
+
None of those? Leave it **Undefined**, or classify it as data — even when the
|
|
276
|
+
bytes disassemble cleanly. "It looked like code" is how a sprite sheet becomes
|
|
277
|
+
four hundred lines of fiction.
|
|
278
|
+
|
|
279
|
+
#### The order of the passes
|
|
280
|
+
|
|
281
|
+
Work the Undefined blocks in four passes, in this order. Do not interleave
|
|
282
|
+
them; each pass makes the next one cheaper.
|
|
283
|
+
|
|
284
|
+
1. **Provably-reachable code** — read from the entry point of each region that
|
|
285
|
+
meets the proof bar above with `anno_disassemble`, then type the range you
|
|
286
|
+
actually verified as `code` with `anno_set_data_type`. **Reading and typing
|
|
287
|
+
are two calls on this surface, and that is deliberate:** `anno_disassemble`
|
|
288
|
+
decodes fresh from the image bytes and writes nothing at all, so nothing is
|
|
289
|
+
ever classified as code by a decoder's guess — the boundary you record is
|
|
290
|
+
the one you read and judged.
|
|
291
|
+
2. **Text** — PETSCII and screencode strings.
|
|
292
|
+
3. **Tables** — byte, word, address and split (lo/hi, hi/lo) tables.
|
|
293
|
+
4. **Whatever is left** — decide data, or leave it Undefined for a human.
|
|
294
|
+
**Never** speculatively disassemble in this pass; by definition nothing here
|
|
295
|
+
met the proof bar.
|
|
296
|
+
|
|
297
|
+
#### Scope, and reading a region
|
|
298
|
+
|
|
299
|
+
1. `anno_get_binary_info` first. Keep `origin`, `size`, `system`, `filename`,
|
|
300
|
+
`description` and `may_contain_undocumented_opcodes`.
|
|
301
|
+
- `system` names the target machine. On a C64 the rest of this skill *is*
|
|
302
|
+
the memory map you need — `lookup` any address a region touches before
|
|
303
|
+
guessing at it.
|
|
304
|
+
- `filename` and `description` are the software context. A known title, a
|
|
305
|
+
known music driver or a known packer changes what a region is likely to
|
|
306
|
+
be.
|
|
307
|
+
- `may_contain_undocumented_opcodes: true` means illegal opcodes (`LAX`,
|
|
308
|
+
`SAX`, `SLO`, `DCP`, `ISC`) may appear. **Do not misclassify those as
|
|
309
|
+
data** — they are valid instructions. The flag is a human's hint, not a
|
|
310
|
+
guarantee: some programs use them with the flag false.
|
|
311
|
+
2. `anno_get_blocks` to see what is already classified, and focus on the
|
|
312
|
+
Undefined entries. `max_results` is REQUIRED with no default; pass a ceiling
|
|
313
|
+
above the range count you expect and compare the returned count against it.
|
|
314
|
+
3. Read each candidate region twice, through `anno_read_region` (which names
|
|
315
|
+
both the `store` and the `image`): `view:
|
|
316
|
+
"hexdump"` shows the byte patterns, and **omitting `view`** gives the
|
|
317
|
+
disassembly view — that is the parameter's documented default — which shows
|
|
318
|
+
how the region would decode. The combined byte count is capped at **4096 bytes** per call
|
|
319
|
+
(`ANNO_READ_REGION_MAX_BYTES`), and a request above the cap is refused by
|
|
320
|
+
name rather than truncated — so walk a large binary in consecutive ranges.
|
|
321
|
+
Chunks of **256–512 bytes** are the practical working size for
|
|
322
|
+
classification; a 4096-byte hexdump is more than can be read carefully in
|
|
323
|
+
one pass.
|
|
324
|
+
|
|
325
|
+
#### Applying the classification
|
|
326
|
+
|
|
327
|
+
- **Code**: `anno_disassemble` from the entry-point address to READ, then
|
|
328
|
+
`anno_set_data_type` with `"code"` to RECORD the range you verified.
|
|
329
|
+
`anno_disassemble` performs no write, so nothing is classified until you say
|
|
330
|
+
so. Type only as far as you actually followed the flow — the end of a routine
|
|
331
|
+
at its `RTS`/`RTI`/`JMP`, not "to the end of the region" — because the typed
|
|
332
|
+
`code` ranges are what `anno_get_cross_references` and `anno_search` decode
|
|
333
|
+
instructions out of later.
|
|
334
|
+
- **Data**: batch the `anno_set_data_type` calls through
|
|
335
|
+
`anno_batch_execute`. A real classification pass is dozens of ranges, and one
|
|
336
|
+
batch is one open/commit/close rather than dozens.
|
|
337
|
+
- **A wrong classification is not a disaster and does not need undoing.**
|
|
338
|
+
`anno_set_data_type` is idempotent over a range: set the correct type again
|
|
339
|
+
over the same range and the previous one is gone, and an identical repeat
|
|
340
|
+
succeeds reporting `changed: false`. (Upstream reaches for an undo call here;
|
|
341
|
+
this project does not expose one, and does not need to.)
|
|
342
|
+
- **Read the disclosures on a successful retype; they are not errors and they
|
|
343
|
+
are never dropped.** `contradictedComments` names comments whose recorded
|
|
344
|
+
confidence now contradicts the type you just applied, and
|
|
345
|
+
`reinterpretedSplitTables` names every split table the write FRAGMENTED, with
|
|
346
|
+
the entry-address pairs it read before and the pairs each surviving remainder
|
|
347
|
+
reads now. A split table's entries re-pair as a function of the row's start
|
|
348
|
+
**and** its length, so a fragment decodes to different 16-bit values than the
|
|
349
|
+
ones a human recorded.
|
|
350
|
+
- A split layout REFUSES an odd byte count — the low half and the high half must
|
|
351
|
+
be the same length.
|
|
352
|
+
- Re-read `anno_get_blocks` after each batch to confirm what actually landed.
|
|
353
|
+
`max_results` is REQUIRED on that read and has no default; the true match
|
|
354
|
+
count is returned beside the list.
|
|
355
|
+
|
|
356
|
+
Example of a valid data-only batch, then the code regions separately:
|
|
211
357
|
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
|
|
358
|
+
```
|
|
359
|
+
anno_batch_execute:
|
|
360
|
+
- anno_set_data_type start=2304 end=2367 data_type="byte"
|
|
361
|
+
- anno_set_data_type start=2368 end=2431 data_type="petscii"
|
|
362
|
+
then: anno_disassemble address=2049 # read, writes nothing
|
|
363
|
+
then: anno_set_data_type start=2049 end=2303 data_type="code"
|
|
364
|
+
then: anno_get_blocks max_results=500 # refresh
|
|
215
365
|
```
|
|
216
366
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
367
|
+
#### The block types
|
|
368
|
+
|
|
369
|
+
| Block type | `data_type` | When |
|
|
370
|
+
|---|---|---|
|
|
371
|
+
| **Code** | `code` | Provably-executed instructions. Read the extent with `anno_disassemble` first — it writes nothing — then record exactly what you verified. |
|
|
372
|
+
| Byte | `byte` | Raw 8-bit data: sprites, bitmaps, charsets, lookup tables, variables, unknowns |
|
|
373
|
+
| Word | `word` | 16-bit little-endian values: 16-bit variables, math constants, SID frequencies |
|
|
374
|
+
| Address | `address` | 16-bit LE pointers — jump tables, vector lists. Creates cross-references |
|
|
375
|
+
| PETSCII text | `petscii` | PETSCII strings: messages, prompts, anything bound for `$FFD2` |
|
|
376
|
+
| Screencode text | `screencode` | Text written straight to screen RAM (`$0400`–`$07E7`) |
|
|
377
|
+
| Lo/Hi address | `lo_hi_address` | Split address table, low bytes first. Even byte count required |
|
|
378
|
+
| Hi/Lo address | `hi_lo_address` | Split address table, high bytes first. Even byte count required |
|
|
379
|
+
| Lo/Hi word | `lo_hi_word` | Split word table, low half first — e.g. a SID frequency table |
|
|
380
|
+
| Hi/Lo word | `hi_lo_word` | Split word table, high half first |
|
|
381
|
+
| External file | `external_file` | Large blobs to export as-is: SID tunes, bitmaps, charsets |
|
|
382
|
+
| Undefined | `undefined` | Reset to unknown. The honest answer for a region you cannot place |
|
|
383
|
+
|
|
384
|
+
#### Recognising each kind
|
|
385
|
+
|
|
386
|
+
**Byte data** — regular patterns that form no valid instruction sequence;
|
|
387
|
+
addressed by `LDA addr,X` / `LDA addr,Y` table lookups; sprite data in 63-byte
|
|
388
|
+
(padded to 64) units, usually grouped; bitmap data in 8-byte character cells;
|
|
389
|
+
colour data confined to `$00`–`$0F`; or random-looking bytes between two code
|
|
390
|
+
blocks whose disassembly is nonsense.
|
|
391
|
+
|
|
392
|
+
**Word data** — byte pairs forming meaningful 16-bit values (screen addresses,
|
|
393
|
+
timer values); loaded low-then-high by adjacent `LDA addr` / `LDA addr+1`.
|
|
394
|
+
|
|
395
|
+
**Address tables** — byte pairs that read as little-endian addresses landing
|
|
396
|
+
*inside* the binary; reached by `JMP ($addr)` or indexed indirect reads. Jump
|
|
397
|
+
tables, dispatch tables and vector lists all live here.
|
|
398
|
+
|
|
399
|
+
**Split lo/hi (or hi/lo) tables** — two equal halves, one of plausible low
|
|
400
|
+
bytes and one of plausible high bytes, referenced separately:
|
|
401
|
+
`LDA lo,X / STA ptr / LDA hi,X / STA ptr+1 / JMP (ptr)`. Recombine the halves
|
|
402
|
+
and check the addresses are real. Lo/Hi (low half first) is the commoner form
|
|
403
|
+
on the 6502. **The total byte count must be even and the halves equal** — an
|
|
404
|
+
odd count means the boundary is in the wrong place.
|
|
405
|
+
|
|
406
|
+
**PETSCII text** — bytes in `$20`–`$7E` (unshifted) or `$C0`–`$DF` (shifted),
|
|
407
|
+
often recognisably English since PETSCII shares `$20`–`$5F` with ASCII;
|
|
408
|
+
terminated by `$00`, `$0D`, or a high-bit sentinel; reached by `$FFD2` (CHROUT)
|
|
409
|
+
or `$AB1E` (BASIC STROUT). `GAME OVER`, `PRESS FIRE`, menus, credits.
|
|
410
|
+
|
|
411
|
+
**Screencode text** — bytes in `$00`–`$3F` where `$00` is `@` and `$01` is `A`;
|
|
412
|
+
copied directly to `$0400`–`$07E7`. `LDA data,X / STA $0400,X` is the
|
|
413
|
+
give-away. A full screen dump is exactly 1000 bytes.
|
|
414
|
+
|
|
415
|
+
**External file** — a large contiguous non-code block matching a known format:
|
|
416
|
+
a `PSID`/`RSID` header, a 2048-byte charset (256 chars × 8 bytes), sprite data
|
|
417
|
+
in multiples of 64, or a bitmap. Export it rather than annotate it.
|
|
418
|
+
|
|
419
|
+
**PETSCII is not screencode.** If it is copied to `$0400`, it is screencode; if
|
|
420
|
+
it is passed to CHROUT, it is PETSCII. Getting this backwards produces text
|
|
421
|
+
that renders as garbage in exactly one of the two places.
|
|
422
|
+
|
|
423
|
+
#### The adjacent-table limitation, and how it was closed
|
|
424
|
+
|
|
425
|
+
**Dated limitation, recorded 2026-08-24; CLOSED 2026-08-29 when the store
|
|
426
|
+
changed underneath it.** The old store auto-merged two adjacent regions of the
|
|
427
|
+
*same* type into one block, so two byte tables side by side — or the two halves
|
|
428
|
+
of a split table sitting next to each other — lost their boundary on read, and
|
|
429
|
+
the answer upstream reached for was a `toggle_splitter` call this project never
|
|
430
|
+
exposed.
|
|
431
|
+
|
|
432
|
+
**The annotation store does not do that.** It never joins two rows of its own
|
|
433
|
+
accord: adjacent same-type ranges stay two rows, and `anno_get_blocks` reports
|
|
434
|
+
them as two. A range you name that *spans* several existing rows still collapses
|
|
435
|
+
to the one row you asked for — that is you asking, not the store deciding — and
|
|
436
|
+
partial overlaps split and preserve the addresses outside your range rather than
|
|
437
|
+
swallowing them.
|
|
438
|
+
|
|
439
|
+
So the working rules are now the ordinary ones:
|
|
440
|
+
|
|
441
|
+
- **Do** rely on the block listing to tell two adjacent same-type tables apart.
|
|
442
|
+
It can, provided you typed them as two calls rather than one spanning call.
|
|
443
|
+
- **Do** still record the boundary in the annotations as well — an
|
|
444
|
+
`anno_set_label_name` at the start of the second table and a line comment on
|
|
445
|
+
both naming the extent you determined. A name and an evidence line survive a
|
|
446
|
+
later retype; a row boundary does not.
|
|
447
|
+
- **Do not** carry the old over-merge caveat into a report taken from this
|
|
448
|
+
store. It was true of the retired one and is not true here.
|
|
449
|
+
|
|
450
|
+
#### Labelling, and the report
|
|
451
|
+
|
|
452
|
+
Name what you classified — `anno_set_label_name` on entry points, tables and
|
|
453
|
+
strings — and comment it with `anno_set_comment` (`"line"` above,
|
|
454
|
+
`"side"` beside). For the conventions to name things *by*, and for the
|
|
455
|
+
comment-block format to use on a subroutine, follow the absorbed routine
|
|
456
|
+
procedure in `src/skills/c64-program-recon/SKILL.md`.
|
|
457
|
+
|
|
458
|
+
Then report, and mean it:
|
|
459
|
+
|
|
460
|
+
- Total blocks by type.
|
|
461
|
+
- Notable findings — "three PETSCII strings", "a lo/hi jump table at `$1200`".
|
|
462
|
+
- **Every region still uncertain or still Undefined, by address.** This is the
|
|
463
|
+
part that makes the pass reusable. A classification report with no uncertain
|
|
464
|
+
regions on a real game is almost always a report that stopped looking.
|
|
465
|
+
- The store revision, read with `anno_save_project`. That verb performs **no
|
|
466
|
+
write** — every classification call above already committed and fsynced its
|
|
467
|
+
own — so quoting the revision is how the report is pinned to an exact store
|
|
468
|
+
state rather than to "after the pass".
|
|
469
|
+
|
|
470
|
+
#### What goes wrong
|
|
471
|
+
|
|
472
|
+
| Symptom | What it actually is |
|
|
473
|
+
|---|---|
|
|
474
|
+
| A region disassembles beautifully but has no incoming reference | Data. Decodability is not evidence; leave it Undefined. |
|
|
475
|
+
| Disassembly full of impossible branches or `BRK` (`$00`) floods | Data misread as code. |
|
|
476
|
+
| Odd-looking instructions, but real `JSR`/`JMP` cross-references land here | Probably code using undocumented opcodes. Check the `may_contain_undocumented_opcodes` hint. |
|
|
477
|
+
| A split table's addresses recombine to nonsense | The half boundary is misplaced, or the table is hi/lo rather than lo/hi. |
|
|
478
|
+
| Two tables you classified separately show up as one block | The adjacent-table limitation above. Not your error. |
|
|
479
|
+
| Text renders as garbage on screen but fine through CHROUT | It is PETSCII, typed as screencode — or the reverse. |
|
|
480
|
+
|
|
481
|
+
### What a symbol in the store actually represents
|
|
482
|
+
|
|
483
|
+
`lookup` at the top of this page answers what a **published** address means —
|
|
484
|
+
a hardware register, a KERNAL entry point, an OS variable. That answer comes
|
|
485
|
+
from four tables and holds for every program.
|
|
486
|
+
|
|
487
|
+
This section is the other half: **what a program's *own* address represents.**
|
|
488
|
+
No table can tell you, because the meaning was decided by the program's code.
|
|
489
|
+
When `lookup` returns a region-only answer — the dominant case for a game's own
|
|
490
|
+
code and variables, as noted above — this is the procedure that gets you a
|
|
491
|
+
name.
|
|
492
|
+
|
|
493
|
+
#### 1. Target and context
|
|
494
|
+
|
|
495
|
+
- **Always start from an explicit address**, `$XXXX` or its decimal
|
|
496
|
+
equivalent. There is no editor cursor in this project's route, and upstream's
|
|
497
|
+
own text forbids relying on one anyway. `anno_get_address_details` composes
|
|
498
|
+
the symbol, comments, block type and cross-references for one explicit
|
|
499
|
+
address in a single call.
|
|
500
|
+
- `anno_get_binary_info` for `system`, `filename`, `description` and
|
|
501
|
+
`may_contain_undocumented_opcodes`. `filename` and `description` are how a
|
|
502
|
+
symbol gets a *domain* name — `lap_counter` in a racing game, `lives` in a
|
|
503
|
+
platformer — instead of a generic one. With undocumented opcodes in play,
|
|
504
|
+
remember that `LAX`, `SAX` and `DCP` have real read/write side effects that
|
|
505
|
+
belong in the data-flow picture.
|
|
506
|
+
|
|
507
|
+
#### 2. Gather the usage
|
|
508
|
+
|
|
509
|
+
`anno_get_cross_references` on the address — naming the `store`, the `image` and
|
|
510
|
+
a REQUIRED `max_results` — returns everywhere it is touched. Read the
|
|
511
|
+
instruction at each site, because the instruction is the evidence:
|
|
512
|
+
|
|
513
|
+
- **Writes**: `STA`, `STX`, `STY`
|
|
514
|
+
- **Reads**: `LDA`, `LDX`, `LDY`, `BIT`, `CMP`, `CPX`, `CPY`, `ADC`, `SBC`
|
|
515
|
+
- **Read-modify-write**: `INC`, `DEC`, `ASL`, `LSR`, `ROL`, `ROR`
|
|
516
|
+
|
|
517
|
+
**Zero cross-references is a result, not a dead end.** Three explanations, in
|
|
518
|
+
order of likelihood:
|
|
519
|
+
|
|
520
|
+
1. It is reached **indirectly**. Check whether it is in the zero page
|
|
521
|
+
(`$00`–`$FF`) and whether nearby code uses `($addr),Y` or `($addr,X)`. An
|
|
522
|
+
indirect pointer's *target* has no direct reference by construction.
|
|
523
|
+
2. It is a **well-known system address** the disassembler does not cross-
|
|
524
|
+
reference. Run `lookup` on it — that is exactly the case the four tables
|
|
525
|
+
above cover.
|
|
526
|
+
3. It is genuinely **dead**: unused variable, or code no longer reached. Say so
|
|
527
|
+
in the report rather than inventing a purpose.
|
|
528
|
+
|
|
529
|
+
#### 3. Place it
|
|
530
|
+
|
|
531
|
+
**A hardware register?** `lookup` it. If one of the four tables names it, take
|
|
532
|
+
the published name and the per-bit breakdown with it — that reading rests on
|
|
533
|
+
hardware and holds for any program.
|
|
534
|
+
|
|
535
|
+
**Inside a well-known global block?** Screen RAM (`$0400`–`$07E7`), colour RAM
|
|
536
|
+
(`$D800`–`$DBFF`), a sprite pointer at VM+`$03F8`. **Do not skip these as
|
|
537
|
+
"obvious".** Name them systematically from the base plus the offset —
|
|
538
|
+
`SCREEN_ROW03_COL12` — so contiguous structures read as structures instead of a
|
|
539
|
+
field of auto-generated offsets.
|
|
540
|
+
|
|
541
|
+
**An external ROM or system routine?** An `e_` prefix, or an address in KERNAL
|
|
542
|
+
space (`$E000`–`$FFFF`), or a standard shadow vector. `lookup` gives the
|
|
543
|
+
routine's published name; rename to the conventional form — `$FFD2` becomes
|
|
544
|
+
`KERNAL_CHROUT`, `$EA31` becomes `SYSTEM_IRQ_HANDLER`.
|
|
545
|
+
|
|
546
|
+
**A 16-bit pointer?** Below `$0100`, and used with indirect-indexed `($xx),Y`
|
|
547
|
+
or indexed-indirect `($xx,X)`. Rename to `ptr_`/`vec_` form and comment what it
|
|
548
|
+
points *to*, which is the thing the name cannot carry.
|
|
549
|
+
|
|
550
|
+
**A flag or bitmask?** Only ever `$00`/`$01` or `$00`/`$FF`; tested with `BIT`
|
|
551
|
+
or `LDA`/`BEQ`. Name it as a predicate — `is_active`, `has_collided`. When the
|
|
552
|
+
individual bits carry separate meanings, that is an enum: define it with
|
|
553
|
+
`anno_create_project_enum` (`$01 = ACTIVE`, `$02 = COLLIDED`, `$04 =
|
|
554
|
+
VISIBLE`) and apply it with `anno_apply_enum_usage` so every bitmask test
|
|
555
|
+
reads as words rather than hex.
|
|
556
|
+
|
|
557
|
+
**A counter or index?** `INC`/`DEC` inside a loop, compared against a limit
|
|
558
|
+
with `CPX`/`CPY`/`CMP`. `loop_idx`, `sprite_count`, `delay_timer`.
|
|
559
|
+
|
|
560
|
+
**A state variable?** Several distinct values, often feeding a dispatch
|
|
561
|
+
(`ASL` / `TAX` / `JMP (table,X)`). Name it `game_state` or `current_mode` — and
|
|
562
|
+
these are the best enum candidates of all. Look for an existing enum first;
|
|
563
|
+
define one (`0 = INIT`, `1 = TITLE`, `2 = GAMEPLAY`, `3 = GAME_OVER`) with a
|
|
564
|
+
real `description` if none matches, then apply it to every instruction reading
|
|
565
|
+
or writing the variable.
|
|
566
|
+
|
|
567
|
+
#### 4. Name it
|
|
568
|
+
|
|
569
|
+
| Symbol kind | Convention | Example |
|
|
570
|
+
|---|---|---|
|
|
571
|
+
| Zero-page variable | `zp_` prefix | `zp_player_lives`, `zp_delay_timer` |
|
|
572
|
+
| Zero-page pointer | `zp_ptr_` prefix | `zp_ptr_screen`, `zp_ptr_dest` |
|
|
573
|
+
| RAM variable | `snake_case` | `score_hi`, `current_level` |
|
|
574
|
+
| Pointer / vector | `ptr_` / `vec_` prefix | `ptr_screen`, `vec_irq` |
|
|
575
|
+
| Hardware register | `UPPER_SNAKE` | `VIC_SPR0_X`, `SID_FREQ_LO1` |
|
|
576
|
+
| Constant / address | `UPPER_SNAKE` | `SCREEN_RAM`, `CHR_ROM_BASE` |
|
|
577
|
+
| Routine entry point | `snake_case` | `init_screen`, `draw_sprite` |
|
|
578
|
+
| External ROM call | `KERNAL_` / `OS_` | `KERNAL_CHROUT`, `KERNAL_CLRCHN` |
|
|
579
|
+
|
|
580
|
+
> **The zero-page rule overrides all of the above.** An address at or below
|
|
581
|
+
> `$FF` **must** carry the `zp_` prefix — a zero-page pointer becomes
|
|
582
|
+
> `zp_ptr_`, a zero-page flag becomes `zp_is_active`, and a zero-page OS
|
|
583
|
+
> variable becomes `zp_`-prefixed too. The prefix makes the addressing mode
|
|
584
|
+
> visible at every use site, which is the whole point.
|
|
585
|
+
|
|
586
|
+
Apply it with `anno_set_label_name`.
|
|
587
|
+
|
|
588
|
+
#### 5. Document it
|
|
589
|
+
|
|
590
|
+
- `anno_set_comment` `"line"` at the definition: the range it occupies, its
|
|
591
|
+
purpose, its bitfield layout if it has one.
|
|
592
|
+
- `anno_set_comment` `"side"` at the interesting *uses*: why this read, why
|
|
593
|
+
this write. "Reset life counter" beside a `STA` is worth more than any name.
|
|
594
|
+
- Define and apply enums where the values form a set (above).
|
|
595
|
+
|
|
596
|
+
**The pointer-formatting step this project does not have.** Where a pointer is
|
|
597
|
+
initialised by immediate loads of a target's low and high bytes — `LDA #<target
|
|
598
|
+
/ STA ptr / LDA #>target / STA ptr+1` — upstream calls `set_immediate_format`
|
|
599
|
+
twice to turn both immediates into a single readable symbol reference. **That
|
|
600
|
+
call is not exposed on this project's surface.** The rule it would serve — "every branch,
|
|
601
|
+
`JSR`/`JMP` and data reference goes through a symbol, so code can move" — still
|
|
602
|
+
supplies the criterion, and the per-call disposition is
|
|
603
|
+
recorded in the manifest named in the attribution header above. Until then,
|
|
604
|
+
reconstruct the target by hand and put it in a side comment on both
|
|
605
|
+
instructions — `; low byte of ptr_sprite_table ($C240)` — so the pointer is
|
|
606
|
+
still readable even though the store cannot format it.
|
|
607
|
+
|
|
608
|
+
#### 6. Report
|
|
609
|
+
|
|
610
|
+
- **Address** and its current label.
|
|
611
|
+
- **Classification**: flag, counter, pointer, hardware register, state
|
|
612
|
+
variable, dead.
|
|
613
|
+
- **Evidence**: the specific cross-references or usage patterns that decided
|
|
614
|
+
it. A classification with no evidence line is a guess wearing a name.
|
|
615
|
+
- **Actions taken**: what was renamed, what was commented, which enums were
|
|
616
|
+
defined or applied.
|
|
617
|
+
- **Uncertainty**: if `anno_get_cross_references` returned nothing, say which
|
|
618
|
+
of the three explanations in step 2 you could and could not rule out.
|
|
223
619
|
|
|
224
620
|
## Troubleshooting
|
|
225
621
|
|
|
@@ -21,7 +21,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
21
21
|
|
|
22
22
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
// memmap.json lives at the skill root, one level up from scripts/, by decision
|
|
24
|
-
//
|
|
24
|
+
// only .mjs modules move into scripts/, data files stay put.
|
|
25
25
|
const MEMMAP_JSON = join(HERE, "..", "memmap.json");
|
|
26
26
|
|
|
27
27
|
// Reference tables, merged into one address -> meaning index. `kind` selects the
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-petcat
|
|
3
|
+
description: Convert a Commodore .prg between PETSCII and ASCII, detokenize a BASIC program into readable text, and read the machine-code handover address out of its startup line, using VICE's own petcat conversion tool as the reference implementation. Use when asked to detokenize a BASIC listing, list what a BASIC stub does, find where a program hands over to machine code, or convert C64 text between PETSCII and ASCII.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Detokenizing BASIC with petcat
|
|
7
|
+
|
|
8
|
+
One capability, one script, one binary (`petcat`) reached only through the
|
|
9
|
+
host-tool execution seam:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
S=src/skills/c64-petcat/scripts/petcat.mjs # from the repo root
|
|
13
|
+
|
|
14
|
+
node $S decode --image path/to/program.prg # detokenize + resolve the SYS handover
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The script wraps `petcat` and nothing else. `--image` is required; `--out-dir`
|
|
18
|
+
is optional, defaulting to the image's own directory, exactly like
|
|
19
|
+
`acme-build`'s own `--out-dir` default. Both are resolved
|
|
20
|
+
**workspace-relative** to the smallest ancestor directory containing both,
|
|
21
|
+
before the request ever reaches the seam — the same resolution
|
|
22
|
+
`acme-build`/`c64-disk-access` already go through.
|
|
23
|
+
|
|
24
|
+
Options: `--image PATH` `--out-dir DIR` `--json`.
|
|
25
|
+
|
|
26
|
+
## Detokenizing and resolving the handover point
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node $S decode --image game.prg --json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Prints the seam's response verbatim as one line of JSON:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{"ok":true,"tool":"petcat.decode","exitStatus":0,"results":[{"path":"/abs/path/game.bas.txt","sha256":"...","byteLength":21}],"stderrTail":"","entrypoint":2064,"entrypointReason":"literal SYS argument on BASIC line 10: sys2064"}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`results[0].path` names the detokenized listing file the seam wrote —
|
|
39
|
+
`petcat`'s own decoded text, captured and digested. The listing itself never
|
|
40
|
+
crosses inline, no matter how short it is.
|
|
41
|
+
|
|
42
|
+
Two fields carry the handover verdict, and BOTH are present on every
|
|
43
|
+
`ok: true` response:
|
|
44
|
+
|
|
45
|
+
- **`entrypoint`** — a number when the program's `SYS` argument is a literal
|
|
46
|
+
decimal value, `null` otherwise.
|
|
47
|
+
- **`entrypointReason`** — always a string. When `entrypoint` is a number, it
|
|
48
|
+
names the BASIC line the value came from. When `entrypoint` is `null`, it
|
|
49
|
+
quotes the unresolved expression verbatim, or states that the listing
|
|
50
|
+
carries no handover instruction at all.
|
|
51
|
+
|
|
52
|
+
A `null` entrypoint is **not a failure** — it is a resolved "no": the tool
|
|
53
|
+
detokenized the program correctly and correctly concluded the entry point is
|
|
54
|
+
not a static address. Reported this way, never as a guessed address: a
|
|
55
|
+
guessed entry point is expensive downstream, spent on a disassembler that
|
|
56
|
+
then has nothing real to work from.
|
|
57
|
+
|
|
58
|
+
Non-JSON mode prints the same information as two lines — the resolved entry
|
|
59
|
+
point (or the decline and its reason) and the listing file's path — never the
|
|
60
|
+
listing's contents inline.
|
|
61
|
+
|
|
62
|
+
## The BASIC dialect
|
|
63
|
+
|
|
64
|
+
Fixed server-side, not a flag on this script or a field on the wire — this
|
|
65
|
+
project's target is fixed to C64 BASIC V2.0 everywhere already, the same
|
|
66
|
+
posture `acme-build` already takes for its own assembler target.
|
|
67
|
+
|
|
68
|
+
## Failure shape
|
|
69
|
+
|
|
70
|
+
A file `petcat` does not recognise as a BASIC program at all — including a
|
|
71
|
+
missing file — is reported as `{"ok":false,"message":"..."}` with a
|
|
72
|
+
non-zero exit code, never a success envelope carrying an empty or guessed
|
|
73
|
+
verdict. `petcat` itself exits `0` even on garbage input; the seam's own
|
|
74
|
+
classifier, not the exit code, is what decides success here.
|
|
75
|
+
|
|
76
|
+
## What this skill does NOT do
|
|
77
|
+
|
|
78
|
+
- **No direct binary spawn.** `petcat` runs host-side; this script only ever
|
|
79
|
+
constructs a typed request and reads the produced listing file back off
|
|
80
|
+
the shared workspace tree — the host-tool execution seam is the only
|
|
81
|
+
route.
|
|
82
|
+
- **No guessed entry point.** A computed or otherwise unresolvable `SYS`
|
|
83
|
+
argument is always reported as a named decline with `entrypoint: null` —
|
|
84
|
+
never a fallback value, never an inline listing scan for "something that
|
|
85
|
+
looks like an address".
|
|
86
|
+
- **No emulator dependency.** This skill names no VICE emulator tool at all
|
|
87
|
+
— it works entirely on files, never on a running machine, by construction.
|