Reproducing the originals: sources, build and verification

← Voltmace Delta 14B Driver

Reproducing the originals: sources, build and verification

Scope. This article explains how this repository regenerates the original KEYPAD and JOYSTIK files, byte-for-byte, from human-readable reverse- engineered sources — despite the originals mixing plain 6502, bit-rotated 6502, tokenised BASIC (also bit-rotated), and captured workspace in one file. It is the build-side companion to The Voltmace Delta 14B driver system, which describes what those files do.


1. The problem

A conventional ROM disassembly has a tidy property: the file is the code, so a disassembler can read it, a human can annotate it, and an assembler can turn the annotated listing straight back into the identical bytes. The Voltmace files break that property three ways:

The goal is nonetheless a full annotated disassembly that reassembles to a byte-identical copy of the whole file — verified automatically in CI — while the sources a person edits are readable assembly and readable BASIC.

2. The toolchain

Four tools cooperate, each doing one job:

3. The editable sources

For each program the source of truth a person edits is:

Everything else — the .asm listing, the .json, and the incbin .dat payload — is generated.

4. The build, region by region

The trick is that the bit-rotated region is carried as an incbin payload that the driver script regenerates from the editable sources on every fantasm disassemble, then cross-checks against the bytes dasmos itself accounted for (its canonical write_included_binaries output). If the rebuilt payload does not match, the build fails loudly.

KEYPAD (build_basic_dat): only the BASIC is encrypted. The build

  1. greedy-tokenises keypad.bas;
  2. reverses the first-line length repair (stores byte 3 as 0, see §6);
  3. rotates every byte right one bit (the inverse of the loader's left-rotate);

and that is the .dat the listing incbins at &1B00. The resident driver itself is plain 6502, disassembled in place with an add_move(0x0A00, 0x1900, 0x100) so it reads at its &0A00 runtime address.

JOYSTIK (build_encoded_dat): the encrypted region is [driver A][driver B][encrypted part of the BASIC], followed by the BASIC's raw tail. The build

  1. disassembles and annotates each 256-byte resident driver variant at its &0A00 runtime address, renders its .asm, and assembles it back to bytes with beebasm (which also proves the disassembly round-trips);
  2. greedy-tokenises joystik.bas and reverses the first-line repair;
  3. concatenates driver A + driver B + the BASIC bytes that fall inside the decode range and rotates the lot right one bit;
  4. appends the BASIC's raw tail (past the loader's page-&4B decode limit).

The result is the .dat the listing incbins at &1A00. So the two joystick resident drivers are readable, annotated assembly and the byte source for the encrypted region — beebasm assembles them, and the build re-encrypts the result.

5. Why the greedy tokeniser

BBC BASIC's ROM tokeniser and these programs' tokeniser disagree in three ways (a keyword may interrupt a hex constant or a name; a conditional keyword is suppressed only before a non-keyword name character). oaknut-basic's default crunch is byte-exact to the ROM and therefore does not reproduce these files; --crunch greedy matches the tool that actually built them. Both programs' BASIC round-trips byte-for-byte under greedy crunch. (This was the finding behind oaknut-basic issue #48.)

6. The first-line length patch

The loader's patch_header writes the true length (&16/&17) into the first BASIC line's length byte at run time, because the file stores it as 0 as part of the protection. So the build must do the opposite: after tokenising the .bas (which yields the correct length), it forces byte 3 back to 0 before rotating and encrypting. The .bas in the repository is therefore the true program (it lists and runs); only the stored program image carries the deliberately broken length.

7. Verification

fantasm verify <program> assembles the generated .asm with beebasm — from the listing's own directory, so the relative incbin resolves — and compares the result against the original file. There is no slice: the whole file must match.

Verification PASSED: 8576 bytes match      # KEYPAD
Verification PASSED: 13312 bytes match     # JOYSTIK

Three independent checks therefore have to agree for a build to pass: dasmos's own round-trip oracle, the driver-script cross-check of the rebuilt payload against dasmos's canonical bytes, and beebasm's whole-file reassembly. CI runs disassemble → lint → verify for both programs on every push.

8. Tooling that had to be built

Producing this to a publishable standard drove five upstream tool changes, all since shipped: