feat: port remaining 5 team/weapon accessor hooks (32 total)
Browse filesAdd SubtractAmmo, CountAliveWorms, CheckTeamWormState0x64,
CheckAnyWormState0x8b, and SetActiveWorm_Maybe replacements.
Extend TeamWeaponState with active_worm_count, last_active_team,
and last_active_alliance fields. Rename SetTeamAliveFlag to
SetActiveWorm_Maybe based on runtime behavior analysis.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CLAUDE.md
CHANGED
|
@@ -46,6 +46,8 @@ WormKit auto-loads any `wk*.dll` from the game directory. Logs appear in the gam
|
|
| 46 |
|
| 47 |
Use the `/replay-test` skill to automatically build, deploy, launch WA.exe with a replay file, capture validation logs, and present results. This is the fastest way to validate struct layouts, hooks, and game state against live WA.exe.
|
| 48 |
|
|
|
|
|
|
|
| 49 |
```bash
|
| 50 |
# Manual invocation (skill runs this automatically):
|
| 51 |
powershell -ExecutionPolicy Bypass -File replay-test.ps1
|
|
@@ -116,6 +118,9 @@ For `__usercall` functions, use a naked trampoline to capture register params be
|
|
| 116 |
A Ghidra MCP bridge is configured in `.mcp.json`. When using Ghidra tools:
|
| 117 |
- **Prefer batch tools** (`batch_create_labels`, `batch_rename_function_components`) — single-item tools have address parsing bugs.
|
| 118 |
- WA.exe is loaded at image base 0x400000 in Ghidra.
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
## Third-Party RE Sources
|
| 121 |
|
|
@@ -124,8 +129,6 @@ A Ghidra MCP bridge is configured in `.mcp.json`. When using Ghidra tools:
|
|
| 124 |
|
| 125 |
## Design Conventions
|
| 126 |
|
| 127 |
-
- `Ptr32 = u32` for pointer fields (compiles on 64-bit host, correct sizes on 32-bit target)
|
| 128 |
-
- `#[repr(u32)]` enums with `TryFrom<u32>` for safe conversion from game memory
|
| 129 |
- Unknown struct fields as `_unknown_XX` padding arrays
|
| 130 |
- Fixed-point: `Fixed(i32)` newtype, 16.16 format (0x10000 = 1.0)
|
| 131 |
- Naked asm uses `naked_asm!` (Rust 1.79+ syntax), not `asm!`
|
|
|
|
| 46 |
|
| 47 |
Use the `/replay-test` skill to automatically build, deploy, launch WA.exe with a replay file, capture validation logs, and present results. This is the fastest way to validate struct layouts, hooks, and game state against live WA.exe.
|
| 48 |
|
| 49 |
+
**Use replay testing to validate assumptions and test theories!** You don't have to figure out everything from disassembly and static analysis. Make a hypothesis, implement it in the DLL, then use replay testing to see if it holds up against the real game. This iterative approach is much faster than trying to get everything right on the first try.
|
| 50 |
+
|
| 51 |
```bash
|
| 52 |
# Manual invocation (skill runs this automatically):
|
| 53 |
powershell -ExecutionPolicy Bypass -File replay-test.ps1
|
|
|
|
| 118 |
A Ghidra MCP bridge is configured in `.mcp.json`. When using Ghidra tools:
|
| 119 |
- **Prefer batch tools** (`batch_create_labels`, `batch_rename_function_components`) — single-item tools have address parsing bugs.
|
| 120 |
- WA.exe is loaded at image base 0x400000 in Ghidra.
|
| 121 |
+
- When you encounter unnamed functions, globals or structs, name them in Ghidra if you know their purpose. Even a guess is helpful for future reference, but add `_Maybe` suffix if uncertain.
|
| 122 |
+
- Remove `_Maybe` suffix when you confirm the purpose.
|
| 123 |
+
- When you learn more about a function or address, update both the Ghidra database (rename function / label and update signature) and the corresponding Rust code.
|
| 124 |
|
| 125 |
## Third-Party RE Sources
|
| 126 |
|
|
|
|
| 129 |
|
| 130 |
## Design Conventions
|
| 131 |
|
|
|
|
|
|
|
| 132 |
- Unknown struct fields as `_unknown_XX` padding arrays
|
| 133 |
- Fixed-point: `Fixed(i32)` newtype, 16.16 format (0x10000 = 1.0)
|
| 134 |
- Naked asm uses `naked_asm!` (Rust 1.79+ syntax), not `asm!`
|
crates/openwa-types/src/address.rs
CHANGED
|
@@ -150,6 +150,17 @@ pub mod va {
|
|
| 150 |
/// Despite the comparison to 100, reads worms[].state NOT .health.
|
| 151 |
/// usercall(EAX=base) → EAX=bool, plain RET.
|
| 152 |
pub const CHECK_WORM_STATE_0X64: u32 = 0x0052_28D0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
// === Graphics / rendering ===
|
| 155 |
|
|
|
|
| 150 |
/// Despite the comparison to 100, reads worms[].state NOT .health.
|
| 151 |
/// usercall(EAX=base) → EAX=bool, plain RET.
|
| 152 |
pub const CHECK_WORM_STATE_0X64: u32 = 0x0052_28D0;
|
| 153 |
+
/// Per-team version of CheckWormState0x64. Returns 1 if any worm on the
|
| 154 |
+
/// specified team has state==0x64. 1 xref.
|
| 155 |
+
/// usercall(EAX=team_idx, ECX=base) → EAX=bool, plain RET.
|
| 156 |
+
pub const CHECK_TEAM_WORM_STATE_0X64: u32 = 0x0052_2930;
|
| 157 |
+
/// Scans all teams for any worm with state 0x8b. 1 xref.
|
| 158 |
+
/// usercall(EAX=base) → EAX=bool, plain RET.
|
| 159 |
+
pub const CHECK_ANY_WORM_STATE_0X8B: u32 = 0x0052_2970;
|
| 160 |
+
/// Sets the active worm for a team. flag=0 deactivates, flag=N sets worm N active.
|
| 161 |
+
/// Called on turn transitions and worm selection (Tab key).
|
| 162 |
+
/// usercall(EAX=base, EDX=team_idx, ESI=worm_index) → void, plain RET. 3 xrefs.
|
| 163 |
+
pub const SET_ACTIVE_WORM_MAYBE: u32 = 0x0052_2500;
|
| 164 |
|
| 165 |
// === Graphics / rendering ===
|
| 166 |
|
crates/openwa-types/src/ddgame.rs
CHANGED
|
@@ -324,21 +324,25 @@ pub struct TeamWeaponState {
|
|
| 324 |
/// 0x2C28: Game phase counter (>=484 = sudden death, >=-2 = normal game)
|
| 325 |
pub game_phase: i32,
|
| 326 |
|
| 327 |
-
// === Alliance tracking (set by CountTeamsByAlliance
|
| 328 |
|
| 329 |
/// 0x2C2C: Current alliance being evaluated
|
| 330 |
pub current_alliance: i32,
|
| 331 |
-
/// 0x2C30:
|
| 332 |
-
pub
|
| 333 |
-
/// 0x2C34: Count of teams
|
| 334 |
pub active_team_count: i32,
|
| 335 |
/// 0x2C38: Count of teams matching current_alliance
|
| 336 |
pub same_alliance_count: i32,
|
| 337 |
/// 0x2C3C: Count of teams not matching current_alliance
|
| 338 |
pub enemy_team_count: i32,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
-
const _: () = assert!(core::mem::size_of::<TeamWeaponState>() ==
|
| 342 |
|
| 343 |
/// Worm state constants and helpers.
|
| 344 |
pub mod worm {
|
|
|
|
| 324 |
/// 0x2C28: Game phase counter (>=484 = sudden death, >=-2 = normal game)
|
| 325 |
pub game_phase: i32,
|
| 326 |
|
| 327 |
+
// === Alliance tracking (set by CountTeamsByAlliance + SetActiveWorm_Maybe) ===
|
| 328 |
|
| 329 |
/// 0x2C2C: Current alliance being evaluated
|
| 330 |
pub current_alliance: i32,
|
| 331 |
+
/// 0x2C30: Count of teams with an active worm (set by SetActiveWorm_Maybe)
|
| 332 |
+
pub active_worm_count: i32,
|
| 333 |
+
/// 0x2C34: Count of active teams with valid alliance (>=0)
|
| 334 |
pub active_team_count: i32,
|
| 335 |
/// 0x2C38: Count of teams matching current_alliance
|
| 336 |
pub same_alliance_count: i32,
|
| 337 |
/// 0x2C3C: Count of teams not matching current_alliance
|
| 338 |
pub enemy_team_count: i32,
|
| 339 |
+
/// 0x2C40: Last team index set active (written by SetActiveWorm_Maybe)
|
| 340 |
+
pub last_active_team: i32,
|
| 341 |
+
/// 0x2C44: Alliance of last activated team (written by SetActiveWorm_Maybe)
|
| 342 |
+
pub last_active_alliance: i32,
|
| 343 |
}
|
| 344 |
|
| 345 |
+
const _: () = assert!(core::mem::size_of::<TeamWeaponState>() == 0x2C48);
|
| 346 |
|
| 347 |
/// Worm state constants and helpers.
|
| 348 |
pub mod worm {
|
crates/openwa-wormkit/src/replacements/team.rs
CHANGED
|
@@ -6,6 +6,9 @@
|
|
| 6 |
//! - IsWormInSpecialState (0x5226B0): check worm state flag
|
| 7 |
//! - GetWormPosition (0x522700): read worm X,Y coordinates
|
| 8 |
//! - CheckWormState0x64 (0x5228D0): check if any worm has state 0x64
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
use openwa_types::address::va;
|
| 11 |
use openwa_types::ddgame::{self, offsets, FullTeamBlock, TeamWeaponState};
|
|
@@ -150,6 +153,125 @@ unsafe extern "cdecl" fn check_worm_state_0x64_impl(base: u32) -> u32 {
|
|
| 150 |
usercall_trampoline!(fn trampoline_check_worm_state_0x64; impl_fn = check_worm_state_0x64_impl;
|
| 151 |
reg = eax);
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
// ============================================================
|
| 154 |
// Hook installation
|
| 155 |
// ============================================================
|
|
@@ -185,6 +307,24 @@ pub fn install() -> Result<(), String> {
|
|
| 185 |
va::CHECK_WORM_STATE_0X64,
|
| 186 |
trampoline_check_worm_state_0x64 as *const (),
|
| 187 |
)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
Ok(())
|
|
|
|
| 6 |
//! - IsWormInSpecialState (0x5226B0): check worm state flag
|
| 7 |
//! - GetWormPosition (0x522700): read worm X,Y coordinates
|
| 8 |
//! - CheckWormState0x64 (0x5228D0): check if any worm has state 0x64
|
| 9 |
+
//! - CheckTeamWormState0x64 (0x522930): per-team version of above
|
| 10 |
+
//! - CheckAnyWormState0x8b (0x522970): scan all teams for state 0x8b
|
| 11 |
+
//! - SetActiveWorm_Maybe (0x522500): update team active state and counters
|
| 12 |
|
| 13 |
use openwa_types::address::va;
|
| 14 |
use openwa_types::ddgame::{self, offsets, FullTeamBlock, TeamWeaponState};
|
|
|
|
| 153 |
usercall_trampoline!(fn trampoline_check_worm_state_0x64; impl_fn = check_worm_state_0x64_impl;
|
| 154 |
reg = eax);
|
| 155 |
|
| 156 |
+
// ============================================================
|
| 157 |
+
// CheckTeamWormState0x64 replacement (0x522930)
|
| 158 |
+
// ============================================================
|
| 159 |
+
// __usercall: EAX = base, EDX = team_idx
|
| 160 |
+
// plain RET, returns EAX = bool (1 if any worm on team has state 0x64)
|
| 161 |
+
//
|
| 162 |
+
// Per-team version of CheckWormState0x64. 1 xref (FUN_00556ad0).
|
| 163 |
+
|
| 164 |
+
unsafe extern "cdecl" fn check_team_worm_state_0x64_impl(base: u32, team_idx: u32) -> u32 {
|
| 165 |
+
let blocks = (base as *const u8).sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 166 |
+
let block = &*blocks.add(team_idx as usize);
|
| 167 |
+
let sentinel = &(*blocks.add(team_idx as usize + 1)).worms[0];
|
| 168 |
+
|
| 169 |
+
if sentinel.sentinel_eliminated() != 0 {
|
| 170 |
+
return 0;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
let worm_count = sentinel.sentinel_worm_count();
|
| 174 |
+
for w in 1..=worm_count as usize {
|
| 175 |
+
if block.worms[w].state == 0x64 {
|
| 176 |
+
return 1;
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
0
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
usercall_trampoline!(fn trampoline_check_team_worm_state_0x64; impl_fn = check_team_worm_state_0x64_impl;
|
| 183 |
+
regs = [eax, edx]);
|
| 184 |
+
|
| 185 |
+
// ============================================================
|
| 186 |
+
// CheckAnyWormState0x8b replacement (0x522970)
|
| 187 |
+
// ============================================================
|
| 188 |
+
// __usercall: EAX = base
|
| 189 |
+
// plain RET, returns EAX = bool (1 if any worm on any team has state 0x8b)
|
| 190 |
+
//
|
| 191 |
+
// Scans all teams. 1 xref (FUN_00557310).
|
| 192 |
+
|
| 193 |
+
unsafe extern "cdecl" fn check_any_worm_state_0x8b_impl(base: u32) -> u32 {
|
| 194 |
+
let state = &*(base as *const TeamWeaponState);
|
| 195 |
+
let blocks = (base as *const u8).sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 196 |
+
|
| 197 |
+
for i in 1..=state.team_count as usize {
|
| 198 |
+
let sentinel = &(*blocks.add(i + 1)).worms[0];
|
| 199 |
+
if sentinel.sentinel_eliminated() != 0 {
|
| 200 |
+
continue;
|
| 201 |
+
}
|
| 202 |
+
let worm_count = sentinel.sentinel_worm_count();
|
| 203 |
+
let worm_block = &*blocks.add(i);
|
| 204 |
+
for w in 1..=worm_count as usize {
|
| 205 |
+
if worm_block.worms[w].state == 0x8b {
|
| 206 |
+
return 1;
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
0
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
usercall_trampoline!(fn trampoline_check_any_worm_state_0x8b; impl_fn = check_any_worm_state_0x8b_impl;
|
| 214 |
+
reg = eax);
|
| 215 |
+
|
| 216 |
+
// ============================================================
|
| 217 |
+
// SetActiveWorm_Maybe replacement (0x522500)
|
| 218 |
+
// ============================================================
|
| 219 |
+
// __usercall: EAX = base, EDX = team_idx (1-indexed), ESI = worm_index
|
| 220 |
+
// plain RET (4 RET sites in original)
|
| 221 |
+
//
|
| 222 |
+
// Sets the active worm for a team. worm_index=0 deactivates, worm_index=N
|
| 223 |
+
// sets worm N as active. Called on turn transitions and worm selection (Tab).
|
| 224 |
+
// Updates active counters and records last_active_team/alliance. 3 xrefs.
|
| 225 |
+
|
| 226 |
+
unsafe extern "cdecl" fn set_active_worm_impl(base: u32, team_idx: u32, worm_index: i32) {
|
| 227 |
+
let state = &mut *(base as *mut TeamWeaponState);
|
| 228 |
+
let base_ptr = base as *mut u8;
|
| 229 |
+
|
| 230 |
+
// Access team_data fields: alliance at (team_idx * 0x51C - 0xC),
|
| 231 |
+
// alive_flag at (team_idx * 0x51C - 0x8) — matches team_data::BASE_OFFSET pattern
|
| 232 |
+
let team_offset = team_idx as usize * 0x51C;
|
| 233 |
+
let alliance_ptr = base_ptr.add(team_offset).sub(0xC) as *const i32;
|
| 234 |
+
let alive_ptr = base_ptr.add(team_offset).sub(0x8) as *mut i32;
|
| 235 |
+
|
| 236 |
+
if worm_index == 0 {
|
| 237 |
+
// Deactivate team
|
| 238 |
+
if *alive_ptr != 0 {
|
| 239 |
+
state.active_worm_count -= 1;
|
| 240 |
+
let alliance = *alliance_ptr;
|
| 241 |
+
if alliance >= 0 {
|
| 242 |
+
state.active_team_count -= 1;
|
| 243 |
+
if alliance == state.current_alliance {
|
| 244 |
+
state.same_alliance_count -= 1;
|
| 245 |
+
} else {
|
| 246 |
+
state.enemy_team_count -= 1;
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
*alive_ptr = 0;
|
| 250 |
+
}
|
| 251 |
+
} else {
|
| 252 |
+
// Activate team — only update counters if not already active
|
| 253 |
+
if *alive_ptr == 0 {
|
| 254 |
+
state.active_worm_count += 1;
|
| 255 |
+
state.last_active_team = team_idx as i32;
|
| 256 |
+
let alliance = *alliance_ptr;
|
| 257 |
+
state.last_active_alliance = alliance;
|
| 258 |
+
if alliance >= 0 {
|
| 259 |
+
state.active_team_count += 1;
|
| 260 |
+
if alliance == state.current_alliance {
|
| 261 |
+
state.same_alliance_count += 1;
|
| 262 |
+
} else {
|
| 263 |
+
state.enemy_team_count += 1;
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
// Always write worm_index (original writes ESI unconditionally)
|
| 268 |
+
*alive_ptr = worm_index;
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
usercall_trampoline!(fn trampoline_set_active_worm; impl_fn = set_active_worm_impl;
|
| 273 |
+
regs = [eax, edx, esi]);
|
| 274 |
+
|
| 275 |
// ============================================================
|
| 276 |
// Hook installation
|
| 277 |
// ============================================================
|
|
|
|
| 307 |
va::CHECK_WORM_STATE_0X64,
|
| 308 |
trampoline_check_worm_state_0x64 as *const (),
|
| 309 |
)?;
|
| 310 |
+
|
| 311 |
+
let _ = hook::install(
|
| 312 |
+
"CheckTeamWormState0x64",
|
| 313 |
+
va::CHECK_TEAM_WORM_STATE_0X64,
|
| 314 |
+
trampoline_check_team_worm_state_0x64 as *const (),
|
| 315 |
+
)?;
|
| 316 |
+
|
| 317 |
+
let _ = hook::install(
|
| 318 |
+
"CheckAnyWormState0x8b",
|
| 319 |
+
va::CHECK_ANY_WORM_STATE_0X8B,
|
| 320 |
+
trampoline_check_any_worm_state_0x8b as *const (),
|
| 321 |
+
)?;
|
| 322 |
+
|
| 323 |
+
let _ = hook::install(
|
| 324 |
+
"SetActiveWorm_Maybe",
|
| 325 |
+
va::SET_ACTIVE_WORM_MAYBE,
|
| 326 |
+
trampoline_set_active_worm as *const (),
|
| 327 |
+
)?;
|
| 328 |
}
|
| 329 |
|
| 330 |
Ok(())
|
crates/openwa-wormkit/src/replacements/weapon.rs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
| 3 |
//! Replaces WA.exe functions that manage weapon ammo in the TeamWeaponState area (DDGame + 0x4628):
|
| 4 |
//! - GetAmmo (0x5225E0): query ammo count with delay/phase checks
|
| 5 |
//! - AddAmmo (0x522640): add ammo to a weapon slot
|
|
|
|
|
|
|
| 6 |
|
| 7 |
use openwa_types::address::va;
|
| 8 |
use openwa_types::ddgame::{self, offsets, FullTeamBlock, TeamWeaponState};
|
|
@@ -32,6 +34,24 @@ unsafe extern "cdecl" fn add_ammo_impl(team_index: u32, amount: i32, base: u32,
|
|
| 32 |
usercall_trampoline!(fn trampoline_add_ammo; impl_fn = add_ammo_impl;
|
| 33 |
regs = [eax, edx]; stack_params = 2; ret_bytes = "0x8");
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
// ============================================================
|
| 36 |
// GetAmmo replacement (0x5225E0)
|
| 37 |
// ============================================================
|
|
@@ -56,7 +76,8 @@ unsafe extern "cdecl" fn get_ammo_impl(team_index: u32, base: u32, weapon_id: u3
|
|
| 56 |
|
| 57 |
// SelectWorm (0x3B) requires >1 alive worm on the team
|
| 58 |
if state.game_phase >= ddgame::GAME_PHASE_NORMAL_MIN && weapon_id == Weapon::SelectWorm as u32 {
|
| 59 |
-
|
|
|
|
| 60 |
return 0;
|
| 61 |
}
|
| 62 |
}
|
|
@@ -64,12 +85,19 @@ unsafe extern "cdecl" fn get_ammo_impl(team_index: u32, base: u32, weapon_id: u3
|
|
| 64 |
state.get_ammo(idx) as u32
|
| 65 |
}
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
let worm_count = sentinel.sentinel_worm_count();
|
| 74 |
let mut alive = 0i32;
|
| 75 |
for w in 1..=worm_count as usize {
|
|
@@ -77,11 +105,11 @@ unsafe fn count_alive_worms(state: &TeamWeaponState, team_index: usize) -> bool
|
|
| 77 |
alive += 1;
|
| 78 |
}
|
| 79 |
}
|
| 80 |
-
alive > 1
|
| 81 |
}
|
| 82 |
|
| 83 |
-
usercall_trampoline!(fn
|
| 84 |
-
regs = [eax,
|
| 85 |
|
| 86 |
// ============================================================
|
| 87 |
// Hook installation
|
|
@@ -100,6 +128,18 @@ pub fn install() -> Result<(), String> {
|
|
| 100 |
va::GET_AMMO,
|
| 101 |
trampoline_get_ammo as *const (),
|
| 102 |
)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
Ok(())
|
|
|
|
| 3 |
//! Replaces WA.exe functions that manage weapon ammo in the TeamWeaponState area (DDGame + 0x4628):
|
| 4 |
//! - GetAmmo (0x5225E0): query ammo count with delay/phase checks
|
| 5 |
//! - AddAmmo (0x522640): add ammo to a weapon slot
|
| 6 |
+
//! - SubtractAmmo (0x522680): decrement ammo count
|
| 7 |
+
//! - CountAliveWorms (0x5225A0): check if >1 worm alive on team
|
| 8 |
|
| 9 |
use openwa_types::address::va;
|
| 10 |
use openwa_types::ddgame::{self, offsets, FullTeamBlock, TeamWeaponState};
|
|
|
|
| 34 |
usercall_trampoline!(fn trampoline_add_ammo; impl_fn = add_ammo_impl;
|
| 35 |
regs = [eax, edx]; stack_params = 2; ret_bytes = "0x8");
|
| 36 |
|
| 37 |
+
// ============================================================
|
| 38 |
+
// SubtractAmmo replacement (0x522680)
|
| 39 |
+
// ============================================================
|
| 40 |
+
// __usercall: EAX = team_index, ECX = team_info_base, [ESP+4] = weapon_id
|
| 41 |
+
// RET 0x4
|
| 42 |
+
|
| 43 |
+
unsafe extern "cdecl" fn subtract_ammo_impl(team_index: u32, base: u32, weapon_id: u32) {
|
| 44 |
+
let state = &mut *(base as *mut TeamWeaponState);
|
| 45 |
+
let idx = state.ammo_index(team_index as usize, weapon_id);
|
| 46 |
+
let ammo = state.get_ammo(idx);
|
| 47 |
+
if ammo > 0 {
|
| 48 |
+
*state.ammo_mut(idx) = ammo - 1;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
usercall_trampoline!(fn trampoline_subtract_ammo; impl_fn = subtract_ammo_impl;
|
| 53 |
+
regs = [eax, ecx]; stack_params = 1; ret_bytes = "0x4");
|
| 54 |
+
|
| 55 |
// ============================================================
|
| 56 |
// GetAmmo replacement (0x5225E0)
|
| 57 |
// ============================================================
|
|
|
|
| 76 |
|
| 77 |
// SelectWorm (0x3B) requires >1 alive worm on the team
|
| 78 |
if state.game_phase >= ddgame::GAME_PHASE_NORMAL_MIN && weapon_id == Weapon::SelectWorm as u32 {
|
| 79 |
+
let base = state as *const TeamWeaponState as u32;
|
| 80 |
+
if count_alive_worms_impl(team_index, base) == 0 {
|
| 81 |
return 0;
|
| 82 |
}
|
| 83 |
}
|
|
|
|
| 85 |
state.get_ammo(idx) as u32
|
| 86 |
}
|
| 87 |
|
| 88 |
+
usercall_trampoline!(fn trampoline_get_ammo; impl_fn = get_ammo_impl;
|
| 89 |
+
regs = [eax, esi, edx]);
|
| 90 |
+
|
| 91 |
+
// ============================================================
|
| 92 |
+
// CountAliveWorms replacement (0x5225A0)
|
| 93 |
+
// ============================================================
|
| 94 |
+
// __usercall: EAX = team_index, ECX = base
|
| 95 |
+
// plain RET, returns EAX = bool (1 if >1 worm alive on team)
|
| 96 |
+
|
| 97 |
+
unsafe extern "cdecl" fn count_alive_worms_impl(team_index: u32, base: u32) -> u32 {
|
| 98 |
+
let blocks = (base as *const u8).sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 99 |
+
let block = &*blocks.add(team_index as usize);
|
| 100 |
+
let sentinel = &(*blocks.add(team_index as usize + 1)).worms[0];
|
| 101 |
let worm_count = sentinel.sentinel_worm_count();
|
| 102 |
let mut alive = 0i32;
|
| 103 |
for w in 1..=worm_count as usize {
|
|
|
|
| 105 |
alive += 1;
|
| 106 |
}
|
| 107 |
}
|
| 108 |
+
if alive > 1 { 1 } else { 0 }
|
| 109 |
}
|
| 110 |
|
| 111 |
+
usercall_trampoline!(fn trampoline_count_alive_worms; impl_fn = count_alive_worms_impl;
|
| 112 |
+
regs = [eax, ecx]);
|
| 113 |
|
| 114 |
// ============================================================
|
| 115 |
// Hook installation
|
|
|
|
| 128 |
va::GET_AMMO,
|
| 129 |
trampoline_get_ammo as *const (),
|
| 130 |
)?;
|
| 131 |
+
|
| 132 |
+
let _ = hook::install(
|
| 133 |
+
"SubtractAmmo",
|
| 134 |
+
va::SUBTRACT_AMMO,
|
| 135 |
+
trampoline_subtract_ammo as *const (),
|
| 136 |
+
)?;
|
| 137 |
+
|
| 138 |
+
let _ = hook::install(
|
| 139 |
+
"CountAliveWorms",
|
| 140 |
+
va::COUNT_ALIVE_WORMS,
|
| 141 |
+
trampoline_count_alive_worms as *const (),
|
| 142 |
+
)?;
|
| 143 |
}
|
| 144 |
|
| 145 |
Ok(())
|