fix: correct SharedData entity type and rename all SpecialFireSubtype/WormState
Browse filesSharedData 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 +58 -38
- crates/openwa-core/src/task/team.rs +11 -9
- crates/openwa-core/src/task/team_message.rs +5 -6
- crates/openwa-core/src/task/worm.rs +14 -14
- crates/openwa-wormkit/src/replacements/weapon.rs +94 -96
- crates/openwa-wormkit/src/replacements/weapon_release.rs +8 -8
- testdata/replays/bomber_parachute.WAgame +3 -0
- testdata/replays/bomber_parachute_expected.log +3 -0
- testdata/replays/napalm.WAgame +3 -0
- testdata/replays/napalm_expected.log +3 -0
|
@@ -205,63 +205,83 @@ impl TryFrom<i32> for FireMethod {
|
|
| 205 |
}
|
| 206 |
}
|
| 207 |
|
| 208 |
-
/// Special weapon subtype
|
| 209 |
-
///
|
| 210 |
-
///
|
| 211 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 212 |
#[repr(i32)]
|
|
|
|
| 213 |
pub enum SpecialFireSubtype {
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
BaseballBat =
|
| 218 |
-
|
| 219 |
-
DragonBall =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
// 7: unknown
|
| 221 |
-
|
|
|
|
| 222 |
Prod = 9,
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
//
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
MailMineMole = 14,
|
| 228 |
// 15: unknown
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
| 232 |
SkipGo = 19,
|
| 233 |
-
|
|
|
|
| 234 |
SelectWorm = 21,
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
| 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::
|
| 245 |
-
2 => Ok(Self::
|
| 246 |
-
3 => Ok(Self::
|
| 247 |
-
4 => Ok(Self::
|
| 248 |
-
5 => Ok(Self::
|
| 249 |
-
6 => Ok(Self::
|
| 250 |
-
8 => Ok(Self::
|
| 251 |
9 => Ok(Self::Prod),
|
| 252 |
-
10 => Ok(Self::
|
| 253 |
-
11 => Ok(Self::
|
| 254 |
-
|
|
|
|
| 255 |
14 => Ok(Self::MailMineMole),
|
| 256 |
-
16 => Ok(Self::
|
| 257 |
-
17 => Ok(Self::
|
| 258 |
-
18 => Ok(Self::
|
| 259 |
19 => Ok(Self::SkipGo),
|
| 260 |
-
20 => Ok(Self::
|
| 261 |
21 => Ok(Self::SelectWorm),
|
| 262 |
-
22 => Ok(Self::
|
| 263 |
-
23 => Ok(Self::
|
| 264 |
-
24 => Ok(Self::
|
| 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 |
}
|
|
@@ -118,20 +118,22 @@ bind_CTaskTeamVTable!(CTaskTeam, base.vtable);
|
|
| 118 |
// ── Typed message handlers ──────────────────────────────────
|
| 119 |
|
| 120 |
impl CTaskTeam {
|
| 121 |
-
/// Handle
|
| 122 |
-
/// CTaskTeam::HandleMessage case 0x2B.
|
| 123 |
///
|
| 124 |
-
///
|
| 125 |
-
///
|
|
|
|
|
|
|
|
|
|
| 126 |
///
|
| 127 |
-
///
|
| 128 |
-
///
|
| 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
|
| 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,
|
|
@@ -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):
|
| 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 |
-
|
|
|
|
| 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::
|
| 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::
|
| 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 |
}
|
|
@@ -37,27 +37,27 @@ pub enum WormState {
|
|
| 37 |
Unknown_0x6A = 0x6A,
|
| 38 |
/// Unknown (within weapon/action range).
|
| 39 |
Unknown_0x6B = 0x6B,
|
| 40 |
-
///
|
| 41 |
-
|
| 42 |
-
///
|
| 43 |
-
|
| 44 |
-
///
|
| 45 |
-
|
| 46 |
/// Air Strike pending — set when air strike fires with `_unknown_208 == 0`.
|
| 47 |
AirStrikePending_Maybe = 0x6F,
|
| 48 |
-
///
|
| 49 |
-
|
| 50 |
-
///
|
| 51 |
-
|
| 52 |
-
///
|
| 53 |
-
|
| 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 |
-
///
|
| 60 |
-
|
| 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.
|
|
@@ -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::
|
| 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::
|
|
|
|
| 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::
|
| 325 |
-
Ok(S::
|
| 326 |
-
Ok(S::
|
|
|
|
| 327 |
Ok(S::MailMineMole) => fire_mail_mine_mole(worm),
|
| 328 |
-
Ok(S::
|
| 329 |
if can_fire_subtype16((*worm).state()) {
|
| 330 |
-
|
| 331 |
} else {
|
| 332 |
CTaskWorm::set_state_raw(worm, WormState::TeleportCancelled_Maybe);
|
| 333 |
}
|
| 334 |
}
|
| 335 |
-
Ok(S::
|
| 336 |
-
Ok(S::
|
| 337 |
Ok(S::SkipGo) => fire_skip_go(worm, entry),
|
| 338 |
-
Ok(S::
|
| 339 |
Ok(S::SelectWorm) => fire_select_worm(worm),
|
| 340 |
-
Ok(S::
|
| 341 |
-
Ok(S::
|
| 342 |
-
Ok(S::
|
| 343 |
_ => {}
|
| 344 |
}
|
| 345 |
}
|
| 346 |
|
| 347 |
/// Worm state validity check for subtype 16 — pure Rust port of 0x516930.
|
| 348 |
-
/// Used by
|
| 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
|
| 357 |
///
|
|
|
|
| 358 |
/// Returns null if not found.
|
| 359 |
-
pub(crate) unsafe fn
|
| 360 |
use openwa_core::task::SharedDataTable;
|
| 361 |
|
| 362 |
let table = SharedDataTable::from_task(worm as *const CTask);
|
| 363 |
-
|
| 364 |
-
table.lookup(0, 0x14) as *mut openwa_core::task::CTaskTeam
|
| 365 |
}
|
| 366 |
|
| 367 |
-
///
|
|
|
|
| 368 |
///
|
| 369 |
-
///
|
| 370 |
-
///
|
| 371 |
-
/// serializes to raw bytes, and dispatches through CTaskTeam's WA vtable.
|
| 372 |
#[inline(never)]
|
| 373 |
-
unsafe fn
|
| 374 |
-
|
| 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 |
-
|
| 386 |
|
| 387 |
-
|
|
|
|
|
|
|
| 388 |
team,
|
| 389 |
worm as *mut openwa_core::task::CTask,
|
| 390 |
-
|
| 391 |
-
|
| 392 |
buf.as_ptr(),
|
| 393 |
);
|
| 394 |
}
|
| 395 |
|
| 396 |
-
/// Send a message to
|
| 397 |
///
|
| 398 |
-
///
|
| 399 |
-
///
|
| 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 =
|
| 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 |
-
|
| 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
|
| 424 |
unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
|
| 425 |
-
let team =
|
| 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 |
-
|
| 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 |
-
///
|
| 468 |
///
|
| 469 |
-
/// Sends message 0x29 to
|
| 470 |
/// WormEntry.turn_action_counter_Maybe by 14 (0x0E).
|
| 471 |
-
unsafe fn
|
| 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
|
| 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 |
-
///
|
| 521 |
///
|
| 522 |
-
/// If CTaskWorm+0x208 == 0: set state to 0x6F (
|
| 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
|
| 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 |
-
///
|
| 574 |
///
|
| 575 |
-
/// Sends three messages to
|
| 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
|
| 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 =
|
| 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 |
-
|
| 592 |
}
|
| 593 |
|
| 594 |
// Message 0x5A (NukeBlast): buf[0]=8
|
| 595 |
-
let team =
|
| 596 |
if !team.is_null() {
|
| 597 |
let mut buf = [0u8; 0x40C];
|
| 598 |
buf[0..4].copy_from_slice(&8i32.to_ne_bytes());
|
| 599 |
-
|
| 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 =
|
| 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 |
-
|
| 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 |
-
///
|
| 697 |
///
|
| 698 |
-
/// Sends message 0x5E to
|
| 699 |
/// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
|
| 700 |
-
unsafe fn
|
| 701 |
use openwa_core::rebase::rb;
|
| 702 |
|
| 703 |
-
// Send message 0x5E to
|
| 704 |
-
let team =
|
| 705 |
if !team.is_null() {
|
| 706 |
-
|
| 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 |
-
///
|
| 760 |
///
|
| 761 |
-
/// Sends message 0x5B to
|
| 762 |
/// sets a gravity center point via FUN_00547E70.
|
| 763 |
/// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
|
| 764 |
-
unsafe fn
|
| 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
|
| 771 |
-
let team =
|
| 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 |
-
|
| 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 |
-
///
|
| 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
|
| 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 |
-
///
|
| 913 |
///
|
| 914 |
-
/// Plays a
|
| 915 |
/// landscape and increments worm counters. The visual/counter path only runs
|
| 916 |
-
/// when CTaskWorm+0x2EC (
|
| 917 |
///
|
| 918 |
/// Convention: usercall(EAX=worm, ESI=worm, EDI=worm), plain RET.
|
| 919 |
-
unsafe fn
|
| 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
|
| 926 |
-
let
|
| 927 |
-
let
|
| 928 |
-
let
|
| 929 |
|
| 930 |
-
// Choose sound: 0x70 if
|
| 931 |
-
let sound_id: u32 = if
|
| 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
|
| 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 =
|
| 950 |
-
entry.pos_y =
|
| 951 |
}
|
| 952 |
}
|
| 953 |
|
| 954 |
-
// If
|
| 955 |
-
if
|
| 956 |
-
// Call PCLandscape vtable[5] to create
|
| 957 |
let landscape = (*ddgame).landscape as *mut u8;
|
| 958 |
let landscape_vt = *(landscape as *const *const usize);
|
| 959 |
-
let
|
| 960 |
core::mem::transmute(*landscape_vt.add(5));
|
| 961 |
-
let sprite1 = (*ddgame).sprite_cache_2[
|
| 962 |
-
let sprite2 = (*ddgame).sprite_cache_2[19 +
|
| 963 |
-
|
| 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 |
-
///
|
| 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
|
|
@@ -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::
|
| 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::
|
| 254 |
if !team.is_null() {
|
| 255 |
-
|
| 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::
|
| 347 |
sound::play_sound_local(
|
| 348 |
task, KnownSoundId::BaseballBatRelease, 3, Fixed::ONE, Fixed::ONE,
|
| 349 |
);
|
| 350 |
}
|
| 351 |
-
Ok(S::
|
| 352 |
sound::play_sound_local(task, SoundId((*entry).fire_method as u32), 3, Fixed::ONE, Fixed::ONE);
|
| 353 |
}
|
| 354 |
-
Ok(S::
|
| 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::
|
| 359 |
if w._unknown_208 == 0 {
|
| 360 |
sound::play_sound_local(task, KnownSoundId::Teleport, 3, Fixed::ONE, Fixed::ONE);
|
| 361 |
}
|
| 362 |
}
|
| 363 |
-
Ok(S::
|
| 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 |
}
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a06787243c61c9fb698306f1bfd739e3de4f4278a76859028fe13176dbbe97e5
|
| 3 |
+
size 12811
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:05085896eec0e0f617ae3d2c7d047027752307434579bfb1cae7419981e59d57
|
| 3 |
+
size 1345
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:83d52a7dae84c43f1580bb9913664bb5d0ba841353e2b4797ca8262e771f4392
|
| 3 |
+
size 15815
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:64f120997c8f57f05df6b05146ef167d5e32109edd23b0c8b8f82f020a606d1d
|
| 3 |
+
size 2034
|