VERA20k

Red Alert 2 Yuri's Revenge — rebuilt from scratch in Rust

View the Project on GitHub YuriPlanet/vera20k

VERA20k

Red Alert 2: Yuri’s Revenge — rebuilt from scratch in Rust.

This page is a short tour of the code. If it disagrees with the code, the code is right.

The engine is built mostly from plain structs and functions, organized by concern in files and folders.

The top-level layout under src/, roughly bottom-up:

A few files at the root of src/ handle skirmish and match startup (skirmish_*.rs, match_bootstrap.rs) and headless retail-map loading for tools (headless_scenario.rs).

When adding a new file, ask which of those concerns it belongs to. If it’s gameplay logic, it goes in sim/. If it touches the GPU, it goes in render/. If it connects the two, it’s app layer.

To learn what a specific file does, read its //! header. Most modules start with a short comment stating their purpose and what they may depend on.

All mutable game state lives in one struct: Simulation (src/sim/world/mod.rs). Match data that never changes during a match (rules, map heights, trigger definitions) is bound beside it in SimRuntime (src/sim/runtime.rs).

Simulation contains, among other fields:

Each GameEntity is one struct with optional fields.

Every infantry unit, vehicle, ship, building and aircraft is the same struct. Always-present fields include stable_id, position, health, owner, facing, type_ref and category. Optional parts are Option<T>: a moving unit has a locomotor, a harvester has a miner.

Behavior lives in functions and in methods on Simulation, grouped by mechanism.

Some examples:

The frame.

One production entry point runs one frame: SimRuntime::advance_frame(), which calls Simulation::advance_master_frame() in src/sim/world/mod.rs. It follows the original game’s frame order: commands due this frame → triggers → ore growth and active superweapon effects → team scripts → the live object pass → vision → power → superweapon timers → combat → crates → production and docks → houses, defeat checks and AI → frame commit → removal of dead objects → state hash. Simulation::advance_tick() is only a test adapter around the same frame.

Output for the rest of the program leaves through per-frame batches (SimFrameOutput: sound events, weapon-fire events, lifecycle outputs, overlay changes, lighting events) that the app drains each frame. Inside sim/, some mechanisms keep their own queues and message links, such as the radio contact bus between objects (sim/radio/), which uses the original’s radio message codes.

Planning history. A May 2026 plan for moving the frame onto the original’s scheduler is at docs/plans/2026-05-28-foundational-scheduler-roadmap-todo.md. Parts of it have landed since then (the active-object pass above), so read the code first.

Rendering.

A 2D renderer using wgpu. At map load, terrain tiles and sprites (buildings, infantry, overlays) are packed into atlas textures — big images containing many images side by side. Voxel models (vehicles, aircraft) are rasterized to 2D images as well; see unit_atlas.rs and the vxl_*.rs files in render/.

Each frame, the app’s presentation code (src/app/presentation/) reads entity state from the simulation — position, facing, health, animation frame — and builds sprite instances, and render/ draws them. Isometric depth is handled by draw order and depth values.

Rendering reads simulation state and never writes back. You can change rendering without touching game logic, and vice versa.

App layer.

The app layer wires everything together: the window and event loop, input, menus, loading, saving, and the hand-off between simulation, renderer and audio. Gameplay rules belong in sim/, not here.

When you click on a unit, the app layer handles that. It figures out which entity you clicked, translates it into a command, and passes it to the simulation. The app layer is the translator between “what the player did” and “what the simulation understands.”

Timing. src/util/fixed_math.rs defines two rates:

The simulation never reads wall-clock time. The app’s frame pacer (src/app/match_runtime/frame_pacer.rs) decides when the next gameplay frame may run, using the original’s GameSpeed timing, and the app advances the simulation one frame at a time.

After each frame, the app layer hands the updated simulation state to the renderer, which draws the frame. It also drains the sound events that the simulation produced (weapon fired, unit died, construction complete) and plays them through the audio system.

Like everything else, it’s split into folders by concern — app/input/, app/presentation/, app/match_runtime/, app/frontend/, app/loading/, app/persistence/ — around one shared AppState struct.

Determinism.

Everything in Simulation must be deterministic: the same state, inputs and random seed give the same result on every supported platform and CPU.

At the end of every frame the simulation produces a state hash. Two clients with the same inputs must agree on it. Lockstep multiplayer depends on this (the network transport does not exist yet), and the diagnostic command log records the hashes to help find desyncs. It’s also why nothing in sim/ is allowed to call into render/, ui/, sidebar/, audio/ or net/.