paavohtl Claude Opus 4.6 (1M context) commited on
Commit
a60dc8d
Β·
1 Parent(s): 8962003

feat: port Surrender and Mail/Mine/Mole weapon handlers to pure Rust

Browse files

Port 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>

crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -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-0x0B: Unknown
1732
- pub _unknown_04: [u8; 8],
 
 
 
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)`.
crates/openwa-wormkit/src/replacements/replay.rs CHANGED
@@ -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
- // If scheme_header < 0 (signed) and v1/v2: clear + defaults
348
- if scheme_version <= 2 && (*(rb(va::G_SCHEME_HEADER) as *const i8)) < 0 {
349
- ptr::write_bytes(rb(va::G_SCHEME_OPTIONS) as *mut u8, 0, 0x4C);
350
- ptr::copy_nonoverlapping(
351
- rb(va::SCHEME_V3_DEFAULTS) as *const u8,
352
- rb(va::G_SCHEME_V3_DATA) as *mut u8, 0x6E,
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
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -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 => call_fire_usercall(w, w, rb(0x51E670)), // Mail/Mine/Mole (EAX=worm)
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 => call_fire_usercall(w, w, rb(0x51E600)), // Surrender (EAX=worm)
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.
testdata/replays/mail_mine_mole.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c81a4b368910b7403aa0386409b27cea530e8d105b9eee0edf155c3f2d55e5c
3
+ size 12078
testdata/replays/mail_mine_mole_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3adc4d59932df7aef98b2bf022f771b5313dfa7d492665451bce0573b168cdc0
3
+ size 1298