fix: correct DDGame constructor — allocation size, arrow collision params
Browse filesThree 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>
|
@@ -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.
|
|
@@ -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 (
|
| 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-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
| 372 |
}
|
| 373 |
|
| 374 |
-
const _: () = assert!(core::mem::size_of::<DDGame>() ==
|
| 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 (
|
| 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
|
| 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
|
| 669 |
-
|
|
|
|
|
|
|
| 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
|
| 1117 |
-
let
|
| 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 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 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 |
-
// ──
|
| 1337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
@@ -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);
|
| 37 |
-
(
|
| 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).
|