paavohtl Claude Opus 4.6 (1M context) commited on
Commit
c6d4e73
·
1 Parent(s): bbd8297

feat: add typed vtables to all CTask subclasses

Browse files

Extend the generic CTask<V>/CGameTask<V> vtable pattern to every task class:

New vtable definitions (via #[vtable] macro, with handle_message + process_frame):
- CTaskTurnGameVTable (0x669F70, 12 slots)
- CTaskFilterVTable (0x669DAC, 12 slots)
- CTaskCloudVTable (0x669D38, 12 slots)
- CTaskFireVTable (0x669DD8, 12 slots)
- CTaskMissileVTable (0x664438, 12 slots)
- CTaskArrowVTable (0x664198, 12 slots)
- CTaskMineVTable (0x6643E8, 12 slots)
- CTaskOilDrumVTable (0x664338, 12 slots)
- CTaskCrateVTable (0x664298, 12 slots)

Each struct now uses typed base: CTask<*const XxxVTable> or
CGameTask<*const XxxVTable>, with bind macro for method dispatch.

CGameTask made generic (V parameter with *const c_void default).
CTaskWorm updated to use CGameTask<*const CTaskWormVTable>.

FieldRegistry derive macro: improved generic substitution to handle
nested types like CTask<V> (recursive type AST walking).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/task/arrow.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  use super::game_task::CGameTask;
2
  use crate::FieldRegistry;
3
 
@@ -8,6 +9,21 @@ crate::define_addresses! {
8
  }
9
  }
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  /// Arrow/bullet projectile entity (Shotgun, Longbow).
12
  ///
13
  /// Extends CGameTask (0xFC bytes). Allocated size: 0x168 bytes.
@@ -20,10 +36,13 @@ crate::define_addresses! {
20
  #[repr(C)]
21
  pub struct CTaskArrow {
22
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94).
23
- pub base: CGameTask,
24
 
25
  /// 0xFC–0x167: Arrow-specific fields. Layout unknown.
26
  pub _unknown_fc: [u8; 0x168 - 0xFC],
27
  }
28
 
29
  const _: () = assert!(core::mem::size_of::<CTaskArrow>() == 0x168);
 
 
 
 
1
+ use super::base::CTask;
2
  use super::game_task::CGameTask;
3
  use crate::FieldRegistry;
4
 
 
9
  }
10
  }
11
 
12
+ /// CTaskArrow vtable — 12 slots. Extends CGameTask vtable with arrow behavior.
13
+ ///
14
+ /// Vtable at Ghidra 0x664198.
15
+ #[openwa_core::vtable(size = 12, va = 0x0066_4198, class = "CTaskArrow")]
16
+ pub struct CTaskArrowVTable {
17
+ /// HandleMessage — processes arrow messages.
18
+ /// thiscall + 4 stack params, RET 0x10.
19
+ #[slot(2)]
20
+ pub handle_message: fn(this: *mut CTaskArrow, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
21
+ /// ProcessFrame — per-frame arrow update.
22
+ /// thiscall + 1 stack param (flags), RET 0x4.
23
+ #[slot(7)]
24
+ pub process_frame: fn(this: *mut CTaskArrow, flags: u32),
25
+ }
26
+
27
  /// Arrow/bullet projectile entity (Shotgun, Longbow).
28
  ///
29
  /// Extends CGameTask (0xFC bytes). Allocated size: 0x168 bytes.
 
36
  #[repr(C)]
37
  pub struct CTaskArrow {
38
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94).
39
+ pub base: CGameTask<*const CTaskArrowVTable>,
40
 
41
  /// 0xFC–0x167: Arrow-specific fields. Layout unknown.
42
  pub _unknown_fc: [u8; 0x168 - 0xFC],
43
  }
44
 
45
  const _: () = assert!(core::mem::size_of::<CTaskArrow>() == 0x168);
46
+
47
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
48
+ bind_CTaskArrowVTable!(CTaskArrow, base.base.vtable);
crates/openwa-core/src/task/cloud.rs CHANGED
@@ -10,6 +10,21 @@ crate::define_addresses! {
10
  }
11
  }
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  /// Airstrike / weather cloud task.
14
  ///
15
  /// Extends CTask directly (not CGameTask). Clouds drift horizontally with wind,
@@ -30,7 +45,7 @@ crate::define_addresses! {
30
  #[repr(C)]
31
  pub struct CTaskCloud {
32
  /// 0x00–0x2F: CTask base
33
- pub base: CTask,
34
  /// 0x30: Parallax scroll layer depth (Fixed; 0x190000 = 25.0 at spawn,
35
  /// decrements by 1 each cloud spawned in a batch)
36
  pub layer_depth: Fixed,
@@ -55,3 +70,6 @@ pub struct CTaskCloud {
55
  }
56
 
57
  const _: () = assert!(core::mem::size_of::<CTaskCloud>() == 0x74);
 
 
 
 
10
  }
11
  }
12
 
13
+ /// CTaskCloud vtable — 12 slots. Extends CTask base (8 slots) with cloud behavior.
14
+ ///
15
+ /// Vtable at Ghidra 0x669D38.
16
+ #[openwa_core::vtable(size = 12, va = 0x0066_9D38, class = "CTaskCloud")]
17
+ pub struct CTaskCloudVTable {
18
+ /// HandleMessage — processes cloud messages (wind updates, render).
19
+ /// thiscall + 4 stack params, RET 0x10.
20
+ #[slot(2)]
21
+ pub handle_message: fn(this: *mut CTaskCloud, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
22
+ /// ProcessFrame — per-frame cloud update.
23
+ /// thiscall + 1 stack param (flags), RET 0x4.
24
+ #[slot(7)]
25
+ pub process_frame: fn(this: *mut CTaskCloud, flags: u32),
26
+ }
27
+
28
  /// Airstrike / weather cloud task.
29
  ///
30
  /// Extends CTask directly (not CGameTask). Clouds drift horizontally with wind,
 
45
  #[repr(C)]
46
  pub struct CTaskCloud {
47
  /// 0x00–0x2F: CTask base
48
+ pub base: CTask<*const CTaskCloudVTable>,
49
  /// 0x30: Parallax scroll layer depth (Fixed; 0x190000 = 25.0 at spawn,
50
  /// decrements by 1 each cloud spawned in a batch)
51
  pub layer_depth: Fixed,
 
70
  }
71
 
72
  const _: () = assert!(core::mem::size_of::<CTaskCloud>() == 0x74);
73
+
74
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
75
+ bind_CTaskCloudVTable!(CTaskCloud, base.vtable);
crates/openwa-core/src/task/filter.rs CHANGED
@@ -9,6 +9,22 @@ crate::define_addresses! {
9
  }
10
  }
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  /// Message-subscription filter task — routes messages selectively to child tasks.
13
  ///
14
  /// CTaskFilter is a CTask subclass that overrides HandleMessage to only forward
@@ -49,7 +65,7 @@ pub struct CTaskFilter {
49
  /// - CTask+0x18 (`_unknown_18`): set to 0
50
  /// - CTask+0x1C (`_unknown_1c`): set to `init_val_1c` constructor param
51
  /// - CTask+0x20 (`_unknown_20`): set to 7 (task type / mode constant)
52
- pub base: CTask,
53
  /// 0x30–0x93: Boolean subscription table, indexed by message type ID (0–99).
54
  ///
55
  /// `subscription_table[id] != 0` means this filter will forward messages of
@@ -61,3 +77,6 @@ pub struct CTaskFilter {
61
  }
62
 
63
  const _: () = assert!(core::mem::size_of::<CTaskFilter>() == 0xB4);
 
 
 
 
9
  }
10
  }
11
 
12
+ /// CTaskFilter vtable — 12 slots. Extends CTask base (8 slots) with filter behavior.
13
+ ///
14
+ /// Vtable at Ghidra 0x669DAC. Slot 2 (HandleMessage) checks the subscription
15
+ /// table before forwarding messages to children.
16
+ #[openwa_core::vtable(size = 12, va = 0x0066_9DAC, class = "CTaskFilter")]
17
+ pub struct CTaskFilterVTable {
18
+ /// HandleMessage — checks subscription table, forwards matching messages.
19
+ /// thiscall + 4 stack params, RET 0x10.
20
+ #[slot(2)]
21
+ pub handle_message: fn(this: *mut CTaskFilter, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
22
+ /// ProcessFrame — per-frame filter update.
23
+ /// thiscall + 1 stack param (flags), RET 0x4.
24
+ #[slot(7)]
25
+ pub process_frame: fn(this: *mut CTaskFilter, flags: u32),
26
+ }
27
+
28
  /// Message-subscription filter task — routes messages selectively to child tasks.
29
  ///
30
  /// CTaskFilter is a CTask subclass that overrides HandleMessage to only forward
 
65
  /// - CTask+0x18 (`_unknown_18`): set to 0
66
  /// - CTask+0x1C (`_unknown_1c`): set to `init_val_1c` constructor param
67
  /// - CTask+0x20 (`_unknown_20`): set to 7 (task type / mode constant)
68
+ pub base: CTask<*const CTaskFilterVTable>,
69
  /// 0x30–0x93: Boolean subscription table, indexed by message type ID (0–99).
70
  ///
71
  /// `subscription_table[id] != 0` means this filter will forward messages of
 
77
  }
78
 
79
  const _: () = assert!(core::mem::size_of::<CTaskFilter>() == 0xB4);
80
+
81
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
82
+ bind_CTaskFilterVTable!(CTaskFilter, base.vtable);
crates/openwa-core/src/task/fire.rs CHANGED
@@ -9,6 +9,21 @@ crate::define_addresses! {
9
  }
10
  }
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  /// Fire/flame entity task.
13
  ///
14
  /// Extends CTask (not CGameTask) — no physics body.
@@ -24,7 +39,7 @@ crate::define_addresses! {
24
  #[repr(C)]
25
  pub struct CTaskFire {
26
  /// 0x00-0x2F: CTask base
27
- pub base: CTask,
28
  /// 0x30: spread counter (incremented while fire is spreading)
29
  pub spread_counter: i32,
30
  /// 0x34: frame countdown; starts at 0xFFFF, decrements each ProcessFrame
@@ -54,3 +69,6 @@ pub struct CTaskFire {
54
  }
55
 
56
  const _: () = assert!(core::mem::size_of::<CTaskFire>() == 0xD8);
 
 
 
 
9
  }
10
  }
11
 
12
+ /// CTaskFire vtable — 12 slots. Extends CTask base (8 slots) with fire behavior.
13
+ ///
14
+ /// Vtable at Ghidra 0x669DD8.
15
+ #[openwa_core::vtable(size = 12, va = 0x0066_9DD8, class = "CTaskFire")]
16
+ pub struct CTaskFireVTable {
17
+ /// HandleMessage — processes fire messages.
18
+ /// thiscall + 4 stack params, RET 0x10.
19
+ #[slot(2)]
20
+ pub handle_message: fn(this: *mut CTaskFire, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
21
+ /// ProcessFrame — per-frame fire update (countdown, spread, damage).
22
+ /// thiscall + 1 stack param (flags), RET 0x4.
23
+ #[slot(7)]
24
+ pub process_frame: fn(this: *mut CTaskFire, flags: u32),
25
+ }
26
+
27
  /// Fire/flame entity task.
28
  ///
29
  /// Extends CTask (not CGameTask) — no physics body.
 
39
  #[repr(C)]
40
  pub struct CTaskFire {
41
  /// 0x00-0x2F: CTask base
42
+ pub base: CTask<*const CTaskFireVTable>,
43
  /// 0x30: spread counter (incremented while fire is spreading)
44
  pub spread_counter: i32,
45
  /// 0x34: frame countdown; starts at 0xFFFF, decrements each ProcessFrame
 
69
  }
70
 
71
  const _: () = assert!(core::mem::size_of::<CTaskFire>() == 0xD8);
72
+
73
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
74
+ bind_CTaskFireVTable!(CTaskFire, base.vtable);
crates/openwa-core/src/task/game_task.rs CHANGED
@@ -31,9 +31,9 @@ pub const CGAMETASK_SOUND_EMITTER_VT: u32 = SOUND_EMITTER_VTABLE;
31
  /// Additional vtable (12 methods at offsets 0x1C-0x48 in vtable)
32
  #[derive(FieldRegistry)]
33
  #[repr(C)]
34
- pub struct CGameTask {
35
  /// 0x00-0x2F: Base CTask fields
36
- pub base: CTask,
37
  /// 0x30-0x83: Subclass-specific data (84 bytes). Each CGameTask derivative
38
  /// uses this region differently:
39
  /// - CTaskWorm: weapon fire state (+0x30 type, +0x34/+0x38 subtypes, +0x3C flag)
 
31
  /// Additional vtable (12 methods at offsets 0x1C-0x48 in vtable)
32
  #[derive(FieldRegistry)]
33
  #[repr(C)]
34
+ pub struct CGameTask<V: 'static = *const core::ffi::c_void> {
35
  /// 0x00-0x2F: Base CTask fields
36
+ pub base: CTask<V>,
37
  /// 0x30-0x83: Subclass-specific data (84 bytes). Each CGameTask derivative
38
  /// uses this region differently:
39
  /// - CTaskWorm: weapon fire state (+0x30 type, +0x34/+0x38 subtypes, +0x3C flag)
crates/openwa-core/src/task/mine_oil_drum.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  use super::game_task::CGameTask;
2
  use crate::FieldRegistry;
3
 
@@ -15,6 +16,36 @@ crate::define_addresses! {
15
  }
16
  }
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  /// Land mine entity task.
19
  ///
20
  /// Extends CGameTask (0xFC bytes). Mines sit on the terrain and arm after
@@ -29,7 +60,7 @@ crate::define_addresses! {
29
  #[repr(C)]
30
  pub struct CTaskMine {
31
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
32
- pub base: CGameTask,
33
  /// 0xFC–0x10F: Unknown mine flags
34
  pub _unknown_fc: [u8; 0x14],
35
  /// 0x110: Object pool slot index (assigned from DDGame+0x3600 pool)
@@ -51,6 +82,9 @@ pub struct CTaskMine {
51
 
52
  const _: () = assert!(core::mem::size_of::<CTaskMine>() == 0x128);
53
 
 
 
 
54
  /// Exploding oil drum entity task.
55
  ///
56
  /// Extends CGameTask (0xFC bytes). Oil drums roll on terrain and explode
@@ -65,7 +99,7 @@ const _: () = assert!(core::mem::size_of::<CTaskMine>() == 0x128);
65
  #[repr(C)]
66
  pub struct CTaskOilDrum {
67
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
68
- pub base: CGameTask,
69
  /// 0xFC: Triggered flag — set to 1 on first impact, starts smoke/fire
70
  pub triggered: u32,
71
  /// 0x100: Object pool slot index
@@ -80,6 +114,9 @@ pub struct CTaskOilDrum {
80
 
81
  const _: () = assert!(core::mem::size_of::<CTaskOilDrum>() == 0x110);
82
 
 
 
 
83
  impl CTaskOilDrum {
84
  /// Returns true if the drum is on fire (flag at CGameTask+0xB0, inside _unknown_98).
85
  ///
 
1
+ use super::base::CTask;
2
  use super::game_task::CGameTask;
3
  use crate::FieldRegistry;
4
 
 
16
  }
17
  }
18
 
19
+ /// CTaskMine vtable — 12 slots. Extends CGameTask vtable with mine behavior.
20
+ ///
21
+ /// Vtable at Ghidra 0x6643E8.
22
+ #[openwa_core::vtable(size = 12, va = 0x0066_43E8, class = "CTaskMine")]
23
+ pub struct CTaskMineVTable {
24
+ /// HandleMessage — processes mine messages (arm, trigger, detonate).
25
+ /// thiscall + 4 stack params, RET 0x10.
26
+ #[slot(2)]
27
+ pub handle_message: fn(this: *mut CTaskMine, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
28
+ /// ProcessFrame — per-frame mine update.
29
+ /// thiscall + 1 stack param (flags), RET 0x4.
30
+ #[slot(7)]
31
+ pub process_frame: fn(this: *mut CTaskMine, flags: u32),
32
+ }
33
+
34
+ /// CTaskOilDrum vtable — 12 slots. Extends CGameTask vtable with oil drum behavior.
35
+ ///
36
+ /// Vtable at Ghidra 0x664338.
37
+ #[openwa_core::vtable(size = 12, va = 0x0066_4338, class = "CTaskOilDrum")]
38
+ pub struct CTaskOilDrumVTable {
39
+ /// HandleMessage — processes oil drum messages.
40
+ /// thiscall + 4 stack params, RET 0x10.
41
+ #[slot(2)]
42
+ pub handle_message: fn(this: *mut CTaskOilDrum, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
43
+ /// ProcessFrame — per-frame oil drum update.
44
+ /// thiscall + 1 stack param (flags), RET 0x4.
45
+ #[slot(7)]
46
+ pub process_frame: fn(this: *mut CTaskOilDrum, flags: u32),
47
+ }
48
+
49
  /// Land mine entity task.
50
  ///
51
  /// Extends CGameTask (0xFC bytes). Mines sit on the terrain and arm after
 
60
  #[repr(C)]
61
  pub struct CTaskMine {
62
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
63
+ pub base: CGameTask<*const CTaskMineVTable>,
64
  /// 0xFC–0x10F: Unknown mine flags
65
  pub _unknown_fc: [u8; 0x14],
66
  /// 0x110: Object pool slot index (assigned from DDGame+0x3600 pool)
 
82
 
83
  const _: () = assert!(core::mem::size_of::<CTaskMine>() == 0x128);
84
 
85
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
86
+ bind_CTaskMineVTable!(CTaskMine, base.base.vtable);
87
+
88
  /// Exploding oil drum entity task.
89
  ///
90
  /// Extends CGameTask (0xFC bytes). Oil drums roll on terrain and explode
 
99
  #[repr(C)]
100
  pub struct CTaskOilDrum {
101
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
102
+ pub base: CGameTask<*const CTaskOilDrumVTable>,
103
  /// 0xFC: Triggered flag — set to 1 on first impact, starts smoke/fire
104
  pub triggered: u32,
105
  /// 0x100: Object pool slot index
 
114
 
115
  const _: () = assert!(core::mem::size_of::<CTaskOilDrum>() == 0x110);
116
 
117
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
118
+ bind_CTaskOilDrumVTable!(CTaskOilDrum, base.base.vtable);
119
+
120
  impl CTaskOilDrum {
121
  /// Returns true if the drum is on fire (flag at CGameTask+0xB0, inside _unknown_98).
122
  ///
crates/openwa-core/src/task/missile.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  use super::game_task::CGameTask;
2
  use crate::fixed::Fixed;
3
  use crate::game::weapon::WeaponSpawnData;
@@ -11,6 +12,21 @@ crate::define_addresses! {
11
  }
12
  }
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  /// Projectile / missile entity task.
15
  ///
16
  /// Extends CGameTask (0xFC bytes). One instance per airborne projectile
@@ -32,7 +48,7 @@ crate::define_addresses! {
32
  #[repr(C)]
33
  pub struct CTaskMissile {
34
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94).
35
- pub base: CGameTask,
36
 
37
  // ---- 0xFC–0x12F: missile init fields ----
38
  /// 0xFC–0x10F: Unknown missile flags and state
@@ -128,6 +144,9 @@ pub struct CTaskMissile {
128
 
129
  const _: () = assert!(core::mem::size_of::<CTaskMissile>() == 0x40C);
130
 
 
 
 
131
  impl CTaskMissile {
132
  /// Missile type from `render_data[0x17]` (= weapon_data[0x1A] for single shots).
133
  ///
 
1
+ use super::base::CTask;
2
  use super::game_task::CGameTask;
3
  use crate::fixed::Fixed;
4
  use crate::game::weapon::WeaponSpawnData;
 
12
  }
13
  }
14
 
15
+ /// CTaskMissile vtable — 12 slots. Extends CGameTask vtable with missile behavior.
16
+ ///
17
+ /// Vtable at Ghidra 0x664438.
18
+ #[openwa_core::vtable(size = 12, va = 0x0066_4438, class = "CTaskMissile")]
19
+ pub struct CTaskMissileVTable {
20
+ /// HandleMessage — processes missile messages.
21
+ /// thiscall + 4 stack params, RET 0x10.
22
+ #[slot(2)]
23
+ pub handle_message: fn(this: *mut CTaskMissile, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
24
+ /// ProcessFrame — per-frame missile update (physics, homing, detonation).
25
+ /// thiscall + 1 stack param (flags), RET 0x4.
26
+ #[slot(7)]
27
+ pub process_frame: fn(this: *mut CTaskMissile, flags: u32),
28
+ }
29
+
30
  /// Projectile / missile entity task.
31
  ///
32
  /// Extends CGameTask (0xFC bytes). One instance per airborne projectile
 
48
  #[repr(C)]
49
  pub struct CTaskMissile {
50
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94).
51
+ pub base: CGameTask<*const CTaskMissileVTable>,
52
 
53
  // ---- 0xFC–0x12F: missile init fields ----
54
  /// 0xFC–0x10F: Unknown missile flags and state
 
144
 
145
  const _: () = assert!(core::mem::size_of::<CTaskMissile>() == 0x40C);
146
 
147
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
148
+ bind_CTaskMissileVTable!(CTaskMissile, base.base.vtable);
149
+
150
  impl CTaskMissile {
151
  /// Missile type from `render_data[0x17]` (= weapon_data[0x1A] for single shots).
152
  ///
crates/openwa-core/src/task/mod.rs CHANGED
@@ -13,16 +13,16 @@ pub mod team;
13
  pub mod turn_game;
14
  pub mod worm;
15
 
16
- pub use arrow::CTaskArrow;
17
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
18
- pub use cloud::CTaskCloud;
19
- pub use filter::CTaskFilter;
20
- pub use fire::CTaskFire;
21
  pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
22
- pub use mine_oil_drum::{CTaskMine, CTaskOilDrum};
23
- pub use missile::{CTaskMissile, MissileType};
24
  pub use overlays::{BungeeTrailTask, WeaponAimTask};
25
- pub use supply_crate::CTaskCrate;
26
  pub use team::{CTaskTeam, CTaskTeamVTable};
27
- pub use turn_game::{CTaskTurnGame, TurnGameCtx};
28
  pub use worm::{CTaskWorm, CTaskWormVTable};
 
13
  pub mod turn_game;
14
  pub mod worm;
15
 
16
+ pub use arrow::{CTaskArrow, CTaskArrowVTable};
17
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
18
+ pub use cloud::{CTaskCloud, CTaskCloudVTable};
19
+ pub use filter::{CTaskFilter, CTaskFilterVTable};
20
+ pub use fire::{CTaskFire, CTaskFireVTable};
21
  pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
22
+ pub use mine_oil_drum::{CTaskMine, CTaskMineVTable, CTaskOilDrum, CTaskOilDrumVTable};
23
+ pub use missile::{CTaskMissile, CTaskMissileVTable, MissileType};
24
  pub use overlays::{BungeeTrailTask, WeaponAimTask};
25
+ pub use supply_crate::{CTaskCrate, CTaskCrateVTable};
26
  pub use team::{CTaskTeam, CTaskTeamVTable};
27
+ pub use turn_game::{CTaskTurnGame, CTaskTurnGameVTable, TurnGameCtx};
28
  pub use worm::{CTaskWorm, CTaskWormVTable};
crates/openwa-core/src/task/supply_crate.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  use super::game_task::CGameTask;
2
  use crate::FieldRegistry;
3
 
@@ -9,6 +10,21 @@ crate::define_addresses! {
9
  }
10
  }
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  /// Weapon/health/utility crate entity task.
13
  ///
14
  /// Extends CGameTask (0xFC bytes). Crates fall from the sky with a parachute,
@@ -26,7 +42,7 @@ crate::define_addresses! {
26
  #[repr(C)]
27
  pub struct CTaskCrate {
28
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
29
- pub base: CGameTask,
30
  /// 0xFC: Unknown (zeroed by constructor)
31
  pub _unknown_fc: u32,
32
  /// 0x100: Object pool slot index (assigned from DDGame+0x3600 pool)
@@ -57,3 +73,6 @@ pub struct CTaskCrate {
57
  }
58
 
59
  const _: () = assert!(core::mem::size_of::<CTaskCrate>() == 0x4B0);
 
 
 
 
1
+ use super::base::CTask;
2
  use super::game_task::CGameTask;
3
  use crate::FieldRegistry;
4
 
 
10
  }
11
  }
12
 
13
+ /// CTaskCrate vtable — 12 slots. Extends CGameTask vtable with crate behavior.
14
+ ///
15
+ /// Vtable at Ghidra 0x664298.
16
+ #[openwa_core::vtable(size = 12, va = 0x0066_4298, class = "CTaskCrate")]
17
+ pub struct CTaskCrateVTable {
18
+ /// HandleMessage — processes crate messages (collection, parachute, etc.).
19
+ /// thiscall + 4 stack params, RET 0x10.
20
+ #[slot(2)]
21
+ pub handle_message: fn(this: *mut CTaskCrate, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
22
+ /// ProcessFrame — per-frame crate update.
23
+ /// thiscall + 1 stack param (flags), RET 0x4.
24
+ #[slot(7)]
25
+ pub process_frame: fn(this: *mut CTaskCrate, flags: u32),
26
+ }
27
+
28
  /// Weapon/health/utility crate entity task.
29
  ///
30
  /// Extends CGameTask (0xFC bytes). Crates fall from the sky with a parachute,
 
42
  #[repr(C)]
43
  pub struct CTaskCrate {
44
  /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
45
+ pub base: CGameTask<*const CTaskCrateVTable>,
46
  /// 0xFC: Unknown (zeroed by constructor)
47
  pub _unknown_fc: u32,
48
  /// 0x100: Object pool slot index (assigned from DDGame+0x3600 pool)
 
73
  }
74
 
75
  const _: () = assert!(core::mem::size_of::<CTaskCrate>() == 0x4B0);
76
+
77
+ // Generate typed vtable method wrappers: handle_message(), process_frame().
78
+ bind_CTaskCrateVTable!(CTaskCrate, base.base.vtable);
crates/openwa-core/src/task/turn_game.rs CHANGED
@@ -89,6 +89,22 @@ pub struct TurnGameCtx {
89
 
90
  const _: () = assert!(core::mem::size_of::<TurnGameCtx>() == 0xAC);
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  /// Root turn-controller task — one instance per game, parent of the entire entity tree.
93
  ///
94
  /// Every worm, team, projectile, and environment task is a child (direct or indirect)
@@ -112,7 +128,7 @@ const _: () = assert!(core::mem::size_of::<TurnGameCtx>() == 0xAC);
112
  #[repr(C)]
113
  pub struct CTaskTurnGame {
114
  /// 0x00-0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
115
- pub base: CTask,
116
  /// 0x30–0xDB: Embedded `TurnGameCtx` sub-object (0xAC bytes).
117
  /// See [`TurnGameCtx`] for field details.
118
  pub game_ctx: TurnGameCtx,
@@ -189,3 +205,6 @@ pub struct CTaskTurnGame {
189
  }
190
 
191
  const _: () = assert!(core::mem::size_of::<CTaskTurnGame>() == 0x2E0);
 
 
 
 
89
 
90
  const _: () = assert!(core::mem::size_of::<TurnGameCtx>() == 0xAC);
91
 
92
+ /// CTaskTurnGame vtable — 12 slots. Extends CTask base (8 slots) with turn-game behavior.
93
+ ///
94
+ /// Vtable at Ghidra 0x669F70. Slot 2 (HandleMessage) is the main turn-flow
95
+ /// dispatcher (30+ message types).
96
+ #[openwa_core::vtable(size = 12, va = 0x0066_9F70, class = "CTaskTurnGame")]
97
+ pub struct CTaskTurnGameVTable {
98
+ /// HandleMessage — processes messages for turn flow control
99
+ /// thiscall + 4 stack params, RET 0x10.
100
+ #[slot(2)]
101
+ pub handle_message: fn(this: *mut CTaskTurnGame, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
102
+ /// ProcessFrame — per-frame turn update.
103
+ /// thiscall + 1 stack param (flags), RET 0x4.
104
+ #[slot(7)]
105
+ pub process_frame: fn(this: *mut CTaskTurnGame, flags: u32),
106
+ }
107
+
108
  /// Root turn-controller task — one instance per game, parent of the entire entity tree.
109
  ///
110
  /// Every worm, team, projectile, and environment task is a child (direct or indirect)
 
128
  #[repr(C)]
129
  pub struct CTaskTurnGame {
130
  /// 0x00-0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
131
+ pub base: CTask<*const CTaskTurnGameVTable>,
132
  /// 0x30–0xDB: Embedded `TurnGameCtx` sub-object (0xAC bytes).
133
  /// See [`TurnGameCtx`] for field details.
134
  pub game_ctx: TurnGameCtx,
 
205
  }
206
 
207
  const _: () = assert!(core::mem::size_of::<CTaskTurnGame>() == 0x2E0);
208
+
209
+ // Generate typed vtable method wrappers: handle_message(), process_frame(), etc.
210
+ bind_CTaskTurnGameVTable!(CTaskTurnGame, base.vtable);
crates/openwa-core/src/task/worm.rs CHANGED
@@ -95,7 +95,7 @@ pub struct CTaskWormVTable {
95
  #[repr(C)]
96
  pub struct CTaskWorm {
97
  /// 0x00–0xFB: CGameTask base (position, velocity, sound emitter, etc.)
98
- pub base: CGameTask,
99
 
100
  /// 0xFC: Team index (0-based); 3rd constructor param
101
  pub team_index: u32,
@@ -189,6 +189,9 @@ pub struct CTaskWorm {
189
 
190
  const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
191
 
 
 
 
192
  impl CTaskWorm {
193
  /// Returns the worm's current state code (lives at offset +0x44, inside
194
  /// `base.subclass_data`).
@@ -214,14 +217,7 @@ impl CTaskWorm {
214
  self.base.subclass_data[12..16].copy_from_slice(&value.to_ne_bytes());
215
  }
216
 
217
- /// Returns a reference to the vtable.
218
- ///
219
- /// # Safety
220
- /// The vtable pointer at offset 0 must be the genuine CTaskWorm vtable
221
- /// (0x6644C8 in Ghidra, rebased at runtime).
222
- pub unsafe fn vtable(&self) -> &'static CTaskWormVTable {
223
- &*(self.base.base.vtable as *const CTaskWormVTable)
224
- }
225
  }
226
 
227
  // ── Snapshot impl ──────────────────────────────────────────
 
95
  #[repr(C)]
96
  pub struct CTaskWorm {
97
  /// 0x00–0xFB: CGameTask base (position, velocity, sound emitter, etc.)
98
+ pub base: CGameTask<*const CTaskWormVTable>,
99
 
100
  /// 0xFC: Team index (0-based); 3rd constructor param
101
  pub team_index: u32,
 
189
 
190
  const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
191
 
192
+ // Generate typed vtable method wrappers: handle_message(), on_contact_entity(), etc.
193
+ bind_CTaskWormVTable!(CTaskWorm, base.base.vtable);
194
+
195
  impl CTaskWorm {
196
  /// Returns the worm's current state code (lives at offset +0x44, inside
197
  /// `base.subclass_data`).
 
217
  self.base.subclass_data[12..16].copy_from_slice(&value.to_ne_bytes());
218
  }
219
 
220
+ // vtable() method is now provided by bind_CTaskWormVTable! macro above.
 
 
 
 
 
 
 
221
  }
222
 
223
  // ── Snapshot impl ──────────────────────────────────────────
crates/openwa-derive/src/lib.rs CHANGED
@@ -586,24 +586,10 @@ pub fn derive_field_registry(input: TokenStream) -> TokenStream {
586
  let doc = extract_doc_comment(&field.attrs);
587
  let field_ty = &field.ty;
588
 
589
- // If the field type is a generic param (e.g., `V`), substitute with its
590
- // default type for size_of. offset_of uses the bare struct name which
591
  // Rust resolves with defaults applied.
592
- let size_ty: Box<dyn quote::ToTokens> =
593
- if let syn::Type::Path(tp) = field_ty {
594
- if tp.path.segments.len() == 1 {
595
- let seg = &tp.path.segments[0];
596
- if let Some(default) = generic_defaults.get(&seg.ident.to_string()) {
597
- Box::new(default.clone())
598
- } else {
599
- Box::new(field_ty.clone())
600
- }
601
- } else {
602
- Box::new(field_ty.clone())
603
- }
604
- } else {
605
- Box::new(field_ty.clone())
606
- };
607
 
608
  // Determine ValueKind: check for #[field(kind = "...")] override, else infer
609
  let kind = parse_field_kind_attr(&field.attrs)
@@ -734,3 +720,59 @@ fn value_kind_token(variant: &str) -> proc_macro2::TokenStream {
734
  let ident = quote::format_ident!("{}", variant);
735
  quote! { openwa_core::registry::ValueKind::#ident }
736
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  let doc = extract_doc_comment(&field.attrs);
587
  let field_ty = &field.ty;
588
 
589
+ // Substitute generic type params (e.g., `V` or `CTask<V>`) with their
590
+ // defaults for size_of. offset_of uses the bare struct name which
591
  // Rust resolves with defaults applied.
592
+ let size_ty = substitute_generics(field_ty, &generic_defaults);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
 
594
  // Determine ValueKind: check for #[field(kind = "...")] override, else infer
595
  let kind = parse_field_kind_attr(&field.attrs)
 
720
  let ident = quote::format_ident!("{}", variant);
721
  quote! { openwa_core::registry::ValueKind::#ident }
722
  }
723
+
724
+ /// Recursively substitute generic type parameters with their defaults.
725
+ ///
726
+ /// For example, given `CTask<V>` with defaults `{V → *const c_void}`,
727
+ /// produces `CTask<*const c_void>`. Handles nested generics and all
728
+ /// common type forms (paths, pointers, arrays, etc.).
729
+ fn substitute_generics(
730
+ ty: &syn::Type,
731
+ defaults: &std::collections::HashMap<String, syn::Type>,
732
+ ) -> syn::Type {
733
+ if defaults.is_empty() {
734
+ return ty.clone();
735
+ }
736
+
737
+ match ty {
738
+ syn::Type::Path(tp) => {
739
+ // Check if the entire path is a generic param name (e.g., `V`)
740
+ if tp.qself.is_none() && tp.path.segments.len() == 1 {
741
+ let seg = &tp.path.segments[0];
742
+ if seg.arguments.is_empty() {
743
+ if let Some(default) = defaults.get(&seg.ident.to_string()) {
744
+ return default.clone();
745
+ }
746
+ }
747
+ }
748
+ // Recursively substitute within generic arguments (e.g., CTask<V>)
749
+ let mut tp = tp.clone();
750
+ for seg in &mut tp.path.segments {
751
+ if let syn::PathArguments::AngleBracketed(ref mut args) = seg.arguments {
752
+ for arg in &mut args.args {
753
+ if let syn::GenericArgument::Type(ref mut inner_ty) = arg {
754
+ *inner_ty = substitute_generics(inner_ty, defaults);
755
+ }
756
+ }
757
+ }
758
+ }
759
+ syn::Type::Path(tp)
760
+ }
761
+ syn::Type::Ptr(ptr) => {
762
+ let mut ptr = ptr.clone();
763
+ ptr.elem = Box::new(substitute_generics(&ptr.elem, defaults));
764
+ syn::Type::Ptr(ptr)
765
+ }
766
+ syn::Type::Array(arr) => {
767
+ let mut arr = arr.clone();
768
+ arr.elem = Box::new(substitute_generics(&arr.elem, defaults));
769
+ syn::Type::Array(arr)
770
+ }
771
+ syn::Type::Reference(r) => {
772
+ let mut r = r.clone();
773
+ r.elem = Box::new(substitute_generics(&r.elem, defaults));
774
+ syn::Type::Reference(r)
775
+ }
776
+ _ => ty.clone(),
777
+ }
778
+ }
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -408,7 +408,7 @@ unsafe fn fire_weapon_special(
408
  unsafe fn lookup_team_task(worm: *const CTaskWorm) -> *mut openwa_core::task::CTaskTeam {
409
  use openwa_core::task::SharedDataTable;
410
 
411
- let task = &(*worm).base.base;
412
  let table = SharedDataTable::from_task(task);
413
  // CTaskTeam is registered with key (0, 0x14) in SharedData
414
  table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTeam
@@ -432,7 +432,7 @@ unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
432
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
433
 
434
  (*team).handle_message(
435
- &raw mut (*worm).base.base,
436
  msg_type,
437
  4,
438
  buf.as_ptr(),
@@ -453,7 +453,7 @@ unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
453
  buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
454
 
455
  (*team).handle_message(
456
- &raw mut (*worm).base.base,
457
  0x5D,
458
  0x408,
459
  buf.as_ptr(),
@@ -726,7 +726,7 @@ unsafe extern "cdecl" fn create_weapon_projectile_impl(
726
  }
727
 
728
  // Look up parent CTaskTurnGame via SharedData (key_esi=0, key_edi=0x19)
729
- let table = SharedDataTable::from_task(task);
730
  let parent = table.lookup(0, 0x19);
731
 
732
  // Allocate CTaskMissile (0x40C bytes)
@@ -900,7 +900,7 @@ unsafe extern "cdecl" fn create_arrow_impl(
900
  }
901
 
902
  // Look up parent CTaskTurnGame via SharedData (key 0, 0x19)
903
- let table = SharedDataTable::from_task(task);
904
  let parent = table.lookup(0, 0x19);
905
 
906
  // Allocate CTaskArrow (0x168 bytes)
 
408
  unsafe fn lookup_team_task(worm: *const CTaskWorm) -> *mut openwa_core::task::CTaskTeam {
409
  use openwa_core::task::SharedDataTable;
410
 
411
+ let task = &(*worm).base.base as *const _ as *const openwa_core::task::CTask;
412
  let table = SharedDataTable::from_task(task);
413
  // CTaskTeam is registered with key (0, 0x14) in SharedData
414
  table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTeam
 
432
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
433
 
434
  (*team).handle_message(
435
+ &raw mut (*worm).base.base as *mut openwa_core::task::CTask,
436
  msg_type,
437
  4,
438
  buf.as_ptr(),
 
453
  buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
454
 
455
  (*team).handle_message(
456
+ &raw mut (*worm).base.base as *mut openwa_core::task::CTask,
457
  0x5D,
458
  0x408,
459
  buf.as_ptr(),
 
726
  }
727
 
728
  // Look up parent CTaskTurnGame via SharedData (key_esi=0, key_edi=0x19)
729
+ let table = SharedDataTable::from_task(task as *const _ as *const openwa_core::task::CTask);
730
  let parent = table.lookup(0, 0x19);
731
 
732
  // Allocate CTaskMissile (0x40C bytes)
 
900
  }
901
 
902
  // Look up parent CTaskTurnGame via SharedData (key 0, 0x19)
903
+ let table = SharedDataTable::from_task(task as *const _ as *const openwa_core::task::CTask);
904
  let parent = table.lookup(0, 0x19);
905
 
906
  // Allocate CTaskArrow (0x168 bytes)