docs: add DDGame constructor port design spec
Browse filesIncremental approach: decompile and map fields, create DDGame::new()
with bridges for sub-calls, then eliminate bridges one at a time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs/superpowers/specs/2026-03-15-ddgame-constructor-design.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DDGame Constructor Port β Design Spec
|
| 2 |
+
|
| 3 |
+
## Context
|
| 4 |
+
|
| 5 |
+
DDGame (0x98B8 bytes) is the central game state object. Its constructor at
|
| 6 |
+
0x56E220 is one of the largest remaining WA.exe dependencies β it allocates
|
| 7 |
+
and initializes all gameplay subsystems (sprites, render queue, weapon table,
|
| 8 |
+
coordinate lists, physics params, etc.).
|
| 9 |
+
|
| 10 |
+
The init chain is already Rust up to this point:
|
| 11 |
+
- `GameEngine__InitHardware` (hardware_init.rs) β creates display, sound, keyboard, palette
|
| 12 |
+
- `DDGameWrapper__Constructor` (game_session.rs) β creates the wrapper, calls DDGame ctor via bridge
|
| 13 |
+
|
| 14 |
+
The DDGame constructor is the last major bridge in the init path. Porting it
|
| 15 |
+
to Rust means the entire game object creation is under our control.
|
| 16 |
+
|
| 17 |
+
## Goal
|
| 18 |
+
|
| 19 |
+
Replace `DDGame__Constructor` (0x56E220, stdcall 9 params + implicit ECX)
|
| 20 |
+
with a Rust `DDGame::new()` that creates the object field by field. Use an
|
| 21 |
+
incremental approach: port trivial inits immediately, bridge sub-constructor
|
| 22 |
+
calls initially, and eliminate bridges one at a time.
|
| 23 |
+
|
| 24 |
+
## Approach: Incremental Field-by-Field
|
| 25 |
+
|
| 26 |
+
### Phase 1 β Decompile & Map
|
| 27 |
+
|
| 28 |
+
1. Decompile DDGame__Constructor (0x56E220) fully in Ghidra
|
| 29 |
+
2. Map every field write to our DDGame struct (already partially mapped in ddgame.rs)
|
| 30 |
+
3. Categorize each initialization:
|
| 31 |
+
- **Trivial**: zero-fill, constant, pointer copy from params β port immediately
|
| 32 |
+
- **Sub-call**: allocates/inits a sub-object β bridge initially
|
| 33 |
+
- **Complex**: loops, conditionals, computed values β understand then port
|
| 34 |
+
4. Add any missing struct fields discovered during decompilation
|
| 35 |
+
5. Label all sub-functions called by the constructor in Ghidra
|
| 36 |
+
|
| 37 |
+
### Phase 2 β Rust DDGame::new()
|
| 38 |
+
|
| 39 |
+
1. Create `DDGame::new(params...) -> *mut DDGame` in openwa-core
|
| 40 |
+
- Allocates 0x98B8 bytes via `wa_malloc` (or `WABox<DDGame>`)
|
| 41 |
+
- Performs all trivial field initializations in Rust
|
| 42 |
+
- Calls WA sub-functions via typed bridge functions for complex parts
|
| 43 |
+
2. Replace the `ddgame_constructor_call` naked bridge in game_session.rs
|
| 44 |
+
with a direct call to `DDGame::new()`
|
| 45 |
+
3. Trap the original DDGame__Constructor (it should no longer be called)
|
| 46 |
+
4. Verify with replay tests
|
| 47 |
+
|
| 48 |
+
### Phase 3 β Bridge Elimination (iterative)
|
| 49 |
+
|
| 50 |
+
Convert each bridged sub-call to pure Rust, simplest first:
|
| 51 |
+
|
| 52 |
+
| Priority | Sub-call | Description |
|
| 53 |
+
|----------|----------|-------------|
|
| 54 |
+
| 1 | Field zero-fill regions | memset-style clears of large unknown regions |
|
| 55 |
+
| 2 | Sprite cache init | Pointer array zeroing / allocation |
|
| 56 |
+
| 3 | Coordinate list init | FUN_004FB3A0 β alloc + init capacity |
|
| 57 |
+
| 4 | Object pool init | DDGame+0x600..0x3600 permutation arrays |
|
| 58 |
+
| 5 | Render queue setup | Alloc + init the RenderQueue object |
|
| 59 |
+
| 6 | Weapon table init | Alloc + populate weapon data tables |
|
| 60 |
+
| 7 | Game state init | Complex conditional logic, terrain hooks |
|
| 61 |
+
|
| 62 |
+
Each bridge elimination is a self-contained commit: convert, test, move on.
|
| 63 |
+
|
| 64 |
+
## Parameters
|
| 65 |
+
|
| 66 |
+
The constructor signature (from Ghidra + CLAUDE.md):
|
| 67 |
+
```
|
| 68 |
+
DDGame__Constructor(
|
| 69 |
+
this, // allocated 0x98B8 bytes
|
| 70 |
+
keyboard, // *mut DDKeyboard
|
| 71 |
+
display, // *mut DDDisplay
|
| 72 |
+
sound, // *mut DSSound (or null)
|
| 73 |
+
palette, // *mut Palette
|
| 74 |
+
music, // *mut Music (or null)
|
| 75 |
+
param7, // unknown (0x1F4 observed)
|
| 76 |
+
caller, // parent/caller pointer (often null)
|
| 77 |
+
game_info // *mut GameInfo
|
| 78 |
+
)
|
| 79 |
+
// Implicit: ECX = network pointer (DDNetGameWrapper or null)
|
| 80 |
+
// stdcall, RET 0x24 (9 stack params = 36 bytes)
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## Critical Files
|
| 84 |
+
|
| 85 |
+
| File | Action |
|
| 86 |
+
|------|--------|
|
| 87 |
+
| `crates/openwa-core/src/engine/ddgame.rs` | Add `DDGame::new()`, fill unknown fields |
|
| 88 |
+
| `crates/openwa-wormkit/src/replacements/game_session.rs` | Replace bridge with `DDGame::new()` |
|
| 89 |
+
| `crates/openwa-core/src/address.rs` | Add addresses for sub-functions |
|
| 90 |
+
|
| 91 |
+
## Verification
|
| 92 |
+
|
| 93 |
+
Each phase is verified independently:
|
| 94 |
+
- **Headless replay test**: determinism preserved (byte-identical log output)
|
| 95 |
+
- **Headful replay test**: game plays, no crashes, validation passes
|
| 96 |
+
- **Manual gameplay**: start match, play a turn, verify no visual/audio glitches
|
| 97 |
+
- **Struct assertions**: `size_of::<DDGame>() == 0x98B8` still holds
|
| 98 |
+
|
| 99 |
+
## Risks
|
| 100 |
+
|
| 101 |
+
- **Unknown fields**: DDGame has large unmapped regions (0x550..0x2600, 0x2E00..0x45EC, etc.). The constructor may write to fields we haven't identified. Mitigation: zero-fill unknowns and compare behavior.
|
| 102 |
+
- **Sub-call ordering**: Some sub-calls may depend on fields set by earlier sub-calls. Mitigation: preserve exact ordering from the decompile.
|
| 103 |
+
- **Implicit state**: ECX (network pointer) and global state may affect initialization. Mitigation: capture all implicit params in the bridge.
|