paavohtl Claude Opus 4.6 (1M context) commited on
Commit
f4cb014
·
1 Parent(s): 3db1f09

feat: port weapon firing pipeline — CreateWeaponProjectile, ProjectileFire, CreateArrow

Browse files

Port the core weapon firing functions from WA.exe to Rust:

- CreateWeaponProjectile (0x51E0F0): full replacement — allocates CTaskMissile
(0x40C bytes), SharedData lookup for parent CTaskTurnGame, pool capacity
check, calls original constructor. All projectile weapons flow through this.

- ProjectileFire (0x51DFB0): full replacement — advances game RNG, computes
random spread angle via Fixed16.16 trig table interpolation, applies 2D
rotation matrix to template velocity, calls ProjectileFire_Single per shot.
Used by Uzi, Handgun, Minigun.

- CreateArrow (0x51ED90): full replacement — allocates CTaskArrow (0x168 bytes),
same pool/SharedData pattern as CWP. Used by Shotgun and Longbow (which fire
physically simulated arrow entities, NOT hitscan).

Also adds:
- WeaponSpawnData struct (11 DWORDs) replacing raw [u32; 11] in CTaskMissile
- CTaskArrow struct (0x168 bytes) with vtable at 0x664198
- CTaskMissile size corrected from 0x41C to 0x40C (matching actual allocation)
- Passthrough hooks on StrikeFire and PlacedExplosive for RE discovery
- Fixed PlacedExplosive bridge bug (was not setting ECX/EDX for usercall)
- Renamed GrenadeFire → StrikeFire (handles AirStrike/NapalmStrike, not grenades)
- New replay tests: cluster_petrol_homing, sheeplauncher

Key RE discoveries:
- CTaskMissile constructor is thiscall (not stdcall as previously documented)
- CTaskArrow constructor IS stdcall (RET 0x10, 4 stack params)
- Nearly all projectile weapons route through CreateWeaponProjectile (type=1/sub38=3)
- Grenades use CWP, not the "GrenadeFire" function (which is actually for strikes)
- Longbow/Shotgun spawn CTaskArrow entities, not hitscan rays

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

crates/openwa-core/src/address.rs CHANGED
@@ -316,6 +316,26 @@ pub mod va {
316
  fn WORM_START_FIRING = 0x0051_B7F0;
317
  fn FIRE_WEAPON = 0x0051_EE60;
318
  fn CREATE_WEAPON_PROJECTILE = 0x0051_E0F0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  }
320
 
321
  // =========================================================================
 
316
  fn WORM_START_FIRING = 0x0051_B7F0;
317
  fn FIRE_WEAPON = 0x0051_EE60;
318
  fn CREATE_WEAPON_PROJECTILE = 0x0051_E0F0;
319
+ /// stdcall(worm, fire_params, local_struct), RET 0xC
320
+ fn PROJECTILE_FIRE = 0x0051_DFB0;
321
+ /// Strike weapons (AirStrike, NapalmStrike, MineStrike, MoleSquadron, MailStrike).
322
+ /// stdcall(worm, &subtype_34, local_struct), RET 0xC.
323
+ /// Spawns CTaskAirStrike or similar. NOT for grenades — grenades use CWP.
324
+ fn STRIKE_FIRE = 0x0051_E2C0;
325
+ /// usercall(ECX=local_struct, EDX=worm, [ESP+4]=fire_params), RET 0x4
326
+ fn PLACED_EXPLOSIVE = 0x0051_EC80;
327
+ /// Spawns CTaskArrow (Shotgun, Longbow). Allocates 0x168 bytes.
328
+ /// thiscall(ECX=worm, fire_params, local_struct), RET 0x8.
329
+ fn CREATE_ARROW = 0x0051_ED90;
330
+ /// stdcall(worm, fire_params, local_struct), RET 0xC
331
+ fn ROPE_TYPE1_FIRE = 0x0051_E1C0;
332
+ /// stdcall(worm, fire_params, local_struct), RET 0xC
333
+ fn ROPE_TYPE3_FIRE = 0x0051_E240;
334
+ /// Called by ProjectileFire per shot.
335
+ /// usercall(EDI=spawn_data, stack=[worm, fire_params]), RET 0x8.
336
+ fn PROJECTILE_FIRE_SINGLE = 0x0051_DCF0;
337
+ /// Sin lookup table (1024 entries of Fixed16.16). cos = sin + 256 entries.
338
+ global SIN_TABLE = 0x006A_1860;
339
  }
340
 
341
  // =========================================================================
crates/openwa-core/src/game/weapon.rs CHANGED
@@ -97,6 +97,46 @@ impl TryFrom<u32> for Weapon {
97
  }
98
  }
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  // ============================================================
101
  // WeaponEntry — per-weapon data in the weapon table (0x1D0 bytes)
102
  // ============================================================
 
97
  }
98
  }
99
 
100
+ // ============================================================
101
+ // WeaponSpawnData — launch parameters passed to CTaskMissile ctor
102
+ // ============================================================
103
+
104
+ /// Spawn parameters for a weapon projectile (0x2C = 44 bytes, 11 DWORDs).
105
+ ///
106
+ /// Built on the stack by fire sub-functions (ProjectileFire, GrenadeFire, etc.)
107
+ /// and passed as param_4 to CTaskMissile::Constructor. Copied verbatim into
108
+ /// CTaskMissile at offset 0x130 (spawn_params field).
109
+ ///
110
+ /// Source: runtime inspection via debug CLI + CTaskMissile constructor decompilation.
111
+ #[derive(Clone, Copy)]
112
+ #[repr(C)]
113
+ pub struct WeaponSpawnData {
114
+ /// [0] Team index of the worm that fired this projectile.
115
+ pub owner_id: u32,
116
+ /// [1] Unknown — observed as 1.
117
+ pub _unknown_04: u32,
118
+ /// [2] Fixed16.16 X position at launch.
119
+ pub spawn_x: Fixed,
120
+ /// [3] Fixed16.16 Y position at launch.
121
+ pub spawn_y: Fixed,
122
+ /// [4] Fixed16.16 horizontal velocity. Copied to CGameTask.speed_x.
123
+ pub initial_speed_x: Fixed,
124
+ /// [5] Fixed16.16 vertical velocity. Copied to CGameTask.speed_y.
125
+ pub initial_speed_y: Fixed,
126
+ /// [6] Fixed16.16 aim cursor X at time of fire.
127
+ pub cursor_x: Fixed,
128
+ /// [7] Fixed16.16 aim cursor Y at time of fire.
129
+ pub cursor_y: Fixed,
130
+ /// [8] Index within a cluster volley (0 for single shot, N for Nth sub-pellet).
131
+ /// Determines which half of weapon_data is copied to render_data.
132
+ pub pellet_index: u32,
133
+ /// [9] Fallback timer — copied to render_data[0x19] if that field was zero.
134
+ pub fallback_timer: u32,
135
+ /// [10] Fallback param — copied to render_data[0x11] if that field was zero.
136
+ pub fallback_param: u32,
137
+ }
138
+ const _: () = assert!(core::mem::size_of::<WeaponSpawnData>() == 0x2C);
139
+
140
  // ============================================================
141
  // WeaponEntry — per-weapon data in the weapon table (0x1D0 bytes)
142
  // ============================================================
crates/openwa-core/src/task/arrow.rs ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::game_task::CGameTask;
2
+ use crate::FieldRegistry;
3
+
4
+ crate::define_addresses! {
5
+ class "CTaskArrow" {
6
+ /// CTaskArrow vtable — projectile entity for Shotgun/Longbow
7
+ vtable CTASK_ARROW_VTABLE = 0x0066_4198;
8
+ }
9
+ }
10
+
11
+ /// Arrow/bullet projectile entity (Shotgun, Longbow).
12
+ ///
13
+ /// Extends CGameTask (0xFC bytes). Allocated size: 0x168 bytes.
14
+ /// Constructor zeros 0x148 of 0x168 bytes (first 0x20 not zeroed).
15
+ ///
16
+ /// Inheritance: CTask → CGameTask → CTaskArrow. class_type = 0x0C (via ctor +0x20 = 12).
17
+ /// Constructor: 0x4FE130, stdcall(this, parent, fire_params, spawn_data), RET 0x10.
18
+ /// Vtable: 0x664198.
19
+ #[derive(FieldRegistry)]
20
+ #[repr(C)]
21
+ pub struct CTaskArrow {
22
+ /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94).
23
+ pub base: CGameTask,
24
+
25
+ /// 0xFC–0x167: Arrow-specific fields. Layout unknown.
26
+ pub _unknown_fc: [u8; 0x168 - 0xFC],
27
+ }
28
+
29
+ const _: () = assert!(core::mem::size_of::<CTaskArrow>() == 0x168);
crates/openwa-core/src/task/missile.rs CHANGED
@@ -1,5 +1,6 @@
1
  use super::game_task::CGameTask;
2
  use crate::fixed::Fixed;
 
3
  use crate::FieldRegistry;
4
 
5
  crate::define_addresses! {
@@ -56,21 +57,9 @@ pub struct CTaskMissile {
56
  pub slot_id: u32,
57
 
58
  // ---- 0x130–0x15B: spawn data (11 DWORDs, from param_4) ----
59
- /// 0x130–0x15B: Spawn parameters (11 DWORDs copied from param_4).
60
- ///
61
- /// Runtime-confirmed indices:
62
- /// [0x00] owner_id — team index that fired this missile
63
- /// [0x01] unknown (= 1 observed)
64
- /// [0x02] spawn_x — Fixed16.16 X at launch
65
- /// [0x03] spawn_y — Fixed16.16 Y at launch
66
- /// [0x04] initial_speed_x — Fixed16.16 horizontal velocity (→ CGameTask.speed_x)
67
- /// [0x05] initial_speed_y — Fixed16.16 vertical velocity (→ CGameTask.speed_y)
68
- /// [0x06] cursor_x — Fixed16.16 aim cursor X (observed = level_width/2)
69
- /// [0x07] cursor_y — Fixed16.16 aim cursor Y (observed = level_height/2)
70
- /// [0x08] pellet_index — index within a cluster volley (0 for single shot)
71
- /// [0x09] fallback_timer — copied to render_data[0x19] only if it was zero
72
- /// [0x0A] fallback_param — copied to render_data[0x11] only if it was zero
73
- pub spawn_params: [u32; 0x0B],
74
 
75
  // ---- 0x15C–0x2D3: weapon/scheme data (94 DWORDs, from param_3) ----
76
  /// 0x15C–0x2D3: Weapon/scheme properties (94 DWORDs copied verbatim from param_3).
@@ -132,11 +121,12 @@ pub struct CTaskMissile {
132
  /// 0x3C8: Horizontal direction sign (+1 or -1, determines facing/travel dir).
133
  /// param_1[0xF2] in constructor. Set to -1 for homing/sheep if initial_speed_x < 0.
134
  pub direction: i32,
135
- /// 0x3CC–0x41B: Unknown trailing state
136
- pub _unknown_3cc: [u8; 0x50],
 
137
  }
138
 
139
- const _: () = assert!(core::mem::size_of::<CTaskMissile>() == 0x41C);
140
 
141
  impl CTaskMissile {
142
  /// Missile type from `render_data[0x17]` (= weapon_data[0x1A] for single shots).
@@ -153,24 +143,24 @@ impl CTaskMissile {
153
  }
154
  }
155
 
156
- /// Spawn X as Fixed16.16 (from spawn_params[2]).
157
  pub fn spawn_x(&self) -> Fixed {
158
- Fixed(self.spawn_params[2] as i32)
159
  }
160
 
161
- /// Spawn Y as Fixed16.16 (from spawn_params[3]).
162
  pub fn spawn_y(&self) -> Fixed {
163
- Fixed(self.spawn_params[3] as i32)
164
  }
165
 
166
- /// Aim cursor X at time of fire, Fixed16.16 (from spawn_params[6]).
167
  pub fn cursor_x(&self) -> Fixed {
168
- Fixed(self.spawn_params[6] as i32)
169
  }
170
 
171
- /// Aim cursor Y at time of fire, Fixed16.16 (from spawn_params[7]).
172
  pub fn cursor_y(&self) -> Fixed {
173
- Fixed(self.spawn_params[7] as i32)
174
  }
175
  }
176
 
@@ -207,9 +197,12 @@ impl crate::snapshot::Snapshot for CTaskMissile {
207
  write_indent(w, i)?; writeln!(w, "launch_seed = 0x{:08X}", self.launch_seed)?;
208
  write_indent(w, i)?; writeln!(w, "slot_id = {}", self.slot_id)?;
209
 
210
- write_indent(w, i)?; write!(w, "spawn_params =")?;
211
- for v in &self.spawn_params { write!(w, " {:08X}", v)?; }
212
- writeln!(w)?;
 
 
 
213
 
214
  write_indent(w, i)?; write!(w, "weapon_data =")?;
215
  for (j, v) in self.weapon_data.iter().enumerate() {
 
1
  use super::game_task::CGameTask;
2
  use crate::fixed::Fixed;
3
+ use crate::game::weapon::WeaponSpawnData;
4
  use crate::FieldRegistry;
5
 
6
  crate::define_addresses! {
 
57
  pub slot_id: u32,
58
 
59
  // ---- 0x130–0x15B: spawn data (11 DWORDs, from param_4) ----
60
+ /// 0x130–0x15B: Spawn parameters copied from constructor param_4.
61
+ /// See `WeaponSpawnData` for field documentation.
62
+ pub spawn_params: WeaponSpawnData,
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  // ---- 0x15C–0x2D3: weapon/scheme data (94 DWORDs, from param_3) ----
65
  /// 0x15C–0x2D3: Weapon/scheme properties (94 DWORDs copied verbatim from param_3).
 
121
  /// 0x3C8: Horizontal direction sign (+1 or -1, determines facing/travel dir).
122
  /// param_1[0xF2] in constructor. Set to -1 for homing/sheep if initial_speed_x < 0.
123
  pub direction: i32,
124
+ /// 0x3CC–0x40B: Unknown trailing state.
125
+ /// Allocation size is 0x40C; constructor zeros bytes 0x00–0x3EB.
126
+ pub _unknown_3cc: [u8; 0x40],
127
  }
128
 
129
+ const _: () = assert!(core::mem::size_of::<CTaskMissile>() == 0x40C);
130
 
131
  impl CTaskMissile {
132
  /// Missile type from `render_data[0x17]` (= weapon_data[0x1A] for single shots).
 
143
  }
144
  }
145
 
146
+ /// Spawn X as Fixed16.16.
147
  pub fn spawn_x(&self) -> Fixed {
148
+ self.spawn_params.spawn_x
149
  }
150
 
151
+ /// Spawn Y as Fixed16.16.
152
  pub fn spawn_y(&self) -> Fixed {
153
+ self.spawn_params.spawn_y
154
  }
155
 
156
+ /// Aim cursor X at time of fire, Fixed16.16.
157
  pub fn cursor_x(&self) -> Fixed {
158
+ self.spawn_params.cursor_x
159
  }
160
 
161
+ /// Aim cursor Y at time of fire, Fixed16.16.
162
  pub fn cursor_y(&self) -> Fixed {
163
+ self.spawn_params.cursor_y
164
  }
165
  }
166
 
 
197
  write_indent(w, i)?; writeln!(w, "launch_seed = 0x{:08X}", self.launch_seed)?;
198
  write_indent(w, i)?; writeln!(w, "slot_id = {}", self.slot_id)?;
199
 
200
+ let sp = &self.spawn_params;
201
+ write_indent(w, i)?; writeln!(w, "spawn_params:")?;
202
+ write_indent(w, i + 1)?; writeln!(w, "owner={} spawn=({}, {}) speed=({}, {})",
203
+ sp.owner_id, sp.spawn_x, sp.spawn_y, sp.initial_speed_x, sp.initial_speed_y)?;
204
+ write_indent(w, i + 1)?; writeln!(w, "cursor=({}, {}) pellet={} fallback=({}, {})",
205
+ sp.cursor_x, sp.cursor_y, sp.pellet_index, sp.fallback_timer, sp.fallback_param)?;
206
 
207
  write_indent(w, i)?; write!(w, "weapon_data =")?;
208
  for (j, v) in self.weapon_data.iter().enumerate() {
crates/openwa-core/src/task/mod.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  pub mod base;
2
  pub mod bit_grid;
3
  pub mod cloud;
@@ -12,6 +13,7 @@ pub mod team;
12
  pub mod turn_game;
13
  pub mod worm;
14
 
 
15
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
16
  pub use cloud::CTaskCloud;
17
  pub use filter::CTaskFilter;
 
1
+ pub mod arrow;
2
  pub mod base;
3
  pub mod bit_grid;
4
  pub mod cloud;
 
13
  pub mod turn_game;
14
  pub mod worm;
15
 
16
+ pub use arrow::CTaskArrow;
17
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
18
  pub use cloud::CTaskCloud;
19
  pub use filter::CTaskFilter;
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -5,7 +5,13 @@
5
  //! - AddAmmo (0x522640): add ammo to a weapon slot
6
  //! - SubtractAmmo (0x522680): decrement ammo count
7
  //! - CountAliveWorms (0x5225A0): check if >1 worm alive on team
8
- //! - FireWeapon (0x51EE60): passthrough with weapon type logging
 
 
 
 
 
 
9
 
10
  use core::sync::atomic::{AtomicU32, Ordering};
11
 
@@ -212,10 +218,10 @@ unsafe extern "cdecl" fn fire_weapon_impl(
212
 
213
  match weapon_type {
214
  1 => match subtype_38 {
215
- 1 => call_fire_stdcall1(w, fire_params, rb(0x51EC80)), // PlacedExplosive
216
  2 => call_fire_stdcall3(w, fire_params, local_struct, rb(0x51DFB0)), // Projectile
217
  3 => call_fire_thiscall2(w, fire_params, local_struct, rb(0x51E0F0)), // CreateWeaponProjectile
218
- 4 => call_fire_thiscall2(w, fire_params, local_struct, rb(0x51ED90)), // Shotgun/Longbow (thiscall)
219
  _ => {}
220
  },
221
  2 => match subtype_38 {
@@ -251,19 +257,22 @@ unsafe extern "cdecl" fn fire_weapon_impl(
251
  // This preserves LLVM's callee-saved registers while providing
252
  // the usercall context that sub-functions expect.
253
 
254
- /// Bridge: PlacedExplosive — stdcall(fire_params), RET 0x4.
 
255
  #[unsafe(naked)]
256
- unsafe extern "C" fn call_fire_stdcall1(
257
- _worm: u32, _fire_params: *const WeaponFireParams, _addr: u32,
258
  ) {
259
  core::arch::naked_asm!(
260
  "push ebx",
261
  "push esi",
262
  "push edi",
263
- "mov esi, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
264
- "mov edi, [esp+16]",
265
- "mov ebx, [esp+24]", // addr
266
- "push [esp+20]", // params
 
 
267
  "call ebx",
268
  "pop edi",
269
  "pop esi",
@@ -534,6 +543,53 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
534
 
535
  // ── Naked asm bridges ───────────────────────────────────────
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET.
538
  /// Args: (eax_val, worm, addr). Saves/restores ESI+EDI. Uses EBX to hold addr.
539
  #[unsafe(naked)]
@@ -618,6 +674,356 @@ unsafe extern "C" fn fire_worm_vtable_0xe(_worm: u32, _msg_id: u32) {
618
  );
619
  }
620
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  // ============================================================
622
  // Hook installation
623
  // ============================================================
@@ -630,6 +1036,17 @@ pub fn install() -> Result<(), String> {
630
  let _ = hook::install("CountAliveWorms", va::COUNT_ALIVE_WORMS, trampoline_count_alive_worms as *const ())?;
631
  let trampoline = hook::install("FireWeapon", va::FIRE_WEAPON, trampoline_fire_weapon as *const ())?;
632
  ORIG_FIRE_WEAPON.store(trampoline as u32, Ordering::Relaxed);
 
 
 
 
 
 
 
 
 
 
 
633
  }
634
 
635
  Ok(())
 
5
  //! - AddAmmo (0x522640): add ammo to a weapon slot
6
  //! - SubtractAmmo (0x522680): decrement ammo count
7
  //! - CountAliveWorms (0x5225A0): check if >1 worm alive on team
8
+ //! - FireWeapon (0x51EE60): full Rust dispatch
9
+ //!
10
+ //! Passthrough hooks on fire sub-functions (log params, call original):
11
+ //! - CreateWeaponProjectile (0x51E0F0): thiscall(ECX=worm, fire_params, local_struct)
12
+ //! - ProjectileFire (0x51DFB0): stdcall(worm, fire_params, local_struct)
13
+ //! - StrikeFire (0x51E2C0): stdcall(worm, &subtype_34, local_struct) — AirStrike/NapalmStrike etc.
14
+ //! - PlacedExplosive (0x51EC80): usercall(ECX=local_struct, EDX=worm, fire_params)
15
 
16
  use core::sync::atomic::{AtomicU32, Ordering};
17
 
 
218
 
219
  match weapon_type {
220
  1 => match subtype_38 {
221
+ 1 => call_fire_placed_explosive(w, fire_params, local_struct, rb(0x51EC80)), // PlacedExplosive
222
  2 => call_fire_stdcall3(w, fire_params, local_struct, rb(0x51DFB0)), // Projectile
223
  3 => call_fire_thiscall2(w, fire_params, local_struct, rb(0x51E0F0)), // CreateWeaponProjectile
224
+ 4 => call_fire_thiscall2(w, fire_params, local_struct, rb(0x51ED90)), // CreateArrow (Shotgun/Longbow)
225
  _ => {}
226
  },
227
  2 => match subtype_38 {
 
257
  // This preserves LLVM's callee-saved registers while providing
258
  // the usercall context that sub-functions expect.
259
 
260
+ /// Bridge: PlacedExplosive — usercall(ECX=local_struct, EDX=worm, [ESP+4]=fire_params), RET 0x4.
261
+ /// Args: (worm, fire_params, local_struct, addr).
262
  #[unsafe(naked)]
263
+ unsafe extern "C" fn call_fire_placed_explosive(
264
+ _worm: u32, _fire_params: *const WeaponFireParams, _local_struct: u32, _addr: u32,
265
  ) {
266
  core::arch::naked_asm!(
267
  "push ebx",
268
  "push esi",
269
  "push edi",
270
+ // Set up usercall registers
271
+ // Stack: 3 saves(12) + ret(4) = 16 to first arg
272
+ "mov edx, [esp+16]", // EDX = worm
273
+ "mov ecx, [esp+24]", // ECX = local_struct
274
+ "mov ebx, [esp+28]", // addr
275
+ "push [esp+20]", // fire_params (stack param)
276
  "call ebx",
277
  "pop edi",
278
  "pop esi",
 
543
 
544
  // ── Naked asm bridges ───────────────────────────────────────
545
 
546
+ /// Bridge: ProjectileFire_Single — usercall(EDI=spawn_data, stack=[worm, fire_params]), RET 0x8.
547
+ /// Args: (spawn_data, worm, fire_params, addr).
548
+ #[unsafe(naked)]
549
+ unsafe extern "C" fn call_projectile_fire_single(
550
+ _spawn_data: u32, _worm: u32, _fire_params: u32, _addr: u32,
551
+ ) {
552
+ core::arch::naked_asm!(
553
+ "push ebx",
554
+ "push esi",
555
+ "push edi",
556
+ // Stack: 3 saves(12) + ret(4) = 16 to first arg
557
+ "mov edi, [esp+16]", // EDI = spawn_data
558
+ "mov ebx, [esp+28]", // addr
559
+ "push [esp+24]", // fire_params
560
+ "push [esp+24]", // worm (shifted +4)
561
+ "call ebx", // RET 0x8 cleans 2 params
562
+ "pop edi",
563
+ "pop esi",
564
+ "pop ebx",
565
+ "ret",
566
+ );
567
+ }
568
+
569
+ /// Bridge: CTaskMissile::Constructor — thiscall(ECX=this, parent, fire_params, spawn_data), RET 0xC.
570
+ /// Args: (this, parent, fire_params, spawn_data, ctor_addr).
571
+ #[unsafe(naked)]
572
+ unsafe extern "C" fn call_missile_ctor(
573
+ _this: u32, _parent: u32, _fire_params: u32, _spawn_data: u32, _addr: u32,
574
+ ) {
575
+ core::arch::naked_asm!(
576
+ "push ebx",
577
+ "push esi",
578
+ "push edi",
579
+ // Stack: 3 saves(12) + ret(4) = 16 to first arg
580
+ "mov ecx, [esp+16]", // ECX = this (buffer)
581
+ "mov ebx, [esp+32]", // addr (16 + 4*4 args = 32)
582
+ "push [esp+28]", // spawn_data
583
+ "push [esp+28]", // fire_params (shifted +4)
584
+ "push [esp+28]", // parent (shifted +8)
585
+ "call ebx", // thiscall: RET 0xC cleans 3 params
586
+ "pop edi",
587
+ "pop esi",
588
+ "pop ebx",
589
+ "ret",
590
+ );
591
+ }
592
+
593
  /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET.
594
  /// Args: (eax_val, worm, addr). Saves/restores ESI+EDI. Uses EBX to hold addr.
595
  #[unsafe(naked)]
 
674
  );
675
  }
676
 
677
+ // ============================================================
678
+ // Passthrough hooks — fire sub-functions
679
+ // ============================================================
680
+ // These hooks log parameters, then call the original WA function.
681
+ // They exist for RE discovery and validation — no behavior change.
682
+
683
+ static ORIG_CREATE_WEAPON_PROJECTILE: AtomicU32 = AtomicU32::new(0);
684
+ static ORIG_PROJECTILE_FIRE: AtomicU32 = AtomicU32::new(0);
685
+ static ORIG_STRIKE_FIRE: AtomicU32 = AtomicU32::new(0);
686
+ static ORIG_PLACED_EXPLOSIVE: AtomicU32 = AtomicU32::new(0);
687
+
688
+ /// Full replacement for CreateWeaponProjectile (0x51E0F0).
689
+ /// Convention: thiscall(ECX=worm, fire_params, local_struct), RET 0x8.
690
+ ///
691
+ /// Allocates a CTaskMissile, calls the original constructor, and returns.
692
+ /// The constructor handles SharedData registration and pool management.
693
+ #[unsafe(naked)]
694
+ unsafe extern "C" fn trampoline_create_weapon_projectile() {
695
+ core::arch::naked_asm!(
696
+ // thiscall: ECX=worm, stack=[ret, fire_params, local_struct]
697
+ // Push cdecl args for Rust impl
698
+ "push dword ptr [esp+8]", // local_struct (ret=4 + param1=4 = 8)
699
+ "push dword ptr [esp+8]", // fire_params (shifted +4)
700
+ "push ecx", // worm (this)
701
+ "call {impl_fn}",
702
+ "add esp, 12",
703
+ "ret 0x8", // clean 2 stack params
704
+ impl_fn = sym create_weapon_projectile_impl,
705
+ );
706
+ }
707
+
708
+ /// Rust implementation of CreateWeaponProjectile.
709
+ ///
710
+ /// Original: 0x51E0F0. Allocates CTaskMissile (0x40C bytes), looks up
711
+ /// parent CTaskTurnGame via SharedData, calls the original constructor.
712
+ unsafe extern "cdecl" fn create_weapon_projectile_impl(
713
+ worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: u32,
714
+ ) {
715
+ use openwa_core::engine::DDGame;
716
+ use openwa_core::rebase::rb;
717
+ use openwa_core::task::SharedDataTable;
718
+ use openwa_core::wa_alloc::wa_malloc;
719
+
720
+ let task = &(*worm).base.base; // CTask base
721
+ let ddgame = task.ddgame as *mut DDGame;
722
+ let ddgame_raw = ddgame as *mut u8;
723
+
724
+ // Pool capacity check: DDGame+0x72A4 + 7 must be <= 700
725
+ let pool_count_ptr = ddgame_raw.add(0x72A4) as *mut i32;
726
+ let pool_count = *pool_count_ptr;
727
+
728
+ if pool_count + 7 > 700 {
729
+ // Pool full — show warning message (replicate overflow path)
730
+ let game_info = *(ddgame_raw.add(0x24) as *const *const u8);
731
+ let counter = *(game_info.add(0xD778) as *const i32);
732
+ if counter < 0x3C {
733
+ *(ddgame_raw.add(0x4624) as *mut i32) = 6;
734
+ // Load string resource 0x70F
735
+ let load_string: unsafe extern "cdecl" fn(u32) -> u32 =
736
+ core::mem::transmute(rb(0x593180));
737
+ let s = load_string(0x70F);
738
+ *(ddgame_raw.add(0x7EF4) as *mut u32) = s;
739
+ }
740
+ return;
741
+ }
742
+
743
+ // Look up parent CTaskTurnGame via SharedData (key_esi=0, key_edi=0x19)
744
+ let table = SharedDataTable::from_task(task);
745
+ let parent = table.lookup(0, 0x19);
746
+
747
+ // Allocate CTaskMissile (0x40C bytes)
748
+ let buffer = wa_malloc(0x40C);
749
+ if buffer.is_null() {
750
+ return;
751
+ }
752
+
753
+ // Zero bytes 0x00..0x3EC (the original only zeros 0x3EC of 0x40C)
754
+ core::ptr::write_bytes(buffer, 0, 0x3EC);
755
+
756
+ // Call original CTaskMissile::Constructor
757
+ // thiscall(ECX=buffer, parent, fire_params, local_struct), RET 0xC
758
+ call_missile_ctor(
759
+ buffer as u32,
760
+ parent as u32,
761
+ fire_params as u32,
762
+ local_struct,
763
+ rb(va::CTASK_MISSILE_CTOR),
764
+ );
765
+
766
+ let _ = log_line(&format!(
767
+ "[Weapon] CreateWeaponProjectile: worm=0x{:08X} missile=0x{:08X}",
768
+ worm as u32, buffer as u32,
769
+ ));
770
+ }
771
+
772
+ /// Full replacement for ProjectileFire (0x51DFB0).
773
+ /// Convention: stdcall(worm, fire_params, local_struct), RET 0xC.
774
+ ///
775
+ /// Builds spawn data with RNG-randomized spread, then calls
776
+ /// ProjectileFire_Single per projectile. Used by Uzi, Handgun, Minigun.
777
+ #[unsafe(naked)]
778
+ unsafe extern "C" fn trampoline_projectile_fire() {
779
+ core::arch::naked_asm!(
780
+ // stdcall: stack=[ret, worm, fire_params, local_struct]
781
+ "push dword ptr [esp+12]", // local_struct
782
+ "push dword ptr [esp+12]", // fire_params (shifted +4)
783
+ "push dword ptr [esp+12]", // worm (shifted +8)
784
+ "call {impl_fn}",
785
+ "add esp, 12",
786
+ "ret 0xC", // clean 3 stack params
787
+ impl_fn = sym projectile_fire_impl,
788
+ );
789
+ }
790
+
791
+ /// Rust implementation of ProjectileFire.
792
+ ///
793
+ /// Algorithm (from disasm at 0x51DFB0):
794
+ /// 1. Copy 11 DWORDs from local_struct (spawn template) to local buffer
795
+ /// 2. Loop fire_params+0xC times:
796
+ /// a. Advance game RNG
797
+ /// b. Random spread angle from RNG × fire_params+0x10 / 360
798
+ /// c. Cos/sin table lookup with linear interpolation
799
+ /// d. 2D rotation matrix on template velocity
800
+ /// e. Write rotated velocity into local spawn data
801
+ /// f. Call ProjectileFire_Single(loop_counter, fire_params) with EDI=spawn_data
802
+ unsafe extern "cdecl" fn projectile_fire_impl(
803
+ worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: *const u32,
804
+ ) {
805
+ use openwa_core::engine::DDGame;
806
+ use openwa_core::rebase::rb;
807
+
808
+ let fire_params_raw = fire_params as *const i32;
809
+ let shot_count = *fire_params_raw.add(3); // fire_params+0xC = projectile count per trigger
810
+ if shot_count <= 0 {
811
+ return;
812
+ }
813
+
814
+ // Copy 11 DWORDs from local_struct to local spawn data
815
+ let mut spawn_data = [0u32; 11];
816
+ core::ptr::copy_nonoverlapping(local_struct, spawn_data.as_mut_ptr(), 11);
817
+
818
+ // Read template velocity from local_struct (fields [4] and [5])
819
+ let template_speed_x = *local_struct.add(4) as i32;
820
+ let template_speed_y = *local_struct.add(5) as i32;
821
+
822
+ // Trig table: sin at SIN_TABLE, cos at SIN_TABLE + 256*4
823
+ let sin_table = rb(va::SIN_TABLE) as *const i32;
824
+ let cos_table = sin_table.add(256); // cos = sin offset by 256 entries (quarter turn)
825
+
826
+ let ddgame = (*worm).base.base.ddgame as *mut DDGame;
827
+ let ddgame_raw = ddgame as *mut u8;
828
+ let rng_ptr = ddgame_raw.add(0x45EC) as *mut u32;
829
+ let frame_counter = *(ddgame_raw.add(0x5CC) as *const u32);
830
+
831
+ for _i in 0..shot_count {
832
+ // Advance game RNG: rng = (frame_counter + rng) * 0x19660D + 0x3C6EF35F
833
+ let rng = frame_counter.wrapping_add(*rng_ptr)
834
+ .wrapping_mul(0x19660D)
835
+ .wrapping_add(0x3C6EF35F);
836
+ *rng_ptr = rng;
837
+
838
+ // Compute spread angle: ((rng_low16 - 0x8000) * spread_param) / 360
839
+ let rng_centered = (rng & 0xFFFF) as i32 - 0x8000;
840
+ let spread_param = *fire_params_raw.add(4); // fire_params+0x10
841
+ let angle = (rng_centered * spread_param) / 360;
842
+
843
+ // Cos/sin table lookup with linear interpolation
844
+ // Table index = (angle >> 6) & 0x3FF (1024 entries)
845
+ // Fractional = (angle & 0x3F) << 10
846
+ let table_index = ((angle >> 6) & 0x3FF) as usize;
847
+ let frac = ((angle & 0x3F) << 10) as i32;
848
+
849
+ let cos_base = *cos_table.add(table_index);
850
+ let cos_next = *cos_table.add(table_index + 1);
851
+ let cos_val = cos_base + fixed_mul(cos_next - cos_base, frac);
852
+
853
+ let sin_base = *sin_table.add(table_index);
854
+ let sin_next = *sin_table.add(table_index + 1);
855
+ let sin_val = sin_base + fixed_mul(sin_next - sin_base, frac);
856
+
857
+ // 2D rotation matrix:
858
+ // speed_x = cos * template_x + sin * template_y
859
+ // speed_y = -sin * template_x + cos * template_y
860
+ let speed_x = fixed_mul(cos_val, template_speed_x)
861
+ + fixed_mul(sin_val, template_speed_y);
862
+ let speed_y = fixed_mul(-sin_val, template_speed_x)
863
+ + fixed_mul(cos_val, template_speed_y);
864
+
865
+ // Write rotated velocity into spawn data
866
+ spawn_data[4] = speed_x as u32;
867
+ spawn_data[5] = speed_y as u32;
868
+
869
+ // Call ProjectileFire_Single(worm, fire_params) with EDI=&spawn_data
870
+ call_projectile_fire_single(
871
+ spawn_data.as_ptr() as u32,
872
+ worm as u32,
873
+ fire_params as u32,
874
+ rb(va::PROJECTILE_FIRE_SINGLE),
875
+ );
876
+ }
877
+
878
+ let _ = log_line(&format!(
879
+ "[Weapon] ProjectileFire: worm=0x{:08X} shots={}",
880
+ worm as u32, shot_count,
881
+ ));
882
+ }
883
+
884
+ /// Fixed-point 16.16 multiply: (a * b) >> 16, using full 64-bit intermediate.
885
+ #[inline(always)]
886
+ fn fixed_mul(a: i32, b: i32) -> i32 {
887
+ ((a as i64 * b as i64) >> 16) as i32
888
+ }
889
+
890
+ /// Full replacement for CreateArrow (0x51ED90).
891
+ /// Convention: thiscall(ECX=worm, fire_params, local_struct), RET 0x8.
892
+ ///
893
+ /// Allocates a CTaskArrow (0x168 bytes), calls the original stdcall constructor.
894
+ /// Used by Shotgun and Longbow.
895
+ #[unsafe(naked)]
896
+ unsafe extern "C" fn trampoline_create_arrow() {
897
+ core::arch::naked_asm!(
898
+ "push dword ptr [esp+8]",
899
+ "push dword ptr [esp+8]",
900
+ "push ecx",
901
+ "call {impl_fn}",
902
+ "add esp, 12",
903
+ "ret 0x8",
904
+ impl_fn = sym create_arrow_impl,
905
+ );
906
+ }
907
+
908
+ unsafe extern "cdecl" fn create_arrow_impl(
909
+ worm: *mut CTaskWorm, fire_params: *const WeaponFireParams, local_struct: u32,
910
+ ) {
911
+ use openwa_core::engine::DDGame;
912
+ use openwa_core::rebase::rb;
913
+ use openwa_core::task::SharedDataTable;
914
+ use openwa_core::wa_alloc::wa_malloc;
915
+
916
+ let task = &(*worm).base.base;
917
+ let ddgame = task.ddgame as *mut DDGame;
918
+ let ddgame_raw = ddgame as *mut u8;
919
+
920
+ // Pool capacity check: DDGame+0x72A4 + 2 must be <= 700
921
+ let pool_count = *(ddgame_raw.add(0x72A4) as *const i32);
922
+ if pool_count + 2 > 700 {
923
+ let game_info = *(ddgame_raw.add(0x24) as *const *const u8);
924
+ let counter = *(game_info.add(0xD778) as *const i32);
925
+ if counter < 0x3C {
926
+ *(ddgame_raw.add(0x4624) as *mut i32) = 6;
927
+ let load_string: unsafe extern "cdecl" fn(u32) -> u32 =
928
+ core::mem::transmute(rb(0x593180));
929
+ let s = load_string(0x70F);
930
+ *(ddgame_raw.add(0x7EF4) as *mut u32) = s;
931
+ }
932
+ return;
933
+ }
934
+
935
+ // Look up parent CTaskTurnGame via SharedData (key 0, 0x19)
936
+ let table = SharedDataTable::from_task(task);
937
+ let parent = table.lookup(0, 0x19);
938
+
939
+ // Allocate CTaskArrow (0x168 bytes)
940
+ let buffer = wa_malloc(0x168);
941
+ if buffer.is_null() {
942
+ return;
943
+ }
944
+ core::ptr::write_bytes(buffer, 0, 0x148);
945
+
946
+ // CTaskArrow::Constructor — stdcall(this, parent, fire_params, local_struct), RET 0x10
947
+ let ctor: unsafe extern "stdcall" fn(u32, u32, u32, u32) =
948
+ core::mem::transmute(rb(va::CTASK_ARROW_CTOR));
949
+ ctor(buffer as u32, parent as u32, fire_params as u32, local_struct);
950
+
951
+ let _ = log_line(&format!(
952
+ "[Weapon] CreateArrow: worm=0x{:08X} arrow=0x{:08X}",
953
+ worm as u32, buffer as u32,
954
+ ));
955
+ }
956
+
957
+ /// Passthrough hook for StrikeFire (0x51E2C0).
958
+ /// Convention: stdcall(worm, &subtype_34, local_struct), RET 0xC.
959
+ #[unsafe(naked)]
960
+ unsafe extern "C" fn trampoline_strike_fire() {
961
+ core::arch::naked_asm!(
962
+ "push eax",
963
+ "push ecx",
964
+ "push edx",
965
+ "push dword ptr [esp+24]", // local_struct
966
+ "push dword ptr [esp+24]", // subtype_34_ptr
967
+ "push dword ptr [esp+24]", // worm
968
+ "call {log_fn}",
969
+ "add esp, 12",
970
+ "pop edx",
971
+ "pop ecx",
972
+ "pop eax",
973
+ "jmp [{orig}]",
974
+ log_fn = sym log_strike_fire,
975
+ orig = sym ORIG_STRIKE_FIRE,
976
+ );
977
+ }
978
+
979
+ unsafe extern "cdecl" fn log_strike_fire(worm: u32, subtype_34_ptr: u32, local_struct: u32) {
980
+ let _ = log_line(&format!(
981
+ "[Weapon] StrikeFire: worm=0x{:08X} subtype_34=0x{:08X} local=0x{:08X}",
982
+ worm, subtype_34_ptr, local_struct,
983
+ ));
984
+ }
985
+
986
+ /// Passthrough hook for PlacedExplosive (0x51EC80).
987
+ /// Convention: usercall(ECX=local_struct, EDX=worm, [ESP+4]=fire_params), RET 0x4.
988
+ #[unsafe(naked)]
989
+ unsafe extern "C" fn trampoline_placed_explosive() {
990
+ core::arch::naked_asm!(
991
+ // Save ALL registers — ECX and EDX are usercall params!
992
+ "push eax",
993
+ "push ecx",
994
+ "push edx",
995
+ "push ebx",
996
+ "push esi",
997
+ "push edi",
998
+ "push ebp",
999
+ // Log: push cdecl args
1000
+ "push dword ptr [esp+32]", // fire_params (7 saves=28 + ret=4 = 32)
1001
+ "push edx", // worm (still valid, saved above)
1002
+ "push ecx", // local_struct (still valid)
1003
+ "call {log_fn}",
1004
+ "add esp, 12",
1005
+ // Restore ALL (ECX and EDX restored for usercall!)
1006
+ "pop ebp",
1007
+ "pop edi",
1008
+ "pop esi",
1009
+ "pop ebx",
1010
+ "pop edx",
1011
+ "pop ecx",
1012
+ "pop eax",
1013
+ // Call original (usercall — ECX, EDX, stack all intact)
1014
+ "jmp [{orig}]",
1015
+ log_fn = sym log_placed_explosive,
1016
+ orig = sym ORIG_PLACED_EXPLOSIVE,
1017
+ );
1018
+ }
1019
+
1020
+ unsafe extern "cdecl" fn log_placed_explosive(local_struct: u32, worm: u32, fire_params: u32) {
1021
+ let _ = log_line(&format!(
1022
+ "[Weapon] PlacedExplosive: worm=0x{:08X} local=0x{:08X} params=0x{:08X}",
1023
+ worm, local_struct, fire_params,
1024
+ ));
1025
+ }
1026
+
1027
  // ============================================================
1028
  // Hook installation
1029
  // ============================================================
 
1036
  let _ = hook::install("CountAliveWorms", va::COUNT_ALIVE_WORMS, trampoline_count_alive_worms as *const ())?;
1037
  let trampoline = hook::install("FireWeapon", va::FIRE_WEAPON, trampoline_fire_weapon as *const ())?;
1038
  ORIG_FIRE_WEAPON.store(trampoline as u32, Ordering::Relaxed);
1039
+
1040
+ // Passthrough hooks on fire sub-functions (log + call original)
1041
+ let t = hook::install("CreateWeaponProjectile", va::CREATE_WEAPON_PROJECTILE, trampoline_create_weapon_projectile as *const ())?;
1042
+ ORIG_CREATE_WEAPON_PROJECTILE.store(t as u32, Ordering::Relaxed);
1043
+ let t = hook::install("ProjectileFire", va::PROJECTILE_FIRE, trampoline_projectile_fire as *const ())?;
1044
+ ORIG_PROJECTILE_FIRE.store(t as u32, Ordering::Relaxed);
1045
+ let t = hook::install("StrikeFire", va::STRIKE_FIRE, trampoline_strike_fire as *const ())?;
1046
+ ORIG_STRIKE_FIRE.store(t as u32, Ordering::Relaxed);
1047
+ let t = hook::install("PlacedExplosive", va::PLACED_EXPLOSIVE, trampoline_placed_explosive as *const ())?;
1048
+ ORIG_PLACED_EXPLOSIVE.store(t as u32, Ordering::Relaxed);
1049
+ let _ = hook::install("CreateArrow", va::CREATE_ARROW, trampoline_create_arrow as *const ())?;
1050
  }
1051
 
1052
  Ok(())
crates/openwa-wormkit/src/validation/mod.rs CHANGED
@@ -1663,8 +1663,8 @@ fn dump_missile_tasks() {
1663
  ));
1664
  let _ = log_validation(&format!(
1665
  " spawn_params: owner={} pellet={} spawn=({:.1},{:.1})",
1666
- m.spawn_params[0],
1667
- m.spawn_params[8],
1668
  m.spawn_x().to_f32(),
1669
  m.spawn_y().to_f32()
1670
  ));
 
1663
  ));
1664
  let _ = log_validation(&format!(
1665
  " spawn_params: owner={} pellet={} spawn=({:.1},{:.1})",
1666
+ m.spawn_params.owner_id,
1667
+ m.spawn_params.pellet_index,
1668
  m.spawn_x().to_f32(),
1669
  m.spawn_y().to_f32()
1670
  ));
testdata/replays/cluster_petrol_homing.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4572aed04b7223eaab40221ca249dfdad51d5a2ed69f3af96909c906ddf9d970
3
+ size 14434
testdata/replays/cluster_petrol_homing_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ab9ae78197fc4d0084cf4cf1b0ca5e18e1cba0bd98e0ac8fe82fe1b686a9ebb4
3
+ size 1280
testdata/replays/sheeplauncher.WAgame ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3e90dd2b24ed148bca3fb52d6c3c7f323a3a3b4a3b309b7c5317238dc29c287
3
+ size 12689
testdata/replays/sheeplauncher_expected.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3b418d351b9f8545fbf90d11f8ae523d1717410122a839f78c7ab9c591eb170a
3
+ size 1803