OpenRA3 v0.0.1: C++26 modules, BIG4/RefPack reader, minimal skirmish
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# Reverse engineering workflow
|
||||
|
||||
OpenRA3 is reconstructed from two sources:
|
||||
|
||||
1. **Architecture** — the GPLv3 SAGE 1.0 tree,
|
||||
[`electronicarts/CnC_Generals_Zero_Hour`](https://github.com/electronicarts/CnC_Generals_Zero_Hour).
|
||||
RA3 runs SAGE 2.0, so class names, message flow and subsystem boundaries
|
||||
carry over even though the code does not.
|
||||
2. **Facts** — the retail `ra3_1.12.game` binary (image base `0x400000`),
|
||||
analysed in Ghidra and, when needed, observed live.
|
||||
|
||||
Nothing in OpenRA3 should assert a structure or constant that is not either
|
||||
copied from the reference or cited to a retail address.
|
||||
|
||||
## Fetching the reference
|
||||
|
||||
```bash
|
||||
tools/fetch_reference.sh # sparse-clone Code/GameEngine into reference/
|
||||
```
|
||||
|
||||
The checkout is git-ignored (large, and GPLv3 terms differ from this repo's).
|
||||
|
||||
## Ghidra
|
||||
|
||||
The retail module is loaded into Ghidra as `ra3_1.12.game`
|
||||
(`x86:LE:32:default`, 34k+ functions). The analysis is driven through the
|
||||
Ghidra MCP bridge, so every recovered fact can be re-derived:
|
||||
|
||||
| Question | Tool call |
|
||||
| --- | --- |
|
||||
| What does an address do? | `decompile_function(address=0x…)` |
|
||||
| What is this function? | `get_function_by_address(address=0x…)` |
|
||||
| Who touches a global? | `get_xrefs_to(address=0x…)` |
|
||||
| What are the vtable slots? | `list_class_members` / `analyze_data_region` |
|
||||
| Where is a string referenced? | `search_strings` + `get_xrefs_to` |
|
||||
|
||||
### Recovery loop
|
||||
|
||||
1. Pick a subsystem from the reference tree (e.g. `MessageStream`).
|
||||
2. Find its RTTI/vtable in the binary via `search_strings` and xrefs.
|
||||
3. Decompile the constructor to recover object size and field init order.
|
||||
4. Decompile the hot methods to recover field meaning.
|
||||
5. Write the OpenRA3 module with a comment citing the address.
|
||||
6. Add a smoke test that pins the behaviour.
|
||||
|
||||
## Recovered symbol map (retail `ra3_1.12.game`)
|
||||
|
||||
### Engine singletons
|
||||
|
||||
Each holds an object pointer (0 when the subsystem is down).
|
||||
|
||||
| Global | Address | Object vtable |
|
||||
| --- | --- | --- |
|
||||
| `TheGameLogic` | `0x00cd8ce4` | `0x00beb630` |
|
||||
| `ThePlayerList` | `0x00ce8c9c` | `0x00c5b9e0` |
|
||||
| `ThePartitionManager` | `0x00ce2f9c` | `0x00c6af98` |
|
||||
| `TheShroudManager` | `0x00ce2fa0` | `0x00c6aefc` |
|
||||
| `ThePlacementGrid` | `0x00cd8d0c` | `0x00bea560` |
|
||||
| `TheRecorder` | `0x00ce2fd0` | `0x00c10544` |
|
||||
| `TheGlobalObjectRegistry` | `0x00cd8d08` | `0x00bea644` |
|
||||
| `ThePlayerTemplateStore` | `0x00ce8ca0` | `0x00c5bc70` |
|
||||
| `TheGameState` | `0x00cdbbc4` | `0x00bef0c0` |
|
||||
| `GlobalData` | `0x00ce2fa8` | `0x00c0d8e4` |
|
||||
| `TheMessageStream` | `0x00ce2fb8` | `0x00c0ecd4` |
|
||||
|
||||
### Reconstructed layouts
|
||||
|
||||
- **`GameLogic`** — tick counter at `+0x50` (incremented once per 30 Hz
|
||||
simulation step); `starting` guard flag at `+0xa7`, raised while a new match
|
||||
initialises.
|
||||
- **`MessageStream`** — intrusive doubly linked list; head at `+0x24`, tail at
|
||||
`+0x28`. `appendMessage` (`0x0060c4a0`) allocates a `0x74`-byte node:
|
||||
`+0x00` next, `+0x04` prev, `+0x08` owner stream, `+0x0c` message type,
|
||||
`+0x10` player index, `+0x18` capacity, `+0x1c` data pointer (`node + 0x20`).
|
||||
- **`Player`** — money via `std::vector<Money*>` at `+0xe4`; power at `+0x74`;
|
||||
team at `+0xac`; relation maps at `+0xfc` / `+0x100`.
|
||||
- **Skirmish setup** (`SkirmishGameInfo`, pointer at `[0x00ce3a78]`) — starting
|
||||
cash `+0x64`, player slots `+0xfc` (stride `0x5c`, 6 slots), faction at
|
||||
`slot + 0x18` (`Empire=2`, `Allied=4`, `Soviet=8`, `Random=7`).
|
||||
|
||||
### Match start
|
||||
|
||||
The BEGIN button calls `SkirmishGameOptionsMenu::start` (`0x00b28d60`), which
|
||||
copies the map, calls `GameInfo::startGame(0)`, seeds the logic random and
|
||||
appends `MSG_NEW_GAME` (`0x2`). `startNewGame` itself is `0x00623e40`. OpenRA3
|
||||
mirrors this two-phase `prepare_new_game` / `start_new_game` split.
|
||||
|
||||
## Container and map formats
|
||||
|
||||
Recovered by inspection of `Data\*.big` (see `ra3.fs` / `ra3.map`).
|
||||
|
||||
### `BIG4` archive
|
||||
|
||||
All integers little-endian except where noted:
|
||||
|
||||
```
|
||||
offset 0 magic "BIG4"
|
||||
offset 4 fileSize u32 LE total archive size
|
||||
offset 8 fileCount u32 BE number of entries
|
||||
offset 12 indexSize u32
|
||||
offset 16 entries fileCount * { offset u32 BE, size u32 BE, name cstring }
|
||||
```
|
||||
|
||||
Entry offsets are absolute; payloads are RefPack-compressed.
|
||||
|
||||
### RefPack
|
||||
|
||||
EA's `10 FB` stream. `ra3.fs::refpack_decompress` implements the 2/3/4-byte
|
||||
commands and the long-literal/stop opcodes; `refpack_output_size` reads the
|
||||
declared output length without decompressing.
|
||||
|
||||
### Map file (`.map`)
|
||||
|
||||
Two layers of compression:
|
||||
|
||||
```
|
||||
BIG4 payload = RefPack -> "EAR\0" + u32 unpacked_size + RefPack -> "CkMp" ...
|
||||
```
|
||||
|
||||
The `CkMp` payload is the compiled SAGE map: a type/field name table followed by
|
||||
chunk data. Player start positions appear as waypoints named
|
||||
`Player_1_Start`, `Player_2_Start`, ... Each waypoint record carries a `Coord3D`
|
||||
(three little-endian `float`s) shortly after the name; `ra3.map` scans forward
|
||||
from the name for the first plausible `(x, y, 0)` triple. Maps that store starts
|
||||
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)`.
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user