paavohtl Claude Opus 4.6 (1M context) commited on
Commit
a0cd512
Β·
1 Parent(s): 0cdcc6e

fix: eliminate noalias UB in vtable dispatch, add typed TeamMessage

Browse files

The bind_! macro generated &mut self methods for vtable calls, giving
LLVM noalias guarantees that WA bridge calls violated. This caused
codegen-sensitive desyncs where unrelated code changes would shift
optimization decisions and expose the UB.

Fix: auto-generate _raw associated functions (taking raw pointers) for
every bind_! vtable method. Migrate all call sites in weapon dispatch,
weapon release, sound, cloud, and filter handlers to use _raw variants
and CTask::ddgame_raw() instead of the Task trait methods.

Also adds TeamMessage enum with NapalmStrike variant as the first typed
message, and CTaskTeam::on_napalm_strike handler (not yet wired up due
to a separate binary layout sensitivity issue under investigation).

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

crates/openwa-core/src/task/base.rs CHANGED
@@ -273,6 +273,60 @@ pub unsafe trait Task {
273
  // Blanket impl for any CTask<V>
274
  unsafe impl<V: Vtable> Task for CTask<V> {}
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  // ---------------------------------------------------------------------------
277
  // Shared-data entity registry
278
  // ---------------------------------------------------------------------------
 
273
  // Blanket impl for any CTask<V>
274
  unsafe impl<V: Vtable> Task for CTask<V> {}
275
 
276
+ // ---------------------------------------------------------------------------
277
+ // Raw-pointer associated functions β€” no &self/&mut self, no noalias UB.
278
+ //
279
+ // Use these instead of Task trait methods when operating on WA-owned objects
280
+ // through raw pointers. Any type whose first field is CTask (at offset 0)
281
+ // can be cast to *mut CTask and used with these functions.
282
+ // ---------------------------------------------------------------------------
283
+
284
+ impl CTask {
285
+ /// Get the DDGame pointer from a raw CTask pointer.
286
+ ///
287
+ /// Reads the `ddgame` field (offset 0x2C) without creating a Rust reference.
288
+ #[inline(always)]
289
+ pub unsafe fn ddgame_raw(this: *const CTask) -> *mut DDGame {
290
+ (*this).ddgame
291
+ }
292
+
293
+ /// Broadcast a message to all children β€” raw-pointer version.
294
+ ///
295
+ /// Pure Rust port of CTask::HandleMessage (0x562F30).
296
+ /// Identical to `Task::broadcast_message` but takes `*mut CTask` instead
297
+ /// of `&mut self`, avoiding noalias UB.
298
+ pub unsafe fn broadcast_message_raw(
299
+ task_ptr: *mut CTask,
300
+ sender: *mut CTask,
301
+ msg_type: u32,
302
+ size: u32,
303
+ data: *const u8,
304
+ ) {
305
+ let mut i: usize = 0;
306
+ loop {
307
+ let child = loop {
308
+ let watermark = core::ptr::read_volatile(
309
+ core::ptr::addr_of!((*task_ptr).children_watermark),
310
+ ) as usize;
311
+ if i >= watermark {
312
+ return;
313
+ }
314
+ let children = core::ptr::read_volatile(
315
+ core::ptr::addr_of!((*task_ptr).children_data),
316
+ );
317
+ let c = *children.add(i);
318
+ i += 1;
319
+ if !c.is_null() {
320
+ break c;
321
+ }
322
+ };
323
+
324
+ let vt = &*((*child).vtable as *const CTaskVtable);
325
+ (vt.handle_message)(child, sender, msg_type, size, data);
326
+ }
327
+ }
328
+ }
329
+
330
  // ---------------------------------------------------------------------------
331
  // Shared-data entity registry
332
  // ---------------------------------------------------------------------------
crates/openwa-core/src/task/mod.rs CHANGED
@@ -10,6 +10,7 @@ pub mod missile;
10
  mod overlays;
11
  pub mod supply_crate;
12
  pub mod team;
 
13
  pub mod turn_game;
14
  pub mod worm;
15
 
@@ -24,6 +25,7 @@ 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, WormState};
29
 
 
10
  mod overlays;
11
  pub mod supply_crate;
12
  pub mod team;
13
+ pub mod team_message;
14
  pub mod turn_game;
15
  pub mod worm;
16
 
 
25
  pub use overlays::{BungeeTrailTask, WeaponAimTask};
26
  pub use supply_crate::{CTaskCrate, CTaskCrateVTable};
27
  pub use team::{CTaskTeam, CTaskTeamVTable};
28
+ pub use team_message::TeamMessage;
29
  pub use turn_game::{CTaskTurnGame, CTaskTurnGameVTable, TurnGameCtx};
30
  pub use worm::{CTaskWorm, CTaskWormVTable, WormState};
31
 
crates/openwa-core/src/task/team.rs CHANGED
@@ -114,3 +114,66 @@ const _: () = assert!(core::mem::size_of::<CTaskTeam>() == 0x460);
114
 
115
  // Generate typed vtable method wrappers: handle_message(), write_replay_state(), etc.
116
  bind_CTaskTeamVTable!(CTaskTeam, base.vtable);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  // Generate typed vtable method wrappers: handle_message(), write_replay_state(), etc.
116
  bind_CTaskTeamVTable!(CTaskTeam, base.vtable);
117
+
118
+ // ── Typed message handlers ──────────────────────────────────
119
+
120
+ impl CTaskTeam {
121
+ /// Handle NapalmStrike message (0x2B) β€” ported from CTaskTeam::HandleMessage.
122
+ ///
123
+ /// Uses raw pointer (not `&mut self`) to avoid noalias issues with broadcast calls.
124
+ ///
125
+ /// Logic:
126
+ /// 1. Team ownership check (msg team_index must match this.team_index)
127
+ /// 2. If game_version > 0xF4: broadcast DetonateWeapon (0x2A) to children
128
+ /// 3. Set per-team napalm flag at ddgame + team_index * 0x51C + 0x4618
129
+ /// 4. Broadcast original message (0x2B) to children
130
+ ///
131
+ /// # Safety
132
+ /// `this` must be a valid CTaskTeam pointer with valid ddgame.
133
+ /// Handle NapalmStrike message (0x2B) β€” ported from CTaskTeam::HandleMessage.
134
+ ///
135
+ /// Uses `CTask::broadcast_message_raw` for child dispatch (no `&mut self`,
136
+ /// no noalias UB). This is a pure Rust handler β€” no bridge to WA code.
137
+ ///
138
+ /// # Safety
139
+ /// `this` must be a valid CTaskTeam pointer with valid ddgame.
140
+ pub unsafe fn on_napalm_strike(
141
+ this: *mut Self,
142
+ sender: *mut CTask,
143
+ msg_team_index: u32,
144
+ ) {
145
+ use crate::game::TaskMessage;
146
+
147
+ let team_index = (*this).team_index;
148
+
149
+ // 1. Team ownership check β€” only process messages for our team
150
+ if msg_team_index != team_index {
151
+ return;
152
+ }
153
+
154
+ // Serialize the message to raw bytes for broadcast to WA children
155
+ let mut buf = [0u8; 8];
156
+ buf[0..4].copy_from_slice(&msg_team_index.to_ne_bytes());
157
+ let task_ptr = this as *mut CTask;
158
+
159
+ // 2. If game_version > 0xF4: broadcast DetonateWeapon (0x2A) to children first
160
+ let ddgame = CTask::ddgame_raw(this as *const CTask);
161
+ let game_version = (*(*ddgame).game_info).game_version;
162
+ if game_version > 0xF4 {
163
+ CTask::broadcast_message_raw(
164
+ task_ptr, sender, TaskMessage::DetonateWeapon as u32, 4, buf.as_ptr(),
165
+ );
166
+ }
167
+
168
+ // 3. Set per-team napalm flag
169
+ // Original: *(ddgame + team_index * 0x51C + 0x4618) = 1
170
+ let flag_ptr = (ddgame as *mut u8)
171
+ .add(team_index as usize * 0x51C + 0x4618) as *mut u32;
172
+ *flag_ptr = 1;
173
+
174
+ // 4. Broadcast original message (0x2B) to children
175
+ CTask::broadcast_message_raw(
176
+ task_ptr, sender, TaskMessage::Surrender as u32, 4, buf.as_ptr(),
177
+ );
178
+ }
179
+ }
crates/openwa-core/src/task/team_message.rs ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Typed messages for CTaskTeam.
2
+ //!
3
+ //! Replaces the raw `(msg_type: u32, size: u32, data: *const u8)` interface
4
+ //! for messages where both sender and receiver are implemented in Rust.
5
+ //! Messages not yet ported remain as raw vtable dispatches.
6
+
7
+ use crate::game::TaskMessage;
8
+
9
+ /// Typed messages handled by CTaskTeam in Rust.
10
+ ///
11
+ /// Each variant corresponds to a specific `TaskMessage` value and carries
12
+ /// its payload as typed fields instead of a raw byte buffer.
13
+ #[derive(Debug, Clone, Copy)]
14
+ pub enum TeamMessage {
15
+ /// 0x2B (Surrender): Napalm strike or similar team-targeted action.
16
+ /// Sets a per-team flag and optionally broadcasts DetonateWeapon.
17
+ NapalmStrike {
18
+ /// Team index (1-based) identifying which team fired.
19
+ team_index: u32,
20
+ },
21
+ // Future variants:
22
+ // WeaponReleased { team_index: u32, worm_index: u32, ... },
23
+ }
24
+
25
+ impl TeamMessage {
26
+ /// Parse a raw message into a typed TeamMessage, if recognized.
27
+ ///
28
+ /// Returns `None` for unrecognized message types (handled by original WA code).
29
+ ///
30
+ /// # Safety
31
+ /// `data` must be valid for `size` bytes when `size > 0`.
32
+ pub unsafe fn from_raw(msg_type: u32, _size: u32, data: *const u8) -> Option<Self> {
33
+ match TaskMessage::try_from(msg_type) {
34
+ Ok(TaskMessage::Surrender) => {
35
+ // data[0..4] = team_index
36
+ if data.is_null() {
37
+ return None;
38
+ }
39
+ let team_index = *(data as *const u32);
40
+ Some(TeamMessage::NapalmStrike { team_index })
41
+ }
42
+ _ => None,
43
+ }
44
+ }
45
+
46
+ /// Serialize this message to a raw buffer for broadcast_message interop.
47
+ ///
48
+ /// Writes the payload into `buf` and returns `(msg_type, size)`.
49
+ pub fn to_raw(&self, buf: &mut [u8]) -> (u32, u32) {
50
+ match self {
51
+ TeamMessage::NapalmStrike { team_index } => {
52
+ buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
53
+ (TaskMessage::Surrender as u32, 4)
54
+ }
55
+ }
56
+ }
57
+ }
crates/openwa-core/src/task/worm.rs CHANGED
@@ -115,18 +115,9 @@ impl WormState {}
115
 
116
  /// Raw-pointer methods for CTaskWorm.
117
  ///
118
- /// These avoid creating `&mut CTaskWorm` references, which give LLVM noalias
119
- /// guarantees that can cause miscompilation when WA code modifies the same
120
- /// memory through raw pointers during bridge calls. Use these in weapon
121
- /// dispatch and other hot paths that mix Rust and WA function calls.
122
  impl CTaskWorm {
123
- /// Call set_state through the vtable without creating `&mut self`.
124
- /// Avoids noalias issues when WA code may alias the worm memory.
125
- pub unsafe fn set_state_raw(this: *mut CTaskWorm, state: WormState) {
126
- let vtable = *(this as *const *const CTaskWormVTable);
127
- ((*vtable).set_state)(this, state);
128
- }
129
-
130
  /// Set the weapon fire completion flag at +0x3C without creating `&mut self`.
131
  pub unsafe fn set_fire_complete_raw(this: *mut CTaskWorm, value: i32) {
132
  let base = this as *mut u8;
 
115
 
116
  /// Raw-pointer methods for CTaskWorm.
117
  ///
118
+ /// `set_state_raw` and other vtable methods are now auto-generated by the
119
+ /// `bind_CTaskWormVTable!` macro. Only non-vtable raw helpers remain here.
 
 
120
  impl CTaskWorm {
 
 
 
 
 
 
 
121
  /// Set the weapon fire completion flag at +0x3C without creating `&mut self`.
122
  pub unsafe fn set_fire_complete_raw(this: *mut CTaskWorm, value: i32) {
123
  let base = this as *mut u8;
crates/openwa-derive/src/lib.rs CHANGED
@@ -385,6 +385,13 @@ fn generate_bind_methods(slots: &[SlotInfo]) -> syn::Result<proc_macro2::TokenSt
385
  quote! { self as *mut Self }
386
  };
387
 
 
 
 
 
 
 
 
388
  // Remaining params
389
  let mut param_decls = Vec::new();
390
  let mut param_names = Vec::new();
@@ -406,11 +413,25 @@ fn generate_bind_methods(slots: &[SlotInfo]) -> syn::Result<proc_macro2::TokenSt
406
  syn::ReturnType::Type(_, ty) => quote! { -> #ty },
407
  };
408
 
 
 
 
 
 
 
 
 
 
409
  methods.push(quote! {
410
  #[doc = #doc]
411
  pub unsafe fn #ident(#self_param #(, #param_decls)*) #ret_ty {
412
  ((*self.$($vtable_field).+).#ident)(#self_cast #(, #param_names)*)
413
  }
 
 
 
 
 
414
  });
415
  }
416
 
 
385
  quote! { self as *mut Self }
386
  };
387
 
388
+ // Raw-pointer `this` param type for the _raw variant
389
+ let raw_this_ty = if this_is_const {
390
+ quote! { *const Self }
391
+ } else {
392
+ quote! { *mut Self }
393
+ };
394
+
395
  // Remaining params
396
  let mut param_decls = Vec::new();
397
  let mut param_names = Vec::new();
 
413
  syn::ReturnType::Type(_, ty) => quote! { -> #ty },
414
  };
415
 
416
+ // `_raw` variant name
417
+ let raw_ident = quote::format_ident!("{}_raw", ident);
418
+ let raw_doc = format!(
419
+ "{}\n\n\
420
+ Raw-pointer variant β€” avoids creating `&mut self`, preventing LLVM noalias \
421
+ miscompilation when WA code modifies the same memory through bridge calls.",
422
+ doc
423
+ );
424
+
425
  methods.push(quote! {
426
  #[doc = #doc]
427
  pub unsafe fn #ident(#self_param #(, #param_decls)*) #ret_ty {
428
  ((*self.$($vtable_field).+).#ident)(#self_cast #(, #param_names)*)
429
  }
430
+
431
+ #[doc = #raw_doc]
432
+ pub unsafe fn #raw_ident(this: #raw_this_ty #(, #param_decls)*) #ret_ty {
433
+ ((*(*this).$($vtable_field).+).#ident)(this #(, #param_names)*)
434
+ }
435
  });
436
  }
437
 
crates/openwa-wormkit/src/replacements/sound.rs CHANGED
@@ -14,7 +14,7 @@ use openwa_core::audio::{play_sound, play_sound_pooled, KnownSoundId, SoundId};
14
  use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
15
  use openwa_core::fixed::Fixed;
16
  use openwa_core::task::worm::CTaskWorm;
17
- use openwa_core::task::{CGameTask, CTask, Task};
18
 
19
  use crate::hook;
20
  use crate::log_line;
@@ -172,13 +172,13 @@ pub(crate) unsafe fn play_sound_local(
172
  pub(crate) unsafe fn stop_worm_sound(worm: *mut CTaskWorm) {
173
  let handle = (*worm).sound_handle;
174
  if handle != 0 {
175
- let ddgame = &*(*worm).ddgame();
176
- let sound = ddgame.sound;
177
  if !sound.is_null() && (handle as i32) >= 0 {
178
  if handle & 0x40000000 != 0 {
179
  // Streaming sound β€” stop via ActiveSoundTable
180
- if !ddgame.active_sounds.is_null() {
181
- (*ddgame.active_sounds).stop_sound(handle & !0x40000000);
182
  }
183
  } else {
184
  // Regular DSSound channel
@@ -198,16 +198,16 @@ pub(crate) unsafe fn stop_worm_sound(worm: *mut CTaskWorm) {
198
  pub(crate) unsafe fn play_worm_sound(worm: *mut CTaskWorm, sound_id: SoundId, volume: Fixed) {
199
  let handle = (*worm).sound_handle;
200
  if handle != 0 {
201
- let ddgame = &*(*worm).ddgame();
202
- let sound = ddgame.sound;
203
  if !sound.is_null() && (handle as i32) >= 0 {
204
  if handle & 0x40000000 == 0 {
205
  // Regular DSSound channel β€” stop via vtable
206
  ((*(*sound).vtable).stop_channel)(sound, handle as i32);
207
  } else {
208
  // Streaming sound β€” stop via ActiveSoundTable
209
- if !ddgame.active_sounds.is_null() {
210
- (*ddgame.active_sounds).stop_sound(handle & !0x40000000);
211
  }
212
  }
213
  }
 
14
  use openwa_core::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
15
  use openwa_core::fixed::Fixed;
16
  use openwa_core::task::worm::CTaskWorm;
17
+ use openwa_core::task::{CGameTask, CTask};
18
 
19
  use crate::hook;
20
  use crate::log_line;
 
172
  pub(crate) unsafe fn stop_worm_sound(worm: *mut CTaskWorm) {
173
  let handle = (*worm).sound_handle;
174
  if handle != 0 {
175
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
176
+ let sound = (*ddgame).sound;
177
  if !sound.is_null() && (handle as i32) >= 0 {
178
  if handle & 0x40000000 != 0 {
179
  // Streaming sound β€” stop via ActiveSoundTable
180
+ if !(*ddgame).active_sounds.is_null() {
181
+ (*(*ddgame).active_sounds).stop_sound(handle & !0x40000000);
182
  }
183
  } else {
184
  // Regular DSSound channel
 
198
  pub(crate) unsafe fn play_worm_sound(worm: *mut CTaskWorm, sound_id: SoundId, volume: Fixed) {
199
  let handle = (*worm).sound_handle;
200
  if handle != 0 {
201
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
202
+ let sound = (*ddgame).sound;
203
  if !sound.is_null() && (handle as i32) >= 0 {
204
  if handle & 0x40000000 == 0 {
205
  // Regular DSSound channel β€” stop via vtable
206
  ((*(*sound).vtable).stop_channel)(sound, handle as i32);
207
  } else {
208
  // Streaming sound β€” stop via ActiveSoundTable
209
+ if !(*ddgame).active_sounds.is_null() {
210
+ (*(*ddgame).active_sounds).stop_sound(handle & !0x40000000);
211
  }
212
  }
213
  }
crates/openwa-wormkit/src/replacements/task/cloud.rs CHANGED
@@ -9,7 +9,7 @@ use openwa_core::game::TaskMessage;
9
  use openwa_core::log::log_line;
10
  use openwa_core::render::queue::{command_type, DrawSpriteCmd};
11
  use openwa_core::task::cloud::CTaskCloud;
12
- use openwa_core::task::{CTask, Task};
13
 
14
  /// CTaskCloud::HandleMessage replacement.
15
  ///
@@ -26,61 +26,60 @@ unsafe extern "thiscall" fn cloud_handle_message(
26
  size: u32,
27
  data: *const u8,
28
  ) {
29
- let cloud = &mut *this;
30
  let msg = TaskMessage::try_from(msg_type);
31
 
32
  match msg {
33
  Ok(TaskMessage::FrameFinish) => {
34
  // Advance Y position
35
- cloud.pos_y = Fixed(cloud.pos_y.0 + cloud.vel_y.0);
36
 
37
  // Advance X position: base velocity + wind * 10
38
- let wind = cloud.wind_accel.0;
39
- cloud.pos_x = Fixed(cloud.pos_x.0 + cloud.vel_x.0 + wind * 10);
40
 
41
  // Wrap X at landscape bounds (with 128.0 Fixed padding)
42
- let ddgame = &*cloud.ddgame();
43
  let padding = Fixed::from_int(128);
44
- let level_left = ddgame.level_bound_min_x - padding;
45
- let level_right = ddgame.level_bound_max_x + padding;
46
 
47
- if cloud.pos_x < level_left {
48
- cloud.pos_x = level_right;
49
- } else if cloud.pos_x > level_right {
50
- cloud.pos_x = level_left;
51
  }
52
 
53
  // Converge wind_accel toward wind_target (clamp step to Β±0x147)
54
- let target = cloud.wind_target.0;
55
- let current = cloud.wind_accel.0;
56
  let diff = target - current;
57
  if diff.abs() < 0x147 {
58
- cloud.wind_accel = Fixed(target);
59
  } else if current < target {
60
- cloud.wind_accel = Fixed(current + 0x147);
61
  } else {
62
- cloud.wind_accel = Fixed(current - 0x147);
63
  }
64
  }
65
 
66
  Ok(TaskMessage::RenderScene) => {
67
- let ddgame = &mut *cloud.ddgame();
68
 
69
  // Only render when rendering phase == 5 (in-game rendering active)
70
- if ddgame.render_phase == 5 {
71
  // Compute parallax X offset: (vel_x + wind * 10) * parallax_scale
72
- let scroll_speed = cloud.vel_x.0 + cloud.wind_accel.0 * 10;
73
- let parallax_x = ((scroll_speed as i64 * ddgame.parallax_scale as i64) >> 16) as i32;
74
- let x = parallax_x + cloud.pos_x.0;
75
 
76
- let rq = &mut *ddgame.render_queue;
77
  if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
78
  *entry = DrawSpriteCmd {
79
  command_type: command_type::DRAW_SPRITE_LOCAL,
80
- layer: cloud.layer_depth.0 as u32,
81
  x_pos: x as u32 & 0xFFFF0000,
82
- y_pos: cloud.pos_y.0 as u32 & 0xFFFF0000,
83
- sprite_id: cloud.sprite_id,
84
  frame: 0,
85
  };
86
  }
@@ -89,15 +88,15 @@ unsafe extern "thiscall" fn cloud_handle_message(
89
 
90
  Ok(TaskMessage::SetWind) => {
91
  if !data.is_null() {
92
- cloud.wind_target = Fixed(*(data as *const i32));
93
  }
94
  }
95
 
96
  _ => {}
97
  }
98
 
99
- // Broadcast to children (pure Rust port of CTask::HandleMessage)
100
- cloud.broadcast_message(sender, msg_type, size, data);
101
  }
102
 
103
  pub fn install() -> Result<(), String> {
 
9
  use openwa_core::log::log_line;
10
  use openwa_core::render::queue::{command_type, DrawSpriteCmd};
11
  use openwa_core::task::cloud::CTaskCloud;
12
+ use openwa_core::task::CTask;
13
 
14
  /// CTaskCloud::HandleMessage replacement.
15
  ///
 
26
  size: u32,
27
  data: *const u8,
28
  ) {
 
29
  let msg = TaskMessage::try_from(msg_type);
30
 
31
  match msg {
32
  Ok(TaskMessage::FrameFinish) => {
33
  // Advance Y position
34
+ (*this).pos_y = Fixed((*this).pos_y.0 + (*this).vel_y.0);
35
 
36
  // Advance X position: base velocity + wind * 10
37
+ let wind = (*this).wind_accel.0;
38
+ (*this).pos_x = Fixed((*this).pos_x.0 + (*this).vel_x.0 + wind * 10);
39
 
40
  // Wrap X at landscape bounds (with 128.0 Fixed padding)
41
+ let ddgame = CTask::ddgame_raw(this as *const CTask);
42
  let padding = Fixed::from_int(128);
43
+ let level_left = (*ddgame).level_bound_min_x - padding;
44
+ let level_right = (*ddgame).level_bound_max_x + padding;
45
 
46
+ if (*this).pos_x < level_left {
47
+ (*this).pos_x = level_right;
48
+ } else if (*this).pos_x > level_right {
49
+ (*this).pos_x = level_left;
50
  }
51
 
52
  // Converge wind_accel toward wind_target (clamp step to Β±0x147)
53
+ let target = (*this).wind_target.0;
54
+ let current = (*this).wind_accel.0;
55
  let diff = target - current;
56
  if diff.abs() < 0x147 {
57
+ (*this).wind_accel = Fixed(target);
58
  } else if current < target {
59
+ (*this).wind_accel = Fixed(current + 0x147);
60
  } else {
61
+ (*this).wind_accel = Fixed(current - 0x147);
62
  }
63
  }
64
 
65
  Ok(TaskMessage::RenderScene) => {
66
+ let ddgame = CTask::ddgame_raw(this as *const CTask);
67
 
68
  // Only render when rendering phase == 5 (in-game rendering active)
69
+ if (*ddgame).render_phase == 5 {
70
  // Compute parallax X offset: (vel_x + wind * 10) * parallax_scale
71
+ let scroll_speed = (*this).vel_x.0 + (*this).wind_accel.0 * 10;
72
+ let parallax_x = ((scroll_speed as i64 * (*ddgame).parallax_scale as i64) >> 16) as i32;
73
+ let x = parallax_x + (*this).pos_x.0;
74
 
75
+ let rq = &mut *(*ddgame).render_queue;
76
  if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
77
  *entry = DrawSpriteCmd {
78
  command_type: command_type::DRAW_SPRITE_LOCAL,
79
+ layer: (*this).layer_depth.0 as u32,
80
  x_pos: x as u32 & 0xFFFF0000,
81
+ y_pos: (*this).pos_y.0 as u32 & 0xFFFF0000,
82
+ sprite_id: (*this).sprite_id,
83
  frame: 0,
84
  };
85
  }
 
88
 
89
  Ok(TaskMessage::SetWind) => {
90
  if !data.is_null() {
91
+ (*this).wind_target = Fixed(*(data as *const i32));
92
  }
93
  }
94
 
95
  _ => {}
96
  }
97
 
98
+ // Broadcast to children β€” raw-pointer version avoids noalias UB
99
+ CTask::broadcast_message_raw(this as *mut CTask, sender, msg_type, size, data);
100
  }
101
 
102
  pub fn install() -> Result<(), String> {
crates/openwa-wormkit/src/replacements/task/filter.rs CHANGED
@@ -6,7 +6,7 @@
6
  use openwa_core::address::va;
7
  use openwa_core::log::log_line;
8
  use openwa_core::task::filter::CTaskFilter;
9
- use openwa_core::task::{CTask, Task};
10
 
11
  /// CTaskFilter::HandleMessage replacement.
12
  ///
@@ -20,17 +20,14 @@ unsafe extern "thiscall" fn filter_handle_message(
20
  size: u32,
21
  data: *const u8,
22
  ) {
23
- let filter = &*this;
24
-
25
  // Check subscription table β€” messages >= 98 always pass through
26
- if (msg_type as usize) < filter.subscription_table.len()
27
- && filter.subscription_table[msg_type as usize] == 0
28
- {
29
  return; // message not subscribed β€” drop silently
30
  }
31
 
32
- // Broadcast to children (pure Rust port of CTask::HandleMessage)
33
- (*this).broadcast_message(sender, msg_type, size, data);
34
  }
35
 
36
  pub fn install() -> Result<(), String> {
 
6
  use openwa_core::address::va;
7
  use openwa_core::log::log_line;
8
  use openwa_core::task::filter::CTaskFilter;
9
+ use openwa_core::task::CTask;
10
 
11
  /// CTaskFilter::HandleMessage replacement.
12
  ///
 
20
  size: u32,
21
  data: *const u8,
22
  ) {
 
 
23
  // Check subscription table β€” messages >= 98 always pass through
24
+ let table = &(*this).subscription_table;
25
+ if (msg_type as usize) < table.len() && table[msg_type as usize] == 0 {
 
26
  return; // message not subscribed β€” drop silently
27
  }
28
 
29
+ // Broadcast to children β€” raw-pointer version avoids noalias UB
30
+ CTask::broadcast_message_raw(this as *mut CTask, sender, msg_type, size, data);
31
  }
32
 
33
  pub fn install() -> Result<(), String> {
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -22,7 +22,8 @@ use openwa_core::fixed::Fixed;
22
  use openwa_core::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
23
  use openwa_core::game::Weapon;
24
  use openwa_core::log::log_line;
25
- use openwa_core::task::Task;
 
26
  use openwa_core::task::worm::{CTaskWorm, WormState};
27
 
28
  use crate::hook::{self, usercall_trampoline};
@@ -322,7 +323,7 @@ unsafe fn fire_weapon_special(
322
  Ok(S::Prod) => fire_prod(worm, ctx as *const u8),
323
  Ok(S::AirStrike) => fire_air_strike(worm),
324
  Ok(S::ScalesOfJustice) => CTaskWorm::set_state_raw(worm, WormState::ScalesOfJustice),
325
- Ok(S::NapalmStrike) => fire_send_team_message(worm, 0x2B),
326
  Ok(S::MailMineMole) => fire_mail_mine_mole(worm),
327
  Ok(S::IndianNuclearTest) => {
328
  if can_fire_subtype16((*worm).state()) {
@@ -356,13 +357,42 @@ fn can_fire_subtype16(state: u32) -> bool {
356
  ///
357
  /// Returns null if not found.
358
  pub(crate) unsafe fn lookup_team_task(worm: *const CTaskWorm) -> *mut openwa_core::task::CTaskTeam {
359
- use openwa_core::task::{SharedDataTable, Task};
360
 
361
- let table = SharedDataTable::from_task((*worm).as_task_ptr());
362
  // CTaskTeam is registered with key (0, 0x14) in SharedData
363
  table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTeam
364
  }
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  /// Send a message to the worm's CTaskTeam entity via SharedData lookup.
367
  ///
368
  /// Pattern shared by Napalm Strike (msg 0x2B), Surrender (msg 0x29), etc.
@@ -380,8 +410,8 @@ unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
380
  let team_index = (*worm).team_index;
381
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
382
 
383
- (*team).handle_message(
384
- (*worm).as_task_ptr_mut(),
385
  msg_type,
386
  4,
387
  buf.as_ptr(),
@@ -401,8 +431,8 @@ unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
401
  buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
402
  buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
403
 
404
- (*team).handle_message(
405
- (*worm).as_task_ptr_mut(),
406
  0x5D,
407
  0x408,
408
  buf.as_ptr(),
@@ -416,7 +446,7 @@ unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
416
  /// In game_version > 0x1C: toggles (set/clear). Otherwise: always sets.
417
  unsafe fn fire_skip_go(worm: *const CTaskWorm, entry: *const WeaponEntry) {
418
  use openwa_core::engine::ddgame::TeamArenaRef;
419
- let ddgame = (*worm).ddgame();
420
  let game_version = (*(*ddgame).game_info).game_version;
421
  let team_index = (*worm).team_index as usize;
422
 
@@ -443,7 +473,7 @@ unsafe fn fire_surrender(worm: *mut CTaskWorm) {
443
 
444
  fire_send_team_message(worm, 0x29);
445
 
446
- let ddgame = (*worm).ddgame();
447
  let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
448
  let team_index = (*worm).team_index as usize;
449
  let worm_index = (*worm).worm_index as usize;
@@ -465,7 +495,7 @@ unsafe fn fire_surrender(worm: *mut CTaskWorm) {
465
  /// - otherwise: skip
466
  unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
467
  use openwa_core::engine::ddgame::TeamArenaRef;
468
- let ddgame = (*worm).ddgame();
469
  let version = (*ddgame).version_flag_4;
470
  let worm_state = (*worm).state();
471
 
@@ -518,7 +548,7 @@ unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
518
  *(worm_raw.add(0x34) as *mut i32) = saved_34;
519
 
520
  // Compute new state: version < 455 β†’ Idle (0x65), else β†’ 0x8B
521
- let ddgame = (*worm).ddgame();
522
  let game_version = (*(*ddgame).game_info).game_version;
523
  let new_state = if game_version < 0x1C7 {
524
  WormState::Idle
@@ -549,7 +579,7 @@ unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
549
  unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
550
  use crate::replacements::sound;
551
 
552
- let ddgame = (*worm).ddgame();
553
  let entry = &*(*worm).active_weapon_entry;
554
  let team = lookup_team_task(worm);
555
 
@@ -558,7 +588,7 @@ unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
558
  let mut buf = [0u8; 0x40C];
559
  buf[0..4].copy_from_slice(&entry.fire_method.to_ne_bytes());
560
  buf[4..8].copy_from_slice(&8i32.to_ne_bytes());
561
- (*team).handle_message((*worm).as_task_ptr_mut(), 0x59, 0x408, buf.as_ptr());
562
  }
563
 
564
  // Message 0x5A (NukeBlast): buf[0]=8
@@ -566,7 +596,7 @@ unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
566
  if !team.is_null() {
567
  let mut buf = [0u8; 0x40C];
568
  buf[0..4].copy_from_slice(&8i32.to_ne_bytes());
569
- (*team).handle_message((*worm).as_task_ptr_mut(), 0x5A, 0x408, buf.as_ptr());
570
  }
571
 
572
  // PlaySoundGlobal(IndianAnthem, 8, 0x10000, 0x10000)
@@ -579,7 +609,7 @@ unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
579
  buf[0..4].copy_from_slice(&entry.fire_params.shot_count.to_ne_bytes());
580
  buf[4..8].copy_from_slice(&2i32.to_ne_bytes());
581
  buf[8..12].copy_from_slice(&(*worm).team_index.to_ne_bytes());
582
- (*team).handle_message((*worm).as_task_ptr_mut(), 0x51, 0x408, buf.as_ptr());
583
  }
584
 
585
  // PlaySoundGlobal(NukeFlash, 5, 0x10000, 0x10000)
@@ -673,8 +703,8 @@ unsafe fn fire_jet_pack(worm: *mut CTaskWorm) {
673
  // Send message 0x5E to CTaskTeam
674
  let team = lookup_team_task(worm);
675
  if !team.is_null() {
676
- (*team).handle_message(
677
- (*worm).as_task_ptr_mut(),
678
  0x5E,
679
  0,
680
  core::ptr::null(),
@@ -684,7 +714,7 @@ unsafe fn fire_jet_pack(worm: *mut CTaskWorm) {
684
  // Play jet pack sound:
685
  // FUN_0053EC70: usercall(EDI=0x6CB) + stdcall(timer_obj)
686
  // FUN_005480F0: usercall(EAX=-21) + stdcall(worm, result, 0x17, &worm_name)
687
- let ddgame = (*worm).ddgame();
688
  let sound_val = call_get_sound_val((*ddgame).timer_obj, rb(0x53EC70));
689
  call_play_sound_usercall(worm, sound_val, 0x17, (*worm).worm_name.as_ptr(), rb(0x5480F0));
690
  }
@@ -734,7 +764,7 @@ unsafe extern "C" fn call_play_sound_usercall(
734
  unsafe fn fire_low_gravity(worm: *mut CTaskWorm) {
735
  use openwa_core::rebase::rb;
736
 
737
- let ddgame = (*worm).ddgame();
738
  let game_version = (*(*ddgame).game_info).game_version;
739
 
740
  // Send message 0x5B to CTaskTeam with weapon info buffer
@@ -747,8 +777,8 @@ unsafe fn fire_low_gravity(worm: *mut CTaskWorm) {
747
  buf[0x0C..0x10].copy_from_slice(&((*worm).selected_weapon as u32).to_ne_bytes());
748
  buf[0x10..0x14].copy_from_slice(&(*worm).team_index.to_ne_bytes());
749
 
750
- (*team).handle_message(
751
- (*worm).as_task_ptr_mut(),
752
  0x5B,
753
  0x408,
754
  buf.as_ptr(),
@@ -802,11 +832,11 @@ unsafe fn fire_girder(
802
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
803
  ) {
804
  use openwa_core::rebase::rb;
805
- use openwa_core::task::{SharedDataTable, Task};
806
  use openwa_core::wa_alloc::wa_malloc;
807
 
808
  // Look up parent task via SharedData (same key as CreateWeaponProjectile)
809
- let table = SharedDataTable::from_task((*worm).as_task_ptr());
810
  let parent = table.lookup(0, 0x19);
811
 
812
  // Allocate CTaskGirder (0xA4 bytes), zero first 0x84
@@ -889,7 +919,7 @@ unsafe extern "C" fn call_fire_usercall(_eax: *const (), _worm: *mut CTaskWorm,
889
  unsafe fn fire_freeze(worm: *mut CTaskWorm) {
890
  use openwa_core::rebase::rb;
891
 
892
- let ddgame = (*worm).ddgame();
893
  let game_version = (*(*ddgame).game_info).game_version;
894
 
895
  // Read freeze position and target from CTaskWorm fields
@@ -987,7 +1017,7 @@ fn special_impact_version_flags(base: u32, version: u8) -> u32 {
987
  /// The original is usercall(ECX=local_struct, ESI=worm) β€” the old bridge
988
  /// did not set ECX, so this port also fixes a latent bug.
989
  unsafe fn fire_drill(worm: *mut CTaskWorm, local_struct: *const u8) {
990
- let ddgame = (*worm).ddgame();
991
  let version = (*ddgame).version_flag_4;
992
  let entry = &*(*worm).active_weapon_entry;
993
  let shot_count = entry.fire_params.shot_count;
@@ -1019,7 +1049,7 @@ unsafe fn fire_drill(worm: *mut CTaskWorm, local_struct: *const u8) {
1019
  unsafe fn fire_prod(worm: *mut CTaskWorm, local_struct: *const u8) {
1020
  use openwa_core::rebase::rb;
1021
 
1022
- let ddgame = (*worm).ddgame();
1023
  let version = (*ddgame).version_flag_4;
1024
  let entry = &*(*worm).active_weapon_entry;
1025
  let shot_count = entry.fire_params.shot_count;
@@ -1157,10 +1187,10 @@ unsafe extern "cdecl" fn create_weapon_projectile_impl(
1157
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
1158
  ) {
1159
  use openwa_core::rebase::rb;
1160
- use openwa_core::task::{SharedDataTable, Task};
1161
  use openwa_core::wa_alloc::wa_malloc;
1162
 
1163
- let ddgame = &mut *(*worm).ddgame();
1164
 
1165
  // Pool capacity check: pool_count + 7 must be <= 700
1166
  if ddgame.object_pool_count + 7 > 700 {
@@ -1169,7 +1199,7 @@ unsafe extern "cdecl" fn create_weapon_projectile_impl(
1169
  }
1170
 
1171
  // Look up parent CTaskTurnGame via SharedData (key_esi=0, key_edi=0x19)
1172
- let table = SharedDataTable::from_task((*worm).as_task_ptr());
1173
  let parent = table.lookup(0, 0x19);
1174
 
1175
  // Allocate CTaskMissile (0x40C bytes)
@@ -1250,7 +1280,7 @@ unsafe extern "cdecl" fn projectile_fire_impl(
1250
  let sin_table = rb(va::SIN_TABLE) as *const i32;
1251
  let cos_table = sin_table.add(256); // cos = sin offset by 256 entries (quarter turn)
1252
 
1253
- let ddgame = &mut *(*worm).ddgame();
1254
 
1255
  for _i in 0..shot_count {
1256
  // Advance game RNG (same LCG as ADVANCE_GAME_RNG at 0x53F320)
@@ -1331,10 +1361,10 @@ unsafe extern "cdecl" fn create_arrow_impl(
1331
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
1332
  ) {
1333
  use openwa_core::rebase::rb;
1334
- use openwa_core::task::{SharedDataTable, Task};
1335
  use openwa_core::wa_alloc::wa_malloc;
1336
 
1337
- let ddgame = &mut *(*worm).ddgame();
1338
 
1339
  // Pool capacity check: pool_count + 2 must be <= 700
1340
  if ddgame.object_pool_count + 2 > 700 {
@@ -1343,7 +1373,7 @@ unsafe extern "cdecl" fn create_arrow_impl(
1343
  }
1344
 
1345
  // Look up parent CTaskTurnGame via SharedData (key 0, 0x19)
1346
- let table = SharedDataTable::from_task((*worm).as_task_ptr());
1347
  let parent = table.lookup(0, 0x19);
1348
 
1349
  // Allocate CTaskArrow (0x168 bytes)
 
22
  use openwa_core::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
23
  use openwa_core::game::Weapon;
24
  use openwa_core::log::log_line;
25
+ use openwa_core::task::team::CTaskTeam;
26
+ use openwa_core::task::CTask;
27
  use openwa_core::task::worm::{CTaskWorm, WormState};
28
 
29
  use crate::hook::{self, usercall_trampoline};
 
323
  Ok(S::Prod) => fire_prod(worm, ctx as *const u8),
324
  Ok(S::AirStrike) => fire_air_strike(worm),
325
  Ok(S::ScalesOfJustice) => CTaskWorm::set_state_raw(worm, WormState::ScalesOfJustice),
326
+ Ok(S::NapalmStrike) => fire_napalm_strike_typed(worm),
327
  Ok(S::MailMineMole) => fire_mail_mine_mole(worm),
328
  Ok(S::IndianNuclearTest) => {
329
  if can_fire_subtype16((*worm).state()) {
 
357
  ///
358
  /// Returns null if not found.
359
  pub(crate) unsafe fn lookup_team_task(worm: *const CTaskWorm) -> *mut openwa_core::task::CTaskTeam {
360
+ use openwa_core::task::SharedDataTable;
361
 
362
+ let table = SharedDataTable::from_task(worm as *const CTask);
363
  // CTaskTeam is registered with key (0, 0x14) in SharedData
364
  table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTeam
365
  }
366
 
367
+ /// Napalm Strike (subtype 14) β€” typed Rust handler.
368
+ ///
369
+ /// Constructs a typed TeamMessage, serializes to raw bytes, and dispatches
370
+ /// through CTaskTeam's WA vtable via handle_message_raw. The typed message
371
+ /// ensures correct field layout without manual byte manipulation.
372
+ #[inline(never)]
373
+ unsafe fn fire_napalm_strike_typed(worm: *mut CTaskWorm) {
374
+ use openwa_core::task::TeamMessage;
375
+
376
+ let team = lookup_team_task(worm);
377
+ if team.is_null() {
378
+ return;
379
+ }
380
+
381
+ let msg = TeamMessage::NapalmStrike {
382
+ team_index: (*worm).team_index,
383
+ };
384
+ let mut buf = [0u8; 0x40C];
385
+ let (msg_type, size) = msg.to_raw(&mut buf);
386
+
387
+ CTaskTeam::handle_message_raw(
388
+ team,
389
+ worm as *mut openwa_core::task::CTask,
390
+ msg_type,
391
+ size,
392
+ buf.as_ptr(),
393
+ );
394
+ }
395
+
396
  /// Send a message to the worm's CTaskTeam entity via SharedData lookup.
397
  ///
398
  /// Pattern shared by Napalm Strike (msg 0x2B), Surrender (msg 0x29), etc.
 
410
  let team_index = (*worm).team_index;
411
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
412
 
413
+ CTaskTeam::handle_message_raw(team,
414
+ worm as *mut openwa_core::task::CTask,
415
  msg_type,
416
  4,
417
  buf.as_ptr(),
 
431
  buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
432
  buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
433
 
434
+ CTaskTeam::handle_message_raw(team,
435
+ worm as *mut openwa_core::task::CTask,
436
  0x5D,
437
  0x408,
438
  buf.as_ptr(),
 
446
  /// In game_version > 0x1C: toggles (set/clear). Otherwise: always sets.
447
  unsafe fn fire_skip_go(worm: *const CTaskWorm, entry: *const WeaponEntry) {
448
  use openwa_core::engine::ddgame::TeamArenaRef;
449
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
450
  let game_version = (*(*ddgame).game_info).game_version;
451
  let team_index = (*worm).team_index as usize;
452
 
 
473
 
474
  fire_send_team_message(worm, 0x29);
475
 
476
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
477
  let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
478
  let team_index = (*worm).team_index as usize;
479
  let worm_index = (*worm).worm_index as usize;
 
495
  /// - otherwise: skip
496
  unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
497
  use openwa_core::engine::ddgame::TeamArenaRef;
498
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
499
  let version = (*ddgame).version_flag_4;
500
  let worm_state = (*worm).state();
501
 
 
548
  *(worm_raw.add(0x34) as *mut i32) = saved_34;
549
 
550
  // Compute new state: version < 455 β†’ Idle (0x65), else β†’ 0x8B
551
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
552
  let game_version = (*(*ddgame).game_info).game_version;
553
  let new_state = if game_version < 0x1C7 {
554
  WormState::Idle
 
579
  unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
580
  use crate::replacements::sound;
581
 
582
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
583
  let entry = &*(*worm).active_weapon_entry;
584
  let team = lookup_team_task(worm);
585
 
 
588
  let mut buf = [0u8; 0x40C];
589
  buf[0..4].copy_from_slice(&entry.fire_method.to_ne_bytes());
590
  buf[4..8].copy_from_slice(&8i32.to_ne_bytes());
591
+ CTaskTeam::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x59, 0x408, buf.as_ptr());
592
  }
593
 
594
  // Message 0x5A (NukeBlast): buf[0]=8
 
596
  if !team.is_null() {
597
  let mut buf = [0u8; 0x40C];
598
  buf[0..4].copy_from_slice(&8i32.to_ne_bytes());
599
+ CTaskTeam::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x5A, 0x408, buf.as_ptr());
600
  }
601
 
602
  // PlaySoundGlobal(IndianAnthem, 8, 0x10000, 0x10000)
 
609
  buf[0..4].copy_from_slice(&entry.fire_params.shot_count.to_ne_bytes());
610
  buf[4..8].copy_from_slice(&2i32.to_ne_bytes());
611
  buf[8..12].copy_from_slice(&(*worm).team_index.to_ne_bytes());
612
+ CTaskTeam::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x51, 0x408, buf.as_ptr());
613
  }
614
 
615
  // PlaySoundGlobal(NukeFlash, 5, 0x10000, 0x10000)
 
703
  // Send message 0x5E to CTaskTeam
704
  let team = lookup_team_task(worm);
705
  if !team.is_null() {
706
+ CTaskTeam::handle_message_raw(team,
707
+ worm as *mut openwa_core::task::CTask,
708
  0x5E,
709
  0,
710
  core::ptr::null(),
 
714
  // Play jet pack sound:
715
  // FUN_0053EC70: usercall(EDI=0x6CB) + stdcall(timer_obj)
716
  // FUN_005480F0: usercall(EAX=-21) + stdcall(worm, result, 0x17, &worm_name)
717
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
718
  let sound_val = call_get_sound_val((*ddgame).timer_obj, rb(0x53EC70));
719
  call_play_sound_usercall(worm, sound_val, 0x17, (*worm).worm_name.as_ptr(), rb(0x5480F0));
720
  }
 
764
  unsafe fn fire_low_gravity(worm: *mut CTaskWorm) {
765
  use openwa_core::rebase::rb;
766
 
767
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
768
  let game_version = (*(*ddgame).game_info).game_version;
769
 
770
  // Send message 0x5B to CTaskTeam with weapon info buffer
 
777
  buf[0x0C..0x10].copy_from_slice(&((*worm).selected_weapon as u32).to_ne_bytes());
778
  buf[0x10..0x14].copy_from_slice(&(*worm).team_index.to_ne_bytes());
779
 
780
+ CTaskTeam::handle_message_raw(team,
781
+ worm as *mut openwa_core::task::CTask,
782
  0x5B,
783
  0x408,
784
  buf.as_ptr(),
 
832
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
833
  ) {
834
  use openwa_core::rebase::rb;
835
+ use openwa_core::task::SharedDataTable;
836
  use openwa_core::wa_alloc::wa_malloc;
837
 
838
  // Look up parent task via SharedData (same key as CreateWeaponProjectile)
839
+ let table = SharedDataTable::from_task(worm as *const CTask);
840
  let parent = table.lookup(0, 0x19);
841
 
842
  // Allocate CTaskGirder (0xA4 bytes), zero first 0x84
 
919
  unsafe fn fire_freeze(worm: *mut CTaskWorm) {
920
  use openwa_core::rebase::rb;
921
 
922
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
923
  let game_version = (*(*ddgame).game_info).game_version;
924
 
925
  // Read freeze position and target from CTaskWorm fields
 
1017
  /// The original is usercall(ECX=local_struct, ESI=worm) β€” the old bridge
1018
  /// did not set ECX, so this port also fixes a latent bug.
1019
  unsafe fn fire_drill(worm: *mut CTaskWorm, local_struct: *const u8) {
1020
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
1021
  let version = (*ddgame).version_flag_4;
1022
  let entry = &*(*worm).active_weapon_entry;
1023
  let shot_count = entry.fire_params.shot_count;
 
1049
  unsafe fn fire_prod(worm: *mut CTaskWorm, local_struct: *const u8) {
1050
  use openwa_core::rebase::rb;
1051
 
1052
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
1053
  let version = (*ddgame).version_flag_4;
1054
  let entry = &*(*worm).active_weapon_entry;
1055
  let shot_count = entry.fire_params.shot_count;
 
1187
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
1188
  ) {
1189
  use openwa_core::rebase::rb;
1190
+ use openwa_core::task::SharedDataTable;
1191
  use openwa_core::wa_alloc::wa_malloc;
1192
 
1193
+ let ddgame = &mut *CTask::ddgame_raw(worm as *const CTask);
1194
 
1195
  // Pool capacity check: pool_count + 7 must be <= 700
1196
  if ddgame.object_pool_count + 7 > 700 {
 
1199
  }
1200
 
1201
  // Look up parent CTaskTurnGame via SharedData (key_esi=0, key_edi=0x19)
1202
+ let table = SharedDataTable::from_task(worm as *const CTask);
1203
  let parent = table.lookup(0, 0x19);
1204
 
1205
  // Allocate CTaskMissile (0x40C bytes)
 
1280
  let sin_table = rb(va::SIN_TABLE) as *const i32;
1281
  let cos_table = sin_table.add(256); // cos = sin offset by 256 entries (quarter turn)
1282
 
1283
+ let ddgame = &mut *CTask::ddgame_raw(worm as *const CTask);
1284
 
1285
  for _i in 0..shot_count {
1286
  // Advance game RNG (same LCG as ADVANCE_GAME_RNG at 0x53F320)
 
1361
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
1362
  ) {
1363
  use openwa_core::rebase::rb;
1364
+ use openwa_core::task::SharedDataTable;
1365
  use openwa_core::wa_alloc::wa_malloc;
1366
 
1367
+ let ddgame = &mut *CTask::ddgame_raw(worm as *const CTask);
1368
 
1369
  // Pool capacity check: pool_count + 2 must be <= 700
1370
  if ddgame.object_pool_count + 2 > 700 {
 
1373
  }
1374
 
1375
  // Look up parent CTaskTurnGame via SharedData (key 0, 0x19)
1376
+ let table = SharedDataTable::from_task(worm as *const CTask);
1377
  let parent = table.lookup(0, 0x19);
1378
 
1379
  // Allocate CTaskArrow (0x168 bytes)
crates/openwa-wormkit/src/replacements/weapon_release.rs CHANGED
@@ -10,8 +10,9 @@ use openwa_core::audio::{KnownSoundId, SoundId};
10
  use openwa_core::fixed::Fixed;
11
  use openwa_core::game::Weapon;
12
  use openwa_core::log::log_line;
 
13
  use openwa_core::task::worm::{CTaskWorm, WormState};
14
- use openwa_core::task::{CGameTask, SharedDataTable, Task};
15
 
16
  use crate::hook::{self, usercall_trampoline};
17
  use crate::replacements::{sound, weapon};
@@ -251,8 +252,9 @@ unsafe extern "cdecl" fn weapon_release_impl(
251
 
252
  let team = weapon::lookup_team_task(worm);
253
  if !team.is_null() {
254
- (*team).handle_message(
255
- (*worm).as_task_ptr_mut(),
 
256
  0x49,
257
  0x408,
258
  msg_buf.as_ptr(),
@@ -260,7 +262,7 @@ unsafe extern "cdecl" fn weapon_release_impl(
260
  }
261
 
262
  // ── 8. Weapon stat counters ─────────────────────────────
263
- let g = &mut *(*worm).ddgame();
264
  let team_id = (*worm).team_index;
265
  let worm_id = (*worm).worm_index;
266
 
@@ -371,7 +373,7 @@ unsafe extern "cdecl" fn weapon_release_impl(
371
 
372
  // ── 10. Visual effect (if triggered by sound dispatch) ──
373
  if do_effect {
374
- let ddgame = &mut *(*worm).ddgame();
375
  let gfx_handler = ddgame.game_state_stream as *const u8;
376
  let palette = *(gfx_handler.add(0x22C) as *const u32);
377
 
@@ -448,7 +450,7 @@ pub(crate) unsafe fn spawn_effect(
448
  write_u32(&mut buf, 0x2C, scale.0 as u32);
449
 
450
  // SharedData lookup for entity type 0x1A (CTaskSpriteAnim)
451
- let table = SharedDataTable::from_task((*worm).as_task_ptr());
452
  let entity = table.lookup(0, 0x1A);
453
  if !entity.is_null() {
454
  let vtable = *(entity as *const *const usize);
 
10
  use openwa_core::fixed::Fixed;
11
  use openwa_core::game::Weapon;
12
  use openwa_core::log::log_line;
13
+ use openwa_core::task::team::CTaskTeam;
14
  use openwa_core::task::worm::{CTaskWorm, WormState};
15
+ use openwa_core::task::{CGameTask, CTask, SharedDataTable, Task};
16
 
17
  use crate::hook::{self, usercall_trampoline};
18
  use crate::replacements::{sound, weapon};
 
252
 
253
  let team = weapon::lookup_team_task(worm);
254
  if !team.is_null() {
255
+ CTaskTeam::handle_message_raw(
256
+ team,
257
+ worm as *mut openwa_core::task::CTask,
258
  0x49,
259
  0x408,
260
  msg_buf.as_ptr(),
 
262
  }
263
 
264
  // ── 8. Weapon stat counters ─────────────────────────────
265
+ let g = &mut *CTask::ddgame_raw(worm as *const CTask);
266
  let team_id = (*worm).team_index;
267
  let worm_id = (*worm).worm_index;
268
 
 
373
 
374
  // ── 10. Visual effect (if triggered by sound dispatch) ──
375
  if do_effect {
376
+ let ddgame = &mut *CTask::ddgame_raw(worm as *const CTask);
377
  let gfx_handler = ddgame.game_state_stream as *const u8;
378
  let palette = *(gfx_handler.add(0x22C) as *const u32);
379
 
 
450
  write_u32(&mut buf, 0x2C, scale.0 as u32);
451
 
452
  // SharedData lookup for entity type 0x1A (CTaskSpriteAnim)
453
+ let table = SharedDataTable::from_task(worm as *const CTask);
454
  let entity = table.lookup(0, 0x1A);
455
  if !entity.is_null() {
456
  let vtable = *(entity as *const *const usize);