feat: port CTaskTeam__CreateWeatherFilter to Rust, extract wa_lcg
Browse filesPort 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 +4 -1
- crates/openwa-core/src/engine/ddgame.rs +2 -7
- crates/openwa-core/src/lib.rs +1 -0
- crates/openwa-core/src/rng.rs +12 -0
- crates/openwa-core/src/task/cloud.rs +1 -0
- crates/openwa-core/src/task/filter.rs +17 -4
- crates/openwa-core/src/task/team.rs +150 -0
- crates/openwa-wormkit/src/replacements/task/cloud.rs +12 -4
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::{
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 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 |
-
///
|
| 24 |
-
/// thiscall
|
| 25 |
#[slot(7)]
|
| 26 |
-
pub
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|