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

feat: add CTaskMissile struct, CTaskBfsIter, and missile memory dump

Browse files

- CTaskMissile (0x41C bytes): fully decompiled constructor layout with
spawn_params (11 DWORDs), weapon_data (94 DWORDs), render_data (42 DWORDs).
render_data is a shifted copy of weapon_data (weapon_data[N+3] for single
shots, weapon_data[N+52] for cluster sub-pellets) — likely for frame
interpolation support in the Steam version.
- MissileType enum: Standard=2, Homing=3, Sheep=4, Cluster=5 (confirmed via
constructor switch analysis).
- CTaskBfsIter: general-purpose BFS tree walker over CTask children using
VecDeque. Replaces inline fixed-size queue arrays in validation code.
- dump_missile_tasks(): uses CTaskBfsIter, routes raw DWORD dump to
OpenWA_validation.log (not OpenWA.log).
- Validation log now cleared unconditionally on DLL load (not just when
OPENWA_VALIDATE=1).

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

crates/openwa-core/src/task/base.rs CHANGED
@@ -150,6 +150,63 @@ impl SharedDataTable {
150
  }
151
  }
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  /// Iterator over all [`SharedDataNode`]s in a [`SharedDataTable`].
154
  ///
155
  /// Created by [`SharedDataTable::iter`]. Walks all 256 buckets in order,
 
150
  }
151
  }
152
 
153
+ /// Breadth-first iterator over the CTask tree.
154
+ ///
155
+ /// Visits every node reachable from `root` by following `children_data`
156
+ /// arrays. Null slots in the sparse children array are skipped automatically.
157
+ ///
158
+ /// Yields raw `*const CTask` pointers. The caller is responsible for casting
159
+ /// to the correct derived type (e.g., by checking the vtable pointer at `[0]`).
160
+ ///
161
+ /// # Example
162
+ /// ```ignore
163
+ /// let iter = unsafe { CTaskBfsIter::new(root_ptr) };
164
+ /// for task in iter {
165
+ /// if unsafe { *(task as *const u32) } == rb(va::CTASK_MISSILE_VTABLE) {
166
+ /// let m = unsafe { &*(task as *const CTaskMissile) };
167
+ /// // ...
168
+ /// }
169
+ /// }
170
+ /// ```
171
+ pub struct CTaskBfsIter {
172
+ queue: std::collections::VecDeque<*const CTask>,
173
+ }
174
+
175
+ impl CTaskBfsIter {
176
+ /// Create a new BFS iterator rooted at `root`.
177
+ ///
178
+ /// # Safety
179
+ /// `root` must be a valid, aligned `*const CTask`. All reachable
180
+ /// `children_data` entries must be either null or valid `*const CTask`.
181
+ pub unsafe fn new(root: *const CTask) -> Self {
182
+ let mut queue = std::collections::VecDeque::new();
183
+ queue.push_back(root);
184
+ Self { queue }
185
+ }
186
+ }
187
+
188
+ impl Iterator for CTaskBfsIter {
189
+ type Item = *const CTask;
190
+
191
+ fn next(&mut self) -> Option<Self::Item> {
192
+ // SAFETY: caller of CTaskBfsIter::new() guarantees node validity.
193
+ unsafe {
194
+ let node = self.queue.pop_front()?;
195
+ let watermark = (*node).children_watermark as usize;
196
+ let data = (*node).children_data as *const u32;
197
+ if !data.is_null() {
198
+ for i in 0..watermark {
199
+ let child_ptr = *data.add(i);
200
+ if child_ptr != 0 {
201
+ self.queue.push_back(child_ptr as *const CTask);
202
+ }
203
+ }
204
+ }
205
+ Some(node)
206
+ }
207
+ }
208
+ }
209
+
210
  /// Iterator over all [`SharedDataNode`]s in a [`SharedDataTable`].
211
  ///
212
  /// Created by [`SharedDataTable::iter`]. Walks all 256 buckets in order,
crates/openwa-core/src/task/missile.rs ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::fixed::Fixed;
2
+ use super::game_task::CGameTask;
3
+
4
+ /// Projectile / missile entity task.
5
+ ///
6
+ /// Extends CGameTask (0xFC bytes). One instance per airborne projectile
7
+ /// (rockets, grenades, mortar shells, homing missiles, sheep, etc.).
8
+ ///
9
+ /// Inheritance: CTask → CGameTask → CTaskMissile. class_type = 0x0B (11).
10
+ /// Constructor: `CTaskMissile__Constructor` (0x507D10, stdcall, 4 params).
11
+ /// Vtable: `CTaskMissile__vtable` (0x00664438).
12
+ ///
13
+ /// Constructor params:
14
+ /// param_1 = this
15
+ /// param_2 = parent task pointer (passed to CGameTask ctor)
16
+ /// param_3 = scheme weapon data (94 DWORDs from WGT blob)
17
+ /// param_4 = spawn data (11 DWORDs: position, velocity, owner, pellet index)
18
+ ///
19
+ /// Source: Ghidra decompilation of 0x507D10 (constructor) and
20
+ /// wkJellyWorm CTaskMissile.h (field layout reference).
21
+ #[repr(C)]
22
+ pub struct CTaskMissile {
23
+ /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94).
24
+ pub base: CGameTask,
25
+
26
+ // ---- 0xFC–0x12F: missile init fields ----
27
+
28
+ /// 0xFC–0x10F: Unknown missile flags and state
29
+ pub _unknown_fc: [u8; 0x14],
30
+ /// 0x110: Unknown
31
+ pub _unknown_110: u32,
32
+ /// 0x114: Unknown
33
+ pub _unknown_114: u32,
34
+ /// 0x118: Unknown — observed being set from scheme data at construction
35
+ pub _unknown_118: u32,
36
+ /// 0x11C: Unknown
37
+ pub _unknown_11c: u32,
38
+ /// 0x120: Unknown
39
+ pub _unknown_120: u32,
40
+ /// 0x124: Unknown
41
+ pub _unknown_124: u32,
42
+ /// 0x128: Position-derived launch seed. Computed by constructor as:
43
+ /// `((spawn_x + spawn_y) / 256 / 20) + 0x10000`. param_1[0x4A].
44
+ pub launch_seed: u32,
45
+ /// 0x12C: Object pool slot index (assigned from DDGame+0x3600 pool).
46
+ /// param_1[0x4B].
47
+ pub slot_id: u32,
48
+
49
+ // ---- 0x130–0x15B: spawn data (11 DWORDs, from param_4) ----
50
+
51
+ /// 0x130–0x15B: Spawn parameters (11 DWORDs copied from param_4).
52
+ ///
53
+ /// Runtime-confirmed indices:
54
+ /// [0x00] owner_id — team index that fired this missile
55
+ /// [0x01] unknown (= 1 observed)
56
+ /// [0x02] spawn_x — Fixed16.16 X at launch
57
+ /// [0x03] spawn_y — Fixed16.16 Y at launch
58
+ /// [0x04] initial_speed_x — Fixed16.16 horizontal velocity (→ CGameTask.speed_x)
59
+ /// [0x05] initial_speed_y — Fixed16.16 vertical velocity (→ CGameTask.speed_y)
60
+ /// [0x06] cursor_x — Fixed16.16 aim cursor X (observed = level_width/2)
61
+ /// [0x07] cursor_y — Fixed16.16 aim cursor Y (observed = level_height/2)
62
+ /// [0x08] pellet_index — index within a cluster volley (0 for single shot)
63
+ /// [0x09] fallback_timer — copied to render_data[0x19] only if it was zero
64
+ /// [0x0A] fallback_param — copied to render_data[0x11] only if it was zero
65
+ pub spawn_params: [u32; 0x0B],
66
+
67
+ // ---- 0x15C–0x2D3: weapon/scheme data (94 DWORDs, from param_3) ----
68
+
69
+ /// 0x15C–0x2D3: Weapon/scheme properties (94 DWORDs copied verbatim from param_3).
70
+ ///
71
+ /// The WGT scheme blob is split into two logical halves:
72
+ /// [0x00..0x34] primary projectile params (→ also mirrored in render_data)
73
+ /// [0x34..0x5E] cluster sub-pellet params (→ render_data when pellet_index > 0)
74
+ ///
75
+ /// Runtime-observed for bazooka:
76
+ /// [0x03] = 137342 → Fixed16.16 ≈ 2.10 (some radius/size)
77
+ /// [0x05] = 100, [0x06] = 50, [0x09] = 48
78
+ /// [0x0F] gravity_pct — 100 → gravity_factor = 1.0 (→ CGameTask+0x58 via render_data)
79
+ /// [0x10] bounce_pct — 100 → bounce_factor = 1.0 (→ CGameTask+0x5C via render_data)
80
+ /// [0x12] friction_pct — 100 → friction_factor = 1.0 (→ CGameTask+0x60 via render_data)
81
+ /// [0x14] = 9000
82
+ /// [0x1A] missile_type — discriminator (2=Standard, 3=Homing, 4=Sheep, 5=Cluster)
83
+ /// [0x1B] = 4194304 → Fixed16.16 = 64.0
84
+ /// [0x1C] render_timer — 1 for bazooka, 30 for grenade (3s fuse @ 10fps)
85
+ pub weapon_data: [u32; 0x5E],
86
+
87
+ // ---- 0x2D4–0x37B: render/physics parameters (42 DWORDs) ----
88
+
89
+ /// 0x2D4–0x37B: Per-projectile render and physics parameters (42 DWORDs).
90
+ ///
91
+ /// This is NOT a separate data block — it is a shifted copy of weapon_data:
92
+ /// if spawn_params[8] == 0 (single shot): render_data[N] = weapon_data[N+3]
93
+ /// if spawn_params[8] > 0 (cluster pellet): render_data[N] = weapon_data[N+52]
94
+ ///
95
+ /// The constructor copies 42 DWORDs from the appropriate source range
96
+ /// into this region (param_1[0xB5..0xDE]). Dynamic physics values update
97
+ /// some entries during flight (e.g. render_data[0x29] @ 0x37C).
98
+ ///
99
+ /// Key indices (relative to render_data, = weapon_data[N+3] for single shots):
100
+ /// [0x0C] gravity_pct → (value << 16) / 100 → CGameTask+0x58 (gravity_factor)
101
+ /// [0x0D] bounce_pct → (value << 16) / 100 → CGameTask+0x5C (bounce_factor)
102
+ /// [0x0F] friction_pct → (value << 16) / 100 → CGameTask+0x60 (friction_factor)
103
+ /// [0x11] = 9000 for bazooka (→ also copied to post-render field at 0x37C)
104
+ /// [0x17] missile_type — type discriminator (see MissileType)
105
+ /// [0x18] = 4194304 → Fixed16.16 = 64.0 (sprite/render size)
106
+ /// [0x19] render_timer — 1 for bazooka, 30 for grenade (3s fuse timer)
107
+ pub render_data: [u32; 0x2A],
108
+
109
+ // ---- 0x37C–0x41B: post-render physics and state ----
110
+
111
+ /// 0x37C–0x39F: Post-render dynamic state. render_data[0x11] is copied to
112
+ /// [0x37C] by the constructor; physics updates the values each frame.
113
+ pub _unknown_37c: [u8; 0x24],
114
+ /// 0x3A0: Fixed16.16 launch speed magnitude (computed from initial velocity).
115
+ /// Observed as 0 for bazooka — may only be populated for specific types.
116
+ pub launch_speed_raw: Fixed,
117
+ /// 0x3A4: Unknown
118
+ pub _unknown_3a4: u32,
119
+ /// 0x3A8: Homing mode enabled flag (nonzero = active homing).
120
+ /// param_1[0xEA] in constructor. Set to 1 when missile_type == 3 and conditions
121
+ /// are met (target acquired).
122
+ pub homing_enabled: u32,
123
+ /// 0x3AC–0x3BF: Unknown homing state fields
124
+ pub _unknown_3ac: [u8; 0x14],
125
+ /// 0x3C0–0x3C7: Unknown
126
+ pub _unknown_3c0: [u8; 8],
127
+ /// 0x3C8: Horizontal direction sign (+1 or -1, determines facing/travel dir).
128
+ /// param_1[0xF2] in constructor. Set to -1 for homing/sheep if initial_speed_x < 0.
129
+ pub direction: i32,
130
+ /// 0x3CC–0x41B: Unknown trailing state
131
+ pub _unknown_3cc: [u8; 0x50],
132
+ }
133
+
134
+ const _: () = assert!(core::mem::size_of::<CTaskMissile>() == 0x41C);
135
+
136
+ impl CTaskMissile {
137
+ /// Missile type from `render_data[0x17]` (= weapon_data[0x1A] for single shots).
138
+ ///
139
+ /// Governs constructor and movement behaviour (homing, sheep walk,
140
+ /// cluster splitting, etc.).
141
+ pub fn missile_type(&self) -> MissileType {
142
+ match self.render_data[0x17] {
143
+ 2 => MissileType::Standard,
144
+ 3 => MissileType::Homing,
145
+ 4 => MissileType::Sheep,
146
+ 5 => MissileType::Cluster,
147
+ n => MissileType::Unknown(n),
148
+ }
149
+ }
150
+
151
+ /// Spawn X as Fixed16.16 (from spawn_params[2]).
152
+ pub fn spawn_x(&self) -> Fixed {
153
+ Fixed(self.spawn_params[2] as i32)
154
+ }
155
+
156
+ /// Spawn Y as Fixed16.16 (from spawn_params[3]).
157
+ pub fn spawn_y(&self) -> Fixed {
158
+ Fixed(self.spawn_params[3] as i32)
159
+ }
160
+
161
+ /// Aim cursor X at time of fire, Fixed16.16 (from spawn_params[6]).
162
+ pub fn cursor_x(&self) -> Fixed {
163
+ Fixed(self.spawn_params[6] as i32)
164
+ }
165
+
166
+ /// Aim cursor Y at time of fire, Fixed16.16 (from spawn_params[7]).
167
+ pub fn cursor_y(&self) -> Fixed {
168
+ Fixed(self.spawn_params[7] as i32)
169
+ }
170
+ }
171
+
172
+ /// Missile movement/behaviour type, encoded in `render_data[0x17]`.
173
+ ///
174
+ /// The constructor switches on this value to set up physics, homing,
175
+ /// direction, and clustering behaviour. Corresponds to weapon_data[0x1A]
176
+ /// for single-shot projectiles.
177
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
178
+ pub enum MissileType {
179
+ /// Standard trajectory projectile (bazooka, mortar, grenade, etc.). Value = 2.
180
+ Standard,
181
+ /// Homing missile — tracks nearest worm. Value = 3.
182
+ Homing,
183
+ /// Sheep / animal projectile — walks on terrain. Value = 4.
184
+ Sheep,
185
+ /// Cluster projectile — spawns sub-pellets on detonation. Value = 5.
186
+ Cluster,
187
+ /// Unknown type code (value 1 never observed; any other unrecognised value).
188
+ Unknown(u32),
189
+ }
crates/openwa-core/src/task/mod.rs CHANGED
@@ -4,17 +4,19 @@ mod filter;
4
  mod fire;
5
  mod game_task;
6
  mod mine_oil_drum;
 
7
  mod overlays;
8
  mod team;
9
  mod turn_game;
10
  mod worm;
11
 
12
- pub use base::{CTask, SharedDataIter, SharedDataNode, SharedDataTable};
13
  pub use cloud::CTaskCloud;
14
  pub use filter::CTaskFilter;
15
  pub use fire::CTaskFire;
16
  pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
17
  pub use mine_oil_drum::{CTaskMine, CTaskOilDrum};
 
18
  pub use overlays::{BungeeTrailTask, WeaponAimTask};
19
  pub use team::CTaskTeam;
20
  pub use turn_game::{CTaskTurnGame, TurnGameCtx};
 
4
  mod fire;
5
  mod game_task;
6
  mod mine_oil_drum;
7
+ mod missile;
8
  mod overlays;
9
  mod team;
10
  mod turn_game;
11
  mod worm;
12
 
13
+ pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
14
  pub use cloud::CTaskCloud;
15
  pub use filter::CTaskFilter;
16
  pub use fire::CTaskFire;
17
  pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
18
  pub use mine_oil_drum::{CTaskMine, CTaskOilDrum};
19
+ pub use missile::{CTaskMissile, MissileType};
20
  pub use overlays::{BungeeTrailTask, WeaponAimTask};
21
  pub use team::CTaskTeam;
22
  pub use turn_game::{CTaskTurnGame, TurnGameCtx};
crates/openwa-wormkit/src/lib.rs CHANGED
@@ -54,6 +54,7 @@ fn clear_log() -> std::io::Result<()> {
54
 
55
  fn run() -> Result<(), String> {
56
  let _ = clear_log();
 
57
 
58
  let delta = openwa_core::rebase::init();
59
  let _ = log_line(&format!(
 
54
 
55
  fn run() -> Result<(), String> {
56
  let _ = clear_log();
57
+ let _ = std::fs::write("OpenWA_validation.log", "");
58
 
59
  let delta = openwa_core::rebase::init();
60
  let _ = log_line(&format!(
crates/openwa-wormkit/src/validation/mod.rs CHANGED
@@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
11
 
12
  use openwa_core::rebase::rb;
13
  use openwa_core::address::va;
14
- use openwa_core::task::{CTask, CGameTask, CTaskTeam, CTaskTurnGame, TurnGameCtx, SharedDataTable};
15
  use openwa_core::ddgame::DDGame;
16
  use openwa_core::ddgame_wrapper::DDGameWrapper;
17
 
@@ -318,6 +318,17 @@ fn validate_struct_offsets(result: &mut ValidationResult) {
318
  check_offset!(result, TurnGameCtx, _hud_textbox_a, 0xA4);
319
  check_offset!(result, TurnGameCtx, _hud_textbox_b, 0xA8);
320
 
 
 
 
 
 
 
 
 
 
 
 
321
  let _ = log_validation(" CTaskTurnGame:");
322
  check_offset!(result, CTaskTurnGame, game_ctx, 0x30);
323
  check_offset!(result, CTaskTurnGame, worm_active, 0x108);
@@ -1190,6 +1201,127 @@ fn dump_ctaskteam_entities() {
1190
  }
1191
  }
1192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1193
  // ---------------------------------------------------------------------------
1194
  // Public entry point — called from wormkit's run()
1195
  // ---------------------------------------------------------------------------
@@ -1262,6 +1394,8 @@ pub fn run() -> Result<(), String> {
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)
@@ -1305,7 +1439,9 @@ pub fn run() -> Result<(), String> {
1305
  Ok(())
1306
  }
1307
 
1308
- /// Start the debug hotkey listener thread (F9=team blocks, F10=landscape).
 
 
1309
  ///
1310
  /// Always available regardless of `OPENWA_VALIDATE`. Skipped in replay-test
1311
  /// mode since auto-capture handles dumps automatically.
@@ -1316,20 +1452,12 @@ pub fn start_hotkeys() {
1316
 
1317
  std::thread::spawn(|| {
1318
  use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
1319
- const VK_F8: i32 = 0x77;
1320
- const VK_F9: i32 = 0x78;
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(); }
1332
- if GetAsyncKeyState(VK_F12) & 1 != 0 { dump_entity_census(); }
1333
  }
1334
  }
1335
  });
 
11
 
12
  use openwa_core::rebase::rb;
13
  use openwa_core::address::va;
14
+ use openwa_core::task::{CTask, CTaskBfsIter, CGameTask, CTaskMissile, MissileType, CTaskTeam, CTaskTurnGame, TurnGameCtx, SharedDataTable};
15
  use openwa_core::ddgame::DDGame;
16
  use openwa_core::ddgame_wrapper::DDGameWrapper;
17
 
 
318
  check_offset!(result, TurnGameCtx, _hud_textbox_a, 0xA4);
319
  check_offset!(result, TurnGameCtx, _hud_textbox_b, 0xA8);
320
 
321
+ let _ = log_validation("");
322
+ let _ = log_validation(" CTaskMissile:");
323
+ check_offset!(result, CTaskMissile, base, 0x00);
324
+ check_offset!(result, CTaskMissile, slot_id, 0x12C);
325
+ check_offset!(result, CTaskMissile, spawn_params, 0x130);
326
+ check_offset!(result, CTaskMissile, weapon_data, 0x15C);
327
+ check_offset!(result, CTaskMissile, render_data, 0x2D4);
328
+ check_offset!(result, CTaskMissile, launch_speed_raw, 0x3A0);
329
+ check_offset!(result, CTaskMissile, homing_enabled, 0x3A8);
330
+ check_offset!(result, CTaskMissile, direction, 0x3C8);
331
+
332
  let _ = log_validation(" CTaskTurnGame:");
333
  check_offset!(result, CTaskTurnGame, game_ctx, 0x30);
334
  check_offset!(result, CTaskTurnGame, worm_active, 0x108);
 
1201
  }
1202
  }
1203
 
1204
+ // ---------------------------------------------------------------------------
1205
+ // CTaskMissile live dump
1206
+ // ---------------------------------------------------------------------------
1207
+ //
1208
+ // CTaskMissile is NOT registered in the SharedData entity table — it is a
1209
+ // transient child of a CTaskFilter node. We walk the full task tree from the
1210
+ // root (CTaskTurnGame) up to 4 levels deep, checking every child's vtable.
1211
+ // Fire a weapon, then press F8.
1212
+
1213
+ /// Hex dump of one CTaskMissile to the validation log.
1214
+ /// Each non-zero DWORD is printed as: +0xOFFSET: 0xVALUE
1215
+ /// Sections are labelled so the output can be cross-referenced with the struct.
1216
+ unsafe fn dump_missile_raw(ptr: u32) {
1217
+ let base = ptr as *const u32;
1218
+ let sections: &[(usize, usize, &str)] = &[
1219
+ (0x000, 0x030, "CTask base"),
1220
+ (0x030, 0x084, "CGameTask _unknown_30"),
1221
+ (0x084, 0x098, "CGameTask pos/speed (0x84–0x97)"),
1222
+ (0x098, 0x0FC, "CGameTask _unknown_98"),
1223
+ (0x0FC, 0x130, "CTaskMissile 0xFC–0x12F"),
1224
+ (0x130, 0x15C, "spawn_params"),
1225
+ (0x15C, 0x2D4, "weapon_data"),
1226
+ (0x2D4, 0x37C, "render_data"),
1227
+ (0x37C, 0x41C, "_unknown_37c / launch_speed / homing / direction"),
1228
+ ];
1229
+ for &(start, end, label) in sections {
1230
+ let _ = log_validation(&format!(" -- {} (0x{:03X}..0x{:03X}) --", label, start, end));
1231
+ let dwords = (end - start) / 4;
1232
+ for i in 0..dwords {
1233
+ let off = start + i * 4;
1234
+ let val = *base.add(off / 4);
1235
+ if val != 0 {
1236
+ let _ = log_validation(&format!(" +0x{:03X}: 0x{:08X} ({})", off, val, val));
1237
+ }
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ fn dump_missile_tasks() {
1243
+ let _ = log_validation("");
1244
+ let _ = log_validation("--- CTaskMissile Entity Dump ---");
1245
+
1246
+ unsafe {
1247
+ let session_ptr = read_u32(rb(va::G_GAME_SESSION));
1248
+ if session_ptr == 0 { let _ = log_validation(" No game session — skipping."); return; }
1249
+ let wrapper_addr = read_u32(session_ptr + 0xA0);
1250
+ if wrapper_addr == 0 { let _ = log_validation(" No DDGameWrapper."); return; }
1251
+ let ddgame_ptr = read_u32(wrapper_addr + 0x488);
1252
+ if ddgame_ptr == 0 { let _ = log_validation(" No DDGame."); return; }
1253
+
1254
+ // Walk up from CTaskLand to root (CTaskTurnGame) via parent links.
1255
+ let task_land_ptr = read_u32(ddgame_ptr + 0x54C);
1256
+ if task_land_ptr == 0 { let _ = log_validation(" CTaskLand NULL — game not loaded."); return; }
1257
+ let mut root = task_land_ptr;
1258
+ for _ in 0..10 {
1259
+ let parent = read_u32(root + 0x04);
1260
+ if parent == 0 { break; }
1261
+ root = parent;
1262
+ }
1263
+
1264
+ let expected_vt = rb(va::CTASK_MISSILE_VTABLE);
1265
+ let _ = log_validation(&format!(" Root (CTaskTurnGame) @ 0x{:08X}", root));
1266
+ let _ = log_validation(&format!(" Expected CTaskMissile vtable: 0x{:08X}", expected_vt));
1267
+
1268
+ let mut found = 0u32;
1269
+ let mut first_ptr: u32 = 0;
1270
+ let mut scanned = 0usize;
1271
+
1272
+ for task_ptr in CTaskBfsIter::new(root as *const CTask) {
1273
+ let node = task_ptr as u32;
1274
+ scanned += 1;
1275
+
1276
+ let vt = read_u32(node);
1277
+ if vt == expected_vt {
1278
+ found += 1;
1279
+ if first_ptr == 0 { first_ptr = node; }
1280
+
1281
+ let m = &*(node as *const CTaskMissile);
1282
+ let pos_x = m.base.pos_x.to_f32();
1283
+ let pos_y = m.base.pos_y.to_f32();
1284
+ let spd_x = m.base.speed_x.to_f32();
1285
+ let spd_y = m.base.speed_y.to_f32();
1286
+ let _ = log_validation(&format!(
1287
+ " missile#{} @ 0x{:08X} slot={} type={:?} homing={} dir={}",
1288
+ found, node, m.slot_id, m.missile_type(), m.homing_enabled, m.direction
1289
+ ));
1290
+ let _ = log_validation(&format!(
1291
+ " pos=({:.2},{:.2}) speed=({:.3},{:.3}) cursor=({:.1},{:.1})",
1292
+ pos_x, pos_y, spd_x, spd_y,
1293
+ m.cursor_x().to_f32(), m.cursor_y().to_f32()
1294
+ ));
1295
+ let _ = log_validation(&format!(
1296
+ " spawn_params: owner={} pellet={} spawn=({:.1},{:.1})",
1297
+ m.spawn_params[0], m.spawn_params[8],
1298
+ m.spawn_x().to_f32(), m.spawn_y().to_f32()
1299
+ ));
1300
+ let _ = log_validation(&format!(
1301
+ " weapon_data[0..10]: {:?}",
1302
+ &m.weapon_data[..10]
1303
+ ));
1304
+ let _ = log_validation(&format!(
1305
+ " render_data[0x15..0x1A]: {:?} (timer={})",
1306
+ &m.render_data[0x15..0x1A],
1307
+ m.render_data[0x19]
1308
+ ));
1309
+ }
1310
+ }
1311
+
1312
+ let _ = log_validation(&format!(" Total CTaskMissile found: {} (scanned {} nodes)", found, scanned));
1313
+
1314
+ if found == 0 {
1315
+ let _ = log_validation(" No missiles in flight — fire a weapon first, then press F8.");
1316
+ return;
1317
+ }
1318
+
1319
+ // Full raw dump of the first missile to cross-validate struct layout.
1320
+ let _ = log_validation(&format!("\n === Raw dump: CTaskMissile @ 0x{:08X} ===", first_ptr));
1321
+ dump_missile_raw(first_ptr);
1322
+ }
1323
+ }
1324
+
1325
  // ---------------------------------------------------------------------------
1326
  // Public entry point — called from wormkit's run()
1327
  // ---------------------------------------------------------------------------
 
1394
  dump_ctaskteam_entities();
1395
  let _ = log_validation(" Running CTask children sub-struct dump (5s mark)...");
1396
  dump_ctask_children();
1397
+ let _ = log_validation(" Running CTaskMissile dump (5s mark)...");
1398
+ dump_missile_tasks();
1399
 
1400
  // Wait briefly for replay to start, then dump game state
1401
  // (fast-forward finishes the replay in ~10-15s total, so dump early)
 
1439
  Ok(())
1440
  }
1441
 
1442
+ /// Start the debug hotkey listener thread.
1443
+ ///
1444
+ /// F8 = CTaskMissile dump (fire a weapon first, then press F8 while missile is in flight).
1445
  ///
1446
  /// Always available regardless of `OPENWA_VALIDATE`. Skipped in replay-test
1447
  /// mode since auto-capture handles dumps automatically.
 
1452
 
1453
  std::thread::spawn(|| {
1454
  use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
1455
+ const VK_F8: i32 = 0x77;
1456
+ let _ = log_validation(" Hotkeys: F8=missile dump (fire weapon, then press F8 while missile is in flight)");
 
 
 
 
1457
  loop {
1458
  std::thread::sleep(std::time::Duration::from_millis(100));
1459
  unsafe {
1460
+ if GetAsyncKeyState(VK_F8) & 1 != 0 { dump_missile_tasks(); }
 
 
 
 
1461
  }
1462
  }
1463
  });