fix(types): correct CTask struct layout from Ghidra decompilation
Browse filesExpanded opaque padding into named fields based on cross-referencing
Ghidra decompilation of CTask::Constructor (0x5625A0) and its helper
(0x562520) with wkJellyWorm CTask.h. Key findings:
- 3rd constructor param is DDGame pointer (stored at 0x2C), not class_type
- Children list fields (0x08-0x18) now individually named
- shared_data (0x24) and owns_shared_data (0x28) identified
- 82/82 static validation checks pass (up from 78)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crates/openwa-harness/src/lib.rs
CHANGED
|
@@ -551,8 +551,8 @@ mod tests {
|
|
| 551 |
eprintln!(" Security cookie initialized");
|
| 552 |
}
|
| 553 |
|
| 554 |
-
// CTask::Constructor is __stdcall: (this, parent,
|
| 555 |
-
type CTaskCtorFn = unsafe extern "stdcall" fn(this: *mut u8, parent: u32,
|
| 556 |
|
| 557 |
unsafe {
|
| 558 |
let ctor: CTaskCtorFn = img.func_ptr(va::CTASK_CONSTRUCTOR);
|
|
@@ -576,9 +576,9 @@ mod tests {
|
|
| 576 |
va::CTASK_VTABLE
|
| 577 |
);
|
| 578 |
|
| 579 |
-
// Verify
|
| 580 |
-
let
|
| 581 |
-
assert_eq!(
|
| 582 |
|
| 583 |
// Verify field at 0x08 is 0x10
|
| 584 |
let field_08 = *((task_ptr as usize + 0x08) as *const u32);
|
|
@@ -591,7 +591,7 @@ mod tests {
|
|
| 591 |
eprintln!("CTask::Constructor succeeded!");
|
| 592 |
eprintln!(" vtable = 0x{vtable_ghidra:08X}");
|
| 593 |
eprintln!(" children = 0x{children:08X}");
|
| 594 |
-
eprintln!("
|
| 595 |
}
|
| 596 |
}
|
| 597 |
}
|
|
|
|
| 551 |
eprintln!(" Security cookie initialized");
|
| 552 |
}
|
| 553 |
|
| 554 |
+
// CTask::Constructor is __stdcall: (this, parent, ddgame) -> this
|
| 555 |
+
type CTaskCtorFn = unsafe extern "stdcall" fn(this: *mut u8, parent: u32, ddgame: u32) -> *mut u8;
|
| 556 |
|
| 557 |
unsafe {
|
| 558 |
let ctor: CTaskCtorFn = img.func_ptr(va::CTASK_CONSTRUCTOR);
|
|
|
|
| 576 |
va::CTASK_VTABLE
|
| 577 |
);
|
| 578 |
|
| 579 |
+
// Verify ddgame pointer at offset 0x2C (3rd ctor param, we passed 0)
|
| 580 |
+
let stored_ddgame = *((task_ptr as usize + 0x2C) as *const u32);
|
| 581 |
+
assert_eq!(stored_ddgame, 0, "ddgame should be 0 (we passed 0)");
|
| 582 |
|
| 583 |
// Verify field at 0x08 is 0x10
|
| 584 |
let field_08 = *((task_ptr as usize + 0x08) as *const u32);
|
|
|
|
| 591 |
eprintln!("CTask::Constructor succeeded!");
|
| 592 |
eprintln!(" vtable = 0x{vtable_ghidra:08X}");
|
| 593 |
eprintln!(" children = 0x{children:08X}");
|
| 594 |
+
eprintln!(" ddgame = {stored_ddgame}");
|
| 595 |
}
|
| 596 |
}
|
| 597 |
}
|
crates/openwa-types/src/task.rs
CHANGED
|
@@ -9,9 +9,7 @@ pub type Ptr32 = u32;
|
|
| 9 |
/// All game objects inherit from CTask. Tasks form a tree via parent/children
|
| 10 |
/// pointers and communicate through the TaskMessage system.
|
| 11 |
///
|
| 12 |
-
///
|
| 13 |
-
///
|
| 14 |
-
/// Source: wkJellyWorm CTask.h
|
| 15 |
///
|
| 16 |
/// Vtable at 0x669F8C (8 methods):
|
| 17 |
/// 0x00: 0x562710 vtable0 (init?)
|
|
@@ -28,15 +26,28 @@ pub struct CTask {
|
|
| 28 |
pub vtable: Ptr32,
|
| 29 |
/// 0x04: Parent task in the hierarchy
|
| 30 |
pub parent: Ptr32,
|
| 31 |
-
/// 0x08:
|
| 32 |
-
pub
|
| 33 |
-
/// 0x0C
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
pub class_type: ClassType,
|
| 38 |
-
/// 0x24
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
|
| 42 |
const _: () = assert!(core::mem::size_of::<CTask>() == 0x30);
|
|
|
|
| 9 |
/// All game objects inherit from CTask. Tasks form a tree via parent/children
|
| 10 |
/// pointers and communicate through the TaskMessage system.
|
| 11 |
///
|
| 12 |
+
/// Source: wkJellyWorm CTask.h, Ghidra decompilation of 0x5625A0 + 0x562520
|
|
|
|
|
|
|
| 13 |
///
|
| 14 |
/// Vtable at 0x669F8C (8 methods):
|
| 15 |
/// 0x00: 0x562710 vtable0 (init?)
|
|
|
|
| 26 |
pub vtable: Ptr32,
|
| 27 |
/// 0x04: Parent task in the hierarchy
|
| 28 |
pub parent: Ptr32,
|
| 29 |
+
/// 0x08: Children list max capacity (set to 0x10 in constructor)
|
| 30 |
+
pub children_max_size: u32,
|
| 31 |
+
/// 0x0C: Children list unknown field (set to 0 in constructor)
|
| 32 |
+
pub children_unk: u32,
|
| 33 |
+
/// 0x10: Children list current size
|
| 34 |
+
pub children_size: u32,
|
| 35 |
+
/// 0x14: Pointer to children data array (allocated 0x60 bytes in constructor)
|
| 36 |
+
pub children_data: Ptr32,
|
| 37 |
+
/// 0x18: Children hash list pointer (set to 0 in constructor)
|
| 38 |
+
pub children_hash: Ptr32,
|
| 39 |
+
/// 0x1C: Unknown (set to 0 by parent-linking helper FUN_00562520)
|
| 40 |
+
pub _unknown_1c: u32,
|
| 41 |
+
/// 0x20: Task classification type (set to ClassType::Task by FUN_00562520,
|
| 42 |
+
/// overridden by derived constructors)
|
| 43 |
pub class_type: ClassType,
|
| 44 |
+
/// 0x24: Shared data buffer pointer (inherited from parent, or allocated
|
| 45 |
+
/// 0x420 bytes for root tasks)
|
| 46 |
+
pub shared_data: Ptr32,
|
| 47 |
+
/// 0x28: 1 if this task owns shared_data (root), 0 if inherited from parent
|
| 48 |
+
pub owns_shared_data: u32,
|
| 49 |
+
/// 0x2C: DDGame pointer (3rd param to CTask::Constructor, stored at this+0x2C)
|
| 50 |
+
pub ddgame: Ptr32,
|
| 51 |
}
|
| 52 |
|
| 53 |
const _: () = assert!(core::mem::size_of::<CTask>() == 0x30);
|
crates/openwa-validator/src/hooks/detour.rs
CHANGED
|
@@ -57,14 +57,15 @@ unsafe extern "stdcall" fn hook_frontend_change_screen(screen_id: u32) {
|
|
| 57 |
static ORIG_CTASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
|
| 58 |
|
| 59 |
/// Hook for CTask::Constructor (0x5625A0).
|
| 60 |
-
/// stdcall(this, parent,
|
| 61 |
-
|
|
|
|
| 62 |
let orig: unsafe extern "stdcall" fn(u32, u32, u32) -> u32 =
|
| 63 |
core::mem::transmute(ORIG_CTASK_CONSTRUCTOR.load(Ordering::Relaxed));
|
| 64 |
-
let result = orig(this, parent,
|
| 65 |
// Log after calling original (so vtable is set)
|
| 66 |
let _ = log_line(&format!(
|
| 67 |
-
"[HOOK] CTask::ctor(this=0x{this:08X}, parent=0x{parent:08X},
|
| 68 |
));
|
| 69 |
result
|
| 70 |
}
|
|
|
|
| 57 |
static ORIG_CTASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
|
| 58 |
|
| 59 |
/// Hook for CTask::Constructor (0x5625A0).
|
| 60 |
+
/// stdcall(this, parent, ddgame) -> this
|
| 61 |
+
/// 3rd param is the DDGame pointer, stored at this+0x2C.
|
| 62 |
+
unsafe extern "stdcall" fn hook_ctask_constructor(this: u32, parent: u32, ddgame: u32) -> u32 {
|
| 63 |
let orig: unsafe extern "stdcall" fn(u32, u32, u32) -> u32 =
|
| 64 |
core::mem::transmute(ORIG_CTASK_CONSTRUCTOR.load(Ordering::Relaxed));
|
| 65 |
+
let result = orig(this, parent, ddgame);
|
| 66 |
// Log after calling original (so vtable is set)
|
| 67 |
let _ = log_line(&format!(
|
| 68 |
+
"[HOOK] CTask::ctor(this=0x{this:08X}, parent=0x{parent:08X}, ddgame=0x{ddgame:08X}) -> 0x{result:08X}"
|
| 69 |
));
|
| 70 |
result
|
| 71 |
}
|
crates/openwa-validator/src/lib.rs
CHANGED
|
@@ -324,8 +324,12 @@ fn validate_struct_offsets(result: &mut ValidationResult) {
|
|
| 324 |
let _ = log_line(" CTask:");
|
| 325 |
check_offset!(result, CTask, vtable, 0x00);
|
| 326 |
check_offset!(result, CTask, parent, 0x04);
|
| 327 |
-
check_offset!(result, CTask,
|
|
|
|
| 328 |
check_offset!(result, CTask, class_type, 0x20);
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
// CGameTask offsets
|
| 331 |
let _ = log_line("");
|
|
|
|
| 324 |
let _ = log_line(" CTask:");
|
| 325 |
check_offset!(result, CTask, vtable, 0x00);
|
| 326 |
check_offset!(result, CTask, parent, 0x04);
|
| 327 |
+
check_offset!(result, CTask, children_max_size, 0x08);
|
| 328 |
+
check_offset!(result, CTask, children_data, 0x14);
|
| 329 |
check_offset!(result, CTask, class_type, 0x20);
|
| 330 |
+
check_offset!(result, CTask, shared_data, 0x24);
|
| 331 |
+
check_offset!(result, CTask, owns_shared_data, 0x28);
|
| 332 |
+
check_offset!(result, CTask, ddgame, 0x2C);
|
| 333 |
|
| 334 |
// CGameTask offsets
|
| 335 |
let _ = log_line("");
|