paavohtl's picture
Claude Opus 4.6 (1M context)
refactor: rename TeamArenaState to TeamArena, use raw pointers for soundness
e80ee6b
Raw History Blame Contribute Delete
23.6 kB
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::<CTaskWorm>() == 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(())
}
}