paavohtl Claude Opus 4.6 (1M context) commited on
Commit
d2350ac
Β·
1 Parent(s): a356f52

docs: add DDGame constructor port design spec

Browse files

Incremental 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.