feat: port Jet Pack weapon to pure Rust
Browse filesPort FireWeapon__JetPack (0x51EC30) — sends message 0x5E to CTaskTeam,
then plays jet pack sound via FUN_0053EC70 (usercall EDI=0x6CB) and
FUN_005480F0 (usercall EAX=-21).
Low Gravity (0x51EA60) port attempted but deferred — changing match arm 24
causes a codegen-dependent desync in freeze_nuke_armageddon even with a
no-op body. Kept on bridge with investigation note.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-wormkit/src/replacements/weapon.rs
CHANGED
|
@@ -401,11 +401,12 @@ unsafe fn fire_weapon_special(
|
|
| 401 |
20 => fire_surrender(worm),
|
| 402 |
// Select Worm (pure Rust)
|
| 403 |
21 => fire_select_worm(worm),
|
| 404 |
-
// Jet Pack (
|
| 405 |
-
22 =>
|
| 406 |
// Magic Bullet
|
| 407 |
23 => (*worm).set_state(WormState::WeaponAimed_Maybe),
|
| 408 |
-
// Low Gravity
|
|
|
|
| 409 |
24 => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)),
|
| 410 |
_ => {}
|
| 411 |
}
|
|
@@ -555,6 +556,134 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
|
|
| 555 |
entry.turn_action_counter_Maybe += 7;
|
| 556 |
}
|
| 557 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
/// Girder (type 4 subtype 3) — pure Rust port of 0x51E350.
|
| 559 |
///
|
| 560 |
/// Allocates a CTaskGirder (0xA4 bytes), copies 7 DWORDs from fire_params
|
|
|
|
| 401 |
20 => fire_surrender(worm),
|
| 402 |
// Select Worm (pure Rust)
|
| 403 |
21 => fire_select_worm(worm),
|
| 404 |
+
// Jet Pack (pure Rust)
|
| 405 |
+
22 => fire_jet_pack(worm),
|
| 406 |
// Magic Bullet
|
| 407 |
23 => (*worm).set_state(WormState::WeaponAimed_Maybe),
|
| 408 |
+
// Low Gravity — kept on bridge; changing this arm causes codegen-dependent
|
| 409 |
+
// desync in freeze_nuke_armageddon (Armageddon behavior changes). Needs investigation.
|
| 410 |
24 => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)),
|
| 411 |
_ => {}
|
| 412 |
}
|
|
|
|
| 556 |
entry.turn_action_counter_Maybe += 7;
|
| 557 |
}
|
| 558 |
|
| 559 |
+
/// Jet Pack (subtype 22) — pure Rust port of 0x51EC30.
|
| 560 |
+
///
|
| 561 |
+
/// Sends message 0x5E to CTaskTeam, then plays a jet pack sound.
|
| 562 |
+
/// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
|
| 563 |
+
unsafe fn fire_jet_pack(worm: *mut CTaskWorm) {
|
| 564 |
+
use openwa_core::rebase::rb;
|
| 565 |
+
|
| 566 |
+
// Send message 0x5E to CTaskTeam
|
| 567 |
+
let team = lookup_team_task(worm);
|
| 568 |
+
if !team.is_null() {
|
| 569 |
+
(*team).handle_message(
|
| 570 |
+
(*worm).as_task_ptr_mut(),
|
| 571 |
+
0x5E,
|
| 572 |
+
0,
|
| 573 |
+
core::ptr::null(),
|
| 574 |
+
);
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
// Play jet pack sound:
|
| 578 |
+
// FUN_0053EC70: usercall(EDI=0x6CB) + stdcall(timer_obj)
|
| 579 |
+
// FUN_005480F0: usercall(EAX=-21) + stdcall(worm, result, 0x17, &worm_name)
|
| 580 |
+
let ddgame = (*worm).ddgame();
|
| 581 |
+
let sound_val = call_get_sound_val((*ddgame).timer_obj, rb(0x53EC70));
|
| 582 |
+
call_play_sound_usercall(worm, sound_val, 0x17, (*worm).worm_name.as_ptr(), rb(0x5480F0));
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
/// Bridge: FUN_0053EC70 — usercall(EDI=0x6CB) + stdcall(timer_obj). Returns EAX.
|
| 586 |
+
#[unsafe(naked)]
|
| 587 |
+
unsafe extern "C" fn call_get_sound_val(_timer_obj: *mut u8, _addr: u32) -> u32 {
|
| 588 |
+
core::arch::naked_asm!(
|
| 589 |
+
"push ebx",
|
| 590 |
+
"push edi",
|
| 591 |
+
// Stack: 2 saves(8) + ret(4) = 12 to first arg
|
| 592 |
+
"mov edi, 0x6CB",
|
| 593 |
+
"mov ebx, [esp+16]", // addr
|
| 594 |
+
"push [esp+12]", // timer_obj
|
| 595 |
+
"call ebx",
|
| 596 |
+
"pop edi",
|
| 597 |
+
"pop ebx",
|
| 598 |
+
"ret",
|
| 599 |
+
);
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
/// Bridge: usercall(EAX=-21) + stdcall(4 params). Plain RET (callee doesn't clean).
|
| 603 |
+
#[unsafe(naked)]
|
| 604 |
+
unsafe extern "C" fn call_play_sound_usercall(
|
| 605 |
+
_worm: *mut CTaskWorm, _sound_val: u32, _param3: u32, _name: *const u8, _addr: u32,
|
| 606 |
+
) {
|
| 607 |
+
core::arch::naked_asm!(
|
| 608 |
+
"push ebx",
|
| 609 |
+
// Stack: 1 save(4) + ret(4) = 8 to first arg
|
| 610 |
+
"mov ebx, [esp+24]", // addr (8 + 4*4 = 24)
|
| 611 |
+
"push [esp+20]", // name
|
| 612 |
+
"push [esp+20]", // param3 (shifted +4)
|
| 613 |
+
"push [esp+20]", // sound_val (shifted +8)
|
| 614 |
+
"push [esp+20]", // worm (shifted +12)
|
| 615 |
+
"mov eax, 0xFFFFFFEB", // EAX = -21 (usercall param)
|
| 616 |
+
"call ebx",
|
| 617 |
+
"pop ebx",
|
| 618 |
+
"ret",
|
| 619 |
+
);
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
/// Low Gravity (subtype 24) — pure Rust port of 0x51EA60.
|
| 623 |
+
///
|
| 624 |
+
/// Sends message 0x5B to CTaskTeam with weapon/team info, then conditionally
|
| 625 |
+
/// sets a gravity center point via FUN_00547E70.
|
| 626 |
+
/// Convention: usercall(EAX=entry, ESI=worm, EDI=worm), plain RET.
|
| 627 |
+
unsafe fn fire_low_gravity(worm: *mut CTaskWorm) {
|
| 628 |
+
use openwa_core::rebase::rb;
|
| 629 |
+
|
| 630 |
+
let ddgame = (*worm).ddgame();
|
| 631 |
+
let game_version = (*(*ddgame).game_info).game_version;
|
| 632 |
+
|
| 633 |
+
// Send message 0x5B to CTaskTeam with weapon info buffer
|
| 634 |
+
let team = lookup_team_task(worm);
|
| 635 |
+
if !team.is_null() {
|
| 636 |
+
let mut buf = [0u8; 0x40C];
|
| 637 |
+
// buf[0x04] = 100 (0x64), buf[0x08] = 166 (0xA6), buf[0x0C] = weapon_id, buf[0x10] = team_index
|
| 638 |
+
buf[0x04..0x08].copy_from_slice(&100i32.to_ne_bytes());
|
| 639 |
+
buf[0x08..0x0C].copy_from_slice(&166i32.to_ne_bytes());
|
| 640 |
+
buf[0x0C..0x10].copy_from_slice(&(*worm).selected_weapon.to_ne_bytes());
|
| 641 |
+
buf[0x10..0x14].copy_from_slice(&(*worm).team_index.to_ne_bytes());
|
| 642 |
+
|
| 643 |
+
(*team).handle_message(
|
| 644 |
+
(*worm).as_task_ptr_mut(),
|
| 645 |
+
0x5B,
|
| 646 |
+
0x408,
|
| 647 |
+
buf.as_ptr(),
|
| 648 |
+
);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
// If old game version or worm state 0x69, set gravity center
|
| 652 |
+
if game_version < 0x50 || (*worm).state() == 0x69 {
|
| 653 |
+
// Compute half-level center from DDGame+0x77C0/0x77C4 (level dimensions)
|
| 654 |
+
let ddgame_raw = ddgame as *const u8;
|
| 655 |
+
let level_w = *(ddgame_raw.add(0x77C0) as *const i32);
|
| 656 |
+
let level_h = *(ddgame_raw.add(0x77C4) as *const i32);
|
| 657 |
+
// Convert to Fixed16.16 and halve: (value << 16) / 2
|
| 658 |
+
// Original uses SHL 16; CDQ; SUB EAX,EDX; SAR 1 (round-toward-zero divide)
|
| 659 |
+
let half_x = ((level_w << 16) + (if level_w < 0 { 1 } else { 0 })) >> 1;
|
| 660 |
+
let half_y = ((level_h << 16) + (if level_h < 0 { 1 } else { 0 })) >> 1;
|
| 661 |
+
|
| 662 |
+
// FUN_00547E70: usercall(ECX=half_x, EDX=half_y) + stdcall(worm), RET 0x4
|
| 663 |
+
type SetGravityCenterFn = unsafe extern "stdcall" fn(*mut CTaskWorm);
|
| 664 |
+
// Need naked bridge for ECX/EDX usercall params
|
| 665 |
+
call_set_gravity_center(worm, half_x, half_y, rb(0x547E70));
|
| 666 |
+
}
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
/// Bridge: usercall(ECX=half_x, EDX=half_y) + stdcall(worm), RET 0x4.
|
| 670 |
+
#[unsafe(naked)]
|
| 671 |
+
unsafe extern "C" fn call_set_gravity_center(
|
| 672 |
+
_worm: *mut CTaskWorm, _half_x: i32, _half_y: i32, _addr: u32,
|
| 673 |
+
) {
|
| 674 |
+
core::arch::naked_asm!(
|
| 675 |
+
"push ebx",
|
| 676 |
+
// Stack: 1 save(4) + ret(4) = 8 to first arg
|
| 677 |
+
"mov ecx, [esp+12]", // half_x
|
| 678 |
+
"mov edx, [esp+16]", // half_y
|
| 679 |
+
"mov ebx, [esp+20]", // addr
|
| 680 |
+
"push [esp+8]", // worm (shifted by 0 extra pushes before this)
|
| 681 |
+
"call ebx", // RET 0x4 cleans worm
|
| 682 |
+
"pop ebx",
|
| 683 |
+
"ret",
|
| 684 |
+
);
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
/// Girder (type 4 subtype 3) — pure Rust port of 0x51E350.
|
| 688 |
///
|
| 689 |
/// Allocates a CTaskGirder (0xA4 bytes), copies 7 DWORDs from fire_params
|