fix: eliminate noalias UB in vtable dispatch, add typed TeamMessage
Browse filesThe 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 +54 -0
- crates/openwa-core/src/task/mod.rs +2 -0
- crates/openwa-core/src/task/team.rs +63 -0
- crates/openwa-core/src/task/team_message.rs +57 -0
- crates/openwa-core/src/task/worm.rs +2 -11
- crates/openwa-derive/src/lib.rs +21 -0
- crates/openwa-wormkit/src/replacements/sound.rs +9 -9
- crates/openwa-wormkit/src/replacements/task/cloud.rs +28 -29
- crates/openwa-wormkit/src/replacements/task/filter.rs +5 -8
- crates/openwa-wormkit/src/replacements/weapon.rs +64 -34
- crates/openwa-wormkit/src/replacements/weapon_release.rs +8 -6
|
@@ -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 |
// ---------------------------------------------------------------------------
|
|
@@ -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 |
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -115,18 +115,9 @@ impl WormState {}
|
|
| 115 |
|
| 116 |
/// Raw-pointer methods for CTaskWorm.
|
| 117 |
///
|
| 118 |
-
///
|
| 119 |
-
///
|
| 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;
|
|
@@ -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 |
|
|
@@ -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
|
| 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 =
|
| 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 =
|
| 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 |
}
|
|
@@ -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::
|
| 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 |
-
|
| 36 |
|
| 37 |
// Advance X position: base velocity + wind * 10
|
| 38 |
-
let wind =
|
| 39 |
-
|
| 40 |
|
| 41 |
// Wrap X at landscape bounds (with 128.0 Fixed padding)
|
| 42 |
-
let 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
|
| 48 |
-
|
| 49 |
-
} else if
|
| 50 |
-
|
| 51 |
}
|
| 52 |
|
| 53 |
// Converge wind_accel toward wind_target (clamp step to Β±0x147)
|
| 54 |
-
let target =
|
| 55 |
-
let current =
|
| 56 |
let diff = target - current;
|
| 57 |
if diff.abs() < 0x147 {
|
| 58 |
-
|
| 59 |
} else if current < target {
|
| 60 |
-
|
| 61 |
} else {
|
| 62 |
-
|
| 63 |
}
|
| 64 |
}
|
| 65 |
|
| 66 |
Ok(TaskMessage::RenderScene) => {
|
| 67 |
-
let 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 =
|
| 73 |
-
let parallax_x = ((scroll_speed as i64 * ddgame.parallax_scale as i64) >> 16) as i32;
|
| 74 |
-
let x = parallax_x +
|
| 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:
|
| 81 |
x_pos: x as u32 & 0xFFFF0000,
|
| 82 |
-
y_pos:
|
| 83 |
-
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 |
-
|
| 93 |
}
|
| 94 |
}
|
| 95 |
|
| 96 |
_ => {}
|
| 97 |
}
|
| 98 |
|
| 99 |
-
// Broadcast to children
|
| 100 |
-
|
| 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> {
|
|
@@ -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::
|
| 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 |
-
|
| 27 |
-
|
| 28 |
-
{
|
| 29 |
return; // message not subscribed β drop silently
|
| 30 |
}
|
| 31 |
|
| 32 |
-
// Broadcast to children
|
| 33 |
-
(
|
| 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> {
|
|
@@ -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::
|
|
|
|
| 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) =>
|
| 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::
|
| 360 |
|
| 361 |
-
let table = SharedDataTable::from_task(
|
| 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 |
-
(
|
| 384 |
-
|
| 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 |
-
(
|
| 405 |
-
|
| 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 = (
|
| 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 = (
|
| 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 = (
|
| 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 = (
|
| 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 = (
|
| 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 |
-
(
|
| 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 |
-
(
|
| 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 |
-
(
|
| 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 |
-
(
|
| 677 |
-
|
| 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 = (
|
| 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 = (
|
| 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 |
-
(
|
| 751 |
-
|
| 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::
|
| 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(
|
| 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 = (
|
| 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 = (
|
| 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 = (
|
| 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::
|
| 1161 |
use openwa_core::wa_alloc::wa_malloc;
|
| 1162 |
|
| 1163 |
-
let ddgame = &mut *(
|
| 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(
|
| 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 *(
|
| 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::
|
| 1335 |
use openwa_core::wa_alloc::wa_malloc;
|
| 1336 |
|
| 1337 |
-
let ddgame = &mut *(
|
| 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(
|
| 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)
|
|
@@ -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 |
-
(
|
| 255 |
-
|
|
|
|
| 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 *(
|
| 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 *(
|
| 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(
|
| 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);
|