paavohtl Claude Opus 4.6 (1M context) commited on
Commit
f5fbbeb
Β·
1 Parent(s): 254b6b5

feat: port WeaponRelease (0x51C3D0) to Rust

Browse files

Full 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 CHANGED
@@ -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;
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -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
crates/openwa-core/src/task/worm.rs CHANGED
@@ -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
- // 0x69–0x6B: unknown (within weapon/action range)
 
 
 
 
 
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
- // 0x6F: unknown
 
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
- // 0x76: unknown
 
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
- // 0x79–0x7A: unknown
 
 
 
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
- // 0x84–0x85: unknown
 
 
 
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
- // 0x8A: end of weapon/action range
 
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–0x1A7: Unknown
281
- pub _unknown_180: [u8; 0x1A8 - 0x180],
 
 
 
 
 
 
 
 
 
 
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–0x247: Unknown
293
- pub _unknown_1f0: [u8; 0x248 - 0x1F0],
294
- /// 0x248: Unknown
295
- pub _unknown_248: u32,
 
 
 
 
 
 
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–0x28B: Aiming data (slots 5–6 in GetEntityData query 0x7D4)
305
- pub _unknown_284: [u32; 2],
306
- /// 0x28C–0x2EF: Unknown
307
- pub _unknown_28c: [u8; 0x64],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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–0x3DB: Unknown (rope anchor, weapon-specific data, etc.)
335
- pub _unknown_370: [u8; 0x3DC - 0x370],
 
 
 
 
 
 
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
crates/openwa-wormkit/src/hook.rs CHANGED
@@ -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) => {
crates/openwa-wormkit/src/replacements/mod.rs CHANGED
@@ -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
  }
crates/openwa-wormkit/src/replacements/sound.rs CHANGED
@@ -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 = &gt.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
  // ============================================================
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -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
- let worm_raw = worm as *mut u8;
568
- let field_208 = *(worm_raw.add(0x208) as *const i32);
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 = *(worm_raw.add(0x2E0) as *const i32);
580
- let fire_y = *(worm_raw.add(0x2E4) as *const i32);
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
- let field_190 = *(worm_raw.add(0x190) as *const i32);
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
- *(worm_raw.add(0x208) as *mut i32) = 0;
603
- *(worm_raw.add(0x198) as *mut i32) = 0;
604
- *(worm_raw.add(0x19C) as *mut i32) = 0;
605
- *(worm_raw.add(0x1AC) as *mut i32) = 0;
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 worm_raw = worm as *const u8;
967
- let freeze_x = *(worm_raw.add(0x2E0) as *const i32);
968
- let freeze_y = *(worm_raw.add(0x2E4) as *const i32);
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 {
crates/openwa-wormkit/src/replacements/weapon_release.rs ADDED
@@ -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
+ }