feat: add CTaskTeamVTable and typed handle_message dispatch
Browse filesDefine CTaskTeamVTable (12 slots, vtable at 0x669EE4) via #[vtable] macro
with typed signatures for handle_message (slot 2), write_replay_state,
free, get_entity_data, and process_frame.
Add CTaskTeam::vtable() accessor and CTaskTeam::handle_message() method
that replaces raw vtable pointer arithmetic in weapon fire handlers.
Extract lookup_team_task() helper for SharedData CTaskTeam lookup.
Note: bind_CTaskTeamVTable! not used because CTask.vtable is *mut u8
(FieldRegistry derive doesn't support generic structs, so CTask<V>
would require manual impls across the entire task hierarchy).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/task/mod.rs
CHANGED
|
@@ -23,6 +23,6 @@ pub use mine_oil_drum::{CTaskMine, CTaskOilDrum};
|
|
| 23 |
pub use missile::{CTaskMissile, MissileType};
|
| 24 |
pub use overlays::{BungeeTrailTask, WeaponAimTask};
|
| 25 |
pub use supply_crate::CTaskCrate;
|
| 26 |
-
pub use team::CTaskTeam;
|
| 27 |
pub use turn_game::{CTaskTurnGame, TurnGameCtx};
|
| 28 |
pub use worm::{CTaskWorm, CTaskWormVTable};
|
|
|
|
| 23 |
pub use missile::{CTaskMissile, MissileType};
|
| 24 |
pub use overlays::{BungeeTrailTask, WeaponAimTask};
|
| 25 |
pub use supply_crate::CTaskCrate;
|
| 26 |
+
pub use team::{CTaskTeam, CTaskTeamVTable};
|
| 27 |
pub use turn_game::{CTaskTurnGame, TurnGameCtx};
|
| 28 |
pub use worm::{CTaskWorm, CTaskWormVTable};
|
crates/openwa-core/src/task/team.rs
CHANGED
|
@@ -10,6 +10,35 @@ crate::define_addresses! {
|
|
| 10 |
}
|
| 11 |
}
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
/// Per-team state-tracker task β one instance per team, child of CTaskTurnGame.
|
| 14 |
///
|
| 15 |
/// Tracks per-team data: which team number it represents, how many worms were
|
|
@@ -82,3 +111,28 @@ pub struct CTaskTeam {
|
|
| 82 |
}
|
| 83 |
|
| 84 |
const _: () = assert!(core::mem::size_of::<CTaskTeam>() == 0x460);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
}
|
| 12 |
|
| 13 |
+
/// CTaskTeam vtable β 12 slots. Extends CTask base (8 slots) with team-specific behavior.
|
| 14 |
+
///
|
| 15 |
+
/// Vtable at Ghidra 0x669EE4. Slot 2 (HandleMessage) is the main team message
|
| 16 |
+
/// dispatcher (1701 instructions, handles weapon fire, surrender, worm selection, etc.)
|
| 17 |
+
#[openwa_core::vtable(size = 12, va = 0x0066_9EE4, class = "CTaskTeam")]
|
| 18 |
+
pub struct CTaskTeamVTable {
|
| 19 |
+
/// WriteReplayState β serializes team state to replay stream.
|
| 20 |
+
/// thiscall + 1 stack param (stream), RET 0x4.
|
| 21 |
+
#[slot(0)]
|
| 22 |
+
pub write_replay_state: fn(this: *mut CTaskTeam, stream: *mut u8),
|
| 23 |
+
/// Free β destructor. thiscall + 1 stack param (flags), RET 0x4.
|
| 24 |
+
#[slot(1)]
|
| 25 |
+
pub free: fn(this: *mut CTaskTeam, flags: u8) -> *mut CTaskTeam,
|
| 26 |
+
/// HandleMessage β processes messages sent to this team (weapon fire,
|
| 27 |
+
/// surrender, worm selection, napalm strike, etc.)
|
| 28 |
+
/// thiscall + 4 stack params, RET 0x10.
|
| 29 |
+
#[slot(2)]
|
| 30 |
+
pub handle_message: fn(this: *mut CTaskTeam, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 31 |
+
/// GetEntityData β returns team data by query code.
|
| 32 |
+
/// thiscall + 3 stack params, RET 0xC.
|
| 33 |
+
#[slot(3)]
|
| 34 |
+
pub get_entity_data: fn(this: *mut CTaskTeam, query: u32, param: u32, out: *mut u32) -> u32,
|
| 35 |
+
// Slots 4-6: inherited CTask methods
|
| 36 |
+
/// ProcessFrame β per-frame team update.
|
| 37 |
+
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 38 |
+
#[slot(7)]
|
| 39 |
+
pub process_frame: fn(this: *mut CTaskTeam, flags: u32),
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
/// Per-team state-tracker task β one instance per team, child of CTaskTurnGame.
|
| 43 |
///
|
| 44 |
/// Tracks per-team data: which team number it represents, how many worms were
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
const _: () = assert!(core::mem::size_of::<CTaskTeam>() == 0x460);
|
| 114 |
+
|
| 115 |
+
impl CTaskTeam {
|
| 116 |
+
/// Get the typed vtable pointer.
|
| 117 |
+
pub fn vtable(&self) -> &CTaskTeamVTable {
|
| 118 |
+
unsafe { &*(self.base.vtable as *const CTaskTeamVTable) }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/// Call HandleMessage (vtable slot 2) on this team task.
|
| 122 |
+
///
|
| 123 |
+
/// This is the main team message dispatcher β handles weapon fire results,
|
| 124 |
+
/// surrender, worm selection, napalm strike, and many other team-level events.
|
| 125 |
+
///
|
| 126 |
+
/// # Safety
|
| 127 |
+
/// `sender` must be a valid CTask pointer. `data` must point to `size` bytes.
|
| 128 |
+
pub unsafe fn handle_message(
|
| 129 |
+
&mut self,
|
| 130 |
+
sender: *mut CTask,
|
| 131 |
+
msg_type: u32,
|
| 132 |
+
size: u32,
|
| 133 |
+
data: *const u8,
|
| 134 |
+
) {
|
| 135 |
+
let vt = self.vtable();
|
| 136 |
+
(vt.handle_message)(self as *mut Self, sender, msg_type, size, data);
|
| 137 |
+
}
|
| 138 |
+
}
|
crates/openwa-wormkit/src/replacements/weapon.rs
CHANGED
|
@@ -402,62 +402,62 @@ unsafe fn fire_weapon_special(
|
|
| 402 |
|
| 403 |
// ββ Pure Rust fire handlers (no bridge needed) ββββββββββββββ
|
| 404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
/// Send a message to the worm's CTaskTeam entity via SharedData lookup.
|
| 406 |
///
|
| 407 |
/// Pattern shared by Napalm Strike (msg 0x2B), Surrender (msg 0x29), etc.
|
| 408 |
-
/// Looks up
|
| 409 |
-
/// then calls entity->vtable[2] (HandleMessage) with the given message type.
|
| 410 |
///
|
| 411 |
/// The 0x40C-byte local buffer is passed as data pointer; team_index is written
|
| 412 |
-
/// at buf+
|
| 413 |
unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
let task = &(*worm).base.base; // CTask base
|
| 417 |
-
let table = SharedDataTable::from_task(task);
|
| 418 |
-
|
| 419 |
-
// Lookup CTaskTeam (key_esi=0, key_edi=0x14)
|
| 420 |
-
let team_entity = table.lookup(0, 0x14);
|
| 421 |
-
if team_entity.is_null() {
|
| 422 |
return;
|
| 423 |
}
|
| 424 |
|
| 425 |
-
// Prepare local buffer (0x40C bytes).
|
| 426 |
-
// Original: buffer[0] = worm->team_index (from worm+0xFC), rest is stack garbage.
|
| 427 |
-
// We zero-fill for safety, then set buffer[0] = team_index.
|
| 428 |
let mut buf = [0u8; 0x40C];
|
| 429 |
let team_index = (*worm).team_index;
|
| 430 |
buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
|
| 431 |
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
|
|
|
| 437 |
}
|
| 438 |
|
| 439 |
/// Select Worm (subtype 21) β pure Rust replacement for 0x51EBE0.
|
| 440 |
///
|
| 441 |
/// Sends message 0x5D to CTaskTeam with buf = [8, team_index, ...].
|
| 442 |
unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
let task = &(*worm).base.base;
|
| 446 |
-
let table = SharedDataTable::from_task(task);
|
| 447 |
-
let team_entity = table.lookup(0, 0x14);
|
| 448 |
-
if team_entity.is_null() {
|
| 449 |
return;
|
| 450 |
}
|
| 451 |
|
| 452 |
let mut buf = [0u8; 0x40C];
|
| 453 |
-
// buf[0..4] = 8 (constant), buf[4..8] = team_index
|
| 454 |
buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
|
| 455 |
buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
|
| 456 |
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
|
|
|
|
|
|
| 461 |
}
|
| 462 |
|
| 463 |
/// Skip Go (subtype 19) β pure Rust replacement for 0x51E8C0.
|
|
|
|
| 402 |
|
| 403 |
// ββ Pure Rust fire handlers (no bridge needed) ββββββββββββββ
|
| 404 |
|
| 405 |
+
/// Look up the CTaskTeam entity for a worm via SharedData.
|
| 406 |
+
///
|
| 407 |
+
/// Returns null if not found.
|
| 408 |
+
unsafe fn lookup_team_task(worm: *const CTaskWorm) -> *mut openwa_core::task::CTaskTeam {
|
| 409 |
+
use openwa_core::task::SharedDataTable;
|
| 410 |
+
|
| 411 |
+
let task = &(*worm).base.base;
|
| 412 |
+
let table = SharedDataTable::from_task(task);
|
| 413 |
+
// CTaskTeam is registered with key (0, 0x14) in SharedData
|
| 414 |
+
table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTeam
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
/// Send a message to the worm's CTaskTeam entity via SharedData lookup.
|
| 418 |
///
|
| 419 |
/// Pattern shared by Napalm Strike (msg 0x2B), Surrender (msg 0x29), etc.
|
| 420 |
+
/// Looks up CTaskTeam, then calls HandleMessage (vtable slot 2).
|
|
|
|
| 421 |
///
|
| 422 |
/// The 0x40C-byte local buffer is passed as data pointer; team_index is written
|
| 423 |
+
/// at buf+0x00 to identify which team fired.
|
| 424 |
unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
|
| 425 |
+
let team = lookup_team_task(worm);
|
| 426 |
+
if team.is_null() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
return;
|
| 428 |
}
|
| 429 |
|
|
|
|
|
|
|
|
|
|
| 430 |
let mut buf = [0u8; 0x40C];
|
| 431 |
let team_index = (*worm).team_index;
|
| 432 |
buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
|
| 433 |
|
| 434 |
+
(*team).handle_message(
|
| 435 |
+
&raw mut (*worm).base.base,
|
| 436 |
+
msg_type,
|
| 437 |
+
4,
|
| 438 |
+
buf.as_ptr(),
|
| 439 |
+
);
|
| 440 |
}
|
| 441 |
|
| 442 |
/// Select Worm (subtype 21) β pure Rust replacement for 0x51EBE0.
|
| 443 |
///
|
| 444 |
/// Sends message 0x5D to CTaskTeam with buf = [8, team_index, ...].
|
| 445 |
unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
|
| 446 |
+
let team = lookup_team_task(worm);
|
| 447 |
+
if team.is_null() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
return;
|
| 449 |
}
|
| 450 |
|
| 451 |
let mut buf = [0u8; 0x40C];
|
|
|
|
| 452 |
buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
|
| 453 |
buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
|
| 454 |
|
| 455 |
+
(*team).handle_message(
|
| 456 |
+
&raw mut (*worm).base.base,
|
| 457 |
+
0x5D,
|
| 458 |
+
0x408,
|
| 459 |
+
buf.as_ptr(),
|
| 460 |
+
);
|
| 461 |
}
|
| 462 |
|
| 463 |
/// Skip Go (subtype 19) β pure Rust replacement for 0x51E8C0.
|