# Crimson Skies — Static Recompilation A static recompilation of **Crimson Skies** (2000) by Zipper Interactive / Microsoft, targeting modern Windows with native x86 execution and a modern graphics pipeline. This is a game preservation project. Crimson Skies is a beloved arcade flight combat game with **zero** existing preservation efforts — no source port, no decompilation, no community engine. The original game suffers from SafeDisc DRM (non-functional on Windows 10+), broken hardware acceleration on modern GPUs, and text rendering corruption. It deserves better. > *"Welcome to the world of air combat, 1930s style."* ## Why This Game? - **No one else is doing this.** Unlike Doom, Quake, or even X-Wing Alliance, Crimson Skies has no community source port or reverse engineering project - **It's broken on modern systems.** SafeDisc DRM was killed by Microsoft in 2015. Even with workarounds (csfix, dgVoodoo2), the game is held together with duct tape - **The engine is shared.** Crimson Skies uses the Zipper Interactive **GameZ/GOS engine** — the same engine as MechWarrior 3 (1999) and Recoil (1999). A successful recompilation could unlock all three games - **It's a genuinely great game** that deserves to be playable for another 25 years ## Project Status | Phase | Status | Description | |-------|--------|-------------| | **Phase 0** | **Complete** | Binary analysis, PE parsing, SafeDisc decryption | | **Phase 1** | **Complete** | Function discovery (8,495 functions — call-graph + IDA vtables + `push imm32` fn-pointers + split entries) | | **Phase 2** | **Complete** | x86-to-C code generation (~1.17M lines, 58 MB, 0 errors) | | **Phase 3** | **Complete** | Compilation and linking (0 errors, 0x10000000 base) | | **Phase 4** | **Complete** | Runtime bringup — CRT init, all 153 static constructors, MFC42 `AfxWinMain`, and the real→lifted boundary | | **Phase 5** | **In Progress** | Win32/DirectX HAL — real DDraw/D3D/DirectPlay via the native DLLs; startup dialog renders and drives | | **Phase 6** | **In Progress** | GOS engine — every subsystem reports `PASSED`; runs into `dshell` | | Phase 7 | Pending | Asset loading — ROF archives, ZBD files, CAB extraction | | Phase 8 | Pending | Menu/UI rendering | | Phase 9 | Pending | Flight model, mission logic, full gameplay | | Phase 10 | Pending | Modern rendering backend (D3D11/Vulkan) | ### Where it stands The recompiled game brings up the whole GOS engine and reaches `dshell`, its game shell. Every runtime self-check is clean (see **Runtime health**), so the work left is game- and graphics-specific rather than infrastructural. **Next:** run on a console session with the GPU visible and follow `dshell` past engine init. Over a remote desktop the adapter enumerates as "Microsoft Remote Display Adapter", no Direct3D driver qualifies, and the game dereferences its null device — the retail binary has no guard there either, so that path needs real hardware, not a fix. ## Binary Analysis | Property | Value | |----------|-------| | **Target** | `CRIMSON.ICD` (2.46 MB) | | **Compiler** | Visual C++ 6.0 (Visual Studio 98) | | **Build Date** | 2000-11-18 | | **Architecture** | x86-32, PE32, image base 0x00400000 | | **Code (.text)** | 0x00401000 - 0x006024BD (2.0 MB) | | **Data (.data)** | 0x00619000 - 4 MB virtual | | **Copy Protection** | SafeDisc v1.50 (BoG_ marker, ICD format) | | **Relocations** | Present (.reloc section) | | **PDB Path** | `D:\zipper\CrimsonRun\run\Crimson.pdb` | | **Functions** | 8,495 (6,232 call-graph + 773 IDA vtable virtuals + ~476 `push imm32` fn-pointers + 1,429 split entries) | | **Lines of C** | ~1.17M (58 MB across 17 source files) | | **Code Gen Errors** | 0 | | **Code Gen Time** | ~12 seconds | | **Compile Errors** | 0 | | **Link Errors** | 0 | | **Image Base** | 0x10000000 (rebased to free original VA range) | | **Post-Gen Fixes** | 1,949 tail calls, 240 switch tables (3,362 arms), 5,454 routed vtable slots | | **Import Bridges** | 597 (from 20 DLLs, auto-generated from IAT) | ### Recompilation Statistics | Metric | Value | |--------|-------| | Functions recompiled | 8,495 | | Total lines of C | ~1,168,000 | | Generated code size | 57 MB | | Source files | 16 + header + dispatch table | | Code generation time | ~9 seconds | | Compilation errors | 0 | ### Engine: Zipper Interactive GameZ / GOS The engine's internal framework is called **GOS** (Game Operating System), providing: - Text rendering (`gos_LoadFont`, `gos_TextDraw`) - Scripting (`gosScript`) — an interpreted scripting language for mission logic - Surface/pane management - Rendering abstraction over Direct3D Immediate Mode PDB path confirms the source tree: `D:\zipper\CrimsonRun\` ### Import Summary (461 functions from 19 DLLs) | DLL | Functions | Purpose | |-----|-----------|---------| | KERNEL32.dll | 117 | Core Win32 | | USER32.dll | 94 | Window management | | GDI32.dll | 22 | Font/text rendering | | MFC42.DLL | 64 | Microsoft Foundation Classes | | MSVCRT.dll | 62 | C Runtime | | MSVCP60.dll | 10 | C++ Standard Library | | IFC21.dll | 19 | Immersion force feedback | | WINMM.dll | 14 | Multimedia (mixer, MCI) | | AVIFIL32.dll | 9 | AVI video playback | | DDRAW.dll | 2 | DirectDraw | | DINPUT.dll | 1 | DirectInput | | DSOUND.dll | 1 | DirectSound | | DPLAYX.dll | 1 | DirectPlay (multiplayer) | | zTiff.dll | 5 | TIFF image loading | | ROFFILE.dll | ~8 | ROF archive file system | | ole32.dll | 3 | COM initialization | | SHELL32.dll | 3 | Shell operations | | ADVAPI32.dll | 6 | Registry | | comdlg32.dll | 2 | File dialogs | ### Asset Formats | Format | Extension | Purpose | Tooling | |--------|-----------|---------|---------| | **ROF** | .ROF | Archive containers with directory tree | ROFFILE.dll (game), mech3ax (community) | | **ZBD** | .ZBD | Data archives (sounds, textures, scripts, models) | mech3ax extracts WAV/PNG/JSON | | **CAB** | .CAB | Per-chapter compressed data (C1-C5) | Standard Microsoft Cabinet | | **GW** | embedded | gosScript mission/UI scripts | Interpreter in INTERP.ZBD | | **MPG** | .MPG | Cutscene video (MPEG-1) | Standard codecs | | **TGA/BMP** | .TGA/.BMP | Textures and UI graphics | Standard formats | | **TIFF** | via zTiff.dll | Additional image format support | zTiff.dll | ## Architecture ``` crimsonskies/ ├── tools/pcrecomp/ # Shared recompilation toolkit (submodule) ├── analysis/ # PE analysis results, function maps │ ├── functions.json # Function address/size/block map │ └── pe_analysis.json # PE header analysis ├── src/ │ ├── game/ │ │ ├── main.c # Entry point, VEH handler, manual overrides │ │ ├── imports.c # Win32/DirectX import bridges │ │ ├── com_mocks.c # COM mock objects (DDraw, D3D, DInput, DSound) │ │ └── recomp/ │ │ ├── recomp_types.h # Register model, memory macros │ │ └── gen/ # Auto-generated C code (gitignored) │ └── hal/ │ └── renderer.c # Modern rendering backend ├── config/ │ └── pe_analysis.json # PE metadata for code generator ├── CMakeLists.txt └── README.md ``` ### Current Runtime Output Driven through the startup dialog, on a machine with a working Direct3D driver: ``` [*] Dispatch table: 8495 recompiled functions Resources: game dialogs present in this image Routed 5785/5785 function-pointer slots to lifted code [*] Calling entry point sub_005F7056... _initterm: called 153, skipped 0 [game] [nvldumd.dll] - NVIDIA GeForce RTX 5070 [game] DRIVER:RGB Emulation - Microsoft Direct3D RGB Software Emulation [game] -----SKIPPED - Does not interface with hardware [game] DRIVER:Direct3D HAL - Microsoft Direct3D Hardware acceleration through D3D HAL [game] +++++OK [game] zbInit: PASSED gModInit: PASSED zWepInit: PASSED [game] zEffInit: PASSED zRndrInit: PASSED zImgInit: PASSED [game] gClsInit: PASSED zSndInit: PASSED zTwoD_Init: PASSED [game] zUtlInit: PASSED zInInit: PASSED [game] dshell(void): STARTING ``` The game renders its real **"Select Video Device"** dialog — original resources, real MFC42, painted by recompiled window procedures — enumerates the actual GPU, rejects the software rasteriser, accepts the hardware driver, and brings up every GOS subsystem. | Metric | Value | |---|---| | Calls per startup | 2,212,705 | | Indirect calls | 49,442 | | real→lifted callbacks | 1,868 | | Routed function-pointer slots | 5,785 | | Functions executed | 996 of 8,495 (11.7%) | ### Runtime health Every self-check the runtime can produce is silent across a full startup: | Check | Result | |---|---| | Heap corruption (`HeapValidate`, sampled) | 0 | | Stack imbalances (`esp` on return) | 0 | | Inconsistent import stack cleanup | 0 of 146 imports | | Unresolved dispatches | 3 (known, see Debugging) | | real→lifted dispatch misses | 0 | | NULL import bridges | 0 | ### How the boundary works A recompilation of a real MFC application is not all-or-nothing: the application is lifted and its framework stays real machine code, so control crosses in both directions. - **lifted → real.** Import calls and COM vtable methods go through `call_native_measured`, which pushes an argument window and *measures* what the callee's `ret` popped rather than knowing any signature. `ECX` is passed unconditionally (it is `this` for a thiscall member and scratch everywhere else). Unbridged IAT slots hold the real DLL address so code that *reads* a slot sees a usable pointer; only slots with a custom bridge hold a sentinel. - **real → lifted.** MFC calls the application back constantly — vtable slots, window procedures, `_onexit` handlers. `tools/pcrecomp/runtime/hybrid` (proven on Encarta 97) provides trampolines; `.rdata`/`.data` slots pointing at lifted function starts are rewritten to them at startup, and callbacks passed as arguments are translated through a cache so one function always maps to the same trampoline (native code compares callback pointers for identity). - **`.text` is mapped as data.** MSVC emits switch jump tables and constant tables into the code section and lifted code reads them at their original VAs. It is `PAGE_READWRITE`, never executable, so a stray jump still faults loudly. ### Build / regen pipeline Order matters, and nothing chains these for you: ```bash python run_pipeline.py analysis/CRIMSON_decrypted.exe src/recomp/gen python tools/fix_gen.py # cross-function gotos -> RECOMP_ITAIL python tools/fix_switch.py # jump tables -> real switch statements python tools/seed_split_entries.py # new ITAIL targets -> entries; repeat if > 0 python tools/seed_data_fnptrs.py # message-map / RTTI handlers -> entries cmake --build build --config Release python tools/copy_resources.py # .rsrc from the original image ``` - **`fix_gen.py`** is not optional: the lifter emits `goto L_x` for cross-function branches, and skipping the rewrite fails the build with `C2094` undefined-label errors. - **`fix_switch.py`** rewrites `RECOMP_ITAIL(MEM32(reg * 4 + table))` into a `switch` over the arms read out of the binary. A computed tail-call target cannot be dispatched — the arms are basic blocks, not functions — so without this every `switch` silently falls out of its function. 240 tables, 3,362 arms. - **`seed_split_entries.py`** promotes jump-into-the-middle targets to entries. Re-run the pipeline while it reports a non-zero count; it converges in a few passes. - **`seed_data_fnptrs.py`** finds functions that exist only as a pointer in `.rdata`/`.data` — MFC message-map handlers and `CRuntimeClass` records, which nothing calls directly and which are not part of a vtable run. Gated on a real MSVC prologue, so a stray constant is not mistaken for code. - **`copy_resources.py`** must run after **every link**. The game's 26 dialog templates, menu, icons and string tables live in the original image's `.rsrc`, and MFC resolves them through `hInstance` — which has to stay this image's real module handle. Without it `CDialog::DoModal()` returns `-1` and the game exits during startup. ### Running The game must run with its own directory as the working directory — it `LoadLibraryA`s `strings.dll` for localized text and calls `exit()` if that fails: ```bash cd game_disc/GAME /path/to/build/bin/Release/crimson.exe /path/to/analysis/CRIMSON_decrypted.exe ``` ### Debugging The runtime reports on itself; these are the signals worth grepping the log for, all of which should be **zero**: | Log line | Means | |---|---| | `[ESP] returned esp=...` | A lifted function returned with the stack lower than it started — it did not consume the return slot. Names the callee and its caller. | | `ITAIL/ICALL: unresolved VA` | A dispatch found no function, import or native code at that address — usually a missed discovery. | | `R2L: no lifted function at` | Native code called back through a thunk to an address we never lifted. | | `[HEAP] CORRUPTION after` | The heap was intact before the named call and is not after it. | Two environment variables: - **`CRIMSON_WATCHDOG=`** (default 10, `0` disables) — a hang produces no exception and therefore no trace dump; the log simply stops. The watchdog samples the call counter and, when it stops moving, prints where execution is and dumps the trace ring. Note that a modal dialog waiting for input looks identical to a hang. - **`CRIMSON_HEAPCHECK=`** (default off) — `HeapValidate` every *n*th call. It walks the whole heap, so `n=200` will not finish a startup; `n=20000` covers 2.2M calls in seconds and still localises a corruption to within *n* calls. A run reports `N of M functions executed` on exit, on a crash and from the watchdog — currently about **11.7%** of what is lifted, the rest sitting behind the Direct3D device. Three `ICALL: unresolved VA` in the video path are known and deliberate. Those functions exist only as pointers written into structs at runtime, so neither the data scan nor the call graph finds them; seeding them as entries splits the functions that contain them and stops the GOS engine initialising, which is worse than the dispatch failing and returning 0. `tools/audit_log.py ` summarises a run: stack cleanup that varies for one import (it must not — the bridge measures it, and a varying result means the emulated stack was not where the bridge thought), imports that always return zero, esp imbalances and unresolved dispatches. The game also logs its own progress (driver enumeration, subsystem init) via `fprintf` on MSVCRT's stderr. That is bridged and appears prefixed with `[game]`; it is usually the fastest way to find out what the game thinks is wrong. ## Building ### Prerequisites - **CMake** 3.20+ - **Visual Studio 2022** (MSVC, x86/Win32 target) - **Python 3.10+** with `capstone`, `pefile` - A legally owned copy of Crimson Skies ### Code Generation ```bash # Step 1: Obtain decrypted binary (see SafeDisc section below) # Step 2: Generate recompiled code python -m tools.pcrecomp.tools --exe CRIMSON_decrypted.exe --all -o src/recomp/gen # Step 3: Build cmake -B build -G "Visual Studio 17 2022" -A Win32 cmake --build build --config Release ``` ### SafeDisc Decryption The retail CRIMSON.ICD has SafeDisc v1.50 encryption (7.8 bits/byte entropy in .text). Options: 1. **Memory dump** (recommended): Use `tools/pcrecomp/tools/drm/safedisc_dump.py` with SafeDiscShim to launch the game and dump decrypted code from memory 2. **csfix**: The community csfix patch includes a pre-decrypted executable 3. **unSafeDisc**: Static decryption tool for SafeDisc v1 ICD files ## Related Projects - [mech3ax](https://github.com/TerranMechworks/mech3ax) — Rust-based asset extractor for MechWarrior 3 and Crimson Skies (ZBD/ROF formats) - [pcrecomp](https://github.com/sp00nznet/pcrecomp) — Shared PC static recompilation toolkit - [xwa](https://github.com/sp00nznet/xwa) — X-Wing Alliance recompilation (reference implementation) Part of the [sp00nznet](https://github.com/sp00nznet) recompilation collection. ## Legal This is a game preservation project. You must own a legal copy of Crimson Skies to use this. No copyrighted game assets are included in this repository. ## Contributing This project is in early stages. If you're interested in helping preserve Crimson Skies: - **Reverse engineers**: Function identification, vtable mapping, string cross-references - **Graphics programmers**: D3D Immediate Mode → modern D3D11/Vulkan translation - **Flight sim fans**: Flight model tuning, mission script documentation - **Modders**: ROF/ZBD format documentation, asset pipeline tools Open an issue or join the discussion!