X-Wing vs TIE Fighter Static Recompilation

September 23, 2026 · View on GitHub

A static recompilation of Star Wars: X-Wing vs TIE Fighter (1997) and its Balance of Power expansion by Totally Games / LucasArts, targeting modern Windows with native x86 execution and a modern graphics pipeline.

XvT is the direct predecessor of the X-Wing Alliance engine — XWA (1999) is built on the same Totally Games flight engine. This project exists primarily to cross-validate the XWA recompilation toolchain and engine findings against the sister title: reproducing the same methodology on the older, simpler codebase confirms the tooling is general (not XWA-specific), and the shared engine internals (object/flight-group model, the DirectPlay single-player session/create subsystem, the software rasterizer) are often clearer here.

Project Status

PhaseStatusDescription
Phase 0CompleteMedia acquisition & extraction — Z_XVT__.EXE (1.34 MB) + XWINGTIE.EXE (launcher) extracted from the Master Game Disc ISO
Phase 1CompleteBinary analysis — PE parsed with the XWA toolchain: x86, base 0x400000, no SafeDisc, DirectPlay-heavy imports
Phase 2Complete (N/A)No copy protection — .text is directly analyzable, no runtime decryption dump needed
Phase 3CompleteFunction discovery — 1,787 with the unmodified XWA finder; 1,790 after fixing the finder (call-target closure, plus one data-referenced WndProc lifted by hand)
Phase 4Completex86-to-C code generation — 1,790 functions, ~364K lines of C. The XWA lifter needed no target-specific changes, but running the result exposed real lifter bugs (see below)
Phase 5CompleteCompilation and linking — CMake (MSVC 2022, x86) + main.c + generated imports.c; xvt_recomp.exe builds, links, and executes recompiled guest code. Required four fixes to the shared lifter/disassembler (see below)
Phase 6CompleteRuntime bringup — the guest VC4.x CRT startup runs unmodified into WinMain; a window is created and real Windows messages dispatch into recompiled code (2,924 calls / 1,114 icalls before the DirectDraw wall). Needed a native WndProc bridge and GetProcAddress native registration; needed none of XWA's CRT-global patching
Phase 7NextDirectX HAL — COM mocks. Interface version measured rather than assumed: XvT drives IDirectDraw v1 (SetDisplayMode takes 3 args), 640×480×8bpp fullscreen-exclusive. XWA's mocks are IDirectDraw4 — reuse their method bodies, not their slot indices
Phase 8Pending2D presentation — DirectDraw surface → window, 8-bit palette, input
Phase 9PendingFlight — .tie load → local craft spawn (sub_00409390) → software raster
Phase 10PendingCross-validation report vs XWA engine findings

See docs/roadmap.md for each phase's exit criterion and the deferral list.

Where the build currently gets to

$ ./build/Release/xvt_recomp.exe game_data/Z_XVT__.EXE
=== X-Wing vs TIE Fighter Static Recompilation ===
[*] Dispatch table: 1790 functions
[*] Mapping guest image range 0x00400000-0x00B60000 (7552 KB)
[*] Guest stack at 03FC0000-047C0000
[*] Loaded .text  VA 0x00401000  1127424/1127424 bytes
[*] Loaded .rdata VA 0x00515000  7680/7680 bytes
[*] Loaded .data  VA 0x00517000  75776/75776 bytes
[*] Loaded .idata VA 0x00B50000  5120/5120 bytes
[*] Registered 188 import bridges
[*] Entering guest at 0x004FD970 ...
[*] WndProc slot 0: guest 0x004C81E0 -> native 10001000
STUB: DDRAW.dll:DirectDrawCreate

Guest CRT startup → WinMain → RegisterClassA → CreateWindowExA → a real window receiving real Windows messages, all dispatched back into recompiled code. 2,924 recompiled calls and 1,114 indirect calls before it stops at DirectDrawCreate, which is still a stub (Phase 7).

Why This Repo — Cross-Validation with XWA

The XWA recomp has drilled down plenty: full boot, concourse rendering, screen-to-screen menu navigation, and a complete instruction-level map of the flight-entry path. It now carries the engine's own mission route into flight and submits real geometry — 230 meshes and ~3,950 vertices per frame, parsed from the actual .tie rather than fabricated by a force-launch path. The open problem there is placement: every object reports position (0,0,0), so the whole mission is stacked at the origin and projects off-screen. Arrival placement is the live question.

XvT runs the same engine family, two years earlier, with a smaller executable and a cleaner separation of the same subsystems. Recompiling it lets us:

  • Confirm reproducibility — the same toolchain (pe_analyze → disasm → lifter → generate) should recompile a second Totally Games title with only per-binary config changes. If it doesn't, the XWA success relied on target-specific hacks. (Confirmed: it does — and running the result surfaced eight genuine lifter bugs neither project would have found by compiling alone.)

  • Validate engine findings — cross-check the object/flight-group record layout, the FG→craft-type mapping (FG+2), and the craft-create flow against XvT's implementation.

  • Illuminate the XWA blocker — XvT's single-player craft spawn is local, with no DirectPlay involved (docs/directplay.md, docs/singleplayer-spawn.md), and the spawn loop sub_00409390 writes a complete per-craft recipe: flight-group stride 0x23, object record at fg+0x1F with stride 0xEC, active tag fg+0x1A = 2, render status obj+0x2CA ∈ {2,4}, and — the part that matters most right now — arrival placement, fg+0x10/fg+0x12 position components with RNG scatter and an orientation word at obj+0x77.

    Honesty about how that hypothesis has fared: the half of it that generalised to XWA was the field recipe. The half that did not was the inference that XWA's DirectPlay route must be the wrong path for single player. XWA has since established that it genuinely does run even a single-player mission through a local DirectPlay session (session name = the mission path with a ~ prefix), and that world build refuses to run until that session create returns non-zero — the fix there was to make the create take its host-with-one-player short path, not to bypass DirectPlay. The two engines diverge on this: XvT's DirectPlay really is multiplayer-only, XWA's is a single-player gate as well. Predecessor and successor are not the same program, and this is where assuming they were would have cost time.

Engine Lineage

X-Wing vs TIE FighterX-Wing Alliance
Release1997 (+ Balance of Power)1999
DeveloperTotally Games / LucasArtsTotally Games / LucasArts
EngineTotally Games flight engine (1st gen multiplayer)Same engine, extended (single-player campaign)
GraphicsDirectDraw + Direct3D IM (execute buffers), software rasterSame
MultiplayerDirectPlay (design core)DirectPlay (also drives SP object create)
CompilerVisual C++ (era: VC4.2/VC5 — confirm on extraction)Visual C++ 6.0
Dev pathK:\... (Totally Games convention)K:\XWA\dev\

Target Media (Phase 0 — done)

You supply your own copy of the game. The retail Master Game Disc carries the executables this project targets; extract them to a game_data/ directory at the repository root (git-ignored, along with any disc images):

ExecutableSizeDateRole
Z_XVT__.EXE1,369,088 B (1.34 MB)1997-04-15Main game binary (the recomp target)
XWINGTIE.EXE322,560 B1997-04-16Front-end launcher / loader

Both sit in the disc root and copy out directly; no installer or unpacking step is needed. The Multiplayer disc and the Balance of Power expansion are not required for anything currently implemented.

Binary Analysis — Z_XVT__.EXE

Parsed with the unmodified XWA toolchain (pefile), confirming the pipeline generalizes to a second Totally Games title:

PropertyValuevs XWA
Architecturex86-32, PE32same
Image base0x00400000same
Entry point0x004FD970—
Timestamp1997-04-162 yrs older
Linker version4.20 (Visual C++ 4.x era)XWA = VC6
Copy protectionNone — no SafeDisc, no .bind, .text directly analyzableXWA = SafeDisc v1 (runtime decryption)
Relocations.reloc present (relocatable)XWA = none (fixed base)
Code (.text)0x401000, vsize 0x113204 (~1.1 MB)XWA = ~1.7 MB
Data (.data)0x517000, vsize 0x638BE0 (~6.2 MB)XWA = ~5.4 MB

Import Summary

DLLFunctionsPurpose
KERNEL32.dll109Core Win32 APIs
USER32.dll33Window management, input
GDI32.dll17Font/text rendering
WINMM.dll14Joystick, timers, CD audio
MSACM32.dll5Audio codec (ACM) — FMV/voice
ADVAPI32.dll4Registry (settings)
DPLAYX.dll3DirectPlay (multiplayer) — 3× XWA's usage
DDRAW.dll1DirectDraw
DINPUT.dll1DirectInput
DSOUND.dll1DirectSound

First cross-validation observations

  1. The toolchain reproduces — end to end, confirmed. The full unmodified XWA pipeline (pe_analyze → disasm/function-finder → lifter → generate) ran on Z_XVT__.EXE with zero target-specific changes and produced 1,787 recompiled functions / ~364K lines of C. XWA's success was not the result of XWA-only hacks — the same tools recompile a second Totally Games title as-is.

  2. XvT is simpler to recompile than XWA — no SafeDisc means the .text needs no runtime decryption dump (tools/dump_memory.py is unnecessary), and .reloc gives us ground-truth for pointer sites.

  3. DirectPlay is more prominent (3 imports vs XWA's 1), matching the multiplayer-first 1997 design. This is the payoff for the effort: the session/create subsystem that gates XWA craft instantiation (XWA memory #48–75) should surface here more directly, since in XvT the DirectPlay object-create path is the primary flow, not a single-player special case.

    (Subsequent static analysis reframed this — see docs/directplay.md: XvT's DirectPlay is multiplayer-only, and single-player spawns craft through a local path, sub_00409390. That reframing is itself the cross-validation result, and it points at the XWA blocker from the other side.)

  4. The toolchain had four general bugs, found by making XvT run rather than just compile. Static output looked fine; execution did not. The most consequential: shr/shl/sar were not registered as flag setters, so any jcc following a shift silently matched the previous flag-setting instruction. In XvT's memset this inverted a branch and turned a 64 KB fill into a 4-billion-byte one on the first frame of CRT startup. The other three: loop targets were not basic-block leaders; int3-only blocks emitted a label with no statement; and discovered call targets were never fed back into function discovery. All four are in XWA's generated code too — regenerating XWA with the fixed toolchain is the first concrete finding flowing back, and it flows XvT → XWA exactly as this repo intended.

  5. XvT's CRT is materially easier than XWA's. XWA had to bypass its VC6 CRT startup entirely (it crashed in locale/env init) and hand-initialise the small-block-heap threshold, the CRT lock table, the heap handle and _pctype before calling WinMain directly. XvT's VC4.x CRT startup runs unmodified all the way into WinMain — no bypass, no CRT-global patching, and g_manual_overrides[] is still empty. Two years earlier really is two years simpler, which is the cross-validation thesis holding up.

  6. And the traffic runs both ways — XWA has since found four FPU bugs XvT still has. While chasing garbage vertices in its flight view, the XWA effort traced them to the shared lifter: the x87 stack was modelled per-function (so any guest function returning a value in st(0) handed back a zeroed local — which made the CRT __ftol return 0 for every float→int conversion); fstp st(i) was off by one; fxch lifted to a no-op; and, most insidiously, the ST(i) register IDs were hardcoded to Capstone 4's numbering while both projects build against Capstone 5, so every FPU register operand indexed negatively into the stack array. XvT's output has all four. None has bitten yet — nothing before the DirectDraw wall touches the FPU — but flight math touches all of them, so they are queued as one batched fix ahead of Phase 9. See docs/roadmap.md → "Known issues, queued".

    Which is the cross-validation working as designed, in both directions: XvT found four lifter bugs by running where XWA had only compiled, and XWA found four more by getting far enough into 3D to see them.

Methodology (mirrors XWA)

The XWA toolchain is vendored into tools/ — copied verbatim, then fixed where running XvT exposed general bugs (the fixes apply to any target, XWA included):

recomp/
├── tools/                      # Python toolchain (vendored from XWA, + fixes)
│   ├── pe_analyze.py           # PE header/section/import parser
│   ├── disasm.py               # Capstone disassembler + function finder (recursive to closure)
│   ├── lifter.py               # x86 → C lifter
│   ├── translator.py           # Pipeline driver (`python -m tools`)
│   ├── gen_bridges.py          # Import-bridge generator (bootstrap only)
│   └── lift_extra.py           # Lift individual missed functions
├── src/game/
│   ├── main.c                  # Entry, memory setup, dispatch, VEH handler
│   ├── imports.c               # 188 Win32/DirectX import bridges (generated, then hand-maintained)
│   ├── com_mocks.c/.h          # COM mocks — Phase 7, not yet written
│   └── recomp/
│       ├── recomp_types.h      # Register/memory/dispatch model (from XWA, unchanged)
│       └── gen/                # Auto-generated lifted C (gitignored, ~364K lines)
├── config/                     # pe_analysis.json, functions.json
├── docs/                       # roadmap + engine analysis notes
├── CMakeLists.txt              # MSVC 2022 x86 build
├── LICENSE                     # MIT (this project's own source only)
└── README.md                   # This file

dump_memory.py is not vendored — XvT has no copy protection, so there is nothing to dump. d3d11_renderer.c is deferred until the simpler present path proves too slow.

The recompilation approach (global register model g_eax…g_edi/g_esp; identity-mapped memory via MEM32/ADDR; three-tier RECOMP_ICALL dispatch; pattern-matched flag conditions) is identical to XWA — see the XWA README. Divergences unique to XvT (older CRT, different DirectX minor version, protection scheme) are documented as they are found.

Building

Requires CMake 3.20+, Visual Studio 2022 (MSVC, x86/Win32 target), and Python 3.10+ with capstone and pefile.

The generated code under src/game/recomp/gen/ is git-ignored, so a fresh clone regenerates it first (~35 min; needs game_data/Z_XVT__.EXE from your own disc):

python -m tools game_data/Z_XVT__.EXE --all -o src/game/recomp/gen --split 1000 \
    --functions-json config/functions.json

cmake -B build -G "Visual Studio 17 2022" -A Win32     # 32-bit is required
cmake --build build --config Release

./build/Release/xvt_recomp.exe game_data/Z_XVT__.EXE

The executable links at 0x10000000 with ASLR disabled, so the guest's original address range (0x00400000–0x00B60000) stays free to be mapped identically — guest VAs are host addresses, which is what makes the lifted MEM32(0x517108) style accesses work unmodified.

Set XVT_ENTRY=0x4XXXXX to enter the guest somewhere other than the PE entry point (0x004FD970, the guest CRT startup).

This project is for game-preservation purposes. You must own a legal copy of Star Wars: X-Wing vs TIE Fighter to use it. No copyrighted game assets, disc images, or executables are included in this repository — you supply your own, and game_data/ plus all disc-image formats are git-ignored. Neither is the lifted C: it is a derivative work of Z_XVT__.EXE, so it is generated locally from your own copy (see Building) and src/game/recomp/gen/ is git-ignored too.

The project's own source — the toolchain, runtime, and documentation — is released under the MIT License. That covers this repository's code only; it grants no rights in the game itself, which remains the property of LucasArts (now Lucasfilm / Disney), or in the lifted C. See the Scope section of LICENSE. Star Wars and X-Wing vs TIE Fighter are trademarks of their respective owners; this project is unaffiliated with and unendorsed by them.

Part of the sp00nznet recompilation collection:

  • pcrecomp — the family's shared recompilation toolkit. This repo's tools/ came from xwa before the lifters were unified there. It is a separate lineage and has not been rebased onto pcrecomp.

  • xwa — X-Wing Alliance recomp (sister engine; primary reference for this project)

  • burnout3 — Xbox x86 recomp (x86-to-C lifter reference)

  • bw — Black & White Win32 recomp (Win32 game patterns)

  • civ — Civilization DOS recomp (16-bit x86 lifting)