refactor: add MapClass struct, use typed pointer in replay map loading
Browse filesCreate MapClass (0x29628 bytes) with vtable pointer and terrain_flag
field. Define MapClassVtable with typed destructor slot.
Replace raw u32/pointer arithmetic in replay map loading with typed
MapClass pointers — constructor, load, vtable destructor calls, and
terrain_flag field access all use the correct type. Eliminates manual
vtable offset calculations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/engine/map_class.rs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// MapClass — landscape/terrain map object (0x29628 bytes).
|
| 2 |
+
///
|
| 3 |
+
/// Used by the replay loader to parse map data from playback.thm files.
|
| 4 |
+
/// Allocated with CRT malloc (0x29628 bytes), constructed via
|
| 5 |
+
/// `MapClass__Constructor` (0x447E80), loaded via `MapClass__Load`
|
| 6 |
+
/// (0x44A9A0), then map info is copied to game state via
|
| 7 |
+
/// `MapClass__CopyInfo` (0x449B60, usercall ESI=this).
|
| 8 |
+
///
|
| 9 |
+
/// After use, released via vtable[1] (thiscall destructor with free flag).
|
| 10 |
+
///
|
| 11 |
+
/// PARTIAL: Only the vtable and terrain_flag field are known.
|
| 12 |
+
#[repr(C)]
|
| 13 |
+
pub struct MapClass {
|
| 14 |
+
/// 0x00000: Vtable pointer.
|
| 15 |
+
pub vtable: *const MapClassVtable,
|
| 16 |
+
/// 0x00004-0x29617: Unknown (map data, terrain grid, etc.).
|
| 17 |
+
pub _unknown_004: [u8; 0x29618 - 4],
|
| 18 |
+
/// 0x29618: Terrain type flag. Zero = cavern terrain (SETZ in replay loader).
|
| 19 |
+
pub terrain_flag: u8,
|
| 20 |
+
/// 0x29619-0x29627: Unknown trailing bytes.
|
| 21 |
+
pub _unknown_29619: [u8; 0x29628 - 0x29619],
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const _: () = assert!(core::mem::size_of::<MapClass>() == 0x29628);
|
| 25 |
+
|
| 26 |
+
/// MapClass vtable (partial — only known slots).
|
| 27 |
+
#[repr(C)]
|
| 28 |
+
pub struct MapClassVtable {
|
| 29 |
+
/// Slot 0: Unknown virtual method.
|
| 30 |
+
pub slot_0: usize,
|
| 31 |
+
/// Slot 1: Destructor — thiscall(this, free_flag). Frees the object if flag & 1.
|
| 32 |
+
pub destructor: unsafe extern "thiscall" fn(*mut MapClass, i32),
|
| 33 |
+
}
|
crates/openwa-core/src/engine/mod.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
pub mod ddgame;
|
|
|
|
| 2 |
pub mod replay;
|
| 3 |
pub mod ddgame_wrapper;
|
| 4 |
pub mod game_info;
|
|
|
|
| 1 |
pub mod ddgame;
|
| 2 |
+
pub mod map_class;
|
| 3 |
pub mod replay;
|
| 4 |
pub mod ddgame_wrapper;
|
| 5 |
pub mod game_info;
|
crates/openwa-wormkit/src/replacements/replay.rs
CHANGED
|
@@ -7,6 +7,7 @@ use crate::hook;
|
|
| 7 |
use crate::log_line;
|
| 8 |
use openwa_core::address::va;
|
| 9 |
use openwa_core::engine::game_info::GameInfo;
|
|
|
|
| 10 |
use openwa_core::engine::replay::{self, ReplayError, ReplayStream, ReplayTeamEntry};
|
| 11 |
use openwa_core::rebase::rb;
|
| 12 |
use openwa_core::wa_alloc::{wa_free, wa_malloc};
|
|
@@ -542,47 +543,36 @@ unsafe fn parse_and_write_v2plus(
|
|
| 542 |
// For positive sub-version (our test case), we need to:
|
| 543 |
// Map loading: construct map object, load playback.thm, copy info, release.
|
| 544 |
if (*gi).replay_map_type >= 1 {
|
| 545 |
-
|
| 546 |
-
let alloc: unsafe extern "cdecl" fn(u32) -> u32 =
|
| 547 |
core::mem::transmute(rb(va::WA_CRT_MALLOC));
|
| 548 |
-
let buf = alloc(
|
| 549 |
-
let
|
| 550 |
-
|
| 551 |
-
let construct: unsafe extern "stdcall" fn(u32, i32) -> *mut u32 =
|
| 552 |
core::mem::transmute(rb(va::MAP_CLASS_CONSTRUCTOR));
|
| 553 |
construct(buf, 1)
|
| 554 |
} else {
|
| 555 |
ptr::null_mut()
|
| 556 |
};
|
| 557 |
|
| 558 |
-
|
| 559 |
-
let load: unsafe extern "stdcall" fn(*mut u32, *const u8, i32) -> i32 =
|
| 560 |
core::mem::transmute(rb(va::MAP_CLASS_LOAD));
|
| 561 |
-
let ok = load(
|
| 562 |
|
| 563 |
if ok == 0 {
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
let vtable = *map_obj;
|
| 567 |
-
let release: unsafe extern "thiscall" fn(*mut u32, i32) =
|
| 568 |
-
core::mem::transmute(*(vtable as *const u32).add(1));
|
| 569 |
-
release(map_obj, 1);
|
| 570 |
}
|
| 571 |
return Err(ReplayError::MapLoadFailure);
|
| 572 |
}
|
| 573 |
|
| 574 |
-
//
|
| 575 |
-
call_usercall_esi(
|
| 576 |
|
| 577 |
-
// Terrain flag:
|
| 578 |
-
(*gi).terrain_flag =
|
| 579 |
-
(*(map_obj as *const u8).add(0x29618) == 0) as u8;
|
| 580 |
|
| 581 |
-
// Release
|
| 582 |
-
|
| 583 |
-
let release: unsafe extern "thiscall" fn(*mut u32, i32) =
|
| 584 |
-
core::mem::transmute(*(vtable as *const u32).add(1));
|
| 585 |
-
release(map_obj, 1);
|
| 586 |
}
|
| 587 |
|
| 588 |
// Log output is written by the caller after map loading completes.
|
|
|
|
| 7 |
use crate::log_line;
|
| 8 |
use openwa_core::address::va;
|
| 9 |
use openwa_core::engine::game_info::GameInfo;
|
| 10 |
+
use openwa_core::engine::map_class::MapClass;
|
| 11 |
use openwa_core::engine::replay::{self, ReplayError, ReplayStream, ReplayTeamEntry};
|
| 12 |
use openwa_core::rebase::rb;
|
| 13 |
use openwa_core::wa_alloc::{wa_free, wa_malloc};
|
|
|
|
| 543 |
// For positive sub-version (our test case), we need to:
|
| 544 |
// Map loading: construct map object, load playback.thm, copy info, release.
|
| 545 |
if (*gi).replay_map_type >= 1 {
|
| 546 |
+
let alloc: unsafe extern "cdecl" fn(u32) -> *mut MapClass =
|
|
|
|
| 547 |
core::mem::transmute(rb(va::WA_CRT_MALLOC));
|
| 548 |
+
let buf = alloc(core::mem::size_of::<MapClass>() as u32);
|
| 549 |
+
let map = if !buf.is_null() {
|
| 550 |
+
let construct: unsafe extern "stdcall" fn(*mut MapClass, i32) -> *mut MapClass =
|
|
|
|
| 551 |
core::mem::transmute(rb(va::MAP_CLASS_CONSTRUCTOR));
|
| 552 |
construct(buf, 1)
|
| 553 |
} else {
|
| 554 |
ptr::null_mut()
|
| 555 |
};
|
| 556 |
|
| 557 |
+
let load: unsafe extern "stdcall" fn(*mut MapClass, *const u8, i32) -> i32 =
|
|
|
|
| 558 |
core::mem::transmute(rb(va::MAP_CLASS_LOAD));
|
| 559 |
+
let ok = load(map, b"data\\playback.thm\0".as_ptr(), 0);
|
| 560 |
|
| 561 |
if ok == 0 {
|
| 562 |
+
if !map.is_null() {
|
| 563 |
+
((*(*map).vtable).destructor)(map, 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
}
|
| 565 |
return Err(ReplayError::MapLoadFailure);
|
| 566 |
}
|
| 567 |
|
| 568 |
+
// Copy map info to game state (usercall ESI=map)
|
| 569 |
+
call_usercall_esi(map as u32, rb(va::MAP_COPY_INFO));
|
| 570 |
|
| 571 |
+
// Terrain flag: zero = cavern terrain
|
| 572 |
+
(*gi).terrain_flag = ((*map).terrain_flag == 0) as u8;
|
|
|
|
| 573 |
|
| 574 |
+
// Release map object
|
| 575 |
+
((*(*map).vtable).destructor)(map, 1);
|
|
|
|
|
|
|
|
|
|
| 576 |
}
|
| 577 |
|
| 578 |
// Log output is written by the caller after map loading completes.
|