feat: expand CTaskTeam struct with runtime-validated field names
Browse filesMemory dump analysis via replay test revealed 6 named fields inside the
previously opaque 0x4C-byte region (0x3C–0x87) and the tail (0x400–0x45F):
alive_worm_count (+0x48): decrements as worms are eliminated
last_launched_weapon (+0x60): confirmed by wkJellyWorm (lastLaunchedWeapon_dword60)
pos_x / pos_y (+0x404/+0x408): per-team Fixed16.16 position
_unknown_40c (+0x40C): Fixed16.16, same across teams (≈666.89 in test)
Also adds dump_ctaskteam_entities() to the validation module (auto-runs at 5s
in replay-test mode, F8 hotkey in interactive mode) and 5 new offset checks.
117/117 PASS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
crates/openwa-core/src/task.rs
CHANGED
|
@@ -589,16 +589,51 @@ pub struct CTaskTeam {
|
|
| 589 |
pub base: CTask,
|
| 590 |
/// 0x30: Secondary interface vtable pointer (Ghidra 0x00669F00)
|
| 591 |
pub _secondary_vtable: u32,
|
|
|
|
| 592 |
pub _unknown_34: u32,
|
| 593 |
/// 0x38: Team number, **1-based**. Matches TeamArenaState slot index.
|
| 594 |
pub team_index: u32,
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
///
|
| 598 |
-
pub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
/// 0x218: Number of CTaskWorm children constructed for this team.
|
| 600 |
pub worm_count: u32,
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
}
|
| 603 |
|
| 604 |
const _: () = assert!(core::mem::size_of::<CTaskTeam>() == 0x460);
|
|
|
|
| 589 |
pub base: CTask,
|
| 590 |
/// 0x30: Secondary interface vtable pointer (Ghidra 0x00669F00)
|
| 591 |
pub _secondary_vtable: u32,
|
| 592 |
+
/// 0x34: Unknown — observed to hold the same value as `team_index` in all runs.
|
| 593 |
pub _unknown_34: u32,
|
| 594 |
/// 0x38: Team number, **1-based**. Matches TeamArenaState slot index.
|
| 595 |
pub team_index: u32,
|
| 596 |
+
/// 0x3C: Unknown — observed 0 for team 1, 1 for team 2.
|
| 597 |
+
pub _unknown_3c: u32,
|
| 598 |
+
/// 0x40: Unknown signed integer — observed −40 for team 1, −20 for team 2.
|
| 599 |
+
pub _unknown_40: i32,
|
| 600 |
+
/// 0x44: Unknown (always 0 in observed runs).
|
| 601 |
+
pub _unknown_44: u32,
|
| 602 |
+
/// 0x48: Number of living worms remaining on this team.
|
| 603 |
+
/// Decrements as worms are eliminated; equals `worm_count` at game start.
|
| 604 |
+
pub alive_worm_count: u32,
|
| 605 |
+
/// 0x4C–0x5B: Unknown.
|
| 606 |
+
pub _unknown_4c: [u8; 16],
|
| 607 |
+
/// 0x5C: Unknown — consistently 11 across teams and runs.
|
| 608 |
+
pub _unknown_5c: u32,
|
| 609 |
+
/// 0x60: Index of the last weapon launched by this team (0 = none launched yet).
|
| 610 |
+
/// Field name confirmed by wkJellyWorm: `lastLaunchedWeapon_dword60`.
|
| 611 |
+
pub last_launched_weapon: u32,
|
| 612 |
+
/// 0x64–0x87: Unknown.
|
| 613 |
+
pub _unknown_64: [u8; 36],
|
| 614 |
+
/// 0x88–0x217: Unknown region (100 DWORDs). Observed non-zero values at the start
|
| 615 |
+
/// (+0x88 = team_index, +0x8C/+0x90 = 3) in a 2-worm bot game; purpose unclear.
|
| 616 |
+
pub _unknown_88: [u32; 100],
|
| 617 |
/// 0x218: Number of CTaskWorm children constructed for this team.
|
| 618 |
pub worm_count: u32,
|
| 619 |
+
/// 0x21C–0x3EB: Unknown.
|
| 620 |
+
pub _unknown_21c: [u8; 0x1D0],
|
| 621 |
+
/// 0x3EC–0x3F3: Unknown flags (observed: 1, 1 then 0xFFFFFFFF×3 at +0x3F4).
|
| 622 |
+
pub _unknown_3ec: [u32; 2],
|
| 623 |
+
/// 0x3F4–0x3FF: Sentinel values (observed: 0xFFFFFFFF in all runs).
|
| 624 |
+
pub _sentinels_3f4: [u32; 3],
|
| 625 |
+
/// 0x400–0x403: Unknown.
|
| 626 |
+
pub _unknown_400: u32,
|
| 627 |
+
/// 0x404: X position (Fixed16.16) — per-team, likely spawn or last-worm position.
|
| 628 |
+
pub pos_x: Fixed,
|
| 629 |
+
/// 0x408: Y position (Fixed16.16) — per-team, likely spawn or last-worm position.
|
| 630 |
+
pub pos_y: Fixed,
|
| 631 |
+
/// 0x40C: Unknown Fixed16.16 value — same for all teams (observed ≈ 666.89).
|
| 632 |
+
pub _unknown_40c: Fixed,
|
| 633 |
+
/// 0x410: Unknown (observed 1 in all runs).
|
| 634 |
+
pub _unknown_410: u32,
|
| 635 |
+
/// 0x414–0x45F: Unknown.
|
| 636 |
+
pub _unknown_414: [u8; 0x4C],
|
| 637 |
}
|
| 638 |
|
| 639 |
const _: () = assert!(core::mem::size_of::<CTaskTeam>() == 0x460);
|
crates/openwa-wormkit/src/validation/mod.rs
CHANGED
|
@@ -299,8 +299,12 @@ fn validate_struct_offsets(result: &mut ValidationResult) {
|
|
| 299 |
|
| 300 |
let _ = log_validation("");
|
| 301 |
let _ = log_validation(" CTaskTeam:");
|
| 302 |
-
check_offset!(result, CTaskTeam, team_index,
|
| 303 |
-
check_offset!(result, CTaskTeam,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
let _ = log_validation("");
|
| 306 |
let _ = log_validation(" CTaskTurnGame:");
|
|
@@ -1020,6 +1024,69 @@ fn dump_turngame() {
|
|
| 1020 |
}
|
| 1021 |
}
|
| 1022 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
// ---------------------------------------------------------------------------
|
| 1024 |
// Public entry point — called from wormkit's run()
|
| 1025 |
// ---------------------------------------------------------------------------
|
|
@@ -1088,6 +1155,8 @@ pub fn run() -> Result<(), String> {
|
|
| 1088 |
dump_worm_tasks();
|
| 1089 |
let _ = log_validation(" Running TurnGame dump (5s mark)...");
|
| 1090 |
dump_turngame();
|
|
|
|
|
|
|
| 1091 |
|
| 1092 |
// Wait briefly for replay to start, then dump game state
|
| 1093 |
// (fast-forward finishes the replay in ~10-15s total, so dump early)
|
|
@@ -1142,15 +1211,17 @@ pub fn start_hotkeys() {
|
|
| 1142 |
|
| 1143 |
std::thread::spawn(|| {
|
| 1144 |
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
|
|
|
|
| 1145 |
const VK_F9: i32 = 0x78;
|
| 1146 |
const VK_F10: i32 = 0x79;
|
| 1147 |
const VK_F11: i32 = 0x7A;
|
| 1148 |
const VK_F12: i32 = 0x7B;
|
| 1149 |
-
let _ = log_validation(" Hotkeys: F9=team blocks, F10=landscape, F11=worm tasks, F12=entity census");
|
| 1150 |
loop {
|
| 1151 |
std::thread::sleep(std::time::Duration::from_millis(100));
|
| 1152 |
unsafe {
|
| 1153 |
-
if GetAsyncKeyState(
|
|
|
|
| 1154 |
if GetAsyncKeyState(VK_F10) & 1 != 0 { dump_landscape(); }
|
| 1155 |
if GetAsyncKeyState(VK_F11) & 1 != 0 { dump_worm_tasks(); }
|
| 1156 |
if GetAsyncKeyState(VK_F12) & 1 != 0 { dump_entity_census(); }
|
|
|
|
| 299 |
|
| 300 |
let _ = log_validation("");
|
| 301 |
let _ = log_validation(" CTaskTeam:");
|
| 302 |
+
check_offset!(result, CTaskTeam, team_index, 0x38);
|
| 303 |
+
check_offset!(result, CTaskTeam, alive_worm_count, 0x48);
|
| 304 |
+
check_offset!(result, CTaskTeam, last_launched_weapon, 0x60);
|
| 305 |
+
check_offset!(result, CTaskTeam, worm_count, 0x218);
|
| 306 |
+
check_offset!(result, CTaskTeam, pos_x, 0x404);
|
| 307 |
+
check_offset!(result, CTaskTeam, pos_y, 0x408);
|
| 308 |
|
| 309 |
let _ = log_validation("");
|
| 310 |
let _ = log_validation(" CTaskTurnGame:");
|
|
|
|
| 1024 |
}
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
+
// ---------------------------------------------------------------------------
|
| 1028 |
+
// CTaskTeam entity dump
|
| 1029 |
+
// ---------------------------------------------------------------------------
|
| 1030 |
+
|
| 1031 |
+
fn dump_ctaskteam_entities() {
|
| 1032 |
+
use openwa_core::task::CTaskTeam;
|
| 1033 |
+
use crate::replacements::input::dump_region;
|
| 1034 |
+
|
| 1035 |
+
let _ = log_validation("");
|
| 1036 |
+
let _ = log_validation("--- CTaskTeam Entity Dump ---");
|
| 1037 |
+
|
| 1038 |
+
unsafe {
|
| 1039 |
+
let session_ptr = read_u32(rb(va::G_GAME_SESSION));
|
| 1040 |
+
if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; }
|
| 1041 |
+
let wrapper_addr = read_u32(session_ptr + 0xA0);
|
| 1042 |
+
if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; }
|
| 1043 |
+
let ddgame_ptr = read_u32(wrapper_addr + 0x488);
|
| 1044 |
+
if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; }
|
| 1045 |
+
|
| 1046 |
+
let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
|
| 1047 |
+
if task_land_ptr == 0 { let _ = log_validation(" CTaskLand NULL — skipping."); return; }
|
| 1048 |
+
let task_land = task_land_ptr as *const openwa_core::task::CTask;
|
| 1049 |
+
let shared_data_ptr = (*task_land).shared_data;
|
| 1050 |
+
if shared_data_ptr.is_null() { let _ = log_validation(" shared_data NULL."); return; }
|
| 1051 |
+
|
| 1052 |
+
let table = SharedDataTable::from_ptr(shared_data_ptr);
|
| 1053 |
+
let expected_vt = rb(va::CTASK_TEAM_VTABLE);
|
| 1054 |
+
let mut found = 0u32;
|
| 1055 |
+
|
| 1056 |
+
for node in table.iter() {
|
| 1057 |
+
let entity = (*node).entity;
|
| 1058 |
+
if entity.is_null() { continue; }
|
| 1059 |
+
if !is_in_rdata(read_u32(entity as u32)) { continue; }
|
| 1060 |
+
if read_u32(entity as u32) != expected_vt { continue; }
|
| 1061 |
+
|
| 1062 |
+
let addr = entity as u32;
|
| 1063 |
+
let team = &*(addr as *const CTaskTeam);
|
| 1064 |
+
let _ = log_validation(&format!(
|
| 1065 |
+
" CTaskTeam[{}] @ 0x{:08X} team_index={} worm_count={} active={} last_weapon={} pos=({:.2},{:.2})",
|
| 1066 |
+
found, addr, team.team_index, team.worm_count,
|
| 1067 |
+
team.alive_worm_count, team.last_launched_weapon,
|
| 1068 |
+
team.pos_x.to_f32(), team.pos_y.to_f32()
|
| 1069 |
+
));
|
| 1070 |
+
|
| 1071 |
+
// Full memory dump in chunks — classify every DWORD.
|
| 1072 |
+
let base = addr as *const u8;
|
| 1073 |
+
dump_region(base, 0x000, 0x30, "CTaskTeam"); // CTask base
|
| 1074 |
+
dump_region(base, 0x030, 0x58, "CTaskTeam"); // 0x30..0x87 (secondary vtable, unknowns)
|
| 1075 |
+
dump_region(base, 0x088, 0x90, "CTaskTeam"); // 0x88..0x117 (item_slots start, worm_count region)
|
| 1076 |
+
dump_region(base, 0x118, 0x100, "CTaskTeam"); // 0x118..0x217 (item_slots end)
|
| 1077 |
+
dump_region(base, 0x218, 0x80, "CTaskTeam"); // 0x218..0x297 (worm_count + unknowns)
|
| 1078 |
+
dump_region(base, 0x298, 0x80, "CTaskTeam"); // 0x298..0x317
|
| 1079 |
+
dump_region(base, 0x318, 0x80, "CTaskTeam"); // 0x318..0x397
|
| 1080 |
+
dump_region(base, 0x398, 0x68, "CTaskTeam"); // 0x398..0x3FF
|
| 1081 |
+
dump_region(base, 0x400, 0x60, "CTaskTeam"); // 0x400..0x45F
|
| 1082 |
+
|
| 1083 |
+
found += 1;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
let _ = log_validation(&format!(" Total CTaskTeam entities: {}", found));
|
| 1087 |
+
}
|
| 1088 |
+
}
|
| 1089 |
+
|
| 1090 |
// ---------------------------------------------------------------------------
|
| 1091 |
// Public entry point — called from wormkit's run()
|
| 1092 |
// ---------------------------------------------------------------------------
|
|
|
|
| 1155 |
dump_worm_tasks();
|
| 1156 |
let _ = log_validation(" Running TurnGame dump (5s mark)...");
|
| 1157 |
dump_turngame();
|
| 1158 |
+
let _ = log_validation(" Running CTaskTeam entity dump (5s mark)...");
|
| 1159 |
+
dump_ctaskteam_entities();
|
| 1160 |
|
| 1161 |
// Wait briefly for replay to start, then dump game state
|
| 1162 |
// (fast-forward finishes the replay in ~10-15s total, so dump early)
|
|
|
|
| 1211 |
|
| 1212 |
std::thread::spawn(|| {
|
| 1213 |
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
|
| 1214 |
+
const VK_F8: i32 = 0x77;
|
| 1215 |
const VK_F9: i32 = 0x78;
|
| 1216 |
const VK_F10: i32 = 0x79;
|
| 1217 |
const VK_F11: i32 = 0x7A;
|
| 1218 |
const VK_F12: i32 = 0x7B;
|
| 1219 |
+
let _ = log_validation(" Hotkeys: F8=team entities, F9=team blocks, F10=landscape, F11=worm tasks, F12=entity census");
|
| 1220 |
loop {
|
| 1221 |
std::thread::sleep(std::time::Duration::from_millis(100));
|
| 1222 |
unsafe {
|
| 1223 |
+
if GetAsyncKeyState(VK_F8) & 1 != 0 { dump_ctaskteam_entities(); }
|
| 1224 |
+
if GetAsyncKeyState(VK_F9) & 1 != 0 { dump_team_blocks(); }
|
| 1225 |
if GetAsyncKeyState(VK_F10) & 1 != 0 { dump_landscape(); }
|
| 1226 |
if GetAsyncKeyState(VK_F11) & 1 != 0 { dump_worm_tasks(); }
|
| 1227 |
if GetAsyncKeyState(VK_F12) & 1 != 0 { dump_entity_census(); }
|