paavohtl Claude Sonnet 4.6 commited on
Commit
b9cbd32
·
1 Parent(s): df7c678

feat: expand CTaskTeam struct with runtime-validated field names

Browse files

Memory 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
- pub _unknown_3c: [u8; 0x4C],
596
- /// 0x88–0x217: Up to 100 weapon/item slot entries written from scheme data when
597
- /// specific scheme flags are set. `worm_count` tracks how many entries are valid.
598
- pub _item_slots: [u32; 100],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  /// 0x218: Number of CTaskWorm children constructed for this team.
600
  pub worm_count: u32,
601
- pub _unknown_21c: [u8; 0x244],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, 0x38);
303
- check_offset!(result, CTaskTeam, worm_count, 0x218);
 
 
 
 
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(VK_F9) & 1 != 0 { dump_team_blocks(); }
 
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(); }