feat: wire in pure Rust fire_armageddon, fix 4-byte buffer offset bug
Browse filesThe 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) =>
|
| 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 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
let team = lookup_turn_game(worm);
|
| 828 |
if !team.is_null() {
|
| 829 |
-
let mut buf = [0u8;
|
| 830 |
-
|
| 831 |
-
buf[0x04..0x08].copy_from_slice(&
|
| 832 |
-
buf[0x08..0x0C].copy_from_slice(&
|
| 833 |
-
buf[0x0C..0x10].copy_from_slice(&(
|
| 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).
|
| 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 |
-
//
|
| 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 *
|
| 168 |
-
offset_y = aim_dir_y *
|
| 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 *
|
| 177 |
-
offset_y = aim_dir_y * scale *
|
| 178 |
}
|
| 179 |
Ok(FireMethod::CreateArrow) => {
|
| 180 |
-
offset_x = aim_dir_x *
|
| 181 |
-
offset_y = aim_dir_y *
|
| 182 |
}
|
| 183 |
_ => {}
|
| 184 |
},
|
| 185 |
Ok(FireType::Rope) => {
|
| 186 |
-
offset_x = aim_dir_x * scale *
|
| 187 |
-
offset_y = aim_dir_y * scale *
|
| 188 |
// Special Y adjustment for state 0x79
|
| 189 |
-
if w.
|
| 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 |
}
|