feat: port WeaponRelease (0x51C3D0) to Rust
Browse filesFull Rust reimplementation of the weapon release orchestration function
that handles ammo sync, spawn offset calculation, network timing, weapon
stat counters, sound/visual effects, and delegates to FireWeapon.
New module weapon_release.rs with:
- usercall trampoline (EAX=worm, 4 stack params, RET 0x10)
- Pure Rust: sync check, spawn offsets, delay, network timing,
SharedData HandleMessage (0x49), 4 weapon stat counters,
3 weapon category classifiers (IsSuperWeapon, CategoryA/B)
- Rust-to-Rust PlaySoundLocal via new sound::play_sound_local() helper
- Bridges for PlayWormSound/StopWormSound/SpawnEffect (addr-as-param)
- Direct call to fire_weapon_impl (made pub(crate))
- DDGame::advance_effect_rng() for secondary RNG at +0x45F0
Also adds 16 named fields to CTaskWorm (landscape_scale, shot_data,
fire_sync_frames, difficulty_level, weapon_params, sound_handle, etc.),
fills all WormState enum gaps (0x69-0x6B, 0x6F, 0x76, 0x79-0x7A,
0x84-0x85, 0x8A), and removes from_raw_unchecked antipattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/address.rs +9 -0
- crates/openwa-core/src/engine/ddgame.rs +14 -0
- crates/openwa-core/src/task/worm.rs +97 -25
- crates/openwa-wormkit/src/hook.rs +21 -0
- crates/openwa-wormkit/src/replacements/mod.rs +2 -0
- crates/openwa-wormkit/src/replacements/sound.rs +28 -0
- crates/openwa-wormkit/src/replacements/weapon.rs +14 -18
- crates/openwa-wormkit/src/replacements/weapon_release.rs +513 -0
|
@@ -345,6 +345,15 @@ pub mod va {
|
|
| 345 |
// =========================================================================
|
| 346 |
|
| 347 |
crate::define_addresses! {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
fn INIT_WEAPON_TABLE = 0x0053_CAB0;
|
| 349 |
fn COUNT_ALIVE_WORMS = 0x0052_25A0;
|
| 350 |
fn GET_AMMO = 0x0052_25E0;
|
|
|
|
| 345 |
// =========================================================================
|
| 346 |
|
| 347 |
crate::define_addresses! {
|
| 348 |
+
/// PlayWormSound: usercall(EDI=worm) + stack(sound_handle_id, volume), RET 0x8.
|
| 349 |
+
/// Stops current streaming sound at worm+0x3B0, then starts a new one.
|
| 350 |
+
fn PLAY_WORM_SOUND = 0x0051_50D0;
|
| 351 |
+
/// StopWormSound: usercall(ESI=worm), plain RET.
|
| 352 |
+
/// Stops streaming sound at worm+0x3B0 and clears the handle.
|
| 353 |
+
fn STOP_WORM_SOUND = 0x0051_5180;
|
| 354 |
+
/// SpawnEffect: complex usercall, RET 0x1C.
|
| 355 |
+
/// Builds a 0x408-byte struct from params, SharedData lookup, HandleMessage(0x56).
|
| 356 |
+
fn SPAWN_EFFECT = 0x0054_7C30;
|
| 357 |
fn INIT_WEAPON_TABLE = 0x0053_CAB0;
|
| 358 |
fn COUNT_ALIVE_WORMS = 0x0052_25A0;
|
| 359 |
fn GET_AMMO = 0x0052_25E0;
|
|
@@ -423,6 +423,20 @@ impl DDGame {
|
|
| 423 |
rng
|
| 424 |
}
|
| 425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
/// Show the "too many objects" warning on the HUD.
|
| 427 |
///
|
| 428 |
/// Sets `hud_status_code = 6` and loads string resource 0x70F into
|
|
|
|
| 423 |
rng
|
| 424 |
}
|
| 425 |
|
| 426 |
+
/// Advance the secondary effect RNG at DDGame+0x45F0 and return the new state.
|
| 427 |
+
///
|
| 428 |
+
/// Formula: `rng = rng * 0x19660D + 0x3C6EF35F` (simpler than [`advance_rng`] β no
|
| 429 |
+
/// frame_counter). Uses `team_health_ratio[0]`, the unused index-0 slot of the
|
| 430 |
+
/// 1-indexed health ratio array, repurposed by WA as a secondary RNG for weapon
|
| 431 |
+
/// release visual effects.
|
| 432 |
+
pub fn advance_effect_rng(&mut self) -> u32 {
|
| 433 |
+
let rng = (self.team_health_ratio[0] as u32)
|
| 434 |
+
.wrapping_mul(0x19660D)
|
| 435 |
+
.wrapping_add(0x3C6EF35F);
|
| 436 |
+
self.team_health_ratio[0] = rng as i32;
|
| 437 |
+
rng
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
/// Show the "too many objects" warning on the HUD.
|
| 441 |
///
|
| 442 |
/// Sets `hud_status_code = 6` and loads string resource 0x70F into
|
|
@@ -29,14 +29,20 @@ pub enum WormState {
|
|
| 29 |
Active = 0x67,
|
| 30 |
/// Active variant β observed in WormEntry. Start of weapon/action range.
|
| 31 |
ActiveVariant_Maybe = 0x68,
|
| 32 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
/// Blowtorch β using the blowtorch weapon.
|
| 34 |
Blowtorch = 0x6C,
|
| 35 |
/// Baseball Bat β swinging the baseball bat. Frequently checked in HandleMessage.
|
| 36 |
BaseballBat = 0x6D,
|
| 37 |
/// Kamikaze β performing kamikaze attack.
|
| 38 |
Kamikaze = 0x6E,
|
| 39 |
-
//
|
|
|
|
| 40 |
/// Dragon Ball β performing dragon ball attack.
|
| 41 |
DragonBall = 0x70,
|
| 42 |
/// Scales of Justice β using scales of justice.
|
|
@@ -50,14 +56,18 @@ pub enum WormState {
|
|
| 50 |
TeleportCancelled_Maybe = 0x74,
|
| 51 |
/// Fire Punch β performing fire punch attack.
|
| 52 |
FirePunch = 0x75,
|
| 53 |
-
//
|
|
|
|
| 54 |
/// Weapon selected β entered via SelectCursor (msg 0x24) from idle/active states.
|
| 55 |
/// HandleMessage sets param[0xa7]=-1 when already in this state.
|
| 56 |
WeaponSelected_Maybe = 0x77,
|
| 57 |
/// Weapon aimed β post-select state. Teleport check accepts this.
|
| 58 |
/// Also used for Magic Bullet weapon fire.
|
| 59 |
WeaponAimed_Maybe = 0x78,
|
| 60 |
-
//
|
|
|
|
|
|
|
|
|
|
| 61 |
/// Aiming with angle β entered for aimed weapons. Sets angle params.
|
| 62 |
/// Teleport check accepts this. Transitions to 0x73 on fire.
|
| 63 |
AimingAngle_Maybe = 0x7B,
|
|
@@ -80,7 +90,10 @@ pub enum WormState {
|
|
| 80 |
Dying1_Maybe = 0x82,
|
| 81 |
/// Dying variant 2 β checked alongside 0x82 in HandleMessage.
|
| 82 |
Dying2_Maybe = 0x83,
|
| 83 |
-
//
|
|
|
|
|
|
|
|
|
|
| 84 |
/// Dead β set by Surrender (msg 0x29). Frequently checked in HandleMessage.
|
| 85 |
Dead = 0x86,
|
| 86 |
/// Dead variant 3.
|
|
@@ -89,19 +102,14 @@ pub enum WormState {
|
|
| 89 |
Unknown_0x88 = 0x88,
|
| 90 |
/// Dying/special animation (from WormEntry state documentation).
|
| 91 |
DyingAnimation_Maybe = 0x89,
|
| 92 |
-
//
|
|
|
|
| 93 |
/// Unknown state checked in CTaskTeam handlers.
|
| 94 |
/// Grouped with idle states in HandleMessage switch cases.
|
| 95 |
Unknown_0x8B = 0x8B,
|
| 96 |
}
|
| 97 |
|
| 98 |
-
impl WormState {
|
| 99 |
-
/// Create a WormState from a raw u8 value without validation.
|
| 100 |
-
/// Use for states not yet added to the enum.
|
| 101 |
-
pub const unsafe fn from_raw_unchecked(val: u8) -> Self {
|
| 102 |
-
core::mem::transmute(val)
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
|
| 106 |
/// Raw-pointer methods for CTaskWorm.
|
| 107 |
///
|
|
@@ -277,8 +285,18 @@ pub struct CTaskWorm {
|
|
| 277 |
pub display_health_raw: u32,
|
| 278 |
/// 0x17C: Target health (matches WormEntry.health). Same byte layout as display_health.
|
| 279 |
pub target_health_raw: u32,
|
| 280 |
-
/// 0x180β
|
| 281 |
-
pub _unknown_180: [u8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
/// 0x1A8: Facing direction copy. -1 = left, +1 = right (same as +0x3DC).
|
| 283 |
pub facing_direction_2: i32,
|
| 284 |
/// 0x1AC: Inverted facing direction. +1 = left, -1 = right.
|
|
@@ -289,10 +307,16 @@ pub struct CTaskWorm {
|
|
| 289 |
/// in one direction. Resets to 0 when movement resumes after a stop.
|
| 290 |
/// Set to -1 when the worm is blocked (e.g. hits a wall).
|
| 291 |
pub movement_streak: i32,
|
| 292 |
-
/// 0x1F0β
|
| 293 |
-
pub _unknown_1f0: [u8;
|
| 294 |
-
///
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
/// 0x24C: Aim angle (fixed-point 16.16, range 0..0x10000 = 0..360 degrees).
|
| 297 |
pub aim_angle: u32,
|
| 298 |
/// 0x250β0x267: Unknown
|
|
@@ -301,10 +325,52 @@ pub struct CTaskWorm {
|
|
| 301 |
pub show_cursor: u32,
|
| 302 |
/// 0x26Cβ0x283: Unknown
|
| 303 |
pub _unknown_26c: [u8; 0x284 - 0x26C],
|
| 304 |
-
/// 0x284
|
| 305 |
-
|
| 306 |
-
///
|
| 307 |
-
pub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
/// 0x2F0: Worm name, null-terminated (max 17 chars, from spawn init_data+3)
|
| 309 |
#[field(kind = "CString")]
|
| 310 |
pub worm_name: [u8; 0x11],
|
|
@@ -331,8 +397,14 @@ pub struct CTaskWorm {
|
|
| 331 |
/// Contains fire type (+0x30), subtypes (+0x34/+0x38), and completion flag (+0x3C).
|
| 332 |
/// Used by WeaponRelease: `MOV EAX, [EDI+0x36C]` before calling FireWeapon.
|
| 333 |
pub active_weapon_entry: *mut WeaponEntry,
|
| 334 |
-
/// 0x370β
|
| 335 |
-
pub _unknown_370: [u8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
/// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
|
| 337 |
pub facing_direction: i32,
|
| 338 |
/// 0x3E0β0x3E3: Unknown
|
|
|
|
| 29 |
Active = 0x67,
|
| 30 |
/// Active variant β observed in WormEntry. Start of weapon/action range.
|
| 31 |
ActiveVariant_Maybe = 0x68,
|
| 32 |
+
/// Unknown (within weapon/action range).
|
| 33 |
+
Unknown_0x69 = 0x69,
|
| 34 |
+
/// Unknown (within weapon/action range).
|
| 35 |
+
Unknown_0x6A = 0x6A,
|
| 36 |
+
/// Unknown (within weapon/action range).
|
| 37 |
+
Unknown_0x6B = 0x6B,
|
| 38 |
/// Blowtorch β using the blowtorch weapon.
|
| 39 |
Blowtorch = 0x6C,
|
| 40 |
/// Baseball Bat β swinging the baseball bat. Frequently checked in HandleMessage.
|
| 41 |
BaseballBat = 0x6D,
|
| 42 |
/// Kamikaze β performing kamikaze attack.
|
| 43 |
Kamikaze = 0x6E,
|
| 44 |
+
/// Air Strike pending β set when air strike fires with `_unknown_208 == 0`.
|
| 45 |
+
AirStrikePending_Maybe = 0x6F,
|
| 46 |
/// Dragon Ball β performing dragon ball attack.
|
| 47 |
DragonBall = 0x70,
|
| 48 |
/// Scales of Justice β using scales of justice.
|
|
|
|
| 56 |
TeleportCancelled_Maybe = 0x74,
|
| 57 |
/// Fire Punch β performing fire punch attack.
|
| 58 |
FirePunch = 0x75,
|
| 59 |
+
/// Unknown (within weapon/action range).
|
| 60 |
+
Unknown_0x76 = 0x76,
|
| 61 |
/// Weapon selected β entered via SelectCursor (msg 0x24) from idle/active states.
|
| 62 |
/// HandleMessage sets param[0xa7]=-1 when already in this state.
|
| 63 |
WeaponSelected_Maybe = 0x77,
|
| 64 |
/// Weapon aimed β post-select state. Teleport check accepts this.
|
| 65 |
/// Also used for Magic Bullet weapon fire.
|
| 66 |
WeaponAimed_Maybe = 0x78,
|
| 67 |
+
/// Unknown. Checked in WeaponRelease spawn offset (type 2, Y adjustment when state == 0x79).
|
| 68 |
+
Unknown_0x79 = 0x79,
|
| 69 |
+
/// Unknown.
|
| 70 |
+
Unknown_0x7A = 0x7A,
|
| 71 |
/// Aiming with angle β entered for aimed weapons. Sets angle params.
|
| 72 |
/// Teleport check accepts this. Transitions to 0x73 on fire.
|
| 73 |
AimingAngle_Maybe = 0x7B,
|
|
|
|
| 90 |
Dying1_Maybe = 0x82,
|
| 91 |
/// Dying variant 2 β checked alongside 0x82 in HandleMessage.
|
| 92 |
Dying2_Maybe = 0x83,
|
| 93 |
+
/// Unknown.
|
| 94 |
+
Unknown_0x84 = 0x84,
|
| 95 |
+
/// Unknown.
|
| 96 |
+
Unknown_0x85 = 0x85,
|
| 97 |
/// Dead β set by Surrender (msg 0x29). Frequently checked in HandleMessage.
|
| 98 |
Dead = 0x86,
|
| 99 |
/// Dead variant 3.
|
|
|
|
| 102 |
Unknown_0x88 = 0x88,
|
| 103 |
/// Dying/special animation (from WormEntry state documentation).
|
| 104 |
DyingAnimation_Maybe = 0x89,
|
| 105 |
+
/// Unknown. End of weapon/action range.
|
| 106 |
+
Unknown_0x8A = 0x8A,
|
| 107 |
/// Unknown state checked in CTaskTeam handlers.
|
| 108 |
/// Grouped with idle states in HandleMessage switch cases.
|
| 109 |
Unknown_0x8B = 0x8B,
|
| 110 |
}
|
| 111 |
|
| 112 |
+
impl WormState {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
/// Raw-pointer methods for CTaskWorm.
|
| 115 |
///
|
|
|
|
| 285 |
pub display_health_raw: u32,
|
| 286 |
/// 0x17C: Target health (matches WormEntry.health). Same byte layout as display_health.
|
| 287 |
pub target_health_raw: u32,
|
| 288 |
+
/// 0x180β0x18F: Unknown
|
| 289 |
+
pub _unknown_180: [u8; 0x190 - 0x180],
|
| 290 |
+
/// 0x190: Secondary position/mode field. Air strike temporarily swaps this with +0x34.
|
| 291 |
+
pub _unknown_190: i32,
|
| 292 |
+
/// 0x194β0x197: Unknown
|
| 293 |
+
pub _unknown_194: [u8; 4],
|
| 294 |
+
/// 0x198: Cleared after air strike fire.
|
| 295 |
+
pub _unknown_198: i32,
|
| 296 |
+
/// 0x19C: Cleared after air strike fire.
|
| 297 |
+
pub _unknown_19c: i32,
|
| 298 |
+
/// 0x1A0β0x1A7: Unknown
|
| 299 |
+
pub _unknown_1a0: [u8; 0x1A8 - 0x1A0],
|
| 300 |
/// 0x1A8: Facing direction copy. -1 = left, +1 = right (same as +0x3DC).
|
| 301 |
pub facing_direction_2: i32,
|
| 302 |
/// 0x1AC: Inverted facing direction. +1 = left, -1 = right.
|
|
|
|
| 307 |
/// in one direction. Resets to 0 when movement resumes after a stop.
|
| 308 |
/// Set to -1 when the worm is blocked (e.g. hits a wall).
|
| 309 |
pub movement_streak: i32,
|
| 310 |
+
/// 0x1F0β0x207: Unknown
|
| 311 |
+
pub _unknown_1f0: [u8; 0x208 - 0x1F0],
|
| 312 |
+
/// 0x208: Action/mode flag. Checked in air strike (must be 0 to fire).
|
| 313 |
+
/// Cleared after air strike completes.
|
| 314 |
+
pub _unknown_208: i32,
|
| 315 |
+
/// 0x20Cβ0x247: Unknown
|
| 316 |
+
pub _unknown_20c: [u8; 0x248 - 0x20C],
|
| 317 |
+
/// 0x248: Landscape scale factor for spawn offset calculation.
|
| 318 |
+
/// Used by WeaponRelease to convert aim params to world-space projectile offsets.
|
| 319 |
+
pub landscape_scale: i32,
|
| 320 |
/// 0x24C: Aim angle (fixed-point 16.16, range 0..0x10000 = 0..360 degrees).
|
| 321 |
pub aim_angle: u32,
|
| 322 |
/// 0x250β0x267: Unknown
|
|
|
|
| 325 |
pub show_cursor: u32,
|
| 326 |
/// 0x26Cβ0x283: Unknown
|
| 327 |
pub _unknown_26c: [u8; 0x284 - 0x26C],
|
| 328 |
+
/// 0x284: Shot/aim data 1 (slot 5 in GetEntityData query 0x7D4).
|
| 329 |
+
/// For weapons 0x22/0x24 (Teleport/Freeze), copied from shot_data_2 on first fire.
|
| 330 |
+
/// Sent in HandleMessage 0x49 buffer during WeaponRelease.
|
| 331 |
+
pub shot_data_1: u32,
|
| 332 |
+
/// 0x288: Shot/aim data 2 (slot 6 in GetEntityData query 0x7D4).
|
| 333 |
+
pub shot_data_2: u32,
|
| 334 |
+
/// 0x28Cβ0x28F: Unknown
|
| 335 |
+
pub _unknown_28c: [u8; 4],
|
| 336 |
+
/// 0x290: Fire sync frame counter 1. Compared with fire_sync_frame_2
|
| 337 |
+
/// in WeaponRelease; when equal, weapon slot table is reset.
|
| 338 |
+
pub fire_sync_frame_1: i32,
|
| 339 |
+
/// 0x294: Fire sync frame counter 2.
|
| 340 |
+
pub fire_sync_frame_2: i32,
|
| 341 |
+
/// 0x298β0x2BB: Unknown
|
| 342 |
+
pub _unknown_298: [u8; 0x2BC - 0x298],
|
| 343 |
+
/// 0x2BC: Network client index. Compared against max client count
|
| 344 |
+
/// (5 or 10 depending on FE version) to compute network delay.
|
| 345 |
+
pub network_client_index: i32,
|
| 346 |
+
/// 0x2C0: Difficulty level (0 = easier/30-frame delay, 1 = harder/60-frame delay).
|
| 347 |
+
pub difficulty_level: i32,
|
| 348 |
+
/// 0x2C4β0x2C7: Unknown
|
| 349 |
+
pub _unknown_2c4: [u8; 4],
|
| 350 |
+
/// 0x2C8: Network/sound condition flag. In type 2 (rope) sound dispatch,
|
| 351 |
+
/// sound plays only when this is 0 OR when it equals 1.
|
| 352 |
+
pub _unknown_2c8: i32,
|
| 353 |
+
/// 0x2CC: Network flag. Nonzero = network mode active.
|
| 354 |
+
/// Used in sound dispatch conditions and HandleMessage 0x49 buffer.
|
| 355 |
+
pub _unknown_2cc: i32,
|
| 356 |
+
/// 0x2D0β0x2DF: Unknown
|
| 357 |
+
pub _unknown_2d0: [u8; 0x2E0 - 0x2D0],
|
| 358 |
+
/// 0x2E0: Weapon parameter 1. Polymorphic per weapon:
|
| 359 |
+
/// - WeaponRelease: ammo_per_turn (copied to release context)
|
| 360 |
+
/// - Air Strike: fire position X
|
| 361 |
+
/// - Freeze: freeze effect X position
|
| 362 |
+
pub weapon_param_1: i32,
|
| 363 |
+
/// 0x2E4: Weapon parameter 2. Polymorphic per weapon:
|
| 364 |
+
/// - WeaponRelease: ammo_per_slot (copied to release context)
|
| 365 |
+
/// - Air Strike: fire position Y
|
| 366 |
+
/// - Freeze: freeze effect Y position
|
| 367 |
+
pub weapon_param_2: i32,
|
| 368 |
+
/// 0x2E8β0x2EB: Unknown
|
| 369 |
+
pub _unknown_2e8: [u8; 4],
|
| 370 |
+
/// 0x2EC: Weapon parameter 3 / launch count.
|
| 371 |
+
/// - Weapons 0x22/0x24: checked == 0 to copy shot_data_2 β shot_data_1
|
| 372 |
+
/// - Freeze: freeze target entity ID
|
| 373 |
+
pub weapon_param_3: i32,
|
| 374 |
/// 0x2F0: Worm name, null-terminated (max 17 chars, from spawn init_data+3)
|
| 375 |
#[field(kind = "CString")]
|
| 376 |
pub worm_name: [u8; 0x11],
|
|
|
|
| 397 |
/// Contains fire type (+0x30), subtypes (+0x34/+0x38), and completion flag (+0x3C).
|
| 398 |
/// Used by WeaponRelease: `MOV EAX, [EDI+0x36C]` before calling FireWeapon.
|
| 399 |
pub active_weapon_entry: *mut WeaponEntry,
|
| 400 |
+
/// 0x370β0x3AF: Unknown (rope anchor, weapon-specific data, etc.)
|
| 401 |
+
pub _unknown_370: [u8; 0x3B0 - 0x370],
|
| 402 |
+
/// 0x3B0: Streaming sound handle. Nonzero when a worm sound effect
|
| 403 |
+
/// (e.g., weapon charge-up) is actively playing. PlayWormSound stores the
|
| 404 |
+
/// new handle here; StopWormSound clears it.
|
| 405 |
+
pub sound_handle: u32,
|
| 406 |
+
/// 0x3B4β0x3DB: Unknown
|
| 407 |
+
pub _unknown_3b4: [u8; 0x3DC - 0x3B4],
|
| 408 |
/// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
|
| 409 |
pub facing_direction: i32,
|
| 410 |
/// 0x3E0β0x3E3: Unknown
|
|
@@ -77,6 +77,27 @@ macro_rules! usercall_trampoline {
|
|
| 77 |
}
|
| 78 |
};
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
// 1 register arg, 5 stack params, ret N
|
| 81 |
(fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
|
| 82 |
stack_params = 5; ret_bytes = $ret:literal) => {
|
|
|
|
| 77 |
}
|
| 78 |
};
|
| 79 |
|
| 80 |
+
// 1 register arg, 4 stack params, ret N
|
| 81 |
+
(fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
|
| 82 |
+
stack_params = 4; ret_bytes = $ret:literal) => {
|
| 83 |
+
#[unsafe(naked)]
|
| 84 |
+
unsafe extern "C" fn $name() {
|
| 85 |
+
core::arch::naked_asm!(
|
| 86 |
+
"push edx",
|
| 87 |
+
"push [esp+20]",
|
| 88 |
+
"push [esp+20]",
|
| 89 |
+
"push [esp+20]",
|
| 90 |
+
"push [esp+20]",
|
| 91 |
+
concat!("push ", stringify!($reg)),
|
| 92 |
+
"call {impl_fn}",
|
| 93 |
+
"add esp, 20",
|
| 94 |
+
"pop edx",
|
| 95 |
+
concat!("ret ", $ret),
|
| 96 |
+
impl_fn = sym $impl,
|
| 97 |
+
);
|
| 98 |
+
}
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
// 1 register arg, 5 stack params, ret N
|
| 102 |
(fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
|
| 103 |
stack_params = 5; ret_bytes = $ret:literal) => {
|
|
@@ -16,6 +16,7 @@ mod speech;
|
|
| 16 |
mod task;
|
| 17 |
mod team;
|
| 18 |
mod weapon;
|
|
|
|
| 19 |
|
| 20 |
/// Write gameplay milestone report. Called from DLL_PROCESS_DETACH.
|
| 21 |
pub fn write_gameplay_report() {
|
|
@@ -41,5 +42,6 @@ pub fn install_all() -> Result<(), String> {
|
|
| 41 |
game_state_hooks::install()?;
|
| 42 |
replay::install()?;
|
| 43 |
task::install()?;
|
|
|
|
| 44 |
Ok(())
|
| 45 |
}
|
|
|
|
| 16 |
mod task;
|
| 17 |
mod team;
|
| 18 |
mod weapon;
|
| 19 |
+
mod weapon_release;
|
| 20 |
|
| 21 |
/// Write gameplay milestone report. Called from DLL_PROCESS_DETACH.
|
| 22 |
pub fn write_gameplay_report() {
|
|
|
|
| 42 |
game_state_hooks::install()?;
|
| 43 |
replay::install()?;
|
| 44 |
task::install()?;
|
| 45 |
+
weapon_release::install()?;
|
| 46 |
Ok(())
|
| 47 |
}
|
|
@@ -130,6 +130,34 @@ unsafe extern "cdecl" fn play_sound_local_impl(
|
|
| 130 |
1
|
| 131 |
}
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
// ============================================================
|
| 134 |
// Sound dispatch helpers (bridge: queue β DSSound)
|
| 135 |
// ============================================================
|
|
|
|
| 130 |
1
|
| 131 |
}
|
| 132 |
|
| 133 |
+
/// Play a local sound on a task β Rust-callable version of PlaySoundLocal (0x4FDFE0).
|
| 134 |
+
///
|
| 135 |
+
/// Queues the sound, marks it as local, records the emitter position, and
|
| 136 |
+
/// increments the task's local sound count. Returns true on success.
|
| 137 |
+
pub(crate) unsafe fn play_sound_local(
|
| 138 |
+
task: *mut CGameTask,
|
| 139 |
+
sound_id: u32,
|
| 140 |
+
flags: u32,
|
| 141 |
+
volume: u32,
|
| 142 |
+
pitch: u32,
|
| 143 |
+
) -> bool {
|
| 144 |
+
let gt = &*task;
|
| 145 |
+
let ddgame = gt.base.ddgame;
|
| 146 |
+
let entry = match queue_sound(ddgame, sound_id, flags, volume, pitch) {
|
| 147 |
+
Some(e) => e,
|
| 148 |
+
None => return false,
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
(*entry).is_local = 1;
|
| 152 |
+
|
| 153 |
+
let emitter = >.sound_emitter;
|
| 154 |
+
(*entry).secondary_vtable = emitter as *const _ as u32;
|
| 155 |
+
((*emitter.vtable).get_position)(emitter, &mut (*entry).pos_x, &mut (*entry).pos_y);
|
| 156 |
+
|
| 157 |
+
(*task).sound_emitter.local_sound_count += 1;
|
| 158 |
+
true
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
// ============================================================
|
| 162 |
// Sound dispatch helpers (bridge: queue β DSSound)
|
| 163 |
// ============================================================
|
|
@@ -195,7 +195,7 @@ unsafe extern "C" fn trampoline_fire_weapon() {
|
|
| 195 |
///
|
| 196 |
/// Completion flag at worm+0x3C (CGameTask.subclass_data[12]).
|
| 197 |
/// Params pointer at entry+0x3C (WeaponEntry.fire_complete) β different object, same offset.
|
| 198 |
-
unsafe extern "cdecl" fn fire_weapon_impl(
|
| 199 |
entry: *const WeaponEntry, local_struct: *const u8, worm: *mut CTaskWorm,
|
| 200 |
) {
|
| 201 |
use openwa_core::rebase::rb;
|
|
@@ -564,11 +564,8 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
|
|
| 564 |
unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
|
| 565 |
use openwa_core::rebase::rb;
|
| 566 |
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
if field_208 == 0 {
|
| 571 |
-
CTaskWorm::set_state_raw(worm,WormState::from_raw_unchecked(0x6F));
|
| 572 |
return;
|
| 573 |
}
|
| 574 |
|
|
@@ -576,14 +573,14 @@ unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
|
|
| 576 |
call_worm_play_sound(worm, 0x36, 0x10000, 3, rb(0x515020));
|
| 577 |
|
| 578 |
// Spawn visual effect: usercall(EAX=0x80000, ECX=x) + stdcall(y, 0, 0, 0, 600, 0x10000, 0x1999)
|
| 579 |
-
let fire_x =
|
| 580 |
-
let fire_y =
|
| 581 |
call_spawn_effect(fire_x, fire_y, rb(0x547C30));
|
| 582 |
|
| 583 |
// Temporarily swap worm+0x34 with worm+0x190, call position update, restore
|
|
|
|
| 584 |
let saved_34 = *(worm_raw.add(0x34) as *const i32);
|
| 585 |
-
|
| 586 |
-
*(worm_raw.add(0x34) as *mut i32) = field_190;
|
| 587 |
call_position_update(worm, fire_x, fire_y, rb(0x4FE070));
|
| 588 |
*(worm_raw.add(0x34) as *mut i32) = saved_34;
|
| 589 |
|
|
@@ -599,10 +596,10 @@ unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
|
|
| 599 |
|
| 600 |
// Clear action fields
|
| 601 |
*(worm_raw.add(0x48) as *mut i32) = 0;
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
|
| 607 |
// FUN_0050D450: usercall(ESI=worm), cleanup/landing check
|
| 608 |
call_worm_landing_check(worm, rb(0x50D450));
|
|
@@ -963,10 +960,9 @@ unsafe fn fire_freeze(worm: *mut CTaskWorm) {
|
|
| 963 |
let game_version = (*(*ddgame).game_info).game_version;
|
| 964 |
|
| 965 |
// Read freeze position and target from CTaskWorm fields
|
| 966 |
-
let
|
| 967 |
-
let
|
| 968 |
-
let
|
| 969 |
-
let freeze_target = *(worm_raw.add(0x2EC) as *const i32);
|
| 970 |
|
| 971 |
// Choose sound: 0x70 if freeze has target or old version, else 0x73
|
| 972 |
let sound_id: u32 = if freeze_target != 0 || game_version < 0x21 {
|
|
|
|
| 195 |
///
|
| 196 |
/// Completion flag at worm+0x3C (CGameTask.subclass_data[12]).
|
| 197 |
/// Params pointer at entry+0x3C (WeaponEntry.fire_complete) β different object, same offset.
|
| 198 |
+
pub(crate) unsafe extern "cdecl" fn fire_weapon_impl(
|
| 199 |
entry: *const WeaponEntry, local_struct: *const u8, worm: *mut CTaskWorm,
|
| 200 |
) {
|
| 201 |
use openwa_core::rebase::rb;
|
|
|
|
| 564 |
unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
|
| 565 |
use openwa_core::rebase::rb;
|
| 566 |
|
| 567 |
+
if (*worm)._unknown_208 == 0 {
|
| 568 |
+
CTaskWorm::set_state_raw(worm,WormState::AirStrikePending_Maybe);
|
|
|
|
|
|
|
|
|
|
| 569 |
return;
|
| 570 |
}
|
| 571 |
|
|
|
|
| 573 |
call_worm_play_sound(worm, 0x36, 0x10000, 3, rb(0x515020));
|
| 574 |
|
| 575 |
// Spawn visual effect: usercall(EAX=0x80000, ECX=x) + stdcall(y, 0, 0, 0, 600, 0x10000, 0x1999)
|
| 576 |
+
let fire_x = (*worm).weapon_param_1;
|
| 577 |
+
let fire_y = (*worm).weapon_param_2;
|
| 578 |
call_spawn_effect(fire_x, fire_y, rb(0x547C30));
|
| 579 |
|
| 580 |
// Temporarily swap worm+0x34 with worm+0x190, call position update, restore
|
| 581 |
+
let worm_raw = worm as *mut u8;
|
| 582 |
let saved_34 = *(worm_raw.add(0x34) as *const i32);
|
| 583 |
+
*(worm_raw.add(0x34) as *mut i32) = (*worm)._unknown_190;
|
|
|
|
| 584 |
call_position_update(worm, fire_x, fire_y, rb(0x4FE070));
|
| 585 |
*(worm_raw.add(0x34) as *mut i32) = saved_34;
|
| 586 |
|
|
|
|
| 596 |
|
| 597 |
// Clear action fields
|
| 598 |
*(worm_raw.add(0x48) as *mut i32) = 0;
|
| 599 |
+
(*worm)._unknown_208 = 0;
|
| 600 |
+
(*worm)._unknown_198 = 0;
|
| 601 |
+
(*worm)._unknown_19c = 0;
|
| 602 |
+
(*worm).facing_direction_inv = 0;
|
| 603 |
|
| 604 |
// FUN_0050D450: usercall(ESI=worm), cleanup/landing check
|
| 605 |
call_worm_landing_check(worm, rb(0x50D450));
|
|
|
|
| 960 |
let game_version = (*(*ddgame).game_info).game_version;
|
| 961 |
|
| 962 |
// Read freeze position and target from CTaskWorm fields
|
| 963 |
+
let freeze_x = (*worm).weapon_param_1;
|
| 964 |
+
let freeze_y = (*worm).weapon_param_2;
|
| 965 |
+
let freeze_target = (*worm).weapon_param_3;
|
|
|
|
| 966 |
|
| 967 |
// Choose sound: 0x70 if freeze has target or old version, else 0x73
|
| 968 |
let sound_id: u32 = if freeze_target != 0 || game_version < 0x21 {
|
|
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! WeaponRelease hook (0x51C3D0).
|
| 2 |
+
//!
|
| 3 |
+
//! Orchestrates weapon firing: ammo sync, spawn offset calculation, network timing,
|
| 4 |
+
//! weapon stat counters, sound/visual effects, then delegates to FireWeapon (in weapon.rs).
|
| 5 |
+
//!
|
| 6 |
+
//! Convention: usercall(EAX=CTaskWorm*) + 4 stack params, RET 0x10.
|
| 7 |
+
|
| 8 |
+
use openwa_core::address::va;
|
| 9 |
+
use openwa_core::log::log_line;
|
| 10 |
+
use openwa_core::rebase::rb;
|
| 11 |
+
use openwa_core::task::worm::CTaskWorm;
|
| 12 |
+
use openwa_core::task::{CGameTask, SharedDataTable, Task};
|
| 13 |
+
|
| 14 |
+
use crate::hook::{self, usercall_trampoline};
|
| 15 |
+
use crate::replacements::{sound, weapon};
|
| 16 |
+
|
| 17 |
+
// ββ Trampoline ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
+
|
| 19 |
+
usercall_trampoline!(fn trampoline_weapon_release; impl_fn = weapon_release_impl;
|
| 20 |
+
reg = eax; stack_params = 4; ret_bytes = "0x10");
|
| 21 |
+
|
| 22 |
+
// ββ WeaponReleaseContext ββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
/// The 0x2C-byte stack-local struct populated by WeaponRelease and passed to
|
| 25 |
+
/// FireWeapon as the `local_struct` (ECX) parameter.
|
| 26 |
+
#[repr(C)]
|
| 27 |
+
#[derive(Clone, Copy)]
|
| 28 |
+
struct WeaponReleaseContext {
|
| 29 |
+
team_id: u32,
|
| 30 |
+
worm_id: u32,
|
| 31 |
+
param_1: u32,
|
| 32 |
+
param_2: u32,
|
| 33 |
+
spawn_offset_x: i32,
|
| 34 |
+
spawn_offset_y: i32,
|
| 35 |
+
ammo_per_turn: u32,
|
| 36 |
+
ammo_per_slot: u32,
|
| 37 |
+
_zero: u32,
|
| 38 |
+
delay: u32,
|
| 39 |
+
network_delay: i32,
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const _: () = assert!(core::mem::size_of::<WeaponReleaseContext>() == 0x2C);
|
| 43 |
+
|
| 44 |
+
// ββ Weapon category classifiers (pure Rust) βββββββββββββββββ
|
| 45 |
+
|
| 46 |
+
/// IsSuperWeapon (0x565960): returns true for "super weapon" IDs.
|
| 47 |
+
/// param_1 is the byte at DDGame+0x7E3F; for weapon 0x3B (SelectWorm) it returns param_1.
|
| 48 |
+
fn is_super_weapon(weapon_id: u32, ddgame_7e3f: u8) -> bool {
|
| 49 |
+
match weapon_id {
|
| 50 |
+
10 | 0x13 | 0x1D | 0x1E | 0x1F | 0x24 | 0x29 | 0x2A | 0x2D | 0x2E | 0x31 | 0x32
|
| 51 |
+
| 0x33 | 0x36 | 0x37 | 0x38 | 0x3C | 0x3D => true,
|
| 52 |
+
0x3B => ddgame_7e3f != 0,
|
| 53 |
+
_ => false,
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/// FUN_005658C0: weapon category A β checks if weapon is in a specific set.
|
| 58 |
+
fn is_weapon_category_a(weapon_id: u32) -> bool {
|
| 59 |
+
matches!(
|
| 60 |
+
weapon_id,
|
| 61 |
+
4 | 5 | 0x17 | 0x18 | 0x19 | 0x1A | 0x1F | 0x2D | 0x2E | 0x30 | 0x32 | 0x34 | 0x35
|
| 62 |
+
| 0x36
|
| 63 |
+
)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/// FUN_00565920: weapon category B β checks if weapon is in a specific set.
|
| 67 |
+
fn is_weapon_category_b(weapon_id: u32) -> bool {
|
| 68 |
+
matches!(weapon_id, 0x1C | 0x2C | 0x2F | 0x32)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// ββ Fixed-point multiply ββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
|
| 73 |
+
/// Fixed-point 16.16 multiply: (a * b) >> 16, using SHRD like the original.
|
| 74 |
+
/// Matches the `IMUL + SHRD EAX,EDX,0x10` pattern in the disassembly.
|
| 75 |
+
#[inline(always)]
|
| 76 |
+
fn fixed_mul_shrd(a: i32, b: i32) -> i32 {
|
| 77 |
+
let product = (a as i64) * (b as i64);
|
| 78 |
+
// SHRD EAX,EDX,0x10: shift the full 64-bit result right by 16
|
| 79 |
+
(product >> 16) as i32
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// ββ Main implementation βββββββββββββββββββββββββββββββββββββ
|
| 83 |
+
|
| 84 |
+
unsafe extern "cdecl" fn weapon_release_impl(
|
| 85 |
+
worm: *mut CTaskWorm,
|
| 86 |
+
param_1: u32,
|
| 87 |
+
param_2: u32,
|
| 88 |
+
param_3: i32,
|
| 89 |
+
param_4: i32,
|
| 90 |
+
) {
|
| 91 |
+
let w = &*worm;
|
| 92 |
+
|
| 93 |
+
// Initialize context struct to zero
|
| 94 |
+
let mut ctx = WeaponReleaseContext {
|
| 95 |
+
team_id: 0,
|
| 96 |
+
worm_id: 0,
|
| 97 |
+
param_1: 0,
|
| 98 |
+
param_2: 0,
|
| 99 |
+
spawn_offset_x: 0,
|
| 100 |
+
spawn_offset_y: 0,
|
| 101 |
+
ammo_per_turn: 0,
|
| 102 |
+
ammo_per_slot: 0,
|
| 103 |
+
_zero: 0,
|
| 104 |
+
delay: 0,
|
| 105 |
+
network_delay: 0,
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
// ββ 1. Sync check βββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
if w.fire_sync_frame_1 == w.fire_sync_frame_2 {
|
| 110 |
+
let ddgame = w.ddgame() as *mut u8;
|
| 111 |
+
*(ddgame.add(0x72E4) as *mut u32) = 0x0E;
|
| 112 |
+
let mut offset = 0u32;
|
| 113 |
+
while (offset as i32) < 0x118 {
|
| 114 |
+
*(ddgame.add(0x73B0).add(offset as usize) as *mut u32) = 0;
|
| 115 |
+
offset += 0x14;
|
| 116 |
+
}
|
| 117 |
+
*(ddgame.add(0x739C) as *mut u32) = 0;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// ββ 2. Populate context fields ββββββββββββββββββββββββββ
|
| 121 |
+
let speed_x = w.base.pos_x.0;
|
| 122 |
+
let speed_y = w.base.pos_y.0;
|
| 123 |
+
ctx.team_id = w.team_index;
|
| 124 |
+
ctx.worm_id = w.worm_index;
|
| 125 |
+
ctx.ammo_per_turn = w.weapon_param_1 as u32;
|
| 126 |
+
ctx.param_2 = param_2;
|
| 127 |
+
ctx.param_1 = param_1;
|
| 128 |
+
ctx.ammo_per_slot = w.weapon_param_2 as u32;
|
| 129 |
+
|
| 130 |
+
let entry = w.active_weapon_entry;
|
| 131 |
+
let fire_type = (*entry).fire_type;
|
| 132 |
+
let fire_subtype_34 = (*entry).fire_subtype_34;
|
| 133 |
+
let fire_subtype_38 = (*entry).fire_subtype_38;
|
| 134 |
+
|
| 135 |
+
// ββ 3. Spawn offset calculation βββββββββββββββββββββββββ
|
| 136 |
+
let landscape_scale = w.landscape_scale;
|
| 137 |
+
|
| 138 |
+
match fire_type {
|
| 139 |
+
1 => match fire_subtype_38 {
|
| 140 |
+
1 => {
|
| 141 |
+
ctx.spawn_offset_x = param_3 * 0x18;
|
| 142 |
+
ctx.spawn_offset_y = param_4 * 0x18;
|
| 143 |
+
}
|
| 144 |
+
2 => {
|
| 145 |
+
// Falls through to type 3 (passthrough)
|
| 146 |
+
ctx.spawn_offset_x = param_3;
|
| 147 |
+
ctx.spawn_offset_y = param_4;
|
| 148 |
+
}
|
| 149 |
+
3 => {
|
| 150 |
+
ctx.spawn_offset_x = fixed_mul_shrd(param_3, landscape_scale) * 0x18;
|
| 151 |
+
ctx.spawn_offset_y = fixed_mul_shrd(param_4, landscape_scale) * 0x18;
|
| 152 |
+
}
|
| 153 |
+
4 => {
|
| 154 |
+
ctx.spawn_offset_x = param_3 * 0x14;
|
| 155 |
+
ctx.spawn_offset_y = param_4 * 0x14;
|
| 156 |
+
}
|
| 157 |
+
_ => {}
|
| 158 |
+
},
|
| 159 |
+
2 => {
|
| 160 |
+
ctx.spawn_offset_x = fixed_mul_shrd(param_3, landscape_scale) * 0x18;
|
| 161 |
+
ctx.spawn_offset_y = fixed_mul_shrd(param_4, landscape_scale) * 0x18;
|
| 162 |
+
// Special Y adjustment for angle 0x79
|
| 163 |
+
if w.state() == 0x79 {
|
| 164 |
+
ctx.spawn_offset_y += w.base.speed_y.0;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
3 => {
|
| 168 |
+
ctx.spawn_offset_x = param_3;
|
| 169 |
+
ctx.spawn_offset_y = param_4;
|
| 170 |
+
}
|
| 171 |
+
4 => {
|
| 172 |
+
if (fire_subtype_34 as u32).wrapping_sub(1) < 0x18 {
|
| 173 |
+
ctx.spawn_offset_x = param_3;
|
| 174 |
+
ctx.spawn_offset_y = param_4;
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
_ => {}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// ββ 4. Delay ββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
+
if w.difficulty_level == 0 {
|
| 182 |
+
ctx.delay = 0x1E;
|
| 183 |
+
} else if w.difficulty_level == 1 {
|
| 184 |
+
ctx.delay = 0x3C;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// ββ 5. Network timing βββββββββββββββββββββββββββββββββββ
|
| 188 |
+
let ddgame_ptr = w.ddgame();
|
| 189 |
+
let ddgame_raw = ddgame_ptr as *const u8;
|
| 190 |
+
let ptr_at_24 = *(ddgame_raw.add(0x24) as *const *const u8);
|
| 191 |
+
let is_network = *ptr_at_24.add(0xD9D0);
|
| 192 |
+
let fe_version = *ptr_at_24.add(0xD9B1);
|
| 193 |
+
|
| 194 |
+
let mut adjust = 0i32;
|
| 195 |
+
let max_clients = if is_network == 0 {
|
| 196 |
+
5
|
| 197 |
+
} else if fe_version < 0x1B {
|
| 198 |
+
10
|
| 199 |
+
} else {
|
| 200 |
+
adjust = -1;
|
| 201 |
+
10
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
let client_idx = w.network_client_index;
|
| 205 |
+
if ((client_idx - adjust) as u32) < ((max_clients - adjust) as u32) {
|
| 206 |
+
ctx.network_delay = (client_idx + 1) * 1000;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// ββ 6. Weapon 0x22/0x24 special βββββββββββββββββββββββββ
|
| 210 |
+
let weapon_id = w.selected_weapon;
|
| 211 |
+
if (weapon_id == 0x22 || weapon_id == 0x24) && w.weapon_param_3 == 0 {
|
| 212 |
+
(*worm).shot_data_1 = w.shot_data_2;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// ββ 7. SharedData HandleMessage (msg 0x49) ββββββββββββββ
|
| 216 |
+
let mut msg_buf = [0u8; 0x408];
|
| 217 |
+
write_u32(&mut msg_buf, 0x00, w.team_index);
|
| 218 |
+
write_u32(&mut msg_buf, 0x04, w.worm_index);
|
| 219 |
+
write_u32(&mut msg_buf, 0x08, w.shot_data_1);
|
| 220 |
+
write_u32(&mut msg_buf, 0x0C, w.shot_data_2);
|
| 221 |
+
write_u32(&mut msg_buf, 0x10, w.fire_sync_frame_1 as u32);
|
| 222 |
+
write_u32(&mut msg_buf, 0x14, w.fire_sync_frame_2 as u32);
|
| 223 |
+
write_u32(
|
| 224 |
+
&mut msg_buf,
|
| 225 |
+
0x18,
|
| 226 |
+
if w._unknown_2cc != 0 { 1 } else { 0 },
|
| 227 |
+
);
|
| 228 |
+
write_u32(&mut msg_buf, 0x1C, weapon_id);
|
| 229 |
+
|
| 230 |
+
let table = SharedDataTable::from_task((*worm).as_task_ptr());
|
| 231 |
+
let team_entity = table.lookup(0, 0x14);
|
| 232 |
+
if !team_entity.is_null() {
|
| 233 |
+
let vtable = *(team_entity as *const *const usize);
|
| 234 |
+
let handle_msg: unsafe extern "thiscall" fn(
|
| 235 |
+
*mut u8,
|
| 236 |
+
*mut u8,
|
| 237 |
+
u32,
|
| 238 |
+
u32,
|
| 239 |
+
*const u8,
|
| 240 |
+
) = core::mem::transmute(*vtable.add(2));
|
| 241 |
+
handle_msg(
|
| 242 |
+
team_entity as *mut u8,
|
| 243 |
+
(*worm).as_task_ptr_mut() as *mut u8,
|
| 244 |
+
0x49,
|
| 245 |
+
0x408,
|
| 246 |
+
msg_buf.as_ptr(),
|
| 247 |
+
);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// ββ 8. Weapon stat counters βββββββββββββββββββββββββββββ
|
| 251 |
+
let ddgame = (*worm).ddgame() as *mut u8;
|
| 252 |
+
let team_id = (*worm).team_index;
|
| 253 |
+
let worm_id = (*worm).worm_index;
|
| 254 |
+
let current_weapon_byte = *ddgame.add(0x7E3F);
|
| 255 |
+
|
| 256 |
+
if is_super_weapon(weapon_id, current_weapon_byte) {
|
| 257 |
+
let counter = ddgame
|
| 258 |
+
.add(0x40D8)
|
| 259 |
+
.add((team_id * 0x51C) as usize)
|
| 260 |
+
.add((worm_id * 0x9C) as usize) as *mut i32;
|
| 261 |
+
*counter += 1;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Range 0x3E..=0x46
|
| 265 |
+
if weapon_id.wrapping_sub(0x3E) <= 8 {
|
| 266 |
+
let counter = ddgame
|
| 267 |
+
.add(0x40D4)
|
| 268 |
+
.add((team_id * 0x51C) as usize)
|
| 269 |
+
.add((worm_id * 0x9C) as usize) as *mut i32;
|
| 270 |
+
*counter += 1;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
if is_weapon_category_a(weapon_id) {
|
| 274 |
+
let counter = ddgame
|
| 275 |
+
.add(0x40D0)
|
| 276 |
+
.add((team_id * 0x51C) as usize)
|
| 277 |
+
.add((worm_id * 0x9C) as usize) as *mut i32;
|
| 278 |
+
*counter += 1;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
if is_weapon_category_b(weapon_id) {
|
| 282 |
+
let counter = ddgame
|
| 283 |
+
.add(0x40CC)
|
| 284 |
+
.add((team_id * 0x51C) as usize)
|
| 285 |
+
.add((worm_id * 0x9C) as usize) as *mut i32;
|
| 286 |
+
*counter += 1;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
// ββ 9. Sound dispatch + 10. Visual effect βββββββββββββββ
|
| 290 |
+
let task = worm as *mut CGameTask;
|
| 291 |
+
let mut do_effect = false;
|
| 292 |
+
let mut effect_state: u32 = 0x73;
|
| 293 |
+
|
| 294 |
+
let w = &*worm; // re-borrow after mutation above
|
| 295 |
+
let entry = w.active_weapon_entry;
|
| 296 |
+
let fire_type = (*entry).fire_type;
|
| 297 |
+
|
| 298 |
+
let play_worm_sound_addr = rb(va::PLAY_WORM_SOUND);
|
| 299 |
+
let stop_worm_sound_addr = rb(va::STOP_WORM_SOUND);
|
| 300 |
+
|
| 301 |
+
match fire_type {
|
| 302 |
+
1 => {
|
| 303 |
+
match (*entry).fire_subtype_34 {
|
| 304 |
+
1 => {
|
| 305 |
+
if w.sound_handle == 0 {
|
| 306 |
+
call_play_worm_sound(worm, 0x1004E, 0x10000, play_worm_sound_addr);
|
| 307 |
+
}
|
| 308 |
+
do_effect = true;
|
| 309 |
+
effect_state = 0x73;
|
| 310 |
+
}
|
| 311 |
+
2 => {
|
| 312 |
+
sound::play_sound_local(task, 0x49, 3, 0x10000, 0x10000);
|
| 313 |
+
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 314 |
+
}
|
| 315 |
+
3 | 7 | 0xB | 0xC => {
|
| 316 |
+
sound::play_sound_local(task, 0x4B, 3, 0x10000, 0x10000);
|
| 317 |
+
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 318 |
+
}
|
| 319 |
+
4 => {
|
| 320 |
+
if w.sound_handle == 0 {
|
| 321 |
+
call_play_worm_sound(worm, 0x1004F, 0x10000, play_worm_sound_addr);
|
| 322 |
+
}
|
| 323 |
+
do_effect = true;
|
| 324 |
+
effect_state = 0x73;
|
| 325 |
+
}
|
| 326 |
+
5 => {
|
| 327 |
+
sound::play_sound_local(task, 0x50, 3, 0x10000, 0x10000);
|
| 328 |
+
do_effect = true;
|
| 329 |
+
effect_state = 0x75;
|
| 330 |
+
}
|
| 331 |
+
6 => {
|
| 332 |
+
sound::play_sound_local(task, 0x52, 3, 0x10000, 0x10000);
|
| 333 |
+
do_effect = true;
|
| 334 |
+
effect_state = 0x73;
|
| 335 |
+
}
|
| 336 |
+
10 => {
|
| 337 |
+
sound::play_sound_local(task, 0x1D, 3, 0x10000, 0x10000);
|
| 338 |
+
call_stop_worm_sound(worm, stop_worm_sound_addr);
|
| 339 |
+
}
|
| 340 |
+
_ => {}
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
2 => {
|
| 344 |
+
if w._unknown_2cc == 0 || w._unknown_2c8 == 1 {
|
| 345 |
+
// Team sound from DDGame+0x7768 + team_id * 0xF0
|
| 346 |
+
let ddgame = w.ddgame() as *const u8;
|
| 347 |
+
let team_sound_offset = (team_id as usize) * 0xF0 + 0x7768;
|
| 348 |
+
let team_sound = *(ddgame.add(team_sound_offset) as *const u32);
|
| 349 |
+
sound::play_sound_local(task, team_sound, 3, 0x10000, 0x10000);
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
// Type 3: no sound
|
| 353 |
+
4 => {
|
| 354 |
+
match (*entry).fire_subtype_34 {
|
| 355 |
+
2 => {
|
| 356 |
+
sound::play_sound_local(task, 0x60, 3, 0x10000, 0x10000);
|
| 357 |
+
}
|
| 358 |
+
3 => {
|
| 359 |
+
let sound_id = (*entry).fire_subtype_38 as u32;
|
| 360 |
+
sound::play_sound_local(task, sound_id, 3, 0x10000, 0x10000);
|
| 361 |
+
}
|
| 362 |
+
4 => {
|
| 363 |
+
let sound_id = *((&raw const (*entry).fire_params as *const u8).add(4)
|
| 364 |
+
as *const u32);
|
| 365 |
+
sound::play_sound_local(task, sound_id, 3, 0x10000, 0x10000);
|
| 366 |
+
}
|
| 367 |
+
10 => {
|
| 368 |
+
if w._unknown_208 == 0 {
|
| 369 |
+
sound::play_sound_local(task, 0x43, 3, 0x10000, 0x10000);
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
0xB => {
|
| 373 |
+
if w.sound_handle == 0 {
|
| 374 |
+
call_play_worm_sound(worm, 0x10035, 0x10000, play_worm_sound_addr);
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
_ => {}
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
_ => {}
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// ββ 10. Visual effect (if triggered by sound dispatch) ββ
|
| 384 |
+
if do_effect {
|
| 385 |
+
let ddgame = &mut *(*worm).ddgame();
|
| 386 |
+
let gfx_handler = *((&raw const *ddgame as *const u8).add(0x528) as *const *const u8);
|
| 387 |
+
let palette = *(gfx_handler.add(0x22C) as *const u32);
|
| 388 |
+
|
| 389 |
+
let rng1 = ddgame.advance_effect_rng();
|
| 390 |
+
let rng2 = ddgame.advance_effect_rng();
|
| 391 |
+
|
| 392 |
+
let rng2_offset = (rng2 & 0xFFFF) as i32 - 0x18000;
|
| 393 |
+
let facing = (*worm).facing_direction_2;
|
| 394 |
+
let rng_scaled = rng2_offset * facing;
|
| 395 |
+
|
| 396 |
+
let rng1_offset = (rng1 & 0xFFFF) as i32 - 0x18000;
|
| 397 |
+
|
| 398 |
+
let facing_flag: u32 = if facing < 1 { 0x40000 } else { 0 };
|
| 399 |
+
let state_flag = facing_flag + effect_state;
|
| 400 |
+
|
| 401 |
+
call_spawn_effect_full(
|
| 402 |
+
worm, speed_x, speed_y, rng_scaled, rng1_offset, palette, state_flag, 0xA0000,
|
| 403 |
+
0x1999, rb(va::SPAWN_EFFECT),
|
| 404 |
+
);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
// ββ 11. Call FireWeapon ββββββββββββββββββββββββββββββββββ
|
| 408 |
+
let entry = (*worm).active_weapon_entry;
|
| 409 |
+
let ctx_ptr = &ctx as *const WeaponReleaseContext as *const u8;
|
| 410 |
+
weapon::fire_weapon_impl(entry, ctx_ptr, worm);
|
| 411 |
+
|
| 412 |
+
let _ = log_line(&format!(
|
| 413 |
+
"[WeaponRelease] worm=0x{:08X} weapon={} type={} sub34={} sub38={}",
|
| 414 |
+
worm as u32, weapon_id, fire_type, fire_subtype_34, fire_subtype_38,
|
| 415 |
+
));
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// ββ Helper ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 419 |
+
|
| 420 |
+
#[inline(always)]
|
| 421 |
+
fn write_u32(buf: &mut [u8], offset: usize, value: u32) {
|
| 422 |
+
buf[offset..offset + 4].copy_from_slice(&value.to_ne_bytes());
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// ββ Bridge functions ββββββββββββββββββββββββββββββββββββββββ
|
| 426 |
+
// All bridges pass the runtime target address as the last cdecl parameter,
|
| 427 |
+
// matching the pattern used in weapon.rs (avoids sym + jmp indirection issues).
|
| 428 |
+
|
| 429 |
+
/// PlayWormSound (0x5150D0): usercall(EDI=worm) + stack(sound_handle_id, volume), RET 0x8.
|
| 430 |
+
#[unsafe(naked)]
|
| 431 |
+
unsafe extern "C" fn call_play_worm_sound(
|
| 432 |
+
_worm: *mut CTaskWorm,
|
| 433 |
+
_sound_id: u32,
|
| 434 |
+
_volume: u32,
|
| 435 |
+
_addr: u32,
|
| 436 |
+
) {
|
| 437 |
+
core::arch::naked_asm!(
|
| 438 |
+
"push edi",
|
| 439 |
+
// Stack: 1 save(4) + ret(4) = 8 to first arg
|
| 440 |
+
"mov edi, [esp+8]", // worm
|
| 441 |
+
"mov eax, [esp+20]", // addr
|
| 442 |
+
"push [esp+16]", // volume
|
| 443 |
+
"push [esp+16]", // sound_id (shifted +4)
|
| 444 |
+
"call eax",
|
| 445 |
+
"pop edi",
|
| 446 |
+
"ret",
|
| 447 |
+
);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
/// StopWormSound (0x515180): usercall(ESI=worm), plain RET.
|
| 451 |
+
#[unsafe(naked)]
|
| 452 |
+
unsafe extern "C" fn call_stop_worm_sound(_worm: *mut CTaskWorm, _addr: u32) {
|
| 453 |
+
core::arch::naked_asm!(
|
| 454 |
+
"push esi",
|
| 455 |
+
// Stack: 1 save(4) + ret(4) = 8 to first arg
|
| 456 |
+
"mov esi, [esp+8]", // worm
|
| 457 |
+
"mov eax, [esp+12]", // addr
|
| 458 |
+
"call eax",
|
| 459 |
+
"pop esi",
|
| 460 |
+
"ret",
|
| 461 |
+
);
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
/// SpawnEffect (0x547C30): usercall(EAX=0x80000, ECX=speed_x, ESI=worm) + 7 stack, RET 0x1C.
|
| 465 |
+
#[unsafe(naked)]
|
| 466 |
+
unsafe extern "C" fn call_spawn_effect_full(
|
| 467 |
+
_worm: *mut CTaskWorm,
|
| 468 |
+
_speed_x: i32,
|
| 469 |
+
_speed_y: i32,
|
| 470 |
+
_rng_scaled: i32,
|
| 471 |
+
_rng_offset: i32,
|
| 472 |
+
_palette: u32,
|
| 473 |
+
_state_flag: u32,
|
| 474 |
+
_size: u32,
|
| 475 |
+
_scale: u32,
|
| 476 |
+
_addr: u32,
|
| 477 |
+
) {
|
| 478 |
+
core::arch::naked_asm!(
|
| 479 |
+
"push esi",
|
| 480 |
+
"push ebx",
|
| 481 |
+
// Stack: 2 saves(8) + ret(4) = 12 to first arg
|
| 482 |
+
// Args: +12=worm, +16=speed_x, +20=speed_y, +24=rng_scaled, +28=rng_offset,
|
| 483 |
+
// +32=palette, +36=state_flag, +40=size, +44=scale, +48=addr
|
| 484 |
+
"mov esi, [esp+12]", // worm β ESI
|
| 485 |
+
"mov ecx, [esp+16]", // speed_x β ECX
|
| 486 |
+
"mov ebx, [esp+48]", // addr
|
| 487 |
+
"push [esp+44]", // scale
|
| 488 |
+
"push [esp+44]", // size
|
| 489 |
+
"push [esp+44]", // state_flag
|
| 490 |
+
"push [esp+44]", // palette
|
| 491 |
+
"push [esp+44]", // rng_offset
|
| 492 |
+
"push [esp+44]", // rng_scaled
|
| 493 |
+
"push [esp+44]", // speed_y (first stack param)
|
| 494 |
+
"mov eax, 0x80000",
|
| 495 |
+
"call ebx",
|
| 496 |
+
"pop ebx",
|
| 497 |
+
"pop esi",
|
| 498 |
+
"ret",
|
| 499 |
+
);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
// ββ Hook installation βββββββββββββββββββββββββββββββββββββββ
|
| 503 |
+
|
| 504 |
+
pub fn install() -> Result<(), String> {
|
| 505 |
+
unsafe {
|
| 506 |
+
hook::install(
|
| 507 |
+
"WeaponRelease",
|
| 508 |
+
va::WEAPON_RELEASE,
|
| 509 |
+
trampoline_weapon_release as *const (),
|
| 510 |
+
)?;
|
| 511 |
+
}
|
| 512 |
+
Ok(())
|
| 513 |
+
}
|