paavohtl Claude Opus 4.6 (1M context) commited on
Commit
6b181ec
·
1 Parent(s): a683d3d

fix: correct DDGame constructor — allocation size, arrow collision params

Browse files

Three constructor bugs found and fixed:

1. Allocation size 0x98B8 → 0x98D8: original malloc(0x98D8) but memset(0x98B8).
Our code allocated only 0x98B8, causing 32-byte heap overrun.

2. Wrong function after InitPaletteGradientSprites: called LoadingProgressTick
(0x5717A0) instead of DisplayGfx__InitTeamPaletteDisplayObjects (0x5703E0).

3. Arrow collision region SpriteRegion params were all wrong:
- ECX should be sprite_w/2 (was 0)
- EDX should be sprite_h - margin_h (was margin_h)
- p2/p3 should be margin values (were 0)
- p4 should be sprite_w - margin_w (was margin_w)
- p5 should be sprite_h/2 (was margin_h)
- p6 should be arrow sprite ptr (was landscape gfx_resource)

This was the root cause of the longbow replay desync — wrong collision
boxes caused different terrain collision detection and RNG divergence.

Found via sub-object hashing: comparing pointer-canonicalized hashes of
objects pointed to by DDGame between Rust and original constructors.

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

crates/openwa-core/src/address.rs CHANGED
@@ -215,6 +215,11 @@ pub mod va {
215
 
216
  // === Gameplay functions ===
217
 
 
 
 
 
 
218
  pub const CREATE_EXPLOSION: u32 = 0x0054_8080;
219
  pub const SPECIAL_IMPACT: u32 = 0x0051_93D0;
220
  pub const SPAWN_OBJECT: u32 = 0x0056_1CF0;
@@ -379,6 +384,11 @@ pub mod va {
379
  /// girder sprites, and creates DDGame+0x37C DisplayGfx object.
380
  /// thiscall(ECX=gfx_dir_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8.
381
  pub const DDGAME_LOAD_HUD_AND_WEAPON_SPRITES: u32 = 0x0053_D0E0;
 
 
 
 
 
382
  /// DDGame__InitPaletteGradientSprites (0x5706D0): creates DisplayGfx objects
383
  /// at DDGame+0x41C.. for each team's palette gradient data from GameInfo.
384
  /// stdcall(wrapper), RET 0x4.
 
215
 
216
  // === Gameplay functions ===
217
 
218
+ /// Game PRNG: rng = (rng + frame_counter) * 0x19660D + 0x3C6EF35F.
219
+ /// Fastcall(ECX=DDGame). Updates DDGame+0x45EC in place.
220
+ pub const ADVANCE_GAME_RNG: u32 = 0x0053_F320;
221
+ /// Terrain hit → debris particles → RNG. 6 iterations, 2 RNG calls per hit.
222
+ pub const GENERATE_DEBRIS_PARTICLES: u32 = 0x0054_6F70;
223
  pub const CREATE_EXPLOSION: u32 = 0x0054_8080;
224
  pub const SPECIAL_IMPACT: u32 = 0x0051_93D0;
225
  pub const SPAWN_OBJECT: u32 = 0x0056_1CF0;
 
384
  /// girder sprites, and creates DDGame+0x37C DisplayGfx object.
385
  /// thiscall(ECX=gfx_dir_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8.
386
  pub const DDGAME_LOAD_HUD_AND_WEAPON_SPRITES: u32 = 0x0053_D0E0;
387
+ /// DisplayGfx__InitTeamPaletteDisplayObjects (0x5703E0): creates team palette
388
+ /// gradient DisplayGfx objects. Reads DDGame+0x7338 (fill_pixel), iterates teams,
389
+ /// creates BitGrid+DisplayGfx for each team's palette gradient.
390
+ /// stdcall(wrapper), RET 0x4.
391
+ pub const DISPLAY_GFX_INIT_TEAM_PALETTE_DISPLAY: u32 = 0x0057_03E0;
392
  /// DDGame__InitPaletteGradientSprites (0x5706D0): creates DisplayGfx objects
393
  /// at DDGame+0x41C.. for each team's palette gradient data from GameInfo.
394
  /// stdcall(wrapper), RET 0x4.
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -25,7 +25,7 @@ use crate::wa_alloc::{wa_malloc, wa_malloc_zeroed};
25
 
26
  /// DDGame — the main game engine object.
27
  ///
28
- /// This is a massive ~39KB struct (0x98B8 bytes) that owns all major subsystems:
29
  /// display, landscape, sound, graphics handlers, and task state machines.
30
  ///
31
  /// Allocated in DDGame__Constructor (0x56E220).
@@ -367,11 +367,14 @@ pub struct DDGame {
367
  /// This is the same flag toggled by spacebar (key 0x35) during replay.
368
  pub fast_forward_active: u32,
369
 
370
- /// 0x98B4-0x98B7: Unknown
371
- pub _unknown_98b4: [u8; 0x98B8 - 0x98B4],
 
 
 
372
  }
373
 
374
- const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
375
 
376
  // ============================================================
377
  // DDGame constructor — replaces DDGame__Constructor (0x56E220)
@@ -396,7 +399,7 @@ const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
396
  /// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx.
397
  ///
398
  /// # Safety
399
- /// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98B8 bytes).
400
  pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
401
  let base = ddgame as *mut u8;
402
 
@@ -645,7 +648,7 @@ pub static mut ON_DDGAME_ALLOC: Option<unsafe fn(*mut u8)> = None;
645
 
646
  /// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
647
  ///
648
- /// Allocates 0x98B8 bytes from WA's heap, initializes all fields, and creates
649
  /// sub-objects. Populates fields on both `wrapper` (DDGameWrapper) and the
650
  /// returned DDGame.
651
  ///
@@ -665,8 +668,10 @@ pub unsafe fn create_ddgame(
665
  game_info: *mut GameInfo,
666
  network_ecx: u32, // implicit ECX from caller
667
  ) -> *mut DDGame {
668
- // ── 1. Allocate and zero-fill (matches: memset(piVar3, 0, 0x98B8)) ──
669
- let ddgame = wa_malloc_zeroed(0x98B8) as *mut DDGame;
 
 
670
  if ddgame.is_null() {
671
  return core::ptr::null_mut();
672
  }
@@ -1108,29 +1113,30 @@ unsafe fn init_graphics_and_resources(
1108
  // Store arrow sprite at DDGame+0x38+i*4
1109
  (*ddgame).arrow_sprites[i as usize] = sprite;
1110
 
1111
- // Calculate collision region dimensions from sprite
 
 
 
 
1112
  if !sprite.is_null() {
1113
  let tsm = &*(sprite as *const BitGrid);
1114
  let sprite_w = tsm.width as i32;
1115
  let sprite_h = tsm.height as i32;
1116
- let half_w = (sprite_w / 2 - 10).max(0);
1117
- let half_h = (sprite_h / 2 - 10).max(0);
1118
 
1119
  // Create SpriteRegion for collision
1120
  let alloc = wa_malloc_zeroed(0x9C);
1121
  let region = if !alloc.is_null() {
1122
- // SpriteRegion params: ECX, EDX, this, p2, p3, p4, p5, p6
1123
- // this[3] = p4 - p2 (width), this[4] = EDX - p3 (height)
1124
- // For arrows: region from (0,0) to (half_w, half_h)
1125
  call_sprite_region_ctor(
1126
  alloc,
1127
- 0, // ECX (x_max for this[5])
1128
- half_h as u32, // EDX (y_max → this[4] = EDX - p3)
1129
- 0, // p2 (x_offset)
1130
- 0, // p3 (y_offset)
1131
- half_w as u32, // p4 (x_limit → this[3] = p4 - p2)
1132
- half_h as u32, // p5 (y_limit for this[6])
1133
- gfx_resource as u32, // p6 (gfx_resource)
1134
  )
1135
  } else {
1136
  core::ptr::null_mut()
@@ -1333,8 +1339,13 @@ unsafe fn init_graphics_and_resources(
1333
  // gradient data from GameInfo. stdcall(wrapper), RET 0x4.
1334
  wa_init_palette_gradient_sprites(wrapper);
1335
 
1336
- // ── Loading progress tick (1 of 2 — after InitPaletteGradientSprites) ──
1337
- call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
 
 
 
 
 
1338
  }
1339
  // ── Gradient image stub (DDGame+0x30) ──
1340
  // Minimal stub: [6]=0 (zero-width) so CTaskLand skips the gradient column loop.
 
25
 
26
  /// DDGame — the main game engine object.
27
  ///
28
+ /// This is a massive ~39KB struct (0x98D8 bytes) that owns all major subsystems:
29
  /// display, landscape, sound, graphics handlers, and task state machines.
30
  ///
31
  /// Allocated in DDGame__Constructor (0x56E220).
 
367
  /// This is the same flag toggled by spacebar (key 0x35) during replay.
368
  pub fast_forward_active: u32,
369
 
370
+ /// 0x98B4-0x98D7: Unknown tail region.
371
+ /// The original constructor allocates 0x98D8 bytes but memsets only 0x98B8.
372
+ /// The remaining 0x20 bytes are not zeroed by the initial memset but may be
373
+ /// initialized by subsequent constructor steps or game code.
374
+ pub _unknown_98b4: [u8; 0x98D8 - 0x98B4],
375
  }
376
 
377
+ const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98D8);
378
 
379
  // ============================================================
380
  // DDGame constructor — replaces DDGame__Constructor (0x56E220)
 
399
  /// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx.
400
  ///
401
  /// # Safety
402
+ /// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98D8 bytes).
403
  pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
404
  let base = ddgame as *mut u8;
405
 
 
648
 
649
  /// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
650
  ///
651
+ /// Allocates 0x98D8 bytes from WA's heap, initializes all fields, and creates
652
  /// sub-objects. Populates fields on both `wrapper` (DDGameWrapper) and the
653
  /// returned DDGame.
654
  ///
 
668
  game_info: *mut GameInfo,
669
  network_ecx: u32, // implicit ECX from caller
670
  ) -> *mut DDGame {
671
+ // ── 1. Allocate and zero-fill ──
672
+ // Original: malloc(0x98D8), memset(ptr, 0, 0x98B8) — last 0x20 bytes not zeroed.
673
+ // We zero the full 0x98D8 for safety (strictly more initialization than original).
674
+ let ddgame = wa_malloc_zeroed(0x98D8) as *mut DDGame;
675
  if ddgame.is_null() {
676
  return core::ptr::null_mut();
677
  }
 
1113
  // Store arrow sprite at DDGame+0x38+i*4
1114
  (*ddgame).arrow_sprites[i as usize] = sprite;
1115
 
1116
+ // Calculate collision region dimensions from sprite.
1117
+ // The original creates a CENTERED collision box with 10px margin:
1118
+ // left/top margin = max(0, dim/2 - 10)
1119
+ // right/bottom = dim - margin
1120
+ // Result: this[5]=this[6]=10 for sprites >20px (the 10px inset).
1121
  if !sprite.is_null() {
1122
  let tsm = &*(sprite as *const BitGrid);
1123
  let sprite_w = tsm.width as i32;
1124
  let sprite_h = tsm.height as i32;
1125
+ let margin_w = (sprite_w / 2 - 10).max(0);
1126
+ let margin_h = (sprite_h / 2 - 10).max(0);
1127
 
1128
  // Create SpriteRegion for collision
1129
  let alloc = wa_malloc_zeroed(0x9C);
1130
  let region = if !alloc.is_null() {
 
 
 
1131
  call_sprite_region_ctor(
1132
  alloc,
1133
+ (sprite_w / 2) as u32, // ECX → this[5] = ECX - p2
1134
+ (sprite_h - margin_h) as u32, // EDX → this[4] = EDX - p3
1135
+ margin_w as u32, // p2 (left margin)
1136
+ margin_h as u32, // p3 (top margin)
1137
+ (sprite_w - margin_w) as u32, // p4 → this[3] = p4 - p2
1138
+ (sprite_h / 2) as u32, // p5 → this[6] = p5 - p3
1139
+ sprite as u32, // p6 = arrow sprite (NOT landscape gfx_resource)
1140
  )
1141
  } else {
1142
  core::ptr::null_mut()
 
1339
  // gradient data from GameInfo. stdcall(wrapper), RET 0x4.
1340
  wa_init_palette_gradient_sprites(wrapper);
1341
 
1342
+ // ── DisplayGfx__InitTeamPaletteDisplayObjects (0x5703E0) ──
1343
+ // Creates team palette gradient display objects. Reads DDGame+0x7338
1344
+ // (fill_pixel), creates BitGrid+DisplayGfx per team.
1345
+ // stdcall(wrapper), RET 0x4.
1346
+ let init_team_palette_display: unsafe extern "stdcall" fn(*mut DDGameWrapper) =
1347
+ core::mem::transmute(rb(va::DISPLAY_GFX_INIT_TEAM_PALETTE_DISPLAY));
1348
+ init_team_palette_display(wrapper);
1349
  }
1350
  // ── Gradient image stub (DDGame+0x30) ──
1351
  // Minimal stub: [6]=0 (zero-width) so CTaskLand skips the gradient column loop.
crates/openwa-wormkit/src/debug_watchpoint.rs CHANGED
@@ -33,9 +33,8 @@ use windows_sys::Win32::System::Diagnostics::Debug::{
33
  /// Offsets to watch. Hardware limit: 4 watchpoints (DR0–DR3).
34
  /// Change these to investigate different fields.
35
  /// NOTE: The base pointer is set by `on_ddgame_alloc()` — can be DDGame or DDGameWrapper.
36
- const WATCH_OFFSETS: [(u32, &str); 2] = [
37
- (0x09C0, "display+0x09C0"),
38
- (0x353C, "display+0x353C"),
39
  ];
40
 
41
  /// DDGame base address (set when allocation is reported).
 
33
  /// Offsets to watch. Hardware limit: 4 watchpoints (DR0–DR3).
34
  /// Change these to investigate different fields.
35
  /// NOTE: The base pointer is set by `on_ddgame_alloc()` — can be DDGame or DDGameWrapper.
36
+ const WATCH_OFFSETS: [(u32, &str); 1] = [
37
+ (0x3A40, "display+0x3A40"),
 
38
  ];
39
 
40
  /// DDGame base address (set when allocation is reported).