docs: update CLAUDE.md for typed vtable infrastructure
Browse filesDocument new patterns from this session:
- Generic CTask<V: Vtable> and CGameTask<V> for typed vtable pointers
- Task trait with ddgame(), as_task_ptr(), broadcast_message()
- read_volatile requirement in broadcast_message (LLVM caching bug)
- Vtable method hooking via vtable_replace! and replacements/task/
- bind macro dotted field path support (base.vtable)
- Decompiler calling convention warning
- CTaskMissile constructor exception (thiscall, not stdcall)
- New key files: task/base.rs, team_arena.rs, ddgame_constructor.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CLAUDE.md
CHANGED
|
@@ -150,9 +150,11 @@ All addresses in `address.rs` are Ghidra VAs. Always use `rb()` to convert to ru
|
|
| 150 |
|
| 151 |
## Calling Convention Rules
|
| 152 |
|
| 153 |
-
These are critical β wrong conventions cause stack corruption and crashes
|
| 154 |
|
| 155 |
-
|
|
|
|
|
|
|
| 156 |
- **VTable methods are `__thiscall`**: ECX = this, remaining params on stack.
|
| 157 |
- **Always check `RET imm16`** in disassembly to verify stack parameter count. The immediate value = bytes of params cleaned by callee (stdcall/thiscall). `RET 0x10` = 16 bytes = 4 params.
|
| 158 |
- **MSVC `__usercall`**: Some functions pass implicit params in registers (e.g., FrontendChangeScreen uses ESI for dialog pointer). These need `#[unsafe(naked)]` trampolines.
|
|
@@ -166,6 +168,8 @@ Hooks use the `minhook` crate. Two patterns:
|
|
| 166 |
|
| 167 |
For `__usercall` functions, use a naked trampoline to capture register params before calling the Rust impl.
|
| 168 |
|
|
|
|
|
|
|
| 169 |
## Desync Debugging
|
| 170 |
|
| 171 |
Replay desyncs (checksum mismatches) can be caused by any code difference β constructor side effects, hooked function behaviour, missing state, wrong calling conventions, etc. Key methodology:
|
|
@@ -196,7 +200,11 @@ Currently dormant β no hooks wired up. Activate by adding calls in `game_sessi
|
|
| 196 |
- `crates/openwa-core/src/wa_call.rs` β Helpers for calling WA functions (thiscall, stdcall wrappers)
|
| 197 |
- `crates/openwa-core/src/rebase.rs` β ASLR delta computation
|
| 198 |
- `crates/openwa-core/src/wa/` β Typed WA function wrappers (MFC, frontend, registry, DDGame)
|
|
|
|
|
|
|
|
|
|
| 199 |
- `crates/openwa-wormkit/src/replacements/` β Function replacements (one file per subsystem)
|
|
|
|
| 200 |
- `docs/re-notes/` β Reverse engineering documentation (task hierarchy, memory map, frontend screens)
|
| 201 |
- `docs/plans/` β Design docs and implementation plans
|
| 202 |
|
|
@@ -312,8 +320,11 @@ vtable_replace!(DSSoundVtable, va::DS_SOUND_VTABLE, {
|
|
| 312 |
- Unknown struct fields as `_unknown_XX` padding arrays
|
| 313 |
- Fixed-point: `Fixed(i32)` newtype, 16.16 format (0x10000 = 1.0)
|
| 314 |
- Naked asm uses `naked_asm!` (Rust 1.79+ syntax), not `asm!`
|
| 315 |
-
- **
|
| 316 |
-
- **`
|
|
|
|
|
|
|
|
|
|
| 317 |
- **`vcall!` macro**: Still available for raw-pointer vtable dispatch without bind wrappers. Expands to `((*(*obj).vtable).method)(obj, args...)`.
|
| 318 |
|
| 319 |
## FFI Style
|
|
|
|
| 150 |
|
| 151 |
## Calling Convention Rules
|
| 152 |
|
| 153 |
+
These are critical β wrong conventions cause stack corruption and crashes.
|
| 154 |
|
| 155 |
+
**The Ghidra decompiler is UNTRUSTWORTHY for calling conventions.** It frequently misidentifies stdcall/thiscall/usercall. Always verify via disassembly: check the `RET imm16` instruction AND the caller's register setup at the call site.
|
| 156 |
+
|
| 157 |
+
- **Constructors are usually `__stdcall`** β `this` passed on stack, not ECX. **Exception**: CTaskMissile constructor (0x507D10) is `__thiscall` (ECX=this, 3 stack params, RET 0xC). Always verify by checking the call site's register setup.
|
| 158 |
- **VTable methods are `__thiscall`**: ECX = this, remaining params on stack.
|
| 159 |
- **Always check `RET imm16`** in disassembly to verify stack parameter count. The immediate value = bytes of params cleaned by callee (stdcall/thiscall). `RET 0x10` = 16 bytes = 4 params.
|
| 160 |
- **MSVC `__usercall`**: Some functions pass implicit params in registers (e.g., FrontendChangeScreen uses ESI for dialog pointer). These need `#[unsafe(naked)]` trampolines.
|
|
|
|
| 168 |
|
| 169 |
For `__usercall` functions, use a naked trampoline to capture register params before calling the Rust impl.
|
| 170 |
|
| 171 |
+
3. **Vtable method replacement**: Use `vtable_replace!` to patch vtable slots at runtime. Write the replacement as `unsafe extern "thiscall" fn`. For base-class call-through, either save the original via `[ORIG_STATIC]` syntax, or call `broadcast_message()` for CTask::HandleMessage. See `replacements/task/cloud.rs`.
|
| 172 |
+
|
| 173 |
## Desync Debugging
|
| 174 |
|
| 175 |
Replay desyncs (checksum mismatches) can be caused by any code difference β constructor side effects, hooked function behaviour, missing state, wrong calling conventions, etc. Key methodology:
|
|
|
|
| 200 |
- `crates/openwa-core/src/wa_call.rs` β Helpers for calling WA functions (thiscall, stdcall wrappers)
|
| 201 |
- `crates/openwa-core/src/rebase.rs` β ASLR delta computation
|
| 202 |
- `crates/openwa-core/src/wa/` β Typed WA function wrappers (MFC, frontend, registry, DDGame)
|
| 203 |
+
- `crates/openwa-core/src/task/base.rs` β CTask<V>, CTaskVtable, Task trait, SharedDataTable, broadcast_message
|
| 204 |
+
- `crates/openwa-core/src/engine/team_arena.rs` β TeamArenaState, WormEntry, TeamHeader, TeamArenaRef
|
| 205 |
+
- `crates/openwa-core/src/engine/ddgame_constructor.rs` β DDGame constructor (create_ddgame, init_graphics_and_resources)
|
| 206 |
- `crates/openwa-wormkit/src/replacements/` β Function replacements (one file per subsystem)
|
| 207 |
+
- `crates/openwa-wormkit/src/replacements/task/` β Vtable method replacements (cloud, filter, ...)
|
| 208 |
- `docs/re-notes/` β Reverse engineering documentation (task hierarchy, memory map, frontend screens)
|
| 209 |
- `docs/plans/` β Design docs and implementation plans
|
| 210 |
|
|
|
|
| 320 |
- Unknown struct fields as `_unknown_XX` padding arrays
|
| 321 |
- Fixed-point: `Fixed(i32)` newtype, 16.16 format (0x10000 = 1.0)
|
| 322 |
- Naked asm uses `naked_asm!` (Rust 1.79+ syntax), not `asm!`
|
| 323 |
+
- **Generic CTask<V> for typed vtables**: `CTask<V: Vtable = *const c_void>` and `CGameTask<V: Vtable = *const c_void>` take a vtable pointer type parameter. Subclasses specify their typed vtable: `CTaskTeam { base: CTask<*const CTaskTeamVTable> }`. The `Vtable` marker trait is auto-implemented by the `#[vtable]` macro. `FieldRegistry` derive supports generics by substituting type params with their defaults for `offset_of`/`size_of`.
|
| 324 |
+
- **`Task` trait**: Provides `task()`, `ddgame()`, `as_task_ptr()`, `as_task_ptr_mut()`, and `broadcast_message()` on all CTask subclasses. Eliminates `.base.base` chains. Implemented for all task types in `task/mod.rs`.
|
| 325 |
+
- **`broadcast_message()`**: Pure Rust port of CTask::HandleMessage (0x562F30). Iterates the sparse children array and calls each child's vtable[2]. Uses `read_volatile` for `children_watermark` and `children_data` β **required** because LLVM caches these reads across virtual dispatch calls even through `*mut`.
|
| 326 |
+
- **Typed vtable structs via `#[vtable(...)]`**: Use the attribute macro to define vtable structs with `fn(this: *mut T, ...)` shorthand and `#[slot(N)]` for sparse layouts. The macro handles `unsafe extern "thiscall"`, gap-filling, registry metadata, generates `bind_!` calling wrappers, and implements the `Vtable` marker trait. See the `#[vtable(...)]` section above.
|
| 327 |
+
- **`bind_XxxVtable!`**: Generated by `#[vtable(...)]`, creates `&mut self` method wrappers on the class struct. Supports dotted field paths for inherited vtables: `bind_CTaskTeamVTable!(CTaskTeam, base.vtable)`.
|
| 328 |
- **`vcall!` macro**: Still available for raw-pointer vtable dispatch without bind wrappers. Expands to `((*(*obj).vtable).method)(obj, args...)`.
|
| 329 |
|
| 330 |
## FFI Style
|