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>
|
@@ -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,
|
|
@@ -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 |
+
}
|
|
@@ -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};
|
|
@@ -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!(
|
|
@@ -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
|
|
|
|
|
|
|
| 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
|
| 1320 |
-
|
| 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)
|
| 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 |
});
|