feat: port 5 DDGame constructor sub-functions to Rust hooks
Browse filesPort DDGame constructor sub-functions as individual MinHook replacements,
so each works in isolation regardless of whether the DDGame constructor
itself is Rust or original WA code. All 5 hooks verified with replay
test (133 PASS, 0 FAIL).
Ported functions:
- DDGame__InitFields (0x526120): zeroes scattered offsets + render indices
- DDGame__InitRenderIndices (0x526080): identity permutation tables at
ddgame+0x72D8 (team index maps, NOT base of DDGame)
- TaskStateMachine__Init (0x4F6370): bit-per-cell grid allocator
- GfxResource__Create_Maybe (0x4F6300): FindEntry β cached load β DisplayGfx
wrap, with LoadImage β IMG_Decode fallback
- FUN_570E20 / DDGame__InitDisplayLayerColors: 3 display vtable[4] calls
Key bugs found and fixed:
- InitRenderIndices base pointer: ESI = ddgame+0x72D8, not ddgame itself.
Original was writing render indices to team_index_map region.
- InitFields return value: caller stores EAX as DDGame pointer at
wrapper+0x488. Missing return caused null DDGame β crash on loading.
Also adds:
- Panic hook (writes [PANIC] to OpenWA.log for crash diagnosis)
- PANIC detection in replay-test.ps1
- IMG_Decode label in Ghidra (was mislabeled TaskStateMachine__Destructor)
- DISPLAYGFX_CONSTRUCTOR and IMG_DECODE address constants
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/address.rs +6 -0
- crates/openwa-core/src/engine/ddgame.rs +236 -9
- crates/openwa-wormkit/src/lib.rs +5 -0
- crates/openwa-wormkit/src/replacements/ddgame_init.rs +146 -0
- crates/openwa-wormkit/src/replacements/game_session.rs +1 -1
- crates/openwa-wormkit/src/replacements/mod.rs +2 -0
- replay-test.ps1 +10 -0
|
@@ -364,6 +364,12 @@ pub mod va {
|
|
| 364 |
pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0;
|
| 365 |
pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
|
| 366 |
pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
// === Higher-level drawing functions ===
|
| 369 |
|
|
|
|
| 364 |
pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0;
|
| 365 |
pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
|
| 366 |
pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
|
| 367 |
+
/// DisplayGfx__Constructor_Maybe (0x4F5E80): stdcall(raw_image) -> DisplayGfx*, RET 0x4.
|
| 368 |
+
pub const DISPLAYGFX_CONSTRUCTOR: u32 = 0x004F_5E80;
|
| 369 |
+
/// IMG_Decode (0x4F5F80): stdcall(output, raw_image, 1) -> DisplayGfx*, RET 0xC.
|
| 370 |
+
/// Reads IMG\x1A header, decodes image data into output buffer.
|
| 371 |
+
/// Ghidra mislabels this as "TaskStateMachine__Destructor_Maybe".
|
| 372 |
+
pub const IMG_DECODE: u32 = 0x004F_5F80;
|
| 373 |
|
| 374 |
// === Higher-level drawing functions ===
|
| 375 |
|
|
@@ -362,6 +362,235 @@ const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98B8);
|
|
| 362 |
// ddgame_wrapper.rs; the bulk of the logic lives here because
|
| 363 |
// it primarily initializes DDGame fields.
|
| 364 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
/// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
|
| 366 |
static mut INIT_FIELDS_ADDR: u32 = 0;
|
| 367 |
|
|
@@ -437,9 +666,8 @@ pub unsafe fn create_ddgame(
|
|
| 437 |
}
|
| 438 |
core::ptr::write_bytes(ddgame as *mut u8, 0, 0x98B8);
|
| 439 |
|
| 440 |
-
// ββ 2. InitFields β
|
| 441 |
-
|
| 442 |
-
call_init_fields(ddgame);
|
| 443 |
|
| 444 |
// ββ 3-4. Store params BEFORE exposing DDGame via wrapper ββ
|
| 445 |
// Critical: game_info must be set before wrapper->ddgame, because the
|
|
@@ -796,16 +1024,15 @@ unsafe fn init_graphics_and_resources(
|
|
| 796 |
};
|
| 797 |
|
| 798 |
// ββ TaskStateMachine at DDGame+0x380 (alloc 0x4C, memset 0x2C) ββ
|
| 799 |
-
//
|
| 800 |
-
// Also EDI=height before the call (implicit param).
|
| 801 |
{
|
| 802 |
let tsm = wa_malloc(0x4C);
|
| 803 |
core::ptr::write_bytes(tsm, 0, 0x2C);
|
| 804 |
if !tsm.is_null() {
|
| 805 |
-
let width =
|
| 806 |
-
let height =
|
| 807 |
-
|
| 808 |
-
*(tsm as *mut u32) = rb(0x664118); //
|
| 809 |
}
|
| 810 |
(*ddgame).task_state_machine = tsm;
|
| 811 |
}
|
|
|
|
| 362 |
// ddgame_wrapper.rs; the bulk of the logic lives here because
|
| 363 |
// it primarily initializes DDGame fields.
|
| 364 |
|
| 365 |
+
// ============================================================
|
| 366 |
+
// Pure Rust implementations of DDGame sub-functions
|
| 367 |
+
// ============================================================
|
| 368 |
+
// These are called both by create_ddgame() and by MinHook
|
| 369 |
+
// trampolines in openwa-wormkit/replacements/ddgame_init.rs.
|
| 370 |
+
|
| 371 |
+
/// Pure Rust implementation of DDGame__InitFields (0x526120).
|
| 372 |
+
///
|
| 373 |
+
/// Zeroes stride-0x194 table entries, calls init_render_indices,
|
| 374 |
+
/// then zeroes coordination/sound entries at 0x8Cxx and 0x98xx.
|
| 375 |
+
///
|
| 376 |
+
/// # Safety
|
| 377 |
+
/// `ddgame` must point to a valid, zero-filled DDGame allocation (0x98B8 bytes).
|
| 378 |
+
pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
|
| 379 |
+
let base = ddgame as *mut u8;
|
| 380 |
+
|
| 381 |
+
// Zero the stride-0x194 table (10 entries)
|
| 382 |
+
for &off in &[
|
| 383 |
+
0x379Cusize, 0x3930, 0x3AC4, 0x3C58, 0x3DEC,
|
| 384 |
+
0x3F80, 0x4114, 0x42A8, 0x443C, 0x45D0,
|
| 385 |
+
] {
|
| 386 |
+
*(base.add(off as usize) as *mut u32) = 0;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
*(base.add(0x64D8) as *mut u32) = 0;
|
| 390 |
+
*(base.add(0x72A4) as *mut u32) = 0;
|
| 391 |
+
|
| 392 |
+
// InitRenderIndices β original sets ESI = ddgame + 0x72D8 before calling
|
| 393 |
+
ddgame_init_render_indices(base.add(0x72D8));
|
| 394 |
+
|
| 395 |
+
// Zero 8 fields at 0x8Cxx
|
| 396 |
+
for &off in &[
|
| 397 |
+
0x8CBCusize, 0x8CC0, 0x8CCC, 0x8CD0,
|
| 398 |
+
0x8CDC, 0x8CE0, 0x8CEC, 0x8CF0,
|
| 399 |
+
] {
|
| 400 |
+
*(base.add(off as usize) as *mut u32) = 0;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
// Zero 8 fields at 0x98xx
|
| 404 |
+
for &off in &[
|
| 405 |
+
0x9850usize, 0x9854, 0x9860, 0x9864,
|
| 406 |
+
0x9870, 0x9874, 0x9880, 0x9884,
|
| 407 |
+
] {
|
| 408 |
+
*(base.add(off as usize) as *mut u32) = 0;
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
/// Pure Rust implementation of DDGame__InitRenderIndices (0x526080).
|
| 413 |
+
///
|
| 414 |
+
/// Convention: usercall(ESI=base_ptr), plain RET.
|
| 415 |
+
///
|
| 416 |
+
/// **Important:** The base pointer is NOT the DDGame pointer!
|
| 417 |
+
/// InitFields calls this with ESI = ddgame + 0x72D8.
|
| 418 |
+
/// All offsets are relative to whatever ESI points to.
|
| 419 |
+
///
|
| 420 |
+
/// Absolute DDGame offsets (base = ddgame+0x72D8):
|
| 421 |
+
/// - base+0xC4 = ddgame+0x739C
|
| 422 |
+
/// - base+0xD8 = ddgame+0x73B0 (eh_vector_constructor_iterator region)
|
| 423 |
+
/// - base+0x378 = ddgame+0x7650 (team_index_map_1)
|
| 424 |
+
/// - base+0x3DC = ddgame+0x76B4 (team_index_map_2)
|
| 425 |
+
/// - base+0x440 = ddgame+0x7718 (team_index_map_3)
|
| 426 |
+
///
|
| 427 |
+
/// # Safety
|
| 428 |
+
/// `base` must point to a valid memory region with at least 0x4A4 bytes.
|
| 429 |
+
pub unsafe fn ddgame_init_render_indices(base: *mut u8) {
|
| 430 |
+
// Zero base+0xC4
|
| 431 |
+
*(base.add(0xC4) as *mut u32) = 0;
|
| 432 |
+
|
| 433 |
+
// eh_vector_constructor_iterator equivalent:
|
| 434 |
+
// FUN_525F40 is fastcall { *param_1 = 0; }
|
| 435 |
+
// 14 entries at stride 0x14 starting from +0xD8
|
| 436 |
+
for i in 0..14usize {
|
| 437 |
+
*(base.add(0xD8 + i * 0x14) as *mut u32) = 0;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// Identity permutation 1: base+0x378 (= ddgame+0x7650), 16 entries (i16)
|
| 441 |
+
for i in 0..16i16 {
|
| 442 |
+
*(base.add(0x378 + i as usize * 2) as *mut i16) = i;
|
| 443 |
+
}
|
| 444 |
+
*(base.add(0x398) as *mut u16) = 0x10;
|
| 445 |
+
*(base.add(0x3DA) as *mut u16) = 0;
|
| 446 |
+
|
| 447 |
+
// Identity permutation 2: base+0x3DC (= ddgame+0x76B4), 16 entries (i16)
|
| 448 |
+
for i in 0..16i16 {
|
| 449 |
+
*(base.add(0x3DC + i as usize * 2) as *mut i16) = i;
|
| 450 |
+
}
|
| 451 |
+
*(base.add(0x3FC) as *mut u16) = 0x10;
|
| 452 |
+
*(base.add(0x43E) as *mut u16) = 0;
|
| 453 |
+
|
| 454 |
+
// Identity permutation 3: base+0x440 (= ddgame+0x7718), 16 entries (i16)
|
| 455 |
+
for i in 0..16i16 {
|
| 456 |
+
*(base.add(0x440 + i as usize * 2) as *mut i16) = i;
|
| 457 |
+
}
|
| 458 |
+
*(base.add(0x4A2) as *mut u16) = 0;
|
| 459 |
+
*(base.add(0x460) as *mut u16) = 0x10;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
/// Pure Rust implementation of TaskStateMachine__Init (0x4F6370).
|
| 463 |
+
///
|
| 464 |
+
/// Convention: usercall(ESI=object, ECX=param1, EDI=height) + 1 stack(width), RET 0x4.
|
| 465 |
+
///
|
| 466 |
+
/// Allocates a bit-per-cell grid buffer. `param1` is typically 1 (cells per unit).
|
| 467 |
+
/// `width` and `height` are pixel dimensions. The buffer is a row-major bitfield
|
| 468 |
+
/// with rows aligned to 4 bytes.
|
| 469 |
+
///
|
| 470 |
+
/// # Safety
|
| 471 |
+
/// `object` must point to a zero-filled allocation of at least 0x2C bytes.
|
| 472 |
+
pub unsafe fn task_state_machine_init(
|
| 473 |
+
object: *mut u8,
|
| 474 |
+
param1: u32,
|
| 475 |
+
width: u32,
|
| 476 |
+
height: u32,
|
| 477 |
+
) {
|
| 478 |
+
// Row stride: bits-to-bytes rounded up, then aligned to 4
|
| 479 |
+
let bits = param1.wrapping_mul(width).wrapping_add(7) as i32;
|
| 480 |
+
let row_stride = ((bits >> 3) + 3) & !3;
|
| 481 |
+
let total_size = row_stride as u32 * height;
|
| 482 |
+
|
| 483 |
+
// Allocate data buffer with 0x20-byte header
|
| 484 |
+
let alloc_size = ((total_size + 3) & !3) + 0x20;
|
| 485 |
+
let buffer = wa_malloc(alloc_size);
|
| 486 |
+
|
| 487 |
+
// Memset twice (matches original β likely redundant but exact match)
|
| 488 |
+
core::ptr::write_bytes(buffer, 0, total_size as usize);
|
| 489 |
+
core::ptr::write_bytes(buffer, 0, total_size as usize);
|
| 490 |
+
|
| 491 |
+
// Set object fields (DWORD array layout)
|
| 492 |
+
let obj = object as *mut u32;
|
| 493 |
+
*obj.add(0) = rb(0x6640EC); // vtable
|
| 494 |
+
*obj.add(1) = 0; // unused
|
| 495 |
+
*obj.add(2) = buffer as u32; // data pointer
|
| 496 |
+
*obj.add(3) = param1; // param1
|
| 497 |
+
*obj.add(4) = row_stride as u32; // row stride
|
| 498 |
+
*obj.add(5) = width; // width
|
| 499 |
+
*obj.add(6) = height; // height
|
| 500 |
+
*obj.add(7) = 0; // unused
|
| 501 |
+
*obj.add(8) = 0; // unused
|
| 502 |
+
*obj.add(9) = width; // width (duplicate)
|
| 503 |
+
*obj.add(10) = height; // height (duplicate)
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
/// Pure Rust implementation of FUN_570E20 (display layer color init).
|
| 507 |
+
///
|
| 508 |
+
/// Convention: usercall(ESI=wrapper), plain RET.
|
| 509 |
+
///
|
| 510 |
+
/// Sets display layer color parameters via display->vtable[4](layer, color).
|
| 511 |
+
/// Layer 1 color depends on gfx_mode and game_version.
|
| 512 |
+
/// Layer 2 = 0x20, Layer 3 = 0x70.
|
| 513 |
+
///
|
| 514 |
+
/// # Safety
|
| 515 |
+
/// `wrapper` must be a valid DDGameWrapper with initialized display and ddgame.
|
| 516 |
+
#[cfg(target_arch = "x86")]
|
| 517 |
+
pub unsafe fn display_layer_color_init(wrapper: *mut DDGameWrapper) {
|
| 518 |
+
let ddgame = (*wrapper).ddgame;
|
| 519 |
+
let game_info = (*ddgame).game_info;
|
| 520 |
+
let game_version = (*game_info).game_version;
|
| 521 |
+
|
| 522 |
+
// wrapper+0x4C8 is gfx_mode (at DDGameWrapper.gfx_mode)
|
| 523 |
+
let layer1_color = if (*wrapper).gfx_mode == 0 {
|
| 524 |
+
// (game_version > -2) - 1: yields 0 if true, -1 if false
|
| 525 |
+
// Then + 0x69: yields 0x69 or 0x68
|
| 526 |
+
if game_version > -2 { 0x69i32 } else { 0x68i32 }
|
| 527 |
+
} else {
|
| 528 |
+
5 + 0x69 // = 0x6E
|
| 529 |
+
};
|
| 530 |
+
|
| 531 |
+
let display = (*wrapper).display as *mut u8;
|
| 532 |
+
let vt = *(display as *const *const u32);
|
| 533 |
+
// vtable[4] (offset 0x10): thiscall(display, layer, color), RET 0x8
|
| 534 |
+
let set_color: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
|
| 535 |
+
core::mem::transmute(*vt.add(4));
|
| 536 |
+
|
| 537 |
+
set_color(display, 1, layer1_color);
|
| 538 |
+
set_color(display, 2, 0x20);
|
| 539 |
+
set_color(display, 3, 0x70);
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
/// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300).
|
| 543 |
+
///
|
| 544 |
+
/// Convention: usercall(ECX=gfx_handler, EAX=name) + 1 stack(output), RET 0x4.
|
| 545 |
+
///
|
| 546 |
+
/// Looks up `name` in the GfxHandler's directory, tries cached load, wraps
|
| 547 |
+
/// as DisplayGfx. Falls back to loading the raw image and decoding it.
|
| 548 |
+
///
|
| 549 |
+
/// # Safety
|
| 550 |
+
/// All pointers must be valid WA objects.
|
| 551 |
+
#[cfg(target_arch = "x86")]
|
| 552 |
+
pub unsafe fn gfx_resource_create(
|
| 553 |
+
gfx_handler: *mut u8,
|
| 554 |
+
name: *const u8,
|
| 555 |
+
output: *mut u8,
|
| 556 |
+
) -> *mut u8 {
|
| 557 |
+
// 1. Try FindEntry β cached load β DisplayGfx wrap
|
| 558 |
+
let entry = call_gfx_find_entry(name, gfx_handler);
|
| 559 |
+
if !entry.is_null() {
|
| 560 |
+
// gfx_handler->vtable[2](entry->field_4) β cached load
|
| 561 |
+
let vt = *(gfx_handler as *const *const u32);
|
| 562 |
+
let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
|
| 563 |
+
core::mem::transmute(*vt.add(2));
|
| 564 |
+
let entry_val = *(entry.add(4) as *const u32);
|
| 565 |
+
let cached = load_cached(gfx_handler, entry_val);
|
| 566 |
+
if !cached.is_null() {
|
| 567 |
+
// DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
|
| 568 |
+
let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
|
| 569 |
+
core::mem::transmute(rb(va::DISPLAYGFX_CONSTRUCTOR) as usize);
|
| 570 |
+
return ctor(cached);
|
| 571 |
+
}
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
// 2. Fallback: LoadImage β IMG_Decode
|
| 575 |
+
let raw_image = call_gfx_load_image(gfx_handler, name);
|
| 576 |
+
if raw_image.is_null() {
|
| 577 |
+
return core::ptr::null_mut();
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
// IMG_Decode: stdcall(output, raw_image, 1), RET 0xC
|
| 581 |
+
let decode: unsafe extern "stdcall" fn(*mut u8, *mut u8, i32) -> *mut u8 =
|
| 582 |
+
core::mem::transmute(rb(va::IMG_DECODE) as usize);
|
| 583 |
+
let result = decode(output, raw_image, 1);
|
| 584 |
+
|
| 585 |
+
// Release raw image: raw_image->vtable[0](1)
|
| 586 |
+
let img_vt = *(raw_image as *const *const u32);
|
| 587 |
+
let release: unsafe extern "thiscall" fn(*mut u8, i32) =
|
| 588 |
+
core::mem::transmute(*img_vt);
|
| 589 |
+
release(raw_image, 1);
|
| 590 |
+
|
| 591 |
+
result
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
/// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
|
| 595 |
static mut INIT_FIELDS_ADDR: u32 = 0;
|
| 596 |
|
|
|
|
| 666 |
}
|
| 667 |
core::ptr::write_bytes(ddgame as *mut u8, 0, 0x98B8);
|
| 668 |
|
| 669 |
+
// ββ 2. InitFields β pure Rust (replaces usercall bridge) ββ
|
| 670 |
+
ddgame_init_fields(ddgame);
|
|
|
|
| 671 |
|
| 672 |
// ββ 3-4. Store params BEFORE exposing DDGame via wrapper ββ
|
| 673 |
// Critical: game_info must be set before wrapper->ddgame, because the
|
|
|
|
| 1024 |
};
|
| 1025 |
|
| 1026 |
// ββ TaskStateMachine at DDGame+0x380 (alloc 0x4C, memset 0x2C) ββ
|
| 1027 |
+
// Pure Rust: allocate object, call task_state_machine_init, override vtable.
|
|
|
|
| 1028 |
{
|
| 1029 |
let tsm = wa_malloc(0x4C);
|
| 1030 |
core::ptr::write_bytes(tsm, 0, 0x2C);
|
| 1031 |
if !tsm.is_null() {
|
| 1032 |
+
let width = (*ddgame).level_width;
|
| 1033 |
+
let height = (*ddgame).level_height;
|
| 1034 |
+
task_state_machine_init(tsm, 1, width, height);
|
| 1035 |
+
*(tsm as *mut u32) = rb(0x664118); // Override vtable to TaskStateMachine
|
| 1036 |
}
|
| 1037 |
(*ddgame).task_state_machine = tsm;
|
| 1038 |
}
|
|
@@ -53,6 +53,11 @@ fn run() -> Result<(), String> {
|
|
| 53 |
let _ = clear_log();
|
| 54 |
let _ = std::fs::write("OpenWA_validation.log", "");
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
let delta = openwa_core::rebase::init();
|
| 57 |
let _ = log_line(&format!(
|
| 58 |
"=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}"
|
|
|
|
| 53 |
let _ = clear_log();
|
| 54 |
let _ = std::fs::write("OpenWA_validation.log", "");
|
| 55 |
|
| 56 |
+
// Install panic hook that writes to our log file
|
| 57 |
+
std::panic::set_hook(Box::new(|info| {
|
| 58 |
+
let _ = log_line(&format!("[PANIC] {info}"));
|
| 59 |
+
}));
|
| 60 |
+
|
| 61 |
let delta = openwa_core::rebase::init();
|
| 62 |
let _ = log_line(&format!(
|
| 63 |
"=== OpenWA WormKit DLL loaded ===\n ASLR delta: 0x{delta:08X}"
|
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Rust replacements for DDGame constructor sub-functions.
|
| 2 |
+
//!
|
| 3 |
+
//! Each function is hooked individually so it works regardless of whether the
|
| 4 |
+
//! DDGame constructor itself is Rust or the original WA code.
|
| 5 |
+
//!
|
| 6 |
+
//! ## Ported functions
|
| 7 |
+
//!
|
| 8 |
+
//! - `DDGame__InitFields` (0x526120): usercall(EDI=ddgame), plain RET β EAX=ddgame
|
| 9 |
+
//! - `DDGame__InitRenderIndices` (0x526080): usercall(ESI=base), plain RET β EAX=base
|
| 10 |
+
//! - `TaskStateMachine__Init` (0x4F6370): usercall(ESI,ECX,EDI) + 1 stack, RET 0x4
|
| 11 |
+
//! - `GfxResource__Create_Maybe` (0x4F6300): usercall(ECX,EAX) + 1 stack, RET 0x4
|
| 12 |
+
//! - `FUN_570E20` (display layer init): usercall(ESI=wrapper), plain RET
|
| 13 |
+
|
| 14 |
+
use openwa_core::address::va;
|
| 15 |
+
use openwa_core::engine::ddgame::{
|
| 16 |
+
DDGame, ddgame_init_fields, ddgame_init_render_indices,
|
| 17 |
+
task_state_machine_init, gfx_resource_create, display_layer_color_init,
|
| 18 |
+
};
|
| 19 |
+
use openwa_core::engine::DDGameWrapper;
|
| 20 |
+
use crate::hook;
|
| 21 |
+
|
| 22 |
+
// βββ DDGame__InitFields (0x526120) ββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
hook::usercall_trampoline!(
|
| 25 |
+
fn init_fields_trampoline;
|
| 26 |
+
impl_fn = impl_init_fields;
|
| 27 |
+
reg = edi
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
extern "cdecl" fn impl_init_fields(ddgame: u32) -> u32 {
|
| 31 |
+
unsafe { ddgame_init_fields(ddgame as *mut DDGame) }
|
| 32 |
+
ddgame // Original: MOV EAX, EDI; RET
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// βββ DDGame__InitRenderIndices (0x526080) βββββββββββββββββββββββββββββββββββ
|
| 36 |
+
|
| 37 |
+
hook::usercall_trampoline!(
|
| 38 |
+
fn init_render_indices_trampoline;
|
| 39 |
+
impl_fn = impl_init_render_indices;
|
| 40 |
+
reg = esi
|
| 41 |
+
);
|
| 42 |
+
|
| 43 |
+
extern "cdecl" fn impl_init_render_indices(base: u32) -> u32 {
|
| 44 |
+
unsafe { ddgame_init_render_indices(base as *mut u8) }
|
| 45 |
+
base // Original: MOV EAX, ESI; RET
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// βββ TaskStateMachine__Init (0x4F6370) ββββββββββββββββββββββββββββββββββββββ
|
| 49 |
+
|
| 50 |
+
extern "cdecl" fn impl_tsm_init(object: u32, param1: u32, height: u32, width: u32) -> u32 {
|
| 51 |
+
unsafe { task_state_machine_init(object as *mut u8, param1, width, height) }
|
| 52 |
+
object // Original: MOV EAX, ESI; RET 0x4
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
#[unsafe(naked)]
|
| 56 |
+
unsafe extern "C" fn tsm_init_trampoline() {
|
| 57 |
+
core::arch::naked_asm!(
|
| 58 |
+
"push edx",
|
| 59 |
+
"push [esp+8]", // width (stack param)
|
| 60 |
+
"push edi", // height
|
| 61 |
+
"push ecx", // param1
|
| 62 |
+
"push esi", // object
|
| 63 |
+
"call {impl_fn}",
|
| 64 |
+
"add esp, 16",
|
| 65 |
+
"pop edx",
|
| 66 |
+
"ret 0x4",
|
| 67 |
+
impl_fn = sym impl_tsm_init,
|
| 68 |
+
);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// βββ FUN_570E20 (display layer color init) ββββββββββββββββββββββββββββββββββ
|
| 72 |
+
|
| 73 |
+
hook::usercall_trampoline!(
|
| 74 |
+
fn display_layer_init_trampoline;
|
| 75 |
+
impl_fn = impl_display_layer_init;
|
| 76 |
+
reg = esi
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
extern "cdecl" fn impl_display_layer_init(wrapper: u32) -> u32 {
|
| 80 |
+
unsafe { display_layer_color_init(wrapper as *mut DDGameWrapper) }
|
| 81 |
+
wrapper
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// βββ GfxResource__Create_Maybe (0x4F6300) βββββββββββββββββββββββββββββββββββ
|
| 85 |
+
|
| 86 |
+
extern "cdecl" fn impl_gfx_resource_create(
|
| 87 |
+
gfx_handler: u32, name: u32, output: u32,
|
| 88 |
+
) -> u32 {
|
| 89 |
+
let result = unsafe {
|
| 90 |
+
gfx_resource_create(gfx_handler as *mut u8, name as *const u8, output as *mut u8)
|
| 91 |
+
};
|
| 92 |
+
result as u32
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
#[unsafe(naked)]
|
| 96 |
+
unsafe extern "C" fn gfx_resource_create_trampoline() {
|
| 97 |
+
core::arch::naked_asm!(
|
| 98 |
+
"push edx",
|
| 99 |
+
"push [esp+8]", // output (stack param)
|
| 100 |
+
"push eax", // name
|
| 101 |
+
"push ecx", // gfx_handler
|
| 102 |
+
"call {impl_fn}",
|
| 103 |
+
"add esp, 12",
|
| 104 |
+
"pop edx",
|
| 105 |
+
"ret 0x4",
|
| 106 |
+
impl_fn = sym impl_gfx_resource_create,
|
| 107 |
+
);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// βββ Hook installation ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 111 |
+
|
| 112 |
+
pub fn install() -> Result<(), String> {
|
| 113 |
+
unsafe {
|
| 114 |
+
hook::install(
|
| 115 |
+
"DDGame__InitFields",
|
| 116 |
+
va::DDGAME_INIT_FIELDS,
|
| 117 |
+
init_fields_trampoline as *const (),
|
| 118 |
+
)?;
|
| 119 |
+
|
| 120 |
+
hook::install(
|
| 121 |
+
"DDGame__InitRenderIndices",
|
| 122 |
+
va::DDGAME_INIT_RENDER_INDICES,
|
| 123 |
+
init_render_indices_trampoline as *const (),
|
| 124 |
+
)?;
|
| 125 |
+
|
| 126 |
+
hook::install(
|
| 127 |
+
"TaskStateMachine__Init",
|
| 128 |
+
va::TASK_STATE_MACHINE_INIT,
|
| 129 |
+
tsm_init_trampoline as *const (),
|
| 130 |
+
)?;
|
| 131 |
+
|
| 132 |
+
hook::install(
|
| 133 |
+
"GfxResource__Create",
|
| 134 |
+
va::GFX_RESOURCE_CREATE,
|
| 135 |
+
gfx_resource_create_trampoline as *const (),
|
| 136 |
+
)?;
|
| 137 |
+
|
| 138 |
+
hook::install(
|
| 139 |
+
"FUN_570E20_DisplayLayerInit",
|
| 140 |
+
va::FUN_570E20,
|
| 141 |
+
display_layer_init_trampoline as *const (),
|
| 142 |
+
)?;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
Ok(())
|
| 146 |
+
}
|
|
@@ -113,7 +113,7 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
|
|
| 113 |
));
|
| 114 |
|
| 115 |
// Toggle: use Rust constructor (true) or original (false)
|
| 116 |
-
const USE_RUST_CTOR: bool =
|
| 117 |
if USE_RUST_CTOR {
|
| 118 |
create_ddgame(
|
| 119 |
this,
|
|
|
|
| 113 |
));
|
| 114 |
|
| 115 |
// Toggle: use Rust constructor (true) or original (false)
|
| 116 |
+
const USE_RUST_CTOR: bool = false;
|
| 117 |
if USE_RUST_CTOR {
|
| 118 |
create_ddgame(
|
| 119 |
this,
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
mod config;
|
|
|
|
| 2 |
mod display;
|
| 3 |
mod frontend;
|
| 4 |
mod game_session;
|
|
@@ -26,5 +27,6 @@ pub fn install_all() -> Result<(), String> {
|
|
| 26 |
sound::install()?;
|
| 27 |
speech::install()?;
|
| 28 |
input::install()?;
|
|
|
|
| 29 |
Ok(())
|
| 30 |
}
|
|
|
|
| 1 |
mod config;
|
| 2 |
+
mod ddgame_init;
|
| 3 |
mod display;
|
| 4 |
mod frontend;
|
| 5 |
mod game_session;
|
|
|
|
| 27 |
sound::install()?;
|
| 28 |
speech::install()?;
|
| 29 |
input::install()?;
|
| 30 |
+
ddgame_init::install()?;
|
| 31 |
Ok(())
|
| 32 |
}
|
|
@@ -179,5 +179,15 @@ if ($Headless) {
|
|
| 179 |
Write-Host " Validation: NOT found in log (may not have reached gameplay)" -ForegroundColor Yellow
|
| 180 |
}
|
| 181 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
Write-Host " Logs: $logDir\" -ForegroundColor Gray
|
| 183 |
}
|
|
|
|
| 179 |
Write-Host " Validation: NOT found in log (may not have reached gameplay)" -ForegroundColor Yellow
|
| 180 |
}
|
| 181 |
}
|
| 182 |
+
# Check for Rust panics in OpenWA log
|
| 183 |
+
if (Test-Path "$logDir\openwa_latest.log") {
|
| 184 |
+
$panics = Select-String -Path "$logDir\openwa_latest.log" -Pattern "\[PANIC\]"
|
| 185 |
+
if ($panics) {
|
| 186 |
+
Write-Host ""
|
| 187 |
+
Write-Host "=== RUST PANIC DETECTED ===" -ForegroundColor Red
|
| 188 |
+
$panics | ForEach-Object { Write-Host " $($_.Line)" -ForegroundColor Red }
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
Write-Host " Logs: $logDir\" -ForegroundColor Gray
|
| 193 |
}
|