How to drive regenerator2000, the recommended disassembler, through its MCP server with the kit's client script. Starting it on a snapshot, the tools that matter, the annotation log, and the traps.
Instal·la
npx skillscat add gamesexplained/gamesexplained/kit-skills-c64-tool-regen2000 Instal·la des del registre de SkillsCat.
regenerator2000
An interactive 6502 disassembler with an MCP server. It loads .vsf
snapshots directly, which is how it is used here: start it on the
steady-state snapshot from 10-orient.
Seeding from a Ghidra text export
The accepted producer is the custom CompleteListingWriter exporter fromghidra-mcp-next (export_full_listing), not Ghidra's stock ASCII exporter.
The compatible source is bundled in kit/c64/ghidra_export/, with its pinned
revision, licence and run instructions in README.md. The bundled script and
synthetic fixture were run on Ghidra 12.1.4. Stock ASCII exports are
unsupported: their label, comment and XREF columns differ. For this importer,
run ExportGhidraListing.java on the existing program and save into work/.
For an existing analysis, follow core/40-sweep, "An existing analysis of
this image". python3 kit/c64/import_ghidra.py <game> <export> converts
labels, comments and types into symbols.json; it never writes a listing.--verify-ram <65536-byte-dump> checks each initialized source row, and--space <name> selects one replacement overlay. Whole ROM overlays are
refused. Keep the source export in work/ and record its hash and makers
under imported in game.json.
Load this map into the disassembler on the chosen snapshot withsymbols_import.py, inspect it, then export through symbols_export.py.
Only listing.py builds the listing. Source annotations are leads until
coverage and verification establish them in this run.
Start and drive
python3 kit/scripts/tools.py r2000 games/<platform>/<slug>/work/<state>.vsf
python3 kit/c64/r2000.py --list
python3 kit/c64/r2000.py --game games/<platform>/<slug> r2000_disassemble '{"address": 57399}'Started on a snapshot in a game's or a part's folder that has exported
annotations, the launcher builds work/<slug>.regen2000proj fromsymbols.json and that snapshot, as symbols_import.py does, starts on
it and says so: a session restarted after a crash or a stop comes back
with the work of the last export. It names any annotation log written
after that export, with the r2000.py --replay that brings its work
back. A game's first session, with nothing exported, starts on the
snapshot itself.
Its native HTTP server binds port 3000 with no option to change it;
it needs a pseudo-terminal even headless. A second clone's disassembler
goes on a port of its own with KIT_R2000_PORT, through the stdio server
and a loopback bridge (kit/c64/INSTALL.md, "Another program on port
3000"); the client, the exporter and the launcher find it throughtools/r2000-port. Addresses in arguments are decimal integers.
A game of several parts (core/10-orient) can have a disassembler
running for each part at once. Start each on a file in its part's
folder (tools.py r2000 games/<platform>/<slug>/parts/<id>/work/<state>.vsf):
the launcher gives the part the first free port from 3000 and keeps it
in the part's work/r2000-port. Every command then names the part's
folder (r2000.py --game <part>, coverage.py <part> --live,symbols_export.py <part>) and reaches that part's session; a part
with none running is refused, never sent to another part's.tools.py status lists them, and tools.py stop r2000 <part> stops
one and leaves the rest.
The client script logs every mutating call togames/<platform>/<slug>/work/annotations.jsonl. That log is crash
insurance: r2000.py --replay <log> rebuilds a fresh session. Run it from
the game folder, or pass --game, so the log lands in the right place.
Annotations written in bulk go through r2000.py --apply <file>: one
line each, $C000 name : line comment, $C000 name, $C000 : line comment or $C100-$C1FF byte, sent as batches ofr2000_batch_execute in the file's order. Only the calls that succeed
are logged, and each that fails is printed with its line number; a line
that does not parse stops the file before anything is sent.
Tools that matter
| Tool | Use |
|---|---|
r2000_disassemble {address} |
mark and decode code from an address |
r2000_read_region {start_address, end_address} |
show a region; disassembles as a side effect, which the log does not capture unless you also log a disassemble |
r2000_set_label_name {address, name} |
name a routine, variable or table; an empty name clears a user name (check automatic symbols afterward) |
r2000_set_comment `{address, type: "line" |
"side", comment}` |
r2000_set_data_type {start_address, end_address, data_type} |
type a data block. The values are lower case: code, byte, word, address, petscii, screencode, lo_hi_address, hi_lo_address, lo_hi_word, hi_lo_word, external_file, undefined |
r2000_get_cross_references {address} |
who reads, writes, calls or jumps to an address; the fastest way to attribute a table |
r2000_get_address_details {address} |
semantics, xrefs, labels and comments at one address; not on a snapshot (Traps) |
r2000_search_memory, r2000_search_disassembly |
bytes or text in memory; a substring, or a regex with use_regex: true, over each field of the listing (register census in one call) |
r2000_get_blocks, r2000_get_symbols, r2000_get_comments |
state; what the coverage and export scripts read |
r2000_undo |
undo the last operation; note that the log does not record undos |
r2000_unpack_binary |
destructive: wipes all annotations. Do not use on an annotated session |
r2000_batch_execute {calls: [{name, arguments}, ...]} |
many calls in one round trip; each entry names its tool as name, not tool |
r2000_save_project |
saves into the project the session started on, which the next start on the snapshot rebuilds from symbols.json; use symbols_export.py instead |
Traps
- The type names going in are not the type names coming out.
set_data_typetakesbyteandlo_hi_address;get_blocksreportsByteandLo/Hi Address. The export script maps between them. A
capitalised value is rejected with "Unknown data_type". addressis for interleaved pointers,lo_hi_addressfor split
tables. A table of low-byte-then-high-byte pairs isaddress. Typing it
aslo_hi_addressreads the first half as low bytes and the second half as
high bytes, invents addresses that are not there, and mints auto symbols
all over the image. The tell is symbols appearing in regions the game never
touches.- A custom alphabet has no data type.
petsciiandscreencodeare the
only text types; a game whose character set is in its own order has to be
typedbytewith the decoded text in the comment. search_disassemblytests each field on its own. The mnemonic, the
operand, the label and the comments are separate strings, so a query
that spans two of them (jmp (,sta $d020) finds nothing in the code,
only comments that happen to quote it. Match the operand's shape withuse_regex: trueandsearch_comments: false(^\([^,]*\)$is every
indirect jump) and filter the results by mnemonic. Once an address has a
label the operand shows the label, not the address: search a named
register by its name, or scan the snapshot for the opcode bytes.get_address_detailsis no use on a snapshot. With a whole 64 KB
image loaded (a.vsf, or the project built from one:get_binary_info
says origin 0, size 65536), 0.9.20 answers every address,$0000to$FFFF, with "Address is outside the loaded binary range", whileread_regionshows the same bytes. Askget_cross_references,get_symbols,get_commentsandread_regioninstead.- The project file (
.regen2000proj) embeds the memory image. It stays inwork/and is never committed.symbols_import.pyrebuilds it fromsymbols.jsonplus a snapshot.
Nothing writes annotations back into it: a project that sat inwork/
while a session annotated holds the comments from before that session.
Started on it again, the next export quietly undoes everything since.
So start every later session on the snapshot, which rebuilds the
project fromsymbols.jsonfirst, never on a project you find inwork/; or comparer2000_get_commentswithsymbols.jsonbefore
the first write. - After any bulk recovery, verify with a clean process, a full replay and
a block-count check, not "the replay didn't error". - The flow tracer can wander into text.
$20isJSR, so a run of
PETSCII spaces decodes asJSR $2020 / JSR $2020, and a trace that
reaches such bytes keeps going: one run minted code blocks inside a
title scroller, reached from nothing but each other. After tracing, askget_cross_referencesabout every small code block, and set the ones
only reached from inside themselves back toundefined. - A
JSRinto ROM traces the RAM underneath. The snapshot holds the
RAM below the BASIC and KERNAL ROMs, so a call to$E544or$FFD2makes
the tracer disassemble whatever the game keeps there. Restore the real
data type and describe any authored bytes underneath, such as sprites
the video chip reads. Explain the ROM meaning at the call site: a data
label on the RAM does not mean the CPU calls that data. Only usecoverage.excludefor bytes proved not to be the game's authored data;
excluding an entry just to remove its automatic ROM label can hide a
byte of the game's picture. Whatever the session calls a KERNAL entry
point (s_FFD2when it traced the call after it started), the export
writes the KERNAL's name (KERNAL_CHROUT,kit/c64/kernal.py), so the
file the session started on does not show. Where the game banks the
KERNAL out and keeps its own code or data at one of those addresses,
that name is wrong: give the address a label of your own. - Auto-generated symbols (branch targets) are minted on every load; the
export keeps them because the coverage denominator uses them, and the
import drops them because the tool regenerates them. - Clearing a renamed label can remove the symbol under it.
set_label_namewith
an empty name on a renamed automatic symbol (0.9.20) deletes it
outright: the address has no symbol until the code that names it is
disassembled again, and the coverage denominator shrinks meanwhile.
That is also the way to turn an automatic symbol into a user label with
the longer span (kit/scripts/ledger.py): clear it and set the new name
in the next call, never the one without the other. An automatic symbol
that has not been renamed may remain, and retracing can mint it again.
Checkget_symbolsafter clearing; do not assume the denominator or
ownership changed just because the call succeeded. - A placeholder operand sends the tracer into zero page. A
JSRorJMPwhose target the program writes before it runs is often assembled
asJSR $0000; the flow tracer follows it and marks$0000onward as
code. Set that range back toundefined, and find the real targets from
whatever writes the operand (a table of routines, a display list). - Undocumented opcodes come out as data. 0.9.20 decodes only the
documented instruction set:LAX,DCP,LXAand the multi-byteNOPs
show as.bytelines inside a code block, the bytes after them are
sometimes decoded as instructions they are not (STX $DFinsideDCP $FF86,X), and the cross-references miss what they touch. Type such
a spotbyteand write the real instructions in its comment
(c64-reference, Undocumented opcodes). A search of the decoded code for
readers of an address is incomplete until these gaps have been read.python3 kit/c64/opcodes.py games/c64/<slug> [<snapshot.vsf>] [--live]
lists every undocumented opcode inside a code block, with what the
disassembler shows there, and decodes the spots typedbyteinside
code;--refs <address>lists every instruction that can touch an
address, documented or not: by name, as a pointer, or through an index,
including one that wraps past$FFFF. - Code that indexes into I/O mints symbols in the RAM beneath.
STA $D800,X,LDA $DDDD,X(a placeholder operand) and the like leave
automatic symbols at$D000-$DFFF. If that RAM holds the game's own
data (coverage.include), type it, clear those symbols (an empty name)
and label the data, or the listing names sprite rows after the chips'
registers.