paavohtl Claude Sonnet 4.6 commited on
Commit
f3cbb8a
Β·
1 Parent(s): 90d1791

refactor: split task.rs into per-type modules under task/

Browse files

Each task struct now lives in its own file. Tightly coupled types
are kept together (TurnGameCtx+CTaskTurnGame, CTaskMine+CTaskOilDrum,
SoundEmitter+CGameTask, CTaskWorm+CTaskWormVTable). mod.rs re-exports
all public types so external import paths are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

crates/openwa-core/src/task.rs DELETED
@@ -1,924 +0,0 @@
1
- use crate::class_type::ClassType;
2
- use crate::ddgame::DDGame;
3
- use crate::fixed::Fixed;
4
-
5
- /// Base task class in WA's entity hierarchy.
6
- ///
7
- /// All game objects inherit from CTask. Tasks form a tree via parent/children
8
- /// pointers and communicate through the TaskMessage system.
9
- ///
10
- /// Source: wkJellyWorm CTask.h, Ghidra decompilation of 0x5625A0 + 0x562520
11
- ///
12
- /// Vtable at 0x669F8C (8 methods):
13
- /// 0x00: 0x562710 vtable0 (init?)
14
- /// 0x04: 0x562620 Free
15
- /// 0x08: 0x562F30 HandleMessage
16
- /// 0x0C: 0x5613D0 unknown
17
- /// 0x10: 0x5613D0 unknown (same as 0x0C)
18
- /// 0x14: 0x562FA0 unknown
19
- /// 0x18: 0x563000 unknown
20
- /// 0x1C: 0x563210 ProcessFrame
21
- #[repr(C)]
22
- pub struct CTask {
23
- /// 0x00: Pointer to virtual method table
24
- pub vtable: *mut u8,
25
- /// 0x04: Parent task in the hierarchy
26
- pub parent: *mut u8,
27
- /// 0x08: Children list max capacity (set to 0x10 in constructor)
28
- pub children_max_size: u32,
29
- /// 0x0C: Children list unknown field (set to 0 in constructor)
30
- pub children_unk: u32,
31
- /// 0x10: Children list current size
32
- pub children_size: u32,
33
- /// 0x14: Pointer to children data array (allocated 0x60 bytes in constructor)
34
- pub children_data: *mut u8,
35
- /// 0x18: Children hash list pointer (set to 0 in constructor)
36
- pub children_hash: *mut u8,
37
- /// 0x1C: Unknown (set to 0 by parent-linking helper FUN_00562520)
38
- pub _unknown_1c: u32,
39
- /// 0x20: Task classification type (set to ClassType::Task by FUN_00562520,
40
- /// overridden by derived constructors)
41
- pub class_type: ClassType,
42
- /// 0x24: Shared data buffer pointer (inherited from parent, or allocated
43
- /// 0x420 bytes for root tasks)
44
- pub shared_data: *mut u8,
45
- /// 0x28: 1 if this task owns shared_data (root), 0 if inherited from parent
46
- pub owns_shared_data: u32,
47
- /// 0x2C: DDGame pointer (3rd param to CTask::Constructor, stored at this+0x2C)
48
- pub ddgame: *mut DDGame,
49
- }
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.
184
- /// Only position and velocity fields have been verified.
185
- ///
186
- /// Source: wkJellyWorm CGameTask.h
187
- ///
188
- /// Additional vtable (12 methods at offsets 0x1C-0x48 in vtable)
189
- #[repr(C)]
190
- pub struct CGameTask {
191
- /// 0x00-0x2F: Base CTask fields
192
- pub base: CTask,
193
- /// 0x30-0x83: Unknown gameplay fields (84 bytes)
194
- pub _unknown_30: [u8; 0x54],
195
- /// 0x84: X position in fixed-point
196
- pub pos_x: Fixed,
197
- /// 0x88: Y position in fixed-point
198
- pub pos_y: Fixed,
199
- /// 0x8C-0x8F: Unknown (4 bytes between pos and speed)
200
- pub _unknown_8c: [u8; 4],
201
- /// 0x90: X velocity in fixed-point
202
- pub speed_x: Fixed,
203
- /// 0x94: Y velocity in fixed-point
204
- pub speed_y: Fixed,
205
- /// 0x98-0xE7: Unknown gameplay fields
206
- pub _unknown_98: [u8; 0x50],
207
- /// 0xE8: Embedded sound emitter sub-object (MSVC multiple inheritance).
208
- pub sound_emitter: SoundEmitter,
209
- }
210
-
211
- const _: () = assert!(core::mem::size_of::<CGameTask>() == 0xFC);
212
-
213
- /// Sound emitter sub-object embedded in CGameTask via MSVC multiple inheritance.
214
- ///
215
- /// Provides spatial audio support. The `this` pointer for its vtable methods
216
- /// points to the start of this sub-object (CGameTask+0xE8), not the CGameTask.
217
- #[repr(C)]
218
- pub struct SoundEmitter {
219
- /// +0x00: Vtable pointer
220
- pub vtable: *const SoundEmitterVTable,
221
- /// +0x04-0x0B: Unknown fields
222
- pub _unknown_04: [u8; 8],
223
- /// +0x0C: Number of active local sounds
224
- pub local_sound_count: i32,
225
- /// +0x10: Back-pointer to containing CGameTask
226
- pub owner: *mut CGameTask,
227
- }
228
-
229
- const _: () = assert!(core::mem::size_of::<SoundEmitter>() == 0x14);
230
-
231
- /// Vtable for the SoundEmitter sub-object (0x669CF8, 12 slots).
232
- ///
233
- /// Slots [0]-[4] are the sound emitter's own interface.
234
- /// Slots [5]-[11] are inherited CTask base methods.
235
- #[repr(C)]
236
- pub struct SoundEmitterVTable {
237
- /// [0] 0x546680: GetPosition(this, out_x, out_y) β€” reads pos_x/pos_y via owner
238
- pub get_position: unsafe extern "thiscall" fn(*const SoundEmitter, *mut u32, *mut u32),
239
- /// [1] 0x5466A0: GetPosition2(this, out_x, out_y) β€” reads CGameTask+0x38/0x3C
240
- pub get_position2: unsafe extern "thiscall" fn(*const SoundEmitter, *mut u32, *mut u32),
241
- /// [2] 0x4260E0: Unknown
242
- pub _unknown_2: *const (),
243
- /// [3] 0x546990: Destructor(this, flags)
244
- pub destructor: unsafe extern "thiscall" fn(*mut SoundEmitter, u32) -> *mut SoundEmitter,
245
- /// [4] 0x546760: HandleMessage β€” sound queue manager
246
- pub handle_message: unsafe extern "thiscall" fn(*mut SoundEmitter, u32, u32, u32, u32),
247
- /// [5]-[11]: Inherited CTask base methods
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
- /// Airstrike / weather cloud task.
431
- ///
432
- /// Extends CTask directly (not CGameTask). Clouds drift horizontally with wind,
433
- /// scroll on a parallax layer, and render as a single sprite.
434
- ///
435
- /// Allocation: 0x74 bytes (operator new in CTaskTeam__CreateWeatherFilter).
436
- /// Constructor: 0x5482E0 (usercall ESI=this, EAX=parent).
437
- /// Vtable: 0x669D38. Class type byte: 0x17.
438
- ///
439
- /// Three cloud sizes chosen by `cloud_type` param (0/1/2):
440
- /// - type 0: sprite 0x268 (large), vel_y 0x200
441
- /// - type 1: sprite 0x269 (medium), vel_y 0x166
442
- /// - type 2: sprite 0x26A (small), vel_y 0xCC
443
- ///
444
- /// Source: Ghidra decompilation of 0x5482E0 (constructor) and
445
- /// 0x5484C0 (HandleMessage update + render branches).
446
- #[repr(C)]
447
- pub struct CTaskCloud {
448
- /// 0x00–0x2F: CTask base
449
- pub base: CTask,
450
- /// 0x30: Parallax scroll layer depth (Fixed; 0x190000 = 25.0 at spawn,
451
- /// decrements by 1 each cloud spawned in a batch)
452
- pub layer_depth: Fixed,
453
- /// 0x34: Y position (Fixed 16.16); updated each frame: pos_y += vel_y
454
- pub pos_y: Fixed,
455
- /// 0x38: Y velocity (Fixed 16.16; set by cloud type: large=0x200, medium=0x166, small=0xCC)
456
- pub vel_y: Fixed,
457
- /// 0x3C: Sprite ID passed to DrawSpriteLocal (0x268=large, 0x269=medium, 0x26A=small)
458
- pub sprite_id: u32,
459
- /// 0x40: X position (Fixed 16.16); wraps at landscape bounds each frame
460
- pub pos_x: Fixed,
461
- /// 0x44: Unknown (set from EDI at call site)
462
- pub _unknown_44: u32,
463
- /// 0x48: X velocity base (Fixed 16.16)
464
- pub vel_x: Fixed,
465
- /// 0x4C: Current wind acceleration (Fixed); converges toward wind_target each frame
466
- pub wind_accel: Fixed,
467
- /// 0x50: Target wind speed (Fixed); set by message 0x54 (wind-change event)
468
- pub wind_target: Fixed,
469
- /// 0x54–0x73: Unknown
470
- pub _unknown_54: [u8; 0x20],
471
- }
472
-
473
- const _: () = assert!(core::mem::size_of::<CTaskCloud>() == 0x74);
474
-
475
- impl CTaskWorm {
476
- /// Returns the worm's current state code (lives at offset +0x44, inside the
477
- /// CGameTask base's `_unknown_30` padding region).
478
- ///
479
- /// Known states: `0x65`=idle, `0x67`=active turn, `0x7F`=drowning,
480
- /// `0x80`=hurt, `0x81`/`0x86`=dead, `0x87`=dead variant, `0x8B`=unknown.
481
- pub fn state(&self) -> u32 {
482
- // SAFETY: offset 0x44 is within CGameTask._unknown_30 (0x30..0x84).
483
- // Aligned to 4 bytes; repr(C) guarantees no reordering.
484
- unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
485
- }
486
-
487
- /// Returns a reference to the vtable.
488
- ///
489
- /// # Safety
490
- /// The vtable pointer at offset 0 must be the genuine CTaskWorm vtable
491
- /// (0x6644C8 in Ghidra, rebased at runtime).
492
- pub unsafe fn vtable(&self) -> &'static CTaskWormVTable {
493
- &*(self.base.base.vtable as *const CTaskWormVTable)
494
- }
495
- }
496
-
497
- /// Land mine entity task.
498
- ///
499
- /// Extends CGameTask (0xFC bytes). Mines sit on the terrain and arm after
500
- /// placement; they detonate on contact once armed.
501
- ///
502
- /// Constructor: 0x506660 (stdcall).
503
- /// Vtable: 0x6643E8. Class type byte: 0x08.
504
- ///
505
- /// Source: Ghidra decompilation of 0x506660 (constructor) and
506
- /// 0x5072E0 (HandleMessage, msg 2/0x15/0x1C/0x4B branches).
507
- #[repr(C)]
508
- pub struct CTaskMine {
509
- /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
510
- pub base: CGameTask,
511
- /// 0xFC–0x10F: Unknown mine flags
512
- pub _unknown_fc: [u8; 0x14],
513
- /// 0x110: Object pool slot index (assigned from DDGame+0x3600 pool)
514
- pub slot_id: u32,
515
- /// 0x114–0x11B: Unknown
516
- pub _unknown_114: [u8; 8],
517
- /// 0x11C: Fuse timer (signed i32).
518
- /// Negative = just placed / disarmed.
519
- /// 0 = armed (will trigger on contact).
520
- /// Positive = countdown ticks remaining.
521
- pub fuse_timer: i32,
522
- /// 0x120–0x123: Unknown (init data param_3[0])
523
- pub _unknown_120: u32,
524
- /// 0x124: Owner team index (param_3[6]; -1 = no owner)
525
- pub owner_team: i32,
526
- }
527
-
528
- const _: () = assert!(core::mem::size_of::<CTaskMine>() == 0x128);
529
-
530
- /// Exploding oil drum entity task.
531
- ///
532
- /// Extends CGameTask (0xFC bytes). Oil drums roll on terrain and explode
533
- /// when hit enough times (health decrements per impact).
534
- ///
535
- /// Constructor: 0x504AF0 (thiscall).
536
- /// Vtable: 0x664338. Class type byte: 0x1E.
537
- ///
538
- /// Source: Ghidra decompilation of 0x504AF0 (constructor) and
539
- /// 0x5050B0 (HandleMessage, msg 2/0x1C branches).
540
- #[repr(C)]
541
- pub struct CTaskOilDrum {
542
- /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
543
- pub base: CGameTask,
544
- /// 0xFC: Triggered flag β€” set to 1 on first impact, starts smoke/fire
545
- pub triggered: u32,
546
- /// 0x100: Object pool slot index
547
- pub slot_id: u32,
548
- /// 0x104: Unknown
549
- pub _unknown_104: u32,
550
- /// 0x108: Health (starts at 0x32 = 50; decremented on damage)
551
- pub health: u32,
552
- /// 0x10C: Rolling animation counter (increments by 0x4000 per frame while moving)
553
- pub roll_counter: u32,
554
- }
555
-
556
- const _: () = assert!(core::mem::size_of::<CTaskOilDrum>() == 0x110);
557
-
558
- impl CTaskOilDrum {
559
- /// Returns true if the drum is on fire (flag at CGameTask+0xB0, inside _unknown_98).
560
- ///
561
- /// # Safety
562
- /// `self` must be a valid, fully-constructed CTaskOilDrum.
563
- pub unsafe fn on_fire(&self) -> bool {
564
- let ptr = (self as *const CTaskOilDrum as *const u8).add(0xB0);
565
- *(ptr as *const u32) != 0
566
- }
567
- }
568
-
569
- /// Per-team state-tracker task β€” one instance per team, child of CTaskTurnGame.
570
- ///
571
- /// Tracks per-team data: which team number it represents, how many worms were
572
- /// spawned, and a weapon/item slot table. Registered in the SharedData hash
573
- /// table with type code 0x15 (21).
574
- ///
575
- /// Inheritance: extends CTask directly (CTask base at +0x00).
576
- /// class_type = 10. Allocation: 0x460 bytes via `operator new` (0x5C0AB8).
577
- /// Constructor: `CTaskTeam__Constructor` (0x555BB0).
578
- /// Primary vtable: `CTaskTeam__vtable2` (0x00669EE4).
579
- /// Secondary interface vtable: 0x00669F00 (at object +0x30).
580
- ///
581
- /// Key constructor logic (0x555BB0):
582
- /// - `team_index` at +0x38: 1-based team number (passed as param_3)
583
- /// - `_item_slots[0..99]` at +0x88: up to 100 weapon/item IDs loaded from scheme
584
- /// - `worm_count` at +0x218: number of CTaskWorm children constructed (1-indexed)
585
- /// - SharedData node (0x30 bytes) registered with key = (team_index, type=0x15)
586
- #[repr(C)]
587
- pub struct CTaskTeam {
588
- /// 0x00–0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
589
- pub base: CTask,
590
- /// 0x30: Secondary interface vtable pointer (Ghidra 0x00669F00)
591
- pub _secondary_vtable: u32,
592
- /// 0x34: Unknown β€” observed to hold the same value as `team_index` in all runs.
593
- pub _unknown_34: u32,
594
- /// 0x38: Team number, **1-based**. Matches TeamArenaState slot index.
595
- pub team_index: u32,
596
- /// 0x3C: Unknown β€” observed 0 for team 1, 1 for team 2.
597
- pub _unknown_3c: u32,
598
- /// 0x40: Unknown signed integer β€” observed βˆ’40 for team 1, βˆ’20 for team 2.
599
- pub _unknown_40: i32,
600
- /// 0x44: Unknown (always 0 in observed runs).
601
- pub _unknown_44: u32,
602
- /// 0x48: Number of living worms remaining on this team.
603
- /// Decrements as worms are eliminated; equals `worm_count` at game start.
604
- pub alive_worm_count: u32,
605
- /// 0x4C–0x5B: Unknown.
606
- pub _unknown_4c: [u8; 16],
607
- /// 0x5C: Unknown β€” consistently 11 across teams and runs.
608
- pub _unknown_5c: u32,
609
- /// 0x60: Index of the last weapon launched by this team (0 = none launched yet).
610
- /// Field name confirmed by wkJellyWorm: `lastLaunchedWeapon_dword60`.
611
- pub last_launched_weapon: u32,
612
- /// 0x64–0x87: Unknown.
613
- pub _unknown_64: [u8; 36],
614
- /// 0x88–0x217: Unknown region (100 DWORDs). Observed non-zero values at the start
615
- /// (+0x88 = team_index, +0x8C/+0x90 = 3) in a 2-worm bot game; purpose unclear.
616
- pub _unknown_88: [u32; 100],
617
- /// 0x218: Number of CTaskWorm children constructed for this team.
618
- pub worm_count: u32,
619
- /// 0x21C–0x3EB: Unknown.
620
- pub _unknown_21c: [u8; 0x1D0],
621
- /// 0x3EC–0x3F3: Unknown flags (observed: 1, 1 then 0xFFFFFFFFΓ—3 at +0x3F4).
622
- pub _unknown_3ec: [u32; 2],
623
- /// 0x3F4–0x3FF: Sentinel values (observed: 0xFFFFFFFF in all runs).
624
- pub _sentinels_3f4: [u32; 3],
625
- /// 0x400–0x403: Unknown.
626
- pub _unknown_400: u32,
627
- /// 0x404: X position (Fixed16.16) β€” per-team, likely spawn or last-worm position.
628
- pub pos_x: Fixed,
629
- /// 0x408: Y position (Fixed16.16) β€” per-team, likely spawn or last-worm position.
630
- pub pos_y: Fixed,
631
- /// 0x40C: Unknown Fixed16.16 value β€” same for all teams (observed β‰ˆ 666.89).
632
- pub _unknown_40c: Fixed,
633
- /// 0x410: Unknown (observed 1 in all runs).
634
- pub _unknown_410: u32,
635
- /// 0x414–0x45F: Unknown.
636
- pub _unknown_414: [u8; 0x4C],
637
- }
638
-
639
- const _: () = assert!(core::mem::size_of::<CTaskTeam>() == 0x460);
640
-
641
- /// Fire/flame entity task.
642
- ///
643
- /// Extends CTask (not CGameTask) β€” no physics body.
644
- /// class_type = 0x18. Allocated 0xD8 bytes.
645
- /// Constructor: CTaskFire__Constructor (0x54F4C0).
646
- /// vtable: CTaskFire__vtable (0x00669DD8).
647
- ///
648
- /// One CTaskFire is spawned per flame sprite. The `timer` field starts
649
- /// at 0xFFFF and counts down each frame; when it reaches zero the fire
650
- /// dies. `lifetime` at +0xB1 is a signed byte: 0xFF (= -1i8) means alive,
651
- /// 0 means the task is being destroyed.
652
- #[repr(C)]
653
- pub struct CTaskFire {
654
- /// 0x00-0x2F: CTask base
655
- pub base: CTask,
656
- /// 0x30: spread counter (incremented while fire is spreading)
657
- pub spread_counter: i32,
658
- /// 0x34: frame countdown; starts at 0xFFFF, decrements each ProcessFrame
659
- pub timer: i32,
660
- /// 0x38: random seed / initial offset for sprite variation
661
- pub rand_offset: u32,
662
- /// 0x3C: burn rate / intensity (higher = more damage per frame)
663
- pub burn_rate: u32,
664
- pub _unknown_40: u32,
665
- /// 0x44: spawn X position (Fixed 16.16)
666
- pub spawn_x: crate::fixed::Fixed,
667
- /// 0x48: spawn Y position (Fixed 16.16)
668
- pub spawn_y: crate::fixed::Fixed,
669
- pub _unknown_4c: [u8; 0x24],
670
- /// 0x70: absolute tick (frame counter) when this flame was spawned
671
- pub spawn_time: u32,
672
- pub _unknown_74: u32,
673
- /// 0x78-0xA7: per-frame spawn parameter table (12 DWORDs)
674
- pub spawn_params: [u32; 12],
675
- /// 0xA8: slot index in the fire-object pool
676
- pub slot_index: u32,
677
- pub _unknown_ac: u32,
678
- pub _flags_b0: u8,
679
- /// 0xB1: lifetime byte; -1 (0xFF as i8) = alive, 0 = dying/dead
680
- pub lifetime: i8,
681
- pub _unknown_b2: [u8; 0x26],
682
- }
683
-
684
- const _: () = assert!(core::mem::size_of::<CTaskFire>() == 0xD8);
685
-
686
- /// Embedded intermediate game-context sub-object within `CTaskTurnGame`.
687
- ///
688
- /// This is the memory region at `CTaskTurnGame+0x30..+0xDB` (0xAC bytes).
689
- /// It is initialised by `CTaskTeam__Constructor_Maybe` (0x550EB0), which:
690
- /// 1. Calls `CTask::Constructor(this, nullptr, ddgame)`
691
- /// 2. Sets the primary vtable to 0x669E34 and class_type to 5
692
- /// 3. Sets a **secondary interface vtable** pointer (Ghidra 0x669C44) at +0x30
693
- /// inside the object (i.e. `TurnGameCtx` base +0x00)
694
- /// 4. Copies `landscape_height = DDGame+0x5E0` as Fixed16.16 to both
695
- /// `land_height` and `land_height_2`
696
- /// 5. Writes -1 sentinels to `_sentinel_18`, `_sentinel_28`, `_sentinel_38`
697
- /// 6. Writes `team_count = *(byte*)(GameInfo+0x44C)` to `team_count`
698
- ///
699
- /// `CTaskTurnGame__Constructor` (0x55B2A0) then overrides the primary vtable
700
- /// to 0x669F70 and class_type to 6 but **leaves this sub-object intact**.
701
- ///
702
- /// The three -1 sentinels at offsets 0x18 / 0x28 / 0x38 are evenly spaced
703
- /// 0x10 bytes apart. Their role is unknown β€” candidates are water-level
704
- /// Y-coordinates (initial -1 = "no water"), zone boundaries, or AI markers.
705
- ///
706
- /// The final 0x44 bytes (0x68–0xAB, `_unknown_68`) are not initialised by
707
- /// either constructor; they appear to be set by `FUN_005514d0` or similar
708
- /// helpers called later. Observed runtime values: +0xA0 = 15, +0xA4/+0xA8
709
- /// are heap pointers.
710
- #[repr(C)]
711
- pub struct TurnGameCtx {
712
- /// +0x00 (= CTaskTurnGame+0x30): Secondary interface vtable pointer.
713
- /// Ghidra: 0x00669C44. Set by both constructors; always 0x669C44 at runtime.
714
- pub _secondary_vtable: u32,
715
- /// +0x04: Unknown β€” not set by constructors (remains 0).
716
- pub _unknown_04: u32,
717
- /// +0x08–0x0F: Unknown β€” explicitly zeroed by constructor.
718
- pub _unknown_08: [u32; 2],
719
- /// +0x10: Landscape height as Fixed16.16. `DDGame+0x5E0 << 16`.
720
- pub land_height: Fixed,
721
- /// +0x14: Landscape height duplicate β€” same value as `land_height`.
722
- pub land_height_2: Fixed,
723
- /// +0x18: Sentinel, always -1 at construction. Role unknown.
724
- pub _sentinel_18: i32,
725
- /// +0x1C–0x27: Unknown β€” explicitly zeroed by constructor.
726
- pub _unknown_1c: [u32; 3],
727
- /// +0x28: Sentinel, always -1 at construction. Role unknown.
728
- pub _sentinel_28: i32,
729
- /// +0x2C–0x37: Unknown β€” explicitly zeroed by constructor.
730
- pub _unknown_2c: [u32; 3],
731
- /// +0x38: Sentinel, always -1 at construction. Role unknown.
732
- pub _sentinel_38: i32,
733
- /// +0x3C–0x4B: Unknown β€” explicitly zeroed by constructor.
734
- pub _unknown_3c: [u32; 4],
735
- /// +0x4C: Number of teams. `*(byte*)(GameInfo+0x44C)` at construction time.
736
- pub team_count: u32,
737
- /// +0x50–0x64: Unknown β€” explicitly zeroed by constructor (param_1[0x20..0x25]).
738
- pub _unknown_50: [u32; 6],
739
- /// +0x68–0x9F: Unknown β€” all explicitly zeroed by `FUN_005514d0` during game
740
- /// setup. Purpose unknown; may be per-team or per-worm state slots.
741
- pub _unknown_68: [u32; 14],
742
- /// +0xA0: Return value of `FUN_00525f50(0)` stored here during game setup.
743
- /// Observed value: 15. `FUN_00525f50` is a slot-allocation helper; this may
744
- /// be a pool slot index or a pre-computed game-state token.
745
- pub _slot_d0: u32,
746
- /// +0xA4: `DDDisplay` textbox handle β€” created by `DDDisplay__ConstructTextbox`
747
- /// with params `(buf, -1280, 2)` if `DDGame+0x7EF8 != 0` (display active).
748
- /// Likely the HUD timer textbox. NULL when display is disabled.
749
- pub _hud_textbox_a: u32,
750
- /// +0xA8: `DDDisplay` textbox handle β€” created with params `(buf, 8, 4)`.
751
- /// Likely a secondary HUD element. NULL when display is disabled.
752
- pub _hud_textbox_b: u32,
753
- }
754
-
755
- const _: () = assert!(core::mem::size_of::<TurnGameCtx>() == 0xAC);
756
-
757
- /// Root turn-controller task β€” one instance per game, parent of the entire entity tree.
758
- ///
759
- /// Every worm, team, projectile, and environment task is a child (direct or indirect)
760
- /// of this node. `CTaskTurnGame` drives the turn loop: it processes 50 game frames
761
- /// per second via `CTaskTurnGame__TurnManager_ProcessFrame` (0x55FDA0), which is
762
- /// called from HandleMessage case 2 (FrameFinish).
763
- ///
764
- /// Inheritance: CTask β†’ CTaskTeam β†’ CTaskTurnGame. Class type 6.
765
- /// Constructor: `CTaskTurnGame__Constructor` (0x55B2B1).
766
- /// vtable: `CTaskTurnGame__vtable` (0x00669F70), 20 slots.
767
- /// Total size: 0x2E0 bytes.
768
- ///
769
- /// Key vtable slots:
770
- /// [0] 0x55B5E0 β€” task-tree state snapshot serialiser
771
- /// [1] 0x55B540 β€” destructor / Free
772
- /// [2] 0x55DC00 β€” HandleMessage (30+ message types)
773
- /// [3] 0x5612E0 β€” HUD data query (responds to msg 0x7D3)
774
- ///
775
- /// All timers decrement by 20 ms per frame (= 1000 ms / 50 fps).
776
- #[repr(C)]
777
- pub struct CTaskTurnGame {
778
- /// 0x00-0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
779
- pub base: CTask,
780
- /// 0x30–0xDB: Embedded `TurnGameCtx` sub-object (0xAC bytes).
781
- /// See [`TurnGameCtx`] for field details.
782
- pub game_ctx: TurnGameCtx,
783
-
784
- // ---- CTaskTurnGame-specific fields (0xDC onwards) ----
785
-
786
- pub _unknown_dc: u32,
787
- pub _unknown_e0: u32,
788
- /// 0xE4: turn-seed: ~(DDGame+0x45EC % 9000), used for random initialisation.
789
- pub _turn_seed: u32,
790
- /// 0xE8: number of teams in this game (copy of DDGame+0x44C).
791
- pub num_teams: u32,
792
- pub _unknown_ec: [u8; 0x1C],
793
- /// 0x108: "worm active" flag β€” non-zero while the current worm is shooting or
794
- /// moving. While non-zero, `turn_timer` is paused.
795
- /// A copy is also written to DDGame+0x7234 at construction.
796
- pub worm_active: u32,
797
- pub _unknown_10c: [u8; 0x20],
798
- /// 0x12C: active team index, **1-based** (0 = no active team).
799
- /// Used to index per-team sound tables in DDGame (stride 0xF0 at DDGame+0x774C).
800
- pub current_team: u32,
801
- /// 0x130: active worm index within the current team, **0-based** (stride 0x9C).
802
- pub current_worm: u32,
803
- /// 0x134: arena team index used for TeamArenaState lookups.
804
- pub arena_team: u32,
805
- /// 0x138: arena worm index used for TeamArenaState lookups.
806
- pub arena_worm: u32,
807
- pub _unknown_13c: u32,
808
- /// 0x140: set to 1 at construction; role unclear.
809
- pub _init_140: u32,
810
- pub _unknown_144: u32,
811
- pub _unknown_148: u32,
812
- /// 0x14C: set to 1 at construction and reset by turn-flow restart; role unclear.
813
- pub _init_14c: u32,
814
- /// 0x150: set to 1 when the active worm's firing phase ends and retreat begins.
815
- pub turn_ended: u32,
816
- /// 0x154: set to 1 when the scheme turn time is 0 (no time limit).
817
- pub no_time_limit: u32,
818
- pub _unknown_158: [u8; 0x14],
819
- /// 0x16C: set to 1 once the "5 seconds left" warning sound has played.
820
- pub warning_sound_played: u32,
821
- pub _unknown_170: u32,
822
- /// 0x174: set while a worm is transitioning out of a special state (e.g. landing
823
- /// after a fly animation); cleared once settled.
824
- pub _worm_state_transition: u32,
825
- /// 0x178: retreat-phase countdown (ms). Initialised from `retreat_time_max` when
826
- /// firing ends; decrements 20 ms/frame. Dispatches message 0x35 at zero.
827
- pub retreat_timer: i32,
828
- /// 0x17C: initial retreat duration (ms) loaded from the scheme at turn start.
829
- pub retreat_time_max: i32,
830
- /// 0x180: last tick when the displayed "seconds remaining" value was updated.
831
- pub last_second_tick: u32,
832
- /// 0x184: idle-phase timer (ms). Initialised to `scheme_turn_time Γ— 1000`.
833
- /// Decrements 20 ms/frame **only while `worm_active == 0`** (timer pauses when
834
- /// the worm is moving/shooting). When it reaches zero, sudden-death is triggered.
835
- /// This is NOT the per-turn HUD countdown β€” see `turn_timer` at +0x18C instead.
836
- pub idle_timer: i32,
837
- /// 0x188: smoothed display copy of `turn_timer`; tracks it at ≀ 0xCCC ms/frame
838
- /// to avoid visual jumping on the HUD.
839
- pub turn_timer_display: i32,
840
- /// 0x18C: per-turn countdown timer (ms) β€” this is what the HUD shows.
841
- /// Decrements 20 ms/frame every game frame. When it reaches zero,
842
- /// `FUN_0055f4f0` fires (turn ends / retreat begins).
843
- pub turn_timer: i32,
844
- pub _unknown_190: [u8; 0x8],
845
- /// 0x198: turn-timer visual pulse intensity (Fixed 16.16); grows as time runs low.
846
- pub timer_flash: u32,
847
- pub _unknown_19c: [u8; 0x138],
848
- /// 0x2D4: frame count accumulated while this team held the active worm.
849
- pub active_worm_frames: u32,
850
- /// 0x2D8: frame count accumulated during the retreat phase.
851
- pub retreat_frames: u32,
852
- /// 0x2DC: timer scale factor derived from scheme (2, 3, or 4; default 4).
853
- pub _timer_scale: u32,
854
- }
855
-
856
- const _: () = assert!(core::mem::size_of::<CTaskTurnGame>() == 0x2E0);
857
-
858
- // ============================================================
859
- // Derived task overlays β€” for accessing task-specific fields
860
- // beyond or within CGameTask that differ per task type.
861
- // ============================================================
862
-
863
- /// Bungee trail rendering task fields.
864
- ///
865
- /// Used by DrawBungeeTrail (0x500720). Fields at 0xBC-0xE4 overlap with
866
- /// CGameTask's `_unknown_98` region β€” different task types may use these
867
- /// offsets for different purposes.
868
- ///
869
- /// Cast a task pointer to this type when you know it's a bungee trail task.
870
- #[repr(C)]
871
- pub struct BungeeTrailTask {
872
- /// 0x00-0x2F: CTask base
873
- pub base: CTask,
874
- /// 0x30-0x83: Unknown
875
- pub _unknown_30: [u8; 0x54],
876
- /// 0x84: X position in fixed-point
877
- pub pos_x: Fixed,
878
- /// 0x88: Y position in fixed-point
879
- pub pos_y: Fixed,
880
- /// 0x8C-0xBB: Unknown
881
- pub _unknown_8c: [u8; 0x30],
882
- /// 0xBC: Trail visible flag (set by InitWormTrail when Bungee is used)
883
- pub trail_visible: i32,
884
- /// 0xC0: Trail start X position
885
- pub trail_start_x: i32,
886
- /// 0xC4: Trail start Y position
887
- pub trail_start_y: i32,
888
- /// 0xC8-0xCF: Unknown
889
- pub _unknown_c8: [u8; 8],
890
- /// 0xD0: Number of trail segments
891
- pub segment_count: i32,
892
- /// 0xD4-0xE3: Unknown
893
- pub _unknown_d4: [u8; 0x10],
894
- /// 0xE4: Pointer to segment data array (8 bytes per segment: 4 padding + 4 angle)
895
- pub segment_data: *const u8,
896
- /// 0xE8: Sound emitter sub-object
897
- pub sound_emitter: SoundEmitter,
898
- }
899
-
900
- const _: () = assert!(core::mem::size_of::<BungeeTrailTask>() == 0xFC);
901
-
902
- /// Weapon aiming task fields.
903
- ///
904
- /// Used by DrawCrosshairLine (0x5197D0). Fields at 0x258+ are in the derived
905
- /// class region beyond CGameTask (0xFC). The exact class name is unknown.
906
- ///
907
- /// Cast a task pointer to this type when you know it's a worm/weapon aiming task.
908
- #[repr(C)]
909
- pub struct WeaponAimTask {
910
- /// 0x00-0xFB: CGameTask base
911
- pub game_task: CGameTask,
912
- /// 0xFC-0x257: Unknown derived fields
913
- pub _unknown_fc: [u8; 0x258 - 0xFC],
914
- /// 0x258: Aiming active flag (nonzero = crosshair visible)
915
- pub aim_active: i32,
916
- /// 0x25C-0x263: Unknown
917
- pub _unknown_25c: [u8; 8],
918
- /// 0x264: Current aim angle (used for trig lookup)
919
- pub aim_angle: u32,
920
- /// 0x268-0x323: Unknown
921
- pub _unknown_268: [u8; 0x324 - 0x268],
922
- /// 0x324: Aim range offset (added to DDGame crosshair scale)
923
- pub aim_range_offset: i32,
924
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crates/openwa-core/src/task/base.rs ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::class_type::ClassType;
2
+ use crate::ddgame::DDGame;
3
+
4
+ /// Base task class in WA's entity hierarchy.
5
+ ///
6
+ /// All game objects inherit from CTask. Tasks form a tree via parent/children
7
+ /// pointers and communicate through the TaskMessage system.
8
+ ///
9
+ /// Source: wkJellyWorm CTask.h, Ghidra decompilation of 0x5625A0 + 0x562520
10
+ ///
11
+ /// Vtable at 0x669F8C (8 methods):
12
+ /// 0x00: 0x562710 vtable0 (init?)
13
+ /// 0x04: 0x562620 Free
14
+ /// 0x08: 0x562F30 HandleMessage
15
+ /// 0x0C: 0x5613D0 unknown
16
+ /// 0x10: 0x5613D0 unknown (same as 0x0C)
17
+ /// 0x14: 0x562FA0 unknown
18
+ /// 0x18: 0x563000 unknown
19
+ /// 0x1C: 0x563210 ProcessFrame
20
+ #[repr(C)]
21
+ pub struct CTask {
22
+ /// 0x00: Pointer to virtual method table
23
+ pub vtable: *mut u8,
24
+ /// 0x04: Parent task in the hierarchy
25
+ pub parent: *mut u8,
26
+ /// 0x08: Children list max capacity (set to 0x10 in constructor)
27
+ pub children_max_size: u32,
28
+ /// 0x0C: Children list unknown field (set to 0 in constructor)
29
+ pub children_unk: u32,
30
+ /// 0x10: Children list current size
31
+ pub children_size: u32,
32
+ /// 0x14: Pointer to children data array (allocated 0x60 bytes in constructor)
33
+ pub children_data: *mut u8,
34
+ /// 0x18: Children hash list pointer (set to 0 in constructor)
35
+ pub children_hash: *mut u8,
36
+ /// 0x1C: Unknown (set to 0 by parent-linking helper FUN_00562520)
37
+ pub _unknown_1c: u32,
38
+ /// 0x20: Task classification type (set to ClassType::Task by FUN_00562520,
39
+ /// overridden by derived constructors)
40
+ pub class_type: ClassType,
41
+ /// 0x24: Shared data buffer pointer (inherited from parent, or allocated
42
+ /// 0x420 bytes for root tasks)
43
+ pub shared_data: *mut u8,
44
+ /// 0x28: 1 if this task owns shared_data (root), 0 if inherited from parent
45
+ pub owns_shared_data: u32,
46
+ /// 0x2C: DDGame pointer (3rd param to CTask::Constructor, stored at this+0x2C)
47
+ pub ddgame: *mut DDGame,
48
+ }
49
+
50
+ const _: () = assert!(core::mem::size_of::<CTask>() == 0x30);
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Shared-data entity registry
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /// A 0x30-byte node in CTask's shared-data entity hash table.
57
+ ///
58
+ /// Inserted by `SharedData__Insert` (0x5406A0, called from task constructors).
59
+ /// All game task types (CTaskWorm, CTaskLand, projectiles, …) share the same
60
+ /// 256-bucket table at `CTask.shared_data`. Use the vtable pointer at
61
+ /// `entity[0]` to identify the object type.
62
+ ///
63
+ /// Hash function (from Ghidra decompilation of `SharedData__Insert`):
64
+ /// ```text
65
+ /// bucket = (key_esi * 0x11 + key_edi) & 0x800000ff;
66
+ /// if (int)bucket < 0 { bucket = bucket.wrapping_sub(1) | 0xffffff00; bucket += 1; }
67
+ /// ```
68
+ /// In practice (small positive key values), this reduces to:
69
+ /// `bucket = (key_esi * 0x11 + key_edi) & 0xff`
70
+ ///
71
+ /// Runtime observation: for `CTaskWorm`, `key_esi` encodes a compound worm
72
+ /// identity (e.g. `0x11` = team 1, worm 1) and `key_edi` is a small integer.
73
+ /// Companion remove function: `SharedData__Remove` (0x540700).
74
+ #[repr(C)]
75
+ pub struct SharedDataNode {
76
+ /// +0x00: Next node in this bucket's linked list (null = end).
77
+ pub next: *mut SharedDataNode,
78
+ /// +0x04: EDI register value at registration time.
79
+ pub key_edi: u32,
80
+ /// +0x08: ESI register value at registration time.
81
+ pub key_esi: u32,
82
+ /// +0x0C: Registered entity pointer (first DWORD = vtable).
83
+ pub entity: *mut u8,
84
+ /// +0x10..0x2F: Unused allocation padding.
85
+ pub _padding: [u8; 0x20],
86
+ }
87
+
88
+ const _: () = assert!(core::mem::size_of::<SharedDataNode>() == 0x30);
89
+
90
+ /// View of the 256-bucket entity hash table at `CTask.shared_data`.
91
+ ///
92
+ /// Root tasks own 0x420 bytes of shared data:
93
+ /// - `0x000..0x3FF`: 256 Γ— `*mut SharedDataNode` bucket heads
94
+ /// - `0x400..0x41F`: Other root-task data (layout unknown)
95
+ ///
96
+ /// All tasks in the same game tree inherit the same `shared_data` pointer, so
97
+ /// any task can be used to access the full table. Use [`SharedDataTable::iter`]
98
+ /// to walk all registered entities and filter by vtable address.
99
+ ///
100
+ /// Registered by `SharedData__Insert` (0x5406A0); removed by
101
+ /// `SharedData__Remove` (0x540700).
102
+ pub struct SharedDataTable {
103
+ buckets: *const *mut SharedDataNode,
104
+ }
105
+
106
+ impl SharedDataTable {
107
+ /// Construct from a raw `CTask.shared_data` pointer.
108
+ ///
109
+ /// # Safety
110
+ /// `ptr` must point to a valid shared-data region of at least 256 Γ— 4 = 1024 bytes.
111
+ pub unsafe fn from_ptr(ptr: *mut u8) -> Self {
112
+ Self { buckets: ptr as *const *mut SharedDataNode }
113
+ }
114
+
115
+ /// Construct from a `CTask` pointer (reads `task.shared_data`).
116
+ ///
117
+ /// # Safety
118
+ /// `task` must be a valid, aligned `CTask` pointer.
119
+ pub unsafe fn from_task(task: *const CTask) -> Self {
120
+ Self::from_ptr((*task).shared_data)
121
+ }
122
+
123
+ /// Compute the bucket index for a (key_esi, key_edi) pair.
124
+ ///
125
+ /// Exact transcription of the hash in `FUN_005406a0`.
126
+ pub fn bucket_for(key_esi: u32, key_edi: u32) -> u32 {
127
+ let mut h = key_esi.wrapping_mul(0x11).wrapping_add(key_edi) & 0x800000ff;
128
+ if (h as i32) < 0 {
129
+ h = h.wrapping_sub(1) | 0xffffff00;
130
+ h = h.wrapping_add(1);
131
+ }
132
+ h
133
+ }
134
+
135
+ /// Iterate all nodes across all 256 buckets.
136
+ ///
137
+ /// # Safety
138
+ /// The table and all linked nodes must be valid and not concurrently modified.
139
+ pub unsafe fn iter(&self) -> SharedDataIter {
140
+ SharedDataIter {
141
+ buckets: self.buckets,
142
+ bucket: 0,
143
+ node: core::ptr::null_mut(),
144
+ }
145
+ }
146
+ }
147
+
148
+ /// Iterator over all [`SharedDataNode`]s in a [`SharedDataTable`].
149
+ ///
150
+ /// Created by [`SharedDataTable::iter`]. Walks all 256 buckets in order,
151
+ /// following `next` pointers within each bucket.
152
+ pub struct SharedDataIter {
153
+ buckets: *const *mut SharedDataNode,
154
+ bucket: usize,
155
+ node: *mut SharedDataNode,
156
+ }
157
+
158
+ impl Iterator for SharedDataIter {
159
+ type Item = *mut SharedDataNode;
160
+
161
+ fn next(&mut self) -> Option<Self::Item> {
162
+ // SAFETY: caller of SharedDataTable::iter() guarantees table validity.
163
+ unsafe {
164
+ loop {
165
+ if !self.node.is_null() {
166
+ let current = self.node;
167
+ self.node = (*self.node).next;
168
+ return Some(current);
169
+ }
170
+ if self.bucket >= 256 {
171
+ return None;
172
+ }
173
+ self.node = *self.buckets.add(self.bucket);
174
+ self.bucket += 1;
175
+ }
176
+ }
177
+ }
178
+ }
crates/openwa-core/src/task/cloud.rs ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::fixed::Fixed;
2
+ use super::base::CTask;
3
+
4
+ /// Airstrike / weather cloud task.
5
+ ///
6
+ /// Extends CTask directly (not CGameTask). Clouds drift horizontally with wind,
7
+ /// scroll on a parallax layer, and render as a single sprite.
8
+ ///
9
+ /// Allocation: 0x74 bytes (operator new in CTaskTeam__CreateWeatherFilter).
10
+ /// Constructor: 0x5482E0 (usercall ESI=this, EAX=parent).
11
+ /// Vtable: 0x669D38. Class type byte: 0x17.
12
+ ///
13
+ /// Three cloud sizes chosen by `cloud_type` param (0/1/2):
14
+ /// - type 0: sprite 0x268 (large), vel_y 0x200
15
+ /// - type 1: sprite 0x269 (medium), vel_y 0x166
16
+ /// - type 2: sprite 0x26A (small), vel_y 0xCC
17
+ ///
18
+ /// Source: Ghidra decompilation of 0x5482E0 (constructor) and
19
+ /// 0x5484C0 (HandleMessage update + render branches).
20
+ #[repr(C)]
21
+ pub struct CTaskCloud {
22
+ /// 0x00–0x2F: CTask base
23
+ pub base: CTask,
24
+ /// 0x30: Parallax scroll layer depth (Fixed; 0x190000 = 25.0 at spawn,
25
+ /// decrements by 1 each cloud spawned in a batch)
26
+ pub layer_depth: Fixed,
27
+ /// 0x34: Y position (Fixed 16.16); updated each frame: pos_y += vel_y
28
+ pub pos_y: Fixed,
29
+ /// 0x38: Y velocity (Fixed 16.16; set by cloud type: large=0x200, medium=0x166, small=0xCC)
30
+ pub vel_y: Fixed,
31
+ /// 0x3C: Sprite ID passed to DrawSpriteLocal (0x268=large, 0x269=medium, 0x26A=small)
32
+ pub sprite_id: u32,
33
+ /// 0x40: X position (Fixed 16.16); wraps at landscape bounds each frame
34
+ pub pos_x: Fixed,
35
+ /// 0x44: Unknown (set from EDI at call site)
36
+ pub _unknown_44: u32,
37
+ /// 0x48: X velocity base (Fixed 16.16)
38
+ pub vel_x: Fixed,
39
+ /// 0x4C: Current wind acceleration (Fixed); converges toward wind_target each frame
40
+ pub wind_accel: Fixed,
41
+ /// 0x50: Target wind speed (Fixed); set by message 0x54 (wind-change event)
42
+ pub wind_target: Fixed,
43
+ /// 0x54–0x73: Unknown
44
+ pub _unknown_54: [u8; 0x20],
45
+ }
46
+
47
+ const _: () = assert!(core::mem::size_of::<CTaskCloud>() == 0x74);
crates/openwa-core/src/task/filter.rs ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::base::CTask;
2
+
3
+ /// Message-subscription filter task β€” routes messages selectively to child tasks.
4
+ ///
5
+ /// CTaskFilter is a CTask subclass that overrides HandleMessage to only forward
6
+ /// messages whose type is marked in a 100-entry boolean subscription table. Each
7
+ /// CTaskFilter instance subscribes to a specific set of message IDs at construction
8
+ /// time; all other messages are silently dropped before reaching the subtree.
9
+ ///
10
+ /// **Role in the task tree**: CTaskTeam creates multiple CTaskFilter children per
11
+ /// team during construction. Each filter represents a different event-routing path
12
+ /// (e.g., movement, UI, game-flow, weather). Messages from CTaskTurnGame propagate
13
+ /// down through these filters, which gate access to their subtrees.
14
+ ///
15
+ /// **Allocation size**: 0xB4 bytes (via operator new in factory functions).
16
+ ///
17
+ /// **Constructor**: `CTaskFilter__Constructor` (0x54F3D0, thiscall):
18
+ /// - `init_val_1c`: stored at CTask+0x1C (role unknown)
19
+ /// - `parent_task`: parent in the task tree (determines shared_data)
20
+ ///
21
+ /// **Key vtable methods** (vtable at 0x669DAC):
22
+ /// - [2] `CTaskFilter__HandleMessage` (0x54F4A0): checks subscription table, forwards
23
+ /// only if `msg_type < 100 && subscription_table[msg_type] != 0`
24
+ /// - [7] `CTaskFilter__SubscribeAll` (0x54F390): sets all 100 entries to 1
25
+ /// - [8] `CTaskFilter__Subscribe` (0x54F370): sets `subscription_table[msg_id] = 1`
26
+ ///
27
+ /// **Four factory functions** (all called by `CTaskTeam__Constructor_Maybe` 0x550E70):
28
+ /// - `FUN_00552030`: subscribes to messages 0, 1, 3, 5
29
+ /// - `FUN_005520D0`: subscribes to messages 0, 1, 2, 3, 0x15, 0x18, 0x1C
30
+ /// - `FUN_00552190`: subscribes to messages 0, 1, 2, 3, 5, 0x15, 0x17, 0x1C, 0x2C–0x2E, 0x4B,
31
+ /// and optionally 0x0E (if `GameInfo+0xD778 < -1`)
32
+ /// - `CTaskTeam__CreateWeatherFilter` (0x552960): subscribes to 1, 2, 3, 0x54, then
33
+ /// spawns `CTaskCloud` children using randomised positions (only if `DDGame+0x777C == 0`)
34
+ #[repr(C)]
35
+ pub struct CTaskFilter {
36
+ /// 0x00–0x2F: CTask base.
37
+ ///
38
+ /// Notable base fields set by CTaskFilter__Constructor:
39
+ /// - CTask+0x18 (`_unknown_18`): set to 0
40
+ /// - CTask+0x1C (`_unknown_1c`): set to `init_val_1c` constructor param
41
+ /// - CTask+0x20 (`_unknown_20`): set to 7 (task type / mode constant)
42
+ pub base: CTask,
43
+ /// 0x30–0x93: Boolean subscription table, indexed by message type ID (0–99).
44
+ ///
45
+ /// `subscription_table[id] != 0` means this filter will forward messages of
46
+ /// that type. Cleared to 0 at construction, then populated by Subscribe/SubscribeAll
47
+ /// calls. Max 100 distinct message IDs (IDs >= 100 always pass through).
48
+ pub subscription_table: [u8; 100],
49
+ /// 0x94–0xB3: Unknown (present in 0xB4-byte allocation; not set by constructor).
50
+ pub _unknown_94: [u8; 0x20],
51
+ }
52
+
53
+ const _: () = assert!(core::mem::size_of::<CTaskFilter>() == 0xB4);
crates/openwa-core/src/task/fire.rs ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::base::CTask;
2
+
3
+ /// Fire/flame entity task.
4
+ ///
5
+ /// Extends CTask (not CGameTask) β€” no physics body.
6
+ /// class_type = 0x18. Allocated 0xD8 bytes.
7
+ /// Constructor: CTaskFire__Constructor (0x54F4C0).
8
+ /// vtable: CTaskFire__vtable (0x00669DD8).
9
+ ///
10
+ /// One CTaskFire is spawned per flame sprite. The `timer` field starts
11
+ /// at 0xFFFF and counts down each frame; when it reaches zero the fire
12
+ /// dies. `lifetime` at +0xB1 is a signed byte: 0xFF (= -1i8) means alive,
13
+ /// 0 means the task is being destroyed.
14
+ #[repr(C)]
15
+ pub struct CTaskFire {
16
+ /// 0x00-0x2F: CTask base
17
+ pub base: CTask,
18
+ /// 0x30: spread counter (incremented while fire is spreading)
19
+ pub spread_counter: i32,
20
+ /// 0x34: frame countdown; starts at 0xFFFF, decrements each ProcessFrame
21
+ pub timer: i32,
22
+ /// 0x38: random seed / initial offset for sprite variation
23
+ pub rand_offset: u32,
24
+ /// 0x3C: burn rate / intensity (higher = more damage per frame)
25
+ pub burn_rate: u32,
26
+ pub _unknown_40: u32,
27
+ /// 0x44: spawn X position (Fixed 16.16)
28
+ pub spawn_x: crate::fixed::Fixed,
29
+ /// 0x48: spawn Y position (Fixed 16.16)
30
+ pub spawn_y: crate::fixed::Fixed,
31
+ pub _unknown_4c: [u8; 0x24],
32
+ /// 0x70: absolute tick (frame counter) when this flame was spawned
33
+ pub spawn_time: u32,
34
+ pub _unknown_74: u32,
35
+ /// 0x78-0xA7: per-frame spawn parameter table (12 DWORDs)
36
+ pub spawn_params: [u32; 12],
37
+ /// 0xA8: slot index in the fire-object pool
38
+ pub slot_index: u32,
39
+ pub _unknown_ac: u32,
40
+ pub _flags_b0: u8,
41
+ /// 0xB1: lifetime byte; -1 (0xFF as i8) = alive, 0 = dying/dead
42
+ pub lifetime: i8,
43
+ pub _unknown_b2: [u8; 0x26],
44
+ }
45
+
46
+ const _: () = assert!(core::mem::size_of::<CTaskFire>() == 0xD8);
crates/openwa-core/src/task/game_task.rs ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::fixed::Fixed;
2
+ use super::base::CTask;
3
+
4
+ /// Game task - extends CTask with physics and gameplay data.
5
+ ///
6
+ /// PARTIAL: Most fields between 0x30-0x83 and 0x98-0xE7 are unknown.
7
+ /// Only position and velocity fields have been verified.
8
+ ///
9
+ /// Source: wkJellyWorm CGameTask.h
10
+ ///
11
+ /// Additional vtable (12 methods at offsets 0x1C-0x48 in vtable)
12
+ #[repr(C)]
13
+ pub struct CGameTask {
14
+ /// 0x00-0x2F: Base CTask fields
15
+ pub base: CTask,
16
+ /// 0x30-0x83: Unknown gameplay fields (84 bytes)
17
+ pub _unknown_30: [u8; 0x54],
18
+ /// 0x84: X position in fixed-point
19
+ pub pos_x: Fixed,
20
+ /// 0x88: Y position in fixed-point
21
+ pub pos_y: Fixed,
22
+ /// 0x8C-0x8F: Unknown (4 bytes between pos and speed)
23
+ pub _unknown_8c: [u8; 4],
24
+ /// 0x90: X velocity in fixed-point
25
+ pub speed_x: Fixed,
26
+ /// 0x94: Y velocity in fixed-point
27
+ pub speed_y: Fixed,
28
+ /// 0x98-0xE7: Unknown gameplay fields
29
+ pub _unknown_98: [u8; 0x50],
30
+ /// 0xE8: Embedded sound emitter sub-object (MSVC multiple inheritance).
31
+ pub sound_emitter: SoundEmitter,
32
+ }
33
+
34
+ const _: () = assert!(core::mem::size_of::<CGameTask>() == 0xFC);
35
+
36
+ /// Sound emitter sub-object embedded in CGameTask via MSVC multiple inheritance.
37
+ ///
38
+ /// Provides spatial audio support. The `this` pointer for its vtable methods
39
+ /// points to the start of this sub-object (CGameTask+0xE8), not the CGameTask.
40
+ #[repr(C)]
41
+ pub struct SoundEmitter {
42
+ /// +0x00: Vtable pointer
43
+ pub vtable: *const SoundEmitterVTable,
44
+ /// +0x04-0x0B: Unknown fields
45
+ pub _unknown_04: [u8; 8],
46
+ /// +0x0C: Number of active local sounds
47
+ pub local_sound_count: i32,
48
+ /// +0x10: Back-pointer to containing CGameTask
49
+ pub owner: *mut CGameTask,
50
+ }
51
+
52
+ const _: () = assert!(core::mem::size_of::<SoundEmitter>() == 0x14);
53
+
54
+ /// Vtable for the SoundEmitter sub-object (0x669CF8, 12 slots).
55
+ ///
56
+ /// Slots [0]-[4] are the sound emitter's own interface.
57
+ /// Slots [5]-[11] are inherited CTask base methods.
58
+ #[repr(C)]
59
+ pub struct SoundEmitterVTable {
60
+ /// [0] 0x546680: GetPosition(this, out_x, out_y) β€” reads pos_x/pos_y via owner
61
+ pub get_position: unsafe extern "thiscall" fn(*const SoundEmitter, *mut u32, *mut u32),
62
+ /// [1] 0x5466A0: GetPosition2(this, out_x, out_y) β€” reads CGameTask+0x38/0x3C
63
+ pub get_position2: unsafe extern "thiscall" fn(*const SoundEmitter, *mut u32, *mut u32),
64
+ /// [2] 0x4260E0: Unknown
65
+ pub _unknown_2: *const (),
66
+ /// [3] 0x546990: Destructor(this, flags)
67
+ pub destructor: unsafe extern "thiscall" fn(*mut SoundEmitter, u32) -> *mut SoundEmitter,
68
+ /// [4] 0x546760: HandleMessage β€” sound queue manager
69
+ pub handle_message: unsafe extern "thiscall" fn(*mut SoundEmitter, u32, u32, u32, u32),
70
+ /// [5]-[11]: Inherited CTask base methods
71
+ pub _base_methods: [*const (); 7],
72
+ }
crates/openwa-core/src/task/mine_oil_drum.rs ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::game_task::CGameTask;
2
+
3
+ /// Land mine entity task.
4
+ ///
5
+ /// Extends CGameTask (0xFC bytes). Mines sit on the terrain and arm after
6
+ /// placement; they detonate on contact once armed.
7
+ ///
8
+ /// Constructor: 0x506660 (stdcall).
9
+ /// Vtable: 0x6643E8. Class type byte: 0x08.
10
+ ///
11
+ /// Source: Ghidra decompilation of 0x506660 (constructor) and
12
+ /// 0x5072E0 (HandleMessage, msg 2/0x15/0x1C/0x4B branches).
13
+ #[repr(C)]
14
+ pub struct CTaskMine {
15
+ /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
16
+ pub base: CGameTask,
17
+ /// 0xFC–0x10F: Unknown mine flags
18
+ pub _unknown_fc: [u8; 0x14],
19
+ /// 0x110: Object pool slot index (assigned from DDGame+0x3600 pool)
20
+ pub slot_id: u32,
21
+ /// 0x114–0x11B: Unknown
22
+ pub _unknown_114: [u8; 8],
23
+ /// 0x11C: Fuse timer (signed i32).
24
+ /// Negative = just placed / disarmed.
25
+ /// 0 = armed (will trigger on contact).
26
+ /// Positive = countdown ticks remaining.
27
+ pub fuse_timer: i32,
28
+ /// 0x120–0x123: Unknown (init data param_3[0])
29
+ pub _unknown_120: u32,
30
+ /// 0x124: Owner team index (param_3[6]; -1 = no owner)
31
+ pub owner_team: i32,
32
+ }
33
+
34
+ const _: () = assert!(core::mem::size_of::<CTaskMine>() == 0x128);
35
+
36
+ /// Exploding oil drum entity task.
37
+ ///
38
+ /// Extends CGameTask (0xFC bytes). Oil drums roll on terrain and explode
39
+ /// when hit enough times (health decrements per impact).
40
+ ///
41
+ /// Constructor: 0x504AF0 (thiscall).
42
+ /// Vtable: 0x664338. Class type byte: 0x1E.
43
+ ///
44
+ /// Source: Ghidra decompilation of 0x504AF0 (constructor) and
45
+ /// 0x5050B0 (HandleMessage, msg 2/0x1C branches).
46
+ #[repr(C)]
47
+ pub struct CTaskOilDrum {
48
+ /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
49
+ pub base: CGameTask,
50
+ /// 0xFC: Triggered flag β€” set to 1 on first impact, starts smoke/fire
51
+ pub triggered: u32,
52
+ /// 0x100: Object pool slot index
53
+ pub slot_id: u32,
54
+ /// 0x104: Unknown
55
+ pub _unknown_104: u32,
56
+ /// 0x108: Health (starts at 0x32 = 50; decremented on damage)
57
+ pub health: u32,
58
+ /// 0x10C: Rolling animation counter (increments by 0x4000 per frame while moving)
59
+ pub roll_counter: u32,
60
+ }
61
+
62
+ const _: () = assert!(core::mem::size_of::<CTaskOilDrum>() == 0x110);
63
+
64
+ impl CTaskOilDrum {
65
+ /// Returns true if the drum is on fire (flag at CGameTask+0xB0, inside _unknown_98).
66
+ ///
67
+ /// # Safety
68
+ /// `self` must be a valid, fully-constructed CTaskOilDrum.
69
+ pub unsafe fn on_fire(&self) -> bool {
70
+ let ptr = (self as *const CTaskOilDrum as *const u8).add(0xB0);
71
+ *(ptr as *const u32) != 0
72
+ }
73
+ }
crates/openwa-core/src/task/mod.rs ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ mod base;
2
+ mod cloud;
3
+ mod filter;
4
+ mod fire;
5
+ mod game_task;
6
+ mod mine_oil_drum;
7
+ mod overlays;
8
+ mod team;
9
+ mod turn_game;
10
+ mod worm;
11
+
12
+ pub use base::{CTask, SharedDataIter, SharedDataNode, SharedDataTable};
13
+ pub use cloud::CTaskCloud;
14
+ pub use filter::CTaskFilter;
15
+ pub use fire::CTaskFire;
16
+ pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
17
+ pub use mine_oil_drum::{CTaskMine, CTaskOilDrum};
18
+ pub use overlays::{BungeeTrailTask, WeaponAimTask};
19
+ pub use team::CTaskTeam;
20
+ pub use turn_game::{CTaskTurnGame, TurnGameCtx};
21
+ pub use worm::{CTaskWorm, CTaskWormVTable};
crates/openwa-core/src/task/overlays.rs ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::fixed::Fixed;
2
+ use super::base::CTask;
3
+ use super::game_task::{CGameTask, SoundEmitter};
4
+
5
+ /// Bungee trail rendering task fields.
6
+ ///
7
+ /// Used by DrawBungeeTrail (0x500720). Fields at 0xBC-0xE4 overlap with
8
+ /// CGameTask's `_unknown_98` region β€” different task types may use these
9
+ /// offsets for different purposes.
10
+ ///
11
+ /// Cast a task pointer to this type when you know it's a bungee trail task.
12
+ #[repr(C)]
13
+ pub struct BungeeTrailTask {
14
+ /// 0x00-0x2F: CTask base
15
+ pub base: CTask,
16
+ /// 0x30-0x83: Unknown
17
+ pub _unknown_30: [u8; 0x54],
18
+ /// 0x84: X position in fixed-point
19
+ pub pos_x: Fixed,
20
+ /// 0x88: Y position in fixed-point
21
+ pub pos_y: Fixed,
22
+ /// 0x8C-0xBB: Unknown
23
+ pub _unknown_8c: [u8; 0x30],
24
+ /// 0xBC: Trail visible flag (set by InitWormTrail when Bungee is used)
25
+ pub trail_visible: i32,
26
+ /// 0xC0: Trail start X position
27
+ pub trail_start_x: i32,
28
+ /// 0xC4: Trail start Y position
29
+ pub trail_start_y: i32,
30
+ /// 0xC8-0xCF: Unknown
31
+ pub _unknown_c8: [u8; 8],
32
+ /// 0xD0: Number of trail segments
33
+ pub segment_count: i32,
34
+ /// 0xD4-0xE3: Unknown
35
+ pub _unknown_d4: [u8; 0x10],
36
+ /// 0xE4: Pointer to segment data array (8 bytes per segment: 4 padding + 4 angle)
37
+ pub segment_data: *const u8,
38
+ /// 0xE8: Sound emitter sub-object
39
+ pub sound_emitter: SoundEmitter,
40
+ }
41
+
42
+ const _: () = assert!(core::mem::size_of::<BungeeTrailTask>() == 0xFC);
43
+
44
+ /// Weapon aiming task fields.
45
+ ///
46
+ /// Used by DrawCrosshairLine (0x5197D0). Fields at 0x258+ are in the derived
47
+ /// class region beyond CGameTask (0xFC). The exact class name is unknown.
48
+ ///
49
+ /// Cast a task pointer to this type when you know it's a worm/weapon aiming task.
50
+ #[repr(C)]
51
+ pub struct WeaponAimTask {
52
+ /// 0x00-0xFB: CGameTask base
53
+ pub game_task: CGameTask,
54
+ /// 0xFC-0x257: Unknown derived fields
55
+ pub _unknown_fc: [u8; 0x258 - 0xFC],
56
+ /// 0x258: Aiming active flag (nonzero = crosshair visible)
57
+ pub aim_active: i32,
58
+ /// 0x25C-0x263: Unknown
59
+ pub _unknown_25c: [u8; 8],
60
+ /// 0x264: Current aim angle (used for trig lookup)
61
+ pub aim_angle: u32,
62
+ /// 0x268-0x323: Unknown
63
+ pub _unknown_268: [u8; 0x324 - 0x268],
64
+ /// 0x324: Aim range offset (added to DDGame crosshair scale)
65
+ pub aim_range_offset: i32,
66
+ }
crates/openwa-core/src/task/team.rs ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::fixed::Fixed;
2
+ use super::base::CTask;
3
+
4
+ /// Per-team state-tracker task β€” one instance per team, child of CTaskTurnGame.
5
+ ///
6
+ /// Tracks per-team data: which team number it represents, how many worms were
7
+ /// spawned, and a weapon/item slot table. Registered in the SharedData hash
8
+ /// table with type code 0x15 (21).
9
+ ///
10
+ /// Inheritance: extends CTask directly (CTask base at +0x00).
11
+ /// class_type = 10. Allocation: 0x460 bytes via `operator new` (0x5C0AB8).
12
+ /// Constructor: `CTaskTeam__Constructor` (0x555BB0).
13
+ /// Primary vtable: `CTaskTeam__vtable2` (0x00669EE4).
14
+ /// Secondary interface vtable: 0x00669F00 (at object +0x30).
15
+ ///
16
+ /// Key constructor logic (0x555BB0):
17
+ /// - `team_index` at +0x38: 1-based team number (passed as param_3)
18
+ /// - `_item_slots[0..99]` at +0x88: up to 100 weapon/item IDs loaded from scheme
19
+ /// - `worm_count` at +0x218: number of CTaskWorm children constructed (1-indexed)
20
+ /// - SharedData node (0x30 bytes) registered with key = (team_index, type=0x15)
21
+ #[repr(C)]
22
+ pub struct CTaskTeam {
23
+ /// 0x00–0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
24
+ pub base: CTask,
25
+ /// 0x30: Secondary interface vtable pointer (Ghidra 0x00669F00)
26
+ pub _secondary_vtable: u32,
27
+ /// 0x34: Unknown β€” observed to hold the same value as `team_index` in all runs.
28
+ pub _unknown_34: u32,
29
+ /// 0x38: Team number, **1-based**. Matches TeamArenaState slot index.
30
+ pub team_index: u32,
31
+ /// 0x3C: Unknown β€” observed 0 for team 1, 1 for team 2.
32
+ pub _unknown_3c: u32,
33
+ /// 0x40: Unknown signed integer β€” observed βˆ’40 for team 1, βˆ’20 for team 2.
34
+ pub _unknown_40: i32,
35
+ /// 0x44: Unknown (always 0 in observed runs).
36
+ pub _unknown_44: u32,
37
+ /// 0x48: Number of living worms remaining on this team.
38
+ /// Decrements as worms are eliminated; equals `worm_count` at game start.
39
+ pub alive_worm_count: u32,
40
+ /// 0x4C–0x5B: Unknown.
41
+ pub _unknown_4c: [u8; 16],
42
+ /// 0x5C: Unknown β€” consistently 11 across teams and runs.
43
+ pub _unknown_5c: u32,
44
+ /// 0x60: Index of the last weapon launched by this team (0 = none launched yet).
45
+ /// Field name confirmed by wkJellyWorm: `lastLaunchedWeapon_dword60`.
46
+ pub last_launched_weapon: u32,
47
+ /// 0x64–0x87: Unknown.
48
+ pub _unknown_64: [u8; 36],
49
+ /// 0x88–0x217: Unknown region (100 DWORDs). Observed non-zero values at the start
50
+ /// (+0x88 = team_index, +0x8C/+0x90 = 3) in a 2-worm bot game; purpose unclear.
51
+ pub _unknown_88: [u32; 100],
52
+ /// 0x218: Number of CTaskWorm children constructed for this team.
53
+ pub worm_count: u32,
54
+ /// 0x21C–0x3EB: Unknown.
55
+ pub _unknown_21c: [u8; 0x1D0],
56
+ /// 0x3EC–0x3F3: Unknown flags (observed: 1, 1 then 0xFFFFFFFFΓ—3 at +0x3F4).
57
+ pub _unknown_3ec: [u32; 2],
58
+ /// 0x3F4–0x3FF: Sentinel values (observed: 0xFFFFFFFF in all runs).
59
+ pub _sentinels_3f4: [u32; 3],
60
+ /// 0x400–0x403: Unknown.
61
+ pub _unknown_400: u32,
62
+ /// 0x404: X position (Fixed16.16) β€” per-team, likely spawn or last-worm position.
63
+ pub pos_x: Fixed,
64
+ /// 0x408: Y position (Fixed16.16) β€” per-team, likely spawn or last-worm position.
65
+ pub pos_y: Fixed,
66
+ /// 0x40C: Unknown Fixed16.16 value β€” same for all teams (observed β‰ˆ 666.89).
67
+ pub _unknown_40c: Fixed,
68
+ /// 0x410: Unknown (observed 1 in all runs).
69
+ pub _unknown_410: u32,
70
+ /// 0x414–0x45F: Unknown.
71
+ pub _unknown_414: [u8; 0x4C],
72
+ }
73
+
74
+ const _: () = assert!(core::mem::size_of::<CTaskTeam>() == 0x460);
crates/openwa-core/src/task/turn_game.rs ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::fixed::Fixed;
2
+ use super::base::CTask;
3
+
4
+ /// Embedded intermediate game-context sub-object within `CTaskTurnGame`.
5
+ ///
6
+ /// This is the memory region at `CTaskTurnGame+0x30..+0xDB` (0xAC bytes).
7
+ /// It is initialised by `CTaskTeam__Constructor_Maybe` (0x550EB0), which:
8
+ /// 1. Calls `CTask::Constructor(this, nullptr, ddgame)`
9
+ /// 2. Sets the primary vtable to 0x669E34 and class_type to 5
10
+ /// 3. Sets a **secondary interface vtable** pointer (Ghidra 0x669C44) at +0x30
11
+ /// inside the object (i.e. `TurnGameCtx` base +0x00)
12
+ /// 4. Copies `landscape_height = DDGame+0x5E0` as Fixed16.16 to both
13
+ /// `land_height` and `land_height_2`
14
+ /// 5. Writes -1 sentinels to `_sentinel_18`, `_sentinel_28`, `_sentinel_38`
15
+ /// 6. Writes `team_count = *(byte*)(GameInfo+0x44C)` to `team_count`
16
+ ///
17
+ /// `CTaskTurnGame__Constructor` (0x55B2A0) then overrides the primary vtable
18
+ /// to 0x669F70 and class_type to 6 but **leaves this sub-object intact**.
19
+ ///
20
+ /// The three -1 sentinels at offsets 0x18 / 0x28 / 0x38 are evenly spaced
21
+ /// 0x10 bytes apart. Their role is unknown β€” candidates are water-level
22
+ /// Y-coordinates (initial -1 = "no water"), zone boundaries, or AI markers.
23
+ ///
24
+ /// The final 0x44 bytes (0x68–0xAB, `_unknown_68`) are not initialised by
25
+ /// either constructor; they appear to be set by `FUN_005514d0` or similar
26
+ /// helpers called later. Observed runtime values: +0xA0 = 15, +0xA4/+0xA8
27
+ /// are heap pointers.
28
+ #[repr(C)]
29
+ pub struct TurnGameCtx {
30
+ /// +0x00 (= CTaskTurnGame+0x30): Secondary interface vtable pointer.
31
+ /// Ghidra: 0x00669C44. Set by both constructors; always 0x669C44 at runtime.
32
+ pub _secondary_vtable: u32,
33
+ /// +0x04: Unknown β€” not set by constructors (remains 0).
34
+ pub _unknown_04: u32,
35
+ /// +0x08–0x0F: Unknown β€” explicitly zeroed by constructor.
36
+ pub _unknown_08: [u32; 2],
37
+ /// +0x10: Landscape height as Fixed16.16. `DDGame+0x5E0 << 16`.
38
+ pub land_height: Fixed,
39
+ /// +0x14: Landscape height duplicate β€” same value as `land_height`.
40
+ pub land_height_2: Fixed,
41
+ /// +0x18: Sentinel, always -1 at construction. Role unknown.
42
+ pub _sentinel_18: i32,
43
+ /// +0x1C–0x27: Unknown β€” explicitly zeroed by constructor.
44
+ pub _unknown_1c: [u32; 3],
45
+ /// +0x28: Sentinel, always -1 at construction. Role unknown.
46
+ pub _sentinel_28: i32,
47
+ /// +0x2C–0x37: Unknown β€” explicitly zeroed by constructor.
48
+ pub _unknown_2c: [u32; 3],
49
+ /// +0x38: Sentinel, always -1 at construction. Role unknown.
50
+ pub _sentinel_38: i32,
51
+ /// +0x3C–0x4B: Unknown β€” explicitly zeroed by constructor.
52
+ pub _unknown_3c: [u32; 4],
53
+ /// +0x4C: Number of teams. `*(byte*)(GameInfo+0x44C)` at construction time.
54
+ pub team_count: u32,
55
+ /// +0x50–0x64: Unknown β€” explicitly zeroed by constructor (param_1[0x20..0x25]).
56
+ pub _unknown_50: [u32; 6],
57
+ /// +0x68–0x9F: Unknown β€” all explicitly zeroed by `FUN_005514d0` during game
58
+ /// setup. Purpose unknown; may be per-team or per-worm state slots.
59
+ pub _unknown_68: [u32; 14],
60
+ /// +0xA0: Return value of `FUN_00525f50(0)` stored here during game setup.
61
+ /// Observed value: 15. `FUN_00525f50` is a slot-allocation helper; this may
62
+ /// be a pool slot index or a pre-computed game-state token.
63
+ pub _slot_d0: u32,
64
+ /// +0xA4: `DDDisplay` textbox handle β€” created by `DDDisplay__ConstructTextbox`
65
+ /// with params `(buf, -1280, 2)` if `DDGame+0x7EF8 != 0` (display active).
66
+ /// Likely the HUD timer textbox. NULL when display is disabled.
67
+ pub _hud_textbox_a: u32,
68
+ /// +0xA8: `DDDisplay` textbox handle β€” created with params `(buf, 8, 4)`.
69
+ /// Likely a secondary HUD element. NULL when display is disabled.
70
+ pub _hud_textbox_b: u32,
71
+ }
72
+
73
+ const _: () = assert!(core::mem::size_of::<TurnGameCtx>() == 0xAC);
74
+
75
+ /// Root turn-controller task β€” one instance per game, parent of the entire entity tree.
76
+ ///
77
+ /// Every worm, team, projectile, and environment task is a child (direct or indirect)
78
+ /// of this node. `CTaskTurnGame` drives the turn loop: it processes 50 game frames
79
+ /// per second via `CTaskTurnGame__TurnManager_ProcessFrame` (0x55FDA0), which is
80
+ /// called from HandleMessage case 2 (FrameFinish).
81
+ ///
82
+ /// Inheritance: CTask β†’ CTaskTeam β†’ CTaskTurnGame. Class type 6.
83
+ /// Constructor: `CTaskTurnGame__Constructor` (0x55B2B1).
84
+ /// vtable: `CTaskTurnGame__vtable` (0x00669F70), 20 slots.
85
+ /// Total size: 0x2E0 bytes.
86
+ ///
87
+ /// Key vtable slots:
88
+ /// [0] 0x55B5E0 β€” task-tree state snapshot serialiser
89
+ /// [1] 0x55B540 β€” destructor / Free
90
+ /// [2] 0x55DC00 β€” HandleMessage (30+ message types)
91
+ /// [3] 0x5612E0 β€” HUD data query (responds to msg 0x7D3)
92
+ ///
93
+ /// All timers decrement by 20 ms per frame (= 1000 ms / 50 fps).
94
+ #[repr(C)]
95
+ pub struct CTaskTurnGame {
96
+ /// 0x00-0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
97
+ pub base: CTask,
98
+ /// 0x30–0xDB: Embedded `TurnGameCtx` sub-object (0xAC bytes).
99
+ /// See [`TurnGameCtx`] for field details.
100
+ pub game_ctx: TurnGameCtx,
101
+
102
+ // ---- CTaskTurnGame-specific fields (0xDC onwards) ----
103
+
104
+ pub _unknown_dc: u32,
105
+ pub _unknown_e0: u32,
106
+ /// 0xE4: turn-seed: ~(DDGame+0x45EC % 9000), used for random initialisation.
107
+ pub _turn_seed: u32,
108
+ /// 0xE8: number of teams in this game (copy of DDGame+0x44C).
109
+ pub num_teams: u32,
110
+ pub _unknown_ec: [u8; 0x1C],
111
+ /// 0x108: "worm active" flag β€” non-zero while the current worm is shooting or
112
+ /// moving. While non-zero, `turn_timer` is paused.
113
+ /// A copy is also written to DDGame+0x7234 at construction.
114
+ pub worm_active: u32,
115
+ pub _unknown_10c: [u8; 0x20],
116
+ /// 0x12C: active team index, **1-based** (0 = no active team).
117
+ /// Used to index per-team sound tables in DDGame (stride 0xF0 at DDGame+0x774C).
118
+ pub current_team: u32,
119
+ /// 0x130: active worm index within the current team, **0-based** (stride 0x9C).
120
+ pub current_worm: u32,
121
+ /// 0x134: arena team index used for TeamArenaState lookups.
122
+ pub arena_team: u32,
123
+ /// 0x138: arena worm index used for TeamArenaState lookups.
124
+ pub arena_worm: u32,
125
+ pub _unknown_13c: u32,
126
+ /// 0x140: set to 1 at construction; role unclear.
127
+ pub _init_140: u32,
128
+ pub _unknown_144: u32,
129
+ pub _unknown_148: u32,
130
+ /// 0x14C: set to 1 at construction and reset by turn-flow restart; role unclear.
131
+ pub _init_14c: u32,
132
+ /// 0x150: set to 1 when the active worm's firing phase ends and retreat begins.
133
+ pub turn_ended: u32,
134
+ /// 0x154: set to 1 when the scheme turn time is 0 (no time limit).
135
+ pub no_time_limit: u32,
136
+ pub _unknown_158: [u8; 0x14],
137
+ /// 0x16C: set to 1 once the "5 seconds left" warning sound has played.
138
+ pub warning_sound_played: u32,
139
+ pub _unknown_170: u32,
140
+ /// 0x174: set while a worm is transitioning out of a special state (e.g. landing
141
+ /// after a fly animation); cleared once settled.
142
+ pub _worm_state_transition: u32,
143
+ /// 0x178: retreat-phase countdown (ms). Initialised from `retreat_time_max` when
144
+ /// firing ends; decrements 20 ms/frame. Dispatches message 0x35 at zero.
145
+ pub retreat_timer: i32,
146
+ /// 0x17C: initial retreat duration (ms) loaded from the scheme at turn start.
147
+ pub retreat_time_max: i32,
148
+ /// 0x180: last tick when the displayed "seconds remaining" value was updated.
149
+ pub last_second_tick: u32,
150
+ /// 0x184: idle-phase timer (ms). Initialised to `scheme_turn_time Γ— 1000`.
151
+ /// Decrements 20 ms/frame **only while `worm_active == 0`** (timer pauses when
152
+ /// the worm is moving/shooting). When it reaches zero, sudden-death is triggered.
153
+ /// This is NOT the per-turn HUD countdown β€” see `turn_timer` at +0x18C instead.
154
+ pub idle_timer: i32,
155
+ /// 0x188: smoothed display copy of `turn_timer`; tracks it at ≀ 0xCCC ms/frame
156
+ /// to avoid visual jumping on the HUD.
157
+ pub turn_timer_display: i32,
158
+ /// 0x18C: per-turn countdown timer (ms) β€” this is what the HUD shows.
159
+ /// Decrements 20 ms/frame every game frame. When it reaches zero,
160
+ /// `FUN_0055f4f0` fires (turn ends / retreat begins).
161
+ pub turn_timer: i32,
162
+ pub _unknown_190: [u8; 0x8],
163
+ /// 0x198: turn-timer visual pulse intensity (Fixed 16.16); grows as time runs low.
164
+ pub timer_flash: u32,
165
+ pub _unknown_19c: [u8; 0x138],
166
+ /// 0x2D4: frame count accumulated while this team held the active worm.
167
+ pub active_worm_frames: u32,
168
+ /// 0x2D8: frame count accumulated during the retreat phase.
169
+ pub retreat_frames: u32,
170
+ /// 0x2DC: timer scale factor derived from scheme (2, 3, or 4; default 4).
171
+ pub _timer_scale: u32,
172
+ }
173
+
174
+ const _: () = assert!(core::mem::size_of::<CTaskTurnGame>() == 0x2E0);
crates/openwa-core/src/task/worm.rs ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::base::CTask;
2
+ use super::game_task::CGameTask;
3
+
4
+ /// Virtual method table for CTaskWorm (vtable at 0x6644C8, 20 slots).
5
+ ///
6
+ /// CTaskWorm overrides 14 of the 20 inherited CTask/CGameTask slots;
7
+ /// 6 slots pass through unchanged. Slot layout by vtable byte offset:
8
+ ///
9
+ /// ```text
10
+ /// 0x00 WriteReplayState 0x04 Free 0x08 HandleMessage
11
+ /// 0x0C GetEntityData 0x10-0x18 inherited 0x1C OnContactEntity
12
+ /// 0x20 OnWormPush 0x24 OnLandBounce 0x28 OnLandSlide
13
+ /// 0x2C OnSink 0x30 inherited 0x34 OnKilled
14
+ /// 0x38 SetState 0x3C CheckPendingAction 0x40 IsNotOnRope
15
+ /// 0x44 inherited 0x48 GetTeamIndex 0x4C inherited
16
+ /// ```
17
+ #[repr(C)]
18
+ pub struct CTaskWormVTable {
19
+ /// [0] 0x0050CAA0: WriteReplayState β€” serializes worm state to a replay stream;
20
+ /// writes entity type byte 0x12, then packs field groups based on current state
21
+ pub write_replay_state: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut u8),
22
+ /// [1] 0x0050C7E0: Free β€” calls inner destructor, then `_free(this)` if flags & 1
23
+ pub free: unsafe extern "thiscall" fn(*mut CTaskWorm, u8) -> *mut CTaskWorm,
24
+ /// [2] 0x00510B40: HandleMessage β€” processes all TaskMessages sent to this worm
25
+ pub handle_message:
26
+ unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CTask, u32, u32, *const u8),
27
+ /// [3] 0x00516780: GetEntityData β€” returns worm data by query code:
28
+ /// 0x7D0=(pos_x, pos_y), 0x7D1/0x7D2=collision test, 0x7D4=full state dump
29
+ pub get_entity_data: unsafe extern "thiscall" fn(*mut CTaskWorm, u32, u32, *mut u32) -> u32,
30
+ /// [4]-[6]: Inherited CTask stubs (not overridden by CTaskWorm)
31
+ pub _inherited_4_to_6: [*const (); 3],
32
+ /// [7] 0x0050D5D0: OnContactEntity β€” handles physical contact with another entity;
33
+ /// worm-on-dying-worm β†’ apply crush damage; projectile close-pass β†’ SetState(0x80)
34
+ pub on_contact_entity:
35
+ unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
36
+ /// [8] 0x0050D9A0: OnWormPush β€” post-contact worm-worm push impulse;
37
+ /// deduplicates via a recent-collision table, then adjusts pos_x or pos_y
38
+ pub on_worm_push: unsafe extern "thiscall" fn(*mut CTaskWorm, *mut CGameTask, u32) -> u32,
39
+ /// [9] 0x0050D810: OnLandBounce β€” worm lands on terrain; plays thud sound, bounce physics
40
+ pub on_land_bounce: unsafe extern "thiscall" fn(*mut CTaskWorm),
41
+ /// [10] 0x0050D820: OnLandSlide β€” secondary landing callback; sliding/friction physics
42
+ pub on_land_slide: unsafe extern "thiscall" fn(*mut CTaskWorm),
43
+ /// [11] 0x0050D570: OnSink β€” worm sinks in water/acid; applies (dx, dy) displacement,
44
+ /// transitions to drowning state (0x7F) unless already flying/rope/dead
45
+ pub on_sink: unsafe extern "thiscall" fn(*mut CTaskWorm, i32, i32) -> u32,
46
+ /// [12]: Inherited (vtable30 from CGameTask β€” not overridden)
47
+ pub _inherited_12: *const (),
48
+ /// [13] 0x0050D3B0: OnKilled β€” worm death; plays death sound (0x3A),
49
+ /// transitions to dead state (0x81 or 0x89) based on game round count
50
+ pub on_killed: unsafe extern "thiscall" fn(*mut CTaskWorm),
51
+ /// [14] 0x0050E850: SetState β€” worm state machine; handles all state transitions
52
+ /// (0x65=idle, 0x67=active turn, 0x7F=drowning, 0x80=hurt, 0x81/0x86=dead, …)
53
+ pub set_state: unsafe extern "thiscall" fn(*mut CTaskWorm, u32),
54
+ /// [15] 0x00516900: CheckPendingAction β€” if field +0xBC is set, calls SetState(0x73)
55
+ pub check_pending_action: unsafe extern "thiscall" fn(*mut CTaskWorm),
56
+ /// [16] 0x00516920: IsNotOnRope β€” returns true if worm state != 0x7C (rope-swinging)
57
+ pub is_not_on_rope: unsafe extern "thiscall" fn(*const CTaskWorm) -> bool,
58
+ /// [17]: Inherited (vtable44 from CGameTask β€” not overridden)
59
+ pub _inherited_17: *const (),
60
+ /// [18] 0x005168F0: GetTeamIndex β€” returns worm's team index (field +0xFC)
61
+ pub get_team_index: unsafe extern "thiscall" fn(*const CTaskWorm) -> u32,
62
+ /// [19]: Inherited (vtable4C from CGameTask β€” not overridden)
63
+ pub _inherited_19: *const (),
64
+ }
65
+
66
+ const _: () = assert!(core::mem::size_of::<CTaskWormVTable>() == 20 * 4);
67
+
68
+ /// Worm entity task β€” the primary playable character in WA.
69
+ ///
70
+ /// Extends CGameTask (0xFC bytes) with worm identity, physics overrides, and
71
+ /// per-worm state. Total size: 0x3FC bytes.
72
+ ///
73
+ /// Constructor: 0x50BFB0 (stdcall, 5 params):
74
+ /// this, parent_task, team_index, worm_index, init_data_ptr
75
+ ///
76
+ /// Vtable at 0x6644C8. Class type byte: 0x12.
77
+ ///
78
+ /// # Important fields in the CGameTask base
79
+ /// The worm state field lives at **offset +0x44** (inside `base._unknown_30`).
80
+ /// Use [`CTaskWorm::state`] to read it without pointer arithmetic.
81
+ ///
82
+ /// Source: Ghidra decompilation of 0x50BFB0, vtable analysis of 0x6644C8,
83
+ /// wkJellyWorm CTaskWorm.h
84
+ #[repr(C)]
85
+ pub struct CTaskWorm {
86
+ /// 0x00–0xFB: CGameTask base (position, velocity, sound emitter, etc.)
87
+ pub base: CGameTask,
88
+
89
+ /// 0xFC: Team index (0-based); 3rd constructor param
90
+ pub team_index: u32,
91
+ /// 0x100: Worm index within team (0-based); 4th constructor param
92
+ pub worm_index: u32,
93
+ /// 0x104: Unknown flag (checked in OnContactEntity)
94
+ pub _unknown_104: u32,
95
+ /// 0x108–0x10F: Unknown
96
+ pub _unknown_108: [u8; 8],
97
+ /// 0x110–0x137: Ten u32s copied from spawn init_data (5th constructor param)
98
+ pub spawn_params: [u32; 10],
99
+ /// 0x138–0x14F: Unknown
100
+ pub _unknown_138: [u8; 0x18],
101
+ /// 0x150: Unknown (slot 9 in GetEntityData query 0x7D4 output)
102
+ pub _unknown_150: u32,
103
+ /// 0x154: Unknown (rope-related; cleared in some SetState transitions)
104
+ pub _unknown_154: u32,
105
+ /// 0x158: Worm pool slot index in DDGame (assigned from pool at construction)
106
+ pub slot_id: u32,
107
+ /// 0x15C–0x247: Unknown
108
+ pub _unknown_15c: [u8; 0xEC],
109
+ /// 0x248–0x25B: Aiming / weapon data (slots 4–8 in GetEntityData query 0x7D4)
110
+ pub _unknown_248: [u32; 5],
111
+ /// 0x25C–0x283: Unknown
112
+ pub _unknown_25c: [u8; 0x28],
113
+ /// 0x284–0x28B: Aiming data (slots 5–6 in GetEntityData query 0x7D4)
114
+ pub _unknown_284: [u32; 2],
115
+ /// 0x28C–0x2EF: Unknown
116
+ pub _unknown_28c: [u8; 0x64],
117
+ /// 0x2F0: Worm name, null-terminated (max 17 chars, from spawn init_data+3)
118
+ pub worm_name: [u8; 0x11],
119
+ /// 0x301: Country / team name from scheme, null-terminated (max 17 chars)
120
+ pub country_name: [u8; 0x11],
121
+ /// 0x312–0x367: Unknown (rope string, state history, etc.)
122
+ pub _unknown_312: [u8; 0x56],
123
+ /// 0x368: Animator / controller object (dispatched via vtable for state animations)
124
+ pub animator: *mut u8,
125
+ /// 0x36C–0x3FB: Unknown (rope anchor, weapon-specific data, etc.)
126
+ pub _unknown_36c: [u8; 0x90],
127
+ }
128
+
129
+ const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
130
+
131
+ impl CTaskWorm {
132
+ /// Returns the worm's current state code (lives at offset +0x44, inside the
133
+ /// CGameTask base's `_unknown_30` padding region).
134
+ ///
135
+ /// Known states: `0x65`=idle, `0x67`=active turn, `0x7F`=drowning,
136
+ /// `0x80`=hurt, `0x81`/`0x86`=dead, `0x87`=dead variant, `0x8B`=unknown.
137
+ pub fn state(&self) -> u32 {
138
+ // SAFETY: offset 0x44 is within CGameTask._unknown_30 (0x30..0x84).
139
+ // Aligned to 4 bytes; repr(C) guarantees no reordering.
140
+ unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
141
+ }
142
+
143
+ /// Returns a reference to the vtable.
144
+ ///
145
+ /// # Safety
146
+ /// The vtable pointer at offset 0 must be the genuine CTaskWorm vtable
147
+ /// (0x6644C8 in Ghidra, rebased at runtime).
148
+ pub unsafe fn vtable(&self) -> &'static CTaskWormVTable {
149
+ &*(self.base.base.vtable as *const CTaskWormVTable)
150
+ }
151
+ }