Files
OpenRA3/README.md
T
EnderTheCoder 70d4beca4f v0.6.0: WebAssembly build with a WebGL backend (playable in the browser)
- ra3.webgl: a Web peer of Vulkan/Direct3D (SDL3 canvas + GLES3/WebGL2), with
  2D blit and the GPU terrain raymarch; GLSL ES shaders; null fallback off
  Emscripten. backend::webgl + default_backend() (webgl under Emscripten).
- Emscripten toolchain + Dockerfile.wasm + scripts/build-wasm.sh; import std,
  global -fexceptions, and Asyncify so the blocking frame loop yields to the
  browser (display::sleep_frame calls emscripten_sleep).
- apps/web/shell.html + apps/web/serve.py (Range-capable dev server).
- Assets: browsers forbid synchronous on-demand reads on the main thread (and
  FS.createLazyFile / the WasmFS fetch backend are worker-only; SDL3's Emscripten
  backend is main-thread DOM only, so the engine cannot run in a worker). The
  wasm build therefore preloads a compact per-map set via OPENRA3_WEB_ASSETS.
- New openra3 textures --map ID lists the loose terrain TGAs a map resolves to
  (terrain::resolve_texture_files, shared with load_textures_from_dir).
- Fixes: webgl heightmap used GL_R16 = 0x8229 (that is R8) -> upload rejected,
  terrain flattened to water; now 0x822A. Logger uses a stdout console sink on
  the web (stderr maps to console.error); a GL error check logs bad uploads.
- CI: wasm image + build jobs.
2026-09-28 22:57:19 +08:00

16 KiB

OpenRA3

A from-scratch, portable re-implementation of Command & Conquer: Red Alert 3 in C++26 using C++ modules and import std;, built with Clang for both Linux and Windows.

Red Alert 3 runs SAGE 2.0. EA never released that engine's C++ source, but it did open-source the closely related SAGE 1.0 engine as electronicarts/CnC_Generals_Zero_Hour (GPLv3). OpenRA3 uses that tree as the architectural reference and reconstructs the RA3-specific behaviour by decompiling the retail ra3_1.12.game binary with Ghidra.

OpenRA3 ships no game assets or binaries. You must own Red Alert 3. The engine reads your own local installation at runtime; nothing from it is ever copied into this repository. Command & Conquer and Red Alert are trademarks of Electronic Arts; this project is unaffiliated with and unsupported by EA.

Status

OpenRA3 is at v0.6.0. The engine compiles and runs headless, and a minimal skirmish is playable: it reads a real multiplayer map out of your install, recovers the player start waypoints, and simulates two sides building a base, extracting ore and fighting until one side is wiped out. The balance is the retail Red Alert 3 balance, pinned in ra3.data from EA's open RA3 XML: damage types, ArmorTemplate percentages, weapon target masks, build costs and the ore economy. Presentation offers Vulkan (ra3.vulkan), Direct3D 11 / 12 (ra3.dx) and WebGL 2 (ra3.webgl, the wasm build) GPU backends with an SDL software blit and null fallbacks; the backend is selectable from the in-game menu and the software renderer still produces headless images. Terrain tiles cross-fade the way the retail Terrain.fx does (a per-cell blend ramp plus a gutter-padded atlas), so material boundaries are smooth instead of a grid of hard lines. An in-window menu lists the maps by their localized name and exposes every render/skirmish option for tweaking before launch. The whole tree builds for Linux (clang + libc++) and cross-compiles to Windows (openra3.exe + SDL3.dll) with llvm-mingw — both using C++26 modules and import std;.

$ openra3 skirmish --game-dir "/game" --map map_mp_2_feasel4
OpenRA3 skirmish  map=map_mp_2_feasel4  source=archive:map_mp_2_feasel4  seed=1
start positions:
  P0 (1338, 1940)
  P1 (1291, 1404)
result: decided  winner=0  frames=9293 (309.8 s)
  P0 Commander  Allied  money=   700 units=15 kills=21 losses=15
  P1 AI         Soviet  money=   376 units= 0 kills=15 losses=21

Offline only

OpenRA3 implements offline skirmish and, eventually, LAN/single-player. There is deliberately no online mode: no EA account, no online service, no matchmaking, no GameSpy/Steam integration, and no networking in the simulation. The match loop is deterministic and self-contained so it can be replayed and tested without any external service.

Assets

The runtime reads a portable assets/ folder next to the executable — there is no --game-dir at run time. The build extracts it from your install:

  • openra3_assets (build target) runs cmake/extract_assets.cmake, which
    • extracts every map (double-unwrapped CkMp) and terrain TGA into assets/ via the engine's own openra3 extract;
    • with -DOPENRA3_EXTRACT_ALL=ON (default) also dumps every .big entry plus models, textures (.png), sound effects/voice and movie audio via the ra3-headless/ra3tools scripts.
  • CMake cache vars: RA3_GAME_DIR (default C:/Red Alert 3), RA3TOOLS_DIR (the ra3tools scripts), PYTHON_EXECUTABLE.
  • The step is skipped when the exe cannot run on the build host (a Windows cross-build in a Linux container); the exe then extracts maps/terrain on first launch, and the full dump can be run explicitly: cmake --build <build> --target openra3_assets on a native host, or openra3 extract --game-dir DIR --out DIR.

No game data is committed or distributed. reference/, assets/ and the ra3tools checkout are git-ignored. Without an install the engine falls back to a built-in test map so the project still builds and runs in CI.

Layout

Path Purpose
src/core/ra3.core.cppm fundamental types, math, strings, random, message stream
src/logic/ra3.logic.cppm objects, players, teams, spatial partition, game loop
src/client/ra3.client.cppm display abstraction + shared interactive loops + client facade
src/display/ra3.display.cppm picks the backend (Vulkan/D3D11/D3D12/WebGL, then SDL) for the app
src/game/ra3.game.cppm RA3 sides, player templates, skirmish defaults
src/data/ra3.data.cppm retail RA3 balance: damage types, armour, weapons, units, economy
src/fs/ra3.fs.cppm BIG4 archives, RefPack codec, local install locator
src/map/ra3.map.cppm map catalog, EAR/RefPack unwrap, start waypoints, gamestrings.csf display names
src/skirmish/ra3.skirmish.cppm base building, economy, AI, combat, win condition
src/terrain/ra3.terrain.cppm CkMp terrain: HeightMapData, BlendTileData (tiles + blends), Terrain.big tiles
src/render/ra3.render.cppm ARGB framebuffer, TGA decode, BMP encode, map compositing, bitmap-font text
src/ui/ra3.ui.*.cppm SDL3 window viewer and menu (null backend when SDL3 is absent)
src/vulkan/ra3.vulkan.*.cppm Vulkan presentation backend and menu (null fallback without a loader)
src/dx/ra3.dx.*.cppm Direct3D 11/12 presentation backend (runtime HLSL; null fallback off Windows)
src/webgl/ra3.webgl.*.cppm WebGL2 presentation backend for the wasm build (GLSL ES; null fallback off Emscripten)
third_party/libenderlog/ vendored C++26 module logger (import ender.log;, MIT) with a native stack fallback for libc++
src/ra3.cppm umbrella module re-exporting the SDK
apps/openra3/main.cpp menu / maps / skirmish / render CLI
tests/ra3_tests.cpp smoke tests (run via ctest)
tools/ reference fetch + Ghidra-driven reconstruction helpers
docs/ architecture & master plan, reverse-engineering notes
Dockerfile Linux build image (dev toolchain + deploy runtime)
Dockerfile.win isolated Windows cross-build image (llvm-mingw + SDL3 MinGW)
cmake/toolchains/ llvm-mingw-x86_64.cmake cross toolchain
scripts/ build-linux.sh / build-windows.sh one-shot builders
.gitlab-ci.yml build both targets → test → package pipeline

Why Clang + import std;

The engine never #includes the standard library: every module does import std;. CMake's support for that (CXX_MODULE_STD) works today with Clang + libc++. Linux uses the distro clang (LLVM 21); Windows cross-compiles with llvm-mingw (clang 23 + libc++ + the libc++ std module). The two toolchains live in separate images so their compilers and standard libraries never interfere.

Build

The host needs no toolchain: each target builds inside its own image.

# Linux (amd64)   -> build/linux/bin/openra3 + .deb
scripts/build-linux.sh

# Linux/arm64     -> build/linux-arm64/bin/openra3 + .deb   (cross)
scripts/build-linux-arm64.sh

# Debian (amd64)  -> build/debian/bin/openra3 + .deb
scripts/build-debian.sh

# Debian (arm64)  -> build/debian-arm64/bin/openra3 + .deb   (cross)
scripts/build-debian-arm64.sh

# Windows (x86_64) -> build/windows/bin/openra3.exe + SDL3.dll, .zip, .msi
scripts/build-windows.sh

# Windows (arm64)  -> build/windows-arm64/bin/openra3.exe + SDL3.dll, .zip   (cross)
scripts/build-windows-arm64.sh

# WebAssembly      -> build/wasm/bin/openra3.html (+ .js/.wasm)
scripts/build-wasm.sh

Every target is cross-built from x86_64 Linux in its own image: llvm-mingw for the Windows targets, clang + libc++ with dpkg multiarch for the arm64 targets, and Debian/Ubuntu-specific images for the .deb packages.

Or drive Docker directly:

docker build --target dev -t openra3-linux:local .
docker run --rm -v "$PWD:/work" -w /work openra3-linux:local \
    cmake -S . -B build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release
docker run --rm -v "$PWD:/work" -w /work openra3-linux:local cmake --build build/linux -j
docker run --rm -v "$PWD:/work" -w /work openra3-linux:local ctest --test-dir build/linux --output-on-failure

docker build -f Dockerfile.win --target dev -t openra3-windows:local .
docker run --rm -v "$PWD:/work" -w /work openra3-windows:local \
    cmake -S . -B build/windows -G Ninja \
        -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/llvm-mingw-x86_64.cmake \
        -DCMAKE_BUILD_TYPE=Release
docker run --rm -v "$PWD:/work" -w /work openra3-windows:local cmake --build build/windows -j

Behind a slow or blocked mirror, pass --build-arg APT_MIRROR=<url>.

The two images are deliberately separate so the Linux (clang + libc++) and Windows (llvm-mingw + MinGW SDL3) toolchains never interfere. The Windows demo is build/windows/bin/openra3.exe next to SDL3.dll:

# show a real map in a Vulkan window
openra3.exe render --game-dir "C:\Red Alert 3" --vulkan

Vulkan is provided by vendored volk + headers (third_party/), resolved at runtime, so neither image needs a Vulkan SDK.

WebAssembly

OpenRA3 also builds to WebAssembly with Emscripten (scripts/build-wasm.sh), producing openra3.html + .js + .wasm. The browser gets the same GPU renderer as the desktop builds: a ra3.webgl backend peers with Vulkan/Direct3D — SDL3 provides the canvas and input, and GLES 3.0 / WebGL2 runs the 2D blit and the terrain raymarcher (GLSL ES ports of the desktop shaders).

Browsers forbid synchronous on-demand file reads on the main thread, so the assets are preloaded into the module's filesystem at build time. Bundle a compact per-map set (one map plus the tiles it uses) rather than the whole dump:

# 1. which loose TGAs does the map resolve to?
openra3 textures --map map_mp_2_feasel1      # prints paths under assets/terrain

# 2. stage one map + those tiles (+ maps/map_names.tsv) into a directory, then
OPENRA3_WEB_ASSETS=/path/to/that/set scripts/build-wasm.sh

# 3. serve the build output (a Range-capable server is included)
python3 apps/web/serve.py --root build/wasm/bin --port 8199
# open http://localhost:8199/openra3.html

The engine's frame loops are blocking; the wasm build yields to the browser via Asyncify (display::sleep_frame calls emscripten_sleep), so the page repaints and handles input. Log records go to console.log at their real level (the console sink uses stdout on the web instead of stderr). Without OPENRA3_WEB_ASSETS the module still loads and starts, but exits at the menu because no maps are found.

Packages

CPack produces the release artifacts; the Docker images and CI run it for every target.

Target Portable Installer
Linux amd64 (Ubuntu 26.04) .tar.gz openra3_<v>_amd64ubuntu26.04.deb
Linux arm64 (Ubuntu 26.04) .tar.gz openra3_<v>_arm64ubuntu26.04.deb
Linux amd64 (Debian 13) .tar.gz openra3_<v>_amd64debian13.deb
Linux arm64 (Debian 13) .tar.gz openra3_<v>_arm64debian13.deb
Windows x86_64 openra3-<v>-windows-x86_64.zip openra3-<v>-windows-x86_64.msi
Windows arm64 openra3-<v>-windows-arm64.zip (WiX-only; see below)

The .deb dependencies are derived from the binary with dpkg-shlibdeps. The .msi is built with wixl (msitools) straight from the Linux cross image; wixl 0.106 has no arm64 support, so Windows/ARM64 ships the portable .zip (the MSI toolchain for ARM64 would be WiX v4 via the .NET SDK).

Logging

Every run writes openra3.log next to the executable through the vendored libenderlog module (import ender.log;). A file sink archives the previous log to openra3.log.<YYYYmmdd-HHMMSS> on open, so each run gets its own file; the active file rotates at 4 MiB and the last 10 archives are kept. Records at warn and above carry a call stack (Windows CaptureStackBackTrace / POSIX execinfo, because libc++ has no <stacktrace>). A hard crash also writes openra3_crash.log with the faulting module and a raw backtrace.

Running a skirmish

# list the maps in your install
docker run --rm -v "/path/to/Red Alert 3:/game:ro" openra3-linux:local \
    /work/build/linux/bin/openra3 maps --game-dir /game

# play a headless skirmish on a real map
docker run --rm -v "/path/to/Red Alert 3:/game:ro" openra3-linux:local \
    /work/build/linux/bin/openra3 skirmish --game-dir /game --map map_mp_2_feasel4 --seed 7

The Windows build is a native openra3.exe — copy it next to SDL3.dll and run it from cmd/PowerShell, e.g. openra3.exe skirmish --game-dir "C:\Red Alert 3".

--frames N caps the simulation length (default 15 minutes of game time at 30 Hz). The result is deterministic for a given map and seed.

Menu

Run openra3 with no arguments (or openra3 menu) to open a window listing the maps by their localized display name (read from the install's gamestrings.csf, e.g. map_mp_2_feasel4 → "Battlebase Beta") with their id below. The Options column exposes every render/skirmish parameter — mode (3D terrain / top-down / skirmish), window size, camera pitch/yaw/height, FOV, zoom, terrain scale, terrain pitch, world size, seed, frame cap, the overview thumbnail toggle and a BMP output path. Up/Down selects, Left/Right changes a value (or moves the text caret on the BMP field), Tab switches between the map list and the options, Enter starts and Esc quits. Starting a 3D or top-down view opens the viewer; closing it returns to the menu. When no window backend is available the same flow falls back to a console picker, and openra3 menu-preview renders one menu frame to a BMP for inspection.

Rendering the map

render draws the real terrain: it parses the map's HeightMapData (elevation grid) and BlendTileData (per-cell tile index plus the per-cell Blends/ThreeWayBlends and their BlendDescriptions), loads the tile textures from Data\Terrain.big (RefPack + TGA), and rasterises the map with an elevation shade. Each cell cross-fades into its blend neighbour with the same linear ramp the retail Terrain.fx uses, so material transitions are smooth; on the GPU path the tile atlas is padded with a replicated gutter so filtering never bleeds between tiles. Match state is overlaid (start markers in yellow, player 0 in blue, player 1 in red). --thumbnail uses the old <map>_art.tga overview instead.

Offscreen image (works anywhere, no display needed):

docker run --rm -v "/path/to/Red Alert 3:/game:ro" -v "$PWD/out:/out" openra3-linux:local \
    /work/build/linux/bin/openra3 render --game-dir /game --map map_mp_2_feasel4 --out /out/map.bmp

On Windows the same command runs natively: openra3.exe render --game-dir "C:\Red Alert 3" --map map_mp_2_feasel4 opens an SDL3 window.

Interactive window (Vulkan with --vulkan, otherwise SDL3). The camera follows the retail tactical view: it opens centred on the first player's start, the wheel zooms, pushing the cursor against a screen edge scrolls, dragging with the left button pans, and Esc quits. --zoom Z sets the initial zoom (1 fits the whole map). In a container you need an X server on the host — on Windows run VcXsrv and launch it with "Disable access control", then:

docker run --rm -v "/path/to/Red Alert 3:/game:ro" -e DISPLAY=host.docker.internal:0.0 \
    openra3-linux:local /work/build/linux/bin/openra3 render --game-dir /game --map map_mp_2_feasel4

If no display is available the viewer falls back to writing openra3_view.bmp. The unit overlay uses an approximate world scale (--world-size, default 5120); exact calibration from the map's heightmap is on the roadmap.

Reverse engineering

The reconstruction is driven by Ghidra against the retail binary. See docs/REVERSE_ENGINEERING.md for the workflow and the recovered symbol map, and tools/ for the helpers.

License

Project code: GPLv3, matching the CnC_Generals_Zero_Hour reference (see LICENSE).