File size: 11,755 Bytes
f3cbb8a 04e939e e98f3c6 045679a f3cbb8a 94ea94e 2b88223 e98f3c6 94ea94e e98f3c6 94ea94e e98f3c6 c6d4e73 e98f3c6 c6d4e73 e98f3c6 c6d4e73 1d3de83 c6d4e73 f3cbb8a e98f3c6 2b88223 e98f3c6 f3cbb8a b04b93b f3cbb8a e98f3c6 2b88223 e98f3c6 045679a f3cbb8a c6d4e73 e98f3c6 f3cbb8a b04b93b 2b88223 b04b93b f3cbb8a 2b88223 b04b93b 2b88223 b04b93b 2b88223 f3cbb8a e98f3c6 f3cbb8a c6d4e73 e98f3c6 c6d4e73 a553079 b04b93b 07389f9 a553079 b04b93b a553079 b04b93b 07389f9 b04b93b a553079 e98f3c6 b04b93b e98f3c6 b04b93b e98f3c6 b04b93b e98f3c6 2b88223 e98f3c6 2b88223 b04b93b 2b88223 e98f3c6 b04b93b e98f3c6 2b88223 e98f3c6 b04b93b e98f3c6 b04b93b e98f3c6 b04b93b e98f3c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | use super::base::CTask;
use crate::fixed::Fixed;
use crate::game::class_type::ClassType;
use crate::FieldRegistry;
crate::define_addresses! {
class "CTaskCloud" {
/// CTaskCloud vtable - cloud/airstrike entity
vtable CTASK_CLOUD_VTABLE = 0x0066_9D38;
/// CTaskCloud constructor (usercall: ESI=this, EAX=parent, EDI=render_y,
/// stack: cloud_type, layer_depth, pos_x, vel_x). RET 0x10.
ctor CTASK_CLOUD_CTOR = 0x0054_82E0;
/// CTaskCloud::WriteReplayState β serializes cloud state to replay stream.
/// thiscall + 1 stack param (stream ptr), RET 0x4.
vmethod CTASK_CLOUD_WRITE_REPLAY_STATE = 0x0054_8430;
/// CTaskCloud::ReadReplayState β deserializes cloud state from replay stream.
/// usercall (ESI=stream, EDI=this). Not a vtable method.
fn CTASK_CLOUD_READ_REPLAY_STATE = 0x0054_8370;
}
}
/// CTaskCloud vtable β 7 slots. Same layout as CTask base; overrides slots 0 and 2.
///
/// Vtable at Ghidra 0x669D38. Size verified by gap to next vtable (CTaskCPU at 0x669D54).
/// CTaskCloud does NOT override ProcessFrame (slot 6) β all update logic is in
/// HandleMessage, responding to the FrameFinish message.
#[openwa_core::vtable(size = 7, va = 0x0066_9D38, class = "CTaskCloud")]
pub struct CTaskCloudVTable {
/// WriteReplayState β serializes cloud state to replay stream.
/// thiscall + 1 stack param (stream ptr), RET 0x4.
#[slot(0)]
pub write_replay_state: fn(this: *mut CTaskCloud, stream: *mut u8),
/// HandleMessage β processes cloud messages (wind updates, render).
/// thiscall + 4 stack params, RET 0x10.
#[slot(2)]
pub handle_message:
fn(this: *mut CTaskCloud, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
}
/// Airstrike / weather cloud task.
///
/// Extends CTask directly (not CGameTask). Clouds drift horizontally with wind,
/// scroll on a parallax layer, and render as a single sprite.
///
/// Allocation: 0x74 bytes (operator new in CTaskTeam__CreateWeatherFilter 0x552960).
/// Constructor: 0x5482E0 (usercall ESI=this, EAX=parent, EDI=render_y).
/// Vtable: 0x669D38. Class type byte: 0x17 (ClassType::Cloud).
///
/// Three cloud sizes chosen by `cloud_type` param (0/1/2):
/// - type 0: sprite 0x268 (large), phase_speed 0x200
/// - type 1: sprite 0x269 (medium), phase_speed 0x166
/// - type 2: sprite 0x26A (small), phase_speed 0xCC
///
/// CreateWeatherFilter spawns clouds with a deterministic LCG (seed 0x12345678),
/// randomizing pos_x within level bounds and vel_x with random sign. The
/// `render_y` field encodes the cloud's vertical screen position as Fixed16
/// (`level_height/16 + scaled_offset` per cloud index) and is passed to
/// `RQ_DrawSpriteLocal` as the sprite Y at render time.
///
/// Source: Ghidra decompilation of 0x5482E0 (constructor), 0x5484C0 (HandleMessage),
/// 0x548430 (WriteReplayState), 0x548370 (ReadReplayState).
#[derive(FieldRegistry)]
#[repr(C)]
pub struct CTaskCloud {
/// 0x00β0x2F: CTask base
pub base: CTask<*const CTaskCloudVTable>,
/// 0x30: Parallax scroll layer depth (Fixed; starts at 0x190000 = 25.0,
/// decrements by 1 each cloud spawned in a batch)
pub layer_depth: Fixed,
/// 0x34: Per-frame animation phase counter (Fixed 16.16); incremented by
/// `phase_speed` each FrameFinish. Passed to `blit_sprite` as the
/// `palette` arg (cmd[5]) β acts as a per-cloud animation/palette index.
/// Constructor initializes it to `(pos_x + render_y) & 0xFFFF`, which is
/// always 0 since both addends have zero low halves.
pub anim_phase: Fixed,
/// 0x38: Animation phase speed (Fixed 16.16; set by cloud type: large=0x200, medium=0x166, small=0xCC)
pub phase_speed: Fixed,
/// 0x3C: Sprite ID passed to DrawSpriteLocal (0x268=large, 0x269=medium, 0x26A=small)
pub sprite_id: u32,
/// 0x40: X position (Fixed 16.16); wraps at landscape bounds each frame
pub pos_x: Fixed,
/// 0x44: Rendered Y position (Fixed 16.16, integer part in upper 16 bits).
/// EDI register from constructor call site. In CreateWeatherFilter this is
/// `(level_height/16 + level_height/10 * i/cloud_count + weather_mod) << 16`,
/// placing each cloud at a small Y near the top of the level.
///
/// Despite contributing to the initial `anim_phase` computation
/// `(pos_x + render_y) & 0xFFFF` in the constructor, that mask discards the
/// integer part and only ever yields 0 (both `pos_x` and `render_y` have
/// their lower 16 bits zero). The integer Y is read here in HandleMessage
/// and passed to `blit_sprite` as the sprite's screen Y.
pub render_y: Fixed,
/// 0x48: X velocity base (Fixed 16.16)
pub vel_x: Fixed,
/// 0x4C: Current wind acceleration (Fixed); converges toward wind_target each frame
pub wind_accel: Fixed,
/// 0x50: Target wind speed (Fixed); set by message 0x54 (wind-change event)
pub wind_target: Fixed,
/// 0x54β0x73: Unused padding. Allocation is 0x74 bytes but only 0x54 bytes are
/// initialized (memset in CreateWeatherFilter). Not touched by constructor,
/// HandleMessage, WriteReplayState, or ReadReplayState.
pub _unknown_54: [u8; 0x20],
}
const _: () = assert!(core::mem::size_of::<CTaskCloud>() == 0x74);
// Generate typed vtable method wrappers: write_replay_state(), handle_message().
bind_CTaskCloudVTable!(CTaskCloud, base.vtable);
use crate::game::TaskMessage;
use crate::render::message::RenderMessage;
use crate::render::sprite::sprite_op::SpriteOp;
/// CTaskCloud::HandleMessage replacement β pure game logic.
///
/// Handles three message types:
/// - FrameFinish: per-frame position update (parallax scroll with wind drift)
/// - RenderScene: draw sprite at computed position
/// - SetWind: set wind target from message data
///
/// Always calls base CTask::HandleMessage at the end (broadcast to children).
pub unsafe extern "thiscall" fn cloud_handle_message(
this: *mut CTaskCloud,
sender: *mut CTask,
msg_type: u32,
size: u32,
data: *const u8,
) {
let msg = TaskMessage::try_from(msg_type);
match msg {
Ok(TaskMessage::FrameFinish) => {
// Advance Y position
(*this).anim_phase = Fixed((*this).anim_phase.0 + (*this).phase_speed.0);
// Advance X position: base velocity + wind * 10
let wind = (*this).wind_accel.0;
(*this).pos_x = Fixed((*this).pos_x.0 + (*this).vel_x.0 + wind * 10);
// Wrap X at landscape bounds (with 128.0 Fixed padding)
let ddgame = CTask::ddgame_raw(this as *const CTask);
let padding = Fixed::from_int(128);
let level_left = (*ddgame).level_bound_min_x - padding;
let level_right = (*ddgame).level_bound_max_x + padding;
if (*this).pos_x < level_left {
(*this).pos_x = level_right;
} else if (*this).pos_x > level_right {
(*this).pos_x = level_left;
}
// Converge wind_accel toward wind_target (clamp step to Β±0x147)
let target = (*this).wind_target.0;
let current = (*this).wind_accel.0;
let diff = target - current;
if diff.abs() < 0x147 {
(*this).wind_accel = Fixed(target);
} else if current < target {
(*this).wind_accel = Fixed(current + 0x147);
} else {
(*this).wind_accel = Fixed(current - 0x147);
}
}
Ok(TaskMessage::RenderScene) => {
let ddgame = CTask::ddgame_raw(this as *const CTask);
// Only render when rendering phase == 5 (in-game rendering active)
if (*ddgame).render_phase == 5 {
// Compute parallax X offset: (vel_x + wind * 10) * parallax_scale
let scroll_speed = (*this).vel_x.0 + (*this).wind_accel.0 * 10;
let parallax_x =
((scroll_speed as i64 * (*ddgame).parallax_scale as i64) >> 16) as i32;
let x = parallax_x + (*this).pos_x.0;
let rq = &mut *(*ddgame).render_queue;
// Original (0x548527..0x54852f) loads `[ESI+0x44]` (render_y)
// into EAX as the usercall Y register, and pushes `anim_phase`
// (`[ESI+0x34]`) as the trailing stack arg that becomes
// `blit_sprite`'s `palette` parameter.
let _ = rq.push_typed(
(*this).layer_depth.0 as u32,
RenderMessage::Sprite {
local: true,
x: Fixed(x).floor(),
y: (*this).render_y.floor(),
sprite: SpriteOp((*this).sprite_id),
palette: (*this).anim_phase.0 as u32,
},
);
}
}
Ok(TaskMessage::SetWind) => {
if !data.is_null() {
(*this).wind_target = Fixed(*(data as *const i32));
}
}
_ => {}
}
// Broadcast to children β raw-pointer version avoids noalias UB
CTask::broadcast_message_raw(this as *mut CTask, sender, msg_type, size, data);
}
/// Cloud type determines sprite and vertical velocity.
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CloudType {
/// Large cloud: sprite 0x268, phase_speed 0x200
Large = 0,
/// Medium cloud: sprite 0x269, phase_speed 0x166
Medium = 1,
/// Small cloud: sprite 0x26A, phase_speed 0xCC
Small = 2,
}
impl CTaskCloud {
/// Initialize CTaskCloud fields on an already-constructed CTask base.
///
/// Pure Rust equivalent of the original constructor at 0x5482E0.
/// The caller must have already:
/// 1. Allocated 0x74 bytes (e.g. via `wa_malloc(0x74)`)
/// 2. Zeroed the first 0x54 bytes (matches original `_memset(ptr, 0, 0x54)`)
/// 3. Called `CTask::Constructor` (0x5625A0) to set up the base task
///
/// This function then sets the vtable, class type, and all cloud-specific fields.
///
/// # Safety
/// `this` must point to a valid, allocated CTaskCloud with CTask base initialized.
pub unsafe fn init(
this: *mut CTaskCloud,
cloud_type: CloudType,
layer_depth: Fixed,
pos_x: Fixed,
vel_x: Fixed,
render_y: Fixed,
) {
use crate::rebase::rb;
// Set vtable pointer to the CTaskCloud vtable (rebased for ASLR)
(*this).base.vtable = rb(CTASK_CLOUD_VTABLE) as *const CTaskCloudVTable;
(*this).base.class_type = ClassType::Cloud;
// Position: x is the initial horizontal position. The original computes
// `anim_phase = (pos_x + render_y) & 0xFFFF`, but both pos_x and render_y
// have their lower 16 bits zero, so the result is always 0.
(*this).pos_x = pos_x;
(*this).anim_phase = Fixed((pos_x.0.wrapping_add(render_y.0)) & 0xFFFF);
(*this).layer_depth = layer_depth;
(*this).render_y = render_y;
(*this).vel_x = vel_x;
(*this).wind_accel = Fixed(0);
(*this).wind_target = Fixed(0);
// Set type-dependent velocity and sprite
match cloud_type {
CloudType::Large => {
(*this).phase_speed = Fixed(0x200);
(*this).sprite_id = 0x268;
}
CloudType::Medium => {
(*this).phase_speed = Fixed(0x166);
(*this).sprite_id = 0x269;
}
CloudType::Small => {
(*this).phase_speed = Fixed(0xCC);
(*this).sprite_id = 0x26A;
}
}
}
}
|