paavohtl Claude Opus 4.6 (1M context) commited on
Commit
daab4e1
Β·
1 Parent(s): d575149

feat: port SpawnEffect (0x547C30) to Rust and hook it

Browse files

Pure Rust implementation of SpawnEffect_Maybe: builds a 0x408-byte
message buffer from params, looks up CTaskSpriteAnim via SharedData
(entity type 0x1A), and sends HandleMessage(0x56).

Hooked with a naked usercall trampoline that captures EAX (constant),
ECX (speed_x), ESI (worm) + 7 stack params. Serves all 17 WA callers.
Also called directly from weapon_release_impl.

This eliminates the last naked asm bridge from weapon_release.rs β€”
the module now has zero bridge functions.

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

crates/openwa-wormkit/src/replacements/weapon_release.rs CHANGED
@@ -10,9 +10,8 @@ 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::rebase::rb;
14
  use openwa_core::task::worm::{CTaskWorm, WormState};
15
- use openwa_core::task::{CGameTask, Task};
16
 
17
  use crate::hook::{self, usercall_trampoline};
18
  use crate::replacements::{sound, weapon};
@@ -138,8 +137,8 @@ unsafe extern "cdecl" fn weapon_release_impl(
138
  }
139
 
140
  // ── 2. Populate context fields ──────────────────────────
141
- let speed_x = w.base.pos_x.0;
142
- let speed_y = w.base.pos_y.0;
143
  ctx.team_id = w.team_index;
144
  ctx.worm_id = w.worm_index;
145
  ctx.ammo_per_turn = w.weapon_param_1 as u32;
@@ -388,9 +387,9 @@ unsafe extern "cdecl" fn weapon_release_impl(
388
  let facing_flag: u32 = if facing < 1 { 0x40000 } else { 0 };
389
  let state_flag = facing_flag + effect_state;
390
 
391
- call_spawn_effect_full(
392
- worm, speed_x, speed_y, rng_scaled, rng1_offset, palette, state_flag, 0xA0000,
393
- 0x1999, rb(va::SPAWN_EFFECT),
394
  );
395
  }
396
 
@@ -411,43 +410,94 @@ fn write_u32(buf: &mut [u8], offset: usize, value: u32) {
411
  buf[offset..offset + 4].copy_from_slice(&value.to_ne_bytes());
412
  }
413
 
414
- // ── Bridge functions ────────────────────────────────────────
415
 
416
- /// SpawnEffect (0x547C30): usercall(EAX=0x80000, ECX=speed_x, ESI=worm) + 7 stack, RET 0x1C.
417
- #[unsafe(naked)]
418
- unsafe extern "C" fn call_spawn_effect_full(
419
- _worm: *mut CTaskWorm,
420
- _speed_x: i32,
421
- _speed_y: i32,
422
- _rng_scaled: i32,
423
- _rng_offset: i32,
424
- _palette: u32,
425
- _state_flag: u32,
426
- _size: u32,
427
- _scale: u32,
428
- _addr: u32,
 
 
 
 
 
429
  ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  core::arch::naked_asm!(
431
- "push esi",
432
  "push ebx",
433
- // Stack: 2 saves(8) + ret(4) = 12 to first arg
434
- // Args: +12=worm, +16=speed_x, +20=speed_y, +24=rng_scaled, +28=rng_offset,
435
- // +32=palette, +36=state_flag, +40=size, +44=scale, +48=addr
436
- "mov esi, [esp+12]", // worm β†’ ESI
437
- "mov ecx, [esp+16]", // speed_x β†’ ECX
438
- "mov ebx, [esp+48]", // addr
439
- "push [esp+44]", // scale
440
- "push [esp+44]", // size
441
- "push [esp+44]", // state_flag
442
- "push [esp+44]", // palette
443
- "push [esp+44]", // rng_offset
444
- "push [esp+44]", // rng_scaled
445
- "push [esp+44]", // speed_y (first stack param)
446
- "mov eax, 0x80000",
447
- "call ebx",
 
 
 
 
 
448
  "pop ebx",
449
- "pop esi",
450
- "ret",
451
  );
452
  }
453
 
@@ -455,6 +505,12 @@ unsafe extern "C" fn call_spawn_effect_full(
455
 
456
  pub fn install() -> Result<(), String> {
457
  unsafe {
 
 
 
 
 
 
458
  hook::install(
459
  "WeaponRelease",
460
  va::WEAPON_RELEASE,
 
10
  use openwa_core::fixed::Fixed;
11
  use openwa_core::game::Weapon;
12
  use openwa_core::log::log_line;
 
13
  use openwa_core::task::worm::{CTaskWorm, WormState};
14
+ use openwa_core::task::{CGameTask, SharedDataTable, Task};
15
 
16
  use crate::hook::{self, usercall_trampoline};
17
  use crate::replacements::{sound, weapon};
 
137
  }
138
 
139
  // ── 2. Populate context fields ──────────────────────────
140
+ let speed_x = w.base.pos_x;
141
+ let speed_y = w.base.pos_y;
142
  ctx.team_id = w.team_index;
143
  ctx.worm_id = w.worm_index;
144
  ctx.ammo_per_turn = w.weapon_param_1 as u32;
 
387
  let facing_flag: u32 = if facing < 1 { 0x40000 } else { 0 };
388
  let state_flag = facing_flag + effect_state;
389
 
390
+ spawn_effect(
391
+ worm, 0x80000, speed_x, speed_y, rng_scaled, rng1_offset, palette, state_flag,
392
+ Fixed(0xA0000), Fixed(0x1999),
393
  );
394
  }
395
 
 
410
  buf[offset..offset + 4].copy_from_slice(&value.to_ne_bytes());
411
  }
412
 
413
+ // ── SpawnEffect (0x547C30) β€” hooked, also called directly ───
414
 
415
+ /// Spawn a visual effect on the sprite anim entity. Pure Rust port of FUN_00547C30.
416
+ ///
417
+ /// Builds a 0x408-byte message buffer from the params, looks up CTaskSpriteAnim
418
+ /// via SharedData (entity type 0x1A), and sends HandleMessage(0x56).
419
+ ///
420
+ /// Called directly from `weapon_release_impl` and via the hook trampoline
421
+ /// for all other WA callers (17 call sites).
422
+ pub(crate) unsafe fn spawn_effect(
423
+ worm: *mut CTaskWorm,
424
+ constant: u32,
425
+ speed_x: Fixed,
426
+ speed_y: Fixed,
427
+ rng_scaled: i32,
428
+ rng_offset: i32,
429
+ palette: u32,
430
+ state_flag: u32,
431
+ size: Fixed,
432
+ scale: Fixed,
433
  ) {
434
+ // Build the message buffer matching the original stack layout.
435
+ // The original pushes ESI (worm ptr) then uses LEA to include it at offset 0.
436
+ let mut buf = [0u8; 0x408];
437
+ write_u32(&mut buf, 0x00, worm as u32);
438
+ write_u32(&mut buf, 0x04, constant);
439
+ write_u32(&mut buf, 0x08, speed_x.0 as u32);
440
+ write_u32(&mut buf, 0x0C, speed_y.0 as u32);
441
+ write_u32(&mut buf, 0x10, rng_scaled as u32);
442
+ write_u32(&mut buf, 0x14, rng_offset as u32);
443
+ // [0x18] = 0 (already zeroed)
444
+ write_u32(&mut buf, 0x1C, palette);
445
+ write_u32(&mut buf, 0x20, state_flag);
446
+ // [0x24] = 0 (already zeroed)
447
+ write_u32(&mut buf, 0x28, size.0 as u32);
448
+ write_u32(&mut buf, 0x2C, scale.0 as u32);
449
+
450
+ // SharedData lookup for entity type 0x1A (CTaskSpriteAnim)
451
+ let table = SharedDataTable::from_task((*worm).as_task_ptr());
452
+ let entity = table.lookup(0, 0x1A);
453
+ if !entity.is_null() {
454
+ let vtable = *(entity as *const *const usize);
455
+ let handle_msg: unsafe extern "thiscall" fn(
456
+ *mut u8, *mut u8, u32, u32, *const u8,
457
+ ) = core::mem::transmute(*vtable.add(2));
458
+ handle_msg(
459
+ entity as *mut u8,
460
+ worm as *mut u8,
461
+ 0x56,
462
+ 0x408,
463
+ buf.as_ptr(),
464
+ );
465
+ }
466
+ }
467
+
468
+ /// Naked trampoline for SpawnEffect (0x547C30).
469
+ ///
470
+ /// Original convention: usercall(EAX=constant, ECX=speed_x, ESI=worm)
471
+ /// + 7 stack params (speed_y, rng_scaled, rng_offset, palette, state_flag, size, scale)
472
+ /// RET 0x1C.
473
+ #[unsafe(naked)]
474
+ unsafe extern "C" fn trampoline_spawn_effect() {
475
  core::arch::naked_asm!(
476
+ // Save callee-saved registers
477
  "push ebx",
478
+ "push ebp",
479
+ "push edi",
480
+ // ESI = worm, EAX = constant, ECX = speed_x
481
+ // Stack after 3 saves(12) + ret(4) = 16; original stack params start at +16
482
+ // Push cdecl args in reverse: scale, size, state_flag, palette, rng_offset,
483
+ // rng_scaled, speed_y, 0(rng_offset placeholder), speed_x(ECX), constant(EAX), worm(ESI)
484
+ "push [esp+40]", // scale (16+24=40)
485
+ "push [esp+40]", // size
486
+ "push [esp+40]", // state_flag
487
+ "push [esp+40]", // palette
488
+ "push [esp+40]", // rng_offset
489
+ "push [esp+40]", // rng_scaled
490
+ "push [esp+40]", // speed_y
491
+ "push ecx", // speed_x (register param)
492
+ "push eax", // constant (register param)
493
+ "push esi", // worm (register param)
494
+ "call {impl_fn}",
495
+ "add esp, 40", // clean 10 cdecl args
496
+ "pop edi",
497
+ "pop ebp",
498
  "pop ebx",
499
+ "ret 0x1C", // clean 7 original stack params
500
+ impl_fn = sym spawn_effect,
501
  );
502
  }
503
 
 
505
 
506
  pub fn install() -> Result<(), String> {
507
  unsafe {
508
+ hook::install(
509
+ "SpawnEffect",
510
+ va::SPAWN_EFFECT,
511
+ trampoline_spawn_effect as *const (),
512
+ )?;
513
+
514
  hook::install(
515
  "WeaponRelease",
516
  va::WEAPON_RELEASE,