paavohtl Claude Sonnet 4.6 commited on
Commit
5785d5c
·
1 Parent(s): f3cbb8a

fix: correct CTask children field names and children count display

Browse files

Runtime analysis of CTask__AppendChild (0x4fdc70) and CTask__UnlinkChild
(0x4fdce0) reveals the true semantics of the children sub-struct:

+0x08 children_capacity — starts 0x10, doubles on overflow
+0x0C children_dirty — flag set to 1 when any child is unlinked
+0x10 children_watermark — insertion counter, never decremented on remove
+0x14 children_data — sparse pointer array; dead slots are nulled

The append-only watermark means CTaskFilter's "(N children)" label was
rising unboundedly as sea bubbles spawned. Fixed by computing live count
from non-null slots rather than reading children_watermark directly.

Also adds dump_ctask_children() to validation (F8 hotkey) for live
verification of these fields. Ghidra functions renamed accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

crates/openwa-core/src/task/base.rs CHANGED
@@ -23,13 +23,18 @@ pub struct CTask {
23
  pub vtable: *mut u8,
24
  /// 0x04: Parent task in the hierarchy
25
  pub parent: *mut u8,
26
- /// 0x08: Children list max capacity (set to 0x10 in constructor)
27
- pub children_max_size: u32,
28
- /// 0x0C: Children list unknown field (set to 0 in constructor)
29
- pub children_unk: u32,
30
- /// 0x10: Children list current size
31
- pub children_size: u32,
32
- /// 0x14: Pointer to children data array (allocated 0x60 bytes in constructor)
 
 
 
 
 
33
  pub children_data: *mut u8,
34
  /// 0x18: Children hash list pointer (set to 0 in constructor)
35
  pub children_hash: *mut u8,
 
23
  pub vtable: *mut u8,
24
  /// 0x04: Parent task in the hierarchy
25
  pub parent: *mut u8,
26
+ /// 0x08: Children array capacity — starts at 0x10, doubles via realloc when full.
27
+ pub children_capacity: u32,
28
+ /// 0x0C: Set to 1 by `FUN_004fdce0` when a child slot is nulled (dirty flag).
29
+ /// Zero at construction. Not decremented; purely a "child was removed" marker.
30
+ pub children_dirty: u32,
31
+ /// 0x10: Insert watermark — incremented on every child insertion, never decremented
32
+ /// on removal. Dead children leave null slots; `children_data[0..children_watermark]`
33
+ /// is a sparse array. This grows without bound within a session (e.g., sea bubbles
34
+ /// continuously spawn/die, each consuming a new slot and doubling capacity as needed).
35
+ pub children_watermark: u32,
36
+ /// 0x14: Pointer to children data array (sparse, allocated 0x60 bytes initially,
37
+ /// reallocated to `children_capacity * 8 + 0x20` bytes on overflow).
38
  pub children_data: *mut u8,
39
  /// 0x18: Children hash list pointer (set to 0 in constructor)
40
  pub children_hash: *mut u8,
crates/openwa-debugui/src/app.rs CHANGED
@@ -76,11 +76,11 @@ unsafe fn get_ddgame() -> Option<*const DDGame> {
76
  /// Read child task pointers from a CTask's children array.
77
  ///
78
  /// The array is **sparse**: slots are nulled when a child is removed rather than
79
- /// compacted. `children_size` is the slot high-watermark (loop upper bound used
80
  /// by CTask::HandleMessage), not the live-child count. We return all slots up to
81
  /// that bound so the caller can filter nulls and display the live set.
82
  unsafe fn read_children(task: *const CTask) -> Vec<u32> {
83
- let slots = (*task).children_size as usize;
84
  let data = (*task).children_data as *const u32;
85
  if data.is_null() || slots == 0 { return Vec::new(); }
86
  // Hard safety cap: 4096 slots × 4 bytes = 16 KB max read
@@ -343,8 +343,8 @@ impl DebugApp {
343
  }
344
  ui.end_row();
345
 
346
- // children_size = slot high-watermark (sparse array); live = non-null slots
347
- ui.label("child slots"); ui.label(format!("{} used / {} cap", (*task).children_size, (*task).children_max_size)); ui.end_row();
348
  ui.label("class_type"); ui.label(format!("{:?}", (*task).class_type)); ui.end_row();
349
  });
350
  });
@@ -353,7 +353,7 @@ impl DebugApp {
353
  // read_children returns all slots (sparse); filter nulls for live count.
354
  let children = read_children(task);
355
  let live_count = children.iter().filter(|&&a| a != 0).count();
356
- let slot_count = (*task).children_size as usize;
357
  if live_count > 0 || slot_count > 0 {
358
  egui::CollapsingHeader::new(format!("Children ({} live / {} slots)", live_count, slot_count))
359
  .default_open(true)
@@ -365,7 +365,8 @@ impl DebugApp {
365
 
366
  // Expand inline if the child itself has children
367
  let child_task = child_addr as *const CTask;
368
- let grandchild_count = (*child_task).children_size;
 
369
 
370
  if grandchild_count > 0 {
371
  // Show as a sub-collapsing header with its own children
@@ -382,7 +383,6 @@ impl DebugApp {
382
  *navigate_to = Some(child_addr);
383
  }
384
  // Show grandchildren
385
- let grandchildren = read_children(child_task);
386
  for gc_addr in &grandchildren {
387
  let gc_addr = *gc_addr;
388
  if gc_addr == 0 { continue; }
 
76
  /// Read child task pointers from a CTask's children array.
77
  ///
78
  /// The array is **sparse**: slots are nulled when a child is removed rather than
79
+ /// compacted. `children_watermark` is the insertion counter (loop upper bound used
80
  /// by CTask::HandleMessage), not the live-child count. We return all slots up to
81
  /// that bound so the caller can filter nulls and display the live set.
82
  unsafe fn read_children(task: *const CTask) -> Vec<u32> {
83
+ let slots = (*task).children_watermark as usize;
84
  let data = (*task).children_data as *const u32;
85
  if data.is_null() || slots == 0 { return Vec::new(); }
86
  // Hard safety cap: 4096 slots × 4 bytes = 16 KB max read
 
343
  }
344
  ui.end_row();
345
 
346
+ // children_watermark = total insertions (sparse array); live = non-null slots
347
+ ui.label("child slots"); ui.label(format!("{} watermark / {} cap", (*task).children_watermark, (*task).children_capacity)); ui.end_row();
348
  ui.label("class_type"); ui.label(format!("{:?}", (*task).class_type)); ui.end_row();
349
  });
350
  });
 
353
  // read_children returns all slots (sparse); filter nulls for live count.
354
  let children = read_children(task);
355
  let live_count = children.iter().filter(|&&a| a != 0).count();
356
+ let slot_count = (*task).children_watermark as usize;
357
  if live_count > 0 || slot_count > 0 {
358
  egui::CollapsingHeader::new(format!("Children ({} live / {} slots)", live_count, slot_count))
359
  .default_open(true)
 
365
 
366
  // Expand inline if the child itself has children
367
  let child_task = child_addr as *const CTask;
368
+ let grandchildren = read_children(child_task);
369
+ let grandchild_count = grandchildren.iter().filter(|&&a| a != 0).count();
370
 
371
  if grandchild_count > 0 {
372
  // Show as a sub-collapsing header with its own children
 
383
  *navigate_to = Some(child_addr);
384
  }
385
  // Show grandchildren
 
386
  for gc_addr in &grandchildren {
387
  let gc_addr = *gc_addr;
388
  if gc_addr == 0 { continue; }
crates/openwa-wormkit/src/validation/mod.rs CHANGED
@@ -262,7 +262,7 @@ fn validate_struct_offsets(result: &mut ValidationResult) {
262
  let _ = log_validation(" CTask:");
263
  check_offset!(result, CTask, vtable, 0x00);
264
  check_offset!(result, CTask, parent, 0x04);
265
- check_offset!(result, CTask, children_max_size, 0x08);
266
  check_offset!(result, CTask, children_data, 0x14);
267
  check_offset!(result, CTask, class_type, 0x20);
268
  check_offset!(result, CTask, shared_data, 0x24);
@@ -1048,6 +1048,85 @@ fn dump_turngame() {
1048
  }
1049
  }
1050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
  // ---------------------------------------------------------------------------
1052
  // CTaskTeam entity dump
1053
  // ---------------------------------------------------------------------------
@@ -1181,6 +1260,8 @@ pub fn run() -> Result<(), String> {
1181
  dump_turngame();
1182
  let _ = log_validation(" Running CTaskTeam entity dump (5s mark)...");
1183
  dump_ctaskteam_entities();
 
 
1184
 
1185
  // Wait briefly for replay to start, then dump game state
1186
  // (fast-forward finishes the replay in ~10-15s total, so dump early)
@@ -1240,11 +1321,11 @@ pub fn start_hotkeys() {
1240
  const VK_F10: i32 = 0x79;
1241
  const VK_F11: i32 = 0x7A;
1242
  const VK_F12: i32 = 0x7B;
1243
- let _ = log_validation(" Hotkeys: F8=team entities, F9=team blocks, F10=landscape, F11=worm tasks, F12=entity census");
1244
  loop {
1245
  std::thread::sleep(std::time::Duration::from_millis(100));
1246
  unsafe {
1247
- if GetAsyncKeyState(VK_F8) & 1 != 0 { dump_ctaskteam_entities(); }
1248
  if GetAsyncKeyState(VK_F9) & 1 != 0 { dump_team_blocks(); }
1249
  if GetAsyncKeyState(VK_F10) & 1 != 0 { dump_landscape(); }
1250
  if GetAsyncKeyState(VK_F11) & 1 != 0 { dump_worm_tasks(); }
 
262
  let _ = log_validation(" CTask:");
263
  check_offset!(result, CTask, vtable, 0x00);
264
  check_offset!(result, CTask, parent, 0x04);
265
+ check_offset!(result, CTask, children_capacity, 0x08);
266
  check_offset!(result, CTask, children_data, 0x14);
267
  check_offset!(result, CTask, class_type, 0x20);
268
  check_offset!(result, CTask, shared_data, 0x24);
 
1048
  }
1049
  }
1050
 
1051
+ // ---------------------------------------------------------------------------
1052
+ // CTask children sub-struct live dump
1053
+ // ---------------------------------------------------------------------------
1054
+ //
1055
+ // We need to confirm the layout of the four fields at CTask+0x08..+0x17:
1056
+ // +0x08 children_capacity (u32, starts 0x10)
1057
+ // +0x0C children_dirty (u32, flag)
1058
+ // +0x10 children_watermark (u32, insertion counter)
1059
+ // +0x14 children_data (*mut u8, array pointer)
1060
+ //
1061
+ // We dump the raw DWORDs at these offsets for a few representative tasks so
1062
+ // we can verify against what WA actually stores at runtime.
1063
+
1064
+ fn dump_ctask_children() {
1065
+ use crate::replacements::input::dump_region;
1066
+
1067
+ let _ = log_validation("");
1068
+ let _ = log_validation("--- CTask children sub-struct dump ---");
1069
+
1070
+ unsafe {
1071
+ let session_ptr = read_u32(rb(va::G_GAME_SESSION));
1072
+ if session_ptr == 0 { let _ = log_validation(" No game session."); return; }
1073
+ let wrapper_addr = read_u32(session_ptr + 0xA0);
1074
+ if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; }
1075
+ let ddgame_ptr = read_u32(wrapper_addr + 0x488);
1076
+ if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; }
1077
+
1078
+ let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
1079
+ if task_land_ptr == 0 { let _ = log_validation(" CTaskLand NULL."); return; }
1080
+
1081
+ // Walk up to root (CTaskTurnGame)
1082
+ let mut cursor = task_land_ptr;
1083
+ for _ in 0..10 {
1084
+ let parent = read_u32(cursor + 0x04);
1085
+ if parent == 0 { break; }
1086
+ cursor = parent;
1087
+ }
1088
+ let root = cursor;
1089
+
1090
+ // Print the children sub-struct fields for root, then walk its first
1091
+ // child (CTaskTeam level) and each of its children (CTaskFilter level).
1092
+ let print_task_children = |label: &str, addr: u32| {
1093
+ if addr == 0 { return; }
1094
+ let cap = read_u32(addr + 0x08);
1095
+ let dirty = read_u32(addr + 0x0C);
1096
+ let wmark = read_u32(addr + 0x10);
1097
+ let data = read_u32(addr + 0x14);
1098
+ let _ = log_validation(&format!(
1099
+ " {} @ 0x{:08X} cap={} dirty={} watermark={} data=0x{:08X}",
1100
+ label, addr, cap, dirty, wmark, data
1101
+ ));
1102
+ // Raw dump of the 0x30-byte CTask base so we can cross-check all fields
1103
+ dump_region(addr as *const u8, 0x00, 0x30, "CTask");
1104
+ };
1105
+
1106
+ print_task_children("root (CTaskTurnGame)", root);
1107
+
1108
+ // First two children of root (CTaskTeam instances)
1109
+ let root_watermark = read_u32(root + 0x10) as usize;
1110
+ let root_data = read_u32(root + 0x14);
1111
+ let mut teams_found = 0;
1112
+ for i in 0..root_watermark.min(64) {
1113
+ let child = read_u32(root_data + i as u32 * 4);
1114
+ if child == 0 { continue; }
1115
+ print_task_children(&format!(" child[{}] (CTaskTeam?)", i), child);
1116
+ // And print the first few children of this child (CTaskFilter level)
1117
+ let child_wmark = read_u32(child + 0x10) as usize;
1118
+ let child_data = read_u32(child + 0x14);
1119
+ for j in 0..child_wmark.min(16) {
1120
+ let gc = read_u32(child_data + j as u32 * 4);
1121
+ if gc == 0 { continue; }
1122
+ print_task_children(&format!(" grandchild[{}] (CTaskFilter?)", j), gc);
1123
+ }
1124
+ teams_found += 1;
1125
+ if teams_found >= 2 { break; }
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
  // ---------------------------------------------------------------------------
1131
  // CTaskTeam entity dump
1132
  // ---------------------------------------------------------------------------
 
1260
  dump_turngame();
1261
  let _ = log_validation(" Running CTaskTeam entity dump (5s mark)...");
1262
  dump_ctaskteam_entities();
1263
+ let _ = log_validation(" Running CTask children sub-struct dump (5s mark)...");
1264
+ dump_ctask_children();
1265
 
1266
  // Wait briefly for replay to start, then dump game state
1267
  // (fast-forward finishes the replay in ~10-15s total, so dump early)
 
1321
  const VK_F10: i32 = 0x79;
1322
  const VK_F11: i32 = 0x7A;
1323
  const VK_F12: i32 = 0x7B;
1324
+ let _ = log_validation(" Hotkeys: F8=ctask children, F9=team blocks, F10=landscape, F11=worm tasks, F12=entity census");
1325
  loop {
1326
  std::thread::sleep(std::time::Duration::from_millis(100));
1327
  unsafe {
1328
+ if GetAsyncKeyState(VK_F8) & 1 != 0 { dump_ctask_children(); }
1329
  if GetAsyncKeyState(VK_F9) & 1 != 0 { dump_team_blocks(); }
1330
  if GetAsyncKeyState(VK_F10) & 1 != 0 { dump_landscape(); }
1331
  if GetAsyncKeyState(VK_F11) & 1 != 0 { dump_worm_tasks(); }