paavohtl Claude Opus 4.6 (1M context) commited on
Commit
2af8d3b
·
1 Parent(s): 35a783f

feat: port Jet Pack weapon to pure Rust

Browse files

Port 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 (EAX=entry)
405
- 22 => call_fire_usercall(entry as *const (), worm, rb(0x51EC30)),
406
  // Magic Bullet
407
  23 => (*worm).set_state(WormState::WeaponAimed_Maybe),
408
- // Low Gravity (EAX=entry)
 
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