paavohtl Claude Opus 4.6 (1M context) commited on
Commit
bb59078
·
1 Parent(s): a8f286b

feat: wire in pure Rust fire_armageddon, fix 4-byte buffer offset bug

Browse files

The Rust port of fire_armageddon (0x51EA60) was complete but never
wired in due to a desync caused by a subtle buffer offset error.

Root cause: the original function writes message fields at buffer
offsets 0x04/0x08/0x0C/0x10, then passes (buffer + 4) as the data
pointer to HandleMessage. This happens because PUSH ESI (argument to
SharedData__Lookup) is cleaned by the callee's RET 4, shifting the
ESP-relative LEA by 4 bytes. The Rust port was passing buffer + 0,
putting all fields 4 bytes too high.

Also:
- Remove dead call_fire_usercall bridge (no longer needed)
- Add CTaskWorm::is_in_state() helper
- Clean up weapon_release.rs: hex→decimal constants, use is_in_state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/task/worm.rs CHANGED
@@ -430,6 +430,10 @@ impl CTaskWorm {
430
  unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
431
  }
432
 
 
 
 
 
433
  // Weapon fire dispatch state:
434
  // - Fire type/subtypes live in the WeaponEntry (via active_weapon_entry at +0x36C)
435
  // - Completion flag lives in CGameTask.subclass_data[12] (this object, at +0x3C)
 
430
  unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
431
  }
432
 
433
+ pub fn is_in_state(&self, state: WormState) -> bool {
434
+ self.state() == state as u32
435
+ }
436
+
437
  // Weapon fire dispatch state:
438
  // - Fire type/subtypes live in the WeaponEntry (via active_weapon_entry at +0x36C)
439
  // - Completion flag lives in CGameTask.subclass_data[12] (this object, at +0x3C)
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -342,7 +342,6 @@ unsafe fn fire_weapon_special(
342
  ctx: *const WeaponReleaseContext,
343
  ) {
344
  use openwa_core::game::weapon::SpecialFireSubtype as S;
345
- use openwa_core::rebase::rb;
346
 
347
  // Pointer to fire_method field, reinterpreted as fire params pointer for Girder
348
  let params_38_ptr = &raw const (*entry).fire_method as *const WeaponFireParams;
@@ -375,7 +374,7 @@ unsafe fn fire_weapon_special(
375
  Ok(S::SelectWorm) => fire_select_worm(worm),
376
  Ok(S::ScalesOfJustice) => fire_scales_of_justice(worm),
377
  Ok(S::JetPack) => CTaskWorm::set_state_raw(worm, WormState::WeaponAimed_Maybe),
378
- Ok(S::Armageddon) => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)),
379
  _ => {}
380
  }
381
  }
@@ -817,21 +816,25 @@ unsafe extern "C" fn call_play_sound_usercall(
817
  /// Sends message 0x5B to CTaskTurnGame with weapon/team info, then conditionally
818
  /// sets a gravity center point via FUN_00547E70.
819
  /// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
 
820
  unsafe fn fire_armageddon(worm: *mut CTaskWorm) {
821
  use openwa_core::rebase::rb;
822
 
823
- let ddgame = CTask::ddgame_raw(worm as *const CTask);
824
- let game_version = (*(*ddgame).game_info).game_version;
825
-
826
- // Send message 0x5B to CTaskTurnGame with weapon info buffer
 
 
 
 
827
  let team = lookup_turn_game(worm);
828
  if !team.is_null() {
829
- let mut buf = [0u8; 0x40C];
830
- // buf[0x04] = 100 (0x64), buf[0x08] = 166 (0xA6), buf[0x0C] = weapon_id, buf[0x10] = team_index
831
- buf[0x04..0x08].copy_from_slice(&100i32.to_ne_bytes());
832
- buf[0x08..0x0C].copy_from_slice(&166i32.to_ne_bytes());
833
- buf[0x0C..0x10].copy_from_slice(&((*worm).selected_weapon as u32).to_ne_bytes());
834
- buf[0x10..0x14].copy_from_slice(&(*worm).team_index.to_ne_bytes());
835
 
836
  CTaskTurnGame::handle_message_raw(
837
  team,
@@ -842,20 +845,20 @@ unsafe fn fire_armageddon(worm: *mut CTaskWorm) {
842
  );
843
  }
844
 
 
 
 
 
 
845
  // If old game version or worm state 0x69, set gravity center
846
- if game_version < 0x50 || (*worm).state() == 0x69 {
847
- // Compute half-level center from DDGame+0x77C0/0x77C4 (level dimensions)
848
  let ddgame_raw = ddgame as *const u8;
849
  let level_w = *(ddgame_raw.add(0x77C0) as *const i32);
850
  let level_h = *(ddgame_raw.add(0x77C4) as *const i32);
851
- // Convert to Fixed16.16 and halve: (value << 16) / 2
852
- // Original uses SHL 16; CDQ; SUB EAX,EDX; SAR 1 (round-toward-zero divide)
853
  let half_x = ((level_w << 16) + (if level_w < 0 { 1 } else { 0 })) >> 1;
854
  let half_y = ((level_h << 16) + (if level_h < 0 { 1 } else { 0 })) >> 1;
855
 
856
- // FUN_00547E70: usercall(ECX=half_x, EDX=half_y) + stdcall(worm), RET 0x4
857
- type SetGravityCenterFn = unsafe extern "stdcall" fn(*mut CTaskWorm);
858
- // Need naked bridge for ECX/EDX usercall params
859
  call_set_gravity_center(worm, half_x, half_y, rb(0x547E70));
860
  }
861
  }
@@ -960,26 +963,6 @@ unsafe extern "C" fn call_girder_ctor(
960
  );
961
  }
962
 
963
- /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET.
964
- /// Kept for Low Gravity until the codegen UB is resolved.
965
- #[unsafe(naked)]
966
- unsafe extern "C" fn call_fire_usercall(_eax: *const (), _worm: *mut CTaskWorm, _addr: u32) {
967
- core::arch::naked_asm!(
968
- "push ebx",
969
- "push esi",
970
- "push edi",
971
- "mov eax, [esp+16]",
972
- "mov esi, [esp+20]",
973
- "mov edi, [esp+20]",
974
- "mov ebx, [esp+24]",
975
- "call ebx",
976
- "pop edi",
977
- "pop esi",
978
- "pop ebx",
979
- "ret",
980
- );
981
- }
982
-
983
  /// Girder/GirderPack (subtype 17) — pure Rust port of 0x51E920.
984
  ///
985
  /// Plays a sound, optionally creates a visual overlay on the
 
342
  ctx: *const WeaponReleaseContext,
343
  ) {
344
  use openwa_core::game::weapon::SpecialFireSubtype as S;
 
345
 
346
  // Pointer to fire_method field, reinterpreted as fire params pointer for Girder
347
  let params_38_ptr = &raw const (*entry).fire_method as *const WeaponFireParams;
 
374
  Ok(S::SelectWorm) => fire_select_worm(worm),
375
  Ok(S::ScalesOfJustice) => fire_scales_of_justice(worm),
376
  Ok(S::JetPack) => CTaskWorm::set_state_raw(worm, WormState::WeaponAimed_Maybe),
377
+ Ok(S::Armageddon) => fire_armageddon(worm),
378
  _ => {}
379
  }
380
  }
 
816
  /// Sends message 0x5B to CTaskTurnGame with weapon/team info, then conditionally
817
  /// sets a gravity center point via FUN_00547E70.
818
  /// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
819
+ ///
820
  unsafe fn fire_armageddon(worm: *mut CTaskWorm) {
821
  use openwa_core::rebase::rb;
822
 
823
+ // Send message 0x5B (Armageddon) to CTaskTurnGame with weapon info buffer.
824
+ //
825
+ // The original allocates a 0x410-byte stack buffer, writes fields at offsets
826
+ // 0x04/0x08/0x0C/0x10 from the buffer base, then passes (buffer_base + 4)
827
+ // as the data pointer to HandleMessage (LEA ECX,[ESP+0x8] after one PUSH
828
+ // was cleaned by the SharedData lookup). So HandleMessage sees:
829
+ // data[0x00] = 100 (0x64), data[0x04] = 166 (0xA6),
830
+ // data[0x08] = weapon_id, data[0x0C] = team_index
831
  let team = lookup_turn_game(worm);
832
  if !team.is_null() {
833
+ let mut buf = [0u8; 0x410];
834
+ buf[0x00..0x04].copy_from_slice(&100i32.to_ne_bytes());
835
+ buf[0x04..0x08].copy_from_slice(&166i32.to_ne_bytes());
836
+ buf[0x08..0x0C].copy_from_slice(&((*worm).selected_weapon as u32).to_ne_bytes());
837
+ buf[0x0C..0x10].copy_from_slice(&(*worm).team_index.to_ne_bytes());
 
838
 
839
  CTaskTurnGame::handle_message_raw(
840
  team,
 
845
  );
846
  }
847
 
848
+ // Re-read DDGame AFTER HandleMessage — the original does this (MOV EAX,[ESI+0x2C]
849
+ // at 0x51EAB0 is after the CALL), and HandleMessage may modify game state.
850
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
851
+ let game_version = (*(*ddgame).game_info).game_version;
852
+
853
  // If old game version or worm state 0x69, set gravity center
854
+ if game_version < 0x50 || (*worm).is_in_state(WormState::Unknown_0x69) {
 
855
  let ddgame_raw = ddgame as *const u8;
856
  let level_w = *(ddgame_raw.add(0x77C0) as *const i32);
857
  let level_h = *(ddgame_raw.add(0x77C4) as *const i32);
858
+ // SHL 16; CDQ; SUB EAX,EDX; SAR 1 — round-toward-zero divide by 2
 
859
  let half_x = ((level_w << 16) + (if level_w < 0 { 1 } else { 0 })) >> 1;
860
  let half_y = ((level_h << 16) + (if level_h < 0 { 1 } else { 0 })) >> 1;
861
 
 
 
 
862
  call_set_gravity_center(worm, half_x, half_y, rb(0x547E70));
863
  }
864
  }
 
963
  );
964
  }
965
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
  /// Girder/GirderPack (subtype 17) — pure Rust port of 0x51E920.
967
  ///
968
  /// Plays a sound, optionally creates a visual overlay on the
crates/openwa-wormkit/src/replacements/weapon_release.rs CHANGED
@@ -164,8 +164,8 @@ unsafe extern "cdecl" fn weapon_release_impl(
164
  match FireType::try_from(fire_type) {
165
  Ok(FireType::Projectile) => match FireMethod::try_from(fire_method) {
166
  Ok(FireMethod::PlacedExplosive) => {
167
- offset_x = aim_dir_x * 0x18;
168
- offset_y = aim_dir_y * 0x18;
169
  }
170
  Ok(FireMethod::ProjectileFire) => {
171
  // Falls through to Strike (passthrough)
@@ -173,20 +173,20 @@ unsafe extern "cdecl" fn weapon_release_impl(
173
  offset_y = aim_dir_y;
174
  }
175
  Ok(FireMethod::CreateWeaponProjectile) => {
176
- offset_x = aim_dir_x * scale * 0x18;
177
- offset_y = aim_dir_y * scale * 0x18;
178
  }
179
  Ok(FireMethod::CreateArrow) => {
180
- offset_x = aim_dir_x * 0x14;
181
- offset_y = aim_dir_y * 0x14;
182
  }
183
  _ => {}
184
  },
185
  Ok(FireType::Rope) => {
186
- offset_x = aim_dir_x * scale * 0x18;
187
- offset_y = aim_dir_y * scale * 0x18;
188
  // Special Y adjustment for state 0x79
189
- if w.state() == WormState::Unknown_0x79 as u32 {
190
  offset_y += w.base.speed_y;
191
  }
192
  }
 
164
  match FireType::try_from(fire_type) {
165
  Ok(FireType::Projectile) => match FireMethod::try_from(fire_method) {
166
  Ok(FireMethod::PlacedExplosive) => {
167
+ offset_x = aim_dir_x * 24;
168
+ offset_y = aim_dir_y * 24;
169
  }
170
  Ok(FireMethod::ProjectileFire) => {
171
  // Falls through to Strike (passthrough)
 
173
  offset_y = aim_dir_y;
174
  }
175
  Ok(FireMethod::CreateWeaponProjectile) => {
176
+ offset_x = aim_dir_x * scale * 24;
177
+ offset_y = aim_dir_y * scale * 24;
178
  }
179
  Ok(FireMethod::CreateArrow) => {
180
+ offset_x = aim_dir_x * 20;
181
+ offset_y = aim_dir_y * 20;
182
  }
183
  _ => {}
184
  },
185
  Ok(FireType::Rope) => {
186
+ offset_x = aim_dir_x * scale * 24;
187
+ offset_y = aim_dir_y * scale * 24;
188
  // Special Y adjustment for state 0x79
189
+ if w.is_in_state(WormState::Unknown_0x79) {
190
  offset_y += w.base.speed_y;
191
  }
192
  }