feat: add runtime-validated PCLandscape struct, landscape dump via F10 hotkey
Browse filesRewrote PCLandscape struct with fields verified against live game memory:
crater sprites, collision bitmap, dirty rect queue, 6 terrain layers,
level/theme paths, visible bounds, GfxHandlers. Added DirtyRect struct
and vtable_slots module. Added 5 new landscape function addresses.
Validator now dumps full PCLandscape state on F10 or after 15s delay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crates/openwa-types/src/address.rs
CHANGED
|
@@ -171,9 +171,18 @@ pub mod va {
|
|
| 171 |
// === Landscape ===
|
| 172 |
|
| 173 |
pub const CONSTRUCT_PC_LANDSCAPE: u32 = 0x0057_ACB0;
|
|
|
|
| 174 |
pub const CONSTRUCT_SPRITE_REGION: u32 = 0x0057_DB20;
|
| 175 |
pub const REDRAW_LAND_REGION: u32 = 0x0057_CC10;
|
| 176 |
pub const WRITE_LAND_RAW: u32 = 0x0057_C300;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
// === Sound ===
|
| 179 |
|
|
|
|
| 171 |
// === Landscape ===
|
| 172 |
|
| 173 |
pub const CONSTRUCT_PC_LANDSCAPE: u32 = 0x0057_ACB0;
|
| 174 |
+
pub const DESTRUCT_PC_LANDSCAPE: u32 = 0x0057_B540;
|
| 175 |
pub const CONSTRUCT_SPRITE_REGION: u32 = 0x0057_DB20;
|
| 176 |
pub const REDRAW_LAND_REGION: u32 = 0x0057_CC10;
|
| 177 |
pub const WRITE_LAND_RAW: u32 = 0x0057_C300;
|
| 178 |
+
/// Applies explosion crater to terrain — destroys pixels + collision (vtable slot 2).
|
| 179 |
+
pub const PC_LANDSCAPE_APPLY_EXPLOSION: u32 = 0x0057_C820;
|
| 180 |
+
/// Draws 8px checkered borders at landscape edges (vtable slot 6).
|
| 181 |
+
pub const PC_LANDSCAPE_DRAW_BORDERS: u32 = 0x0057_D7F0;
|
| 182 |
+
/// Redraws a single terrain row (vtable slot 8).
|
| 183 |
+
pub const PC_LANDSCAPE_REDRAW_ROW: u32 = 0x0057_CF60;
|
| 184 |
+
/// Clips and merges dirty rectangles for terrain redraw.
|
| 185 |
+
pub const PC_LANDSCAPE_CLIP_AND_MERGE: u32 = 0x0057_D2B0;
|
| 186 |
|
| 187 |
// === Sound ===
|
| 188 |
|
crates/openwa-types/src/landscape.rs
CHANGED
|
@@ -1,61 +1,121 @@
|
|
| 1 |
use crate::task::Ptr32;
|
| 2 |
|
| 3 |
-
/// PCLandscape — terrain/landscape subsystem.
|
| 4 |
///
|
| 5 |
-
/// Created by PCLandscape__Constructor (0x57ACB0).
|
| 6 |
-
/// Vtable: 0x66B208
|
| 7 |
///
|
| 8 |
-
/// Manages terrain data
|
| 9 |
-
/// Loads Water.dir and Level.dir
|
| 10 |
-
/// Allocates a 384KB (0x60000) terrain buffer.
|
| 11 |
///
|
| 12 |
-
///
|
| 13 |
-
///
|
| 14 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
#[repr(C)]
|
| 16 |
pub struct PCLandscape {
|
| 17 |
/// 0x000: Vtable pointer (0x66B208)
|
| 18 |
pub vtable: Ptr32,
|
| 19 |
-
/// 0x004: Parent DDGame pointer
|
| 20 |
pub ddgame: Ptr32,
|
| 21 |
-
/// 0x008-
|
| 22 |
-
|
| 23 |
-
///
|
| 24 |
-
pub
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
///
|
| 32 |
-
pub
|
| 33 |
-
///
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
pub initialized: u32,
|
| 37 |
-
///
|
| 38 |
-
pub
|
| 39 |
-
///
|
| 40 |
-
pub
|
| 41 |
-
///
|
| 42 |
-
|
| 43 |
-
///
|
| 44 |
-
pub
|
| 45 |
-
///
|
| 46 |
-
pub
|
| 47 |
-
///
|
| 48 |
-
pub
|
| 49 |
-
///
|
| 50 |
-
pub
|
| 51 |
-
///
|
| 52 |
-
pub
|
| 53 |
-
///
|
| 54 |
-
pub
|
| 55 |
-
///
|
| 56 |
-
pub
|
| 57 |
-
///
|
| 58 |
-
pub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
const _: () = assert!(core::mem::size_of::<PCLandscape>() == 0xB40);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
use crate::task::Ptr32;
|
| 2 |
|
| 3 |
+
/// PCLandscape — terrain/landscape subsystem (0xB40 bytes).
|
| 4 |
///
|
| 5 |
+
/// Created by `PCLandscape__Constructor` (0x57ACB0). Vtable: 0x66B208 (32 slots).
|
|
|
|
| 6 |
///
|
| 7 |
+
/// Manages terrain pixel data (8-bit indexed/paletted), collision bitmap,
|
| 8 |
+
/// water effects, and level graphics. Loads `Water.dir` and `Level.dir`.
|
|
|
|
| 9 |
///
|
| 10 |
+
/// ## Rendering pipeline
|
| 11 |
+
/// - Terrain is stored as multiple pixel layers (DisplayGfx objects)
|
| 12 |
+
/// - Collision uses a 1-bit-per-pixel packed bitmap at +0x0D0
|
| 13 |
+
/// - Dirty rects queued via `RedrawLandRegion` (up to 256, 8 bytes each)
|
| 14 |
+
/// - `DrawLandscape` (0x5A2790) blits landscape pixels to framebuffer
|
| 15 |
+
/// (memcpy for opaque, per-pixel color-key for transparent)
|
| 16 |
+
/// - `WriteLandRaw` (0x57C300) modifies terrain across all layers + collision
|
| 17 |
+
///
|
| 18 |
+
/// ## Key dimensions (stored in DDGame, set by constructor)
|
| 19 |
+
/// - DDGame+0x77C0: level width
|
| 20 |
+
/// - DDGame+0x77C4: level height
|
| 21 |
+
/// - DDGame+0x77C8: width × height (total pixels)
|
| 22 |
+
///
|
| 23 |
+
/// Ghidra uses DWORD-indexed offsets (param_1[N] = byte offset N*4).
|
| 24 |
#[repr(C)]
|
| 25 |
pub struct PCLandscape {
|
| 26 |
/// 0x000: Vtable pointer (0x66B208)
|
| 27 |
pub vtable: Ptr32,
|
| 28 |
+
/// 0x004: Parent DDGame pointer (param_1[1])
|
| 29 |
pub ddgame: Ptr32,
|
| 30 |
+
/// 0x008-0x0CB: Pre-rendered crater sprites for 15 explosion sizes.
|
| 31 |
+
/// param_1[2..17]: crater image ptrs, param_1[18..33]: secondary ptrs.
|
| 32 |
+
/// Indexed by `explosion_size * 15 / 100`.
|
| 33 |
+
pub crater_sprites: [Ptr32; 16],
|
| 34 |
+
pub crater_sprites_secondary: [Ptr32; 16],
|
| 35 |
+
/// 0x088-0x0CB: Unknown
|
| 36 |
+
pub _unknown_088: [u8; 0x44],
|
| 37 |
+
/// 0x0CC: Resource handle (param_1[0x33])
|
| 38 |
+
pub resource_handle: Ptr32,
|
| 39 |
+
/// 0x0D0: Collision bitmap pointer — 1 bit per pixel, packed into DWORDs.
|
| 40 |
+
/// Width = (level_width + 7) / 8 rounded to 4-byte alignment.
|
| 41 |
+
pub collision_bitmap: Ptr32,
|
| 42 |
+
/// 0x0D4: Dirty rect array — 256 entries, each 8 bytes (x1,y1,x2,y2 as u16).
|
| 43 |
+
/// Queued by `RedrawLandRegion`, flushed during frame render.
|
| 44 |
+
pub dirty_rects: [DirtyRect; 256],
|
| 45 |
+
/// 0x8D4: Number of dirty rects queued (max 256, overflows call flush)
|
| 46 |
+
pub dirty_rect_count: u32,
|
| 47 |
+
/// 0x8D8: Dirty flag — set to 1 when any rect is queued
|
| 48 |
+
pub dirty_flag: u8,
|
| 49 |
+
/// 0x8D9-0x8EB: Unknown
|
| 50 |
+
pub _unknown_8d9: [u8; 0x13],
|
| 51 |
+
/// 0x8EC: Unknown (zero at runtime)
|
| 52 |
+
pub _unknown_8ec: u32,
|
| 53 |
+
/// 0x8F0: Unknown (0x9B at runtime — small integer, not pointer)
|
| 54 |
+
pub _unknown_8f0: u32,
|
| 55 |
+
/// 0x8F4: Unknown (zero at runtime)
|
| 56 |
+
pub _unknown_8f4: u32,
|
| 57 |
+
/// 0x8F8-0x8FF: Unknown
|
| 58 |
+
pub _unknown_8f8: [u8; 8],
|
| 59 |
+
/// 0x900: Unknown (NOT DDGame — runtime value 0x13300048 doesn't match DDGame ptr)
|
| 60 |
+
pub _unknown_900: Ptr32,
|
| 61 |
+
/// 0x904: Initialized flag (param_1[0x241], set to 1)
|
| 62 |
pub initialized: u32,
|
| 63 |
+
/// 0x908: Terrain layer 0 — DisplayGfx* (collision visual / background)
|
| 64 |
+
pub layer_0: Ptr32,
|
| 65 |
+
/// 0x90C: Terrain layer 1 — DisplayGfx*
|
| 66 |
+
pub layer_1: Ptr32,
|
| 67 |
+
/// 0x910: Terrain layer 2 — DisplayGfx* (main terrain image).
|
| 68 |
+
/// Pixel data at `*(layer_2 + 8)`, width at `*(layer_2 + 0x14)`,
|
| 69 |
+
/// height at `*(layer_2 + 0x18)`, stride at `*(layer_2 + 0x10)`.
|
| 70 |
+
pub layer_terrain: Ptr32,
|
| 71 |
+
/// 0x914: Terrain layer 3 — DisplayGfx* (edge/shading layer)
|
| 72 |
+
pub layer_edges: Ptr32,
|
| 73 |
+
/// 0x918-0x91B: Unknown
|
| 74 |
+
pub _unknown_918: [u8; 4],
|
| 75 |
+
/// 0x91C: Terrain layer 4 — DisplayGfx* (shadow/overlay)
|
| 76 |
+
pub layer_shadow: Ptr32,
|
| 77 |
+
/// 0x920: Terrain layer 5 — DisplayGfx*
|
| 78 |
+
pub layer_5: Ptr32,
|
| 79 |
+
/// 0x924: Level directory path (char buffer, ~0x100 bytes)
|
| 80 |
+
pub level_dir_path: [u8; 0x100],
|
| 81 |
+
/// 0xA24: Theme/data directory path (char buffer, 0x100 bytes)
|
| 82 |
+
pub theme_dir_path: [u8; 0x100],
|
| 83 |
+
/// 0xB24-0xB2F: Visible bounds (left, top, right, bottom)
|
| 84 |
+
pub visible_left: u32,
|
| 85 |
+
pub visible_top: u32,
|
| 86 |
+
pub visible_right: u32,
|
| 87 |
+
pub visible_bottom: u32,
|
| 88 |
+
/// 0xB34: GfxHandler for Level.dir (param_1[0x2CD])
|
| 89 |
+
pub level_gfx_handler: Ptr32,
|
| 90 |
+
/// 0xB38: GfxHandler for Water.dir (param_1[0x2CE])
|
| 91 |
+
pub water_gfx_handler: Ptr32,
|
| 92 |
+
/// 0xB3C: Remaining control flag
|
| 93 |
+
pub _unknown_b3c: u32,
|
| 94 |
}
|
| 95 |
|
| 96 |
const _: () = assert!(core::mem::size_of::<PCLandscape>() == 0xB40);
|
| 97 |
+
|
| 98 |
+
/// A dirty rectangle entry in the PCLandscape dirty rect queue.
|
| 99 |
+
/// Coordinates are in landscape pixel space.
|
| 100 |
+
#[repr(C)]
|
| 101 |
+
#[derive(Clone, Copy)]
|
| 102 |
+
pub struct DirtyRect {
|
| 103 |
+
pub x1: u16,
|
| 104 |
+
pub y1: u16,
|
| 105 |
+
pub x2: u16,
|
| 106 |
+
pub y2: u16,
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
const _: () = assert!(core::mem::size_of::<DirtyRect>() == 8);
|
| 110 |
+
|
| 111 |
+
/// PCLandscape vtable slot indices (32 entries at 0x66B208).
|
| 112 |
+
pub mod vtable_slots {
|
| 113 |
+
/// Slot 0: Destructor (0x57B540)
|
| 114 |
+
pub const DESTRUCTOR: usize = 0;
|
| 115 |
+
/// Slot 2: Apply explosion crater (0x57C820) — terrain destruction
|
| 116 |
+
pub const APPLY_EXPLOSION: usize = 2;
|
| 117 |
+
/// Slot 6: Draw borders — 8px checkered borders at landscape edges (0x57D7F0)
|
| 118 |
+
pub const DRAW_BORDERS: usize = 6;
|
| 119 |
+
/// Slot 8: Redraw single row (0x57CF60)
|
| 120 |
+
pub const REDRAW_ROW: usize = 8;
|
| 121 |
+
}
|
crates/openwa-validator/src/lib.rs
CHANGED
|
@@ -617,6 +617,158 @@ fn dump_team_blocks() {
|
|
| 617 |
}
|
| 618 |
}
|
| 619 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
// ---------------------------------------------------------------------------
|
| 621 |
// Main validation entry point
|
| 622 |
// ---------------------------------------------------------------------------
|
|
@@ -678,16 +830,27 @@ fn run_validation() -> Result<(), Box<dyn std::error::Error>> {
|
|
| 678 |
});
|
| 679 |
let _ = log_line(" Team block dump thread started (30s delay).");
|
| 680 |
|
| 681 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
std::thread::spawn(|| {
|
| 683 |
const VK_F9: i32 = 0x78;
|
| 684 |
-
|
|
|
|
| 685 |
loop {
|
| 686 |
std::thread::sleep(std::time::Duration::from_millis(100));
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
|
|
|
|
|
|
|
|
|
| 691 |
}
|
| 692 |
}
|
| 693 |
});
|
|
|
|
| 617 |
}
|
| 618 |
}
|
| 619 |
|
| 620 |
+
// ---------------------------------------------------------------------------
|
| 621 |
+
// PCLandscape dump (for landscape struct verification)
|
| 622 |
+
// ---------------------------------------------------------------------------
|
| 623 |
+
|
| 624 |
+
fn dump_landscape() {
|
| 625 |
+
use openwa_types::landscape::PCLandscape;
|
| 626 |
+
|
| 627 |
+
let dump_num = DUMP_COUNTER.fetch_add(1, Ordering::Relaxed);
|
| 628 |
+
let _ = log_line("");
|
| 629 |
+
let _ = log_line(&format!("--- PCLandscape Dump #{} ---", dump_num));
|
| 630 |
+
|
| 631 |
+
unsafe {
|
| 632 |
+
let session_ptr = read_u32(rb(va::G_GAME_SESSION));
|
| 633 |
+
if session_ptr == 0 {
|
| 634 |
+
let _ = log_line(" No game session — skipping.");
|
| 635 |
+
return;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
let wrapper_addr = read_u32(session_ptr + 0xA0);
|
| 639 |
+
if wrapper_addr == 0 {
|
| 640 |
+
let _ = log_line(" No DDGameWrapper — need to start a game first.");
|
| 641 |
+
return;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
let landscape_ptr = read_u32(wrapper_addr + 0x4CC);
|
| 645 |
+
if landscape_ptr == 0 {
|
| 646 |
+
let _ = log_line(" PCLandscape is NULL — no level loaded.");
|
| 647 |
+
return;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
let land = &*(landscape_ptr as *const PCLandscape);
|
| 651 |
+
let _ = log_line(&format!(" PCLandscape @ 0x{:08X}", landscape_ptr));
|
| 652 |
+
|
| 653 |
+
// Vtable validation
|
| 654 |
+
let expected_vt = rb(va::PC_LANDSCAPE_VTABLE);
|
| 655 |
+
let vt_ok = land.vtable == expected_vt;
|
| 656 |
+
let _ = log_line(&format!(" vtable: 0x{:08X} (expected 0x{:08X}) {}",
|
| 657 |
+
land.vtable, expected_vt, if vt_ok { "OK" } else { "MISMATCH" }));
|
| 658 |
+
|
| 659 |
+
// DDGame pointer
|
| 660 |
+
let _ = log_line(&format!(" ddgame: 0x{:08X}", land.ddgame));
|
| 661 |
+
let _ = log_line(&format!(" _unknown_900: 0x{:08X}", land._unknown_900));
|
| 662 |
+
|
| 663 |
+
// Collision bitmap
|
| 664 |
+
let _ = log_line(&format!(" collision_bitmap: 0x{:08X}", land.collision_bitmap));
|
| 665 |
+
|
| 666 |
+
// Initialized flag
|
| 667 |
+
let _ = log_line(&format!(" initialized: {}", land.initialized));
|
| 668 |
+
|
| 669 |
+
// Crater sprites — count non-null
|
| 670 |
+
let primary_count = land.crater_sprites.iter().filter(|&&p| p != 0).count();
|
| 671 |
+
let secondary_count = land.crater_sprites_secondary.iter().filter(|&&p| p != 0).count();
|
| 672 |
+
let _ = log_line(&format!(" crater_sprites: {}/16 non-null, secondary: {}/16 non-null",
|
| 673 |
+
primary_count, secondary_count));
|
| 674 |
+
|
| 675 |
+
// Terrain layers
|
| 676 |
+
let _ = log_line(&format!(" layer_0: 0x{:08X}", land.layer_0));
|
| 677 |
+
let _ = log_line(&format!(" layer_1: 0x{:08X}", land.layer_1));
|
| 678 |
+
let _ = log_line(&format!(" layer_terrain: 0x{:08X}", land.layer_terrain));
|
| 679 |
+
let _ = log_line(&format!(" layer_edges: 0x{:08X}", land.layer_edges));
|
| 680 |
+
let _ = log_line(&format!(" layer_shadow: 0x{:08X}", land.layer_shadow));
|
| 681 |
+
let _ = log_line(&format!(" layer_5: 0x{:08X}", land.layer_5));
|
| 682 |
+
|
| 683 |
+
// If layer_terrain is valid, read DisplayGfx fields
|
| 684 |
+
if land.layer_terrain != 0 {
|
| 685 |
+
let dgfx = land.layer_terrain;
|
| 686 |
+
let pixel_data = read_u32(dgfx + 0x08);
|
| 687 |
+
let stride = read_u32(dgfx + 0x10);
|
| 688 |
+
let width = read_u32(dgfx + 0x14);
|
| 689 |
+
let height = read_u32(dgfx + 0x18);
|
| 690 |
+
let _ = log_line(&format!(" layer_terrain DisplayGfx: pixels=0x{:08X} stride={} width={} height={}",
|
| 691 |
+
pixel_data, stride, width, height));
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
// Dirty rects
|
| 695 |
+
let _ = log_line(&format!(" dirty_rect_count: {}", land.dirty_rect_count));
|
| 696 |
+
let _ = log_line(&format!(" dirty_flag: {}", land.dirty_flag));
|
| 697 |
+
if land.dirty_rect_count > 0 && land.dirty_rect_count <= 256 {
|
| 698 |
+
for i in 0..land.dirty_rect_count.min(5) as usize {
|
| 699 |
+
let r = &land.dirty_rects[i];
|
| 700 |
+
let _ = log_line(&format!(" rect[{}]: ({},{})..({},{})",
|
| 701 |
+
i, r.x1, r.y1, r.x2, r.y2));
|
| 702 |
+
}
|
| 703 |
+
if land.dirty_rect_count > 5 {
|
| 704 |
+
let _ = log_line(&format!(" ... and {} more", land.dirty_rect_count - 5));
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
// Unknown fields around palette area
|
| 709 |
+
let _ = log_line(&format!(" _unknown_8ec: 0x{:08X}", land._unknown_8ec));
|
| 710 |
+
let _ = log_line(&format!(" _unknown_8f0: 0x{:08X}", land._unknown_8f0));
|
| 711 |
+
let _ = log_line(&format!(" _unknown_8f4: 0x{:08X}", land._unknown_8f4));
|
| 712 |
+
|
| 713 |
+
// Resource handle
|
| 714 |
+
let _ = log_line(&format!(" resource_handle: 0x{:08X}", land.resource_handle));
|
| 715 |
+
|
| 716 |
+
// GfxHandlers
|
| 717 |
+
let _ = log_line(&format!(" level_gfx_handler: 0x{:08X}", land.level_gfx_handler));
|
| 718 |
+
let _ = log_line(&format!(" water_gfx_handler: 0x{:08X}", land.water_gfx_handler));
|
| 719 |
+
|
| 720 |
+
// Visible bounds
|
| 721 |
+
let _ = log_line(&format!(" visible_bounds: left={} top={} right={} bottom={}",
|
| 722 |
+
land.visible_left, land.visible_top, land.visible_right, land.visible_bottom));
|
| 723 |
+
|
| 724 |
+
// Path buffers — read as C strings
|
| 725 |
+
let level_path = std::ffi::CStr::from_ptr(land.level_dir_path.as_ptr() as *const i8);
|
| 726 |
+
let theme_path = std::ffi::CStr::from_ptr(land.theme_dir_path.as_ptr() as *const i8);
|
| 727 |
+
let _ = log_line(&format!(" level_dir_path: {:?}", level_path));
|
| 728 |
+
let _ = log_line(&format!(" theme_dir_path: {:?}", theme_path));
|
| 729 |
+
|
| 730 |
+
// DDGame level dimensions (from DDGame, not PCLandscape)
|
| 731 |
+
let ddgame_ptr = land.ddgame;
|
| 732 |
+
if ddgame_ptr != 0 {
|
| 733 |
+
let level_w = read_u32(ddgame_ptr + 0x77C0);
|
| 734 |
+
let level_h = read_u32(ddgame_ptr + 0x77C4);
|
| 735 |
+
let level_total = read_u32(ddgame_ptr + 0x77C8);
|
| 736 |
+
let _ = log_line(&format!(" DDGame level dims: {}x{} (total={})",
|
| 737 |
+
level_w, level_h, level_total));
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
// Vtable slot dump (first 10 entries)
|
| 741 |
+
let _ = log_line(" Vtable slots:");
|
| 742 |
+
let vt_addr = land.vtable;
|
| 743 |
+
for slot in 0..10u32 {
|
| 744 |
+
let entry = read_u32(vt_addr + slot * 4);
|
| 745 |
+
let in_text = is_in_text(entry);
|
| 746 |
+
let _ = log_line(&format!(" [{}]: 0x{:08X} {}",
|
| 747 |
+
slot, entry, if in_text { "" } else { "(NOT .text)" }));
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
let _ = log_line(&format!(" _unknown_b3c: 0x{:08X}", land._unknown_b3c));
|
| 751 |
+
|
| 752 |
+
// Hex dump of unknown regions for discovery
|
| 753 |
+
let _ = log_line(" _unknown_088 (0x44 bytes):");
|
| 754 |
+
let _ = log_hex_dump(&land._unknown_088);
|
| 755 |
+
let _ = log_line(" _unknown_8d9 (0x13 bytes):");
|
| 756 |
+
let _ = log_hex_dump(&land._unknown_8d9);
|
| 757 |
+
let _ = log_line(" _unknown_8f8 (8 bytes):");
|
| 758 |
+
let _ = log_hex_dump(&land._unknown_8f8);
|
| 759 |
+
let _ = log_line(" _unknown_918 (4 bytes):");
|
| 760 |
+
let _ = log_hex_dump(&land._unknown_918);
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
fn log_hex_dump(data: &[u8]) {
|
| 765 |
+
for chunk in data.chunks(16) {
|
| 766 |
+
let hex: String = chunk.iter().map(|b| format!("{:02X}", b)).collect::<Vec<_>>().join(" ");
|
| 767 |
+
let ascii: String = chunk.iter().map(|&b| if (0x20..0x7F).contains(&b) { b as char } else { '.' }).collect();
|
| 768 |
+
let _ = log_line(&format!(" {} | {}", hex, ascii));
|
| 769 |
+
}
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
// ---------------------------------------------------------------------------
|
| 773 |
// Main validation entry point
|
| 774 |
// ---------------------------------------------------------------------------
|
|
|
|
| 830 |
});
|
| 831 |
let _ = log_line(" Team block dump thread started (30s delay).");
|
| 832 |
|
| 833 |
+
// 8b. Landscape dump — wait 15s (landscape exists at menu too)
|
| 834 |
+
std::thread::spawn(move || {
|
| 835 |
+
std::thread::sleep(std::time::Duration::from_secs(15));
|
| 836 |
+
dump_landscape();
|
| 837 |
+
});
|
| 838 |
+
let _ = log_line(" Landscape dump thread started (15s delay).");
|
| 839 |
+
|
| 840 |
+
// 9. Hotkey listener — F9 = team blocks, F10 = landscape
|
| 841 |
std::thread::spawn(|| {
|
| 842 |
const VK_F9: i32 = 0x78;
|
| 843 |
+
const VK_F10: i32 = 0x79;
|
| 844 |
+
let _ = log_line(" Hotkey listener started (F9=team blocks, F10=landscape).");
|
| 845 |
loop {
|
| 846 |
std::thread::sleep(std::time::Duration::from_millis(100));
|
| 847 |
+
unsafe {
|
| 848 |
+
if GetAsyncKeyState(VK_F9) & 1 != 0 {
|
| 849 |
+
dump_team_blocks();
|
| 850 |
+
}
|
| 851 |
+
if GetAsyncKeyState(VK_F10) & 1 != 0 {
|
| 852 |
+
dump_landscape();
|
| 853 |
+
}
|
| 854 |
}
|
| 855 |
}
|
| 856 |
});
|