ReverseApoc is a byte-matching decompilation of X-COM: Apocalypse (Mythos Games / MicroProse, 1997, DOS). The goal is readable C source that, compiled with the original Watcom toolchain, rebuilds the shipped executables byte for byte. The project exists to help the OpenApoc team understand the original logic, mechanics and bugs, the enemy AI in particular. It is not a port: there is no plan to make the game run natively on modern systems. It is also a proof of concept for AI-assisted reverse engineering with Claude Code, where every result is checked by a deterministic byte comparison.
Important
This repository contains no game assets or code bytes. No original executables, no disassembly, no data files and no compiler binaries are committed. You need your own copy of the game (Steam or GOG) and, for matching, your own copy of the Watcom compiler. Disassembly (asm/) and build output (build/) are regenerated locally from your files and are gitignored. See docs/legal.md.
The progress page shows, for each EXE, its three figures (each with a one-line definition), its percent bar and a strip of one tick per game and thunk function in address order, then this mosaic. Once config/mechanics.toml maps the OpenApoc checklist items to the original code, a Mechanics coverage section lists each item and tier with two figures: must match (the functions the item needs, stopping at hub functions many items share) and full closure (every game or thunk function its call tree reaches). A tier reads matched only when status.csv says every must-match function is.
The six badges (per EXE its strict, functions and fuzzy figures, each coloured by its own value), the mosaic, the progress page and the raw numbers (progress.json, as percent, functions_percent and fuzzy_percent) are regenerated on every push to main, together with the combined progress-shield.json, progress-shield-functions.json and progress-shield-fuzzy.json endpoints (kept for consumers, not shown here) and the objdiff-shaped report.json, and published to GitHub Pages. They come from committed tables: the status tables that reverseapoc gate writes (config/ufo2p/status.csv, config/tacp/status.csv; never edited by hand) plus each EXE's functions.csv and units.csv. Each EXE, and both combined, has three figures:
- strict, the headline and the only one proven by byte comparison: matched bytes of game and thunk functions over all game and thunk bytes plus the uncovered bytes (thunks count as code); the combined figure comes from summed bytes, not an average;
- functions, the function ratio: matched game and thunk functions over all game and thunk functions, a count, not a size;
- fuzzy, objdiff's closeness with partial credit: each game and thunk function's best objdiff score (100% when matched, its best when partial, 0 otherwise) weighted by its bytes, over the same bytes as strict; report.json carries the same number as
fuzzy_match_percent.
The mosaic has one cell per function, grouped by EXE and unit and sized by bytes:
- matched: byte-identical to the original, proven by objdiff, the relocation check and the whole-EXE SHA-1;
- partial: an attempt scored above 0% in objdiff but is not byte-identical yet; it counts for nothing in the strict percentage or the function ratio (only the fuzzy figure gives it credit);
- unmatched: no gate-written matched or partial row; the rebuilt EXE still carries the original bytes of the function, kept by the in-place splice;
- library: the Watcom runtime library, not decompiled and outside all three figures; data-in-code is excluded too. Game functions on the owner-signed assembler list (
config/asm-functions.csv: hand-written assembly, which cannot match from C) are drawn and counted with the library, outside all three figures, in the badges, the progress page, progress.json and report.json; decomphub.progress.json still counts them as game code; - uncovered: bytes inside a unit span that no functions.csv row of any kind covers (runtime and data-in-code rows count as covering), mostly alignment filler between functions. They are in the denominator. The 3-byte tail of UFO2P's code that lies outside every unit is not counted.
Each unit with room for a label carries one: a game unit its unit name (the name its C file takes under src/ufo2p/ or src/tacp/, such as u_00089980), a library unit the name of its largest runtime function (such as __int7, the FPU emulator). A name too long for its unit is cut short and ends with an ellipsis, as long as at least six of its characters still fit. Small units have none.
The strict and functions figures, per EXE, are committed for DecompHub (a percent and a ratio; it has no fuzzy track): Progress data. That file changes only when a gate EXE/UNIT run passes.
uv run reverseapoc drive runs unattended matching nights. Each attempt at one function is one fresh Claude Code
session in a locked-down checkout, and nothing the session claims counts: the driver compiles and compares the bytes
itself. In plain terms, Claude writes a recipe (the C), the original compiler bakes it, and only an exact copy of the
original counts.
%%{init: {"flowchart": {"wrappingWidth": 260}}}%%
flowchart TD
A["Step 1: preflight<br/>Docker, CLI checked<br/>clean git tree<br/>asm regenerated"] -->|a check fails| R["REFUSED<br/>nothing starts"]
A -->|all checks pass| B["Step 2: pick a function<br/>easiest first<br/>Ghidra draft as hint"]
B --> C["Step 3: context pack<br/>target asm<br/>neighbours and draft<br/>compiler rules"]
C --> D
subgraph S["Step 4: one Claude Code session, guardrails on"]
D["Edit the C<br/>of its one unit file"] --> E["Step 5: attempt check<br/>up to 25 per attempt"]
E --> F["Watcom C 10.6<br/>compile in container"]
F --> G["objdiff score<br/>relocation check<br/>splice into EXE copy<br/>whole-EXE SHA-1"]
G -->|not identical yet| D
end
G -->|session ends| H["Step 6: driver check<br/>file audit, C89 lint<br/>full gate"]
H -->|byte-identical| M["MATCHED<br/>one commit with<br/>explicit paths"]
H -->|not identical| P["PARTIAL<br/>best C kept as notes<br/>unit reset"]
H -->|write outside the claim| X["Guardrail stop<br/>strays restored<br/>night ends<br/>nothing committed"]
P -->|under 3 attempts| C
P -->|3 attempts| L["Difficult list<br/>retried next pass"]
classDef matched fill:#008300,stroke:#005a00,color:#ffffff
classDef partial fill:#eda100,stroke:#a86f00,color:#0b0b0b
classDef stop fill:#52514e,stroke:#2b2a28,color:#ffffff
class M matched
class P,L partial
class R,X stop
- Preflight. Before anything starts, the driver checks that Docker answers and the build container is in sync, that the Claude CLI is in the tested range, and that the git tree is clean, so every later change can be traced to a session. It compiles the Python tooling once (sessions never write cache files) and regenerates the disassembly of both EXEs. Any failure is a refusal: nothing runs.
- Pick a function. The picker takes small, easy functions first, alternating UFO2P and TACP, at most two per unit, and skips anything already matched or on the difficult list. Ghidra adds a rough C draft for each pick as a hint (some picks have none).
- Build the context pack. One file per function with what a session needs: the target disassembly, the neighbouring functions, the Ghidra draft and the Watcom coding rules.
- One Claude Code session. The driver starts a fresh
claude -psession with the pack and thewatcom-decompskill. Guardrail hooks deny almost everything: the session may edit only its one unit file (for examplesrc/ufo2p/u_00089980.c), add a header or write its notes, and it may run only three commands:attempt context(re-read the pack),attempt asm(read the target disassembly) andattempt check. A canary run proves the hooks deny before any night starts. attempt check. Inside the container, the unit is compiled with the original Watcom C 10.6 compiler and flags (-4r -s -fpi). objdiff scores the new function against the original, a relocation check confirms every reference lands on the same target, and the function is spliced into a copy of your own EXE, whose SHA-1 must equal the retail file's. The session reads the result, edits the C (a type, the statement order, a loop's shape) and checks again, at most 25 times per attempt.- The driver checks the work. When the session ends, the driver audits every file it touched (a write outside the claim stops the night), lints the unit as C89 and runs the full gate in the container. Only the gate's exit 0 makes a match.
| Result | What it means | What the driver does |
|---|---|---|
| MATCHED | The C compiles to the original bytes: objdiff 100%, the relocations agree, and the spliced EXE keeps the retail SHA-1. | One commit with explicit paths (the unit, status.csv, decomphub.progress.json, progress/attempts.jsonl), marked Matched-By: reverseapoc drive. It counts in the progress figures. |
| PARTIAL | The C scores above 0% in objdiff but is not byte-identical yet. | The best C is kept under build/ with its score, the unit is reset, and the function goes back in the queue. After 3 attempts it moves to the difficult list for a later pass. It counts for nothing in the strict percentage or the function ratio; the fuzzy figure gives it partial credit. |
On the first pilot night (run 20261026-005209), 40 sessions gave 3 MATCHED and 37 PARTIAL; the closest partials
reached 87.5% and 87.2%. At the end of every night the driver rebuilds both EXEs, checks their SHA-1s and re-runs the
gate on every match before it commits the attempt log.
Details (flags, the refusal reasons, the canary, reset rules and the console): docs/workflow.md.
Only the DOS release is supported. Steam and GOG ship byte-identical game executables, so either store works (Steam is preferred, GOG is the fallback).
| File | Role | Size (bytes) | md5 | Scope |
|---|---|---|---|---|
UFOEXE/UFO2P.EXE |
Main game program, module ufo24 |
1,705,790 | d2e874a76344cb9c48afce57357aab31 |
Target |
TACEXE/TACP.EXE |
Tactical program, module tac4 |
3,161,594 | 644382a6caa787cff613c50a170b5152 |
Target |
UFOEXE/SMKP.EXE |
Smacker video player | 271,436 | 2d7933f255348a60536d811391b461ba |
Deferred |
XCOMAPOC.EXE |
16-bit launcher | 18,171 | 867f7997fae1714313b873f15359c4fb |
Out of scope |
SHA-1 values go into the hash manifest in Phase 2. SETUP.EXE (Borland C++, 1991) is out of scope. The disc image that both stores ship (cd.iso on Steam, CD/XCOM.BIN on GOG, byte-identical) contains the original v1.00 builds: UFO2P.EXE 1,702,206 bytes with CRC32 0x4749FFC1, the value OpenApoc's extractors expect, and TACP.EXE 3,170,298 bytes. The installed files above are a later build. OpenApoc reads from the disc image, so its offsets describe the CD build; this project targets the installed build first and records both (docs/toolchain.md).
A Windows 95 release also existed. On what it contains, OpenApoc community member FilmBoy84 recalls:
Yeah, IIRC the Win9x version is just an installer wrapper. Game beneath is identical and just uses DOS 7.x that's part of Win9x instead of DOS 6.2x that was the last packaged version of MS-DOS
(OpenApoc Discord, FilmBoy84, 2026-10-15, message link)
This is a recollection, not a verified fact. Hashing a retail Win9x executable against the DOS one is welcome future work.
ReverseApoc uses a hybrid solution: our own matching oracle, plus decomp.wtf's "fork mode" as the agent layer. (Phase 6 update: spike 006 INVALIDATED fork mode (plan 06-01), so the agent layer is our own (D-03, D-04); see the notes below.)
The oracle (ours, assembled from reusable parts):
- The original Watcom C 10.x
wcc386, supplied by you, runs under wibo inside a Docker image. - A Python
configure.pygenerates a ninja build with per-file compiler flags. - A Python LE splitter reads the executables. Matched functions are spliced in place into a copy of the original EXE, and a whole-EXE SHA-1 check gates every build. The DOS/4GW Professional kernel stays an opaque blob taken from your own file.
- An OMF-to-ELF converter lets objdiff diff Watcom objects and produce progress reports.
The agent layer:
- decomp.wtf v0.10.6 in fork mode, pinned and lightly patched, installed in WSL2 and run as a separate tool (a subprocess, never imported). Fork mode accepts a local repo that builds with
configure.py+ ninja and producesobjdiff.json. It judges candidates with objdiff and brings function claims, MCP tools, guardrails and a dashboard. (Not used in Phase 6: spike 006 INVALIDATED fork mode (06-01). The agent layer is our own (D-03, D-04), with no MCP server (D-44); agents call the Bash-allowlisted attempt wrapper. decomp.wtf is re-evaluated for Phase 7.) - Our driver re-verifies the bytes before any commit. An agent never judges its own match.
- If objdiff cannot diff the converted objects cleanly (a Phase 5 spike), the fallback is our own minimal agent layer: an MCP server, claims and guardrails, Claude Code only. (Phase 6 update: objdiff diffs the converted objects (spike 005 VALIDATED), but spike 006 INVALIDATED fork mode, so Phase 6 builds its own agent layer anyway: the driver, the guardrail hooks and the context pack, with no MCP server (D-44); agents call the Bash-allowlisted attempt wrapper.)
The order of work follows Abahbob's rule: get a byte-identical rebuild of raw bytes first, decompile second.
| Project | License | What we use |
|---|---|---|
| KKND-Decomp | CC0-1.0 | LE reader, in-place page splice with SHA-1 gate, function discovery tools |
| Theme Hospital decomp | GPLv3 | omf2elf.py, the configure.py + ninja + objdiff pattern; the reference for a 100% matched Watcom 10.6 DOS/4GW game |
| NHL95PC | No licence file; used with the owner's permission, with attribution | tools/reverseapoc/search.py adapts the declaration pattern and the type-move loop of its tools/permute.py, the block search of tools/scope_decls.py and the seeded shuffles of tools/permute_decls.py; tools/reverseapoc/classify.py adapts the register pattern of its tools/match/classify_diff.py |
| retrokit | Apache-2.0 | Headless Ghidra LE loader, Watcom calling conventions, function export, MCP server (external tool) (not used: Phase 5 runs Ghidra headless in the Docker image with the lastonesky loader) |
| decomp.wtf | MIT (archived 2026-10-15) | Agent harness, claims, guardrails, dashboard; used in fork mode as a separate tool (not used in Phase 6: spike 006 INVALIDATED fork mode; the agent layer is our own, D-03/D-04) |
Files copied from Theme Hospital keep their copyright headers. That is why tools/ is GPLv3.
- Compiler: Watcom C 10.6
wcc386(C, not C++) under the default rule-4r -s -fpi. The runtime library is 10.6 byte for byte; on game code 10.5, 10.6 and 11.0 stay tied after the two Phase 5 leaf matches, and later matches are logged to separate them (see docs/toolchain.md). - Extender: DOS/4GW Professional, bound into each EXE (LE format, two objects, no debug info).
- Analysis: Ghidra 12.1.4 with the lastonesky ghidra-lx-loader, headless in the Docker image.
- Diff and reports: objdiff, behind an OMF-to-ELF converter.
- Tooling: Python 3.14 with uv, everything build-related in Docker.
Details, evidence and pinned versions: docs/toolchain.md.
Bring your own files. Nothing below downloads the game, and the compiler download is a separate opt-in command (see the end of this section).
- Copy
.env.exampleto.envand pointAPOC_STEAM_FOLDER(orAPOC_GOG_FOLDER) at your install. uv run reverseapoc ingestcopies the executables into the gitignoredorig/and checks their hashes.docker compose builddocker compose run --rm build uv run pytestgit config core.hooksPath .githooksenables the trackedcommit-msghook, which adds aCo-Authored-Bytrailer to commits made under Claude Code (including GSD's own commits and merges). Override the value withgit config reverseapoc.coauthor "Name <email>".
docker compose run --rm test runs the test suite in the container in one step. To get the original Watcom compilers, run uv run reverseapoc fetch-watcom and then uv run reverseapoc install-watcom (opt-in, at your own responsibility); they land in the gitignored third_party/watcom/. Sources and details: Getting the Watcom compilers.
- docs/workflow.md: the per-function loop, the guardrails and the Milestone 1 roadmap.
- docs/ceremonies.md: which review, simplify and comment passes run on Python tooling and which may touch matched C.
- docs/model-routing.md: the model arms (Claude Code, Codex CLI, Antigravity CLI, OpenRouter) and their guards; docs/model-comparison.md: what each model measured on our matching task, per size bucket.
- GSD structures milestones and phases (discuss, plan, execute, verify). superpowers drives the work inside a phase (brainstorming, TDD on tooling, verification before completion).
- Coding rules: docs/coding-style.md. Matching notes: docs/matching.md. Journal: docs/learnings.md. Design: docs/superpowers/specs/2026-10-19-reverseapoc-design.md. Research: docs/research/.
The Substack archive gives the same date for Macabeus's first two chapters, so chapter 1 may be older.
- AnyPS5: README layout, progress mosaic and badge pattern (a porting tool, not a decomp).
- sm64 and pokered: SHA-1 tables and the bring-your-own-files convention.
- augustus: a reimplementation of Caesar III, not a decomp; README structure only.
- awesome-game-decompilations, awesome-decomp-games and PCGamingWiki's list of unofficial ports: where to look for comparable projects.
- NHL95PC: Watcom 10.0 DOS game, compiler pinned by library matching.
- Blood-RE: matching decomp built with Watcom 10.6 and TASM 3.1.
- f15se2-re: F-15 Strike Eagle II, the only MicroProse DOS reconstruction found (Microsoft C 5.1).
- fnv_decomp: Fallout: New Vegas, matched function by function with MSVC 2005/2008 by about 50 volunteers' AI agents through a claim server (fnvdecomp.com; agent rules at f3decomp.com/agents.md). What we take from it: research note.
- OpenApoc: the reimplementation this project serves.
- OpenXcom: reimplementation of UFO: Enemy Unknown and Terror from the Deep, not Apocalypse.
No Watcom matching decomp of an X-COM title turned up in the lists searched. ReverseApoc is the closest found, not a claimed first.
Verdicts in short; the full table with reasons, install method and status is in docs/tools.md.
| Area | Use | Later | Skip |
|---|---|---|---|
| Analysis | Ghidra, GhidraMCP (Later) | REA | BSim |
| Diff and matching | objdiff | coddog (idea reimplemented now), mizuchi, decomp_settings | m2c, decomp-permuter, asm-differ, Kappa (two ideas ported) |
| C source tools | ast-grep, clang-format | ||
| Loops | Nigel, /goal, autoresearch (pattern) | Ralph loop (prototype) | Bilevel-Autoresearch |
| Planning | GSD, superpowers, OpenSpec spec format (Phase 8) | OpenSpec tool, SpecKit | |
| Understanding | Understand-Anything, Groma | ||
| Style and review | i-have-adhd (chat and comments) | Ponytail, pstack, thermo-nuclear review, cpp-pro, Mindrally cpp, Qt agent-skills | |
| Retrieval | jina embeddings, Voyage (optional), SQLite + numpy | pgvector, RxDB |
- recomp.fyi: listed through a
.recomp.jsonat the repo root; it reads an objdiffreport.jsonartifact daily for the strict percentage (matched_code_percent). - decomphub.com: submitted through its reviewed form at https://app.decomphub.com/submit once the repo is public (no file format, no badge).
- decomp.dev: closed to new projects in October 2026, so progress is self-hosted on GitHub Pages.
- Decompedia: community wiki on decompilation, a place to be listed later.
- decomp.me: shareable scratches for hard functions (it has
wcc10.5,wcc10.6andwcc11.0). Used sparingly; we do not flood it with agent scratches.
AI tools, Claude Code in particular, are used openly in this project, from research to decompilation attempts. No AI output is trusted on its own: every match is verified by byte comparison (objdiff, relocation checks and the whole-EXE SHA-1) run by the build, not by the agent. Prior reverse engineering by the OpenApoc community (its data extractors and ongoing Ghidra-based work) is credited wherever it is reused.
The tooling under tools/ is GPLv3, because it includes modules from the Theme Hospital decomp with their headers kept. The decompiled C under src/ reproduces copyrighted game logic for interoperability and research and is published to support OpenApoc; no rights in it are claimed and no assets ship with it. The behaviour specs under specs/ (Phase 8) are CC0-1.0, written in our own words for clean-room reimplementation; see docs/clean-room.md. See docs/legal.md.
Use GitHub issues for bugs, questions and proposals. Please never attach game files, executables or disassembly to an issue or pull request.
You can also spend your own AI plans on matching nights. This is how.
- Your own copy of the game.
uv run reverseapoc ingestcopies the executables intoorig/and checks their hashes. Nothing copyrighted ships with this repo. - The Watcom C 10.x compiler, which you supply yourself (see Getting started).
- Docker and uv.
- Whatever plans or keys you choose to spend: a Claude Code plan, the Codex CLI on a ChatGPT plan, the Antigravity CLI on a Google AI plan, an OpenRouter key. You pay for what you use. Nobody else's account is involved.
- Follow Getting started on your own clone, then run
docker compose run --rm test. - Run
npx @opengsd/gsd-core@1.18.0 --claude --localandgit config core.hooksPath .githooksonce per clone. - A drive runs only in the main checkout of a clean clone. Do not run two drives at once, and write nothing under the checkout while one runs.
Each arm proves its guardrails once, with one real session, before its first night. The arms are in config/driver/backends.toml.
- A Claude arm:
uv run reverseapoc drive --canary --night-model claude-haiku-5-5. - A hosted or non-Anthropic arm adds its egress opt-in:
uv run reverseapoc drive --canary --night-model deepseek-flash --allow-egress deepseek-flash. - A Codex or Antigravity arm will not run in a night until its canary has passed on your installed CLI (
build/driver/canary/<arm>.json).
Say what you want to spend, in your plan's own terms, with --arm-budget NAME=BUDGET. BUDGET is <p>%week, <p>%5h, $<usd> or <n>credits. The driver turns it into a session cap from config/driver/quota.toml. The cap is never above the arm's max_sessions. Quote a $, or your shell will eat it.
Always look at the dry run first. It spends nothing:
uv run reverseapoc drive --dry-run --arm claude-haiku-5-5=10 --arm-budget claude-haiku-5-5=5%week \
--arm deepseek-flash=1 --arm-budget 'deepseek-flash=$10' --allow-egress deepseek-flash \
--max-sessions 200
It prints one line per budget: the budget, the quota one session takes (with its source and date), the sessions it buys and the matches to expect at the measured rate. Here: 166 Haiku sessions (about 8.5 matches) and 33 DeepSeek sessions (about 13, from a small sample). The same command without --dry-run runs the night. The figures are rough; see Quota per session. Claude Code users can ask the drive-budget project skill to build the command from a plain request.
A non-Anthropic or hosted arm sends disassembly of the game off your machine, to that provider. The driver refuses such an arm unless you name it with --allow-egress ARM. Opt in only if you accept that. A local model (qwen-local) sends nothing.
- A match is a commit the driver makes on your clone, after the container gate proved it byte for byte. Open those commits as a pull request.
- CI and the owner re-run the gate on every function. Bytes prove a match, never an agent's word.
- Credit goes in the commit trailers:
Matched-Bynames the driver (and a hosted arm's model),Co-Authored-Bynames each model that held the function, and the commit author is you.
- Never commit game files, disassembly or build output (
orig/,asm/,build/,third_party/). - Never edit
orig/, the hash manifests underconfig/or the build scripts to make a check pass. - A matched function stays byte-identical: no restructuring, no inline assembly to force a match.
- Claude Code is the agent runtime here. Codex CLI and Antigravity CLI may run only as
reverseapoc drivesessions. Commit no config for other runtimes (no.codex/,.gemini/orAGENTS.md). - No secrets in tracked files. Keys live in your user environment.
The full rules are in CLAUDE.md.