paavohtl Claude Opus 4.6 (1M context) commited on
Commit
113d28e
·
1 Parent(s): e98f3c6

feat: port CTaskTeam__CreateWeatherFilter to Rust, extract wa_lcg

Browse files

Port the cloud spawning factory (0x552960) that creates a CTaskFilter
and spawns CTaskCloud children with deterministic LCG randomization.

- Add rng.rs with shared wa_lcg() used by advance_rng, advance_effect_rng,
and cloud spawning (eliminates duplicated LCG constants)
- Port create_weather_filter in task/team.rs with naked asm bridge for
CTaskFilter constructor (usercall ECX=0x1B)
- Add Subscribe (slot 8) and SubscribeAll (slot 7) to CTaskFilterVTable
- Add CTASK_TEAM_CREATE_WEATHER_FILTER and CTASK_FILTER_SUBSCRIBE addresses
- Hook via MinHook in wormkit cloud.rs

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

crates/openwa-core/src/address.rs CHANGED
@@ -41,7 +41,10 @@ pub mod va {
41
  CTASK_CLOUD_CTOR, CTASK_CLOUD_READ_REPLAY_STATE, CTASK_CLOUD_VTABLE,
42
  CTASK_CLOUD_WRITE_REPLAY_STATE,
43
  };
44
- pub use crate::task::filter::{CTASK_FILTER_CTOR, CTASK_FILTER_VTABLE};
 
 
 
45
  pub use crate::task::fire::{CTASK_FIRE_CTOR, CTASK_FIRE_VTABLE};
46
  pub use crate::task::game_task::{
47
  CGAMETASK_CONSTRUCTOR, CGAMETASK_SOUND_EMITTER_VT, CGAMETASK_VT0, CGAMETASK_VT1_FREE,
 
41
  CTASK_CLOUD_CTOR, CTASK_CLOUD_READ_REPLAY_STATE, CTASK_CLOUD_VTABLE,
42
  CTASK_CLOUD_WRITE_REPLAY_STATE,
43
  };
44
+ pub use crate::task::filter::{
45
+ CTASK_FILTER_CTOR, CTASK_FILTER_SUBSCRIBE, CTASK_FILTER_VTABLE,
46
+ CTASK_TEAM_CREATE_WEATHER_FILTER,
47
+ };
48
  pub use crate::task::fire::{CTASK_FIRE_CTOR, CTASK_FIRE_VTABLE};
49
  pub use crate::task::game_task::{
50
  CGAMETASK_CONSTRUCTOR, CGAMETASK_SOUND_EMITTER_VT, CGAMETASK_VT0, CGAMETASK_VT1_FREE,
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -509,10 +509,7 @@ impl DDGame {
509
  /// shared RNG — both gameplay and visual effects advance it. Any difference in
510
  /// RNG call count between Rust and original code causes replay desync.
511
  pub fn advance_rng(&mut self) -> u32 {
512
- let rng = (self.frame_counter as u32)
513
- .wrapping_add(self.game_rng)
514
- .wrapping_mul(0x19660D)
515
- .wrapping_add(0x3C6EF35F);
516
  self.game_rng = rng;
517
  rng
518
  }
@@ -524,9 +521,7 @@ impl DDGame {
524
  /// 1-indexed health ratio array, repurposed by WA as a secondary RNG for weapon
525
  /// release visual effects.
526
  pub fn advance_effect_rng(&mut self) -> u32 {
527
- let rng = (self.team_health_ratio[0] as u32)
528
- .wrapping_mul(0x19660D)
529
- .wrapping_add(0x3C6EF35F);
530
  self.team_health_ratio[0] = rng as i32;
531
  rng
532
  }
 
509
  /// shared RNG — both gameplay and visual effects advance it. Any difference in
510
  /// RNG call count between Rust and original code causes replay desync.
511
  pub fn advance_rng(&mut self) -> u32 {
512
+ let rng = crate::rng::wa_lcg((self.frame_counter as u32).wrapping_add(self.game_rng));
 
 
 
513
  self.game_rng = rng;
514
  rng
515
  }
 
521
  /// 1-indexed health ratio array, repurposed by WA as a secondary RNG for weapon
522
  /// release visual effects.
523
  pub fn advance_effect_rng(&mut self) -> u32 {
524
+ let rng = crate::rng::wa_lcg(self.team_health_ratio[0] as u32);
 
 
525
  self.team_health_ratio[0] = rng as i32;
526
  rng
527
  }
crates/openwa-core/src/lib.rs CHANGED
@@ -47,5 +47,6 @@ pub mod snapshot;
47
  pub mod task;
48
  pub mod vtable;
49
  pub mod wa;
 
50
  pub mod wa_alloc;
51
  pub mod wa_call;
 
47
  pub mod task;
48
  pub mod vtable;
49
  pub mod wa;
50
+ pub mod rng;
51
  pub mod wa_alloc;
52
  pub mod wa_call;
crates/openwa-core/src/rng.rs ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! WA's LCG random number generator.
2
+
3
+ /// Single step of WA's LCG: `state * 0x19660D + 0x3C6EF35F`.
4
+ ///
5
+ /// This is the core PRNG used throughout WA.exe. The game RNG
6
+ /// ([`DDGame::advance_rng`](crate::engine::ddgame::DDGame::advance_rng))
7
+ /// adds `frame_counter` before the multiply; the effect RNG and standalone
8
+ /// uses (e.g. cloud spawning) use this directly.
9
+ #[inline]
10
+ pub fn wa_lcg(state: u32) -> u32 {
11
+ state.wrapping_mul(0x19660D).wrapping_add(0x3C6EF35F)
12
+ }
crates/openwa-core/src/task/cloud.rs CHANGED
@@ -254,3 +254,4 @@ impl CTaskCloud {
254
  }
255
  }
256
  }
 
 
254
  }
255
  }
256
  }
257
+
crates/openwa-core/src/task/filter.rs CHANGED
@@ -5,11 +5,20 @@ crate::define_addresses! {
5
  class "CTaskFilter" {
6
  /// CTaskFilter vtable - role unclear; 4 instances in a 2-team 3-worm game
7
  vtable CTASK_FILTER_VTABLE = 0x0066_9DAC;
 
 
8
  ctor CTASK_FILTER_CTOR = 0x0054_F3D0;
 
 
 
9
  }
 
 
 
 
10
  }
11
 
12
- /// CTaskFilter vtable — 12 slots. Extends CTask base (8 slots) with filter behavior.
13
  ///
14
  /// Vtable at Ghidra 0x669DAC. Slot 2 (HandleMessage) checks the subscription
15
  /// table before forwarding messages to children.
@@ -20,10 +29,14 @@ pub struct CTaskFilterVTable {
20
  #[slot(2)]
21
  pub handle_message:
22
  fn(this: *mut CTaskFilter, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
23
- /// ProcessFrame — per-frame filter update.
24
- /// thiscall + 1 stack param (flags), RET 0x4.
25
  #[slot(7)]
26
- pub process_frame: fn(this: *mut CTaskFilter, flags: u32),
 
 
 
 
27
  }
28
 
29
  /// Message-subscription filter task — routes messages selectively to child tasks.
 
5
  class "CTaskFilter" {
6
  /// CTaskFilter vtable - role unclear; 4 instances in a 2-team 3-worm game
7
  vtable CTASK_FILTER_VTABLE = 0x0066_9DAC;
8
+ /// CTaskFilter constructor (usercall: ECX=init_val_1c, stdcall params: this, parent).
9
+ /// RET 0x8. Returns constructed pointer in EAX.
10
  ctor CTASK_FILTER_CTOR = 0x0054_F3D0;
11
+ /// CTaskFilter::Subscribe — sets subscription_table[msg_id] = 1.
12
+ /// thiscall + 1 stack param (msg_id), RET 0x4.
13
+ vmethod CTASK_FILTER_SUBSCRIBE = 0x0054_F370;
14
  }
15
+
16
+ /// CTaskTeam__CreateWeatherFilter — creates a CTaskFilter + spawns CTaskCloud children.
17
+ /// stdcall, 1 stack param (parent task), RET 0x4.
18
+ fn/Stdcall CTASK_TEAM_CREATE_WEATHER_FILTER = 0x0055_2960;
19
  }
20
 
21
+ /// CTaskFilter vtable — 12 slots. Extends CTask base (7 slots) with filter behavior.
22
  ///
23
  /// Vtable at Ghidra 0x669DAC. Slot 2 (HandleMessage) checks the subscription
24
  /// table before forwarding messages to children.
 
29
  #[slot(2)]
30
  pub handle_message:
31
  fn(this: *mut CTaskFilter, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
32
+ /// SubscribeAll — sets all 100 subscription table entries to 1.
33
+ /// thiscall, no stack params.
34
  #[slot(7)]
35
+ pub subscribe_all: fn(this: *mut CTaskFilter),
36
+ /// Subscribe — sets subscription_table[msg_id] = 1.
37
+ /// thiscall + 1 stack param (msg_id), RET 0x4.
38
+ #[slot(8)]
39
+ pub subscribe: fn(this: *mut CTaskFilter, msg_id: u32),
40
  }
41
 
42
  /// Message-subscription filter task — routes messages selectively to child tasks.
crates/openwa-core/src/task/team.rs CHANGED
@@ -177,3 +177,153 @@ impl CTaskTeam {
177
  );
178
  }
179
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  );
178
  }
179
  }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // CTaskTeam__CreateWeatherFilter (0x552960) — cloud spawning factory
183
+ // ---------------------------------------------------------------------------
184
+
185
+ use crate::address::va;
186
+ use crate::engine::ddgame::DDGame;
187
+ use crate::rebase::rb;
188
+ use crate::rng::wa_lcg;
189
+ use crate::task::cloud::{CTaskCloud, CloudType};
190
+ use crate::task::filter::CTaskFilter;
191
+ use crate::wa_alloc::WABox;
192
+
193
+ /// Bridge: call CTaskFilter constructor (usercall: ECX=0x1B, stdcall params: this, parent).
194
+ /// Returns constructed pointer in EAX.
195
+ ///
196
+ /// # Safety
197
+ /// `this` must point to a zeroed 0xB4-byte allocation. `parent` must be a valid CTask.
198
+ #[unsafe(naked)]
199
+ unsafe extern "cdecl" fn call_filter_ctor(
200
+ _this: *mut u8,
201
+ _parent: *mut u8,
202
+ _addr: u32,
203
+ ) -> *mut CTaskFilter {
204
+ core::arch::naked_asm!(
205
+ "mov ecx, 0x1b", // init_val_1c register param
206
+ "mov eax, [esp+12]", // addr (3rd cdecl param)
207
+ "push [esp+8]", // push parent (2nd cdecl param)
208
+ "push [esp+8]", // push this (1st cdecl param, shifted by 1 push)
209
+ "call eax", // stdcall, callee cleans 2 params (RET 8)
210
+ "ret", // cdecl return; caller cleans our 3 params
211
+ )
212
+ }
213
+
214
+ /// Pure Rust port of CTaskTeam__CreateWeatherFilter (0x552960).
215
+ ///
216
+ /// Creates a CTaskFilter subscribed to messages {1, 2, 3, 0x54}, then spawns
217
+ /// CTaskCloud children with deterministic LCG randomization.
218
+ ///
219
+ /// Cloud count: 32 if `level_width_raw == 0`, else 10.
220
+ ///
221
+ /// Called from CTaskTeam constructor. stdcall with 1 param (parent task).
222
+ ///
223
+ /// # Safety
224
+ /// `parent` must be a valid CTask pointer (typically a CTaskTeam child).
225
+ pub unsafe extern "stdcall" fn create_weather_filter(parent: *mut CTask) {
226
+ // 1. Allocate and construct CTaskFilter
227
+ let filter_box = WABox::<CTaskFilter>::alloc(0xB4, 0x94);
228
+ let filter = call_filter_ctor(
229
+ filter_box.as_ptr() as *mut u8,
230
+ parent as *mut u8,
231
+ rb(va::CTASK_FILTER_CTOR),
232
+ );
233
+ filter_box.leak(); // ownership transfers to the task tree
234
+
235
+ // 2. Subscribe to messages: FrameStart(1), FrameFinish(2), RenderScene(3), SetWind(0x54)
236
+ (*filter).subscription_table[1] = 1;
237
+ (*filter).subscription_table[2] = 1;
238
+ (*filter).subscription_table[3] = 1;
239
+ (*filter).subscription_table[0x54] = 1;
240
+
241
+ // 3. Read level geometry from DDGame
242
+ let ddgame = (*parent).ddgame;
243
+ let level_right = (*ddgame).level_bound_max_x.0;
244
+ let level_left = (*ddgame).level_bound_min_x.0;
245
+ let level_width_int = (level_right - level_left) >> 16; // integer pixels
246
+ let level_min_x_int = (*ddgame).level_bound_min_x.0 >> 16; // integer part (signed)
247
+ let level_height = (*ddgame).level_height as i32;
248
+
249
+ // 4. Determine cloud count and weather modifier
250
+ let (cloud_count, weather_mod): (i32, i32) = if (*ddgame).level_width_raw != 0 {
251
+ (10, 0)
252
+ } else {
253
+ (32, -256)
254
+ };
255
+
256
+ if cloud_count <= 0 || level_width_int <= 0 {
257
+ return;
258
+ }
259
+
260
+ // 5. LCG random loop — spawn clouds as children of the filter
261
+ let mut rng_state: u32 = 0x12345678;
262
+ let mut layer_depth = 0x19_0000i32; // Fixed 25.0
263
+ let mut accum_3i = 0i32;
264
+
265
+ // Height contribution: (level_height + rounding) / 16
266
+ // Matches MSVC signed-division rounding: (val + (val >> 31 & 15)) >> 4
267
+ let height_round = if level_height < 0 {
268
+ level_height & 0xF
269
+ } else {
270
+ 0
271
+ };
272
+ let height_contrib = (level_height + height_round) >> 4;
273
+
274
+ // Height tenth: level_height / 10 with MSVC magic-number divide
275
+ let height_tenth = {
276
+ let h = level_height as i64;
277
+ let raw = ((h * 0x6666_6667) >> 34) as i32;
278
+ raw + ((raw as u32 >> 31) as i32) // round toward zero
279
+ };
280
+
281
+ let ctask_ctor: unsafe extern "stdcall" fn(*mut u8, *mut u8, *mut DDGame) =
282
+ core::mem::transmute(rb(va::CTASK_CONSTRUCTOR) as usize);
283
+
284
+ for i in 0..cloud_count {
285
+ // X position: random within level bounds
286
+ let rand_frac = (rng_state & 0xFFFF) as i32;
287
+ rng_state = wa_lcg(rng_state);
288
+
289
+ let pos_x_int = rand_frac % level_width_int + level_min_x_int;
290
+
291
+ // Y seed: height/16 + scaled offset + weather modifier
292
+ let y_offset = height_tenth * i / cloud_count;
293
+ let y_seed = ((height_contrib + y_offset + weather_mod) << 16) as u32;
294
+
295
+ // X velocity: random magnitude + 0x8000, random sign
296
+ let vel_x_base = (rng_state & 0xFFFF) as i32 + 0x8000;
297
+ rng_state = wa_lcg(rng_state);
298
+ let vel_x = if rng_state & 0x10000 != 0 {
299
+ Fixed(-vel_x_base)
300
+ } else {
301
+ Fixed(vel_x_base)
302
+ };
303
+ rng_state = wa_lcg(rng_state); // advance a third time per iteration
304
+
305
+ // Cloud type: distributes large→medium→small across the batch
306
+ let cloud_type = match 2 - accum_3i / cloud_count {
307
+ 0 => CloudType::Large,
308
+ 1 => CloudType::Medium,
309
+ _ => CloudType::Small,
310
+ };
311
+
312
+ // Allocate cloud, construct CTask base, then init cloud fields
313
+ let cloud_box = WABox::<CTaskCloud>::alloc(0x74, 0x54);
314
+ let cloud_ptr = cloud_box.as_ptr();
315
+ ctask_ctor(cloud_ptr as *mut u8, filter as *mut u8, ddgame);
316
+ CTaskCloud::init(
317
+ cloud_ptr,
318
+ cloud_type,
319
+ Fixed(layer_depth),
320
+ Fixed(pos_x_int << 16),
321
+ vel_x,
322
+ y_seed,
323
+ );
324
+ cloud_box.leak(); // ownership transfers to the task tree
325
+
326
+ layer_depth -= 1;
327
+ accum_3i += 3;
328
+ }
329
+ }
crates/openwa-wormkit/src/replacements/task/cloud.rs CHANGED
@@ -1,10 +1,10 @@
1
- //! CTaskCloud vtable hooks.
2
  //!
3
- //! Thin wormkit shim — game logic lives in `openwa_core::task::cloud::cloud_handle_message`.
4
 
5
  use openwa_core::address::va;
6
  use openwa_core::log::log_line;
7
- use openwa_core::task::cloud;
8
 
9
  pub fn install() -> Result<(), String> {
10
  use openwa_core::vtable_replace;
@@ -13,6 +13,14 @@ pub fn install() -> Result<(), String> {
13
  handle_message => cloud::cloud_handle_message,
14
  })?;
15
 
16
- let _ = log_line("[Cloud] HandleMessage hooked via vtable_replace");
 
 
 
 
 
 
 
 
17
  Ok(())
18
  }
 
1
+ //! CTaskCloud vtable hooks and CreateWeatherFilter replacement.
2
  //!
3
+ //! Thin wormkit shim — game logic lives in `openwa_core::task::cloud`.
4
 
5
  use openwa_core::address::va;
6
  use openwa_core::log::log_line;
7
+ use openwa_core::task::{cloud, team};
8
 
9
  pub fn install() -> Result<(), String> {
10
  use openwa_core::vtable_replace;
 
13
  handle_message => cloud::cloud_handle_message,
14
  })?;
15
 
16
+ unsafe {
17
+ crate::hook::install(
18
+ "CTaskTeam__CreateWeatherFilter",
19
+ va::CTASK_TEAM_CREATE_WEATHER_FILTER,
20
+ team::create_weather_filter as *const (),
21
+ )?;
22
+ }
23
+
24
+ let _ = log_line("[Cloud] HandleMessage + CreateWeatherFilter hooked");
25
  Ok(())
26
  }