paavohtl Claude Opus 4.6 (1M context) commited on
Commit
89c4a78
·
1 Parent(s): d7f1232

feat: replace FireWeapon dispatch with Rust implementation

Browse files

Full Rust replacement for FireWeapon (0x51EE60) — the weapon firing
dispatcher that routes 4 weapon types × subtypes to 20+ handlers.

Key discovery: EAX at entry = *(CTaskWorm+0x36C) = worm self-pointer,
so weapon_ctx == worm for all dispatch paths. All sub-functions use
EAX=ESI=EDI=worm as their implicit register context.

Implementation:
- Naked trampoline captures EAX/ECX/stack, saves callee-saved regs
- Type 1: PlacedExplosive, Projectile, CreateWeaponProjectile, Shotgun
- Type 2: RopeType1, CreateWeaponProjectile, RopeType3
- Type 3: GrenadeMortar
- Type 4: 24 special weapon subtypes (vtable calls + usercall bridges)
- All bridges save/restore ESI/EDI/EBX and set ESI=EDI=worm

Verified: headless PASS (byte-identical), headful 133+3 / 0 FAIL.

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

crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -128,57 +128,327 @@ usercall_trampoline!(fn trampoline_count_alive_worms; impl_fn = count_alive_worm
128
  regs = [eax, ecx]);
129
 
130
  // ============================================================
131
- // FireWeapon passthrough (0x51EE60)
132
  // ============================================================
133
- // usercall(EAX=weapon_ctx) + 1 stack param (wrapper), RET 0x4.
134
- // weapon_ctx+0x30 = weapon type (1-4)
135
- // weapon_ctx+0x34 = subtype for types 3,4
136
- // weapon_ctx+0x38 = subtype for types 1,2
 
 
 
 
 
 
 
 
 
 
137
 
138
  static ORIG_FIRE_WEAPON: AtomicU32 = AtomicU32::new(0);
139
 
140
- /// Logger called from the naked passthrough.
141
- /// `weapon_ctx` = EAX = pointer to weapon data (from CTaskWorm+0x36C).
142
- /// `worm_ptr` = stack param = CTaskWorm pointer (pushed by WeaponRelease).
143
- unsafe extern "cdecl" fn fire_weapon_log(weapon_ctx: u32, worm_ptr: u32) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  let ctx = weapon_ctx as *const u8;
145
  let weapon_type = *(ctx.add(0x30) as *const i32);
146
  let subtype_34 = *(ctx.add(0x34) as *const i32);
147
  let subtype_38 = *(ctx.add(0x38) as *const i32);
 
 
148
 
149
- // Read selected weapon from CTaskWorm+0x170
150
- let weapon_id = *((worm_ptr as *const u8).add(0x170) as *const u32);
151
  let weapon_name = Weapon::try_from(weapon_id)
152
  .map(|w| format!("{:?}", w))
153
  .unwrap_or_else(|id| format!("Unknown({})", id));
154
-
155
  let _ = log_line(&format!(
156
  "[Weapon] FireWeapon: {} (id={}) type={} sub34={} sub38={}",
157
  weapon_name, weapon_id, weapon_type, subtype_34, subtype_38
158
  ));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
 
161
- /// Naked passthrough: save regs → call logger → restore → jmp original.
162
- /// Stack layout at entry: [ret_addr] [wrapper_param]
163
- /// We read wrapper_param to pass to the logger as the worm pointer.
 
 
 
 
 
 
 
 
 
 
 
 
164
  #[unsafe(naked)]
165
- unsafe extern "C" fn trampoline_fire_weapon() {
166
  core::arch::naked_asm!(
167
- "push eax",
168
- "push ecx",
169
- "push edx",
170
- // call logger(weapon_ctx=EAX, worm_ptr=[ESP+16])
171
- // ESP+16 because: 3 pushes (12 bytes) + ret_addr (4) = 16 to stack param
172
- "push [esp+16]",
173
- "push eax",
174
- "call {log_fn}",
175
- "add esp, 8",
176
- "pop edx",
177
- "pop ecx",
178
- "pop eax",
179
- "jmp [{orig}]",
180
- log_fn = sym fire_weapon_log,
181
- orig = sym ORIG_FIRE_WEAPON,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  );
183
  }
184
 
 
128
  regs = [eax, ecx]);
129
 
130
  // ============================================================
131
+ // FireWeapon replacement (0x51EE60)
132
  // ============================================================
133
+ // Convention: usercall(EAX=worm, ECX=local_struct) + 1 stack(worm), RET 0x4.
134
+ // Note: EAX = *(CTaskWorm+0x36C) = worm self-pointer, so EAX == stack param.
135
+ //
136
+ // Weapon launch data offsets (relative to worm/EAX):
137
+ // +0x30 = weapon type (1-4)
138
+ // +0x34 = subtype for types 3,4
139
+ // +0x38 = subtype for types 1,2
140
+ // +0x3C = params base
141
+ //
142
+ // worm = CTaskWorm pointer (ESI in original).
143
+ // local_struct = stack-local buffer from WeaponRelease (ECX at call site).
144
+ //
145
+ // Sub-functions are usercall: ESI=worm, ECX=local_struct (for some).
146
+ // We capture all three in our naked trampoline.
147
 
148
  static ORIG_FIRE_WEAPON: AtomicU32 = AtomicU32::new(0);
149
 
150
+ /// Naked trampoline for FireWeapon.
151
+ /// Must save ALL callee-saved registers (ESI, EDI, EBX, EBP) because the
152
+ /// Rust cdecl impl may clobber them, and WeaponRelease (our caller) depends
153
+ /// on them being preserved.
154
+ #[unsafe(naked)]
155
+ unsafe extern "C" fn trampoline_fire_weapon() {
156
+ core::arch::naked_asm!(
157
+ // Save all callee-saved + EDX
158
+ "push ebx",
159
+ "push esi",
160
+ "push edi",
161
+ "push ebp",
162
+ "push edx",
163
+ // Push cdecl args: (weapon_ctx=EAX, local_struct=ECX, worm)
164
+ // Stack: 5 pushes (20) + ret (4) = 24 to stack param
165
+ "push [esp+24]", // worm
166
+ "push ecx", // local_struct
167
+ "push eax", // weapon_ctx
168
+ "call {impl_fn}",
169
+ "add esp, 12",
170
+ // Restore everything
171
+ "pop edx",
172
+ "pop ebp",
173
+ "pop edi",
174
+ "pop esi",
175
+ "pop ebx",
176
+ "ret 0x4",
177
+ impl_fn = sym fire_weapon_impl,
178
+ );
179
+ }
180
+
181
+ /// Rust implementation of FireWeapon dispatch.
182
+ unsafe extern "cdecl" fn fire_weapon_impl(weapon_ctx: u32, local_struct: u32, worm: u32) {
183
  let ctx = weapon_ctx as *const u8;
184
  let weapon_type = *(ctx.add(0x30) as *const i32);
185
  let subtype_34 = *(ctx.add(0x34) as *const i32);
186
  let subtype_38 = *(ctx.add(0x38) as *const i32);
187
+ let params = weapon_ctx.wrapping_add(0x3C);
188
+ let worm_ptr = worm as *mut u8;
189
 
190
+ // Log weapon fire
191
+ let weapon_id = *(worm_ptr.add(0x170) as *const u32);
192
  let weapon_name = Weapon::try_from(weapon_id)
193
  .map(|w| format!("{:?}", w))
194
  .unwrap_or_else(|id| format!("Unknown({})", id));
 
195
  let _ = log_line(&format!(
196
  "[Weapon] FireWeapon: {} (id={}) type={} sub34={} sub38={}",
197
  weapon_name, weapon_id, weapon_type, subtype_34, subtype_38
198
  ));
199
+
200
+ // worm+0x3C = completion flag
201
+ *(worm_ptr.add(0x3C) as *mut i32) = 0;
202
+
203
+ use openwa_core::rebase::rb;
204
+
205
+ match weapon_type {
206
+ 1 => match subtype_38 {
207
+ 1 => call_fire_stdcall1(worm, params, rb(0x51EC80)), // PlacedExplosive
208
+ 2 => call_fire_stdcall3(worm, params, local_struct, rb(0x51DFB0)), // Projectile
209
+ 3 => call_fire_thiscall2(worm, params, local_struct, rb(0x51E0F0)), // CreateWeaponProjectile
210
+ 4 => call_fire_stdcall2(worm, params, local_struct, rb(0x51ED90)), // Shotgun
211
+ _ => {}
212
+ },
213
+ 2 => match subtype_38 {
214
+ 1 => call_fire_stdcall3(worm, params, local_struct, rb(0x51E1C0)), // RopeType1
215
+ 2 => call_fire_thiscall2(worm, params, local_struct, rb(0x51E0F0)), // CreateWeaponProjectile
216
+ 3 => call_fire_stdcall3(worm, params, local_struct, rb(0x51E240)), // RopeType3
217
+ _ => {}
218
+ },
219
+ 3 => {
220
+ let params_34 = weapon_ctx.wrapping_add(0x34);
221
+ call_fire_stdcall3(worm, params_34, local_struct, rb(0x51E2C0)); // GrenadeMortar
222
+ }
223
+ 4 => {
224
+ let params_38 = weapon_ctx.wrapping_add(0x38);
225
+ fire_weapon_special(subtype_34, params_38, worm, local_struct);
226
+ }
227
+ _ => {}
228
+ }
229
+
230
+ *(worm_ptr.add(0x3C) as *mut i32) = 1;
231
  }
232
 
233
+ // ── Sub-function bridges ────────────────────────────────────
234
+ // All preserve ESI=worm for usercall sub-functions.
235
+
236
+ // ── Sub-function bridges ────────────────────────────────────
237
+ // All bridges set ESI=worm AND EDI=worm before calling, since
238
+ // sub-functions are usercall and read both registers implicitly.
239
+ // Stack offsets are carefully calculated for each bridge.
240
+
241
+ // ── Sub-function bridges ────────────────────────────────────
242
+ // All bridges save/restore ESI+EDI, set ESI=EDI=worm, then call.
243
+ // This preserves LLVM's callee-saved registers while providing
244
+ // the usercall context that sub-functions expect.
245
+
246
+ /// Bridge: PlacedExplosive — stdcall(params), RET 0x4.
247
+ /// Our args: (worm, params, addr)
248
  #[unsafe(naked)]
249
+ unsafe extern "C" fn call_fire_stdcall1(_worm: u32, _params: u32, _addr: u32) {
250
  core::arch::naked_asm!(
251
+ "push ebx",
252
+ "push esi",
253
+ "push edi",
254
+ "mov esi, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
255
+ "mov edi, [esp+16]",
256
+ "mov ebx, [esp+24]", // addr
257
+ "push [esp+20]", // params
258
+ "call ebx",
259
+ "pop edi",
260
+ "pop esi",
261
+ "pop ebx",
262
+ "ret",
263
+ );
264
+ }
265
+
266
+ /// Bridge: Projectile/Rope/Grenade — stdcall(worm, params, local_struct), RET 0xC.
267
+ /// Our args: (worm, params, local_struct, addr)
268
+ #[unsafe(naked)]
269
+ unsafe extern "C" fn call_fire_stdcall3(_worm: u32, _params: u32, _local: u32, _addr: u32) {
270
+ core::arch::naked_asm!(
271
+ "push ebx",
272
+ "push esi",
273
+ "push edi",
274
+ "mov esi, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
275
+ "mov edi, [esp+16]",
276
+ "mov ebx, [esp+28]", // addr (saves=12 + ret=4 + 3 args=12 = 28)
277
+ "push [esp+24]", // local_struct
278
+ "push [esp+24]", // params (shifted +4)
279
+ "push [esp+24]", // worm (shifted +8)
280
+ "call ebx",
281
+ "pop edi",
282
+ "pop esi",
283
+ "pop ebx",
284
+ "ret",
285
+ );
286
+ }
287
+
288
+ /// Bridge: CreateWeaponProjectile — thiscall(ECX=worm, params, local_struct), RET 0x8.
289
+ /// Our args: (worm, params, local_struct, addr)
290
+ #[unsafe(naked)]
291
+ unsafe extern "C" fn call_fire_thiscall2(_worm: u32, _params: u32, _local: u32, _addr: u32) {
292
+ core::arch::naked_asm!(
293
+ "push ebx",
294
+ "push esi",
295
+ "push edi",
296
+ "mov esi, [esp+16]", // worm
297
+ "mov edi, [esp+16]",
298
+ "mov ecx, [esp+16]", // ECX = worm (this)
299
+ "mov ebx, [esp+28]", // addr
300
+ "push [esp+24]", // local_struct
301
+ "push [esp+24]", // params (shifted +4)
302
+ "call ebx",
303
+ "pop edi",
304
+ "pop esi",
305
+ "pop ebx",
306
+ "ret",
307
+ );
308
+ }
309
+
310
+ /// Bridge: Shotgun — stdcall(params, local_struct), RET 0x8.
311
+ /// Our args: (worm, params, local_struct, addr)
312
+ #[unsafe(naked)]
313
+ unsafe extern "C" fn call_fire_stdcall2(_worm: u32, _params: u32, _local: u32, _addr: u32) {
314
+ core::arch::naked_asm!(
315
+ "push ebx",
316
+ "push esi",
317
+ "push edi",
318
+ "mov esi, [esp+16]", // worm
319
+ "mov edi, [esp+16]",
320
+ "mov ebx, [esp+28]", // addr
321
+ "push [esp+24]", // local_struct
322
+ "push [esp+24]", // params (shifted +4)
323
+ "call ebx",
324
+ "pop edi",
325
+ "pop esi",
326
+ "pop ebx",
327
+ "ret",
328
+ );
329
+ }
330
+
331
+ /// Type 4 (special) weapon dispatch.
332
+ /// Type 4 (special) weapon dispatch. All sub-functions use EAX=ESI=EDI=worm.
333
+ unsafe fn fire_weapon_special(subtype: i32, params_38: u32, worm: u32, local_struct: u32) {
334
+ use openwa_core::rebase::rb;
335
+
336
+ // All handlers use EAX=worm (self-pointer at CTaskWorm+0x36C).
337
+ match subtype {
338
+ 1 => fire_worm_vtable_0xe(worm, 0x6C), // Blowtorch
339
+ 2 => call_fire_usercall(worm, worm, rb(0x51E3E0)), // Pneumatic Drill
340
+ 3 => call_fire_stdcall3(worm, params_38, local_struct, rb(0x51E350)), // Girder
341
+ 4 => fire_worm_vtable_0xe(worm, 0x6D), // Baseball Bat
342
+ 5 => fire_worm_vtable_0xe(worm, 0x75), // Fire Punch
343
+ 6 => fire_worm_vtable_0xe(worm, 0x70), // Dragon Ball
344
+ 8 => fire_worm_vtable_0xe(worm, 0x6E), // Kamikaze
345
+ 9 => call_fire_usercall_stdcall1(worm, worm, local_struct, rb(0x51E480)), // Prod
346
+ 10 => call_fire_usercall(worm, worm, rb(0x51E710)), // Air Strike (EAX=worm)
347
+ 11 => fire_worm_vtable_0xe(worm, 0x71), // Scales of Justice
348
+ 13 => call_fire_usercall(worm, worm, rb(0x51E5C0)), // Napalm Strike
349
+ 14 => call_fire_usercall(worm, worm, rb(0x51E670)), // Mail/Mine/Mole (EAX=worm!)
350
+ 16 => {
351
+ // Teleport: check condition, then fire or vtable fallback
352
+ let result = call_fire_usercall_ret(worm, worm, rb(0x516930));
353
+ if result != 0 {
354
+ call_fire_usercall(worm, worm, rb(0x51EB00));
355
+ } else {
356
+ fire_worm_vtable_0xe(worm, 0x74);
357
+ }
358
+ }
359
+ 17 => call_fire_usercall(worm, worm, rb(0x51E920)), // Freeze
360
+ 18 => fire_worm_vtable_0xe(worm, 0x72), // Suicide Bomber
361
+ 19 => call_fire_usercall(worm, worm, rb(0x51E8C0)), // Skip Go
362
+ 20 => call_fire_usercall(worm, worm, rb(0x51E600)), // Surrender
363
+ 21 => call_fire_usercall(worm, worm, rb(0x51EBE0)), // Select Worm
364
+ 22 => call_fire_usercall(worm, worm, rb(0x51EC30)), // Jet Pack
365
+ 23 => fire_worm_vtable_0xe(worm, 0x78), // Magic Bullet
366
+ 24 => call_fire_usercall(worm, worm, rb(0x51EA60)), // Low Gravity / Fast Walk
367
+ _ => {}
368
+ }
369
+ }
370
+
371
+ /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET.
372
+ /// Args: (eax_val, worm, addr). Saves/restores ESI+EDI. Uses EBX to hold addr.
373
+ #[unsafe(naked)]
374
+ unsafe extern "C" fn call_fire_usercall(_eax: u32, _worm: u32, _addr: u32) {
375
+ core::arch::naked_asm!(
376
+ "push ebx",
377
+ "push esi",
378
+ "push edi",
379
+ "mov eax, [esp+16]", // eax_val (3 saves=12 + ret=4 = 16)
380
+ "mov esi, [esp+20]", // worm
381
+ "mov edi, [esp+20]",
382
+ "mov ebx, [esp+24]", // addr
383
+ "call ebx",
384
+ "pop edi",
385
+ "pop esi",
386
+ "pop ebx",
387
+ "ret",
388
+ );
389
+ }
390
+
391
+ /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET, returns EAX.
392
+ #[unsafe(naked)]
393
+ unsafe extern "C" fn call_fire_usercall_ret(_eax: u32, _worm: u32, _addr: u32) -> i32 {
394
+ core::arch::naked_asm!(
395
+ "push ebx",
396
+ "push esi",
397
+ "push edi",
398
+ "mov eax, [esp+16]",
399
+ "mov esi, [esp+20]",
400
+ "mov edi, [esp+20]",
401
+ "mov ebx, [esp+24]",
402
+ "call ebx",
403
+ "pop edi",
404
+ "pop esi",
405
+ "pop ebx",
406
+ "ret",
407
+ );
408
+ }
409
+
410
+ /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm) + stdcall(1 param), RET 0x4.
411
+ #[unsafe(naked)]
412
+ unsafe extern "C" fn call_fire_usercall_stdcall1(
413
+ _eax: u32, _worm: u32, _param: u32, _addr: u32,
414
+ ) {
415
+ core::arch::naked_asm!(
416
+ "push ebx",
417
+ "push esi",
418
+ "push edi",
419
+ "mov eax, [esp+16]", // eax_val
420
+ "mov esi, [esp+20]", // worm
421
+ "mov edi, [esp+20]",
422
+ "mov ebx, [esp+28]", // addr (3 saves=12 + ret=4 + 3 args=12 = 28)
423
+ "push [esp+24]", // param
424
+ "call ebx",
425
+ "pop edi",
426
+ "pop esi",
427
+ "pop ebx",
428
+ "ret",
429
+ );
430
+ }
431
+
432
+ /// Call worm->vtable[0xE](msg_id) — thiscall(ECX=worm, stack=msg_id), RET 0x4.
433
+ /// Original: `(**(code **)(*worm + 0x38))(msg_id)`.
434
+ /// Uses naked bridge to avoid LLVM stack tracking issues with push/RET mismatch.
435
+ #[unsafe(naked)]
436
+ unsafe extern "C" fn fire_worm_vtable_0xe(_worm: u32, _msg_id: u32) {
437
+ core::arch::naked_asm!(
438
+ "push ebx",
439
+ "push esi",
440
+ "push edi",
441
+ "mov ecx, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
442
+ "mov esi, [esp+16]", // ESI = worm
443
+ "mov edi, [esp+16]", // EDI = worm
444
+ "mov ebx, [ecx]", // vtable
445
+ "mov ebx, [ebx+0x38]",// vtable[0xE]
446
+ "push [esp+20]", // msg_id
447
+ "call ebx", // thiscall: ECX=worm, RET 0x4 cleans msg_id
448
+ "pop edi",
449
+ "pop esi",
450
+ "pop ebx",
451
+ "ret",
452
  );
453
  }
454