refactor: use FullTeamBlock structs in team/weapon hooks, add F9 dump hotkey
Browse filesRefactor team.rs and weapon.rs replacements to use WormEntry and
FullTeamBlock struct field access instead of raw pointer arithmetic.
Rename HasFullHealthWorm → CheckWormState0x64 (reads state, not health).
Add named worm state constants (IDLE=0x65, ACTIVE=0x67, DEAD=0x87).
Add F9 hotkey to validator for on-demand team block dumps.
Cross-validate struct access against old raw pointer patterns (8/8 pass).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crates/openwa-types/src/address.rs
CHANGED
|
@@ -144,9 +144,10 @@ pub mod va {
|
|
| 144 |
/// Reads worm X,Y position into output pointers.
|
| 145 |
/// usercall(EAX=team_index, ECX=worm_index, [ESP+4]=base, [ESP+8]=&x, [ESP+C]=&y), RET 0xC.
|
| 146 |
pub const GET_WORM_POSITION: u32 = 0x0052_2700;
|
| 147 |
-
/// Checks if any
|
|
|
|
| 148 |
/// usercall(EAX=base) → EAX=bool, plain RET.
|
| 149 |
-
pub const
|
| 150 |
|
| 151 |
// === Graphics / rendering ===
|
| 152 |
|
|
|
|
| 144 |
/// Reads worm X,Y position into output pointers.
|
| 145 |
/// usercall(EAX=team_index, ECX=worm_index, [ESP+4]=base, [ESP+8]=&x, [ESP+C]=&y), RET 0xC.
|
| 146 |
pub const GET_WORM_POSITION: u32 = 0x0052_2700;
|
| 147 |
+
/// Checks if any worm has state 0x64 (100). 11 xrefs in gameplay code.
|
| 148 |
+
/// Despite the comparison to 100, reads worms[].state NOT .health.
|
| 149 |
/// usercall(EAX=base) → EAX=bool, plain RET.
|
| 150 |
+
pub const CHECK_WORM_STATE_0X64: u32 = 0x0052_28D0;
|
| 151 |
|
| 152 |
// === Graphics / rendering ===
|
| 153 |
|
crates/openwa-types/src/ddgame.rs
CHANGED
|
@@ -119,6 +119,19 @@ pub mod offsets {
|
|
| 119 |
/// GetAmmo/AddAmmo/SubtractAmmo.
|
| 120 |
pub const TEAM_WEAPON_STATE: usize = 0x4628;
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
// === FUN_00526120 init offsets (stride 0x194, 10 entries) ===
|
| 123 |
pub const INIT_TABLE_BASE: usize = 0x379C;
|
| 124 |
pub const INIT_TABLE_STRIDE: usize = 0x194;
|
|
@@ -156,6 +169,106 @@ pub struct TeamEntry {
|
|
| 156 |
|
| 157 |
const _: () = assert!(core::mem::size_of::<TeamEntry>() == 0x51C);
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
/// Team weapon state area within DDGame (at DDGame + 0x4628).
|
| 160 |
///
|
| 161 |
/// Contains per-team entries and an interleaved ammo/delay table.
|
|
@@ -211,27 +324,23 @@ pub struct TeamWeaponState {
|
|
| 211 |
|
| 212 |
const _: () = assert!(core::mem::size_of::<TeamWeaponState>() == 0x2C40);
|
| 213 |
|
| 214 |
-
/// Worm
|
| 215 |
-
/// Worm data lives at NEGATIVE offsets from the TeamEntry base pointer.
|
| 216 |
pub mod worm {
|
| 217 |
-
//
|
| 218 |
-
|
| 219 |
-
///
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
pub const STATE_OFFSET: usize = 0x598;
|
| 229 |
-
/// Full health value (used in HasFullHealthWorm)
|
| 230 |
-
pub const FULL_HEALTH: i32 = 100;
|
| 231 |
|
| 232 |
/// Special worm states — worm is dying/drowning/in special animation.
|
| 233 |
/// Checked by IsWormInSpecialState (0x5226B0).
|
| 234 |
-
///
|
| 235 |
pub const SPECIAL_STATES: [u32; 6] = [0x80, 0x81, 0x82, 0x83, 0x85, 0x89];
|
| 236 |
|
| 237 |
/// Check if a worm state value is a "special" state.
|
|
@@ -245,19 +354,15 @@ pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
|
|
| 245 |
pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
|
| 246 |
|
| 247 |
/// Team data offsets within TeamWeaponState (relative to base pointer).
|
|
|
|
|
|
|
| 248 |
pub mod team_data {
|
| 249 |
-
/// Offset to first team's per-team data block
|
|
|
|
|
|
|
| 250 |
pub const BASE_OFFSET: usize = 0x510;
|
| 251 |
/// Alive flag within per-team data block (at +4 from team data start)
|
| 252 |
pub const ALIVE_FLAG: usize = 4;
|
| 253 |
-
/// Eliminated flag — at team_entry - 0x10
|
| 254 |
-
pub const ELIMINATED_OFFSET: usize = 0x10;
|
| 255 |
-
/// Alternative team pointer base used in HasFullHealthWorm
|
| 256 |
-
pub const ALT_BASE_OFFSET: usize = 0x518;
|
| 257 |
-
/// Worm data offset from alt base (negative: team_ptr - 0x4F8)
|
| 258 |
-
pub const ALT_WORM_ARRAY_OFFSET: usize = 0x4F8;
|
| 259 |
-
/// Eliminated flag from alt base (negative: team_ptr - 0xC)
|
| 260 |
-
pub const ALT_ELIMINATED_OFFSET: usize = 0x0C;
|
| 261 |
}
|
| 262 |
|
| 263 |
impl TeamWeaponState {
|
|
|
|
| 119 |
/// GetAmmo/AddAmmo/SubtractAmmo.
|
| 120 |
pub const TEAM_WEAPON_STATE: usize = 0x4628;
|
| 121 |
|
| 122 |
+
// === Team block array (7 × FullTeamBlock, stride 0x51C) ===
|
| 123 |
+
/// Start of team block array within DDGame (7 blocks, stride 0x51C).
|
| 124 |
+
/// Derived: entry_ptr(team=0) - 0x598 = 0x4628 - 0x598 = 0x4090.
|
| 125 |
+
/// Runtime-confirmed: block[0] is zeroed preamble, blocks[1-6] hold team data.
|
| 126 |
+
pub const TEAM_BLOCKS: usize = 0x4090;
|
| 127 |
+
|
| 128 |
+
/// Byte offset from TeamWeaponState base back to FullTeamBlock array start.
|
| 129 |
+
/// `blocks_ptr = (tws_base as *const u8).sub(TWS_TO_BLOCKS) as *const FullTeamBlock`
|
| 130 |
+
///
|
| 131 |
+
/// entry_ptr(0) = DDGame+0x4628 = TEAM_BLOCKS + 0x598.
|
| 132 |
+
/// 0x598 = sizeof(FullTeamBlock) + 0x7C = one block + offset into sentinel worm[0].
|
| 133 |
+
pub const TWS_TO_BLOCKS: usize = 0x598;
|
| 134 |
+
|
| 135 |
// === FUN_00526120 init offsets (stride 0x194, 10 entries) ===
|
| 136 |
pub const INIT_TABLE_BASE: usize = 0x379C;
|
| 137 |
pub const INIT_TABLE_STRIDE: usize = 0x194;
|
|
|
|
| 169 |
|
| 170 |
const _: () = assert!(core::mem::size_of::<TeamEntry>() == 0x51C);
|
| 171 |
|
| 172 |
+
// ============================================================
|
| 173 |
+
// Per-worm and per-team block structs
|
| 174 |
+
// ============================================================
|
| 175 |
+
|
| 176 |
+
/// Per-worm data entry (0x9C bytes, stride between consecutive worms).
|
| 177 |
+
///
|
| 178 |
+
/// 8 slots per team. Slot 0 is a sentinel/header (team metadata), slots 1-7
|
| 179 |
+
/// hold playable worm data. GetTeamTotalHealth iterates starting from slot 1.
|
| 180 |
+
///
|
| 181 |
+
/// Field offsets confirmed by runtime memory dump (validator DLL):
|
| 182 |
+
/// - state at 0x00: 0x67 = active/selected, 0x65 = idle, 0x80+ = special
|
| 183 |
+
/// - active_flag at 0x0C: 1 for alive worms
|
| 184 |
+
/// - max_health at 0x58: initial health value (100)
|
| 185 |
+
/// - health at 0x5C: current health (100 = full)
|
| 186 |
+
/// - name at 0x78: null-terminated worm name string (~20 bytes)
|
| 187 |
+
///
|
| 188 |
+
/// Sentinel (slot 0) has different layout — see `sentinel_*` methods:
|
| 189 |
+
/// - +0x6C (_unknown_60\[0x0C\]): eliminated flag
|
| 190 |
+
/// - +0x78 (name\[0..4\]): worm_count
|
| 191 |
+
/// - +0x80 (name\[8..12\]): alliance_id
|
| 192 |
+
/// - +0x84 (name\[12..\]): team name string
|
| 193 |
+
/// - +0x98 (_unknown_90\[8..12\]): value 0x190 (400), unknown purpose
|
| 194 |
+
#[repr(C)]
|
| 195 |
+
pub struct WormEntry {
|
| 196 |
+
/// 0x00: Worm state machine state.
|
| 197 |
+
/// Values: 0x65=idle, 0x67=active/selected, 0x68=active variant.
|
| 198 |
+
/// Special states {0x80..0x85, 0x89} = dying/drowning/special animation.
|
| 199 |
+
pub state: u32,
|
| 200 |
+
/// 0x04-0x0B: Unknown
|
| 201 |
+
pub _unknown_04: [u8; 8],
|
| 202 |
+
/// 0x0C: Active/alive flag (1 for alive worms in game, 0 otherwise).
|
| 203 |
+
pub active_flag: i32,
|
| 204 |
+
/// 0x10-0x57: Unknown
|
| 205 |
+
pub _unknown_10: [u8; 0x48],
|
| 206 |
+
/// 0x58: Max health (initial health value, typically 100).
|
| 207 |
+
pub max_health: i32,
|
| 208 |
+
/// 0x5C: Current health. Used by GetTeamTotalHealth.
|
| 209 |
+
pub health: i32,
|
| 210 |
+
/// 0x60-0x77: Unknown.
|
| 211 |
+
/// In sentinel: +0x6C = eliminated flag for the team.
|
| 212 |
+
pub _unknown_60: [u8; 0x18],
|
| 213 |
+
/// 0x78: Worm name, null-terminated ASCII string (~20 bytes).
|
| 214 |
+
/// In sentinel: +0x78 = worm_count (i32), +0x80 = alliance_id (i32),
|
| 215 |
+
/// +0x84 = team name string.
|
| 216 |
+
pub name: [u8; 0x18],
|
| 217 |
+
/// 0x90-0x9B: Unknown (zeroed in runtime dump for playable worms).
|
| 218 |
+
/// GetWormPosition reads pos_x/pos_y from +0x90/+0x94 via negative entry_ptr
|
| 219 |
+
/// arithmetic, but values appear transient — not populated at rest.
|
| 220 |
+
/// Actual worm positions live in CGameTask objects (+0x84/+0x88).
|
| 221 |
+
pub _unknown_90: [u8; 0x0C],
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
const _: () = assert!(core::mem::size_of::<WormEntry>() == 0x9C);
|
| 225 |
+
|
| 226 |
+
impl WormEntry {
|
| 227 |
+
/// Read worm_count from this entry when it's a sentinel (slot 0).
|
| 228 |
+
/// Stored at self.name[0..4] (= WormEntry offset 0x78) as little-endian i32.
|
| 229 |
+
///
|
| 230 |
+
/// # Safety
|
| 231 |
+
/// Only valid when called on a sentinel worm (slot 0 of a FullTeamBlock).
|
| 232 |
+
pub unsafe fn sentinel_worm_count(&self) -> i32 {
|
| 233 |
+
*(self.name.as_ptr() as *const i32)
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
/// Read eliminated flag from this entry when it's a sentinel (slot 0).
|
| 237 |
+
/// Stored at self._unknown_60[0x0C] (= WormEntry offset 0x6C) as i32.
|
| 238 |
+
/// Nonzero = team is eliminated.
|
| 239 |
+
///
|
| 240 |
+
/// # Safety
|
| 241 |
+
/// Only valid when called on a sentinel worm (slot 0 of a FullTeamBlock).
|
| 242 |
+
pub unsafe fn sentinel_eliminated(&self) -> i32 {
|
| 243 |
+
*(self._unknown_60.as_ptr().add(0x0C) as *const i32)
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/// Full per-team data block (0x51C bytes, 6 teams in DDGame).
|
| 248 |
+
///
|
| 249 |
+
/// Contains 8 WormEntry slots (0x4E0 bytes) followed by 0x3C bytes of
|
| 250 |
+
/// team metadata. Slot 0 is a sentinel/header; slots 1-7 are playable worms.
|
| 251 |
+
///
|
| 252 |
+
/// **Block indexing**: Block 0 is unused (preamble, all zeros). Actual team
|
| 253 |
+
/// data starts at block 1. entry_ptr(team=N) = DDGame+0x4628+N*0x51C, which
|
| 254 |
+
/// lands at block\[N+1\].worm\[0\]+0x7C. Negative offsets reach back into
|
| 255 |
+
/// block\[N\]'s worm data. So entry_ptr(1) accesses block\[1\]'s worms.
|
| 256 |
+
///
|
| 257 |
+
/// **Sentinel worm\[0\]** stores metadata for the team accessed by the
|
| 258 |
+
/// PREVIOUS entry_ptr index:
|
| 259 |
+
/// - +0x78: worm_count (accessed as entry_ptr-4)
|
| 260 |
+
/// - +0x80: alliance_id (accessed as entry_ptr+4)
|
| 261 |
+
/// - +0x84: team name string
|
| 262 |
+
#[repr(C)]
|
| 263 |
+
pub struct FullTeamBlock {
|
| 264 |
+
/// 0x000-0x4DF: 8 worm entries (stride 0x9C)
|
| 265 |
+
pub worms: [WormEntry; 8],
|
| 266 |
+
/// 0x4E0-0x51B: Team metadata (0x3C bytes)
|
| 267 |
+
pub _metadata: [u8; 0x3C],
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
const _: () = assert!(core::mem::size_of::<FullTeamBlock>() == 0x51C);
|
| 271 |
+
|
| 272 |
/// Team weapon state area within DDGame (at DDGame + 0x4628).
|
| 273 |
///
|
| 274 |
/// Contains per-team entries and an interleaved ammo/delay table.
|
|
|
|
| 324 |
|
| 325 |
const _: () = assert!(core::mem::size_of::<TeamWeaponState>() == 0x2C40);
|
| 326 |
|
| 327 |
+
/// Worm state constants and helpers.
|
|
|
|
| 328 |
pub mod worm {
|
| 329 |
+
// --- Known state values (runtime-validated via validator dumps) ---
|
| 330 |
+
|
| 331 |
+
/// Transitional state checked by CheckWormState0x64 (0x5228D0). 11 xrefs.
|
| 332 |
+
/// Appears briefly during turn transitions.
|
| 333 |
+
pub const STATE_TRANSITIONAL: u32 = 0x64;
|
| 334 |
+
/// Idle — worm is waiting, not currently active.
|
| 335 |
+
pub const STATE_IDLE: u32 = 0x65;
|
| 336 |
+
/// Active — worm is currently selected and taking its turn.
|
| 337 |
+
pub const STATE_ACTIVE: u32 = 0x67;
|
| 338 |
+
/// Dead — worm has been killed (hp=0). Persists across turns.
|
| 339 |
+
pub const STATE_DEAD: u32 = 0x87;
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
/// Special worm states — worm is dying/drowning/in special animation.
|
| 342 |
/// Checked by IsWormInSpecialState (0x5226B0).
|
| 343 |
+
/// All values in the 0x80+ range. 0x87 (dead) is also in this set.
|
| 344 |
pub const SPECIAL_STATES: [u32; 6] = [0x80, 0x81, 0x82, 0x83, 0x85, 0x89];
|
| 345 |
|
| 346 |
/// Check if a worm state value is a "special" state.
|
|
|
|
| 354 |
pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
|
| 355 |
|
| 356 |
/// Team data offsets within TeamWeaponState (relative to base pointer).
|
| 357 |
+
/// Used by CountTeamsByAlliance which accesses a different sentinel layout
|
| 358 |
+
/// than the entry_ptr-based functions (offset +0x70/+0x74 vs +0x78/+0x80).
|
| 359 |
pub mod team_data {
|
| 360 |
+
/// Offset to first team's per-team data block (from TWS base).
|
| 361 |
+
/// Maps to block[2].worms[0]+0x70 — a separate alliance/alive pair
|
| 362 |
+
/// distinct from the entry_ptr-based alliance_id at worms[0]+0x80.
|
| 363 |
pub const BASE_OFFSET: usize = 0x510;
|
| 364 |
/// Alive flag within per-team data block (at +4 from team data start)
|
| 365 |
pub const ALIVE_FLAG: usize = 4;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
}
|
| 367 |
|
| 368 |
impl TeamWeaponState {
|
crates/openwa-validator/Cargo.toml
CHANGED
|
@@ -12,4 +12,8 @@ openwa-types = { path = "../openwa-types" }
|
|
| 12 |
minhook = "0.9"
|
| 13 |
|
| 14 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 15 |
-
windows-sys = { version = "0.59", features = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
minhook = "0.9"
|
| 13 |
|
| 14 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 15 |
+
windows-sys = { version = "0.59", features = [
|
| 16 |
+
"Win32_System_Memory",
|
| 17 |
+
"Win32_System_LibraryLoader",
|
| 18 |
+
"Win32_UI_Input_KeyboardAndMouse",
|
| 19 |
+
] }
|
crates/openwa-validator/src/lib.rs
CHANGED
|
@@ -10,6 +10,9 @@ use openwa_types::task::{CTask, CGameTask};
|
|
| 10 |
use openwa_types::ddgame::DDGame;
|
| 11 |
use openwa_types::ddgame_wrapper::DDGameWrapper;
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
// ---------------------------------------------------------------------------
|
| 14 |
// ASLR rebasing
|
| 15 |
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +20,7 @@ use openwa_types::ddgame_wrapper::DDGameWrapper;
|
|
| 17 |
/// Delta to add to Ghidra addresses to get runtime addresses.
|
| 18 |
/// Computed as: actual_base - IMAGE_BASE (0x400000)
|
| 19 |
static REBASE_DELTA: AtomicU32 = AtomicU32::new(0);
|
|
|
|
| 20 |
|
| 21 |
/// Rebase a Ghidra VA to the actual runtime address.
|
| 22 |
#[inline]
|
|
@@ -24,10 +28,6 @@ pub(crate) fn rb(ghidra_addr: u32) -> u32 {
|
|
| 24 |
ghidra_addr.wrapping_add(REBASE_DELTA.load(Ordering::Relaxed))
|
| 25 |
}
|
| 26 |
|
| 27 |
-
extern "system" {
|
| 28 |
-
fn GetModuleHandleA(lpModuleName: *const u8) -> *mut c_void;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
fn init_rebase() -> i32 {
|
| 32 |
let base = unsafe { GetModuleHandleA(std::ptr::null()) } as u32;
|
| 33 |
let delta = base.wrapping_sub(va::IMAGE_BASE);
|
|
@@ -472,6 +472,151 @@ fn deferred_global_validation() {
|
|
| 472 |
let _ = log_line(&format!(" Deferred {}", result.summary_line()));
|
| 473 |
}
|
| 474 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
// ---------------------------------------------------------------------------
|
| 476 |
// Main validation entry point
|
| 477 |
// ---------------------------------------------------------------------------
|
|
@@ -526,5 +671,26 @@ fn run_validation() -> Result<(), Box<dyn std::error::Error>> {
|
|
| 526 |
});
|
| 527 |
let _ = log_line(" Polling thread started (10s delay).");
|
| 528 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
Ok(())
|
| 530 |
}
|
|
|
|
| 10 |
use openwa_types::ddgame::DDGame;
|
| 11 |
use openwa_types::ddgame_wrapper::DDGameWrapper;
|
| 12 |
|
| 13 |
+
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
| 14 |
+
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
|
| 15 |
+
|
| 16 |
// ---------------------------------------------------------------------------
|
| 17 |
// ASLR rebasing
|
| 18 |
// ---------------------------------------------------------------------------
|
|
|
|
| 20 |
/// Delta to add to Ghidra addresses to get runtime addresses.
|
| 21 |
/// Computed as: actual_base - IMAGE_BASE (0x400000)
|
| 22 |
static REBASE_DELTA: AtomicU32 = AtomicU32::new(0);
|
| 23 |
+
static DUMP_COUNTER: AtomicU32 = AtomicU32::new(0);
|
| 24 |
|
| 25 |
/// Rebase a Ghidra VA to the actual runtime address.
|
| 26 |
#[inline]
|
|
|
|
| 28 |
ghidra_addr.wrapping_add(REBASE_DELTA.load(Ordering::Relaxed))
|
| 29 |
}
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
fn init_rebase() -> i32 {
|
| 32 |
let base = unsafe { GetModuleHandleA(std::ptr::null()) } as u32;
|
| 33 |
let delta = base.wrapping_sub(va::IMAGE_BASE);
|
|
|
|
| 472 |
let _ = log_line(&format!(" Deferred {}", result.summary_line()));
|
| 473 |
}
|
| 474 |
|
| 475 |
+
// ---------------------------------------------------------------------------
|
| 476 |
+
// Team block memory dump (for WormEntry/FullTeamBlock verification)
|
| 477 |
+
// ---------------------------------------------------------------------------
|
| 478 |
+
|
| 479 |
+
fn dump_team_blocks() {
|
| 480 |
+
use openwa_types::ddgame::{offsets, FullTeamBlock, TeamWeaponState};
|
| 481 |
+
|
| 482 |
+
let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
|
| 483 |
+
let _ = log_line("");
|
| 484 |
+
let _ = log_line(&format!("--- Team Block Dump #{} ---", dump_num));
|
| 485 |
+
|
| 486 |
+
unsafe {
|
| 487 |
+
let session_ptr = read_u32(rb(va::G_GAME_SESSION));
|
| 488 |
+
if session_ptr == 0 {
|
| 489 |
+
let _ = log_line(" No game session — skipping.");
|
| 490 |
+
return;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
let wrapper_addr = read_u32(session_ptr + 0xA0);
|
| 494 |
+
if wrapper_addr == 0 {
|
| 495 |
+
let _ = log_line(" No DDGameWrapper — need to start a game first.");
|
| 496 |
+
return;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
let ddgame_ptr = read_u32(wrapper_addr + 0x488);
|
| 500 |
+
if ddgame_ptr == 0 {
|
| 501 |
+
let _ = log_line(" No DDGame — need to be in gameplay.");
|
| 502 |
+
return;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
let _ = log_line(&format!(" DDGame = 0x{:08X}", ddgame_ptr));
|
| 506 |
+
|
| 507 |
+
// Get TeamWeaponState via struct
|
| 508 |
+
let tws_base = (ddgame_ptr + offsets::TEAM_WEAPON_STATE as u32) as *const u8;
|
| 509 |
+
let tws = &*(tws_base as *const TeamWeaponState);
|
| 510 |
+
let _ = log_line(&format!(" team_count = {} (TeamWeaponState.team_count)", tws.team_count));
|
| 511 |
+
|
| 512 |
+
// Get FullTeamBlock array via TWS_TO_BLOCKS offset
|
| 513 |
+
let blocks = tws_base.sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 514 |
+
let blocks_addr = blocks as u32;
|
| 515 |
+
let _ = log_line(&format!(" blocks_base = 0x{:08X} (DDGame+0x{:X})",
|
| 516 |
+
blocks_addr, blocks_addr - ddgame_ptr));
|
| 517 |
+
|
| 518 |
+
let mut result = ValidationResult::new();
|
| 519 |
+
|
| 520 |
+
// Validate TWS_TO_BLOCKS: blocks_base should == DDGame + TEAM_BLOCKS
|
| 521 |
+
let expected_blocks = ddgame_ptr + offsets::TEAM_BLOCKS as u32;
|
| 522 |
+
result.check("TWS_TO_BLOCKS derivation",
|
| 523 |
+
blocks_addr == expected_blocks,
|
| 524 |
+
&format!("got 0x{:08X}, expected 0x{:08X}", blocks_addr, expected_blocks));
|
| 525 |
+
|
| 526 |
+
// Validate each real team (1-indexed: blocks[1..=team_count])
|
| 527 |
+
let num_blocks = (tws.team_count as u32 + 1).max(3).min(7);
|
| 528 |
+
for b in 0..num_blocks {
|
| 529 |
+
let block = &*blocks.add(b as usize);
|
| 530 |
+
let _ = log_line(&format!("\n === Block {} (0x{:08X}) ===",
|
| 531 |
+
b, blocks_addr + b * 0x51C));
|
| 532 |
+
|
| 533 |
+
// Sentinel: block[b+1].worms[0] holds metadata for entry_ptr(b)
|
| 534 |
+
if (b + 1) < 7 {
|
| 535 |
+
let sentinel = &(*blocks.add(b as usize + 1)).worms[0];
|
| 536 |
+
let worm_count = sentinel.sentinel_worm_count();
|
| 537 |
+
let eliminated = sentinel.sentinel_eliminated();
|
| 538 |
+
|
| 539 |
+
// Cross-check sentinel vs raw entry_ptr reads
|
| 540 |
+
let entry_ptr = tws_base.add(b as usize * 0x51C);
|
| 541 |
+
let raw_worm_count = *(entry_ptr.sub(4) as *const i32);
|
| 542 |
+
let raw_alliance = *(entry_ptr.add(4) as *const i32);
|
| 543 |
+
|
| 544 |
+
result.check(
|
| 545 |
+
&format!("block[{}] sentinel_worm_count vs raw", b),
|
| 546 |
+
worm_count == raw_worm_count,
|
| 547 |
+
&format!("struct={}, raw={}", worm_count, raw_worm_count),
|
| 548 |
+
);
|
| 549 |
+
|
| 550 |
+
let _ = log_line(&format!(" sentinel: worm_count={}, eliminated={}, alliance(entry_ptr+4)={}",
|
| 551 |
+
worm_count, eliminated, raw_alliance));
|
| 552 |
+
|
| 553 |
+
// Dump playable worms using struct field access
|
| 554 |
+
for w in 0..8usize {
|
| 555 |
+
let worm = &block.worms[w];
|
| 556 |
+
let active = worm.active_flag;
|
| 557 |
+
let name_bytes = &worm.name;
|
| 558 |
+
let name_len = name_bytes.iter().position(|&c| c == 0).unwrap_or(name_bytes.len());
|
| 559 |
+
let name_str = core::str::from_utf8(&name_bytes[..name_len]).unwrap_or("?");
|
| 560 |
+
|
| 561 |
+
if worm.state != 0 || worm.health != 0 || active != 0 || w == 0 {
|
| 562 |
+
let _ = log_line(&format!(
|
| 563 |
+
" worm[{}]: state=0x{:04X} active={} max_hp={} hp={} name=\"{}\"",
|
| 564 |
+
w, worm.state, active, worm.max_health, worm.health, name_str
|
| 565 |
+
));
|
| 566 |
+
}
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
// Validate GetTeamTotalHealth pattern: sum block.worms[1..=count].health
|
| 570 |
+
if worm_count > 0 && worm_count <= 7 {
|
| 571 |
+
let struct_total: i32 = (1..=worm_count as usize)
|
| 572 |
+
.map(|w| block.worms[w].health)
|
| 573 |
+
.sum();
|
| 574 |
+
|
| 575 |
+
// Cross-check vs raw pointer method (old ARRAY_OFFSET=0x4A0 pattern)
|
| 576 |
+
let raw_health_ptr = entry_ptr.sub(0x4A0) as *const i32;
|
| 577 |
+
let mut raw_total = 0i32;
|
| 578 |
+
for i in 0..worm_count {
|
| 579 |
+
raw_total += *raw_health_ptr.add(i as usize * (0x9C / 4));
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
result.check(
|
| 583 |
+
&format!("block[{}] health sum (struct vs raw)", b),
|
| 584 |
+
struct_total == raw_total,
|
| 585 |
+
&format!("struct={}, raw={}", struct_total, raw_total),
|
| 586 |
+
);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
// Validate IsWormInSpecialState pattern: block.worms[w].state
|
| 590 |
+
// Cross-check worm[1].state via struct vs raw (STATE_OFFSET=0x598)
|
| 591 |
+
if worm_count > 0 {
|
| 592 |
+
let struct_state = block.worms[1].state;
|
| 593 |
+
let raw_state = *(entry_ptr.sub(0x598).add(0x9C) as *const u32);
|
| 594 |
+
result.check(
|
| 595 |
+
&format!("block[{}] worm[1].state (struct vs raw)", b),
|
| 596 |
+
struct_state == raw_state,
|
| 597 |
+
&format!("struct=0x{:X}, raw=0x{:X}", struct_state, raw_state),
|
| 598 |
+
);
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
// Validate CheckWormState0x64 pattern: reads worms[].state, NOT .health
|
| 602 |
+
// Show both values for each worm so we can verify
|
| 603 |
+
if worm_count > 0 {
|
| 604 |
+
let _ = log_line(&format!(" CheckWormState0x64 field check (state vs health):"));
|
| 605 |
+
for w in 1..=worm_count as usize {
|
| 606 |
+
let worm = &block.worms[w];
|
| 607 |
+
let _ = log_line(&format!(
|
| 608 |
+
" worm[{}]: state=0x{:04X}({}) health=0x{:04X}({})",
|
| 609 |
+
w, worm.state, worm.state, worm.health, worm.health
|
| 610 |
+
));
|
| 611 |
+
}
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
let _ = log_line(&format!("\n Struct Validation {}", result.summary_line()));
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
// ---------------------------------------------------------------------------
|
| 621 |
// Main validation entry point
|
| 622 |
// ---------------------------------------------------------------------------
|
|
|
|
| 671 |
});
|
| 672 |
let _ = log_line(" Polling thread started (10s delay).");
|
| 673 |
|
| 674 |
+
// 8. Team block dump — wait 30s for user to start a game
|
| 675 |
+
std::thread::spawn(move || {
|
| 676 |
+
std::thread::sleep(std::time::Duration::from_secs(30));
|
| 677 |
+
dump_team_blocks();
|
| 678 |
+
});
|
| 679 |
+
let _ = log_line(" Team block dump thread started (30s delay).");
|
| 680 |
+
|
| 681 |
+
// 9. Hotkey listener — F9 triggers a team block dump on demand
|
| 682 |
+
std::thread::spawn(|| {
|
| 683 |
+
const VK_F9: i32 = 0x78;
|
| 684 |
+
let _ = log_line(" Hotkey listener started (press F9 to dump team blocks).");
|
| 685 |
+
loop {
|
| 686 |
+
std::thread::sleep(std::time::Duration::from_millis(100));
|
| 687 |
+
let state = unsafe { GetAsyncKeyState(VK_F9) };
|
| 688 |
+
// Bit 0 = key was pressed since last call
|
| 689 |
+
if state & 1 != 0 {
|
| 690 |
+
dump_team_blocks();
|
| 691 |
+
}
|
| 692 |
+
}
|
| 693 |
+
});
|
| 694 |
+
|
| 695 |
Ok(())
|
| 696 |
}
|
crates/openwa-wormkit/src/replacements/team.rs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
| 5 |
//! - GetTeamTotalHealth (0x5224D0): sum worm health for a team
|
| 6 |
//! - IsWormInSpecialState (0x5226B0): check worm state flag
|
| 7 |
//! - GetWormPosition (0x522700): read worm X,Y coordinates
|
| 8 |
-
//! -
|
| 9 |
|
| 10 |
use openwa_types::address::va;
|
| 11 |
-
use openwa_types::ddgame::{self, TeamWeaponState};
|
| 12 |
|
| 13 |
use crate::hook::{self, usercall_trampoline};
|
| 14 |
|
|
@@ -17,6 +17,11 @@ use crate::hook::{self, usercall_trampoline};
|
|
| 17 |
// ============================================================
|
| 18 |
// __usercall: EAX = base, EDI = alliance_id
|
| 19 |
// plain RET
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
unsafe extern "cdecl" fn count_teams_by_alliance_impl(base: u32, alliance_id: i32) {
|
| 22 |
let state = &mut *(base as *mut TeamWeaponState);
|
|
@@ -25,9 +30,6 @@ unsafe extern "cdecl" fn count_teams_by_alliance_impl(base: u32, alliance_id: i3
|
|
| 25 |
state.same_alliance_count = 0;
|
| 26 |
state.enemy_team_count = 0;
|
| 27 |
|
| 28 |
-
// Teams are 1-indexed; iterate 1..=team_count
|
| 29 |
-
// Per-team data: base + 0x510 + (team_1based - 1) * 0x51C
|
| 30 |
-
// At [+0]: alliance field, [+4]: alive flag
|
| 31 |
let base_ptr = base as *const u8;
|
| 32 |
for i in 0..state.team_count {
|
| 33 |
let team_data = base_ptr.add(ddgame::team_data::BASE_OFFSET + i as usize * 0x51C);
|
|
@@ -54,22 +56,18 @@ usercall_trampoline!(fn trampoline_count_teams_by_alliance; impl_fn = count_team
|
|
| 54 |
// plain RET, returns EAX = total health
|
| 55 |
|
| 56 |
unsafe extern "cdecl" fn get_team_total_health_impl(team_index: u32, base: u32) -> i32 {
|
| 57 |
-
let
|
| 58 |
-
let
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
if *((team_entry - ddgame::team_data::ELIMINATED_OFFSET) as *const i32) != 0 {
|
| 62 |
return 0;
|
| 63 |
}
|
| 64 |
|
| 65 |
-
let worm_count =
|
| 66 |
let mut total = 0i32;
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
for _ in 0..worm_count {
|
| 70 |
-
total += *(ptr as *const i32);
|
| 71 |
-
ptr = ptr.add(ddgame::worm::STRIDE);
|
| 72 |
-
}
|
| 73 |
}
|
| 74 |
total
|
| 75 |
}
|
|
@@ -86,11 +84,9 @@ usercall_trampoline!(fn trampoline_get_team_total_health; impl_fn = get_team_tot
|
|
| 86 |
unsafe extern "cdecl" fn is_worm_in_special_state_impl(
|
| 87 |
team_index: u32, worm_index: u32, base: u32,
|
| 88 |
) -> u32 {
|
| 89 |
-
let
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
let state_val = *((addr.wrapping_sub(ddgame::worm::STATE_OFFSET)) as *const u32);
|
| 93 |
-
if ddgame::worm::is_special_state(state_val) { 1 } else { 0 }
|
| 94 |
}
|
| 95 |
|
| 96 |
usercall_trampoline!(fn trampoline_is_worm_in_special_state; impl_fn = is_worm_in_special_state_impl;
|
|
@@ -102,50 +98,56 @@ usercall_trampoline!(fn trampoline_is_worm_in_special_state; impl_fn = is_worm_i
|
|
| 102 |
// __usercall: EAX = team_index, ECX = worm_index,
|
| 103 |
// [ESP+4] = base, [ESP+8] = out_x, [ESP+C] = out_y
|
| 104 |
// RET 0xC
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
unsafe extern "cdecl" fn get_worm_position_impl(
|
| 107 |
team_index: u32, worm_index: u32, base: u32, out_x: *mut i32, out_y: *mut i32,
|
| 108 |
) {
|
| 109 |
-
let
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
*out_x = *(
|
| 113 |
-
*out_y = *((
|
| 114 |
}
|
| 115 |
|
| 116 |
usercall_trampoline!(fn trampoline_get_worm_position; impl_fn = get_worm_position_impl;
|
| 117 |
regs = [eax, ecx]; stack_params = 3; ret_bytes = "0xC");
|
| 118 |
|
| 119 |
// ============================================================
|
| 120 |
-
//
|
| 121 |
// ============================================================
|
| 122 |
// __usercall: EAX = base
|
| 123 |
-
// plain RET, returns EAX = bool (1 if any
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
let state = &*(base as *const TeamWeaponState);
|
| 127 |
-
let
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
| 132 |
continue;
|
| 133 |
}
|
| 134 |
-
let worm_count =
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
return 1;
|
| 140 |
-
}
|
| 141 |
-
worm_ptr = worm_ptr.add(ddgame::worm::STRIDE);
|
| 142 |
}
|
| 143 |
}
|
| 144 |
}
|
| 145 |
0
|
| 146 |
}
|
| 147 |
|
| 148 |
-
usercall_trampoline!(fn
|
| 149 |
reg = eax);
|
| 150 |
|
| 151 |
// ============================================================
|
|
@@ -179,9 +181,9 @@ pub fn install() -> Result<(), String> {
|
|
| 179 |
)?;
|
| 180 |
|
| 181 |
let _ = hook::install(
|
| 182 |
-
"
|
| 183 |
-
va::
|
| 184 |
-
|
| 185 |
)?;
|
| 186 |
}
|
| 187 |
|
|
|
|
| 5 |
//! - GetTeamTotalHealth (0x5224D0): sum worm health for a team
|
| 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};
|
| 12 |
|
| 13 |
use crate::hook::{self, usercall_trampoline};
|
| 14 |
|
|
|
|
| 17 |
// ============================================================
|
| 18 |
// __usercall: EAX = base, EDI = alliance_id
|
| 19 |
// plain RET
|
| 20 |
+
//
|
| 21 |
+
// NOTE: This function uses a DIFFERENT sentinel layout than entry_ptr-based
|
| 22 |
+
// functions. It reads alliance/alive at TWS+0x510+i*0x51C (= sentinel+0x70/0x74),
|
| 23 |
+
// not the entry_ptr-based sentinel+0x78/0x80. Left as raw pointer math until
|
| 24 |
+
// the +0x70/+0x74 sentinel fields are better understood.
|
| 25 |
|
| 26 |
unsafe extern "cdecl" fn count_teams_by_alliance_impl(base: u32, alliance_id: i32) {
|
| 27 |
let state = &mut *(base as *mut TeamWeaponState);
|
|
|
|
| 30 |
state.same_alliance_count = 0;
|
| 31 |
state.enemy_team_count = 0;
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
let base_ptr = base as *const u8;
|
| 34 |
for i in 0..state.team_count {
|
| 35 |
let team_data = base_ptr.add(ddgame::team_data::BASE_OFFSET + i as usize * 0x51C);
|
|
|
|
| 56 |
// plain RET, returns EAX = total health
|
| 57 |
|
| 58 |
unsafe extern "cdecl" fn get_team_total_health_impl(team_index: u32, base: u32) -> i32 {
|
| 59 |
+
let blocks = (base as *const u8).sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 60 |
+
let block = &*blocks.add(team_index as usize);
|
| 61 |
+
let sentinel = &(*blocks.add(team_index as usize + 1)).worms[0];
|
| 62 |
|
| 63 |
+
if sentinel.sentinel_eliminated() != 0 {
|
|
|
|
| 64 |
return 0;
|
| 65 |
}
|
| 66 |
|
| 67 |
+
let worm_count = sentinel.sentinel_worm_count();
|
| 68 |
let mut total = 0i32;
|
| 69 |
+
for w in 1..=worm_count as usize {
|
| 70 |
+
total += block.worms[w].health;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
total
|
| 73 |
}
|
|
|
|
| 84 |
unsafe extern "cdecl" fn is_worm_in_special_state_impl(
|
| 85 |
team_index: u32, worm_index: u32, base: u32,
|
| 86 |
) -> u32 {
|
| 87 |
+
let blocks = (base as *const u8).sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 88 |
+
let block = &*blocks.add(team_index as usize);
|
| 89 |
+
if ddgame::worm::is_special_state(block.worms[worm_index as usize].state) { 1 } else { 0 }
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
usercall_trampoline!(fn trampoline_is_worm_in_special_state; impl_fn = is_worm_in_special_state_impl;
|
|
|
|
| 98 |
// __usercall: EAX = team_index, ECX = worm_index,
|
| 99 |
// [ESP+4] = base, [ESP+8] = out_x, [ESP+C] = out_y
|
| 100 |
// RET 0xC
|
| 101 |
+
//
|
| 102 |
+
// Reads pos_x/pos_y from WormEntry._unknown_90[0..4] / [4..8].
|
| 103 |
+
// These values appear transient — actual worm positions live in CGameTask objects.
|
| 104 |
|
| 105 |
unsafe extern "cdecl" fn get_worm_position_impl(
|
| 106 |
team_index: u32, worm_index: u32, base: u32, out_x: *mut i32, out_y: *mut i32,
|
| 107 |
) {
|
| 108 |
+
let blocks = (base as *const u8).sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 109 |
+
let block = &*blocks.add(team_index as usize);
|
| 110 |
+
let worm = &block.worms[worm_index as usize];
|
| 111 |
+
*out_x = *(worm._unknown_90.as_ptr() as *const i32);
|
| 112 |
+
*out_y = *(worm._unknown_90.as_ptr().add(4) as *const i32);
|
| 113 |
}
|
| 114 |
|
| 115 |
usercall_trampoline!(fn trampoline_get_worm_position; impl_fn = get_worm_position_impl;
|
| 116 |
regs = [eax, ecx]; stack_params = 3; ret_bytes = "0xC");
|
| 117 |
|
| 118 |
// ============================================================
|
| 119 |
+
// CheckWormState0x64 replacement (0x5228D0)
|
| 120 |
// ============================================================
|
| 121 |
// __usercall: EAX = base
|
| 122 |
+
// plain RET, returns EAX = bool (1 if any worm has state 0x64)
|
| 123 |
+
//
|
| 124 |
+
// Previously named "HasFullHealthWorm" but Ghidra disassembly confirms it
|
| 125 |
+
// reads worms[].state (offset +0x00), NOT worms[].health (offset +0x5C).
|
| 126 |
+
// It compares to 0x64 (100 decimal). State 0x64 is likely a transitional
|
| 127 |
+
// state distinct from 0x65 (idle). 11 xrefs in gameplay code.
|
| 128 |
+
|
| 129 |
+
unsafe extern "cdecl" fn check_worm_state_0x64_impl(base: u32) -> u32 {
|
| 130 |
let state = &*(base as *const TeamWeaponState);
|
| 131 |
+
let blocks = (base as *const u8).sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 132 |
|
| 133 |
+
// Iterate real teams (1-indexed: team 1..=team_count)
|
| 134 |
+
for i in 1..=state.team_count as usize {
|
| 135 |
+
let sentinel = &(*blocks.add(i + 1)).worms[0];
|
| 136 |
+
if sentinel.sentinel_eliminated() != 0 {
|
| 137 |
continue;
|
| 138 |
}
|
| 139 |
+
let worm_count = sentinel.sentinel_worm_count();
|
| 140 |
+
let worm_block = &*blocks.add(i);
|
| 141 |
+
for w in 1..=worm_count as usize {
|
| 142 |
+
if worm_block.worms[w].state == 0x64 {
|
| 143 |
+
return 1;
|
|
|
|
|
|
|
|
|
|
| 144 |
}
|
| 145 |
}
|
| 146 |
}
|
| 147 |
0
|
| 148 |
}
|
| 149 |
|
| 150 |
+
usercall_trampoline!(fn trampoline_check_worm_state_0x64; impl_fn = check_worm_state_0x64_impl;
|
| 151 |
reg = eax);
|
| 152 |
|
| 153 |
// ============================================================
|
|
|
|
| 181 |
)?;
|
| 182 |
|
| 183 |
let _ = hook::install(
|
| 184 |
+
"CheckWormState0x64",
|
| 185 |
+
va::CHECK_WORM_STATE_0X64,
|
| 186 |
+
trampoline_check_worm_state_0x64 as *const (),
|
| 187 |
)?;
|
| 188 |
}
|
| 189 |
|
crates/openwa-wormkit/src/replacements/weapon.rs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
//! - AddAmmo (0x522640): add ammo to a weapon slot
|
| 6 |
|
| 7 |
use openwa_types::address::va;
|
| 8 |
-
use openwa_types::ddgame::{self, TeamWeaponState};
|
| 9 |
use openwa_types::weapon::Weapon;
|
| 10 |
|
| 11 |
use crate::hook::{self, usercall_trampoline};
|
|
@@ -64,23 +64,17 @@ 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 |
-
/// Returns true if more than 1 worm is alive on the team.
|
| 68 |
-
///
|
| 69 |
-
/// Worm data lives at negative offsets from the TeamEntry:
|
| 70 |
-
/// - worm_count at team_entry_addr - 0x4
|
| 71 |
-
/// - worm array at team_entry_addr - 0x4A0 (stride 0x9C, health at [0])
|
| 72 |
unsafe fn count_alive_worms(state: &TeamWeaponState, team_index: usize) -> bool {
|
| 73 |
let base = state as *const TeamWeaponState as *const u8;
|
| 74 |
-
let
|
| 75 |
-
let
|
|
|
|
|
|
|
| 76 |
let mut alive = 0i32;
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if *(ptr as *const i32) > 0 {
|
| 81 |
-
alive += 1;
|
| 82 |
-
}
|
| 83 |
-
ptr = ptr.add(ddgame::worm::STRIDE);
|
| 84 |
}
|
| 85 |
}
|
| 86 |
alive > 1
|
|
|
|
| 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};
|
| 9 |
use openwa_types::weapon::Weapon;
|
| 10 |
|
| 11 |
use crate::hook::{self, usercall_trampoline};
|
|
|
|
| 64 |
state.get_ammo(idx) as u32
|
| 65 |
}
|
| 66 |
|
| 67 |
+
/// Returns true if more than 1 worm is alive on the team (health > 0).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
unsafe fn count_alive_worms(state: &TeamWeaponState, team_index: usize) -> bool {
|
| 69 |
let base = state as *const TeamWeaponState as *const u8;
|
| 70 |
+
let blocks = base.sub(offsets::TWS_TO_BLOCKS) as *const FullTeamBlock;
|
| 71 |
+
let block = &*blocks.add(team_index);
|
| 72 |
+
let sentinel = &(*blocks.add(team_index + 1)).worms[0];
|
| 73 |
+
let worm_count = sentinel.sentinel_worm_count();
|
| 74 |
let mut alive = 0i32;
|
| 75 |
+
for w in 1..=worm_count as usize {
|
| 76 |
+
if block.worms[w].health > 0 {
|
| 77 |
+
alive += 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
}
|
| 80 |
alive > 1
|