feat: add SharedData entity census and CTaskFilter RE
Browse files- Add SharedDataNode/SharedDataTable/SharedDataIter types to task.rs for
iterating the 256-bucket entity hash table at CTask.shared_data
- Add 8 new CTASK_*_VTABLE constants to address.rs (Worm, Land, TurnGame,
Team, Filter, Dirt, SpriteAnim, CPU)
- Add CTaskFilter struct (0xB4 bytes) with subscription_table[100] at +0x30;
documents the message-subscription filter pattern used per team
- Add dump_entity_census() to validation: iterates SharedData table, groups
by vtable, names all known task types; auto-runs at 5s mark and on F12
- Refactor dump_worm_tasks() to use SharedDataTable::iter()
RE findings:
- CTaskFilter overrides HandleMessage to drop messages not in its
subscription table; 4 instances per team with different message sets
- CTaskCPU is transient (only present during AI turn)
- Dead worms are removed from SharedData on death
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@@ -47,8 +47,22 @@ pub mod va {
|
|
| 47 |
pub const OPENGL_CPU_VTABLE: u32 = 0x0067_74C0;
|
| 48 |
/// WaterEffect vtable (0xBC-byte object)
|
| 49 |
pub const WATER_EFFECT_VTABLE: u32 = 0x0066_B268;
|
| 50 |
-
/// CTaskLand vtable -
|
| 51 |
pub const CTASK_LAND_VTABLE: u32 = 0x0066_4388;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
/// Sprite vtable (0x70-byte objects, 8 entries)
|
| 53 |
pub const SPRITE_VTABLE: u32 = 0x0066_418C;
|
| 54 |
|
|
|
|
| 47 |
pub const OPENGL_CPU_VTABLE: u32 = 0x0067_74C0;
|
| 48 |
/// WaterEffect vtable (0xBC-byte object)
|
| 49 |
pub const WATER_EFFECT_VTABLE: u32 = 0x0066_B268;
|
| 50 |
+
/// CTaskLand vtable - landscape/terrain task (DDGame+0x054C)
|
| 51 |
pub const CTASK_LAND_VTABLE: u32 = 0x0066_4388;
|
| 52 |
+
/// CTaskWorm vtable - worm entity task (constructor 0x50BFB0)
|
| 53 |
+
pub const CTASK_WORM_VTABLE: u32 = 0x0066_44C8;
|
| 54 |
+
/// CTaskTurnGame vtable - global turn flow manager (1 per game)
|
| 55 |
+
pub const CTASK_TURN_GAME_VTABLE: u32 = 0x0066_9F70;
|
| 56 |
+
/// CTaskTeam vtable - per-team task (1 per team, constructor 0x555BF0)
|
| 57 |
+
pub const CTASK_TEAM_VTABLE: u32 = 0x0066_9EE4;
|
| 58 |
+
/// CTaskFilter vtable - role unclear; 4 instances in a 2-team 3-worm game
|
| 59 |
+
pub const CTASK_FILTER_VTABLE: u32 = 0x0066_9DAC;
|
| 60 |
+
/// CTaskDirt vtable - dirt/particle system (1 per game, constructor 0x54EDF0)
|
| 61 |
+
pub const CTASK_DIRT_VTABLE: u32 = 0x0066_9D74;
|
| 62 |
+
/// CTaskSpriteAnim vtable - sprite animation manager (1 per game, constructor 0x5466F0)
|
| 63 |
+
pub const CTASK_SPRITE_ANIM_VTABLE: u32 = 0x0066_9D00;
|
| 64 |
+
/// CTaskCPU vtable - AI/CPU bot controller (1 per game, constructor 0x548620)
|
| 65 |
+
pub const CTASK_CPU_VTABLE: u32 = 0x0066_9D54;
|
| 66 |
/// Sprite vtable (0x70-byte objects, 8 entries)
|
| 67 |
pub const SPRITE_VTABLE: u32 = 0x0066_418C;
|
| 68 |
|
|
@@ -50,6 +50,134 @@ pub struct CTask {
|
|
| 50 |
|
| 51 |
const _: () = assert!(core::mem::size_of::<CTask>() == 0x30);
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
/// Game task - extends CTask with physics and gameplay data.
|
| 54 |
///
|
| 55 |
/// PARTIAL: Most fields between 0x30-0x83 and 0x98-0xE7 are unknown.
|
|
@@ -120,6 +248,207 @@ pub struct SoundEmitterVTable {
|
|
| 120 |
pub _base_methods: [*const (); 7],
|
| 121 |
}
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
// ============================================================
|
| 124 |
// Derived task overlays — for accessing task-specific fields
|
| 125 |
// beyond or within CGameTask that differ per task type.
|
|
|
|
| 50 |
|
| 51 |
const _: () = assert!(core::mem::size_of::<CTask>() == 0x30);
|
| 52 |
|
| 53 |
+
// ---------------------------------------------------------------------------
|
| 54 |
+
// Shared-data entity registry
|
| 55 |
+
// ---------------------------------------------------------------------------
|
| 56 |
+
|
| 57 |
+
/// A 0x30-byte node in CTask's shared-data entity hash table.
|
| 58 |
+
///
|
| 59 |
+
/// Inserted by `SharedData__Insert` (0x5406A0, called from task constructors).
|
| 60 |
+
/// All game task types (CTaskWorm, CTaskLand, projectiles, …) share the same
|
| 61 |
+
/// 256-bucket table at `CTask.shared_data`. Use the vtable pointer at
|
| 62 |
+
/// `entity[0]` to identify the object type.
|
| 63 |
+
///
|
| 64 |
+
/// Hash function (from Ghidra decompilation of `SharedData__Insert`):
|
| 65 |
+
/// ```text
|
| 66 |
+
/// bucket = (key_esi * 0x11 + key_edi) & 0x800000ff;
|
| 67 |
+
/// if (int)bucket < 0 { bucket = bucket.wrapping_sub(1) | 0xffffff00; bucket += 1; }
|
| 68 |
+
/// ```
|
| 69 |
+
/// In practice (small positive key values), this reduces to:
|
| 70 |
+
/// `bucket = (key_esi * 0x11 + key_edi) & 0xff`
|
| 71 |
+
///
|
| 72 |
+
/// Runtime observation: for `CTaskWorm`, `key_esi` encodes a compound worm
|
| 73 |
+
/// identity (e.g. `0x11` = team 1, worm 1) and `key_edi` is a small integer.
|
| 74 |
+
/// Companion remove function: `SharedData__Remove` (0x540700).
|
| 75 |
+
#[repr(C)]
|
| 76 |
+
pub struct SharedDataNode {
|
| 77 |
+
/// +0x00: Next node in this bucket's linked list (null = end).
|
| 78 |
+
pub next: *mut SharedDataNode,
|
| 79 |
+
/// +0x04: EDI register value at registration time.
|
| 80 |
+
pub key_edi: u32,
|
| 81 |
+
/// +0x08: ESI register value at registration time.
|
| 82 |
+
pub key_esi: u32,
|
| 83 |
+
/// +0x0C: Registered entity pointer (first DWORD = vtable).
|
| 84 |
+
pub entity: *mut u8,
|
| 85 |
+
/// +0x10..0x2F: Unused allocation padding.
|
| 86 |
+
pub _padding: [u8; 0x20],
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const _: () = assert!(core::mem::size_of::<SharedDataNode>() == 0x30);
|
| 90 |
+
|
| 91 |
+
/// View of the 256-bucket entity hash table at `CTask.shared_data`.
|
| 92 |
+
///
|
| 93 |
+
/// Root tasks own 0x420 bytes of shared data:
|
| 94 |
+
/// - `0x000..0x3FF`: 256 × `*mut SharedDataNode` bucket heads
|
| 95 |
+
/// - `0x400..0x41F`: Other root-task data (layout unknown)
|
| 96 |
+
///
|
| 97 |
+
/// All tasks in the same game tree inherit the same `shared_data` pointer, so
|
| 98 |
+
/// any task can be used to access the full table. Use [`SharedDataTable::iter`]
|
| 99 |
+
/// to walk all registered entities and filter by vtable address.
|
| 100 |
+
///
|
| 101 |
+
/// Registered by `SharedData__Insert` (0x5406A0); removed by
|
| 102 |
+
/// `SharedData__Remove` (0x540700).
|
| 103 |
+
pub struct SharedDataTable {
|
| 104 |
+
buckets: *const *mut SharedDataNode,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
impl SharedDataTable {
|
| 108 |
+
/// Construct from a raw `CTask.shared_data` pointer.
|
| 109 |
+
///
|
| 110 |
+
/// # Safety
|
| 111 |
+
/// `ptr` must point to a valid shared-data region of at least 256 × 4 = 1024 bytes.
|
| 112 |
+
pub unsafe fn from_ptr(ptr: *mut u8) -> Self {
|
| 113 |
+
Self { buckets: ptr as *const *mut SharedDataNode }
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/// Construct from a `CTask` pointer (reads `task.shared_data`).
|
| 117 |
+
///
|
| 118 |
+
/// # Safety
|
| 119 |
+
/// `task` must be a valid, aligned `CTask` pointer.
|
| 120 |
+
pub unsafe fn from_task(task: *const CTask) -> Self {
|
| 121 |
+
Self::from_ptr((*task).shared_data)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/// Compute the bucket index for a (key_esi, key_edi) pair.
|
| 125 |
+
///
|
| 126 |
+
/// Exact transcription of the hash in `FUN_005406a0`.
|
| 127 |
+
pub fn bucket_for(key_esi: u32, key_edi: u32) -> u32 {
|
| 128 |
+
let mut h = key_esi.wrapping_mul(0x11).wrapping_add(key_edi) & 0x800000ff;
|
| 129 |
+
if (h as i32) < 0 {
|
| 130 |
+
h = h.wrapping_sub(1) | 0xffffff00;
|
| 131 |
+
h = h.wrapping_add(1);
|
| 132 |
+
}
|
| 133 |
+
h
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/// Iterate all nodes across all 256 buckets.
|
| 137 |
+
///
|
| 138 |
+
/// # Safety
|
| 139 |
+
/// The table and all linked nodes must be valid and not concurrently modified.
|
| 140 |
+
pub unsafe fn iter(&self) -> SharedDataIter {
|
| 141 |
+
SharedDataIter {
|
| 142 |
+
buckets: self.buckets,
|
| 143 |
+
bucket: 0,
|
| 144 |
+
node: core::ptr::null_mut(),
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/// Iterator over all [`SharedDataNode`]s in a [`SharedDataTable`].
|
| 150 |
+
///
|
| 151 |
+
/// Created by [`SharedDataTable::iter`]. Walks all 256 buckets in order,
|
| 152 |
+
/// following `next` pointers within each bucket.
|
| 153 |
+
pub struct SharedDataIter {
|
| 154 |
+
buckets: *const *mut SharedDataNode,
|
| 155 |
+
bucket: usize,
|
| 156 |
+
node: *mut SharedDataNode,
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
impl Iterator for SharedDataIter {
|
| 160 |
+
type Item = *mut SharedDataNode;
|
| 161 |
+
|
| 162 |
+
fn next(&mut self) -> Option<Self::Item> {
|
| 163 |
+
// SAFETY: caller of SharedDataTable::iter() guarantees table validity.
|
| 164 |
+
unsafe {
|
| 165 |
+
loop {
|
| 166 |
+
if !self.node.is_null() {
|
| 167 |
+
let current = self.node;
|
| 168 |
+
self.node = (*self.node).next;
|
| 169 |
+
return Some(current);
|
| 170 |
+
}
|
| 171 |
+
if self.bucket >= 256 {
|
| 172 |
+
return None;
|
| 173 |
+
}
|
| 174 |
+
self.node = *self.buckets.add(self.bucket);
|
| 175 |
+
self.bucket += 1;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
/// Game task - extends CTask with physics and gameplay data.
|
| 182 |
///
|
| 183 |
/// PARTIAL: Most fields between 0x30-0x83 and 0x98-0xE7 are unknown.
|
|
|
|
| 248 |
pub _base_methods: [*const (); 7],
|
| 249 |
}
|
| 250 |
|
| 251 |
+
/// Virtual method table for CTaskWorm (vtable at 0x6644C8, 20 slots).
|
| 252 |
+
///
|
| 253 |
+
/// CTaskWorm overrides 14 of the 20 inherited CTask/CGameTask slots;
|
| 254 |
+
/// 6 slots pass through unchanged. Slot layout by vtable byte offset:
|
| 255 |
+
///
|
| 256 |
+
/// ```text
|
| 257 |
+
/// 0x00 WriteReplayState 0x04 Free 0x08 HandleMessage
|
| 258 |
+
/// 0x0C GetEntityData 0x10-0x18 inherited 0x1C OnContactEntity
|
| 259 |
+
/// 0x20 OnWormPush 0x24 OnLandBounce 0x28 OnLandSlide
|
| 260 |
+
/// 0x2C OnSink 0x30 inherited 0x34 OnKilled
|
| 261 |
+
/// 0x38 SetState 0x3C CheckPendingAction 0x40 IsNotOnRope
|
| 262 |
+
/// 0x44 inherited 0x48 GetTeamIndex 0x4C inherited
|
| 263 |
+
/// ```
|
| 264 |
+
#[repr(C)]
|
| 265 |
+
pub struct CTaskWormVTable {
|
| 266 |
+
/// [0] 0x0050CAA0: WriteReplayState — serializes worm state to a replay stream;
|
| 267 |
+
/// writes entity type byte 0x12, then packs field groups based on current state
|
| 268 |
+
pub write_replay_state: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut u8),
|
| 269 |
+
/// [1] 0x0050C7E0: Free — calls inner destructor, then `_free(this)` if flags & 1
|
| 270 |
+
pub free: unsafe extern "thiscall" fn(*mut CTaskWorm, u8) -> *mut CTaskWorm,
|
| 271 |
+
/// [2] 0x00510B40: HandleMessage — processes all TaskMessages sent to this worm
|
| 272 |
+
pub handle_message:
|
| 273 |
+
unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CTask, u32, u32, *const u8),
|
| 274 |
+
/// [3] 0x00516780: GetEntityData — returns worm data by query code:
|
| 275 |
+
/// 0x7D0=(pos_x, pos_y), 0x7D1/0x7D2=collision test, 0x7D4=full state dump
|
| 276 |
+
pub get_entity_data: unsafe extern "thiscall" fn(*mut CTaskWorm, u32, u32, *mut u32) -> u32,
|
| 277 |
+
/// [4]-[6]: Inherited CTask stubs (not overridden by CTaskWorm)
|
| 278 |
+
pub _inherited_4_to_6: [*const (); 3],
|
| 279 |
+
/// [7] 0x0050D5D0: OnContactEntity — handles physical contact with another entity;
|
| 280 |
+
/// worm-on-dying-worm → apply crush damage; projectile close-pass → SetState(0x80)
|
| 281 |
+
pub on_contact_entity:
|
| 282 |
+
unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
|
| 283 |
+
/// [8] 0x0050D9A0: OnWormPush — post-contact worm-worm push impulse;
|
| 284 |
+
/// deduplicates via a recent-collision table, then adjusts pos_x or pos_y
|
| 285 |
+
pub on_worm_push: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
|
| 286 |
+
/// [9] 0x0050D810: OnLandBounce — worm lands on terrain; plays thud sound, bounce physics
|
| 287 |
+
pub on_land_bounce: unsafe extern "thiscall" fn(*mut CTaskWorm),
|
| 288 |
+
/// [10] 0x0050D820: OnLandSlide — secondary landing callback; sliding/friction physics
|
| 289 |
+
pub on_land_slide: unsafe extern "thiscall" fn(*mut CTaskWorm),
|
| 290 |
+
/// [11] 0x0050D570: OnSink — worm sinks in water/acid; applies (dx, dy) displacement,
|
| 291 |
+
/// transitions to drowning state (0x7F) unless already flying/rope/dead
|
| 292 |
+
pub on_sink: unsafe extern "thiscall" fn(*mut CTaskWorm, i32, i32) -> u32,
|
| 293 |
+
/// [12]: Inherited (vtable30 from CGameTask — not overridden)
|
| 294 |
+
pub _inherited_12: *const (),
|
| 295 |
+
/// [13] 0x0050D3B0: OnKilled — worm death; plays death sound (0x3A),
|
| 296 |
+
/// transitions to dead state (0x81 or 0x89) based on game round count
|
| 297 |
+
pub on_killed: unsafe extern "thiscall" fn(*mut CTaskWorm),
|
| 298 |
+
/// [14] 0x0050E850: SetState — worm state machine; handles all state transitions
|
| 299 |
+
/// (0x65=idle, 0x67=active turn, 0x7F=drowning, 0x80=hurt, 0x81/0x86=dead, …)
|
| 300 |
+
pub set_state: unsafe extern "thiscall" fn(*mut CTaskWorm, u32),
|
| 301 |
+
/// [15] 0x00516900: CheckPendingAction — if field +0xBC is set, calls SetState(0x73)
|
| 302 |
+
pub check_pending_action: unsafe extern "thiscall" fn(*mut CTaskWorm),
|
| 303 |
+
/// [16] 0x00516920: IsNotOnRope — returns true if worm state != 0x7C (rope-swinging)
|
| 304 |
+
pub is_not_on_rope: unsafe extern "thiscall" fn(*const CTaskWorm) -> bool,
|
| 305 |
+
/// [17]: Inherited (vtable44 from CGameTask — not overridden)
|
| 306 |
+
pub _inherited_17: *const (),
|
| 307 |
+
/// [18] 0x005168F0: GetTeamIndex — returns worm's team index (field +0xFC)
|
| 308 |
+
pub get_team_index: unsafe extern "thiscall" fn(*const CTaskWorm) -> u32,
|
| 309 |
+
/// [19]: Inherited (vtable4C from CGameTask — not overridden)
|
| 310 |
+
pub _inherited_19: *const (),
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
const _: () = assert!(core::mem::size_of::<CTaskWormVTable>() == 20 * 4);
|
| 314 |
+
|
| 315 |
+
/// Worm entity task — the primary playable character in WA.
|
| 316 |
+
///
|
| 317 |
+
/// Extends CGameTask (0xFC bytes) with worm identity, physics overrides, and
|
| 318 |
+
/// per-worm state. Total size: 0x3FC bytes.
|
| 319 |
+
///
|
| 320 |
+
/// Constructor: 0x50BFB0 (stdcall, 5 params):
|
| 321 |
+
/// this, parent_task, team_index, worm_index, init_data_ptr
|
| 322 |
+
///
|
| 323 |
+
/// Vtable at 0x6644C8. Class type byte: 0x12.
|
| 324 |
+
///
|
| 325 |
+
/// # Important fields in the CGameTask base
|
| 326 |
+
/// The worm state field lives at **offset +0x44** (inside `base._unknown_30`).
|
| 327 |
+
/// Use [`CTaskWorm::state`] to read it without pointer arithmetic.
|
| 328 |
+
///
|
| 329 |
+
/// Source: Ghidra decompilation of 0x50BFB0, vtable analysis of 0x6644C8,
|
| 330 |
+
/// wkJellyWorm CTaskWorm.h
|
| 331 |
+
#[repr(C)]
|
| 332 |
+
pub struct CTaskWorm {
|
| 333 |
+
/// 0x00–0xFB: CGameTask base (position, velocity, sound emitter, etc.)
|
| 334 |
+
pub base: CGameTask,
|
| 335 |
+
|
| 336 |
+
/// 0xFC: Team index (0-based); 3rd constructor param
|
| 337 |
+
pub team_index: u32,
|
| 338 |
+
/// 0x100: Worm index within team (0-based); 4th constructor param
|
| 339 |
+
pub worm_index: u32,
|
| 340 |
+
/// 0x104: Unknown flag (checked in OnContactEntity)
|
| 341 |
+
pub _unknown_104: u32,
|
| 342 |
+
/// 0x108–0x10F: Unknown
|
| 343 |
+
pub _unknown_108: [u8; 8],
|
| 344 |
+
/// 0x110–0x137: Ten u32s copied from spawn init_data (5th constructor param)
|
| 345 |
+
pub spawn_params: [u32; 10],
|
| 346 |
+
/// 0x138–0x14F: Unknown
|
| 347 |
+
pub _unknown_138: [u8; 0x18],
|
| 348 |
+
/// 0x150: Unknown (slot 9 in GetEntityData query 0x7D4 output)
|
| 349 |
+
pub _unknown_150: u32,
|
| 350 |
+
/// 0x154: Unknown (rope-related; cleared in some SetState transitions)
|
| 351 |
+
pub _unknown_154: u32,
|
| 352 |
+
/// 0x158: Worm pool slot index in DDGame (assigned from pool at construction)
|
| 353 |
+
pub slot_id: u32,
|
| 354 |
+
/// 0x15C–0x247: Unknown
|
| 355 |
+
pub _unknown_15c: [u8; 0xEC],
|
| 356 |
+
/// 0x248–0x25B: Aiming / weapon data (slots 4–8 in GetEntityData query 0x7D4)
|
| 357 |
+
pub _unknown_248: [u32; 5],
|
| 358 |
+
/// 0x25C–0x283: Unknown
|
| 359 |
+
pub _unknown_25c: [u8; 0x28],
|
| 360 |
+
/// 0x284–0x28B: Aiming data (slots 5–6 in GetEntityData query 0x7D4)
|
| 361 |
+
pub _unknown_284: [u32; 2],
|
| 362 |
+
/// 0x28C–0x2EF: Unknown
|
| 363 |
+
pub _unknown_28c: [u8; 0x64],
|
| 364 |
+
/// 0x2F0: Worm name, null-terminated (max 17 chars, from spawn init_data+3)
|
| 365 |
+
pub worm_name: [u8; 0x11],
|
| 366 |
+
/// 0x301: Country / team name from scheme, null-terminated (max 17 chars)
|
| 367 |
+
pub country_name: [u8; 0x11],
|
| 368 |
+
/// 0x312–0x367: Unknown (rope string, state history, etc.)
|
| 369 |
+
pub _unknown_312: [u8; 0x56],
|
| 370 |
+
/// 0x368: Animator / controller object (dispatched via vtable for state animations)
|
| 371 |
+
pub animator: *mut u8,
|
| 372 |
+
/// 0x36C–0x3FB: Unknown (rope anchor, weapon-specific data, etc.)
|
| 373 |
+
pub _unknown_36c: [u8; 0x90],
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
|
| 377 |
+
|
| 378 |
+
/// Message-subscription filter task — routes messages selectively to child tasks.
|
| 379 |
+
///
|
| 380 |
+
/// CTaskFilter is a CTask subclass that overrides HandleMessage to only forward
|
| 381 |
+
/// messages whose type is marked in a 100-entry boolean subscription table. Each
|
| 382 |
+
/// CTaskFilter instance subscribes to a specific set of message IDs at construction
|
| 383 |
+
/// time; all other messages are silently dropped before reaching the subtree.
|
| 384 |
+
///
|
| 385 |
+
/// **Role in the task tree**: CTaskTeam creates multiple CTaskFilter children per
|
| 386 |
+
/// team during construction. Each filter represents a different event-routing path
|
| 387 |
+
/// (e.g., movement, UI, game-flow, weather). Messages from CTaskTurnGame propagate
|
| 388 |
+
/// down through these filters, which gate access to their subtrees.
|
| 389 |
+
///
|
| 390 |
+
/// **Allocation size**: 0xB4 bytes (via operator new in factory functions).
|
| 391 |
+
///
|
| 392 |
+
/// **Constructor**: `CTaskFilter__Constructor` (0x54F3D0, thiscall):
|
| 393 |
+
/// - `init_val_1c`: stored at CTask+0x1C (role unknown)
|
| 394 |
+
/// - `parent_task`: parent in the task tree (determines shared_data)
|
| 395 |
+
///
|
| 396 |
+
/// **Key vtable methods** (vtable at 0x669DAC):
|
| 397 |
+
/// - [2] `CTaskFilter__HandleMessage` (0x54F4A0): checks subscription table, forwards
|
| 398 |
+
/// only if `msg_type < 100 && subscription_table[msg_type] != 0`
|
| 399 |
+
/// - [7] `CTaskFilter__SubscribeAll` (0x54F390): sets all 100 entries to 1
|
| 400 |
+
/// - [8] `CTaskFilter__Subscribe` (0x54F370): sets `subscription_table[msg_id] = 1`
|
| 401 |
+
///
|
| 402 |
+
/// **Four factory functions** (all called by `CTaskTeam__Constructor_Maybe` 0x550E70):
|
| 403 |
+
/// - `FUN_00552030`: subscribes to messages 0, 1, 3, 5
|
| 404 |
+
/// - `FUN_005520D0`: subscribes to messages 0, 1, 2, 3, 0x15, 0x18, 0x1C
|
| 405 |
+
/// - `FUN_00552190`: subscribes to messages 0, 1, 2, 3, 5, 0x15, 0x17, 0x1C, 0x2C–0x2E, 0x4B,
|
| 406 |
+
/// and optionally 0x0E (if `GameInfo+0xD778 < -1`)
|
| 407 |
+
/// - `CTaskTeam__CreateWeatherFilter` (0x552960): subscribes to 1, 2, 3, 0x54, then
|
| 408 |
+
/// spawns `CTaskCloud` children using randomised positions (only if `DDGame+0x777C == 0`)
|
| 409 |
+
#[repr(C)]
|
| 410 |
+
pub struct CTaskFilter {
|
| 411 |
+
/// 0x00–0x2F: CTask base.
|
| 412 |
+
///
|
| 413 |
+
/// Notable base fields set by CTaskFilter__Constructor:
|
| 414 |
+
/// - CTask+0x18 (`_unknown_18`): set to 0
|
| 415 |
+
/// - CTask+0x1C (`_unknown_1c`): set to `init_val_1c` constructor param
|
| 416 |
+
/// - CTask+0x20 (`_unknown_20`): set to 7 (task type / mode constant)
|
| 417 |
+
pub base: CTask,
|
| 418 |
+
/// 0x30–0x93: Boolean subscription table, indexed by message type ID (0–99).
|
| 419 |
+
///
|
| 420 |
+
/// `subscription_table[id] != 0` means this filter will forward messages of
|
| 421 |
+
/// that type. Cleared to 0 at construction, then populated by Subscribe/SubscribeAll
|
| 422 |
+
/// calls. Max 100 distinct message IDs (IDs >= 100 always pass through).
|
| 423 |
+
pub subscription_table: [u8; 100],
|
| 424 |
+
/// 0x94–0xB3: Unknown (present in 0xB4-byte allocation; not set by constructor).
|
| 425 |
+
pub _unknown_94: [u8; 0x20],
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
const _: () = assert!(core::mem::size_of::<CTaskFilter>() == 0xB4);
|
| 429 |
+
|
| 430 |
+
impl CTaskWorm {
|
| 431 |
+
/// Returns the worm's current state code (lives at offset +0x44, inside the
|
| 432 |
+
/// CGameTask base's `_unknown_30` padding region).
|
| 433 |
+
///
|
| 434 |
+
/// Known states: `0x65`=idle, `0x67`=active turn, `0x7F`=drowning,
|
| 435 |
+
/// `0x80`=hurt, `0x81`/`0x86`=dead, `0x87`=dead variant, `0x8B`=unknown.
|
| 436 |
+
pub fn state(&self) -> u32 {
|
| 437 |
+
// SAFETY: offset 0x44 is within CGameTask._unknown_30 (0x30..0x84).
|
| 438 |
+
// Aligned to 4 bytes; repr(C) guarantees no reordering.
|
| 439 |
+
unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
/// Returns a reference to the vtable.
|
| 443 |
+
///
|
| 444 |
+
/// # Safety
|
| 445 |
+
/// The vtable pointer at offset 0 must be the genuine CTaskWorm vtable
|
| 446 |
+
/// (0x6644C8 in Ghidra, rebased at runtime).
|
| 447 |
+
pub unsafe fn vtable(&self) -> &'static CTaskWormVTable {
|
| 448 |
+
&*(self.base.base.vtable as *const CTaskWormVTable)
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
// ============================================================
|
| 453 |
// Derived task overlays — for accessing task-specific fields
|
| 454 |
// beyond or within CGameTask that differ per task type.
|
|
@@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|
| 11 |
|
| 12 |
use openwa_core::rebase::rb;
|
| 13 |
use openwa_core::address::va;
|
| 14 |
-
use openwa_core::task::{CTask, CGameTask};
|
| 15 |
use openwa_core::ddgame::DDGame;
|
| 16 |
use openwa_core::ddgame_wrapper::DDGameWrapper;
|
| 17 |
|
|
@@ -711,6 +711,215 @@ fn log_hex_dump(data: &[u8]) {
|
|
| 711 |
}
|
| 712 |
}
|
| 713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
// ---------------------------------------------------------------------------
|
| 715 |
// Public entry point — called from wormkit's run()
|
| 716 |
// ---------------------------------------------------------------------------
|
|
@@ -773,6 +982,10 @@ pub fn run() -> Result<(), String> {
|
|
| 773 |
std::thread::sleep(std::time::Duration::from_secs(3));
|
| 774 |
let _ = log_validation(" Running deferred global validation...");
|
| 775 |
deferred_global_validation();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
|
| 777 |
// Wait briefly for replay to start, then dump game state
|
| 778 |
// (fast-forward finishes the replay in ~10-15s total, so dump early)
|
|
@@ -827,18 +1040,18 @@ pub fn start_hotkeys() {
|
|
| 827 |
|
| 828 |
std::thread::spawn(|| {
|
| 829 |
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
|
| 830 |
-
const VK_F9: i32
|
| 831 |
const VK_F10: i32 = 0x79;
|
| 832 |
-
|
|
|
|
|
|
|
| 833 |
loop {
|
| 834 |
std::thread::sleep(std::time::Duration::from_millis(100));
|
| 835 |
unsafe {
|
| 836 |
-
if GetAsyncKeyState(VK_F9) & 1 != 0 {
|
| 837 |
-
|
| 838 |
-
}
|
| 839 |
-
if GetAsyncKeyState(
|
| 840 |
-
dump_landscape();
|
| 841 |
-
}
|
| 842 |
}
|
| 843 |
}
|
| 844 |
});
|
|
|
|
| 11 |
|
| 12 |
use openwa_core::rebase::rb;
|
| 13 |
use openwa_core::address::va;
|
| 14 |
+
use openwa_core::task::{CTask, CGameTask, SharedDataTable};
|
| 15 |
use openwa_core::ddgame::DDGame;
|
| 16 |
use openwa_core::ddgame_wrapper::DDGameWrapper;
|
| 17 |
|
|
|
|
| 711 |
}
|
| 712 |
}
|
| 713 |
|
| 714 |
+
// ---------------------------------------------------------------------------
|
| 715 |
+
// Entity census — enumerate all registered task types via shared_data table
|
| 716 |
+
// ---------------------------------------------------------------------------
|
| 717 |
+
|
| 718 |
+
fn dump_entity_census() {
|
| 719 |
+
let _ = log_validation("");
|
| 720 |
+
let _ = log_validation("--- Entity Census (SharedData hash table) ---");
|
| 721 |
+
|
| 722 |
+
unsafe {
|
| 723 |
+
let session_ptr = read_u32(rb(va::G_GAME_SESSION));
|
| 724 |
+
if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; }
|
| 725 |
+
let wrapper_addr = read_u32(session_ptr + 0xA0);
|
| 726 |
+
if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; }
|
| 727 |
+
let ddgame_ptr = read_u32(wrapper_addr + 0x488);
|
| 728 |
+
if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; }
|
| 729 |
+
|
| 730 |
+
let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
|
| 731 |
+
if task_land_ptr == 0 {
|
| 732 |
+
let _ = log_validation(" CTaskLand NULL — game not loaded yet.");
|
| 733 |
+
return;
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
let task_land = task_land_ptr as *const CTask;
|
| 737 |
+
let shared_data_ptr = (*task_land).shared_data;
|
| 738 |
+
if shared_data_ptr.is_null() {
|
| 739 |
+
let _ = log_validation(" shared_data is NULL.");
|
| 740 |
+
return;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
let table = SharedDataTable::from_ptr(shared_data_ptr);
|
| 744 |
+
|
| 745 |
+
// Name table: Ghidra VA → display name
|
| 746 |
+
let known: &[(u32, &str)] = &[
|
| 747 |
+
(va::CTASK_WORM_VTABLE, "CTaskWorm"),
|
| 748 |
+
(va::CTASK_LAND_VTABLE, "CTaskLand"),
|
| 749 |
+
(va::CTASK_TURN_GAME_VTABLE, "CTaskTurnGame"),
|
| 750 |
+
(va::CTASK_TEAM_VTABLE, "CTaskTeam"),
|
| 751 |
+
(va::CTASK_FILTER_VTABLE, "CTaskFilter"),
|
| 752 |
+
(va::CTASK_DIRT_VTABLE, "CTaskDirt"),
|
| 753 |
+
(va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"),
|
| 754 |
+
(va::CTASK_CPU_VTABLE, "CTaskCPU"),
|
| 755 |
+
(va::CTASK_VTABLE, "CTask"),
|
| 756 |
+
(va::CGAMETASK_VTABLE, "CGameTask"),
|
| 757 |
+
];
|
| 758 |
+
|
| 759 |
+
// Collect (ghidra_va, entity_ptr) for every node.
|
| 760 |
+
let delta = rb(va::IMAGE_BASE).wrapping_sub(va::IMAGE_BASE);
|
| 761 |
+
let mut entries: Vec<(u32, u32)> = Vec::new();
|
| 762 |
+
let mut total = 0u32;
|
| 763 |
+
|
| 764 |
+
for node in table.iter() {
|
| 765 |
+
let entity = (*node).entity;
|
| 766 |
+
if entity.is_null() { continue; }
|
| 767 |
+
let vtable_runtime = read_u32(entity as u32);
|
| 768 |
+
if !is_in_rdata(vtable_runtime) { continue; }
|
| 769 |
+
let vtable_ghidra = vtable_runtime.wrapping_sub(delta);
|
| 770 |
+
entries.push((vtable_ghidra, entity as u32));
|
| 771 |
+
total += 1;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
let _ = log_validation(&format!(" Total entities: {}", total));
|
| 775 |
+
|
| 776 |
+
// Group by vtable, sort by count descending.
|
| 777 |
+
let mut groups: Vec<(u32, Vec<u32>)> = Vec::new();
|
| 778 |
+
for (vt, ptr) in &entries {
|
| 779 |
+
if let Some(g) = groups.iter_mut().find(|(v, _)| *v == *vt) {
|
| 780 |
+
g.1.push(*ptr);
|
| 781 |
+
} else {
|
| 782 |
+
groups.push((*vt, vec![*ptr]));
|
| 783 |
+
}
|
| 784 |
+
}
|
| 785 |
+
groups.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
|
| 786 |
+
|
| 787 |
+
for (vt_ghidra, ptrs) in &groups {
|
| 788 |
+
let name = known.iter().find(|(v, _)| *v == *vt_ghidra)
|
| 789 |
+
.map(|(_, n)| *n)
|
| 790 |
+
.unwrap_or("UNKNOWN");
|
| 791 |
+
let _ = log_validation(&format!(
|
| 792 |
+
" {:>3}x {:<20} (vtable 0x{:08X})",
|
| 793 |
+
ptrs.len(), name, vt_ghidra
|
| 794 |
+
));
|
| 795 |
+
for &ptr in ptrs.iter().take(8) {
|
| 796 |
+
use openwa_core::task::CTaskWorm;
|
| 797 |
+
if *vt_ghidra == va::CTASK_WORM_VTABLE {
|
| 798 |
+
let w = &*(ptr as *const CTaskWorm);
|
| 799 |
+
let nlen = w.worm_name.iter().position(|&c| c == 0).unwrap_or(w.worm_name.len());
|
| 800 |
+
let wname = core::str::from_utf8(&w.worm_name[..nlen]).unwrap_or("?");
|
| 801 |
+
let _ = log_validation(&format!(
|
| 802 |
+
" @ 0x{:08X} team={} idx={} state=0x{:02X} name=\"{}\"",
|
| 803 |
+
ptr, w.team_index, w.worm_index, w.state(), wname
|
| 804 |
+
));
|
| 805 |
+
} else {
|
| 806 |
+
let a = read_u32(ptr + 4);
|
| 807 |
+
let b = read_u32(ptr + 8);
|
| 808 |
+
let c = read_u32(ptr + 12);
|
| 809 |
+
let d = read_u32(ptr + 16);
|
| 810 |
+
let _ = log_validation(&format!(
|
| 811 |
+
" @ 0x{:08X} [+4:{:08X} +8:{:08X} +c:{:08X} +10:{:08X}]",
|
| 812 |
+
ptr, a, b, c, d
|
| 813 |
+
));
|
| 814 |
+
}
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
}
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
// ---------------------------------------------------------------------------
|
| 821 |
+
// CTaskWorm entity dump
|
| 822 |
+
// ---------------------------------------------------------------------------
|
| 823 |
+
|
| 824 |
+
fn dump_worm_tasks() {
|
| 825 |
+
use openwa_core::task::CTaskWorm;
|
| 826 |
+
|
| 827 |
+
let _ = log_validation("");
|
| 828 |
+
let _ = log_validation("--- CTaskWorm Entity Dump ---");
|
| 829 |
+
|
| 830 |
+
unsafe {
|
| 831 |
+
let session_ptr = read_u32(rb(va::G_GAME_SESSION));
|
| 832 |
+
if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; }
|
| 833 |
+
|
| 834 |
+
let wrapper_addr = read_u32(session_ptr + 0xA0);
|
| 835 |
+
if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; }
|
| 836 |
+
|
| 837 |
+
let ddgame_ptr = read_u32(wrapper_addr + 0x488);
|
| 838 |
+
if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; }
|
| 839 |
+
|
| 840 |
+
// DDGame+0x54C = CTaskLand* — always present once map is loaded.
|
| 841 |
+
let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
|
| 842 |
+
if task_land_ptr == 0 {
|
| 843 |
+
let _ = log_validation(" CTaskLand is NULL — game not fully loaded yet.");
|
| 844 |
+
return;
|
| 845 |
+
}
|
| 846 |
+
let _ = log_validation(&format!(" DDGame @ 0x{:08X}", ddgame_ptr));
|
| 847 |
+
let _ = log_validation(&format!(" CTaskLand @ 0x{:08X}", task_land_ptr));
|
| 848 |
+
|
| 849 |
+
// CTask.shared_data at +0x24 — root tasks own 0x420 bytes with a
|
| 850 |
+
// 256-bucket hash table (FUN_005406a0). All tasks in the same game
|
| 851 |
+
// tree share this block via the inherited shared_data pointer.
|
| 852 |
+
let task_land = task_land_ptr as *const CTask;
|
| 853 |
+
let shared_data_ptr = (*task_land).shared_data;
|
| 854 |
+
if shared_data_ptr.is_null() {
|
| 855 |
+
let _ = log_validation(" shared_data is NULL.");
|
| 856 |
+
return;
|
| 857 |
+
}
|
| 858 |
+
let _ = log_validation(&format!(" shared_data @ 0x{:08X}", shared_data_ptr as u32));
|
| 859 |
+
|
| 860 |
+
let table = SharedDataTable::from_ptr(shared_data_ptr);
|
| 861 |
+
|
| 862 |
+
let expected_vtable = rb(va::CTASK_WORM_VTABLE) as *const u8;
|
| 863 |
+
let _ = log_validation(&format!(" Expected CTaskWorm vtable: 0x{:08X}", expected_vtable as u32));
|
| 864 |
+
|
| 865 |
+
let mut worm_count = 0u32;
|
| 866 |
+
|
| 867 |
+
for node in table.iter() {
|
| 868 |
+
let candidate = (*node).entity;
|
| 869 |
+
if candidate.is_null() { continue; }
|
| 870 |
+
// Vtable is the first pointer in the object; must be in .rdata.
|
| 871 |
+
let vtable = read_u32(candidate as u32) as *const u8;
|
| 872 |
+
if !is_in_rdata(vtable as u32) || vtable != expected_vtable { continue; }
|
| 873 |
+
|
| 874 |
+
worm_count += 1;
|
| 875 |
+
let worm = &*(candidate as *const CTaskWorm);
|
| 876 |
+
|
| 877 |
+
let state = worm.state();
|
| 878 |
+
let pos_x_f = worm.base.pos_x.0 as f32 / 65536.0;
|
| 879 |
+
let pos_y_f = worm.base.pos_y.0 as f32 / 65536.0;
|
| 880 |
+
let team_idx = worm.team_index;
|
| 881 |
+
let worm_idx = worm.worm_index;
|
| 882 |
+
let slot_id = worm.slot_id;
|
| 883 |
+
|
| 884 |
+
let name_len = worm.worm_name.iter().position(|&c| c == 0).unwrap_or(worm.worm_name.len());
|
| 885 |
+
let name = core::str::from_utf8(&worm.worm_name[..name_len]).unwrap_or("?");
|
| 886 |
+
|
| 887 |
+
let _ = log_validation(&format!(
|
| 888 |
+
" worm#{} @ 0x{:08X}: team={} worm_idx={} slot={} state=0x{:04X} pos=({:.1},{:.1}) name=\"{}\"",
|
| 889 |
+
worm_count, candidate as u32,
|
| 890 |
+
team_idx, worm_idx, slot_id,
|
| 891 |
+
state, pos_x_f, pos_y_f, name
|
| 892 |
+
));
|
| 893 |
+
|
| 894 |
+
// Cross-validate against WormEntry in TeamArena.
|
| 895 |
+
// WormEntry = DDGame+0x4090 + team*0x51C + worm_idx*0x9C
|
| 896 |
+
// +0x00: state, +0x78: name (17 bytes)
|
| 897 |
+
if team_idx < 6 && worm_idx < 8 {
|
| 898 |
+
let entry_addr = ddgame_ptr + 0x4090 + team_idx * 0x51C + worm_idx * 0x9C;
|
| 899 |
+
let entry_state = read_u32(entry_addr);
|
| 900 |
+
let mut entry_name = [0u8; 17];
|
| 901 |
+
for i in 0..17usize {
|
| 902 |
+
entry_name[i] = read_u8(entry_addr + 0x78 + i as u32);
|
| 903 |
+
if entry_name[i] == 0 { break; }
|
| 904 |
+
}
|
| 905 |
+
let entry_name_len = entry_name.iter().position(|&c| c == 0).unwrap_or(17);
|
| 906 |
+
let entry_name_str = core::str::from_utf8(&entry_name[..entry_name_len]).unwrap_or("?");
|
| 907 |
+
|
| 908 |
+
let state_msg = if state == entry_state { "state:OK".to_string() }
|
| 909 |
+
else { format!("state:MISMATCH(entry=0x{:04X})", entry_state) };
|
| 910 |
+
let name_msg = if name == entry_name_str { "name:OK".to_string() }
|
| 911 |
+
else { format!("name:MISMATCH(entry=\"{}\")", entry_name_str) };
|
| 912 |
+
let _ = log_validation(&format!(
|
| 913 |
+
" xcheck WormEntry[{},{}] @ 0x{:08X}: {} {}",
|
| 914 |
+
team_idx, worm_idx, entry_addr, state_msg, name_msg
|
| 915 |
+
));
|
| 916 |
+
}
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
let _ = log_validation(&format!(" Total CTaskWorm entities found: {}", worm_count));
|
| 920 |
+
}
|
| 921 |
+
}
|
| 922 |
+
|
| 923 |
// ---------------------------------------------------------------------------
|
| 924 |
// Public entry point — called from wormkit's run()
|
| 925 |
// ---------------------------------------------------------------------------
|
|
|
|
| 982 |
std::thread::sleep(std::time::Duration::from_secs(3));
|
| 983 |
let _ = log_validation(" Running deferred global validation...");
|
| 984 |
deferred_global_validation();
|
| 985 |
+
let _ = log_validation(" Running entity census (5s mark)...");
|
| 986 |
+
dump_entity_census();
|
| 987 |
+
let _ = log_validation(" Running worm entity dump (5s mark)...");
|
| 988 |
+
dump_worm_tasks();
|
| 989 |
|
| 990 |
// Wait briefly for replay to start, then dump game state
|
| 991 |
// (fast-forward finishes the replay in ~10-15s total, so dump early)
|
|
|
|
| 1040 |
|
| 1041 |
std::thread::spawn(|| {
|
| 1042 |
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
|
| 1043 |
+
const VK_F9: i32 = 0x78;
|
| 1044 |
const VK_F10: i32 = 0x79;
|
| 1045 |
+
const VK_F11: i32 = 0x7A;
|
| 1046 |
+
const VK_F12: i32 = 0x7B;
|
| 1047 |
+
let _ = log_validation(" Hotkeys: F9=team blocks, F10=landscape, F11=worm tasks, F12=entity census");
|
| 1048 |
loop {
|
| 1049 |
std::thread::sleep(std::time::Duration::from_millis(100));
|
| 1050 |
unsafe {
|
| 1051 |
+
if GetAsyncKeyState(VK_F9) & 1 != 0 { dump_team_blocks(); }
|
| 1052 |
+
if GetAsyncKeyState(VK_F10) & 1 != 0 { dump_landscape(); }
|
| 1053 |
+
if GetAsyncKeyState(VK_F11) & 1 != 0 { dump_worm_tasks(); }
|
| 1054 |
+
if GetAsyncKeyState(VK_F12) & 1 != 0 { dump_entity_census(); }
|
|
|
|
|
|
|
| 1055 |
}
|
| 1056 |
}
|
| 1057 |
});
|