gamesexplained

tool-regen2000

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.

gamesexplained 15 9 Actualitzat fa 2 setm.
GitHub

Instal·la

npx skillscat add gamesexplained/gamesexplained/kit-skills-c64-tool-regen2000

Instal·la des del registre de SkillsCat.

SKILL.md

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 from
ghidra-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 with
symbols_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 from
symbols.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 through
tools/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 to
games/<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 of
r2000_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_type takes byte and lo_hi_address; get_blocks reports
    Byte and Lo/Hi Address. The export script maps between them. A
    capitalised value is rejected with "Unknown data_type".
  • address is for interleaved pointers, lo_hi_address for split
    tables.
    A table of low-byte-then-high-byte pairs is address. Typing it
    as lo_hi_address reads 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. petscii and screencode are the
    only text types; a game whose character set is in its own order has to be
    typed byte with the decoded text in the comment.
  • search_disassembly tests 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 with
    use_regex: true and search_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_details is 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, $0000 to
    $FFFF, with "Address is outside the loaded binary range", while
    read_region shows the same bytes. Ask get_cross_references,
    get_symbols, get_comments and read_region instead.
  • The project file (.regen2000proj) embeds the memory image. It stays in
    work/ and is never committed. symbols_import.py rebuilds it from
    symbols.json plus a snapshot.
    Nothing writes annotations back into it: a project that sat in work/
    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 from symbols.json first, never on a project you find in
    work/; or compare r2000_get_comments with symbols.json before
    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. $20 is JSR, so a run of
    PETSCII spaces decodes as JSR $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, ask
    get_cross_references about every small code block, and set the ones
    only reached from inside themselves back to undefined.
  • A JSR into ROM traces the RAM underneath. The snapshot holds the
    RAM below the BASIC and KERNAL ROMs, so a call to $E544 or $FFD2 makes
    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 use
    coverage.exclude for 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_FFD2 when 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_name with
    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.
    Check get_symbols after clearing; do not assume the denominator or
    ownership changed just because the call succeeded.
  • A placeholder operand sends the tracer into zero page. A JSR or
    JMP whose target the program writes before it runs is often assembled
    as JSR $0000; the flow tracer follows it and marks $0000 onward as
    code. Set that range back to undefined, 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, LXA and the multi-byte NOPs
    show as .byte lines inside a code block, the bytes after them are
    sometimes decoded as instructions they are not (STX $DF inside
    DCP $FF86,X), and the cross-references miss what they touch. Type such
    a spot byte and 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 typed byte inside
    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.

Categories