paavohtl Claude Opus 4.6 (1M context) commited on
Commit
2b88223
·
1 Parent(s): 59bfee7

fix(cloud): use field 0x44 as render Y, not field 0x34

Browse files

CTaskCloud::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=y_seed,
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=y_seed).
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 y_seed
56
- /// (cloud count) contributes to the initial pos_y via `(pos_x + y_seed) & 0xFFFF`,
57
- /// placing clouds near y ≈ 0 so they drift downward.
 
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: Y position (Fixed 16.16); updated each frame: pos_y += vel_y.
70
- /// Initial value: (pos_x + y_seed) & 0xFFFF (near zero, clouds drift downward).
 
 
 
 
 
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 seed value — EDI register from constructor call site.
79
- /// In CreateWeatherFilter this is the total cloud count (e.g. 10).
80
- /// Used to compute initial pos_y: (pos_x + y_seed) & 0xFFFF.
81
- /// Serialized in replay state but not read during gameplay.
82
- pub y_seed: u32,
 
 
 
 
 
 
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).pos_y.0 as u32 & 0xFFFF0000,
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
- y_seed: u32,
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, y is derived from x + seed
 
 
232
  (*this).pos_x = pos_x;
233
- (*this).pos_y = Fixed((pos_x.0.wrapping_add(y_seed as i32)) & 0xFFFF);
234
  (*this).layer_depth = layer_depth;
235
- (*this).y_seed = y_seed;
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
- // 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;
@@ -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
- y_seed,
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