paavohtl Claude Opus 4.6 (1M context) commited on
Commit
0572750
·
1 Parent(s): e1d5d02

feat: port weapon fire sub-functions to Rust — Drill, Prod, vtable calls

Browse files

Replace naked asm bridges with typed Rust implementations:
- fire_worm_vtable_0xe → (*worm).set_state() for 8 weapons (9 call sites)
- Teleport check (0x516930) → pure Rust can_teleport() helper
- Drill fire (0x51E3E0) → pure Rust fire_drill(), fixes latent ECX bug
where bridge didn't set ECX=local_struct
- Prod fire (0x51E480) → pure Rust fire_prod() with trig interpolation;
dy must be negated (original asm uses IMUL -0x66666667)
- Add typed SpecialImpact wrapper (stdcall, 13 params)
- Delete 3 naked asm bridges: fire_worm_vtable_0xe,
call_fire_usercall_ret, call_fire_usercall_stdcall1

Add 3 replay tests: drill_prod_rope (Drill×4, Prod×3),
kamikaze (Kamikaze, Suicide Bomber),
various_weapons_1 (Bat, Blowtorch, Scales×3, Dragon Ball×2, more)

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

crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -357,40 +357,66 @@ unsafe fn fire_weapon_special(
357
  let params_38_ptr = &raw const (*entry).fire_subtype_38 as *const WeaponFireParams;
358
 
359
  match subtype {
360
- 1 => fire_worm_vtable_0xe(worm, 0x6C), // Blowtorch
361
- 2 => call_fire_usercall(entry as *const (), worm, rb(0x51E3E0)), // Drill (EAX=entry)
362
- 3 => call_fire_stdcall3(worm, params_38_ptr, local_struct, rb(0x51E350)), // Girder
363
- 4 => fire_worm_vtable_0xe(worm, 0x6D), // Baseball Bat
364
- 5 => fire_worm_vtable_0xe(worm, 0x75), // Fire Punch
365
- 6 => fire_worm_vtable_0xe(worm, 0x70), // Dragon Ball
366
- 8 => fire_worm_vtable_0xe(worm, 0x6E), // Kamikaze
367
- 9 => call_fire_usercall_stdcall1(entry as *const (), worm, local_struct, rb(0x51E480)), // Prod (EAX=entry)
368
- 10 => call_fire_usercall(worm as *const (), worm, rb(0x51E710)), // Air Strike (EAX=worm)
369
- 11 => fire_worm_vtable_0xe(worm, 0x71), // Scales of Justice
370
- 13 => fire_send_team_message(worm, 0x2B), // Napalm Strike
371
- 14 => fire_mail_mine_mole(worm), // Mail/Mine/Mole (pure Rust)
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  16 => {
373
- // Teleport: MOV EAX,[ESI+0x44] (worm state) before check
374
  let worm_state = (*worm).state();
375
- let result = call_fire_usercall_ret(worm_state as *const (), worm, rb(0x516930));
376
- if result != 0 {
377
- call_fire_usercall(result as *const (), worm, rb(0x51EB00));
378
  } else {
379
- fire_worm_vtable_0xe(worm, 0x74);
380
  }
381
  }
382
- 17 => call_fire_usercall(worm as *const (), worm, rb(0x51E920)), // Freeze (EAX=worm)
383
- 18 => fire_worm_vtable_0xe(worm, 0x72), // Suicide Bomber
384
- 19 => fire_skip_go(worm, entry), // Skip Go (pure Rust)
385
- 20 => fire_surrender(worm), // Surrender (pure Rust)
386
- 21 => fire_select_worm(worm), // Select Worm (pure Rust)
387
- 22 => call_fire_usercall(entry as *const (), worm, rb(0x51EC30)), // Jet Pack (EAX=entry)
388
- 23 => fire_worm_vtable_0xe(worm, 0x78), // Magic Bullet
389
- 24 => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)), // Low Grav (EAX=entry)
 
 
 
 
 
 
 
 
390
  _ => {}
391
  }
392
  }
393
 
 
 
 
 
 
 
 
394
  // ── Pure Rust fire handlers (no bridge needed) ──────────────
395
 
396
  /// Look up the CTaskTeam entity for a worm via SharedData.
@@ -515,7 +541,7 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
515
  && (worm_state == 0x7D || (worm_state == 0x78 && version < 8)));
516
 
517
  if should_call_vtable {
518
- fire_worm_vtable_0xe(worm, 0x65);
519
  }
520
 
521
  fire_send_team_message(worm, 0x28);
@@ -527,6 +553,130 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
527
  entry.turn_action_counter_Maybe += 7;
528
  }
529
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  // ── Naked asm bridges ───────────────────────────────────────
531
 
532
  /// Bridge: ProjectileFire_Single — usercall(EDI=spawn_data, stack=[worm, fire_params]), RET 0x8.
@@ -596,69 +746,6 @@ unsafe extern "C" fn call_fire_usercall(_eax: *const (), _worm: *mut CTaskWorm,
596
  );
597
  }
598
 
599
- /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET, returns EAX.
600
- #[unsafe(naked)]
601
- unsafe extern "C" fn call_fire_usercall_ret(_eax: *const (), _worm: *mut CTaskWorm, _addr: u32) -> i32 {
602
- core::arch::naked_asm!(
603
- "push ebx",
604
- "push esi",
605
- "push edi",
606
- "mov eax, [esp+16]",
607
- "mov esi, [esp+20]",
608
- "mov edi, [esp+20]",
609
- "mov ebx, [esp+24]",
610
- "call ebx",
611
- "pop edi",
612
- "pop esi",
613
- "pop ebx",
614
- "ret",
615
- );
616
- }
617
-
618
- /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm) + stdcall(1 param), RET 0x4.
619
- #[unsafe(naked)]
620
- unsafe extern "C" fn call_fire_usercall_stdcall1(
621
- _eax: *const (), _worm: *mut CTaskWorm, _param: *const u8, _addr: u32,
622
- ) {
623
- core::arch::naked_asm!(
624
- "push ebx",
625
- "push esi",
626
- "push edi",
627
- "mov eax, [esp+16]", // eax_val
628
- "mov esi, [esp+20]", // worm
629
- "mov edi, [esp+20]",
630
- "mov ebx, [esp+28]", // addr (3 saves=12 + ret=4 + 3 args=12 = 28)
631
- "push [esp+24]", // param
632
- "call ebx",
633
- "pop edi",
634
- "pop esi",
635
- "pop ebx",
636
- "ret",
637
- );
638
- }
639
-
640
- /// Call worm->vtable[0xE](msg_id) — thiscall(ECX=worm, stack=msg_id), RET 0x4.
641
- /// Original: `(**(code **)(*worm + 0x38))(msg_id)`.
642
- /// Uses naked bridge to avoid LLVM stack tracking issues with push/RET mismatch.
643
- #[unsafe(naked)]
644
- unsafe extern "C" fn fire_worm_vtable_0xe(_worm: *mut CTaskWorm, _msg_id: u32) {
645
- core::arch::naked_asm!(
646
- "push ebx",
647
- "push esi",
648
- "push edi",
649
- "mov ecx, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
650
- "mov esi, [esp+16]", // ESI = worm
651
- "mov edi, [esp+16]", // EDI = worm
652
- "mov ebx, [ecx]", // vtable
653
- "mov ebx, [ebx+0x38]",// vtable[0xE]
654
- "push [esp+20]", // msg_id
655
- "call ebx", // thiscall: ECX=worm, RET 0x4 cleans msg_id
656
- "pop edi",
657
- "pop esi",
658
- "pop ebx",
659
- "ret",
660
- );
661
- }
662
 
663
  // ============================================================
664
  // Passthrough hooks — fire sub-functions
 
357
  let params_38_ptr = &raw const (*entry).fire_subtype_38 as *const WeaponFireParams;
358
 
359
  match subtype {
360
+ // Blowtorch
361
+ 1 => (*worm).set_state(0x6C),
362
+ // Pneumatic Drill (pure Rust)
363
+ 2 => fire_drill(worm, local_struct),
364
+ // Girder
365
+ 3 => call_fire_stdcall3(worm, params_38_ptr, local_struct, rb(0x51E350)),
366
+ // Baseball Bat
367
+ 4 => (*worm).set_state(0x6D),
368
+ // Fire Punch
369
+ 5 => (*worm).set_state(0x75),
370
+ // Dragon Ball
371
+ 6 => (*worm).set_state(0x70),
372
+ // Kamikaze
373
+ 8 => (*worm).set_state(0x6E),
374
+ // Prod (pure Rust)
375
+ 9 => fire_prod(worm, local_struct),
376
+ // Air Strike (EAX=worm)
377
+ 10 => call_fire_usercall(worm as *const (), worm, rb(0x51E710)),
378
+ // Scales of Justice
379
+ 11 => (*worm).set_state(0x71),
380
+ // Napalm Strike
381
+ 13 => fire_send_team_message(worm, 0x2B),
382
+ // Mail/Mine/Mole (pure Rust)
383
+ 14 => fire_mail_mine_mole(worm),
384
+ // Teleport: check worm state, then execute or cancel
385
  16 => {
 
386
  let worm_state = (*worm).state();
387
+ if can_teleport(worm_state) {
388
+ call_fire_usercall(worm_state as *const (), worm, rb(0x51EB00));
 
389
  } else {
390
+ (*worm).set_state(0x74);
391
  }
392
  }
393
+ // Freeze (EAX=worm)
394
+ 17 => call_fire_usercall(worm as *const (), worm, rb(0x51E920)),
395
+ // Suicide Bomber
396
+ 18 => (*worm).set_state(0x72),
397
+ // Skip Go (pure Rust)
398
+ 19 => fire_skip_go(worm, entry),
399
+ // Surrender (pure Rust)
400
+ 20 => fire_surrender(worm),
401
+ // Select Worm (pure Rust)
402
+ 21 => fire_select_worm(worm),
403
+ // Jet Pack (EAX=entry)
404
+ 22 => call_fire_usercall(entry as *const (), worm, rb(0x51EC30)),
405
+ // Magic Bullet
406
+ 23 => (*worm).set_state(0x78),
407
+ // Low Gravity (EAX=entry)
408
+ 24 => call_fire_usercall(entry as *const (), worm, rb(0x51EA60)),
409
  _ => {}
410
  }
411
  }
412
 
413
+ /// Teleport validity check — pure Rust port of 0x516930.
414
+ /// Returns true if the worm's current state allows teleportation.
415
+ /// Valid states: 0x78 (idle-aim), 0x7B-0x7D (pre-fire range).
416
+ fn can_teleport(state: u32) -> bool {
417
+ state == 0x78 || (0x7B..=0x7D).contains(&state)
418
+ }
419
+
420
  // ── Pure Rust fire handlers (no bridge needed) ──────────────
421
 
422
  /// Look up the CTaskTeam entity for a worm via SharedData.
 
541
  && (worm_state == 0x7D || (worm_state == 0x78 && version < 8)));
542
 
543
  if should_call_vtable {
544
+ (*worm).set_state(0x65);
545
  }
546
 
547
  fire_send_team_message(worm, 0x28);
 
553
  entry.turn_action_counter_Maybe += 7;
554
  }
555
 
556
+ // ── SpecialImpact wrapper ─────────────────────────────────
557
+
558
+ /// Typed wrapper for SpecialImpact (0x5193D0).
559
+ /// Convention: stdcall with 13 params (RET 0x34 = 52 bytes).
560
+ ///
561
+ /// Used by Drill, Prod, and other melee/impact weapons. Applies damage in
562
+ /// a directional area from the worm's position.
563
+ #[allow(clippy::too_many_arguments)]
564
+ unsafe fn call_special_impact(
565
+ worm: *mut CTaskWorm,
566
+ x: i32, y: i32,
567
+ radius_x: i32, radius_y: i32,
568
+ weapon_type: i32,
569
+ dx: i32, dy: i32,
570
+ p8: i32, p9: i32, p10: i32,
571
+ flags: u32, p12: i32,
572
+ ) {
573
+ use openwa_core::rebase::rb;
574
+ type SpecialImpactFn = unsafe extern "stdcall" fn(
575
+ *mut CTaskWorm, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, u32, i32,
576
+ );
577
+ let func: SpecialImpactFn = core::mem::transmute(rb(va::SPECIAL_IMPACT));
578
+ func(worm, x, y, radius_x, radius_y, weapon_type, dx, dy, p8, p9, p10, flags, p12);
579
+ }
580
+
581
+ /// Compute version-dependent flags for SpecialImpact.
582
+ ///
583
+ /// Pattern shared by Drill and Prod:
584
+ /// - Base flags OR 0x20 if version >= 2
585
+ /// - Base flags OR 0x10 if version >= 8
586
+ fn special_impact_version_flags(base: u32, version: u8) -> u32 {
587
+ let mut flags = base;
588
+ if version >= 2 { flags |= 0x20; }
589
+ if version >= 8 { flags |= 0x10; }
590
+ flags
591
+ }
592
+
593
+ /// Pneumatic Drill (subtype 2) — pure Rust port of 0x51E3E0.
594
+ ///
595
+ /// Calls SpecialImpact with facing-offset position and scaled direction.
596
+ /// The original is usercall(ECX=local_struct, ESI=worm) — the old bridge
597
+ /// did not set ECX, so this port also fixes a latent bug.
598
+ unsafe fn fire_drill(worm: *mut CTaskWorm, local_struct: *const u8) {
599
+ let ddgame = (*worm).ddgame();
600
+ let version = (*ddgame).version_flag_4;
601
+ let entry = &*(*worm).active_weapon_entry;
602
+ let shot_count = entry.fire_params.shot_count;
603
+ let weapon_type = entry.fire_subtype_38;
604
+ let facing = (*worm).facing_direction_2;
605
+
606
+ // Cast local_struct to WeaponSpawnData for field access (offsets match)
607
+ let spawn = &*(local_struct as *const WeaponSpawnData);
608
+
609
+ let x = facing * 0x1A_0000 + spawn.spawn_x.0;
610
+ let y = spawn.spawn_y.0;
611
+ let dx = (spawn.initial_speed_x.0 * shot_count) / 10;
612
+ let dy = (spawn.initial_speed_y.0 * shot_count) / 10;
613
+ let flags = special_impact_version_flags(0x21C4C, version);
614
+
615
+ call_special_impact(
616
+ worm, x, y,
617
+ 0x1A_0000, 0x1E_0000,
618
+ weapon_type, dx, dy,
619
+ 6, 0x61, 0x51,
620
+ flags, 1,
621
+ );
622
+ }
623
+
624
+ /// Prod (subtype 9) — pure Rust port of 0x51E480.
625
+ ///
626
+ /// Like Drill but with trig interpolation on the spread angle.
627
+ /// Convention: usercall(EDI=worm) + 1 stack param (local_struct), RET 0x4.
628
+ unsafe fn fire_prod(worm: *mut CTaskWorm, local_struct: *const u8) {
629
+ use openwa_core::rebase::rb;
630
+
631
+ let ddgame = (*worm).ddgame();
632
+ let version = (*ddgame).version_flag_4;
633
+ let entry = &*(*worm).active_weapon_entry;
634
+ let shot_count = entry.fire_params.shot_count;
635
+ let spread = entry.fire_params.spread;
636
+ let weapon_type = entry.fire_subtype_38;
637
+ let facing = (*worm).facing_direction_2;
638
+
639
+ let spawn = &*(local_struct as *const WeaponSpawnData);
640
+
641
+ // Convert spread (degrees) to engine angle units: (spread << 16) / 360
642
+ let angle = ((spread as u32) << 16) / 0x168;
643
+
644
+ // Interpolated sin/cos lookup (same pattern as projectile_fire_impl)
645
+ let sin_table = rb(va::SIN_TABLE) as *const i32;
646
+ let cos_table = sin_table.add(256);
647
+
648
+ let table_index = ((angle >> 6) & 0x3FF) as usize;
649
+ let frac = ((angle & 0x3F) << 10) as i32;
650
+
651
+ let cos_base = *cos_table.add(table_index);
652
+ let cos_next = *cos_table.add(table_index + 1);
653
+ let cos_val = cos_base + fixed_mul(cos_next - cos_base, frac);
654
+
655
+ let sin_base = *sin_table.add(table_index);
656
+ let sin_next = *sin_table.add(table_index + 1);
657
+ let sin_val = sin_base + fixed_mul(sin_next - sin_base, frac);
658
+
659
+ // Scale trig results by shot_count and divide by 10.
660
+ // In WA's coordinate system (Y increases downward), the vertical component is negated.
661
+ // Original asm uses IMUL 0x99999999 (= -0x66666667) for dy, which negates the result.
662
+ // dx = (sin_val * shot_count) / 10 * facing
663
+ // dy = -(cos_val * shot_count) / 10
664
+ let dx = ((sin_val * shot_count) / 10) * facing;
665
+ let dy = -((cos_val * shot_count) / 10);
666
+
667
+ let x = facing * 0x6_0000 + spawn.spawn_x.0;
668
+ let y = spawn.spawn_y.0;
669
+ let flags = special_impact_version_flags(0xC4C, version);
670
+
671
+ call_special_impact(
672
+ worm, x, y,
673
+ 0xC_0000, 0xC_0000,
674
+ weapon_type, dx, dy,
675
+ 0, 0, 0,
676
+ flags, 1,
677
+ );
678
+ }
679
+
680
  // ── Naked asm bridges ───────────────────────────────────────
681
 
682
  /// Bridge: ProjectileFire_Single — usercall(EDI=spawn_data, stack=[worm, fire_params]), RET 0x8.
 
746
  );
747
  }
748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
  // ============================================================
751
  // Passthrough hooks — fire sub-functions
testdata/replays/drill_prod_rope.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33aa87bf1ab7bb954ae21d5dc178cbb2a0196712fe5dd090b066b7a8b23f2a1b
3
+ size 20440
testdata/replays/drill_prod_rope_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0bf673c8d5273a8d0e67b930674e56cf16a8e06933fa7c1e642057b2d1cb0eae
3
+ size 2320
testdata/replays/kamikaze.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:79f91bddf709c993f764eb01c505a36939b39f2d4d5eeadd844ff784bb394587
3
+ size 9613
testdata/replays/kamikaze_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:99ed67cd8537e6c40f33a043f2fc56f7fd7081dfb685eb77abb881a6eea89201
3
+ size 1105
testdata/replays/various_weapons_1.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:10cdf52df18cd2eaab7ade52aac50676eec14be7709aaa06213f6c90f4c8bbf5
3
+ size 21775
testdata/replays/various_weapons_1_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0db0acb7d5aff0df01e8dbbacd283de94393515f76342e546acd8ca6ae24d9a7
3
+ size 2822