paavohtl Claude Opus 4.6 (1M context) commited on
Commit
3b55aa0
·
1 Parent(s): 9686af7

fix: correct SharedData entity type and rename all SpecialFireSubtype/WormState

Browse files

SharedData key (0, 0x14) returns CTaskTurnGame, not CTaskTeam.
CTaskTurnGame overrides HandleMessage (0x55DC00) with turn-flow logic
that CTaskTeam::HandleMessage (0x557310) lacks — end-turn handler,
surrender sound. Calling CTaskTeam directly caused 1-frame retreat
time differences and broken round endings in 6 replays.

- lookup_team_task -> lookup_turn_game, returns *mut CTaskTurnGame
- All callers use CTaskTurnGame::handle_message_raw (vtable dispatch)
- Renamed all SpecialFireSubtype variants from wrong old mapping to
confirmed weapon->sub34 values from replay test logs
- Renamed 7 WormState variants to match actual weapons (FirePunch=0x6C,
Kamikaze=0x6D, PneumaticDrill=0x6E, Blowtorch=0x71, SuicideBomber=0x75)
- Renamed handler functions (fire_freeze->fire_girder, fire_air_strike->
fire_teleport, fire_jet_pack->fire_scales_of_justice, etc.)
- Added Parachute=12 to SpecialFireSubtype
- TeamMessage::StrikeFire -> Surrender
- Added napalm and bomber_parachute replay tests

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

crates/openwa-core/src/game/weapon.rs CHANGED
@@ -205,63 +205,83 @@ impl TryFrom<i32> for FireMethod {
205
  }
206
  }
207
 
208
- /// Special weapon subtype (WeaponEntry+0x34, when fire_type == Special).
209
- ///
210
- /// Each value corresponds to a specific weapon's fire handler.
211
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
212
  #[repr(i32)]
 
213
  pub enum SpecialFireSubtype {
214
- Blowtorch = 1,
215
- PneumaticDrill = 2,
216
- Girder = 3,
217
- BaseballBat = 4,
218
- FirePunch = 5,
219
- DragonBall = 6,
 
 
 
 
 
 
220
  // 7: unknown
221
- Kamikaze = 8,
 
222
  Prod = 9,
223
- AirStrike = 10,
224
- ScalesOfJustice = 11,
225
- // 12: unknown
226
- StrikeFire = 13,
 
 
 
 
227
  MailMineMole = 14,
228
  // 15: unknown
229
- IndianNuclearTest = 16,
230
- Freeze = 17,
231
- SuicideBomber = 18,
 
 
 
232
  SkipGo = 19,
233
- Surrender = 20,
 
234
  SelectWorm = 21,
235
- JetPack = 22,
236
- MagicBullet = 23,
237
- LowGravity = 24,
 
 
 
238
  }
239
 
240
  impl TryFrom<i32> for SpecialFireSubtype {
241
  type Error = i32;
242
  fn try_from(v: i32) -> Result<Self, i32> {
243
  match v {
244
- 1 => Ok(Self::Blowtorch),
245
- 2 => Ok(Self::PneumaticDrill),
246
- 3 => Ok(Self::Girder),
247
- 4 => Ok(Self::BaseballBat),
248
- 5 => Ok(Self::FirePunch),
249
- 6 => Ok(Self::DragonBall),
250
- 8 => Ok(Self::Kamikaze),
251
  9 => Ok(Self::Prod),
252
- 10 => Ok(Self::AirStrike),
253
- 11 => Ok(Self::ScalesOfJustice),
254
- 13 => Ok(Self::StrikeFire),
 
255
  14 => Ok(Self::MailMineMole),
256
- 16 => Ok(Self::IndianNuclearTest),
257
- 17 => Ok(Self::Freeze),
258
- 18 => Ok(Self::SuicideBomber),
259
  19 => Ok(Self::SkipGo),
260
- 20 => Ok(Self::Surrender),
261
  21 => Ok(Self::SelectWorm),
262
- 22 => Ok(Self::JetPack),
263
- 23 => Ok(Self::MagicBullet),
264
- 24 => Ok(Self::LowGravity),
265
  _ => Err(v),
266
  }
267
  }
 
205
  }
206
  }
207
 
208
+ /// Special weapon subtype — the raw `sub34` value from the weapon table,
209
+ /// used directly as the switch discriminant in FireWeapon (0x51EE60) case 4.
210
+ /// Names are based on confirmed weapon->sub34 mappings from replay test logs.
211
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
212
  #[repr(i32)]
213
+ #[allow(non_camel_case_types)]
214
  pub enum SpecialFireSubtype {
215
+ /// FirePunch weapon (id=16, sub34=1).
216
+ FirePunch = 1,
217
+ /// BaseballBat weapon (id=35, sub34=2). Handler calls PneumaticDrill/SpecialImpact logic.
218
+ BaseballBat = 2,
219
+ /// DragonBall weapon (id=17, sub34=3). Handler allocates CTaskGirder.
220
+ DragonBall = 3,
221
+ /// Kamikaze weapon (id=18, sub34=4).
222
+ Kamikaze = 4,
223
+ /// SuicideBomber weapon (id=19, sub34=5).
224
+ SuicideBomber = 5,
225
+ /// Unknown — no weapon observed using sub34=6 in replay tests.
226
+ Unknown6 = 6,
227
  // 7: unknown
228
+ /// PneumaticDrill weapon (id=33, sub34=8).
229
+ PneumaticDrill = 8,
230
  Prod = 9,
231
+ /// Teleport weapon (id=40, sub34=10).
232
+ Teleport = 10,
233
+ /// Blowtorch weapon (id=32, sub34=11).
234
+ Blowtorch = 11,
235
+ /// Parachute weapon (id=39, sub34=12).
236
+ Parachute = 12,
237
+ /// Surrender weapon (id=58, sub34=13). Sends message 0x2B (TaskMessage::Surrender).
238
+ Surrender = 13,
239
  MailMineMole = 14,
240
  // 15: unknown
241
+ /// NuclearTest weapon (id=55, sub34=16).
242
+ NuclearTest = 16,
243
+ /// Girder/GirderPack weapons (id=34/36, sub34=17).
244
+ Girder = 17,
245
+ /// Unknown — no weapon observed using sub34=18 in replay tests.
246
+ Unknown18 = 18,
247
  SkipGo = 19,
248
+ /// Freeze weapon (id=60, sub34=20). Sends message 0x29 (TaskMessage::Freeze).
249
+ Freeze = 20,
250
  SelectWorm = 21,
251
+ /// ScalesOfJustice weapon (id=41, sub34=22).
252
+ ScalesOfJustice = 22,
253
+ /// JetPack weapon (id=62, sub34=23).
254
+ JetPack = 23,
255
+ /// Armageddon weapon (id=56, sub34=24).
256
+ Armageddon = 24,
257
  }
258
 
259
  impl TryFrom<i32> for SpecialFireSubtype {
260
  type Error = i32;
261
  fn try_from(v: i32) -> Result<Self, i32> {
262
  match v {
263
+ 1 => Ok(Self::FirePunch),
264
+ 2 => Ok(Self::BaseballBat),
265
+ 3 => Ok(Self::DragonBall),
266
+ 4 => Ok(Self::Kamikaze),
267
+ 5 => Ok(Self::SuicideBomber),
268
+ 6 => Ok(Self::Unknown6),
269
+ 8 => Ok(Self::PneumaticDrill),
270
  9 => Ok(Self::Prod),
271
+ 10 => Ok(Self::Teleport),
272
+ 11 => Ok(Self::Blowtorch),
273
+ 12 => Ok(Self::Parachute),
274
+ 13 => Ok(Self::Surrender),
275
  14 => Ok(Self::MailMineMole),
276
+ 16 => Ok(Self::NuclearTest),
277
+ 17 => Ok(Self::Girder),
278
+ 18 => Ok(Self::Unknown18),
279
  19 => Ok(Self::SkipGo),
280
+ 20 => Ok(Self::Freeze),
281
  21 => Ok(Self::SelectWorm),
282
+ 22 => Ok(Self::ScalesOfJustice),
283
+ 23 => Ok(Self::JetPack),
284
+ 24 => Ok(Self::Armageddon),
285
  _ => Err(v),
286
  }
287
  }
crates/openwa-core/src/task/team.rs CHANGED
@@ -118,20 +118,22 @@ bind_CTaskTeamVTable!(CTaskTeam, base.vtable);
118
  // ── Typed message handlers ──────────────────────────────────
119
 
120
  impl CTaskTeam {
121
- /// Handle StrikeFire / Surrender message (0x2B) — ported from
122
- /// CTaskTeam::HandleMessage case 0x2B.
123
  ///
124
- /// Used by SpecialFireSubtype::StrikeFire (subtype 13), shared by
125
- /// Napalm Strike, Surrender, and other weapons.
 
 
 
126
  ///
127
- /// Uses `CTask::broadcast_message_raw` for child dispatch (no `&mut self`,
128
- /// no noalias UB). This is a pure Rust handler — no bridge to WA code.
129
- ///
130
- /// **Status:** Has a behavioral difference vs the WA handler — under investigation.
131
  ///
132
  /// # Safety
133
  /// `this` must be a valid CTaskTeam pointer with valid ddgame.
134
- pub unsafe fn on_strike_fire(
135
  this: *mut Self,
136
  sender: *mut CTask,
137
  msg_team_index: u32,
 
118
  // ── Typed message handlers ──────────────────────────────────
119
 
120
  impl CTaskTeam {
121
+ /// Handle message 0x2B (Surrender) at the CTaskTeam level — ported from
122
+ /// CTaskTeam::HandleMessage (0x557310) case 0x2B.
123
  ///
124
+ /// **Important:** This is only the CTaskTeam layer. The entity at SharedData
125
+ /// key (0, 0x14) is actually a CTaskTurnGame (inherits CTaskTeam).
126
+ /// CTaskTurnGame::HandleMessage (0x55DC00) wraps this with:
127
+ /// - End-turn logic (FUN_0055C300) if the active team surrenders
128
+ /// - Surrender sound playback
129
  ///
130
+ /// To port message 0x2B fully, CTaskTurnGame::HandleMessage case 0x2B must
131
+ /// also be ported. Until then, use vtable dispatch (handle_message_raw) on the
132
+ /// CTaskTurnGame to hit the TurnGame override.
 
133
  ///
134
  /// # Safety
135
  /// `this` must be a valid CTaskTeam pointer with valid ddgame.
136
+ pub unsafe fn on_surrender_fire(
137
  this: *mut Self,
138
  sender: *mut CTask,
139
  msg_team_index: u32,
crates/openwa-core/src/task/team_message.rs CHANGED
@@ -12,11 +12,10 @@ use crate::game::TaskMessage;
12
  /// its payload as typed fields instead of a raw byte buffer.
13
  #[derive(Debug, Clone, Copy)]
14
  pub enum TeamMessage {
15
- /// 0x2B (Surrender): Strike fire / team-targeted action.
16
- /// Used by SpecialFireSubtype::StrikeFire (subtype 13) — shared by
17
- /// Napalm Strike, Surrender, and other weapons that send a team message.
18
  /// Sets a per-team flag and optionally broadcasts DetonateWeapon.
19
- StrikeFire {
 
20
  /// Team index (1-based) identifying which team fired.
21
  team_index: u32,
22
  },
@@ -38,7 +37,7 @@ impl TeamMessage {
38
  return None;
39
  }
40
  let team_index = *(data as *const u32);
41
- Some(TeamMessage::StrikeFire { team_index })
42
  }
43
  _ => None,
44
  }
@@ -49,7 +48,7 @@ impl TeamMessage {
49
  /// Writes the payload into `buf` and returns `(msg_type, size)`.
50
  pub fn to_raw(&self, buf: &mut [u8]) -> (u32, u32) {
51
  match self {
52
- TeamMessage::StrikeFire { team_index } => {
53
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
54
  (TaskMessage::Surrender as u32, 4)
55
  }
 
12
  /// its payload as typed fields instead of a raw byte buffer.
13
  #[derive(Debug, Clone, Copy)]
14
  pub enum TeamMessage {
15
+ /// 0x2B (TaskMessage::Surrender): Sent by the Surrender weapon (subtype 13).
 
 
16
  /// Sets a per-team flag and optionally broadcasts DetonateWeapon.
17
+ /// Handled by CTaskTurnGame::HandleMessage which also triggers end-turn logic.
18
+ Surrender {
19
  /// Team index (1-based) identifying which team fired.
20
  team_index: u32,
21
  },
 
37
  return None;
38
  }
39
  let team_index = *(data as *const u32);
40
+ Some(TeamMessage::Surrender { team_index })
41
  }
42
  _ => None,
43
  }
 
48
  /// Writes the payload into `buf` and returns `(msg_type, size)`.
49
  pub fn to_raw(&self, buf: &mut [u8]) -> (u32, u32) {
50
  match self {
51
+ TeamMessage::Surrender { team_index } => {
52
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
53
  (TaskMessage::Surrender as u32, 4)
54
  }
crates/openwa-core/src/task/worm.rs CHANGED
@@ -37,27 +37,27 @@ pub enum WormState {
37
  Unknown_0x6A = 0x6A,
38
  /// Unknown (within weapon/action range).
39
  Unknown_0x6B = 0x6B,
40
- /// Blowtorch — using the blowtorch weapon.
41
- Blowtorch = 0x6C,
42
- /// Baseball Bat — swinging the baseball bat. Frequently checked in HandleMessage.
43
- BaseballBat = 0x6D,
44
- /// Kamikaze — performing kamikaze attack.
45
- Kamikaze = 0x6E,
46
  /// Air Strike pending — set when air strike fires with `_unknown_208 == 0`.
47
  AirStrikePending_Maybe = 0x6F,
48
- /// Dragon Ball — performing dragon ball attack.
49
- DragonBall = 0x70,
50
- /// Scales of Justice — using scales of justice.
51
- ScalesOfJustice = 0x71,
52
- /// Suicide Bomber — performing suicide bomber attack.
53
- SuicideBomber = 0x72,
54
  /// Weapon charging — entered from aiming states (0x7B, 0x7C) before release.
55
  /// Also set by CheckPendingAction when field +0xBC is nonzero.
56
  WeaponCharging_Maybe = 0x73,
57
  /// Teleport cancelled — teleport failed or was denied.
58
  TeleportCancelled_Maybe = 0x74,
59
- /// Fire Punch — performing fire punch attack.
60
- FirePunch = 0x75,
61
  /// Unknown (within weapon/action range).
62
  Unknown_0x76 = 0x76,
63
  /// Weapon selected — entered via SelectCursor (msg 0x24) from idle/active states.
 
37
  Unknown_0x6A = 0x6A,
38
  /// Unknown (within weapon/action range).
39
  Unknown_0x6B = 0x6B,
40
+ /// Fire Punch — performing fire punch attack (sub34=1).
41
+ FirePunch = 0x6C,
42
+ /// Kamikaze — performing kamikaze attack (sub34=4). Frequently checked in HandleMessage.
43
+ Kamikaze = 0x6D,
44
+ /// Pneumatic Drill — using the pneumatic drill (sub34=8).
45
+ PneumaticDrill = 0x6E,
46
  /// Air Strike pending — set when air strike fires with `_unknown_208 == 0`.
47
  AirStrikePending_Maybe = 0x6F,
48
+ /// Unknown — sub34=6 has no replay test log data.
49
+ Unknown_0x70 = 0x70,
50
+ /// Blowtorch — using the blowtorch weapon (sub34=11).
51
+ Blowtorch = 0x71,
52
+ /// Unknown — sub34=18 has no replay test log data.
53
+ Unknown_0x72 = 0x72,
54
  /// Weapon charging — entered from aiming states (0x7B, 0x7C) before release.
55
  /// Also set by CheckPendingAction when field +0xBC is nonzero.
56
  WeaponCharging_Maybe = 0x73,
57
  /// Teleport cancelled — teleport failed or was denied.
58
  TeleportCancelled_Maybe = 0x74,
59
+ /// Suicide Bomber — performing suicide bomber attack (sub34=5).
60
+ SuicideBomber = 0x75,
61
  /// Unknown (within weapon/action range).
62
  Unknown_0x76 = 0x76,
63
  /// Weapon selected — entered via SelectCursor (msg 0x24) from idle/active states.
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -22,7 +22,7 @@ use openwa_core::fixed::Fixed;
22
  use openwa_core::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
23
  use openwa_core::game::Weapon;
24
  use openwa_core::log::log_line;
25
- use openwa_core::task::team::CTaskTeam;
26
  use openwa_core::task::CTask;
27
  use openwa_core::task::worm::{CTaskWorm, WormState};
28
 
@@ -313,39 +313,40 @@ unsafe fn fire_weapon_special(
313
  let params_38_ptr = &raw const (*entry).fire_method as *const WeaponFireParams;
314
 
315
  match S::try_from(subtype) {
316
- Ok(S::Blowtorch) => CTaskWorm::set_state_raw(worm, WormState::Blowtorch),
317
- Ok(S::PneumaticDrill) => fire_drill(worm, ctx as *const u8),
318
- Ok(S::Girder) => fire_girder(worm, params_38_ptr, ctx as *const u8),
319
- Ok(S::BaseballBat) => CTaskWorm::set_state_raw(worm, WormState::BaseballBat),
320
  Ok(S::FirePunch) => CTaskWorm::set_state_raw(worm, WormState::FirePunch),
321
- Ok(S::DragonBall) => CTaskWorm::set_state_raw(worm, WormState::DragonBall),
 
322
  Ok(S::Kamikaze) => CTaskWorm::set_state_raw(worm, WormState::Kamikaze),
 
 
 
323
  Ok(S::Prod) => fire_prod(worm, ctx as *const u8),
324
- Ok(S::AirStrike) => fire_air_strike(worm),
325
- Ok(S::ScalesOfJustice) => CTaskWorm::set_state_raw(worm, WormState::ScalesOfJustice),
326
- Ok(S::StrikeFire) => fire_strike_typed(worm),
 
327
  Ok(S::MailMineMole) => fire_mail_mine_mole(worm),
328
- Ok(S::IndianNuclearTest) => {
329
  if can_fire_subtype16((*worm).state()) {
330
- fire_indian_nuclear_test(worm);
331
  } else {
332
  CTaskWorm::set_state_raw(worm, WormState::TeleportCancelled_Maybe);
333
  }
334
  }
335
- Ok(S::Freeze) => fire_freeze(worm),
336
- Ok(S::SuicideBomber) => CTaskWorm::set_state_raw(worm, WormState::SuicideBomber),
337
  Ok(S::SkipGo) => fire_skip_go(worm, entry),
338
- Ok(S::Surrender) => fire_surrender(worm),
339
  Ok(S::SelectWorm) => fire_select_worm(worm),
340
- Ok(S::JetPack) => fire_jet_pack(worm),
341
- Ok(S::MagicBullet) => CTaskWorm::set_state_raw(worm, WormState::WeaponAimed_Maybe),
342
- Ok(S::LowGravity) => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)),
343
  _ => {}
344
  }
345
  }
346
 
347
  /// Worm state validity check for subtype 16 — pure Rust port of 0x516930.
348
- /// Used by Indian Nuclear Test (and possibly Teleport) to gate firing.
349
  fn can_fire_subtype16(state: u32) -> bool {
350
  state == WormState::WeaponAimed_Maybe as u32
351
  || (WormState::AimingAngle_Maybe as u32..=WormState::PreFire_Maybe as u32).contains(&state)
@@ -353,55 +354,52 @@ fn can_fire_subtype16(state: u32) -> bool {
353
 
354
  // ── Pure Rust fire handlers (no bridge needed) ──────────────
355
 
356
- /// Look up the CTaskTeam entity for a worm via SharedData.
357
  ///
 
358
  /// Returns null if not found.
359
- pub(crate) unsafe fn lookup_team_task(worm: *const CTaskWorm) -> *mut openwa_core::task::CTaskTeam {
360
  use openwa_core::task::SharedDataTable;
361
 
362
  let table = SharedDataTable::from_task(worm as *const CTask);
363
- // CTaskTeam is registered with key (0, 0x14) in SharedData
364
- table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTeam
365
  }
366
 
367
- /// StrikeFire (subtype 13) — typed Rust handler.
 
368
  ///
369
- /// Shared by Napalm Strike, Surrender, and other weapons that send
370
- /// message 0x2B to CTaskTeam. Constructs a typed TeamMessage::StrikeFire,
371
- /// serializes to raw bytes, and dispatches through CTaskTeam's WA vtable.
372
  #[inline(never)]
373
- unsafe fn fire_strike_typed(worm: *mut CTaskWorm) {
374
- use openwa_core::task::TeamMessage;
375
-
376
- let team = lookup_team_task(worm);
377
  if team.is_null() {
378
  return;
379
  }
380
 
381
- let msg = TeamMessage::StrikeFire {
382
- team_index: (*worm).team_index,
383
- };
384
  let mut buf = [0u8; 0x40C];
385
- let (msg_type, size) = msg.to_raw(&mut buf);
386
 
387
- CTaskTeam::handle_message_raw(
 
 
388
  team,
389
  worm as *mut openwa_core::task::CTask,
390
- msg_type,
391
- size,
392
  buf.as_ptr(),
393
  );
394
  }
395
 
396
- /// Send a message to the worm's CTaskTeam entity via SharedData lookup.
397
  ///
398
- /// Pattern shared by Napalm Strike (msg 0x2B), Surrender (msg 0x29), etc.
399
- /// Looks up CTaskTeam, then calls HandleMessage (vtable slot 2).
400
  ///
401
  /// The 0x40C-byte local buffer is passed as data pointer; team_index is written
402
  /// at buf+0x00 to identify which team fired.
403
  unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
404
- let team = lookup_team_task(worm);
405
  if team.is_null() {
406
  return;
407
  }
@@ -410,7 +408,7 @@ unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
410
  let team_index = (*worm).team_index;
411
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
412
 
413
- CTaskTeam::handle_message_raw(team,
414
  worm as *mut openwa_core::task::CTask,
415
  msg_type,
416
  4,
@@ -420,9 +418,9 @@ unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
420
 
421
  /// Select Worm (subtype 21) — pure Rust replacement for 0x51EBE0.
422
  ///
423
- /// Sends message 0x5D to CTaskTeam with buf = [8, team_index, ...].
424
  unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
425
- let team = lookup_team_task(worm);
426
  if team.is_null() {
427
  return;
428
  }
@@ -431,7 +429,7 @@ unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
431
  buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
432
  buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
433
 
434
- CTaskTeam::handle_message_raw(team,
435
  worm as *mut openwa_core::task::CTask,
436
  0x5D,
437
  0x408,
@@ -464,11 +462,11 @@ unsafe fn fire_skip_go(worm: *const CTaskWorm, entry: *const WeaponEntry) {
464
  }
465
  }
466
 
467
- /// Surrender (subtype 20) — pure Rust replacement for 0x51E600.
468
  ///
469
- /// Sends message 0x29 to CTaskTeam with buf = [team_index], then increments
470
  /// WormEntry.turn_action_counter_Maybe by 14 (0x0E).
471
- unsafe fn fire_surrender(worm: *mut CTaskWorm) {
472
  use openwa_core::engine::ddgame::TeamArenaRef;
473
 
474
  fire_send_team_message(worm, 0x29);
@@ -484,7 +482,7 @@ unsafe fn fire_surrender(worm: *mut CTaskWorm) {
484
  /// Mail/Mine/Mole (subtype 14) — pure Rust replacement for 0x51E670.
485
  ///
486
  /// Conditionally calls worm->vtable[0xE](0x65) based on game version and worm state,
487
- /// then sends message 0x28 to CTaskTeam, then increments
488
  /// WormEntry.turn_action_counter_Maybe by 7.
489
  ///
490
  /// Version check logic (from disassembly at 0x51E670):
@@ -517,14 +515,14 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
517
  entry.turn_action_counter_Maybe += 7;
518
  }
519
 
520
- /// Air Strike (subtype 10) — pure Rust port of 0x51E710.
521
  ///
522
- /// If CTaskWorm+0x208 == 0: set state to 0x6F (air strike pending).
523
  /// Otherwise: play sound, spawn visual effect, update position, compute
524
  /// new state based on game version, clear action fields.
525
  ///
526
  /// Convention: usercall(EAX=worm, ESI=worm, EDI=worm), plain RET.
527
- unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
528
  use openwa_core::rebase::rb;
529
 
530
  if (*worm)._unknown_208 == 0 {
@@ -570,46 +568,46 @@ unsafe fn fire_air_strike(worm: *mut CTaskWorm) {
570
  // Debug log block (ddgame+0x8144) omitted — only writes to debug file
571
  }
572
 
573
- /// Indian Nuclear Test (subtype 16) — pure Rust port of 0x51EB00.
574
  ///
575
- /// Sends three messages to CTaskTeam: RaiseWater (0x59), NukeBlast (0x5A),
576
  /// PoisonWorm (0x51), and plays two sounds. Gated by can_fire_subtype16.
577
  ///
578
  /// Convention: usercall(EAX=worm_state, ESI=worm, EDI=worm), plain RET.
579
- unsafe fn fire_indian_nuclear_test(worm: *mut CTaskWorm) {
580
  use crate::replacements::sound;
581
 
582
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
583
  let entry = &*(*worm).active_weapon_entry;
584
- let team = lookup_team_task(worm);
585
 
586
  // Message 0x59 (RaiseWater): buf[0]=fire_method, buf[4]=8
587
  if !team.is_null() {
588
  let mut buf = [0u8; 0x40C];
589
  buf[0..4].copy_from_slice(&entry.fire_method.to_ne_bytes());
590
  buf[4..8].copy_from_slice(&8i32.to_ne_bytes());
591
- CTaskTeam::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x59, 0x408, buf.as_ptr());
592
  }
593
 
594
  // Message 0x5A (NukeBlast): buf[0]=8
595
- let team = lookup_team_task(worm);
596
  if !team.is_null() {
597
  let mut buf = [0u8; 0x40C];
598
  buf[0..4].copy_from_slice(&8i32.to_ne_bytes());
599
- CTaskTeam::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x5A, 0x408, buf.as_ptr());
600
  }
601
 
602
  // PlaySoundGlobal(IndianAnthem, 8, 0x10000, 0x10000)
603
  sound::queue_sound(ddgame, KnownSoundId::IndianAnthem.into(), 8, Fixed::ONE, Fixed::ONE);
604
 
605
  // Message 0x51 (PoisonWorm): buf[0]=shot_count, buf[4]=2, buf[8]=team_index
606
- let team = lookup_team_task(worm);
607
  if !team.is_null() {
608
  let mut buf = [0u8; 0x40C];
609
  buf[0..4].copy_from_slice(&entry.fire_params.shot_count.to_ne_bytes());
610
  buf[4..8].copy_from_slice(&2i32.to_ne_bytes());
611
  buf[8..12].copy_from_slice(&(*worm).team_index.to_ne_bytes());
612
- CTaskTeam::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x51, 0x408, buf.as_ptr());
613
  }
614
 
615
  // PlaySoundGlobal(NukeFlash, 5, 0x10000, 0x10000)
@@ -693,17 +691,17 @@ unsafe extern "C" fn call_worm_landing_check(_worm: *mut CTaskWorm, _addr: u32)
693
  );
694
  }
695
 
696
- /// Jet Pack (subtype 22) — pure Rust port of 0x51EC30.
697
  ///
698
- /// Sends message 0x5E to CTaskTeam, then plays a jet pack sound.
699
  /// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
700
- unsafe fn fire_jet_pack(worm: *mut CTaskWorm) {
701
  use openwa_core::rebase::rb;
702
 
703
- // Send message 0x5E to CTaskTeam
704
- let team = lookup_team_task(worm);
705
  if !team.is_null() {
706
- CTaskTeam::handle_message_raw(team,
707
  worm as *mut openwa_core::task::CTask,
708
  0x5E,
709
  0,
@@ -756,19 +754,19 @@ unsafe extern "C" fn call_play_sound_usercall(
756
  );
757
  }
758
 
759
- /// Low Gravity (subtype 24) — pure Rust port of 0x51EA60.
760
  ///
761
- /// Sends message 0x5B to CTaskTeam with weapon/team info, then conditionally
762
  /// sets a gravity center point via FUN_00547E70.
763
  /// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
764
- unsafe fn fire_low_gravity(worm: *mut CTaskWorm) {
765
  use openwa_core::rebase::rb;
766
 
767
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
768
  let game_version = (*(*ddgame).game_info).game_version;
769
 
770
- // Send message 0x5B to CTaskTeam with weapon info buffer
771
- let team = lookup_team_task(worm);
772
  if !team.is_null() {
773
  let mut buf = [0u8; 0x40C];
774
  // buf[0x04] = 100 (0x64), buf[0x08] = 166 (0xA6), buf[0x0C] = weapon_id, buf[0x10] = team_index
@@ -777,7 +775,7 @@ unsafe fn fire_low_gravity(worm: *mut CTaskWorm) {
777
  buf[0x0C..0x10].copy_from_slice(&((*worm).selected_weapon as u32).to_ne_bytes());
778
  buf[0x10..0x14].copy_from_slice(&(*worm).team_index.to_ne_bytes());
779
 
780
- CTaskTeam::handle_message_raw(team,
781
  worm as *mut openwa_core::task::CTask,
782
  0x5B,
783
  0x408,
@@ -821,14 +819,14 @@ unsafe extern "C" fn call_set_gravity_center(
821
  );
822
  }
823
 
824
- /// Girder (type 4 subtype 3) — pure Rust port of 0x51E350.
825
  ///
826
  /// Allocates a CTaskGirder (0xA4 bytes), copies 7 DWORDs from fire_params
827
  /// as constructor arguments, and calls the constructor. The constructor handles
828
  /// actually placing the girder on the landscape.
829
  ///
830
  /// Convention: stdcall(worm, fire_params, local_struct), RET 0xC.
831
- unsafe fn fire_girder(
832
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
833
  ) {
834
  use openwa_core::rebase::rb;
@@ -909,26 +907,26 @@ unsafe extern "C" fn call_fire_usercall(_eax: *const (), _worm: *mut CTaskWorm,
909
  );
910
  }
911
 
912
- /// Freeze (subtype 17) — pure Rust port of 0x51E920.
913
  ///
914
- /// Plays a freeze sound, optionally creates a freeze visual overlay on the
915
  /// landscape and increments worm counters. The visual/counter path only runs
916
- /// when CTaskWorm+0x2EC (freeze_target_team) is nonzero.
917
  ///
918
  /// Convention: usercall(EAX=worm, ESI=worm, EDI=worm), plain RET.
919
- unsafe fn fire_freeze(worm: *mut CTaskWorm) {
920
  use openwa_core::rebase::rb;
921
 
922
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
923
  let game_version = (*(*ddgame).game_info).game_version;
924
 
925
- // Read freeze position and target from CTaskWorm fields
926
- let freeze_x = (*worm).weapon_param_1;
927
- let freeze_y = (*worm).weapon_param_2;
928
- let freeze_target = (*worm).weapon_param_3;
929
 
930
- // Choose sound: 0x70 if freeze has target or old version, else 0x73
931
- let sound_id: u32 = if freeze_target != 0 || game_version < 0x21 {
932
  0x70
933
  } else {
934
  0x73
@@ -939,28 +937,28 @@ unsafe fn fire_freeze(worm: *mut CTaskWorm) {
939
  let play_sound: PlaySoundGlobalFn = core::mem::transmute(rb(va::PLAY_SOUND_GLOBAL));
940
  let result = play_sound(worm, sound_id, 3, 0x10000, 0x10000);
941
 
942
- // If sound was queued, set its position to the freeze location
943
  if result != 0 {
944
  let queue_count = (*ddgame).sound_queue_count;
945
  if queue_count > 0 {
946
  let entry = &mut (*ddgame).sound_queue[(queue_count - 1) as usize];
947
  entry.is_local = 1;
948
  entry.secondary_vtable = 0;
949
- entry.pos_x = freeze_x as u32;
950
- entry.pos_y = freeze_y as u32;
951
  }
952
  }
953
 
954
- // If freeze has a target, apply the visual effect and update counters
955
- if freeze_target != 0 {
956
- // Call PCLandscape vtable[5] to create freeze overlay
957
  let landscape = (*ddgame).landscape as *mut u8;
958
  let landscape_vt = *(landscape as *const *const usize);
959
- let freeze_visual: unsafe extern "thiscall" fn(*mut u8, i32, i32, *mut u8, *mut u8) =
960
  core::mem::transmute(*landscape_vt.add(5));
961
- let sprite1 = (*ddgame).sprite_cache_2[freeze_target as usize];
962
- let sprite2 = (*ddgame).sprite_cache_2[19 + freeze_target as usize];
963
- freeze_visual(landscape, freeze_x >> 16, freeze_y >> 16, sprite1, sprite2);
964
 
965
  // Increment WormEntry counters
966
  let arena = openwa_core::engine::ddgame::TeamArenaRef::from_ptr(
@@ -1011,7 +1009,7 @@ fn special_impact_version_flags(base: u32, version: u8) -> u32 {
1011
  flags
1012
  }
1013
 
1014
- /// Pneumatic Drill (subtype 2) — pure Rust port of 0x51E3E0.
1015
  ///
1016
  /// Calls SpecialImpact with facing-offset position and scaled direction.
1017
  /// The original is usercall(ECX=local_struct, ESI=worm) — the old bridge
 
22
  use openwa_core::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
23
  use openwa_core::game::Weapon;
24
  use openwa_core::log::log_line;
25
+ use openwa_core::task::turn_game::CTaskTurnGame;
26
  use openwa_core::task::CTask;
27
  use openwa_core::task::worm::{CTaskWorm, WormState};
28
 
 
313
  let params_38_ptr = &raw const (*entry).fire_method as *const WeaponFireParams;
314
 
315
  match S::try_from(subtype) {
 
 
 
 
316
  Ok(S::FirePunch) => CTaskWorm::set_state_raw(worm, WormState::FirePunch),
317
+ Ok(S::BaseballBat) => fire_drill(worm, ctx as *const u8),
318
+ Ok(S::DragonBall) => fire_dragon_ball(worm, params_38_ptr, ctx as *const u8),
319
  Ok(S::Kamikaze) => CTaskWorm::set_state_raw(worm, WormState::Kamikaze),
320
+ Ok(S::SuicideBomber) => CTaskWorm::set_state_raw(worm, WormState::SuicideBomber),
321
+ Ok(S::Unknown6) => CTaskWorm::set_state_raw(worm, WormState::Unknown_0x70),
322
+ Ok(S::PneumaticDrill) => CTaskWorm::set_state_raw(worm, WormState::PneumaticDrill),
323
  Ok(S::Prod) => fire_prod(worm, ctx as *const u8),
324
+ Ok(S::Teleport) => fire_teleport(worm),
325
+ Ok(S::Blowtorch) => CTaskWorm::set_state_raw(worm, WormState::Blowtorch),
326
+ Ok(S::Parachute) => {} // TODO: parachute handler
327
+ Ok(S::Surrender) => fire_surrender(worm),
328
  Ok(S::MailMineMole) => fire_mail_mine_mole(worm),
329
+ Ok(S::NuclearTest) => {
330
  if can_fire_subtype16((*worm).state()) {
331
+ fire_nuclear_test(worm);
332
  } else {
333
  CTaskWorm::set_state_raw(worm, WormState::TeleportCancelled_Maybe);
334
  }
335
  }
336
+ Ok(S::Girder) => fire_girder(worm),
337
+ Ok(S::Unknown18) => CTaskWorm::set_state_raw(worm, WormState::Unknown_0x72),
338
  Ok(S::SkipGo) => fire_skip_go(worm, entry),
339
+ Ok(S::Freeze) => fire_freeze(worm),
340
  Ok(S::SelectWorm) => fire_select_worm(worm),
341
+ Ok(S::ScalesOfJustice) => fire_scales_of_justice(worm),
342
+ Ok(S::JetPack) => CTaskWorm::set_state_raw(worm, WormState::WeaponAimed_Maybe),
343
+ Ok(S::Armageddon) => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)),
344
  _ => {}
345
  }
346
  }
347
 
348
  /// Worm state validity check for subtype 16 — pure Rust port of 0x516930.
349
+ /// Used by Nuclear Test to gate firing.
350
  fn can_fire_subtype16(state: u32) -> bool {
351
  state == WormState::WeaponAimed_Maybe as u32
352
  || (WormState::AimingAngle_Maybe as u32..=WormState::PreFire_Maybe as u32).contains(&state)
 
354
 
355
  // ── Pure Rust fire handlers (no bridge needed) ──────────────
356
 
357
+ /// Look up the CTaskTurnGame entity for a worm via SharedData.
358
  ///
359
+ /// The entity at key (0, 0x14) is a CTaskTurnGame (inherits CTaskTeam).
360
  /// Returns null if not found.
361
+ pub(crate) unsafe fn lookup_turn_game(worm: *const CTaskWorm) -> *mut openwa_core::task::CTaskTurnGame {
362
  use openwa_core::task::SharedDataTable;
363
 
364
  let table = SharedDataTable::from_task(worm as *const CTask);
365
+ table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTurnGame
 
366
  }
367
 
368
+ /// Surrender (subtype 13) — sends message 0x2B (TaskMessage::Surrender) to
369
+ /// CTaskTurnGame via vtable dispatch.
370
  ///
371
+ /// TurnGame::HandleMessage (0x55DC00) delegates to CTaskTeam (0x557310) for
372
+ /// the broadcast, then handles end-turn logic and surrender sound.
 
373
  #[inline(never)]
374
+ unsafe fn fire_surrender(worm: *mut CTaskWorm) {
375
+ let team = lookup_turn_game(worm);
 
 
376
  if team.is_null() {
377
  return;
378
  }
379
 
 
 
 
380
  let mut buf = [0u8; 0x40C];
381
+ buf[0..4].copy_from_slice(&(*worm).team_index.to_ne_bytes());
382
 
383
+ // Dispatch through vtable — hits CTaskTurnGame::HandleMessage (0x55DC00)
384
+ // which delegates to CTaskTeam (0x557310) and handles end-turn/sound.
385
+ CTaskTurnGame::handle_message_raw(
386
  team,
387
  worm as *mut openwa_core::task::CTask,
388
+ 0x2B,
389
+ 4,
390
  buf.as_ptr(),
391
  );
392
  }
393
 
394
+ /// Send a message to CTaskTurnGame via SharedData lookup + vtable dispatch.
395
  ///
396
+ /// Shared pattern: look up CTaskTurnGame at key (0, 0x14), write team_index
397
+ /// into a 0x40C-byte buffer, and call HandleMessage (vtable slot 2).
398
  ///
399
  /// The 0x40C-byte local buffer is passed as data pointer; team_index is written
400
  /// at buf+0x00 to identify which team fired.
401
  unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
402
+ let team = lookup_turn_game(worm);
403
  if team.is_null() {
404
  return;
405
  }
 
408
  let team_index = (*worm).team_index;
409
  buf[0..4].copy_from_slice(&team_index.to_ne_bytes());
410
 
411
+ CTaskTurnGame::handle_message_raw(team,
412
  worm as *mut openwa_core::task::CTask,
413
  msg_type,
414
  4,
 
418
 
419
  /// Select Worm (subtype 21) — pure Rust replacement for 0x51EBE0.
420
  ///
421
+ /// Sends message 0x5D to CTaskTurnGame with buf = [8, team_index, ...].
422
  unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
423
+ let team = lookup_turn_game(worm);
424
  if team.is_null() {
425
  return;
426
  }
 
429
  buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
430
  buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
431
 
432
+ CTaskTurnGame::handle_message_raw(team,
433
  worm as *mut openwa_core::task::CTask,
434
  0x5D,
435
  0x408,
 
462
  }
463
  }
464
 
465
+ /// Freeze weapon (subtype 20) — pure Rust replacement for 0x51E600.
466
  ///
467
+ /// Sends message 0x29 (TaskMessage::Freeze) to CTaskTurnGame, then increments
468
  /// WormEntry.turn_action_counter_Maybe by 14 (0x0E).
469
+ unsafe fn fire_freeze(worm: *mut CTaskWorm) {
470
  use openwa_core::engine::ddgame::TeamArenaRef;
471
 
472
  fire_send_team_message(worm, 0x29);
 
482
  /// Mail/Mine/Mole (subtype 14) — pure Rust replacement for 0x51E670.
483
  ///
484
  /// Conditionally calls worm->vtable[0xE](0x65) based on game version and worm state,
485
+ /// then sends message 0x28 to CTaskTurnGame, then increments
486
  /// WormEntry.turn_action_counter_Maybe by 7.
487
  ///
488
  /// Version check logic (from disassembly at 0x51E670):
 
515
  entry.turn_action_counter_Maybe += 7;
516
  }
517
 
518
+ /// Teleport (subtype 10) — pure Rust port of 0x51E710.
519
  ///
520
+ /// If CTaskWorm+0x208 == 0: set state to 0x6F (AirStrikePending_Maybe).
521
  /// Otherwise: play sound, spawn visual effect, update position, compute
522
  /// new state based on game version, clear action fields.
523
  ///
524
  /// Convention: usercall(EAX=worm, ESI=worm, EDI=worm), plain RET.
525
+ unsafe fn fire_teleport(worm: *mut CTaskWorm) {
526
  use openwa_core::rebase::rb;
527
 
528
  if (*worm)._unknown_208 == 0 {
 
568
  // Debug log block (ddgame+0x8144) omitted — only writes to debug file
569
  }
570
 
571
+ /// Nuclear Test (subtype 16) — pure Rust port of 0x51EB00.
572
  ///
573
+ /// Sends three messages to CTaskTurnGame: RaiseWater (0x59), NukeBlast (0x5A),
574
  /// PoisonWorm (0x51), and plays two sounds. Gated by can_fire_subtype16.
575
  ///
576
  /// Convention: usercall(EAX=worm_state, ESI=worm, EDI=worm), plain RET.
577
+ unsafe fn fire_nuclear_test(worm: *mut CTaskWorm) {
578
  use crate::replacements::sound;
579
 
580
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
581
  let entry = &*(*worm).active_weapon_entry;
582
+ let team = lookup_turn_game(worm);
583
 
584
  // Message 0x59 (RaiseWater): buf[0]=fire_method, buf[4]=8
585
  if !team.is_null() {
586
  let mut buf = [0u8; 0x40C];
587
  buf[0..4].copy_from_slice(&entry.fire_method.to_ne_bytes());
588
  buf[4..8].copy_from_slice(&8i32.to_ne_bytes());
589
+ CTaskTurnGame::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x59, 0x408, buf.as_ptr());
590
  }
591
 
592
  // Message 0x5A (NukeBlast): buf[0]=8
593
+ let team = lookup_turn_game(worm);
594
  if !team.is_null() {
595
  let mut buf = [0u8; 0x40C];
596
  buf[0..4].copy_from_slice(&8i32.to_ne_bytes());
597
+ CTaskTurnGame::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x5A, 0x408, buf.as_ptr());
598
  }
599
 
600
  // PlaySoundGlobal(IndianAnthem, 8, 0x10000, 0x10000)
601
  sound::queue_sound(ddgame, KnownSoundId::IndianAnthem.into(), 8, Fixed::ONE, Fixed::ONE);
602
 
603
  // Message 0x51 (PoisonWorm): buf[0]=shot_count, buf[4]=2, buf[8]=team_index
604
+ let team = lookup_turn_game(worm);
605
  if !team.is_null() {
606
  let mut buf = [0u8; 0x40C];
607
  buf[0..4].copy_from_slice(&entry.fire_params.shot_count.to_ne_bytes());
608
  buf[4..8].copy_from_slice(&2i32.to_ne_bytes());
609
  buf[8..12].copy_from_slice(&(*worm).team_index.to_ne_bytes());
610
+ CTaskTurnGame::handle_message_raw(team,worm as *mut openwa_core::task::CTask, 0x51, 0x408, buf.as_ptr());
611
  }
612
 
613
  // PlaySoundGlobal(NukeFlash, 5, 0x10000, 0x10000)
 
691
  );
692
  }
693
 
694
+ /// Scales of Justice (subtype 22) — pure Rust port of 0x51EC30.
695
  ///
696
+ /// Sends message 0x5E to CTaskTurnGame, then plays a sound.
697
  /// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
698
+ unsafe fn fire_scales_of_justice(worm: *mut CTaskWorm) {
699
  use openwa_core::rebase::rb;
700
 
701
+ // Send message 0x5E to CTaskTurnGame
702
+ let team = lookup_turn_game(worm);
703
  if !team.is_null() {
704
+ CTaskTurnGame::handle_message_raw(team,
705
  worm as *mut openwa_core::task::CTask,
706
  0x5E,
707
  0,
 
754
  );
755
  }
756
 
757
+ /// Armageddon (subtype 24) — pure Rust port of 0x51EA60.
758
  ///
759
+ /// Sends message 0x5B to CTaskTurnGame with weapon/team info, then conditionally
760
  /// sets a gravity center point via FUN_00547E70.
761
  /// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
762
+ unsafe fn fire_armageddon(worm: *mut CTaskWorm) {
763
  use openwa_core::rebase::rb;
764
 
765
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
766
  let game_version = (*(*ddgame).game_info).game_version;
767
 
768
+ // Send message 0x5B to CTaskTurnGame with weapon info buffer
769
+ let team = lookup_turn_game(worm);
770
  if !team.is_null() {
771
  let mut buf = [0u8; 0x40C];
772
  // buf[0x04] = 100 (0x64), buf[0x08] = 166 (0xA6), buf[0x0C] = weapon_id, buf[0x10] = team_index
 
775
  buf[0x0C..0x10].copy_from_slice(&((*worm).selected_weapon as u32).to_ne_bytes());
776
  buf[0x10..0x14].copy_from_slice(&(*worm).team_index.to_ne_bytes());
777
 
778
+ CTaskTurnGame::handle_message_raw(team,
779
  worm as *mut openwa_core::task::CTask,
780
  0x5B,
781
  0x408,
 
819
  );
820
  }
821
 
822
+ /// DragonBall (type 4 subtype 3) — pure Rust port of 0x51E350.
823
  ///
824
  /// Allocates a CTaskGirder (0xA4 bytes), copies 7 DWORDs from fire_params
825
  /// as constructor arguments, and calls the constructor. The constructor handles
826
  /// actually placing the girder on the landscape.
827
  ///
828
  /// Convention: stdcall(worm, fire_params, local_struct), RET 0xC.
829
+ unsafe fn fire_dragon_ball(
830
  worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u8,
831
  ) {
832
  use openwa_core::rebase::rb;
 
907
  );
908
  }
909
 
910
+ /// Girder/GirderPack (subtype 17) — pure Rust port of 0x51E920.
911
  ///
912
+ /// Plays a sound, optionally creates a visual overlay on the
913
  /// landscape and increments worm counters. The visual/counter path only runs
914
+ /// when CTaskWorm+0x2EC (weapon_param_3) is nonzero.
915
  ///
916
  /// Convention: usercall(EAX=worm, ESI=worm, EDI=worm), plain RET.
917
+ unsafe fn fire_girder(worm: *mut CTaskWorm) {
918
  use openwa_core::rebase::rb;
919
 
920
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
921
  let game_version = (*(*ddgame).game_info).game_version;
922
 
923
+ // Read girder position and sprite index from CTaskWorm fields
924
+ let girder_x = (*worm).weapon_param_1;
925
+ let girder_y = (*worm).weapon_param_2;
926
+ let girder_sprite = (*worm).weapon_param_3;
927
 
928
+ // Choose sound: 0x70 if girder has sprite or old version, else 0x73
929
+ let sound_id: u32 = if girder_sprite != 0 || game_version < 0x21 {
930
  0x70
931
  } else {
932
  0x73
 
937
  let play_sound: PlaySoundGlobalFn = core::mem::transmute(rb(va::PLAY_SOUND_GLOBAL));
938
  let result = play_sound(worm, sound_id, 3, 0x10000, 0x10000);
939
 
940
+ // If sound was queued, set its position to the girder location
941
  if result != 0 {
942
  let queue_count = (*ddgame).sound_queue_count;
943
  if queue_count > 0 {
944
  let entry = &mut (*ddgame).sound_queue[(queue_count - 1) as usize];
945
  entry.is_local = 1;
946
  entry.secondary_vtable = 0;
947
+ entry.pos_x = girder_x as u32;
948
+ entry.pos_y = girder_y as u32;
949
  }
950
  }
951
 
952
+ // If girder has a sprite, apply the visual overlay and update counters
953
+ if girder_sprite != 0 {
954
+ // Call PCLandscape vtable[5] to create girder overlay
955
  let landscape = (*ddgame).landscape as *mut u8;
956
  let landscape_vt = *(landscape as *const *const usize);
957
+ let girder_visual: unsafe extern "thiscall" fn(*mut u8, i32, i32, *mut u8, *mut u8) =
958
  core::mem::transmute(*landscape_vt.add(5));
959
+ let sprite1 = (*ddgame).sprite_cache_2[girder_sprite as usize];
960
+ let sprite2 = (*ddgame).sprite_cache_2[19 + girder_sprite as usize];
961
+ girder_visual(landscape, girder_x >> 16, girder_y >> 16, sprite1, sprite2);
962
 
963
  // Increment WormEntry counters
964
  let arena = openwa_core::engine::ddgame::TeamArenaRef::from_ptr(
 
1009
  flags
1010
  }
1011
 
1012
+ /// BaseballBat (subtype 2) — pure Rust port of 0x51E3E0.
1013
  ///
1014
  /// Calls SpecialImpact with facing-offset position and scaled direction.
1015
  /// The original is usercall(ECX=local_struct, ESI=worm) — the old bridge
crates/openwa-wormkit/src/replacements/weapon_release.rs CHANGED
@@ -10,7 +10,7 @@ use openwa_core::audio::{KnownSoundId, SoundId};
10
  use openwa_core::fixed::Fixed;
11
  use openwa_core::game::Weapon;
12
  use openwa_core::log::log_line;
13
- use openwa_core::task::team::CTaskTeam;
14
  use openwa_core::task::worm::{CTaskWorm, WormState};
15
  use openwa_core::task::{CGameTask, CTask, SharedDataTable, Task};
16
 
@@ -250,9 +250,9 @@ unsafe extern "cdecl" fn weapon_release_impl(
250
  );
251
  write_u32(&mut msg_buf, 0x1C, weapon as u32);
252
 
253
- let team = weapon::lookup_team_task(worm);
254
  if !team.is_null() {
255
- CTaskTeam::handle_message_raw(
256
  team,
257
  worm as *mut openwa_core::task::CTask,
258
  0x49,
@@ -343,24 +343,24 @@ unsafe extern "cdecl" fn weapon_release_impl(
343
  Ok(FireType::Special) => {
344
  use openwa_core::game::weapon::SpecialFireSubtype as S;
345
  match S::try_from((*entry).special_subtype) {
346
- Ok(S::PneumaticDrill) => {
347
  sound::play_sound_local(
348
  task, KnownSoundId::BaseballBatRelease, 3, Fixed::ONE, Fixed::ONE,
349
  );
350
  }
351
- Ok(S::Girder) => {
352
  sound::play_sound_local(task, SoundId((*entry).fire_method as u32), 3, Fixed::ONE, Fixed::ONE);
353
  }
354
- Ok(S::BaseballBat) => {
355
  // Sound ID from fire_params.spread (polymorphic use of field)
356
  sound::play_sound_local(task, SoundId((*entry).fire_params.spread as u32), 3, Fixed::ONE, Fixed::ONE);
357
  }
358
- Ok(S::AirStrike) => {
359
  if w._unknown_208 == 0 {
360
  sound::play_sound_local(task, KnownSoundId::Teleport, 3, Fixed::ONE, Fixed::ONE);
361
  }
362
  }
363
- Ok(S::ScalesOfJustice) => {
364
  if w.sound_handle == 0 {
365
  sound::play_worm_sound(worm, SoundId(0x10035), Fixed::ONE);
366
  }
 
10
  use openwa_core::fixed::Fixed;
11
  use openwa_core::game::Weapon;
12
  use openwa_core::log::log_line;
13
+ use openwa_core::task::turn_game::CTaskTurnGame;
14
  use openwa_core::task::worm::{CTaskWorm, WormState};
15
  use openwa_core::task::{CGameTask, CTask, SharedDataTable, Task};
16
 
 
250
  );
251
  write_u32(&mut msg_buf, 0x1C, weapon as u32);
252
 
253
+ let team = weapon::lookup_turn_game(worm);
254
  if !team.is_null() {
255
+ CTaskTurnGame::handle_message_raw(
256
  team,
257
  worm as *mut openwa_core::task::CTask,
258
  0x49,
 
343
  Ok(FireType::Special) => {
344
  use openwa_core::game::weapon::SpecialFireSubtype as S;
345
  match S::try_from((*entry).special_subtype) {
346
+ Ok(S::BaseballBat) => {
347
  sound::play_sound_local(
348
  task, KnownSoundId::BaseballBatRelease, 3, Fixed::ONE, Fixed::ONE,
349
  );
350
  }
351
+ Ok(S::DragonBall) => {
352
  sound::play_sound_local(task, SoundId((*entry).fire_method as u32), 3, Fixed::ONE, Fixed::ONE);
353
  }
354
+ Ok(S::Kamikaze) => {
355
  // Sound ID from fire_params.spread (polymorphic use of field)
356
  sound::play_sound_local(task, SoundId((*entry).fire_params.spread as u32), 3, Fixed::ONE, Fixed::ONE);
357
  }
358
+ Ok(S::Teleport) => {
359
  if w._unknown_208 == 0 {
360
  sound::play_sound_local(task, KnownSoundId::Teleport, 3, Fixed::ONE, Fixed::ONE);
361
  }
362
  }
363
+ Ok(S::Blowtorch) => {
364
  if w.sound_handle == 0 {
365
  sound::play_worm_sound(worm, SoundId(0x10035), Fixed::ONE);
366
  }
testdata/replays/bomber_parachute.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a06787243c61c9fb698306f1bfd739e3de4f4278a76859028fe13176dbbe97e5
3
+ size 12811
testdata/replays/bomber_parachute_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05085896eec0e0f617ae3d2c7d047027752307434579bfb1cae7419981e59d57
3
+ size 1345
testdata/replays/napalm.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:83d52a7dae84c43f1580bb9913664bb5d0ba841353e2b4797ca8262e771f4392
3
+ size 15815
testdata/replays/napalm_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64f120997c8f57f05df6b05146ef167d5e32109edd23b0c8b8f82f020a606d1d
3
+ size 2034