fix(cloud): use field 0x44 as render Y, not field 0x34
Browse filesCTaskCloud::HandleMessage RenderScene was sending `pos_y` (field 0x34)
as the sprite Y, which the constructor masks to `(pos_x + render_y) &
0xFFFF` — always 0 — pinning every cloud to the very top of the level.
The original at 0x548527..0x54852f loads `[ESI+0x44]` into EAX (the
usercall Y register for RQ_DrawSpriteLocal) and pushes `[ESI+0x34]` as
the trailing stack arg, which lands in the queue command's `palette`
slot and acts as a per-cloud animation/palette index incremented each
frame by `vel_y`.
Rename the misnamed `y_seed: u32` to `render_y: Fixed`, swap the two
values in the DrawSpriteCmd write, and correct the field docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/task/cloud.rs
CHANGED
|
@@ -7,7 +7,7 @@ crate::define_addresses! {
|
|
| 7 |
class "CTaskCloud" {
|
| 8 |
/// CTaskCloud vtable - cloud/airstrike entity
|
| 9 |
vtable CTASK_CLOUD_VTABLE = 0x0066_9D38;
|
| 10 |
-
/// CTaskCloud constructor (usercall: ESI=this, EAX=parent, EDI=
|
| 11 |
/// stack: cloud_type, layer_depth, pos_x, vel_x). RET 0x10.
|
| 12 |
ctor CTASK_CLOUD_CTOR = 0x0054_82E0;
|
| 13 |
/// CTaskCloud::WriteReplayState — serializes cloud state to replay stream.
|
|
@@ -43,7 +43,7 @@ pub struct CTaskCloudVTable {
|
|
| 43 |
/// scroll on a parallax layer, and render as a single sprite.
|
| 44 |
///
|
| 45 |
/// Allocation: 0x74 bytes (operator new in CTaskTeam__CreateWeatherFilter 0x552960).
|
| 46 |
-
/// Constructor: 0x5482E0 (usercall ESI=this, EAX=parent, EDI=
|
| 47 |
/// Vtable: 0x669D38. Class type byte: 0x17 (ClassType::Cloud).
|
| 48 |
///
|
| 49 |
/// Three cloud sizes chosen by `cloud_type` param (0/1/2):
|
|
@@ -52,9 +52,10 @@ pub struct CTaskCloudVTable {
|
|
| 52 |
/// - type 2: sprite 0x26A (small), vel_y 0xCC
|
| 53 |
///
|
| 54 |
/// CreateWeatherFilter spawns clouds with a deterministic LCG (seed 0x12345678),
|
| 55 |
-
/// randomizing pos_x within level bounds and vel_x with random sign. The
|
| 56 |
-
///
|
| 57 |
-
///
|
|
|
|
| 58 |
///
|
| 59 |
/// Source: Ghidra decompilation of 0x5482E0 (constructor), 0x5484C0 (HandleMessage),
|
| 60 |
/// 0x548430 (WriteReplayState), 0x548370 (ReadReplayState).
|
|
@@ -66,8 +67,13 @@ pub struct CTaskCloud {
|
|
| 66 |
/// 0x30: Parallax scroll layer depth (Fixed; starts at 0x190000 = 25.0,
|
| 67 |
/// decrements by 1 each cloud spawned in a batch)
|
| 68 |
pub layer_depth: Fixed,
|
| 69 |
-
/// 0x34:
|
| 70 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
pub pos_y: Fixed,
|
| 72 |
/// 0x38: Y velocity (Fixed 16.16; set by cloud type: large=0x200, medium=0x166, small=0xCC)
|
| 73 |
pub vel_y: Fixed,
|
|
@@ -75,11 +81,17 @@ pub struct CTaskCloud {
|
|
| 75 |
pub sprite_id: u32,
|
| 76 |
/// 0x40: X position (Fixed 16.16); wraps at landscape bounds each frame
|
| 77 |
pub pos_x: Fixed,
|
| 78 |
-
/// 0x44: Y
|
| 79 |
-
///
|
| 80 |
-
///
|
| 81 |
-
///
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
/// 0x48: X velocity base (Fixed 16.16)
|
| 84 |
pub vel_x: Fixed,
|
| 85 |
/// 0x4C: Current wind acceleration (Fixed); converges toward wind_target each frame
|
|
@@ -164,13 +176,18 @@ pub unsafe extern "thiscall" fn cloud_handle_message(
|
|
| 164 |
|
| 165 |
let rq = &mut *(*ddgame).render_queue;
|
| 166 |
if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
*entry = DrawSpriteCmd {
|
| 168 |
command_type: command_type::DRAW_SPRITE_LOCAL,
|
| 169 |
layer: (*this).layer_depth.0 as u32,
|
| 170 |
x_pos: x as u32 & 0xFFFF0000,
|
| 171 |
-
y_pos: (*this).
|
| 172 |
sprite_id: (*this).sprite_id,
|
| 173 |
-
frame: 0,
|
| 174 |
};
|
| 175 |
}
|
| 176 |
}
|
|
@@ -220,7 +237,7 @@ impl CTaskCloud {
|
|
| 220 |
layer_depth: Fixed,
|
| 221 |
pos_x: Fixed,
|
| 222 |
vel_x: Fixed,
|
| 223 |
-
|
| 224 |
) {
|
| 225 |
use crate::rebase::rb;
|
| 226 |
|
|
@@ -228,11 +245,13 @@ impl CTaskCloud {
|
|
| 228 |
(*this).base.vtable = rb(CTASK_CLOUD_VTABLE) as *const CTaskCloudVTable;
|
| 229 |
(*this).base.class_type = ClassType::Cloud;
|
| 230 |
|
| 231 |
-
// Position: x is the initial horizontal position
|
|
|
|
|
|
|
| 232 |
(*this).pos_x = pos_x;
|
| 233 |
-
(*this).pos_y = Fixed((pos_x.0.wrapping_add(
|
| 234 |
(*this).layer_depth = layer_depth;
|
| 235 |
-
(*this).
|
| 236 |
(*this).vel_x = vel_x;
|
| 237 |
(*this).wind_accel = Fixed(0);
|
| 238 |
(*this).wind_target = Fixed(0);
|
|
|
|
| 7 |
class "CTaskCloud" {
|
| 8 |
/// CTaskCloud vtable - cloud/airstrike entity
|
| 9 |
vtable CTASK_CLOUD_VTABLE = 0x0066_9D38;
|
| 10 |
+
/// CTaskCloud constructor (usercall: ESI=this, EAX=parent, EDI=render_y,
|
| 11 |
/// stack: cloud_type, layer_depth, pos_x, vel_x). RET 0x10.
|
| 12 |
ctor CTASK_CLOUD_CTOR = 0x0054_82E0;
|
| 13 |
/// CTaskCloud::WriteReplayState — serializes cloud state to replay stream.
|
|
|
|
| 43 |
/// scroll on a parallax layer, and render as a single sprite.
|
| 44 |
///
|
| 45 |
/// Allocation: 0x74 bytes (operator new in CTaskTeam__CreateWeatherFilter 0x552960).
|
| 46 |
+
/// Constructor: 0x5482E0 (usercall ESI=this, EAX=parent, EDI=render_y).
|
| 47 |
/// Vtable: 0x669D38. Class type byte: 0x17 (ClassType::Cloud).
|
| 48 |
///
|
| 49 |
/// Three cloud sizes chosen by `cloud_type` param (0/1/2):
|
|
|
|
| 52 |
/// - type 2: sprite 0x26A (small), vel_y 0xCC
|
| 53 |
///
|
| 54 |
/// CreateWeatherFilter spawns clouds with a deterministic LCG (seed 0x12345678),
|
| 55 |
+
/// randomizing pos_x within level bounds and vel_x with random sign. The
|
| 56 |
+
/// `render_y` field encodes the cloud's vertical screen position as Fixed16
|
| 57 |
+
/// (`level_height/16 + scaled_offset` per cloud index) and is passed to
|
| 58 |
+
/// `RQ_DrawSpriteLocal` as the sprite Y at render time.
|
| 59 |
///
|
| 60 |
/// Source: Ghidra decompilation of 0x5482E0 (constructor), 0x5484C0 (HandleMessage),
|
| 61 |
/// 0x548430 (WriteReplayState), 0x548370 (ReadReplayState).
|
|
|
|
| 67 |
/// 0x30: Parallax scroll layer depth (Fixed; starts at 0x190000 = 25.0,
|
| 68 |
/// decrements by 1 each cloud spawned in a batch)
|
| 69 |
pub layer_depth: Fixed,
|
| 70 |
+
/// 0x34: Per-frame phase counter (Fixed 16.16); incremented by `vel_y`
|
| 71 |
+
/// each FrameFinish. Despite the name, this is **not** the rendered Y —
|
| 72 |
+
/// `render_y` is. The original passes this value as the trailing stack
|
| 73 |
+
/// arg to `RQ_DrawSpriteLocal`, where it ends up in the queue command's
|
| 74 |
+
/// `palette` slot (cmd[5]) as a per-cloud animation/palette index.
|
| 75 |
+
/// Constructor initializes it to `(pos_x + render_y) & 0xFFFF`, which is
|
| 76 |
+
/// always 0 since both addends have zero low halves.
|
| 77 |
pub pos_y: Fixed,
|
| 78 |
/// 0x38: Y velocity (Fixed 16.16; set by cloud type: large=0x200, medium=0x166, small=0xCC)
|
| 79 |
pub vel_y: Fixed,
|
|
|
|
| 81 |
pub sprite_id: u32,
|
| 82 |
/// 0x40: X position (Fixed 16.16); wraps at landscape bounds each frame
|
| 83 |
pub pos_x: Fixed,
|
| 84 |
+
/// 0x44: Rendered Y position (Fixed 16.16, integer part in upper 16 bits).
|
| 85 |
+
/// EDI register from constructor call site. In CreateWeatherFilter this is
|
| 86 |
+
/// `(level_height/16 + level_height/10 * i/cloud_count + weather_mod) << 16`,
|
| 87 |
+
/// placing each cloud at a small Y near the top of the level.
|
| 88 |
+
///
|
| 89 |
+
/// Despite contributing to the initial `pos_y` computation
|
| 90 |
+
/// `(pos_x + render_y) & 0xFFFF` in the constructor, that mask discards the
|
| 91 |
+
/// integer part and only ever yields 0 (both `pos_x` and `render_y` have
|
| 92 |
+
/// their lower 16 bits zero). The integer Y is read here in HandleMessage
|
| 93 |
+
/// and passed to `RQ_DrawSpriteLocal` as the sprite's screen Y.
|
| 94 |
+
pub render_y: Fixed,
|
| 95 |
/// 0x48: X velocity base (Fixed 16.16)
|
| 96 |
pub vel_x: Fixed,
|
| 97 |
/// 0x4C: Current wind acceleration (Fixed); converges toward wind_target each frame
|
|
|
|
| 176 |
|
| 177 |
let rq = &mut *(*ddgame).render_queue;
|
| 178 |
if let Some(entry) = rq.alloc::<DrawSpriteCmd>() {
|
| 179 |
+
// Original (0x548527..0x54852f) loads `[ESI+0x44]` (render_y)
|
| 180 |
+
// into EAX as the usercall Y register, and pushes `pos_y`
|
| 181 |
+
// (`[ESI+0x34]`) as the trailing stack arg that becomes the
|
| 182 |
+
// queue entry's `palette` field. `pos_y` grows monotonically
|
| 183 |
+
// each frame and acts as a per-cloud animation/palette index.
|
| 184 |
*entry = DrawSpriteCmd {
|
| 185 |
command_type: command_type::DRAW_SPRITE_LOCAL,
|
| 186 |
layer: (*this).layer_depth.0 as u32,
|
| 187 |
x_pos: x as u32 & 0xFFFF0000,
|
| 188 |
+
y_pos: (*this).render_y.0 as u32 & 0xFFFF0000,
|
| 189 |
sprite_id: (*this).sprite_id,
|
| 190 |
+
frame: (*this).pos_y.0 as u32,
|
| 191 |
};
|
| 192 |
}
|
| 193 |
}
|
|
|
|
| 237 |
layer_depth: Fixed,
|
| 238 |
pos_x: Fixed,
|
| 239 |
vel_x: Fixed,
|
| 240 |
+
render_y: Fixed,
|
| 241 |
) {
|
| 242 |
use crate::rebase::rb;
|
| 243 |
|
|
|
|
| 245 |
(*this).base.vtable = rb(CTASK_CLOUD_VTABLE) as *const CTaskCloudVTable;
|
| 246 |
(*this).base.class_type = ClassType::Cloud;
|
| 247 |
|
| 248 |
+
// Position: x is the initial horizontal position. The original computes
|
| 249 |
+
// `pos_y = (pos_x + render_y) & 0xFFFF`, but both pos_x and render_y
|
| 250 |
+
// have their lower 16 bits zero, so the result is always 0.
|
| 251 |
(*this).pos_x = pos_x;
|
| 252 |
+
(*this).pos_y = Fixed((pos_x.0.wrapping_add(render_y.0)) & 0xFFFF);
|
| 253 |
(*this).layer_depth = layer_depth;
|
| 254 |
+
(*this).render_y = render_y;
|
| 255 |
(*this).vel_x = vel_x;
|
| 256 |
(*this).wind_accel = Fixed(0);
|
| 257 |
(*this).wind_target = Fixed(0);
|
crates/openwa-core/src/task/team.rs
CHANGED
|
@@ -288,9 +288,10 @@ pub unsafe extern "stdcall" fn create_weather_filter(parent: *mut CTask) {
|
|
| 288 |
|
| 289 |
let pos_x_int = rand_frac % level_width_int + level_min_x_int;
|
| 290 |
|
| 291 |
-
//
|
|
|
|
| 292 |
let y_offset = height_tenth * i / cloud_count;
|
| 293 |
-
let
|
| 294 |
|
| 295 |
// X velocity: random magnitude + 0x8000, random sign
|
| 296 |
let vel_x_base = (rng_state & 0xFFFF) as i32 + 0x8000;
|
|
@@ -318,7 +319,7 @@ pub unsafe extern "stdcall" fn create_weather_filter(parent: *mut CTask) {
|
|
| 318 |
Fixed(layer_depth),
|
| 319 |
Fixed(pos_x_int << 16),
|
| 320 |
vel_x,
|
| 321 |
-
|
| 322 |
);
|
| 323 |
// ownership transfers to the task tree
|
| 324 |
|
|
|
|
| 288 |
|
| 289 |
let pos_x_int = rand_frac % level_width_int + level_min_x_int;
|
| 290 |
|
| 291 |
+
// Render Y: height/16 + scaled offset + weather modifier, in Fixed16.16
|
| 292 |
+
// (integer pixel value placed in the upper 16 bits).
|
| 293 |
let y_offset = height_tenth * i / cloud_count;
|
| 294 |
+
let render_y = Fixed((height_contrib + y_offset + weather_mod) << 16);
|
| 295 |
|
| 296 |
// X velocity: random magnitude + 0x8000, random sign
|
| 297 |
let vel_x_base = (rng_state & 0xFFFF) as i32 + 0x8000;
|
|
|
|
| 319 |
Fixed(layer_depth),
|
| 320 |
Fixed(pos_x_int << 16),
|
| 321 |
vel_x,
|
| 322 |
+
render_y,
|
| 323 |
);
|
| 324 |
// ownership transfers to the task tree
|
| 325 |
|