Skip to content

Preservation and reverse engineering

Daniel Nylander edited this page Aug 31, 2026 · 60 revisions

Project Eon: preservation and reverse engineering

This wiki records observed bytes, bounded code paths, and Project Eon's current SDL runtime. It is not a complete manual for the original games. A control or meaning is named only where independently evidenced.

Preservation contract

Project Eon is a preservation-first SDL3 reimplementation of Millennium 2.2 and Deuteros. Recognised user-supplied media are content-identified by hashes, recursively scanned, and read in place. The project never unpacks, copies, installs, mutates, redistributes, or commits commercial game data. Malformed formats and unproven code paths fail closed at explicit boundaries.

Original presentation uses recovered pixels. Modern presentation may add renderer-only chrome, but must not alter logic, saves, or original asset bytes. Target support means the supplied release is identified and preserved; it is not a parity claim and never permits fallback to another platform/language.

Evidence model

  1. Verified bytes: exact range, file/disk offset or runtime address, SHA-256.
  2. Verified code path: instructions/control flow proven only to a named boundary.
  3. Observed behaviour: captured from genuine media/runtime without extrapolation.
  4. Inference: labelled and never promoted to implementation fact.
  5. Unknown: a hard stop, never a reason to synthesize behaviour.

Each evidence addition records release/platform/language, file size/hash, disk/archive offset, runtime address, byte-range hash, uncertainty, and malformed/tamper test. The authoritative hash ledger is PRESERVATION.md; method and hypotheses live in research.md.

When a report reaches a native ABI boundary, --inspect names the minimum reference-trace inputs instead of guessing them. Spanish Millennium requires that image's own child return/AL, file-operation, CPU and interrupt trace; no English executable or state is substituted. Millennium Atari requires GEMDOS Fopen, TRAP #14/Line-A, configuration-load and codec/palette/planar evidence. Deuteros Atari requires XBIOS Floprd, callback frame, dispatch word, and selected-vector D1/D2 returns. These are collection checklists for hash-identified, read-only reference execution, not requests to emulate missing values.

Reference-trace v2 currently admits three strict diagnostics-only adapters: English Millennium DOS startup, Deuteros Atari ST boot, the English Millennium Amiga Defjam bootstrap handoff, and the Deuteros Amiga title-stage ABI boundary. Each adapter has an exact outer release hash, source-site schema, and bounded event grammar. A validated trace is provenance only: it never replays events, invokes a native service, or becomes runtime input. See REFERENCE_TRACE_FORMAT.md.

The public CLI integration test admits every v2 adapter only against its exact supplied archive and checks its diagnostic report. This caught and fixed two reader defects: checksum field names must admit their decimal suffix, and a clean bounded event read is not treated as an I/O failure. It remains a test of the evidence reader, not a synthetic game session or replay.

Recognised corpus

Game Recognised releases Current runtime boundary
Millennium 2.2 DOS English/Spanish, Amiga English, Atari ST English English DOS P00 title and local title-input boundary; GX canvas and immutable save are inspection evidence only; remaining paths bounded
Deuteros Amiga English, Atari ST English Amiga opening VM/palette/bitmap/initial-audio and title-stage evidence; Atari bootstrap bounded

Media and format knowledge

Archives, FAT12, ADF

Nested ZIP handling bounds output, validates Deflate completion, CRC-32 and SHA-256. FAT12 parsing validates geometry, root directory, cluster-chain bounds and cycles before a file becomes readable. It serves DOS floppies and Millennium Atari ST media; output is never written back.

ZIP recognition rejects multi-disk, ZIP64, and encrypted archives. The central directory must occupy its declared exact range; local header flags, method and filename must agree with it. For ordinary entries, local CRC and size fields agree too. Bit-3 data-descriptor entries retain the central directory as their authority and their decoded payload is still CRC-checked: genuine Spanish MRTE.IMG uses this form with zero local CRC/compressed size. No archive is extracted to disk.

The Spanish Millennium DOS image is 720 KiB FAT12 with 512-byte sectors, two sectors/cluster, two FATs and 112 root slots. Names alone are never proof; each range is checked against original media.

ADF geometry is 80 tracks × 2 sides × 11 sectors × 512 bytes. AmigaDOS carry-around checksums are verified where a standard filesystem exists. Deuteros's DOS\0 system disk and DEU\0 custom/protected disk are distinct: custom data are never misrepresented as AmigaDOS. Raw track requests remain raw-offset evidence until loader semantics are proven.

The genuine Millennium Atari FAT12 MILL22B.INF (84,720 bytes, SHA-256 e315b0ec01f2fe429fdce101765577b893d031389c540de1fbe43eca121d53e9) has an isolated MILL22E.INF literal at file +0x11600. This records original file-chain provenance only: no proven native open, directory selection, decompression, palette association, planar layout, or display path exists, so Project Eon does not render its packed contents.

Amiga and Atari ST corpus boundary census

Outer and inner archive labels are catalogue metadata, not release identity. The census below is anchored in complete leaf-image hashes and observed container bytes; names such as cr, a, or save are never treated as original-program semantics.

Corpus Container boundary Admitted entry evidence Documentation/control status
Millennium Amiga Six supplied ADFs; raw-sector program path is distinct from the valid Razor filesystem. Defjam-family bootstrap requests $24200..$923ff to $41000, then $16400..$423ff to $68000; the resident span is hash-identified. No live standalone manual is recognised. Function-key trainer text appears only in altered variants and is not original control evidence.
Deuteros Amiga Clean system disk is DOS\0; clean data disk is DEU\0, whose logical block 880 is custom raw data, not an AmigaDOS root. Clean system boot loads $5800 to $20000 and enters $21734; title transfer remains separately bounded. Genuine raw text contains load/save prompts, but no input-dispatcher reference is recovered.
Millennium Atari ST Physical .stx dumps remain physical-media containers; they are not silently converted to flat FAT images. The hash-identified Equinox FAT12 media admit MILENIUM.TOS and the $77000 bootstrap only; the first MILL22A.inf Fopen is a GEMDOS boundary. Visible mouse/keyboard and prompt text has no recovered code-to-input map.
Deuteros Atari ST The 737,280-byte candidates have BPB-shaped boot sectors, but their apparent root records are not a live FAT12 namespace. The protected raw boot chain reaches stages through explicit nine-sector reads; XBIOS callbacks and state selection remain boundaries. No standalone manual is supplied. A separate 1,056,768-byte development/tools disk is excluded from game-control evidence.

The clean Deuteros Amiga system ADF has a 336-byte embedded text region at ADF $78bc0..$78d0f (SHA-256 66b312b5e7b148bdfe0e43af4d6cc6f4b451ed05f83be8edd7a1e11f17264680) containing LOAD, Press 'L', SAVE, and Press 'S'. Its framing and input caller remain unknown, so Project Eon does not expose L or S as reconstructed gameplay controls.

Millennium Atari ST Disk 1 has a distinct physical-dump text span at $12420..$1258f (368 bytes, SHA-256 6330b762858bb4b1fb0bc17f4f577eca3b1e8de4c078fd3fc01192bcd05a89f7) containing SAVE GAME, LOAD GAME, press left button to continue..., MOUSE MODE, and KEYBOARD MODE. This is content evidence from leaf SHA-256 081d8bc102b8c7669c5cb21abace9b08532bc0b34164f11465d0c87b63a422fd, not a filesystem claim or SDL mapping. It remains inspection-only until physical-track decoding and a caller-connected trace prove its use.

Millennium DOS LIB resources

TITLE.LIB and GX.LIB have a six-byte banked header (count, directory offset, 64 KiB bank) and 12-byte entries (16-bit resource offset, bank, reserved zero, NUL-padded eight-character name). The parser rejects invalid flags/padding, duplicate names, non-monotonic resources and out-of-range data.

English TITLE.LIB has 38 entries; P00 is a 320×200 indexed title frame. English GX.LIB has 180 entries; IMG00/IMG01 are recovered canvas evidence. LAST.LIB supplies codec-2 indexed-bitmap evidence. RGB6 palette/translation data are retained per supplied release; Spanish pixels are never recoloured with English data.

The only live standalone launcher document is Spanish FAT12 MILL.BAT (437 bytes, SHA-256 1fbb8246d496a6b3a35759a917ef7ae7ba36487de73104f2df81f5a1f8d9f474). It preserves the original IBM, IBM e, IBM m, TANDY, and EGA320 launch choices; it is not gameplay-control evidence. Embedded English/Spanish static text also contains literal prompts for space/left button and mouse/keyboard modes, but no caller-connected code proves bindings. They remain inspectable original text, never SDL mappings. Apparent Spanish README records in FAT allocation slack are excluded because they are not live files.

Spanish FAT12 TITLES.EXE is separately admitted only at SHA-256 02082c35e18cee330f7d1b88098f502e68011f7e47a3a649961f6f03d1d14fe7. Its own bytes prove title entry $1b80, private wrapper $0122, and post-title $1968 → $1931: five AX=$0013 private calls followed by $1917. This records shared machine-code facts without reusing English assets, drivers, ABI effects, or frames.

Spanish EGA640.BIN (4,630 bytes, SHA-256 ef031b0b6e720ab2dafc1eb6373ddb76e0ff15f7b59ac785265c5136be153daf) and MCGA.BIN (4,346 bytes, SHA-256 3fb76b2ccccffc304b0525cd410b940bbb61e3d1a7a90340d72e5683d7f0211d) are independently identity-gated. Their own dispatch tables retain function $0006 targets $08a6/$0705 and function $0013 targets $0d37/$0905. Matching offsets document local structure; they never authorize an English-driver substitution or private ABI execution.

For English DOS specifically, pointer-table entries 271, 350, 390, 398, and 399 select raw records at $12a7, $1d88, $2aef, $2bcd, and $2be3. Their hash-locked printable substrings are left button / space, press space bar to continue..., press left button to continue..., MOUSE MODE, and KEYBOARD MODE. The static-data loader requests the file, but neither the destination nor an input-dispatch caller is recovered; these strings remain provenance, not host controls.

The Millennium Atari physical dump has a separately hash-locked 368-byte span at $12420..$1258f (SHA-256 6330b762858bb4b1fb0bc17f4f577eca3b1e8de4c078fd3fc01192bcd05a89f7) containing the corresponding save/load, button, mouse-mode, and keyboard-mode literals. It is not made into a menu/control mapping.

Project Eon now indexes the genuine Disk 1 STX as its physical container: 80 track records and 800 unique sectors. It returns direct, immutable sector spans rather than manufacturing a flat .st image. For the supplied disk, T0/H0/S1 is at container +$c0 (512 bytes, SHA-256 d0601ec6e1bbea0d5f4d5ba37130148e6670225b6337d001f4d4e6b8fc45fd08), and T1/H0/S9 is at +$1570 (512 bytes, SHA-256 096869a11a3f601c587bb915c6c93d7985f8eb2185dc2d0f2839286df9905dad), with the MILL22B.inf literal at sector-relative +$be. These are container provenance facts only: no filesystem, boot path, or executable handoff is inferred from them.

Deuteros Amiga bundles and Atari binaries

Deuteros bundle headers are 60-byte big-endian records with five offsets; first two carry size/mode/object-count. Recovered channel records begin with 10 bytes state then big-endian opcodes $00..$14. Palette data are RGB4. Four-plane RLE layouts, indexed payload boundaries and 14-byte Paula descriptors are independently validated. Current audio covers known initial playback only.

MILENIUM.TOS has PRG header 0x601a and a validated 227-site relocation stream; no selected load base or relocated image is invented. Deuteros Atari vectors/raw load overlap are preserved as byte geometry only; XBIOS/callback/resource semantics are boundaries.

Millennium Atari's hash-locked Fopen fall-through begins at target +$1a (26 bytes, SHA-256 663d5f1418326aa9c0efde064ad95bda21c84d7f23241ce3505f21f1f07474d0). It prepares GEMDOS Fread selector $003f with buffer $2a500, count $20000, and OS-owned D0 handle, issues TRAP #1, then cleans 12 bytes from the stack. No handle, service result or buffer content is executed, synthesized, or interpreted.

If that original Fread conditionally maps file byte zero to $2a500, the leading JMP $2aa88 maps to file +$588. The complete $2aa88..$2aaa9 prefix is 34 bytes, SHA-256 dede20eddbd8015da1d1a4f2f5e53424c2bc2195bff238d830ea24c9f522ea59. Its SR-dependent local branch converges at JSR $2a51c and returns into the separately bounded +$5aa candidate. This is conditional byte/address evidence only: Eon does not provide a GEMDOS result, select SR, execute the JSR, or infer a platform effect.

Execution maps, not gameplay claims

Millennium DOS

English 16-bit binaries are treated as flat loaded images. TITLES.EXE title resource/poll/hand-off and 2200AD.EXE startup/main-loop are hash-locked. The latter loop's CALL at $d3db is a 16-bit near call: $d3de + $3b27 wraps to $0f05, not $10f05. Its seven bytes (executable +$e05) are b4 06 b2 ff cd 21 c3, SHA-256 96715350de2c4a159dd994b1c40dafb21c038f38f52937aa1a54491485146dc6: DOS nonblocking console input followed by return. The loop recognises raw actions 0x0b, 0x0c, and F-key scan-code range 0x3b..0x44; these numbers are not yet original gameplay command names. This is static evidence only: Project Eon does not enter 2200AD.EXE from the title boundary.

TITLES.EXE's complete input caller range proves only an AL-zero/nonzero test: every nonzero result takes one exit path, with no scan-code comparison, key name or control binding. Its static post-input bytes include LOADING 2, but they reach an unrecovered private interrupt wrapper. The launcher keeps showing the recovered original P00 title frame after input availability; it never draws a synthetic loading screen or claims that the process exit, DOS return, 2200ad.exe request, GX selection or game startup occurred.

Before that input boundary, the original title decoder requests P00 and then P01–P25 through a 37-step, 368-byte-stride transition. Those codec-2 records are hash-locked as original resource evidence, but decode into runtime buffers followed by an opaque private-driver call. No segment setup, composition, colour transfer or visible transition is inferred or rendered.

Project Eon now verifies the exact static P01–P25 sequence as 37 read-only codec-2 patches, each 16×23 / 368 indices. P01's decoded-index SHA-256 is 330db310a838487f4afea0011c1ba5f381e4ed7ad97d95e4745e7be2d2d8aaa1; P25's is d7e44c796aed167010cdef9ab7ccef38b3b260854b51b2fba818972f30dd35dd. The static mode-2 lookup range (for example P02 00 cc 00) is retained only as data. It proves neither selected mode nor timing, composition, DAC lifetime, framebuffer destination, or a title animation.

After the nonzero-input exit reaches $1968, original code sets AX to five and calls local $1931. That loop runs exactly five times: AX becomes $0013, the private INT 91h wrapper $0122 is called, then local $1917. This establishes the order after the patch sequence but not the private-driver ABI, helper effects, composition, or a BIOS-visible result; nothing is executed by Project Eon.

Both hash-identified installed drivers give function $0013 one ABI-independent local meaning: EGA640 target $0d37 and MCGA target $0905 load VGA status port $03da, wait for bit $08 to clear, then wait for it to set and return. It is an original vertical-retrace wait, recorded without host port I/O or a claim of frame output.

Post-title helper $1917 has a fixed 15-iteration selector loop. $18f9 adds unknown $1181 to accumulator $18f7, masks $03ff, loads an original byte at that offset, and repeatedly subtracts $18 from values at least $24. Its caller adds one and calls loader $1712. This is a bounded potential resource-index path only: state, selected byte, destination buffers and output remain unknown.

Loader $1712 zero-extends the selected byte, multiplies it by $0170, and stores it at $1341. It consumes 15 fixed four-byte entries at $1768..$17a3 (SHA-256 9c40c1fa63248237383703aa0aaf6659630e8d4fb48bc6ddd1c633ed4d26846f), copies their two words to local cells, and calls private INT 91h function $0006. EGA640 $08a6 and MCGA $0705 hash-checked entry prefixes clamp ES:BX word +$10 against 320 - word[+$08], returning for a non-positive result. This is a clipping boundary, not a host blit: record ABI, pointers, writes, and visible output remain unknown.

The caller fixes only part of that ES:BX record: $1712 sets ES=CS and BX=$1349; $174a stores selected table words at $134f (record +6) and $1351 (record +8), with related segment cell $134b (record +2). Codec-2 setup stores decoded height/width at $1357/$1359 (record +$0e/+$10). Remaining words, pointer provenance, table meaning, composition, and driver writes remain explicit boundaries.

Loader $1712 zero-extends that selected byte, multiplies it by $0170, and stores the result at $1341. It consumes 15 fixed four-byte entries at $1768..$17a3 (SHA-256 9c40c1fa63248237383703aa0aaf6659630e8d4fb48bc6ddd1c633ed4d26846f), copies their two words to local cells, and calls private INT 91h function $0006. This proves index-to-buffer/table setup, not a resource name, coordinates, blit ABI, destination, or visible pixels.

The adjacent $135e routine is now byte-verified separately. It loads one of the original far pointers at $010c/$0110, writes its offset and segment to $1341/$1343, and writes CS to $134b; $1712 later replaces only the offset word. EGA640 executes LDS DI, ES:[BX] at $08d9; MCGA executes LDS SI, ES:[BX] at $072e after its push of DS. Consequently record $1349 + $00 is a far-pointer offset and $134b is its segment. No direct local writer of the offset occurs in the hash-identified executable, so ownership, pointed-to format, and every transfer remain explicit private-ABI boundaries.

The drivers provide further format-neutral reads after that load. EGA reads source words +$04 at $08dc, +$02 at $08df, and +$00 at $08eb; MCGA reads +$02 at $0731, then executes LDS SI,[SI+$04] at $0736. These byte-locked accesses bound the records more tightly, but establish neither source ownership, pointed-to format, transfer destination nor visible output.

F8's C6 06 30 DA 00 changes private, initially unknown runtime byte $da30 to zero before any call/conditional branch. It is an evidence overlay, never a COM/save write. F8 preflight/selector/record traces stop at opaque helpers. English-only action $0b observes and toggles $07f9 before opaque helper $0666; Spanish shares handler bytes but lacks a proven matching dispatch route. The helper's $0666..$0681 prefix maps to executable +0x0566, is 28 bytes, and hashes to 8dc7586f3809a14f3ed6acd601cd42486841adb9d9cb09d3e9b1ed727329e485; it stops at $067b → $05f7 without dereferencing its native segment/table or invoking the helper.

The English 2200AD.EXE caller at $d335 reaches a hash-locked loader at $11ce for original 2200GX.EXE (SHA-256 093f8416de6d23837d2faf82360ef79777c2c2bf146619aafad87626c61ab6fb). The 41-byte loader at executable +$10ce has SHA-256 a8972b74ad9d1dfabe508c42b7fcda0fb45e0d449613449ab8a2763ca8ecff45, uses the literal name at $11c2, reads segment cell $0118, and has static calls $11d1 → $053a, $11e4 → $0574, $11ec → $0596. Adapter $6c52..$6c72 has SHA-256 b34e5abf8ecd790fce3e7a032d7a7fcacc073d03909e98fd33f9503113e3ad87; it encodes a RETF at $6c68 to overlay offset zero with local continuation $6c69. Its 20-byte entry dispatcher at overlay +$0000..+$0013 has SHA-256 f4d657fcbdda23d7f0fdf2bbf48405d0a04e8b8149df064607f49042525fbd55: it clears AH, indexes AL through the 21-word table +$0015..+$003e (SHA-256 4d04568e05378787921012654fe9c157419ce7c07f9943b51135258f32a06df3), then near-returns to the RETF at +$0014. The caller-connected selector prefix $d343..$d375 (executable +$d243, 51 bytes, SHA-256 571626e83b0787401f89c8586c12dfb4d4221c44e0a9786727d2314b09327091) reads unmodelled runtime byte $da05, selects AX $000e/$0012/$0014 for values $03/$04/$02 and $000f otherwise, writes its paired DX literal to $4b6e, then calls $6c52. Those four selectors resolve to raw offsets $0090/$0097/$00a7/$009f; their records converge through overlay +$0090..+$00ed (94 bytes, SHA-256 8d412472415d513482b5c70198bb1aa04fa0d25798dd5f4b40b262151c489736), which copies local overlay words and returns. Startup's INT $91 function $1f is now separately bounded in original hash-identified drivers: EGA entry $0235 returns AH $04 plus local byte $008a; MCGA entry $024c returns AH $01 plus local byte $00ac; neither reads ES:BX. Driver installation/selection and runtime state remain unproven, so Project Eon never selects a driver, fixes $da05, supplies the selector, assumes calls return, runs overlay code, or claims a record, resource or screen effect.

MILL.COM's original command-tail scanner makes the driver request explicit: e/E maps to selector $01 / ega640.bin, m/M to $02 / mcga.bin; an empty tail instead reaches a hardware-dependent detector. The scanner and later map are hash-locked in the repository, but the supplied bytes do not prove a user tail, hardware observation, DOS load, interrupt installation, or title/runtime state. Project Eon reports the chain and never turns it into a host-side default-driver policy.

The launcher’s TITLES.EXE then 2200ad.exe requests share a local DOS EXEC wrapper; their post-call checks are AND AL,AL, not AND AX,AX. The helper $031c..$034b is 48 bytes (SHA-256 62cee56837e015eecc218906046c1e1c19a7ad9ba87e6580f99674eac0976b58): it fills parent parameter-block segment fields, invokes DOS EXEC at $0337, restores the parent context, then takes an unmodelled carry/result split. TITLES.EXE does have a title-side function-$00 candidate—AX zero, ES:BX $CS:$1ac4 with record 01 00, through private wrapper $0122..$012e—but MCGA function $00 reaches only BIOS INT $10 query/set/verify boundaries. No BIOS/DOS result, child completion, vector survival or later 2200AD start is supplied. The driver bytes queried by function $1f are mutable: original EGA functions $00/$1e can write $008a, and MCGA $00/$1e can write $00ac; no successful caller-connected title/start path establishes either value. Thus neither the supplied zero bytes nor an inferred return is substituted at runtime.

Spanish FAT12 uses its own IBM.COM, rather than an English launcher. Its caller $023d..$0252 has SHA-256 6e1cf860908aa88e9427efac371439744c1a10f5bb5fcc7d9588a7f18085cbb7: it requests literal TITLES.EXE at $071d, conditionally then 2200ad.exe at $0728, through local callee $0339..$0368 (SHA-256 c2f5b915a0fbbc7a25d8a3f4c0e5fcc97eb197d44048eaff53e2046eb6e7c32c). Both Spanish targets are independently hash-identified. This is a request/control-flow chain only; Project Eon invokes no DOS call, supplies no result, assumes no return, and does not execute either target ABI or substitute English state.

The callee's 48-byte local span additionally proves the exact request setup: it inherits the caller's DS:DX filename, establishes ES:BX = CS:$0708, and calls INT 21h with AX=$4b00. Its carry branch $0362 → $0369 skips the local AH=$4d child-status query and return. Those are byte-locked register/control-flow facts only: carry, child status, program return and both target ABIs remain unexecuted preservation boundaries.

Spanish 2200AD.EXE has its own 70-byte, hash-locked startup prefix at $d2cd (SHA-256 acbfcacc4cfac948944e42181f2fe0dfec11b9ab2c9b79b8aff79d958c5469c6). It establishes SS=CS, SP=$da00, prepares AX=$001f and ES:BX=CS:$d1bb, then calls private wrapper $0124. If that call returns, AL==$01 selects $d1be; every other value selects $d1d2. Project Eon provides no private result, takes neither branch, and creates no Spanish game state.

The immediate local continuations are separately hash-locked. $044e..$0455 (SHA-256 38889279a8b89e0e600bb25298015ccd8aadc09ea3858a1790097b3f7ff4ea8f) writes literal $01 to $da05 and returns. $0466..$047c (SHA-256 b17db26fa4fa8b7307fb767ff98351bd6dcca202829dd2d9348ff4991942d779) initializes CX to $10, reads 16 original table bytes at $0456..$0465 (00..07,38..3f), prepares AX=$1000 for INT $10, increments BL, and has a local loop edge. Register effects of the BIOS call are external, so this is not a claim that 16 calls occur; no host palette or screen is changed.

Millennium Amiga/Atari and Deuteros

Millennium Amiga loader requests and Atari bootstrap/config traps stop at verified raw boundaries; no unproven decompressor, OS trap, transform or game loop is emulated. The Defjam bootstrap requests raw $24200/$6e000 to $41000 then directly invokes it, with no caller-connected transform algorithm; its bytes are not direct 68000 code. The raw first-stage source has shared exec.library, graphics.library, and input.device text anchors (including table-like keyboard characters), but has no recovered runtime output mapping; those raw strings establish no executable input, graphics or display path. One independently admitted Amiga resident entry records a negative-D3 branch at $6850e → $68598. The 100-byte continuation at ADF +$16998 has SHA-256 716e8bf1db5d7cad89a0074cf6fe7cc6a0a66d73379814bac181a5f6c4a9e500; it contains an external JMP $7bcf8 at $685ee and a local alternate terminal tail at $685f4..$685fd (ADF +$169f4, SHA-256 5b120eaef941ac336d22e4f76adaeefd8c1d6795d105685f048074edd49c3a6c). The tail's two encoded $2800 immediates and RTS are recorded, but neither predecessor predicate, register effect, or external target is executed or assigned gameplay meaning. The immediately following local sequence $685fe..$68619 maps to ADF +$169fe, is 28 bytes, and hashes to a45ff5eca6e3594574b464574fa0aae3027bd2ea11472770708c96f4d21b56cc across all supplied variants. It records byte-store encodings to $7b3b5/$7b3bc, a D0 test, BNE.S $68612 → $68616 with an alternate RTS at $68614, and BPL.S $68616 → $6861a with alternate RTS at $68618. $6861a is a strict continuation boundary; no registers, cells, store, predicate, or next path is interpreted or executed. Deuteros title execution likewise stops at its first unknown Exec base/vector boundary $40450; only its static post-vector custom-register prefix is preserved, not invoked.

The Atari caller-connected target $2b2be..$2b3a5 is now hash-locked through its RTS (232 bytes, SHA-256 85c58759b0cb2f067734fb006aa543fc74926422187506914c823ceaaf9c6cd8). It stays wholly local but depends on caller-owned registers and pointers, so it provides no safe display/input result. Deuteros' static post-Exec continuation is likewise recorded through $40573, not executed; all Exec returns and later internal calls remain preservation boundaries.

Deuteros Amiga bootstrap evidence loads $2c00/$1600 → $12800; profile-zero requests raw stage $5800/$4200 → $20000 and enters $21734. The channel-request continuation at main-stage $21892..$218cb maps to ADF +$7092, is 58 bytes, and hashes to 120fba90e0b4fa9e96d8a6cf95fbac512d67d7daa42c3776ce0d3066b3f02ee9. It records only static branches, BSR/JSR targets, a bit-6 polling loop, and final $218c8 → $217f6 return edge; it does not choose a condition, invoke services, or simulate the input port. Its first BSR callee $2229c..$2232f maps to ADF +$7a9c, is 148 bytes, and hashes to d1a162af50f92b60d03b1da4ab186a547e46d145b0599cfbbeff7fb5af324ac1; bit-5 polling, the DBRA loop, library-vector calls and services are static evidence only. The second BSR callee $224a2..$224cb maps to ADF +$7ca2, is 42 bytes, and hashes to d4e9a1ee0065537a627cdd9ee8827f11d5fa28e0f860aacb21bbdc7e11784bd1; its low-memory/custom-register writes are retained only as static encodings and are never applied. The following returned service $22a5a..$22b89 maps to ADF +$825a, is 304 bytes, and hashes to d5fdbdacd004d2cf377ea0dbaefb9d8b308ba23b568cfb3785456622bde49d19; its embedded descriptors and flag/runtime-cell writes remain static facts. Its adjacent complete local entry $22b8a..$22be9 maps to ADF +$838a, is 96 bytes, and hashes to 10ed8be15c107dbb56ca98eb8d17ffd2bce3910dd169d67ba058447c9031b1ff. It tests byte $22a30, takes zero to $22b94 or returns at $22b92, then has four conditional descriptor-field copy encodings based at $22a6e with stride $000e and a final RTS at $22be8. These remain raw control/data-flow facts: no caller state, pointers, branches, or copies are executed. Return-dependent title tails record $206a0 → $12ff8, literal profiles 2/4/3 at $12ffc, then stop before JMP $12800. Predecessor returns must be caller-observed; no controller value is fabricated. Deuteros Atari also records a literal state-1 BRA.W encoding at Disk 1 +$9d800 and its byte-bounded printable block at +$9d80a (length $438, 18 runs, SHA-256 8dd46e7c760a38d07273b18a4cbd3c03eb44a6b57c8c401580dd47fa4646484e). The block is crack-era raw-media metadata, never original UI: it is not rendered, translated, parsed, or assigned a consumer. Deuteros Atari otherwise stops before unproven OS callbacks/resource semantics.

The common Deuteros title setup now has an independently hash-locked graphics boundary. $1ed80..$1edf5 (ADF +$79d80, 118 bytes, SHA-256 42c96aa502e36711ed274b9ddf4d2d1de53abfebb4ebdf88fa99346d2b03e30b) supplies literal graphics.library, D0 zero and Exec base $4 to vector -$228(A6). A zero result self-loops; nonzero is stored at $12fec, but no vector call/result is performed. Its next local helper $1eda6..$1edf5 (ADF +$79da6, 80 bytes, SHA-256 d6b37bc6431a1fe9145ae9403a5165028ccfd856a6529d1752f824b166807223) retains a 20-word RGB4 copy from $1ed24 (40 bytes SHA-256 5903a1c83619d7667c04ac1f3c923dfaa3a1ce0d090d6fd95109616a9b506a55) while propagating unknown display-base $12ff4. These are raw provenance facts only: no library, display memory, palette effect or title screen is synthesized.

The immediate caller-connected local callee at $1f182..$1f195 (ADF +$7a182, 20 bytes, SHA-256 9b02afb723e201cacb93d18d87613dee0f56369707867989209a41d9430ec5f3) reads external cell $1f168, clears D1, and encodes DBRA with D0 $1f3f: $1f40 sequential four-byte zero writes before RTS $1f194. The destination is unresolved; no host memory is cleared and the target is not named a screen.

The following local $1f196..$1f22f routine (ADF +$7a196, 154 bytes, SHA-256 31fc346d9d2647001899a2e939482aa97bd8bc94221ae81384787997928bb42b) bounds a four-pass byte combiner. It rejects unsigned D2 outside $40..$137 and D3 outside $24..$6f, selects an eight-byte source-table entry at $1f8ec from D0's low nibble, uses D3 stride $28, and advances unresolved $1f168 by $1f40 between four passes. Input registers, source table and destination remain unmaterialised; this is not a renderer.

The direct response helper $1f230..$1f259 (ADF +$7a230, 42 bytes, SHA-256 ed2794b7bb16f17ca9690b367c9465c75ff52838356bf6b46d9744cb16da1054) waits while word $1eed6 is zero, reads it again, and returns at $1f258 if it remains zero. A nonzero path returns the first byte at $1eec0, shifts 20 following bytes down by one with DBRA D1 $13, decrements $1eed6, and returns. Known title callers include $40638 and $407b4; Project Eon does not supply queue state, writers, reads, or concurrency.

Static backtracking identifies a producer but also the hard title-input boundary. Setup $1ef74 writes callback $1f056 into descriptor $1ef48 + $12 and crosses Exec vector -$1ce(A6). Callback $1f056..$1f14f (ADF +$7a056, 250 bytes, SHA-256 ff4b055b2d5128465c891debcad00ff4e53cbf661de47b9ee3d6278f33d5e5f8) has a local byte-one route that appends a byte from $1ee20 to $1eec0 + [$1eed6] and increments $1eed6. Registration, invocation, A0 ABI, table semantics, and the real input device remain unknown, so title input is not implemented.

The callback source interval is separately locked as $1ee20..$1eebf (160 bytes, SHA-256 2f00ffdf05ab28379e97e91e98fa764e45769d7ea55363846543becf7552e265). The original $50 bound plus conditional +$50 adjustment proves that this is the complete locally addressable source span, not the meaning of its bytes. The registration's direct RTS at $1f052, immediately after the Exec-vector call, also establishes that no local service result is inspected; it does not establish that the service returns.

More callback-local routes are now opcode-locked without assigning event meaning. The original mirrors A0+$04 to $1ef2e. Its event-two gate at $1ee16 either copies caller words A0+$0a/$0c to $1ee10/$1ee12 before an unresolved $20118 service, or masks a caller word with $7f, accepts only $68/$69, and derives a two-bit value from A0+$08 to $1ffd4. The byte-one producer selects a $50 half of the source interval from the low three bits of caller A0+$08. All A0 ownership, callbacks, services, input devices and byte meanings remain unresolved.

The Amiga title return path has two byte-locked graphics.library -$1a4 setup phases. At $4069a the first supplies $12e12, $1ffda, $20056, stores the last address at $2008e, and loops at $4071a while snapshots of $1ffc8/$1ffce/$1ffd4 still compare equal. A difference reaches the second setup, which substitutes $1ffe6 for the third address before the existing palette-restoration tail. No library call, poll writer, display allocation, bitplane layout or renderer is inferred.

For Deuteros Atari, the 20-byte, hash-locked continuation immediately after the supervisor callback's TRAP #14 is now recorded. It reads RAM word $25f4, compares $00071100, joins at stage +$f2 on its BEQ.S, and contains static BSR targets at +$800 and +$1122. The RAM provenance, XBIOS callback frame/results and both BSR effects remain unexecuted boundaries.

The registration body $1ef74..$1f051 is separately hash-locked (ADF +$79f74, 222 bytes, SHA-256 f571a8e5e48c29fe3d6f493e503e2a3a0b3328ac4cafb425808eff48804c4f27): request $1eefa + $1c receives 9, $1eefa + $28 receives descriptor $1ef48, and Exec base $4 is called through vector -$1ce. These operands establish an ABI-shaped data path, never a request type, event name, device, or runtime result.

Running safely

cmake -S . -B build -G Ninja -DEON_REAL_DATA_DIR=/path/to/game-data
cmake --build build --parallel
ctest --test-dir build --output-on-failure
./build/project-eon --inspect /path/to/game-data
./build/project-eon --verify-data millennium --data /path/to/release
./build/project-eon --game millennium --platform dos --presentation original --data /path/to/release
./build/project-eon --game deuteros --platform amiga --presentation modern --data /path/to/release

--data and --data-dir are aliases. Defaults are ~/.projecteon on Linux/macOS and <install-directory>\data on Windows; checking a missing default does not create it. Launcher strings use PO catalogues, including Swedish; original in-game text stays in supplied data.

Current runtime coverage

The launcher only admits a hash-recognised platform/release identity. Its unfiltered inspection report separates admission from coverage: READY / RECOVERED PATH does not claim complete game parity, and READY / BOOTSTRAP ONLY is used for Atari ST releases. A card with no recognised media is unavailable; a card with multiple verified original editions must receive a specific release-language selection before it can start. The project never falls back across DOS, Amiga, Atari ST, or original-language identities.

Millennium's DOS save inspector is read-only. It accepts either a direct 9,538-byte 2200SAVE.I file or the exact verified English DOS archive, then prints the strict envelope and 38 recovered positional records. It neither unpacks the archive nor gives unproven gameplay meanings to those record fields. The supplied archive and save bytes remain in place.

The Deuteros Amiga opening uses original VM events, original pixels, and the known initial audio route. When it reaches the title handoff, scheduling stops at that exact boundary. The view may expose raw 20-word RGB4 palette evidence and numeric VM provenance, but it does not claim to have allocated a title screen, invoked graphics.library, or run the unrecovered Exec/callback path.

For Millennium Atari, execution stops before GEMDOS Fopen results; for Deuteros Atari, it stops before protected XBIOS/callback/state selection. These are intentional, release-specific preservation boundaries. The scanner reports the exact physical-media and bootstrap evidence without executing a guest OS service.

Controls actually evidenced

These are Project Eon SDL controls, not an asserted original-game key reference.

Context Control Verified effect
Launcher: game cards Left/Right, Enter, Space, click Choose the game, then advance to platform cards
Launcher: platform cards Left/Right, Enter, Space, click Choose a hash-verified available platform; unavailable cards cannot start
Launcher: presentation cards Left/Right, Enter, Space, click Original/Modern start directly; Custom opens settings, then requires explicit start
Launcher D Open/refresh scanner
Launcher F1 Original/Modern presentation
Project Eon modern presentation F10 Open/close host graphics settings; Up/Down selects and Left/Right changes resolution, aspect ratio, pixel reconstruction, smooth scaling, scanlines, or frame
Launcher Escape Quit, or return to menu without direct --game
Launcher Gamepad D-pad Left/Right Select game card
Launcher Gamepad D-pad Up/Down Select platform when not forced by CLI
Launcher Gamepad South/A or Start Start selected card
Launcher Gamepad Back Quit, or return to menu without direct --game
Millennium English DOS title Any non-Escape/non-F1/non-F10 key Signals recovered DOS availability poll; launcher remains at the title boundary
Millennium DOS static evidence Left/Right, F1–F10 No live SDL binding; raw save/action paths are inspectable preservation evidence only
Deuteros Amiga opening Hold Space/Enter Held physical signal into recovered VM timing/gate; not proven original title-selection binding
Deuteros Amiga opening Hold gamepad South/A Same held physical signal as Space/Enter; no additional title/game action is inferred

The F10 popup is Project Eon renderer chrome, not an original F10 binding. It is consumed before the DOS title poll and changes no original asset, game logic, state, or save. No complete original keyboard/mouse reference, save/load semantics, menu meanings, special-action meanings, or full-game controls have been recovered.

Read-only physical capture runners

The repository contains two preflight helpers for reviewed external recorders. They are evidence-acquisition tools, not Project Eon runtime components or emulator distributions. Each requires an absolute path to a new cache output, refuses /tmp, repository and supplied-media output paths, and requires a visible X11/Wayland display. A physical operator must interact with the emulator window; headless SDL, debugger routes, playback and recorder-side input are not admissible.

The Millennium DOS route admits only the documented English DOS ZIP and a reviewed DOSBox-X binary. It rehashes the ZIP before and after a fresh archivemount -o ro view, requires ro,nosuid,nodev, forces the normal CPU core, and retains raw event/result/host-input-delivery files outside Git:

python3 tools/run_millennium_dos_capture.py \
  --source-release /absolute/path/to/Millennium-Return-to-Earth_DOS_EN.zip \
  --recorder /absolute/path/to/reviewed/dosbox-x \
  --output /home/you/.cache/project-eon-tools/millennium-dos-capture-<UTC>

The Deuteros Amiga route admits only the documented English release ZIP, Kickstart 1.3 ZIP and reviewed FS-UAE binary. It independently mounts the outer release, each nested disk archive and Kickstart archive with ro,nosuid,nodev,default_permissions, verifies the clean ADF/ROM hashes and sets floppy_write_protect = 1 for both drives:

python3 tools/run_deuteros_amiga_capture.py \
  --source-release /absolute/path/to/Deuteros-The-Next-Millennium_Amiga_EN.zip \
  --kickstart-archive '/absolute/path/to/Kickstart v1.3 r34.005 (1987-12)(Commodore)(A500-A1000-A2000-CDTV)[!].zip' \
  --recorder /absolute/path/to/reviewed/fs-uae \
  --timing-profile realtime \
  --output /home/you/.cache/project-eon-tools/deuteros-amiga-capture-<UTC>

realtime is the default and the only timing-faithful Deuteros profile. The finite warp profile is receipt-bound only for separately labelled diagnostic reachability work; it cannot establish original timing, gameplay, title-screen behaviour, or preservation behaviour.

Neither helper creates an admitted trace or runtime route. Raw recorder output, input timeline, configuration, ROMs, screenshots, audio and original media remain outside the repository. Independent review and the strict reference trace assembler/validator are still required before any captured observation can affect an adapter.

Receipt and ledger verification

The current runners write Millennium receipt v11 and Deuteros receipt v8. Verify a completed external capture without opening its original game media:

python3 tools/verify_capture_receipt.py \
  --kind millennium-dos --capture /absolute/cache/millennium-capture
python3 tools/verify_capture_receipt.py \
  --kind deuteros-amiga --capture /absolute/cache/deuteros-capture

The current receipts bind the complete recorder-console identity and bounded retained prefix. Millennium v11 additionally binds its reviewed machine profile, private-vector/handler recorder grammar, bounded host-delivery summary, and an explicit host-process early-stop reason only after the complete byte-identical eight-record diagnostic receipt: two Millennium returns, private-vector, handler entry/return, two title re-entries, then one unhandled-interrupt observation. Deuteros v7 additionally binds its reviewed timing profile, bounded host-delivery summary, and a raw-PC grammar that distinguishes the cycle-exact core's prefetched IR word from the memory word at the observed PC. Deuteros v8 recomputes an opaque per-site summary of those word pairs. Neither raw word is asserted to be an original-media instruction without separate execution evidence. Older receipt directories stay diagnostic-only and must be reproduced, never edited or upgraded in place. A successful receipt check proves only the external evidence route; it does not admit a trace or turn an unobserved game result into runtime behaviour.

Two fresh real-media no-input repetitions were independently accepted on 2026-08-31 after these receipt boundaries were tightened. The Millennium v10 repeat retained the same five-event and eight-result identities as the prior diagnostic and stopped after the complete ordered sequence; the source archive was unchanged after capture. The Deuteros realtime v7 repeat retained the same 256-record raw-PC identity (128 observations at each $1fe84 and $1fe96), with separate IR and memory words, no host-input receipt, and unchanged source and Kickstart archive identities. A fresh Deuteros v8 realtime run independently reproduced that same raw-PC identity and its two per-site 7202/7202 and 7208/7208 opaque word-pair summaries, with no host-input receipt. These repeats establish recorder-route reproducibility only. They do not establish a title bridge, input acceptance, frame, audio, ABI result, timing result, or gameplay behaviour.

A fresh Millennium v11 svga_s3 run on the same date independently reproduced the 367-byte event identity and the byte-exact 786-byte callback-loop receipt, with no host-input receipt. It stopped with the explicit external status known-unhandled-interrupt / 126 after 0.72 seconds, and the recognised archive remained unchanged. This confirms only the v11 host-stop contract; it does not establish a DOS/private-interrupt ABI, title result, input, frame, audio, or gameplay behaviour.

The repository's four preservation ledgers are cross-checked without opening any user media:

python3 tools/verify_preservation_ledger.py

It requires exact agreement on the recognised release universe, parser and recovery mappings, parity vocabulary, disassembly source provenance and span bounds. Linux, macOS and Windows CI run this check explicitly.

Modern asset packs

Modern packs are separate user-selected directories, never game-media subdirectories. The pack.eonmodern manifest binds independently created or licensed-derivative external art to one recognised release hash and verifies each asset's safe relative path, size, and SHA-256. Discovery is non-recursive and rejects symlinks, traversals, malformed manifests, changed files, and unknown releases. The current command is report-only:

./build/project-eon --data /path/to/original-media --inspect --modern-packs /path/to/packs

Inspection neither installs, selects, decodes, nor renders a pack. Explicit Modern launches may use only two finite, compiled renderer mappings after the same release recheck: a 640×400/1280×800 English Millennium DOS P00 title, or the English Deuteros Amiga held-input opening sequence. The latter requires all 82 hash-verified 640×400 or 1280×800 PNG frames at one complete tier; the 4× tier wins only when it is also complete. Each selected PNG is structurally validated and rehashed before upload.

The Deuteros sequence follows existing recovered VM ticks 1–82 only. It does not run an animation clock, generate an input event, change audio, alter the original source-frame composition, create a title screen, or continue beyond the original tick-82 handoff. Tick 82's external frame is displayed only when the original VM actually reached that held-input handoff. Original mode never opens, decodes, or selects external art. A missing, malformed, changed, or partial sequence is rejected as a whole; no mixed per-frame/tier fallback is permitted.

The authoritative format and integration boundary are in MODERN_ASSET_PACK_FORMAT.md.

Reference-trace capture and assembly

Reference traces are external, hash-addressed preservation evidence. They are not game data, replay scripts, emulator snapshots, or runtime input. Validating one proves only that its declared provenance and event grammar are internally consistent; it does not prove emulator correctness or gameplay behaviour.

Capture with an emulator or debugger capable of recording raw observations. Keep the original release read-only and retain its exact outer-release SHA-256 and byte length. Record the emulator binary/version/hash, configuration hash, command-tail hash, input-timeline hash, timestamps, and every path-specific required observation. Do not extract, copy, normalise, reorder, or alter original media to fit a recorder. ROMs, screenshots, audio, emulator configuration, traces, and all commercial media remain user-owned and must stay outside Git and packages.

The reviewed external DOSBox-X and FS-UAE recorders can retain bounded raw observations and physical-input delivery receipts, but no currently retained capture proves the full Millennium title-to-game transition or the Deuteros title display ABI. Deuteros still needs legal Kickstart evidence plus ordered Exec/graphics results, callback frames, display state, physical input, frame and audio checkpoints. Unknown runtime values are rejected; no synthetic ABI shim is substituted.

After capture, use tools/record_reference_trace.py only to assemble pre-recorded external evidence into a new user-owned capture directory. It accepts absolute, non-symlink regular input files; identity-checks them at open; rehashes the recognised source release before and after; copies only the external event stream; and writes a manifest plus an assembled-not-admitted receipt. It neither starts an emulator nor records, replays, normalises, or infers events. The output directory must be new.

python3 tools/record_reference_trace.py \
  --source-release /absolute/path/to/user-owned-release.zip \
  --events /absolute/path/to/external-recorder-events \
  --metadata /absolute/path/to/capture-metadata.tsv \
  --output /absolute/path/to/new-capture

Metadata is LF-only UTF-8 key<TAB>value. It declares capture provenance (format, game/platform/language, timestamps, emulator and configuration identity, command-tail and input-timeline hashes); the assembler derives event and source identity fields and rejects attempts to supply them. V2 metadata also declares one registered adapter, which is bound to its exact outer release; physical-media adapters additionally bind their source-media and stage hashes. Assembly is not admission: use the Project Eon CLI's --reference-trace validation only after capture is complete. A rejected capture must be retained or recollected, never repaired by changing event order, results, timestamps, or source identity.

The authoritative complete contracts are the Reference trace format, External reference-trace assembly, and preservation hash ledger.

Primary records