feat: port Napalm Strike and Select Worm to pure Rust
Browse filesAdd SharedDataTable::lookup() β pure Rust equivalent of FUN_004FDF90.
Looks up an entity by (key_esi, key_edi) in the 256-bucket hash table.
Port two weapon fire handlers using the new lookup:
- Napalm Strike (subtype 13): lookup CTaskTeam, send message 0x2B
- Select Worm (subtype 21): lookup CTaskTeam, send message 0x5D
with buf = [8, team_index]
Both share the fire_send_team_message() helper which handles the
SharedData lookup + HandleMessage vtable dispatch pattern.
Verified: headless PASS (byte-identical).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/task/base.rs
CHANGED
|
@@ -139,6 +139,25 @@ impl SharedDataTable {
|
|
| 139 |
h
|
| 140 |
}
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
/// Iterate all nodes across all 256 buckets.
|
| 143 |
///
|
| 144 |
/// # Safety
|
|
|
|
| 139 |
h
|
| 140 |
}
|
| 141 |
|
| 142 |
+
/// Look up an entity by key pair. Returns the entity pointer, or null.
|
| 143 |
+
///
|
| 144 |
+
/// Pure Rust equivalent of `FUN_004FDF90` (SharedData__Lookup).
|
| 145 |
+
/// fastcall(ECX=key_esi, EDX=key_edi, stack=task) in the original.
|
| 146 |
+
///
|
| 147 |
+
/// # Safety
|
| 148 |
+
/// The table and all linked nodes must be valid.
|
| 149 |
+
pub unsafe fn lookup(&self, key_esi: u32, key_edi: u32) -> *mut u8 {
|
| 150 |
+
let bucket = Self::bucket_for(key_esi, key_edi) as usize;
|
| 151 |
+
let mut node = *self.buckets.add(bucket);
|
| 152 |
+
while !node.is_null() {
|
| 153 |
+
if (*node).key_edi == key_edi && (*node).key_esi == key_esi {
|
| 154 |
+
return (*node).entity;
|
| 155 |
+
}
|
| 156 |
+
node = (*node).next;
|
| 157 |
+
}
|
| 158 |
+
core::ptr::null_mut()
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
/// Iterate all nodes across all 256 buckets.
|
| 162 |
///
|
| 163 |
/// # Safety
|
crates/openwa-wormkit/src/replacements/weapon.rs
CHANGED
|
@@ -367,7 +367,7 @@ unsafe fn fire_weapon_special(
|
|
| 367 |
9 => call_fire_usercall_stdcall1(e, w, local_struct, rb(0x51E480)), // Prod (EAX=entry)
|
| 368 |
10 => call_fire_usercall(w, w, rb(0x51E710)), // Air Strike (EAX=worm)
|
| 369 |
11 => fire_worm_vtable_0xe(w, 0x71), // Scales of Justice
|
| 370 |
-
13 =>
|
| 371 |
14 => call_fire_usercall(w, w, rb(0x51E670)), // Mail/Mine/Mole (EAX=worm)
|
| 372 |
16 => {
|
| 373 |
// Teleport: MOV EAX,[ESI+0x44] (worm state) before check
|
|
@@ -383,7 +383,7 @@ unsafe fn fire_weapon_special(
|
|
| 383 |
18 => fire_worm_vtable_0xe(w, 0x72), // Suicide Bomber
|
| 384 |
19 => fire_skip_go(worm, entry), // Skip Go (pure Rust)
|
| 385 |
20 => call_fire_usercall(w, w, rb(0x51E600)), // Surrender (EAX=worm)
|
| 386 |
-
21 =>
|
| 387 |
22 => call_fire_usercall(e, w, rb(0x51EC30)), // Jet Pack (EAX=entry)
|
| 388 |
23 => fire_worm_vtable_0xe(w, 0x78), // Magic Bullet
|
| 389 |
24 => call_fire_usercall(e, w, rb(0x51EA60)), // Low Grav (EAX=entry)
|
|
@@ -393,6 +393,62 @@ unsafe fn fire_weapon_special(
|
|
| 393 |
|
| 394 |
// ββ Pure Rust fire handlers (no bridge needed) ββββββββββββββ
|
| 395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
/// Skip Go (subtype 19) β pure Rust replacement for 0x51E8C0.
|
| 397 |
///
|
| 398 |
/// Toggles a bit in the team's `TeamHeader.turn_action_flags` (+0x7C).
|
|
|
|
| 367 |
9 => call_fire_usercall_stdcall1(e, w, local_struct, rb(0x51E480)), // Prod (EAX=entry)
|
| 368 |
10 => call_fire_usercall(w, w, rb(0x51E710)), // Air Strike (EAX=worm)
|
| 369 |
11 => fire_worm_vtable_0xe(w, 0x71), // Scales of Justice
|
| 370 |
+
13 => fire_send_team_message(worm, 0x2B), // Napalm Strike
|
| 371 |
14 => call_fire_usercall(w, w, rb(0x51E670)), // Mail/Mine/Mole (EAX=worm)
|
| 372 |
16 => {
|
| 373 |
// Teleport: MOV EAX,[ESI+0x44] (worm state) before check
|
|
|
|
| 383 |
18 => fire_worm_vtable_0xe(w, 0x72), // Suicide Bomber
|
| 384 |
19 => fire_skip_go(worm, entry), // Skip Go (pure Rust)
|
| 385 |
20 => call_fire_usercall(w, w, rb(0x51E600)), // Surrender (EAX=worm)
|
| 386 |
+
21 => fire_select_worm(worm), // Select Worm (pure Rust)
|
| 387 |
22 => call_fire_usercall(e, w, rb(0x51EC30)), // Jet Pack (EAX=entry)
|
| 388 |
23 => fire_worm_vtable_0xe(w, 0x78), // Magic Bullet
|
| 389 |
24 => call_fire_usercall(e, w, rb(0x51EA60)), // Low Grav (EAX=entry)
|
|
|
|
| 393 |
|
| 394 |
// ββ Pure Rust fire handlers (no bridge needed) ββββββββββββββ
|
| 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.
|
| 399 |
+
/// Looks up entity with key (0, 0x14=CTaskTeam) in worm's shared data table,
|
| 400 |
+
/// then calls entity->vtable[2] (HandleMessage) with the given message type.
|
| 401 |
+
///
|
| 402 |
+
/// The 0x40C-byte local buffer is passed as data pointer; team_index is written
|
| 403 |
+
/// at buf+0x08 to identify which team fired.
|
| 404 |
+
unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
|
| 405 |
+
use openwa_core::task::SharedDataTable;
|
| 406 |
+
|
| 407 |
+
let task = &(*worm).base.base; // CTask base
|
| 408 |
+
let table = SharedDataTable::from_task(task);
|
| 409 |
+
|
| 410 |
+
// Lookup CTaskTeam (key_esi=0, key_edi=0x14)
|
| 411 |
+
let team_entity = table.lookup(0, 0x14);
|
| 412 |
+
if team_entity.is_null() {
|
| 413 |
+
return;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// Prepare local buffer (0x40C bytes, mostly zero)
|
| 417 |
+
let mut buf = [0u8; 0x40C];
|
| 418 |
+
let team_index = (*worm).team_index;
|
| 419 |
+
buf[8..12].copy_from_slice(&team_index.to_ne_bytes());
|
| 420 |
+
|
| 421 |
+
// Call entity->vtable[2] = HandleMessage(sender=worm, msg, size=4, data=&buf)
|
| 422 |
+
let vtable = *(team_entity as *const u32);
|
| 423 |
+
let handle_message: unsafe extern "thiscall" fn(*mut u8, *mut CTaskWorm, u32, u32, *const u8) =
|
| 424 |
+
core::mem::transmute(*(vtable as *const u32).add(2));
|
| 425 |
+
handle_message(team_entity, worm, msg_type, 4, buf.as_ptr());
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
/// Select Worm (subtype 21) β pure Rust replacement for 0x51EBE0.
|
| 429 |
+
///
|
| 430 |
+
/// Sends message 0x5D to CTaskTeam with buf = [8, team_index, ...].
|
| 431 |
+
unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
|
| 432 |
+
use openwa_core::task::SharedDataTable;
|
| 433 |
+
|
| 434 |
+
let task = &(*worm).base.base;
|
| 435 |
+
let table = SharedDataTable::from_task(task);
|
| 436 |
+
let team_entity = table.lookup(0, 0x14);
|
| 437 |
+
if team_entity.is_null() {
|
| 438 |
+
return;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
let mut buf = [0u8; 0x40C];
|
| 442 |
+
// buf[0..4] = 8 (constant), buf[4..8] = team_index
|
| 443 |
+
buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
|
| 444 |
+
buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
|
| 445 |
+
|
| 446 |
+
let vtable = *(team_entity as *const u32);
|
| 447 |
+
let handle_message: unsafe extern "thiscall" fn(*mut u8, *mut CTaskWorm, u32, u32, *const u8) =
|
| 448 |
+
core::mem::transmute(*(vtable as *const u32).add(2));
|
| 449 |
+
handle_message(team_entity, worm, 0x5D, 0x408, buf.as_ptr());
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
/// Skip Go (subtype 19) β pure Rust replacement for 0x51E8C0.
|
| 453 |
///
|
| 454 |
/// Toggles a bit in the team's `TeamHeader.turn_action_flags` (+0x7C).
|