docs: update CLAUDE.md for WeaponRelease port, sound types, bridge patterns
Browse files- Add weapon_release.rs, sound.rs, game/weapon.rs type enums to Key Files
- Document trap hook pattern (install_trap!) and bridge function rules
(addr-as-param pattern, EAX register preservation)
- Add SoundId/KnownSoundId pattern to FFI Style
- Document secondary effect RNG at DDGame+0x45F0
- Update hooking patterns section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CLAUDE.md
CHANGED
|
@@ -171,12 +171,19 @@ Hooks use the `minhook` crate. Two patterns:
|
|
| 171 |
For `__usercall` functions, use a naked trampoline to capture register params before calling the Rust impl.
|
| 172 |
|
| 173 |
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`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
## Desync Debugging
|
| 176 |
|
| 177 |
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:
|
| 178 |
|
| 179 |
-
1. **WA uses a single shared RNG** (DDGame+0x45EC, `AdvanceGameRNG` at 0x53F320) for both gameplay AND visual effects. There is no separate "visual RNG." Even purely decorative things like particle sprites affect the game RNG and will cause desyncs in headless mode if handled differently.
|
| 180 |
2. **DDGame flat memory matching is NOT sufficient.** Constructors and hooks have side effects on sub-objects (display, GfxHandler, PCLandscape). Compare all objects pointed to by DDGame AND DDGameWrapper.
|
| 181 |
3. **Use hardware watchpoints** (`debug_watchpoint.rs`) with stack traces to find what writes a specific field. DR0βDR3 + VEH handler gives "who wrote this byte?" answers without an external debugger.
|
| 182 |
4. **Per-frame RNG logging** (DDGame+0x45EC) pinpoints the exact frame where simulation diverges. Binary search on frames, not code.
|
|
@@ -203,10 +210,14 @@ Currently dormant β no hooks wired up. Activate by adding calls in `game_sessi
|
|
| 203 |
- `crates/openwa-core/src/rebase.rs` β ASLR delta computation
|
| 204 |
- `crates/openwa-core/src/wa/` β Typed WA function wrappers (MFC, frontend, registry, DDGame)
|
| 205 |
- `crates/openwa-core/src/task/base.rs` β CTask<V>, CTaskVtable (7 slots), Task trait, SharedDataTable, broadcast_message
|
| 206 |
-
- `crates/openwa-core/src/task/worm.rs` β CTaskWorm, CTaskWormVTable (20 slots), WormState enum
|
|
|
|
|
|
|
| 207 |
- `crates/openwa-core/src/engine/team_arena.rs` β TeamArenaState, WormEntry, TeamHeader, TeamArenaRef
|
| 208 |
- `crates/openwa-core/src/engine/ddgame_constructor.rs` β DDGame constructor (create_ddgame, init_graphics_and_resources)
|
| 209 |
-
- `crates/openwa-wormkit/src/replacements/weapon.rs` β
|
|
|
|
|
|
|
| 210 |
- `crates/openwa-wormkit/src/replacements/` β Function replacements (one file per subsystem)
|
| 211 |
- `crates/openwa-wormkit/src/replacements/task/` β Vtable method replacements (cloud, filter, ...)
|
| 212 |
- `docs/re-notes/` β Reverse engineering documentation (task hierarchy, memory map, frontend screens)
|
|
@@ -340,7 +351,8 @@ Add type safety incrementally where it's beneficial β this is a reverse engine
|
|
| 340 |
- **Wrapper structs over raw values**: Create `#[repr(C)]` structs for known memory layouts. Access fields by name, not pointer arithmetic. Even partially-known structs (with `_unknown_XX` padding) are better than raw offsets.
|
| 341 |
- **Handle newtypes for opaque pointers**: When a pointer's target layout is unknown, wrap it in a newtype (e.g., `WavPlayerHandle(u32)`, `CWndHandle(u32)`) with methods that encapsulate the unsafe calls. This keeps inline asm and raw pointer work out of hook logic.
|
| 342 |
- **Typed pointers over integers**: Prefer `*mut DDGame` over `u32` for pointer parameters. Use `*const c_char` for C string pointers, not `*const u8`.
|
| 343 |
-
- **Constants over magic numbers**: Name addresses (`va::FESFX_WAV_PLAYER`), sizes (`MAX_PATH`), and offsets. Magic numbers in code should be rare and commented.
|
|
|
|
| 344 |
- **Wrap inline asm in safe-to-call functions**: Isolate `asm!` / `naked_asm!` blocks in small dedicated functions (e.g., `get_team_config_name()`, `wav_player_stop_raw()`). Hook functions should read like normal Rust, calling into asm wrappers only when needed.
|
| 345 |
- **ESI/EDI are LLVM-reserved on x86**: Cannot use `in("esi")` or `in("edi")` in `core::arch::asm!`. Use `#[unsafe(naked)]` functions with `naked_asm!` when these registers are needed.
|
| 346 |
- **`heapless::CString<N>`** for stack-allocated null-terminated path buffers (auto nul terminator, `as_ptr()` returns `*const c_char`).
|
|
|
|
| 171 |
For `__usercall` functions, use a naked trampoline to capture register params before calling the Rust impl.
|
| 172 |
|
| 173 |
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`.
|
| 174 |
+
4. **Trap hook** (`install_trap!`): For functions whose only caller is now ported Rust. Panics if called unexpectedly. Used for FireWeapon (0x51EE60) after WeaponRelease was ported.
|
| 175 |
+
|
| 176 |
+
### Bridge function patterns
|
| 177 |
+
|
| 178 |
+
When calling unported WA functions from Rust, use naked asm bridges. **Always pass the runtime target address as a cdecl parameter** (e.g., `rb(va::FUNC_ADDR)` as the last arg). Do NOT use `sym` + `jmp [ptr]` indirection through static pointers β this causes crashes due to relocation/PIC issues on x86 DLLs.
|
| 179 |
+
|
| 180 |
+
When a register param (e.g., EAX) must be set for the target function, load the call target into a **different** register (EBX, etc.) before the `call`. Do not use EAX for the target if EAX is also a parameter.
|
| 181 |
|
| 182 |
## Desync Debugging
|
| 183 |
|
| 184 |
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:
|
| 185 |
|
| 186 |
+
1. **WA uses a single shared RNG** (DDGame+0x45EC, `AdvanceGameRNG` at 0x53F320) for both gameplay AND visual effects. There is no separate "visual RNG." Even purely decorative things like particle sprites affect the game RNG and will cause desyncs in headless mode if handled differently. A secondary effect RNG exists at DDGame+0x45F0 (`advance_effect_rng()`, simpler LCG without frame_counter) β used by WeaponRelease visual effects. Uses `team_health_ratio[0]` (unused index-0 slot).
|
| 187 |
2. **DDGame flat memory matching is NOT sufficient.** Constructors and hooks have side effects on sub-objects (display, GfxHandler, PCLandscape). Compare all objects pointed to by DDGame AND DDGameWrapper.
|
| 188 |
3. **Use hardware watchpoints** (`debug_watchpoint.rs`) with stack traces to find what writes a specific field. DR0βDR3 + VEH handler gives "who wrote this byte?" answers without an external debugger.
|
| 189 |
4. **Per-frame RNG logging** (DDGame+0x45EC) pinpoints the exact frame where simulation diverges. Binary search on frames, not code.
|
|
|
|
| 210 |
- `crates/openwa-core/src/rebase.rs` β ASLR delta computation
|
| 211 |
- `crates/openwa-core/src/wa/` β Typed WA function wrappers (MFC, frontend, registry, DDGame)
|
| 212 |
- `crates/openwa-core/src/task/base.rs` β CTask<V>, CTaskVtable (7 slots), Task trait, SharedDataTable, broadcast_message
|
| 213 |
+
- `crates/openwa-core/src/task/worm.rs` β CTaskWorm, CTaskWormVTable (20 slots), WormState enum (all gaps filled)
|
| 214 |
+
- `crates/openwa-core/src/game/weapon.rs` β Weapon enum, WeaponEntry, FireType/FireMethod/SpecialFireSubtype enums, WeaponFireParams
|
| 215 |
+
- `crates/openwa-core/src/audio/sound.rs` β SoundId newtype (any u32) + KnownSoundId enum (1-126 SFX)
|
| 216 |
- `crates/openwa-core/src/engine/team_arena.rs` β TeamArenaState, WormEntry, TeamHeader, TeamArenaRef
|
| 217 |
- `crates/openwa-core/src/engine/ddgame_constructor.rs` β DDGame constructor (create_ddgame, init_graphics_and_resources)
|
| 218 |
+
- `crates/openwa-wormkit/src/replacements/weapon.rs` β FireWeapon dispatch (trapped, called from weapon_release), 20+ subtypes pure Rust
|
| 219 |
+
- `crates/openwa-wormkit/src/replacements/weapon_release.rs` β WeaponRelease (0x51C3D0), SpawnEffect (0x547C30) β fully ported
|
| 220 |
+
- `crates/openwa-wormkit/src/replacements/sound.rs` β Sound queue, PlaySoundLocal/Global hooks, worm sound functions (stop/play)
|
| 221 |
- `crates/openwa-wormkit/src/replacements/` β Function replacements (one file per subsystem)
|
| 222 |
- `crates/openwa-wormkit/src/replacements/task/` β Vtable method replacements (cloud, filter, ...)
|
| 223 |
- `docs/re-notes/` β Reverse engineering documentation (task hierarchy, memory map, frontend screens)
|
|
|
|
| 351 |
- **Wrapper structs over raw values**: Create `#[repr(C)]` structs for known memory layouts. Access fields by name, not pointer arithmetic. Even partially-known structs (with `_unknown_XX` padding) are better than raw offsets.
|
| 352 |
- **Handle newtypes for opaque pointers**: When a pointer's target layout is unknown, wrap it in a newtype (e.g., `WavPlayerHandle(u32)`, `CWndHandle(u32)`) with methods that encapsulate the unsafe calls. This keeps inline asm and raw pointer work out of hook logic.
|
| 353 |
- **Typed pointers over integers**: Prefer `*mut DDGame` over `u32` for pointer parameters. Use `*const c_char` for C string pointers, not `*const u8`.
|
| 354 |
+
- **Constants over magic numbers**: Name addresses (`va::FESFX_WAV_PLAYER`), sizes (`MAX_PATH`), and offsets. Magic numbers in code should be rare and commented. Use typed enums (`Weapon`, `KnownSoundId`, `FireType`, `SpecialFireSubtype`) and `Fixed` for volume/pitch instead of raw `u32`/`i32`.
|
| 355 |
+
- **SoundId pattern**: `SoundId(u32)` is a transparent newtype for any sound ID (WA uses IDs > 126 for speech). `KnownSoundId` is the typed enum for known SFX (1-126). Sound functions accept `impl Into<SoundId>` so both work. Never reject unknown IDs in hooks β speech/voice lines use high IDs.
|
| 356 |
- **Wrap inline asm in safe-to-call functions**: Isolate `asm!` / `naked_asm!` blocks in small dedicated functions (e.g., `get_team_config_name()`, `wav_player_stop_raw()`). Hook functions should read like normal Rust, calling into asm wrappers only when needed.
|
| 357 |
- **ESI/EDI are LLVM-reserved on x86**: Cannot use `in("esi")` or `in("edi")` in `core::arch::asm!`. Use `#[unsafe(naked)]` functions with `naked_asm!` when these registers are needed.
|
| 358 |
- **`heapless::CString<N>`** for stack-allocated null-terminated path buffers (auto nul terminator, `as_ptr()` returns `*const c_char`).
|