v0.3.0: display abstraction, map-browser menu, SAGE terrain tiling/blends
- ra3.client::display: shared interactive loops; SDL and Vulkan backends implement only the primitives (init/present/poll_event/window_size/ key_down/present_terrain). ra3.display picks the backend. - Menu: maps by localized name (gamestrings.csf), red/gold theme, hover highlight, mouse + keyboard, wheel scroll, fullscreen and FPS/vsync options, loading progress bar. - Terrain: continuous tile sampling via a texture array (REPEAT, uv = cell/(2*cellSize)) removes per-cell grid seams; SAGE blend ramp for material transitions; FPS label + top-right minimap overlays. - Skip the skirmish sim for map views; reuse the Vulkan texture; no idle terrain redraw.
This commit is contained in:
+56
-20
@@ -15,19 +15,36 @@ with llvm-mingw for Windows.
|
||||
└───────────────────┬───────────────────┘
|
||||
┌──────────────┬───────────────┼───────────────┬──────────────┐
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
ra3.ui ra3.render ra3.skirmish ra3.client ra3.game
|
||||
SDL3 window framebuffer match rules display/loop RA3 sides
|
||||
ra3.display ra3.render ra3.skirmish ra3.client ra3.game
|
||||
backend pick framebuffer match rules display+loops RA3 sides
|
||||
│ │ │ │ │
|
||||
└──────┬───────┘ └───────┬───────┘ │
|
||||
▼ ▼ │
|
||||
ra3.core ra3.logic │
|
||||
simulation │
|
||||
┌───────────────────────────────────────┬──────────────────────┘
|
||||
▼ ▼
|
||||
ra3.map ra3.fs
|
||||
map catalog BIG4 + RefPack
|
||||
┌──┴───┐ │ └───────┬───────┘ │
|
||||
▼ ▼ ▼ ▼ │
|
||||
ra3.ui ra3.vulkan ra3.terrain ra3.logic │
|
||||
SDL Vulkan heightmap/blends simulation │
|
||||
┌──────────────────────┘
|
||||
▼
|
||||
ra3.map / ra3.fs
|
||||
catalog / BIG4 + RefPack
|
||||
```
|
||||
|
||||
## Presentation layer
|
||||
|
||||
`ra3.client::display` is the single seam between the engine and the windowing
|
||||
stack. A backend implements only the low-level primitives (`init`, `present`,
|
||||
`poll_event`, `window_size`, `key_down`, `shutdown`) and, optionally,
|
||||
`present_terrain`; the interactive loops (menu, image viewer, camera viewer,
|
||||
GPU terrain) and the native-to-`ui_event` mapping live once in the base class,
|
||||
so the SDL and Vulkan paths cannot drift. SDL3 remains the platform layer (it
|
||||
owns the window, input and the low-cost blit backend); Vulkan is the accelerated
|
||||
backend. `ra3.display` selects the first backend that starts, and the app never
|
||||
references a backend by name.
|
||||
|
||||
The Vulkan backend currently hand-rolls its pipelines. As materials, skybox and
|
||||
HUD arrive, a thin RHI + render-graph belongs between `ra3.client` and the
|
||||
Vulkan/SDL backends; `present_terrain` is the placeholder for that step.
|
||||
|
||||
|
||||
## Module responsibilities
|
||||
|
||||
### `ra3.core`
|
||||
@@ -44,9 +61,20 @@ spatial grid, and the 30 Hz `game_logic` driver (`prepare_new_game` /
|
||||
`start_new_game` / `update`).
|
||||
|
||||
### `ra3.client`
|
||||
The presentation boundary: an abstract `display` with a `headless_display`
|
||||
implementation, and `game_client`, the seam a future W3D/D3D9 renderer plugs
|
||||
into.
|
||||
The presentation boundary: the abstract `display` (low-level primitives plus the
|
||||
shared interactive loops and `ui_event` mapping), a `headless_display` for
|
||||
CPU-only runs, and `game_client`, the facade that owns the simulation and drives
|
||||
the frame loop.
|
||||
|
||||
### `ra3.display`
|
||||
Backend selection: tries Vulkan, then SDL, then reports failure so the caller can
|
||||
fall back to an offscreen image. The only module that names a concrete backend.
|
||||
|
||||
### `ra3.terrain`
|
||||
The real map terrain: parses `CkMp`'s `HeightMapData` and `BlendTileData`
|
||||
(tiles, `Blends`/`ThreeWayBlends` and their `BlendDescription`s), loads the
|
||||
`Terrain.big` tile textures, and rasterises the map (software `render`/`render3d`
|
||||
and the GPU `gpu_terrain` for `present_terrain`).
|
||||
|
||||
### `ra3.game`
|
||||
Red Alert 3 data that SAGE keeps in `PlayerTemplate`: the three sides
|
||||
@@ -62,7 +90,8 @@ on demand.
|
||||
### `ra3.map`
|
||||
Map discovery and loading: scans `MapsMultiplayer.big` for main map entries,
|
||||
unwraps the two compression layers (`BIG4` RefPack → `EAR\0` wrapper → RefPack →
|
||||
`CkMp`), and recovers `Player_N_Start` waypoint coordinates. Degenerate
|
||||
`CkMp`), recovers `Player_N_Start` waypoint coordinates, and decodes the
|
||||
localized display names from the install's `gamestrings.csf`. Degenerate
|
||||
extractions are rejected so the caller can fall back.
|
||||
|
||||
### `ra3.skirmish`
|
||||
@@ -72,14 +101,21 @@ passive + harvester income, a simple build AI, movement and combat on a fixed
|
||||
|
||||
### `ra3.render`
|
||||
A dependency-free software renderer: an ARGB8888 `image` framebuffer with
|
||||
blit/line/circle primitives, a TGA decoder for the game's map art, a 24-bit BMP
|
||||
encoder for headless output, and `compose` which overlays a world grid and
|
||||
markers (start positions, live units) onto the map art.
|
||||
blit/line/circle/text primitives (an embedded 8x8 bitmap font), a TGA decoder for
|
||||
the game's map art, a 24-bit BMP encoder for headless output, and `compose` which
|
||||
overlays a world grid and markers (start positions, live units) onto the map art.
|
||||
|
||||
### `ra3.ui`
|
||||
The windowed viewer. The SDL3 backend streams the rendered image to a texture
|
||||
with pan/zoom and Esc-to-quit; when the build has no SDL3, a null backend
|
||||
returns `false` so the caller writes an offscreen image instead.
|
||||
The SDL3 backend: `sdl_display` implements the `ra3.client::display` primitives
|
||||
using an `SDL_Renderer` streaming texture. Cheap and dependency-light, it is the
|
||||
fallback when Vulkan is unavailable. A null backend returns `false` so the build
|
||||
still runs without SDL3.
|
||||
|
||||
### `ra3.vulkan`
|
||||
The accelerated backend: `vulkan_display` implements the same primitives and
|
||||
adds `present_terrain`, a GPU heightfield raymarcher (mipmapped tile atlas with a
|
||||
replicated gutter and the retail SAGE blend ramp). A null backend reports failure
|
||||
when no Vulkan loader is present.
|
||||
|
||||
## Design rules
|
||||
|
||||
|
||||
@@ -78,6 +78,27 @@ Each holds an object pointer (0 when the subsystem is down).
|
||||
cash `+0x64`, player slots `+0xfc` (stride `0x5c`, 6 slots), faction at
|
||||
`slot + 0x18` (`Empire=2`, `Allied=4`, `Soviet=8`, `Random=7`).
|
||||
|
||||
### Tactical view (camera)
|
||||
|
||||
The in-game camera is the `TheTacticalView` object, held in the global at
|
||||
`0x00cdb7b4`. Its vtable accessors return zoom, pitch (current/target), yaw,
|
||||
world position and FOV; the debug overlay `FUN_005ef0a0` prints them through the
|
||||
format string at `0x00c0b900`. Mouse state is the singleton at `0x00ce9284`
|
||||
(cursor position vtable slot `+0x3c`, button down `+0x48`); the keyboard manager
|
||||
is `0x00ce927c` (modifier mask `+0x38`).
|
||||
|
||||
Per-map camera tuning is a named-field table in `.rdata` (around
|
||||
`0x00c11a54`): `cameraMinHeight`, `cameraMaxHeight`, `cameraPitchAngle`,
|
||||
`cameraYawAngle`, `cameraScrollSpeedScalar`, `cameraGroundMinHeight`,
|
||||
`cameraGroundMaxHeight`. The map-load chunk `CHUNK_TacticalView`
|
||||
(`0x00beea14`, consumed near `0x00548a00`) seeds the view from the map; the
|
||||
tutorial actions `LOCK_CAMERA_SCROLL` / `LOCK_CAMERA_ZOOM` / `LOCK_CAMERA_ROTATION`
|
||||
gate the controls.
|
||||
|
||||
OpenRA3 has no 3D terrain yet, so `ra3::render::view_camera` reproduces the
|
||||
*controls* - wheel zoom, screen-edge scroll, clamped pan and opening on the
|
||||
player's start - over the 2D map overview, not the retail perspective camera.
|
||||
|
||||
### Match start
|
||||
|
||||
The BEGIN button calls `SkirmishGameOptionsMenu::start` (`0x00b28d60`), which
|
||||
@@ -127,9 +148,84 @@ in `MPPositionList` instead yield no waypoints and fall back.
|
||||
Verified example (`map_mp_2_feasel4`): `Player_1_Start` = `(1338.9, 1940.5, 0)`,
|
||||
`Player_2_Start` = `(1290.8, 1404.9, 0)`.
|
||||
|
||||
### Terrain (`ra3.terrain`)
|
||||
|
||||
The `CkMp` tree is a flat chunk list: `"CkMp"`, `u32 assetCount`, the name
|
||||
table (`{ u8 len, name, u32 index }`, index descending from `assetCount`), then
|
||||
`{ u32 index, u16 version, u32 size, data }` per chunk. On
|
||||
`map_mp_2_feasel4` the terrain is `HeightMapData` v6 (540 x 600, border 20,
|
||||
`u16` elevations, scale `0.0390625`) and `BlendTileData` v27.
|
||||
|
||||
`BlendTileData` opens with `NumTiles`, the `u16` tile grid, then the
|
||||
blend/three-way/cliff tables (`u16` for v27); the passability flag arrays that
|
||||
follow are not needed for rendering, so the texture table is located by
|
||||
scanning for its `{ cellStart, cellCount, cellSize, magic }` + `u16`-prefixed
|
||||
name entries. A tile value is `(cellIndex << 2) | variant`, and `cellIndex`
|
||||
indexes the global `TextureCellCount`-cell table (each texture owning
|
||||
`cellSize^2` 64 px cells). The textures themselves are
|
||||
`art\terrain\<stem>.tga` in `Terrain.big` / `Core11.big` (RefPack + 256x256
|
||||
TGA). Rendered top-down, `map_mp_2_feasel4` correlates 0.94 with the official
|
||||
`_art.tga` overview.
|
||||
|
||||
### Terrain blending (`BlendTileData` tail)
|
||||
|
||||
After the tile grid, `BlendTileData` stores three per-cell `u16` tables —
|
||||
`Blends`, `ThreeWayBlends` and `CliffTextures` — then `TextureCellCount`,
|
||||
`BlendsCount`, the texture table, two magic words and `BlendsCount - 1` blend
|
||||
descriptions (18 bytes each: `u32 secondaryTile`, four direction bytes,
|
||||
`u8 flags`, `u8 twoSided`, `u32 0xFFFFFFFF`, `u32 0x7ADA0000`). A non-zero
|
||||
`Blends[cell]` is a 1-based index into the descriptions; `secondaryTile` is a
|
||||
packed tile value (`secondaryTile >> 2` is its cell).
|
||||
|
||||
The retail `Terrain.fx` (compiled `terrain.fxo`, parameters `Terrain.BaseTexture`,
|
||||
`Terrain.MacroTexture`, `MapCellSize`, `IsTerrainAtlasEnabled`; technique
|
||||
`TerrainTile`) cross-fades a cell's base tile into `secondaryTile` with a linear
|
||||
ramp selected by `BlendDirection`: `1` right, `2` top, `4` top-right, `8`
|
||||
top-left, where `flags` bit 0 flips the axis and bit 1 marks a two-sided
|
||||
diagonal. OpenSAGE's `Terrain.frag` reconstructs the exact
|
||||
`CalculateBlendFactor`; `ra3::terrain::blend_factor` and `shaders/terrain.frag`
|
||||
mirror it. The row axis is *not* inverted (73% of long-axis blends point at a
|
||||
neighbour of the same texture, versus 25% inverted). On `map_mp_2_feasel4`,
|
||||
33828 of 324000 cells carry a blend and there are 7094 descriptions.
|
||||
|
||||
The GPU atlas pads every 64 px tile with a 2-texel replicated gutter. Packed
|
||||
edge-to-edge, bilinear/mipmap filtering averaged two unrelated tiles at every
|
||||
cell border — that cross-tile bleed was the visible grid line the hardware path
|
||||
drew. (`shaders/terrain.frag` samples `cell_stride = cell_texels + 2 * gutter`.)
|
||||
|
||||
### Open question: per-cell tile sampling
|
||||
|
||||
Measured on `map_mp_2_feasel4`, sampling each cell as its own 64 px block and
|
||||
restarting the UV every cell leaves a 1.39x edge spike at cell boundaries
|
||||
(43.4 vs 31.1 mean gradient at 8 px/cell). Two candidate mappings reduce it and
|
||||
need a visual decision against the retail art:
|
||||
|
||||
| Mapping | Boundary/interior |
|
||||
| --- | --- |
|
||||
| per-cell 64 px block (current) | 1.39 |
|
||||
| OpenSAGE `BlendTileTextureIndex` Morton layout, 32 px block | 1.18 |
|
||||
| continuous `uv / (cellSize * 2)`, per OpenSAGE `Terrain.frag` | 1.11 |
|
||||
|
||||
The retail `Terrain.frag` (OpenSAGE) samples the tile texture *continuously*
|
||||
(`uv / (CellSize * 2)`), so adjacent cells never restart the texture; our
|
||||
per-cell restart is the remaining source of grid lines. Switching the atlas
|
||||
from 64 px blocks to continuous 32 px regions (or the Morton 8x8 layout) is the
|
||||
next step, pending an art correlation check.
|
||||
|
||||
### Map display names
|
||||
|
||||
The skirmish map list labels live in `Data\English.big`'s
|
||||
`data\gamestrings.csf` (the newest `Lang-English*.big` wins) under
|
||||
`MAP:<UPPERCASE_ID>`, e.g. `MAP:MAP_MP_2_FEASEL4` = "Battlebase Beta". CSF
|
||||
values are UTF-16 code units whose low byte is XORed with `0xFF`
|
||||
(`ra3::map::parse_map_names`). `openra3 extract` writes the decoded table to
|
||||
`maps/map_names.tsv`; `openra3 menu` shows them instead of the raw map id.
|
||||
|
||||
|
||||
### Still to recover
|
||||
|
||||
- `MPPositionList` layout (per-player starts for maps without waypoints).
|
||||
- Map dimensions / `HeightMapData` / `BlendTileData`.
|
||||
- Compiled asset blobs (`map.bin`, `global.bin`, `static.*.bin`) and the
|
||||
`.manifest` schema used to deserialise them.
|
||||
- Cliff textures and the `CliffTextureMapping` UV remap (`CliffTextures` is
|
||||
parsed but not yet drawn).
|
||||
- Compiled asset blobs (`global.bin`, `static.*.bin`) and the `.manifest`
|
||||
schema used to deserialise them.
|
||||
|
||||
+27
-16
@@ -18,25 +18,36 @@ that each one produces something that builds and runs.
|
||||
`import std;` in every module; added an isolated Windows cross-build
|
||||
(llvm-mingw + SDL3 MinGW) producing `openra3.exe` + `SDL3.dll`, alongside
|
||||
the Linux build. Both images are separate to keep the toolchains apart.
|
||||
- [x] **v0.3.2 — real terrain.** `ra3.terrain` parses the map's `HeightMapData`
|
||||
and `BlendTileData` and rasterises the actual terrain from the install's
|
||||
`Terrain.big` tile textures (top-down, elevation-shaded), replacing the
|
||||
`<map>_art.tga` overview as what `render` draws.
|
||||
- [x] **v0.3.0 — real balance + skirmish + Vulkan.** `ra3.data` pins the retail
|
||||
numbers (damage types, `ArmorTemplate` percentages, weapons, units,
|
||||
structures, ore economy) audited from EA's open RA3 XML; `ra3.skirmish` is
|
||||
rebuilt on them (armour resolution, weapon target masks, pay-as-you-go
|
||||
build queue, power, tech prerequisites, refinery ore cycle, team-wipe
|
||||
victory); and `ra3.vulkan` replaces the presentation stack with Vulkan
|
||||
(SDL3 surface, embedded SPIR-V), with a null fallback where no loader
|
||||
exists.
|
||||
|
||||
## Next
|
||||
|
||||
- [ ] **v0.1.1 — correct world calibration.** Parse the map heightmap
|
||||
dimensions (and `MPPositionList` starts) so the unit overlay lines up with
|
||||
the map art instead of using an assumed world size.
|
||||
- [ ] **v0.2.0 — data & file formats.** Parse compiled gameplay assets
|
||||
(`GameObject`, `WeaponTemplate`, `ArmorTemplate`, `LocomotorTemplate`) so
|
||||
units use the real balance numbers instead of OpenRA3's stand-ins.
|
||||
- [ ] **v0.3.0 — deterministic simulation.** Real update modules, locomotor
|
||||
movement, weapons/damage/armour resolution, build queues and the tech
|
||||
tree, pathfinding and shroud.
|
||||
- [ ] **v0.4.0 — AI.** Skirmish AI: build states, team composition, attack
|
||||
waves (the reference tree's `AI*` modules).
|
||||
- [ ] **v0.5.0 — full renderer.** Terrain heightmap rendering, W3D models and a
|
||||
D3D9/Vulkan/OpenGL client backend implementing `ra3::client::display`,
|
||||
plus full input.
|
||||
- [ ] **v0.6.0 — content.** Load real maps, units, powers and strings; play a
|
||||
skirmish end-to-end with a UI.
|
||||
- [ ] **v0.3.1 — tactical map view.** The Vulkan/SDL viewer reproduces the
|
||||
retail tactical view (`TheTacticalView`, `ra3_1.12.game` `0x00cdb7b4`):
|
||||
opens centred on the player's start, wheel zoom, edge scroll and clamped
|
||||
pan. A hand-rolled HUD was tried and removed — the real in-game interface
|
||||
is `.apt` art that must be recovered first, so it is not on the near
|
||||
roadmap.
|
||||
- [ ] **v0.4.0 — real update modules.** Locomotor movement, projectiles and
|
||||
warheads, build placement, shroud, and pathfinding, driven by the
|
||||
`GameObject` module set recovered from the binary.
|
||||
- [ ] **v0.5.0 — data-driven content.** Deserialise the compiled assets
|
||||
(`global.bin`/`static.*.bin` + `.manifest`) so `ra3.data` reads the
|
||||
install instead of pinned constants; real maps, models and strings.
|
||||
- [ ] **v0.6.0 — full renderer.** Perspective terrain (the heightmap is decoded
|
||||
but drawn top-down today), W3D models and the full in-game client on
|
||||
Vulkan, plus input.
|
||||
|
||||
## Cross-cutting tracks
|
||||
|
||||
|
||||
Reference in New Issue
Block a user