use super::base::CTask; use super::game_task::CGameTask; use crate::fixed::Fixed; use crate::game::weapon::WeaponEntry; use crate::game::Weapon; use crate::FieldRegistry; /// Worm state machine states. /// /// CTaskWorm's `SetState` (vtable slot 14) transitions between these. /// The state byte lives at CTaskWorm+0x44 (inside `base.subclass_data`). /// Also stored in WormEntry.state in the TeamArena. /// /// States 0x68..=0x8A are the "weapon/action active" range — checked by /// `(state - 0x68) < 0x23` in HandleMessage. States 0x80+ are dying/dead. /// Names are best guesses from behavioral observation and disassembly. /// /// Source: CTaskWorm::HandleMessage (0x510B40) decompilation + weapon fire dispatch. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u8)] #[allow(non_camel_case_types)] pub enum WormState { /// Transitional state checked by CheckWormState0x64 (0x5228D0). /// Appears briefly during turn transitions. Transitional = 0x64, /// Idle — not this worm's turn. Also used by MailMineMole to re-enter idle. Idle = 0x65, /// Idle variant — grouped with 0x65/0x67 in HandleMessage switch cases. IdleVariant_Maybe = 0x66, /// Active turn — this worm is currently being controlled. Active = 0x67, /// Active variant — observed in WormEntry. Start of weapon/action range. ActiveVariant_Maybe = 0x68, /// Unknown (within weapon/action range). Unknown_0x69 = 0x69, /// Unknown (within weapon/action range). Unknown_0x6A = 0x6A, /// Unknown (within weapon/action range). Unknown_0x6B = 0x6B, /// Fire Punch — performing fire punch attack (sub34=1). FirePunch = 0x6C, /// Kamikaze — performing kamikaze attack (sub34=4). Frequently checked in HandleMessage. Kamikaze = 0x6D, /// Pneumatic Drill — using the pneumatic drill (sub34=8). PneumaticDrill = 0x6E, /// Air Strike pending — set when air strike fires with `_unknown_208 == 0`. AirStrikePending_Maybe = 0x6F, /// Unknown — sub34=6 has no replay test log data. Unknown_0x70 = 0x70, /// Blowtorch — using the blowtorch weapon (sub34=11). Blowtorch = 0x71, /// Unknown — sub34=18 has no replay test log data. Unknown_0x72 = 0x72, /// Weapon charging — entered from aiming states (0x7B, 0x7C) before release. /// Also set by CheckPendingAction when field +0xBC is nonzero. WeaponCharging_Maybe = 0x73, /// Teleport cancelled — teleport failed or was denied. TeleportCancelled_Maybe = 0x74, /// Suicide Bomber — performing suicide bomber attack (sub34=5). SuicideBomber = 0x75, /// Unknown (within weapon/action range). Unknown_0x76 = 0x76, /// Weapon selected — entered via SelectCursor (msg 0x24) from idle/active states. /// HandleMessage sets param[0xa7]=-1 when already in this state. WeaponSelected_Maybe = 0x77, /// Weapon aimed — post-select state. Teleport check accepts this. /// Also used for Magic Bullet weapon fire. WeaponAimed_Maybe = 0x78, /// Unknown. Checked in WeaponRelease spawn offset (type 2, Y adjustment when state == 0x79). Unknown_0x79 = 0x79, /// Unknown. Unknown_0x7A = 0x7A, /// Aiming with angle — entered for aimed weapons. Sets angle params. /// Teleport check accepts this. Transitions to 0x73 on fire. AimingAngle_Maybe = 0x7B, /// Rope swinging — IsNotOnRope checks `state != 0x7C`. /// Transitions to 0x73 on fire. RopeSwinging = 0x7C, /// Pre-fire variant — MailMineMole version check uses this. /// Transitions to 0x7E or 0x65 depending on FUN_004fb580. PreFire_Maybe = 0x7D, /// Post-fire / special movement — entered from 0x78 and 0x7D /// when FUN_004fb580 returns nonzero. PostFire_Maybe = 0x7E, /// Drowning — worm fell in water. Drowning = 0x7F, /// Hurt — worm took damage. Hurt = 0x80, /// Dead variant 1. Dead1 = 0x81, /// Dying variant 1 — checked alongside 0x83 in HandleMessage. Dying1_Maybe = 0x82, /// Dying variant 2 — checked alongside 0x82 in HandleMessage. Dying2_Maybe = 0x83, /// Unknown. Unknown_0x84 = 0x84, /// Unknown. Unknown_0x85 = 0x85, /// Dead — set by Surrender (msg 0x29). Frequently checked in HandleMessage. Dead = 0x86, /// Dead variant 3. Dead3 = 0x87, /// Unknown — grouped with idle states (0x65/0x66/0x67/0x8B) in HandleMessage. Unknown_0x88 = 0x88, /// Dying/special animation (from WormEntry state documentation). DyingAnimation_Maybe = 0x89, /// Unknown. End of weapon/action range. Unknown_0x8A = 0x8A, /// Unknown state checked in CTaskTeam handlers. /// Grouped with idle states in HandleMessage switch cases. Unknown_0x8B = 0x8B, } impl WormState {} /// Raw-pointer methods for CTaskWorm. /// /// `set_state_raw` and other vtable methods are now auto-generated by the /// `bind_CTaskWormVTable!` macro. Only non-vtable raw helpers remain here. impl CTaskWorm { /// Set the weapon fire completion flag at +0x3C without creating `&mut self`. pub unsafe fn set_fire_complete_raw(this: *mut CTaskWorm, value: i32) { let base = this as *mut u8; core::ptr::write(base.add(0x3C) as *mut i32, value); } /// Fire subtype 1 at CGameTask+0x34 (subclass_data[4..8]). /// Air strike temporarily swaps this with `_unknown_190` during position update. pub unsafe fn fire_subtype_1(this: *const CTaskWorm) -> i32 { i32::from_ne_bytes((&(*this).base.subclass_data)[4..8].try_into().unwrap()) } /// Set fire subtype 1 without creating `&mut self` (avoids LLVM noalias violations). pub unsafe fn set_fire_subtype_1_raw(this: *mut CTaskWorm, value: i32) { (&mut (*this).base.subclass_data)[4..8].copy_from_slice(&value.to_ne_bytes()); } /// Set the action field at CGameTask+0x48 (subclass_data[0x18..0x1C]). /// Cleared after air strike completion alongside `_unknown_208` / `_unknown_19x`. pub unsafe fn set_action_field_raw(this: *mut CTaskWorm, value: i32) { (&mut (*this).base.subclass_data)[0x18..0x1C].copy_from_slice(&value.to_ne_bytes()); } } crate::define_addresses! { class "CTaskWorm" { /// CTaskWorm vtable vtable CTASK_WORM_VTABLE = 0x0066_44C8; /// CTaskWorm constructor ctor CTASK_WORM_CONSTRUCTOR = 0x0050_BFB0; } } /// Virtual method table for CTaskWorm (vtable at 0x6644C8, 20 slots). /// /// CTaskWorm overrides 14 of the 20 inherited CTask/CGameTask slots; /// 6 slots pass through unchanged. Slot layout by vtable byte offset: /// /// ```text /// Slot Offset Name Base class /// ---- ------ ------------------- ---------- /// 0 0x00 WriteReplayState CTask (overridden) /// 1 0x04 Free CTask (overridden) /// 2 0x08 HandleMessage CTask (overridden) /// 3 0x0C GetEntityData CTask (overridden) /// 4 0x10 (stub, returns 0) CTask — inherited /// 5 0x14 ProcessChildren CTask — inherited /// 6 0x18 ProcessFrame CTask — inherited /// 7 0x1C OnContactEntity CGameTask (overridden) /// 8 0x20 OnWormPush CGameTask (overridden) /// 9 0x24 OnLandBounce CGameTask (overridden) /// 10 0x28 OnLandSlide CGameTask? (new) /// 11 0x2C OnSink CGameTask? (new) /// 12 0x30 (inherited) CGameTask /// 13 0x34 OnKilled CGameTask (overridden) /// 14 0x38 SetState CTaskWorm (new) /// 15 0x3C CheckPendingAction CTaskWorm (new) /// 16 0x40 IsNotOnRope CTaskWorm (new) /// 17 0x44 (inherited) CGameTask /// 18 0x48 GetTeamIndex CTaskWorm (new) /// 19 0x4C (inherited) CGameTask /// ``` #[openwa_core::vtable(size = 20)] pub struct CTaskWormVTable { /// WriteReplayState — serializes worm state to a replay stream #[slot(0)] pub write_replay_state: fn(this: *mut CTaskWorm, stream: *mut u8), /// Free — calls inner destructor, then `_free(this)` if flags & 1 #[slot(1)] pub free: fn(this: *mut CTaskWorm, flags: u8) -> *mut CTaskWorm, /// HandleMessage — processes all TaskMessages sent to this worm #[slot(2)] pub handle_message: fn(this: *mut CTaskWorm, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8), /// GetEntityData — returns worm data by query code #[slot(3)] pub get_entity_data: fn(this: *mut CTaskWorm, query: u32, param: u32, out: *mut u32) -> u32, // Slots 4-6: Inherited CTask stubs (auto-filled as usize) /// OnContactEntity — handles physical contact with another entity #[slot(7)] pub on_contact_entity: fn(this: *mut CTaskWorm, other: *mut CGameTask, flags: u32) -> u32, /// OnWormPush — post-contact worm-worm push impulse #[slot(8)] pub on_worm_push: fn(this: *mut CTaskWorm, other: *mut CGameTask, flags: u32) -> u32, /// OnLandBounce — worm lands on terrain; plays thud sound, bounce physics #[slot(9)] pub on_land_bounce: fn(this: *mut CTaskWorm), /// OnLandSlide — secondary landing callback; sliding/friction physics #[slot(10)] pub on_land_slide: fn(this: *mut CTaskWorm), /// OnSink — worm sinks in water/acid; transitions to drowning state #[slot(11)] pub on_sink: fn(this: *mut CTaskWorm, dx: i32, dy: i32) -> u32, // Slot 12: Inherited (auto-filled) /// OnKilled — worm death; plays death sound, transitions to dead state #[slot(13)] pub on_killed: fn(this: *mut CTaskWorm), /// SetState — worm state machine; handles all state transitions #[slot(14)] pub set_state: fn(this: *mut CTaskWorm, state: WormState), /// CheckPendingAction — if field +0xBC is set, calls SetState(0x73) #[slot(15)] pub check_pending_action: fn(this: *mut CTaskWorm), /// IsNotOnRope — returns true if worm state != 0x7C (rope-swinging) #[slot(16)] pub is_not_on_rope: fn(this: *const CTaskWorm) -> bool, // Slot 17: Inherited (auto-filled) /// GetTeamIndex — returns worm's team index (field +0xFC) #[slot(18)] pub get_team_index: fn(this: *const CTaskWorm) -> u32, // Slot 19: Inherited (auto-filled) } /// Worm entity task — the primary playable character in WA. /// /// Extends CGameTask (0xFC bytes) with worm identity, physics overrides, and /// per-worm state. Total size: 0x3FC bytes. /// /// Constructor: 0x50BFB0 (stdcall, 5 params): /// this, parent_task, team_index, worm_index, init_data_ptr /// /// Vtable at 0x6644C8. Class type byte: 0x12. /// /// # Important fields in the CGameTask base /// The worm state field lives at **offset +0x44** (inside `base.subclass_data`). /// Use [`CTaskWorm::state`] to read it without pointer arithmetic. /// /// Source: Ghidra decompilation of 0x50BFB0, vtable analysis of 0x6644C8, /// wkJellyWorm CTaskWorm.h #[derive(FieldRegistry)] #[repr(C)] pub struct CTaskWorm { /// 0x00–0xFB: CGameTask base (position, velocity, sound emitter, etc.) pub base: CGameTask<*const CTaskWormVTable>, /// 0xFC: Team index (0-based); 3rd constructor param pub team_index: u32, /// 0x100: Worm index within team (0-based); 4th constructor param pub worm_index: u32, /// 0x104: Unknown flag (checked in OnContactEntity) pub _unknown_104: u32, /// 0x108–0x10F: Unknown pub _unknown_108: [u8; 8], /// 0x110–0x137: Ten u32s copied from spawn init_data (5th constructor param) pub spawn_params: [u32; 10], /// 0x138–0x143: Unknown pub _unknown_138: [u8; 0x0C], /// 0x144: Poison source bitmask — tracks which alliances have poisoned this worm. /// PoisonWorm handler (msg 0x51): `poison_source_mask |= alliance_bit`. /// Prevents double-poisoning from the same source. pub poison_source_mask: u32, /// 0x148: Poison damage per turn. 0 = not poisoned. /// PoisonWorm handler: `poison_damage += msg_data[0]`. /// ApplyPoison handler (msg 0x3E): subtracts this from health each turn. pub poison_damage: i32, /// 0x14C–0x14F: Unknown pub _unknown_14c: [u8; 4], /// 0x150: Unknown (slot 9 in GetEntityData query 0x7D4 output) pub _unknown_150: u32, /// 0x154: Unknown (rope-related; cleared in some SetState transitions) pub _unknown_154: u32, /// 0x158: Worm pool slot index in DDGame (assigned from pool at construction) pub slot_id: u32, /// 0x15C–0x163: Unknown pub _unknown_15c: [u8; 0x164 - 0x15C], /// 0x164: Frames the worm has stayed stationary (no movement). Resets on movement. pub stationary_frames: u32, /// 0x168–0x16F: Unknown pub _unknown_168: [u8; 0x170 - 0x168], /// 0x170: Currently selected weapon ID. pub selected_weapon: Weapon, /// 0x174–0x177: Unknown pub _unknown_174: [u8; 4], /// 0x178: Display health (animated toward target). Used for health bar interpolation. /// Stored as `00 00 XX 00` where XX is health — actual layout is u16 at +0x17A. pub display_health_raw: u32, /// 0x17C: Target health (matches WormEntry.health). Same byte layout as display_health. pub target_health_raw: u32, /// 0x180–0x18F: Unknown pub _unknown_180: [u8; 0x190 - 0x180], /// 0x190: Secondary position/mode field. Air strike temporarily swaps this with +0x34. pub _unknown_190: i32, /// 0x194–0x197: Unknown pub _unknown_194: [u8; 4], /// 0x198: Cleared after air strike fire. pub _unknown_198: i32, /// 0x19C: Cleared after air strike fire. pub _unknown_19c: i32, /// 0x1A0–0x1A7: Unknown pub _unknown_1a0: [u8; 0x1A8 - 0x1A0], /// 0x1A8: Facing direction copy. -1 = left, +1 = right (same as +0x3DC). pub facing_direction_2: i32, /// 0x1AC: Inverted facing direction. +1 = left, -1 = right. pub facing_direction_inv: i32, /// 0x1B0–0x1EB: Unknown pub _unknown_1b0: [u8; 0x1EC - 0x1B0], /// 0x1EC: Movement streak counter. Increases ~once per second while moving /// in one direction. Resets to 0 when movement resumes after a stop. /// Set to -1 when the worm is blocked (e.g. hits a wall). pub movement_streak: i32, /// 0x1F0–0x207: Unknown pub _unknown_1f0: [u8; 0x208 - 0x1F0], /// 0x208: Action/mode flag. Checked in air strike (must be 0 to fire). /// Cleared after air strike completes. pub _unknown_208: i32, /// 0x20C–0x247: Unknown pub _unknown_20c: [u8; 0x248 - 0x20C], /// 0x248: Landscape scale factor for spawn offset calculation (Fixed 16.16). /// Used by WeaponRelease to convert aim params to world-space projectile offsets. pub landscape_scale: Fixed, /// 0x24C: Aim angle (fixed-point 16.16, range 0..0x10000 = 0..360 degrees). pub aim_angle: u32, /// 0x250–0x267: Unknown pub _unknown_250: [u8; 0x268 - 0x250], /// 0x268: Show aiming cursor flag (nonzero = cursor visible). pub show_cursor: u32, /// 0x26C–0x283: Unknown pub _unknown_26c: [u8; 0x284 - 0x26C], /// 0x284: Shot/aim data 1 (slot 5 in GetEntityData query 0x7D4). /// For weapons 0x22/0x24 (Teleport/Freeze), copied from shot_data_2 on first fire. /// Sent in HandleMessage 0x49 buffer during WeaponRelease. pub shot_data_1: u32, /// 0x288: Shot/aim data 2 (slot 6 in GetEntityData query 0x7D4). pub shot_data_2: u32, /// 0x28C–0x28F: Unknown pub _unknown_28c: [u8; 4], /// 0x290: Fire sync frame counter 1. Compared with fire_sync_frame_2 /// in WeaponRelease; when equal, weapon slot table is reset. pub fire_sync_frame_1: i32, /// 0x294: Fire sync frame counter 2. pub fire_sync_frame_2: i32, /// 0x298–0x2BB: Unknown pub _unknown_298: [u8; 0x2BC - 0x298], /// 0x2BC: Network client index. Compared against max client count /// (5 or 10 depending on FE version) to compute network delay. pub network_client_index: i32, /// 0x2C0: Difficulty level (0 = easier/30-frame delay, 1 = harder/60-frame delay). pub difficulty_level: i32, /// 0x2C4–0x2C7: Unknown pub _unknown_2c4: [u8; 4], /// 0x2C8: Network/sound condition flag. In type 2 (rope) sound dispatch, /// sound plays only when this is 0 OR when it equals 1. pub _unknown_2c8: i32, /// 0x2CC: Network flag. Nonzero = network mode active. /// Used in sound dispatch conditions and HandleMessage 0x49 buffer. pub _unknown_2cc: i32, /// 0x2D0–0x2DF: Unknown pub _unknown_2d0: [u8; 0x2E0 - 0x2D0], /// 0x2E0: Weapon parameter 1. Polymorphic per weapon: /// - WeaponRelease: ammo_per_turn (copied to release context) /// - Air Strike: fire position X /// - Freeze: freeze effect X position pub weapon_param_1: i32, /// 0x2E4: Weapon parameter 2. Polymorphic per weapon: /// - WeaponRelease: ammo_per_slot (copied to release context) /// - Air Strike: fire position Y /// - Freeze: freeze effect Y position pub weapon_param_2: i32, /// 0x2E8–0x2EB: Unknown pub _unknown_2e8: [u8; 4], /// 0x2EC: Weapon parameter 3 / launch count. /// - Weapons 0x22/0x24: checked == 0 to copy shot_data_2 → shot_data_1 /// - Freeze: freeze target entity ID pub weapon_param_3: i32, /// 0x2F0: Worm name, null-terminated (max 17 chars, from spawn init_data+3) #[field(kind = "CString")] pub worm_name: [u8; 0x11], /// 0x301: Country / team name from scheme, null-terminated (max 17 chars) #[field(kind = "CString")] pub country_name: [u8; 0x11], /// 0x312: Health display string, null-terminated ASCII (e.g. "100", "88"). /// Updated when health changes for the floating health number display. #[field(kind = "CString")] pub health_text: [u8; 0x09], /// 0x31B: Poison damage display string, null-terminated ASCII (e.g. "5", ""). /// Shown as the green poison damage number above the worm. #[field(kind = "CString")] pub poison_text: [u8; 0x09], /// 0x324–0x333: Unknown pub _unknown_324: [u8; 0x334 - 0x324], /// 0x334: Facing direction copy. -1 = left, +1 = right (same as +0x3DC). pub facing_direction_3: i32, /// 0x338–0x367: Unknown pub _unknown_338: [u8; 0x368 - 0x338], /// 0x368: Animator / controller object (dispatched via vtable for state animations) pub animator: *mut u8, /// 0x36C: Active weapon entry pointer. Points to `&WeaponTable.entries[selected_weapon]`. /// Contains fire type (+0x30), subtypes (+0x34/+0x38), and completion flag (+0x3C). /// Used by WeaponRelease: `MOV EAX, [EDI+0x36C]` before calling FireWeapon. pub active_weapon_entry: *mut WeaponEntry, /// 0x370–0x3AF: Unknown (rope anchor, weapon-specific data, etc.) pub _unknown_370: [u8; 0x3B0 - 0x370], /// 0x3B0: Streaming sound handle. Nonzero when a worm sound effect /// (e.g., weapon charge-up) is actively playing. PlayWormSound stores the /// new handle here; StopWormSound clears it. pub sound_handle: i32, /// 0x3B4: Secondary sound handle, used by FUN_00515020 (teleport/weapon sounds). /// Same stop/play semantics as `sound_handle` but a separate channel. pub sound_handle_2: i32, /// 0x3B8–0x3DB: Unknown pub _unknown_3b8: [u8; 0x3DC - 0x3B8], /// 0x3DC: Facing direction. -1 = facing left, +1 = facing right. pub facing_direction: i32, /// 0x3E0–0x3E3: Unknown pub _unknown_3e0: u32, /// 0x3E4: Input: aim up key held (nonzero = adjusting aim upward). pub input_aim_up: u32, /// 0x3E8: Input: aim down key held (nonzero = adjusting aim downward). pub input_aim_down: u32, /// 0x3EC: Input: move left key held (nonzero = worm walking left). pub input_move_left: u32, /// 0x3F0: Input: move right key held (nonzero = worm walking right). pub input_move_right: u32, /// 0x3F4–0x3FB: Unknown pub _unknown_3f4: [u8; 0x3FC - 0x3F4], } const _: () = assert!(core::mem::size_of::() == 0x3FC); // Generate typed vtable method wrappers: handle_message(), on_contact_entity(), etc. bind_CTaskWormVTable!(CTaskWorm, base.base.vtable); impl CTaskWorm { /// Returns the worm's current state code (lives at offset +0x44, inside /// `base.subclass_data`). See [`WormState`] for known values. pub fn state(&self) -> u32 { u32::from_ne_bytes(self.base.subclass_data[0x14..0x18].try_into().unwrap()) } pub fn is_in_state(&self, state: WormState) -> bool { self.state() == state as u32 } // Weapon fire dispatch state: // - Fire type/subtypes live in the WeaponEntry (via active_weapon_entry at +0x36C) // - Completion flag lives in CGameTask.subclass_data[12] (this object, at +0x3C) /// Weapon fire completion flag at CGameTask+0x3C (subclass_data[12]). /// Set to 0 before FireWeapon dispatch, 1 after. pub fn fire_complete(&self) -> i32 { i32::from_ne_bytes(self.base.subclass_data[12..16].try_into().unwrap()) } /// Set the weapon fire completion flag. pub fn set_fire_complete(&mut self, value: i32) { self.base.subclass_data[12..16].copy_from_slice(&value.to_ne_bytes()); } // vtable() method is now provided by bind_CTaskWormVTable! macro above. } // ── Snapshot impl ────────────────────────────────────────── #[cfg(target_arch = "x86")] impl crate::snapshot::Snapshot for CTaskWorm { unsafe fn write_snapshot( &self, w: &mut dyn core::fmt::Write, indent: usize, ) -> core::fmt::Result { use crate::snapshot::{write_indent, write_raw_region}; let i = indent; let b = &self.base; // CGameTask write_indent(w, i)?; writeln!(w, "pos = ({}, {})", b.pos_x, b.pos_y)?; write_indent(w, i)?; writeln!(w, "speed = ({}, {})", b.speed_x, b.speed_y)?; write_indent(w, i)?; writeln!(w, "angle = {}", b.angle)?; write_indent(w, i)?; writeln!(w, "team_index = {}", self.team_index)?; write_indent(w, i)?; writeln!(w, "worm_index = {}", self.worm_index)?; write_indent(w, i)?; writeln!(w, "slot_id = {}", self.slot_id)?; write_indent(w, i)?; writeln!(w, "selected_weapon = {:?}", self.selected_weapon)?; write_indent(w, i)?; writeln!(w, "facing = {}", self.facing_direction)?; write_indent(w, i)?; writeln!(w, "aim_angle = 0x{:08X}", self.aim_angle)?; write_indent(w, i)?; write!(w, "spawn_params =")?; for v in &self.spawn_params { write!(w, " {:08X}", v)?; } writeln!(w)?; // Raw dump of unknown regions write_indent(w, i)?; writeln!(w, "_unknown_1f0 ({} bytes):", self._unknown_1f0.len())?; write_raw_region( w, self._unknown_1f0.as_ptr(), self._unknown_1f0.len(), i + 1, )?; write_indent(w, i)?; writeln!(w, "_unknown_28c ({} bytes):", self._unknown_28c.len())?; write_raw_region( w, self._unknown_28c.as_ptr(), self._unknown_28c.len(), i + 1, )?; write_indent(w, i)?; writeln!(w, "_unknown_370 ({} bytes):", self._unknown_370.len())?; write_raw_region( w, self._unknown_370.as_ptr(), self._unknown_370.len(), i + 1, )?; Ok(()) } }