feat: port Surrender and Mail/Mine/Mole weapon handlers to pure Rust
Browse filesPort two FireWeapon sub-handlers from WA bridge calls to pure Rust:
- Surrender (0x51E600): SharedData lookup + HandleMessage(0x29) + WormEntry counter
- Mail/Mine/Mole (0x51E670): version-conditional vtable call + HandleMessage(0x28) + counter
Fix replay loader V2 scheme bug: our code was clearing super weapon data
(memset 0x4C) for both V1 and V2 schemes when header < 0, but the original
WA only clears for V1. V2 schemes already contain valid super weapon data.
Other changes:
- Name WormEntry fields: effect_counter_04_Maybe (+0x04), turn_action_counter_Maybe (+0x08)
- Add TeamArenaRef::team_worm_mut() for mutable worm entry access
- Add mail_mine_mole replay test (Mail Strike, Mine Strike, Mole Squadron, Surrender)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@@ -1728,8 +1728,11 @@ pub struct WormEntry {
|
|
| 1728 |
/// Values: 0x65=idle, 0x67=active/selected, 0x68=active variant.
|
| 1729 |
/// Special states {0x80..0x85, 0x89} = dying/drowning/special animation.
|
| 1730 |
pub state: u32,
|
| 1731 |
-
/// 0x04
|
| 1732 |
-
pub
|
|
|
|
|
|
|
|
|
|
| 1733 |
/// 0x0C: Active/alive flag (1 for alive worms in game, 0 otherwise).
|
| 1734 |
pub active_flag: i32,
|
| 1735 |
/// 0x10-0x57: Unknown
|
|
@@ -2051,6 +2054,16 @@ impl TeamArenaRef {
|
|
| 2051 |
&*(ptr as *const WormEntry)
|
| 2052 |
}
|
| 2053 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2054 |
/// Get a team's block and its header in one call.
|
| 2055 |
///
|
| 2056 |
/// Returns `(block[team_idx], &block[team_idx+1].header.team)`.
|
|
|
|
| 1728 |
/// Values: 0x65=idle, 0x67=active/selected, 0x68=active variant.
|
| 1729 |
/// Special states {0x80..0x85, 0x89} = dying/drowning/special animation.
|
| 1730 |
pub state: u32,
|
| 1731 |
+
/// 0x04: Unknown counter. Incremented by Freeze (+10).
|
| 1732 |
+
pub effect_counter_04_Maybe: i32,
|
| 1733 |
+
/// 0x08: Turn action counter. Incremented by weapon handlers:
|
| 1734 |
+
/// Surrender (+14), Mail/Mine/Mole (+7), Freeze (+3).
|
| 1735 |
+
pub turn_action_counter_Maybe: i32,
|
| 1736 |
/// 0x0C: Active/alive flag (1 for alive worms in game, 0 otherwise).
|
| 1737 |
pub active_flag: i32,
|
| 1738 |
/// 0x10-0x57: Unknown
|
|
|
|
| 2054 |
&*(ptr as *const WormEntry)
|
| 2055 |
}
|
| 2056 |
|
| 2057 |
+
/// Get a mutable reference to a specific worm entry.
|
| 2058 |
+
pub unsafe fn team_worm_mut(&self, team_idx: usize, worm_num: usize) -> &mut WormEntry {
|
| 2059 |
+
let ptr = self
|
| 2060 |
+
.base
|
| 2061 |
+
.add(team_idx * 0x51C)
|
| 2062 |
+
.add(worm_num * 0x9C)
|
| 2063 |
+
.sub(0x598);
|
| 2064 |
+
&mut *(ptr as *mut WormEntry)
|
| 2065 |
+
}
|
| 2066 |
+
|
| 2067 |
/// Get a team's block and its header in one call.
|
| 2068 |
///
|
| 2069 |
/// Returns `(block[team_idx], &block[team_idx+1].header.team)`.
|
|
@@ -344,13 +344,21 @@ unsafe fn parse_and_write_v2plus(
|
|
| 344 |
let scheme_slice = s.advance_raw(scheme_data_size)?;
|
| 345 |
ptr::copy_nonoverlapping(scheme_slice.as_ptr(), rb(va::G_SCHEME_DATA) as *mut u8, scheme_data_size);
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
}
|
| 355 |
|
| 356 |
// Validate extended options for v3
|
|
|
|
| 344 |
let scheme_slice = s.advance_raw(scheme_data_size)?;
|
| 345 |
ptr::copy_nonoverlapping(scheme_slice.as_ptr(), rb(va::G_SCHEME_DATA) as *mut u8, scheme_data_size);
|
| 346 |
|
| 347 |
+
let header_val = *(rb(va::G_SCHEME_HEADER) as *const i8);
|
| 348 |
+
|
| 349 |
+
// If scheme_header < 0 (signed): apply defaults.
|
| 350 |
+
// V1: clear super weapons (0x4C bytes) + copy V3 defaults.
|
| 351 |
+
// V2: copy V3 defaults ONLY (super weapon data is already in the payload).
|
| 352 |
+
if header_val < 0 {
|
| 353 |
+
if scheme_version == 1 {
|
| 354 |
+
ptr::write_bytes(rb(va::G_SCHEME_OPTIONS) as *mut u8, 0, 0x4C);
|
| 355 |
+
}
|
| 356 |
+
if scheme_version <= 2 {
|
| 357 |
+
ptr::copy_nonoverlapping(
|
| 358 |
+
rb(va::SCHEME_V3_DEFAULTS) as *const u8,
|
| 359 |
+
rb(va::G_SCHEME_V3_DATA) as *mut u8, 0x6E,
|
| 360 |
+
);
|
| 361 |
+
}
|
| 362 |
}
|
| 363 |
|
| 364 |
// Validate extended options for v3
|
|
@@ -369,7 +369,7 @@ unsafe fn fire_weapon_special(
|
|
| 369 |
10 => call_fire_usercall(w, w, rb(0x51E710)), // Air Strike (EAX=worm)
|
| 370 |
11 => fire_worm_vtable_0xe(w, 0x71), // Scales of Justice
|
| 371 |
13 => fire_send_team_message(worm, 0x2B), // Napalm Strike
|
| 372 |
-
14 =>
|
| 373 |
16 => {
|
| 374 |
// Teleport: MOV EAX,[ESI+0x44] (worm state) before check
|
| 375 |
let worm_state = (*worm).state();
|
|
@@ -383,7 +383,7 @@ unsafe fn fire_weapon_special(
|
|
| 383 |
17 => call_fire_usercall(w, w, rb(0x51E920)), // Freeze (EAX=worm)
|
| 384 |
18 => fire_worm_vtable_0xe(w, 0x72), // Suicide Bomber
|
| 385 |
19 => fire_skip_go(worm, entry), // Skip Go (pure Rust)
|
| 386 |
-
20 =>
|
| 387 |
21 => fire_select_worm(worm), // Select Worm (pure Rust)
|
| 388 |
22 => call_fire_usercall(e, w, rb(0x51EC30)), // Jet Pack (EAX=entry)
|
| 389 |
23 => fire_worm_vtable_0xe(w, 0x78), // Magic Bullet
|
|
@@ -479,6 +479,61 @@ unsafe fn fire_skip_go(worm: *const CTaskWorm, entry: *const WeaponEntry) {
|
|
| 479 |
}
|
| 480 |
}
|
| 481 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
// ββ Naked asm bridges βββββββββββββββββββββββββββββββββββββββ
|
| 483 |
|
| 484 |
/// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET.
|
|
|
|
| 369 |
10 => call_fire_usercall(w, w, rb(0x51E710)), // Air Strike (EAX=worm)
|
| 370 |
11 => fire_worm_vtable_0xe(w, 0x71), // Scales of Justice
|
| 371 |
13 => fire_send_team_message(worm, 0x2B), // Napalm Strike
|
| 372 |
+
14 => fire_mail_mine_mole(worm), // Mail/Mine/Mole (pure Rust)
|
| 373 |
16 => {
|
| 374 |
// Teleport: MOV EAX,[ESI+0x44] (worm state) before check
|
| 375 |
let worm_state = (*worm).state();
|
|
|
|
| 383 |
17 => call_fire_usercall(w, w, rb(0x51E920)), // Freeze (EAX=worm)
|
| 384 |
18 => fire_worm_vtable_0xe(w, 0x72), // Suicide Bomber
|
| 385 |
19 => fire_skip_go(worm, entry), // Skip Go (pure Rust)
|
| 386 |
+
20 => fire_surrender(worm), // Surrender (pure Rust)
|
| 387 |
21 => fire_select_worm(worm), // Select Worm (pure Rust)
|
| 388 |
22 => call_fire_usercall(e, w, rb(0x51EC30)), // Jet Pack (EAX=entry)
|
| 389 |
23 => fire_worm_vtable_0xe(w, 0x78), // Magic Bullet
|
|
|
|
| 479 |
}
|
| 480 |
}
|
| 481 |
|
| 482 |
+
/// Surrender (subtype 20) β pure Rust replacement for 0x51E600.
|
| 483 |
+
///
|
| 484 |
+
/// Sends message 0x29 to CTaskTeam with buf = [team_index], then increments
|
| 485 |
+
/// WormEntry.turn_action_counter_Maybe by 14 (0x0E).
|
| 486 |
+
unsafe fn fire_surrender(worm: *mut CTaskWorm) {
|
| 487 |
+
use openwa_core::engine::ddgame::TeamArenaRef;
|
| 488 |
+
use openwa_core::engine::DDGame;
|
| 489 |
+
|
| 490 |
+
fire_send_team_message(worm, 0x29);
|
| 491 |
+
|
| 492 |
+
let ddgame = (*worm).base.base.ddgame;
|
| 493 |
+
let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
|
| 494 |
+
let team_index = (*worm).team_index as usize;
|
| 495 |
+
let worm_index = (*worm).worm_index as usize;
|
| 496 |
+
let entry = arena.team_worm_mut(team_index, worm_index);
|
| 497 |
+
entry.turn_action_counter_Maybe += 14;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
/// Mail/Mine/Mole (subtype 14) β pure Rust replacement for 0x51E670.
|
| 501 |
+
///
|
| 502 |
+
/// Conditionally calls worm->vtable[0xE](0x65) based on game version and worm state,
|
| 503 |
+
/// then sends message 0x28 to CTaskTeam, then increments
|
| 504 |
+
/// WormEntry.turn_action_counter_Maybe by 7.
|
| 505 |
+
///
|
| 506 |
+
/// Version check logic (from disassembly at 0x51E670):
|
| 507 |
+
/// - version < 2: call vtable[0xE](0x65)
|
| 508 |
+
/// - 2 <= version < 5: skip vtable call
|
| 509 |
+
/// - version >= 5 && worm state == 0x7D: call vtable
|
| 510 |
+
/// - version >= 5 && worm state == 0x78 && version < 8: call vtable
|
| 511 |
+
/// - otherwise: skip
|
| 512 |
+
unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
|
| 513 |
+
use openwa_core::engine::ddgame::TeamArenaRef;
|
| 514 |
+
use openwa_core::engine::DDGame;
|
| 515 |
+
|
| 516 |
+
let ddgame = (*worm).base.base.ddgame as *mut DDGame;
|
| 517 |
+
let version = *((ddgame as *const u8).add(0x7E40));
|
| 518 |
+
let worm_state = (*worm).state();
|
| 519 |
+
|
| 520 |
+
let should_call_vtable = version < 2
|
| 521 |
+
|| (version >= 5
|
| 522 |
+
&& (worm_state == 0x7D || (worm_state == 0x78 && version < 8)));
|
| 523 |
+
|
| 524 |
+
if should_call_vtable {
|
| 525 |
+
fire_worm_vtable_0xe(worm as u32, 0x65);
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
fire_send_team_message(worm, 0x28);
|
| 529 |
+
|
| 530 |
+
let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
|
| 531 |
+
let team_index = (*worm).team_index as usize;
|
| 532 |
+
let worm_index = (*worm).worm_index as usize;
|
| 533 |
+
let entry = arena.team_worm_mut(team_index, worm_index);
|
| 534 |
+
entry.turn_action_counter_Maybe += 7;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
// ββ Naked asm bridges βββββββββββββββββββββββββββββββββββββββ
|
| 538 |
|
| 539 |
/// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET.
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c81a4b368910b7403aa0386409b27cea530e8d105b9eee0edf155c3f2d55e5c
|
| 3 |
+
size 12078
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3adc4d59932df7aef98b2bf022f771b5313dfa7d492665451bce0573b168cdc0
|
| 3 |
+
size 1298
|