paavohtl Claude Opus 4.6 (1M context) commited on
Commit
2835c9a
·
1 Parent(s): 6cbc730

feat: add CTaskCrate struct, raw field debug UI, new task fields

Browse files

Add CTaskCrate (0x4B0 bytes, vtable 0x664298) with slot_id, timer,
scheme_data block, and sequence_ref. 17 vtable methods labelled in
Ghidra.

Add raw DWORD inspector to egui debug UI for CTaskMine, CTaskOilDrum,
CTaskCrate, CTaskWorm, and CTaskMissile — shows every field with
offset, hex/decimal value, and name. All sections collapsed by default.

New fields discovered via runtime observation:
- CGameTask.angle (+0x8C): rotation angle, fixed-point 16.16
- CTaskMine.fuse_timer: corrected from +0x11C to +0x118
- CTaskCrate+0x24C: health crate healing amount
- CTaskWorm: selected_weapon (+0x170), facing_direction copies
(+0x1A8/+0x1AC/+0x334), aim_angle (+0x24C), show_cursor (+0x268),
stationary_frames (+0x164), movement_streak (+0x1EC),
facing_direction (+0x3DC), input keys (+0x3E4..+0x3F0)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/address.rs CHANGED
@@ -80,6 +80,8 @@ pub mod va {
80
  pub const CTASK_MINE_VTABLE: u32 = 0x0066_43E8;
81
  /// CTaskOilDrum vtable - oil drum entity (constructor 0x504AF0)
82
  pub const CTASK_OILDRUM_VTABLE: u32 = 0x0066_4338;
 
 
83
  /// CTaskCloud vtable - cloud/airstrike entity (constructor 0x5482E0)
84
  pub const CTASK_CLOUD_VTABLE: u32 = 0x0066_9D38;
85
  /// CTaskSeaBubble vtable - water bubble particle (constructor 0x554FE0)
 
80
  pub const CTASK_MINE_VTABLE: u32 = 0x0066_43E8;
81
  /// CTaskOilDrum vtable - oil drum entity (constructor 0x504AF0)
82
  pub const CTASK_OILDRUM_VTABLE: u32 = 0x0066_4338;
83
+ /// CTaskCrate vtable - weapon/health/utility crate (constructor 0x502490)
84
+ pub const CTASK_CRATE_VTABLE: u32 = 0x0066_4298;
85
  /// CTaskCloud vtable - cloud/airstrike entity (constructor 0x5482E0)
86
  pub const CTASK_CLOUD_VTABLE: u32 = 0x0066_9D38;
87
  /// CTaskSeaBubble vtable - water bubble particle (constructor 0x554FE0)
crates/openwa-core/src/task/game_task.rs CHANGED
@@ -19,8 +19,8 @@ pub struct CGameTask {
19
  pub pos_x: Fixed,
20
  /// 0x88: Y position in fixed-point
21
  pub pos_y: Fixed,
22
- /// 0x8C-0x8F: Unknown (4 bytes between pos and speed)
23
- pub _unknown_8c: [u8; 4],
24
  /// 0x90: X velocity in fixed-point
25
  pub speed_x: Fixed,
26
  /// 0x94: Y velocity in fixed-point
 
19
  pub pos_x: Fixed,
20
  /// 0x88: Y position in fixed-point
21
  pub pos_y: Fixed,
22
+ /// 0x8C: Rotation angle (fixed-point 16.16).
23
+ pub angle: Fixed,
24
  /// 0x90: X velocity in fixed-point
25
  pub speed_x: Fixed,
26
  /// 0x94: Y velocity in fixed-point
crates/openwa-core/src/task/mine_oil_drum.rs CHANGED
@@ -18,13 +18,15 @@ pub struct CTaskMine {
18
  pub _unknown_fc: [u8; 0x14],
19
  /// 0x110: Object pool slot index (assigned from DDGame+0x3600 pool)
20
  pub slot_id: u32,
21
- /// 0x114–0x11B: Unknown
22
- pub _unknown_114: [u8; 8],
23
- /// 0x11C: Fuse timer (signed i32).
24
  /// Negative = just placed / disarmed.
25
  /// 0 = armed (will trigger on contact).
26
  /// Positive = countdown ticks remaining.
27
  pub fuse_timer: i32,
 
 
28
  /// 0x120–0x123: Unknown (init data param_3[0])
29
  pub _unknown_120: u32,
30
  /// 0x124: Owner team index (param_3[6]; -1 = no owner)
 
18
  pub _unknown_fc: [u8; 0x14],
19
  /// 0x110: Object pool slot index (assigned from DDGame+0x3600 pool)
20
  pub slot_id: u32,
21
+ /// 0x114: Unknown
22
+ pub _unknown_114: u32,
23
+ /// 0x118: Fuse timer (signed i32).
24
  /// Negative = just placed / disarmed.
25
  /// 0 = armed (will trigger on contact).
26
  /// Positive = countdown ticks remaining.
27
  pub fuse_timer: i32,
28
+ /// 0x11C: Unknown
29
+ pub _unknown_11c: u32,
30
  /// 0x120–0x123: Unknown (init data param_3[0])
31
  pub _unknown_120: u32,
32
  /// 0x124: Owner team index (param_3[6]; -1 = no owner)
crates/openwa-core/src/task/mod.rs CHANGED
@@ -1,5 +1,6 @@
1
  mod base;
2
  mod cloud;
 
3
  mod filter;
4
  mod fire;
5
  mod game_task;
@@ -12,6 +13,7 @@ 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};
 
1
  mod base;
2
  mod cloud;
3
+ mod supply_crate;
4
  mod filter;
5
  mod fire;
6
  mod game_task;
 
13
 
14
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
15
  pub use cloud::CTaskCloud;
16
+ pub use supply_crate::CTaskCrate;
17
  pub use filter::CTaskFilter;
18
  pub use fire::CTaskFire;
19
  pub use game_task::{CGameTask, SoundEmitter, SoundEmitterVTable};
crates/openwa-core/src/task/supply_crate.rs ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::game_task::CGameTask;
2
+
3
+ /// Weapon/health/utility crate entity task.
4
+ ///
5
+ /// Extends CGameTask (0xFC bytes). Crates fall from the sky with a parachute,
6
+ /// land on terrain, and are collected by worms on contact. Can contain weapons,
7
+ /// health, or utility items depending on crate type.
8
+ ///
9
+ /// Constructor: 0x502490 (stdcall, 4 params: this, parent, scheme_data, spawn_mode).
10
+ /// Vtable: 0x664298. Class type byte: 0x0F.
11
+ ///
12
+ /// The constructor copies 0xE5 DWORDs (0x394 bytes) of scheme/crate data from
13
+ /// param_3 into offset 0x110, making the crate carry its full configuration.
14
+ ///
15
+ /// Source: Ghidra decompilation of 0x502490, wkJellyWorm CTaskCrate.h
16
+ #[repr(C)]
17
+ pub struct CTaskCrate {
18
+ /// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94)
19
+ pub base: CGameTask,
20
+ /// 0xFC: Unknown (zeroed by constructor)
21
+ pub _unknown_fc: u32,
22
+ /// 0x100: Object pool slot index (assigned from DDGame+0x3600 pool)
23
+ pub slot_id: u32,
24
+ /// 0x104: Unknown (zeroed by constructor)
25
+ pub _unknown_104: u32,
26
+ /// 0x108: Unknown (zeroed by constructor, also cleared for spawn_mode=1)
27
+ pub _unknown_108: u32,
28
+ /// 0x10C: Timer/counter (zeroed by constructor; set to scheme_data[0x52]*1000
29
+ /// for crate_type == 3)
30
+ pub timer: u32,
31
+ /// 0x110–0x4A3: Scheme/crate data (0xE5 DWORDs = 0x394 bytes, copied from
32
+ /// constructor param_3). Contains weapon properties, crate type, quantities, etc.
33
+ ///
34
+ /// Key indices (DWORD offsets from 0x110):
35
+ /// [0x05] (0x124): crate_type — discriminator (3=timed?, 5=airstrike?)
36
+ /// [0x4F] (0x24C): health crate healing amount
37
+ /// [0x45] (0x224): scheme weapon data sub-field
38
+ /// [0x55] (0x264): nonzero triggers additional init
39
+ /// [0x85] (0x324): additional scheme params
40
+ pub scheme_data: [u32; 0xE5],
41
+ /// 0x4A4: Unknown (zeroed by constructor)
42
+ pub _unknown_4a4: u32,
43
+ /// 0x4A8: Sequence/reference index (-1 = none; set conditionally from DDGame+0x51C)
44
+ pub sequence_ref: i32,
45
+ /// 0x4AC: Unknown (zeroed by constructor as param_1[299])
46
+ pub _unknown_4ac: u32,
47
+ }
48
+
49
+ const _: () = assert!(core::mem::size_of::<CTaskCrate>() == 0x4B0);
crates/openwa-core/src/task/worm.rs CHANGED
@@ -104,12 +104,38 @@ pub struct CTaskWorm {
104
  pub _unknown_154: u32,
105
  /// 0x158: Worm pool slot index in DDGame (assigned from pool at construction)
106
  pub slot_id: u32,
107
- /// 0x15C–0x247: Unknown
108
- pub _unknown_15c: [u8; 0xEC],
109
- /// 0x248–0x25B: Aiming / weapon data (slots 4–8 in GetEntityData query 0x7D4)
110
- pub _unknown_248: [u32; 5],
111
- /// 0x25C–0x283: Unknown
112
- pub _unknown_25c: [u8; 0x28],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  /// 0x284–0x28B: Aiming data (slots 5–6 in GetEntityData query 0x7D4)
114
  pub _unknown_284: [u32; 2],
115
  /// 0x28C–0x2EF: Unknown
@@ -118,12 +144,30 @@ pub struct CTaskWorm {
118
  pub worm_name: [u8; 0x11],
119
  /// 0x301: Country / team name from scheme, null-terminated (max 17 chars)
120
  pub country_name: [u8; 0x11],
121
- /// 0x312–0x367: Unknown (rope string, state history, etc.)
122
- pub _unknown_312: [u8; 0x56],
 
 
 
 
123
  /// 0x368: Animator / controller object (dispatched via vtable for state animations)
124
  pub animator: *mut u8,
125
- /// 0x36C–0x3FB: Unknown (rope anchor, weapon-specific data, etc.)
126
- pub _unknown_36c: [u8; 0x90],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  }
128
 
129
  const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
 
104
  pub _unknown_154: u32,
105
  /// 0x158: Worm pool slot index in DDGame (assigned from pool at construction)
106
  pub slot_id: u32,
107
+ /// 0x15C–0x163: Unknown
108
+ pub _unknown_15c: [u8; 0x164 - 0x15C],
109
+ /// 0x164: Frames the worm has stayed stationary (no movement). Resets on movement.
110
+ pub stationary_frames: u32,
111
+ /// 0x168–0x16F: Unknown
112
+ pub _unknown_168: [u8; 0x170 - 0x168],
113
+ /// 0x170: Currently selected weapon ID.
114
+ pub selected_weapon: u32,
115
+ /// 0x174–0x1A7: Unknown
116
+ pub _unknown_174: [u8; 0x1A8 - 0x174],
117
+ /// 0x1A8: Facing direction copy. -1 = left, +1 = right (same as +0x3DC).
118
+ pub facing_direction_2: i32,
119
+ /// 0x1AC: Inverted facing direction. +1 = left, -1 = right.
120
+ pub facing_direction_inv: i32,
121
+ /// 0x1B0–0x1EB: Unknown
122
+ pub _unknown_1b0: [u8; 0x1EC - 0x1B0],
123
+ /// 0x1EC: Movement streak counter. Increases ~once per second while moving
124
+ /// in one direction. Resets to 0 when movement resumes after a stop.
125
+ /// Set to -1 when the worm is blocked (e.g. hits a wall).
126
+ pub movement_streak: i32,
127
+ /// 0x1F0–0x247: Unknown
128
+ pub _unknown_1f0: [u8; 0x248 - 0x1F0],
129
+ /// 0x248: Unknown
130
+ pub _unknown_248: u32,
131
+ /// 0x24C: Aim angle (fixed-point 16.16, range 0..0x10000 = 0..360 degrees).
132
+ pub aim_angle: u32,
133
+ /// 0x250–0x267: Unknown
134
+ pub _unknown_250: [u8; 0x268 - 0x250],
135
+ /// 0x268: Show aiming cursor flag (nonzero = cursor visible).
136
+ pub show_cursor: u32,
137
+ /// 0x26C–0x283: Unknown
138
+ pub _unknown_26c: [u8; 0x284 - 0x26C],
139
  /// 0x284–0x28B: Aiming data (slots 5–6 in GetEntityData query 0x7D4)
140
  pub _unknown_284: [u32; 2],
141
  /// 0x28C–0x2EF: Unknown
 
144
  pub worm_name: [u8; 0x11],
145
  /// 0x301: Country / team name from scheme, null-terminated (max 17 chars)
146
  pub country_name: [u8; 0x11],
147
+ /// 0x312–0x333: Unknown (rope string, state history, etc.)
148
+ pub _unknown_312: [u8; 0x334 - 0x312],
149
+ /// 0x334: Facing direction copy. -1 = left, +1 = right (same as +0x3DC).
150
+ pub facing_direction_3: i32,
151
+ /// 0x338–0x367: Unknown
152
+ pub _unknown_338: [u8; 0x368 - 0x338],
153
  /// 0x368: Animator / controller object (dispatched via vtable for state animations)
154
  pub animator: *mut u8,
155
+ /// 0x36C–0x3DB: Unknown (rope anchor, weapon-specific data, etc.)
156
+ pub _unknown_36c: [u8; 0x3DC - 0x36C],
157
+ /// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
158
+ pub facing_direction: i32,
159
+ /// 0x3E0–0x3E3: Unknown
160
+ pub _unknown_3e0: u32,
161
+ /// 0x3E4: Input: aim up key held (nonzero = adjusting aim upward).
162
+ pub input_aim_up: u32,
163
+ /// 0x3E8: Input: aim down key held (nonzero = adjusting aim downward).
164
+ pub input_aim_down: u32,
165
+ /// 0x3EC: Input: move left key held (nonzero = worm walking left).
166
+ pub input_move_left: u32,
167
+ /// 0x3F0: Input: move right key held (nonzero = worm walking right).
168
+ pub input_move_right: u32,
169
+ /// 0x3F4–0x3FB: Unknown
170
+ pub _unknown_3f4: [u8; 0x3FC - 0x3F4],
171
  }
172
 
173
  const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
crates/openwa-debugui/src/app.rs CHANGED
@@ -2,7 +2,7 @@ use eframe::egui;
2
  use openwa_core::address::va;
3
  use openwa_core::engine::{DDGame, DDGameWrapper};
4
  use openwa_core::rebase::rb;
5
- use openwa_core::task::{CTask, CTaskCloud, CTaskFire, CTaskMine, CTaskOilDrum, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx};
6
 
7
  use crate::log;
8
 
@@ -22,6 +22,7 @@ const KNOWN_VTABLES: &[(u32, &str)] = &[
22
  (va::CTASK_MISSILE_VTABLE, "CTaskMissile"),
23
  (va::CTASK_MINE_VTABLE, "CTaskMine"),
24
  (va::CTASK_OILDRUM_VTABLE, "CTaskOilDrum"),
 
25
  (va::CTASK_CLOUD_VTABLE, "CTaskCloud"),
26
  (va::CTASK_SEA_BUBBLE_VTABLE, "CTaskSeaBubble"),
27
  (va::CTASK_FIRE_VTABLE, "CTaskFire"),
@@ -279,6 +280,113 @@ impl DebugApp {
279
  }
280
  }
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  // ---------------------------------------------------------------------------
283
  // Panel: Struct Inspector
284
  // ---------------------------------------------------------------------------
@@ -403,41 +511,48 @@ impl DebugApp {
403
 
404
  // --- CTaskMine-specific fields ---
405
  if name == "CTaskMine" {
406
- let mine = addr as *const CTaskMine;
407
- egui::CollapsingHeader::new("CTaskMine")
408
- .default_open(true)
409
- .show(ui, |ui| {
410
- egui::Grid::new("mine_grid").striped(true).show(ui, |ui| {
411
- ui.label("pos_x"); ui.label(format!("{:.1}", (*mine).base.pos_x.to_f32())); ui.end_row();
412
- ui.label("pos_y"); ui.label(format!("{:.1}", (*mine).base.pos_y.to_f32())); ui.end_row();
413
- ui.label("speed_x"); ui.label(format!("{:.4}", (*mine).base.speed_x.to_f32())); ui.end_row();
414
- ui.label("speed_y"); ui.label(format!("{:.4}", (*mine).base.speed_y.to_f32())); ui.end_row();
415
- let ft = (*mine).fuse_timer;
416
- let ft_label = if ft < 0 { "disarmed".to_owned() } else if ft == 0 { "ARMED".to_owned() } else { format!("{} ticks", ft) };
417
- ui.label("fuse_timer"); ui.label(ft_label); ui.end_row();
418
- ui.label("owner_team"); ui.label(format!("{}", (*mine).owner_team)); ui.end_row();
419
- ui.label("slot_id"); ui.label(format!("{}", (*mine).slot_id)); ui.end_row();
420
- });
421
- });
422
  }
423
 
424
  // --- CTaskOilDrum-specific fields ---
425
  if name == "CTaskOilDrum" {
426
- let drum = addr as *const CTaskOilDrum;
427
- egui::CollapsingHeader::new("CTaskOilDrum")
428
- .default_open(true)
429
- .show(ui, |ui| {
430
- egui::Grid::new("drum_grid").striped(true).show(ui, |ui| {
431
- ui.label("pos_x"); ui.label(format!("{:.1}", (*drum).base.pos_x.to_f32())); ui.end_row();
432
- ui.label("pos_y"); ui.label(format!("{:.1}", (*drum).base.pos_y.to_f32())); ui.end_row();
433
- ui.label("speed_x"); ui.label(format!("{:.4}", (*drum).base.speed_x.to_f32())); ui.end_row();
434
- ui.label("speed_y"); ui.label(format!("{:.4}", (*drum).base.speed_y.to_f32())); ui.end_row();
435
- ui.label("health"); ui.label(format!("{}", (*drum).health)); ui.end_row();
436
- ui.label("on_fire"); ui.label(format!("{}", (*drum).on_fire())); ui.end_row();
437
- ui.label("triggered"); ui.label(format!("{}", (*drum).triggered != 0)); ui.end_row();
438
- ui.label("slot_id"); ui.label(format!("{}", (*drum).slot_id)); ui.end_row();
439
- });
440
- });
 
 
 
 
 
 
 
 
 
 
441
  }
442
 
443
  // --- CTaskCloud-specific fields ---
@@ -539,24 +654,77 @@ impl DebugApp {
539
 
540
  // --- CTaskWorm-specific fields ---
541
  if name == "CTaskWorm" || (*task).class_type == openwa_core::game::ClassType::Worm {
 
542
  let worm = addr as *const CTaskWorm;
543
- egui::CollapsingHeader::new("CTaskWorm")
544
- .default_open(true)
545
- .show(ui, |ui| {
546
- egui::Grid::new("worm_grid").striped(true).show(ui, |ui| {
547
- ui.label("state"); ui.label(format!("{:#04X}", (*worm).state())); ui.end_row();
548
- ui.label("team_index"); ui.label(format!("{}", (*worm).team_index)); ui.end_row();
549
- ui.label("worm_index"); ui.label(format!("{}", (*worm).worm_index)); ui.end_row();
550
- ui.label("pos_x"); ui.label(format!("{:.2}", (*worm).base.pos_x.to_f32())); ui.end_row();
551
- ui.label("pos_y"); ui.label(format!("{:.2}", (*worm).base.pos_y.to_f32())); ui.end_row();
552
- ui.label("speed_x"); ui.label(format!("{:.4}", (*worm).base.speed_x.to_f32())); ui.end_row();
553
- ui.label("speed_y"); ui.label(format!("{:.4}", (*worm).base.speed_y.to_f32())); ui.end_row();
554
- let name_bytes = &(*worm).worm_name;
555
- let nul = name_bytes.iter().position(|&b| b == 0).unwrap_or(name_bytes.len());
556
- let worm_name = std::str::from_utf8(&name_bytes[..nul]).unwrap_or("?");
557
- ui.label("name"); ui.label(worm_name); ui.end_row();
558
- });
559
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  }
561
 
562
  }});
 
2
  use openwa_core::address::va;
3
  use openwa_core::engine::{DDGame, DDGameWrapper};
4
  use openwa_core::rebase::rb;
5
+ use openwa_core::task::{CTask, CTaskCloud, CTaskFire, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx};
6
 
7
  use crate::log;
8
 
 
22
  (va::CTASK_MISSILE_VTABLE, "CTaskMissile"),
23
  (va::CTASK_MINE_VTABLE, "CTaskMine"),
24
  (va::CTASK_OILDRUM_VTABLE, "CTaskOilDrum"),
25
+ (va::CTASK_CRATE_VTABLE, "CTaskCrate"),
26
  (va::CTASK_CLOUD_VTABLE, "CTaskCloud"),
27
  (va::CTASK_SEA_BUBBLE_VTABLE, "CTaskSeaBubble"),
28
  (va::CTASK_FIRE_VTABLE, "CTaskFire"),
 
280
  }
281
  }
282
 
283
+ // ---------------------------------------------------------------------------
284
+ // Raw field viewer for CGameTask-derived types
285
+ // ---------------------------------------------------------------------------
286
+
287
+ /// CGameTask field labels for the base class region (0x00..0xFC).
288
+ const CGAMETASK_FIELD_LABELS: &[(usize, &str)] = &[
289
+ // CTask base (0x00..0x30)
290
+ (0x00, "vtable"),
291
+ (0x04, "parent"),
292
+ (0x08, "children_data"),
293
+ (0x0C, "children_watermark"),
294
+ (0x10, "children_capacity"),
295
+ (0x14, "class_type"),
296
+ (0x18, "state_machine"),
297
+ (0x1C, "shared_data"),
298
+ (0x20, "owns_shared_data"),
299
+ (0x24, "msg_handler"),
300
+ (0x28, "creation_flags"),
301
+ (0x2C, "ddgame"),
302
+ // CGameTask fields (0x30..0xFC)
303
+ (0x84, "pos_x"),
304
+ (0x88, "pos_y"),
305
+ (0x8C, "angle"),
306
+ (0x90, "speed_x"),
307
+ (0x94, "speed_y"),
308
+ // SoundEmitter (0xE8..0xFC)
309
+ (0xE8, "emitter_vtable"),
310
+ (0xEC, "emitter_unk04"),
311
+ (0xF0, "emitter_unk08"),
312
+ (0xF4, "local_snd_count"),
313
+ (0xF8, "emitter_owner"),
314
+ ];
315
+
316
+ /// Display all DWORDs of a CGameTask-derived entity with labelled fields.
317
+ /// `extra_labels` provides field names for offsets beyond the CGameTask base (0xFC+).
318
+ unsafe fn show_game_task_raw_fields(
319
+ ui: &mut egui::Ui,
320
+ addr: u32,
321
+ type_name: &str,
322
+ total_size: usize,
323
+ extra_labels: &[(usize, &str)],
324
+ ) {
325
+ let base = addr as *const u32;
326
+
327
+ let label_for = |off: usize| -> Option<&str> {
328
+ CGAMETASK_FIELD_LABELS.iter()
329
+ .find(|&&(o, _)| o == off)
330
+ .map(|&(_, n)| n)
331
+ .or_else(|| extra_labels.iter()
332
+ .find(|&&(o, _)| o == off)
333
+ .map(|&(_, n)| n))
334
+ };
335
+
336
+ // Sections: CTask base, CGameTask unknowns, pos/speed, more unknowns, emitter,
337
+ // then type-specific in 0x80-byte chunks to keep each section manageable.
338
+ let mut sections: Vec<(usize, usize, String)> = vec![
339
+ (0x000, 0x030, "CTask base".into()),
340
+ (0x030, 0x084, "CGameTask +0x30".into()),
341
+ (0x084, 0x098, "pos / speed / angle".into()),
342
+ (0x098, 0x0E8, "CGameTask +0x98".into()),
343
+ (0x0E8, 0x0FC, "SoundEmitter".into()),
344
+ ];
345
+ // Split type-specific region into chunks of 0x80 bytes
346
+ let mut chunk_start = 0x0FC;
347
+ while chunk_start < total_size {
348
+ let chunk_end = (chunk_start + 0x80).min(total_size);
349
+ sections.push((chunk_start, chunk_end,
350
+ format!("{} +0x{:03X}..0x{:03X}", type_name, chunk_start, chunk_end)));
351
+ chunk_start = chunk_end;
352
+ }
353
+
354
+ for (start, end, section_name) in &sections {
355
+ let (start, end) = (*start, *end);
356
+ if start >= total_size { break; }
357
+ let end = end.min(total_size);
358
+ let header = format!("{} (0x{:03X}..0x{:03X})", section_name, start, end);
359
+ let default_open = false;
360
+ egui::CollapsingHeader::new(header)
361
+ .id_source(format!("{}_{}_{:03X}", type_name, addr, start))
362
+ .default_open(default_open)
363
+ .show(ui, |ui| {
364
+ egui::Grid::new(format!("raw_{}_{}_{:03X}", type_name, addr, start))
365
+ .striped(true)
366
+ .num_columns(3)
367
+ .show(ui, |ui| {
368
+ ui.strong("Offset");
369
+ ui.strong("Value");
370
+ ui.strong("Field");
371
+ ui.end_row();
372
+
373
+ let dwords = (end - start) / 4;
374
+ for i in 0..dwords {
375
+ let off = start + i * 4;
376
+ let val = *base.add(off / 4);
377
+ let field_name = label_for(off).unwrap_or("");
378
+
379
+ ui.label(format!("+0x{:03X}", off));
380
+ // Show as hex + signed decimal
381
+ ui.label(format!("{:#010X} ({})", val, val as i32));
382
+ ui.label(field_name);
383
+ ui.end_row();
384
+ }
385
+ });
386
+ });
387
+ }
388
+ }
389
+
390
  // ---------------------------------------------------------------------------
391
  // Panel: Struct Inspector
392
  // ---------------------------------------------------------------------------
 
511
 
512
  // --- CTaskMine-specific fields ---
513
  if name == "CTaskMine" {
514
+ show_game_task_raw_fields(ui, addr, "CTaskMine", 0x128, &[
515
+ (0x0FC, "unknown_fc[0]"),
516
+ (0x100, "unknown_fc[4]"),
517
+ (0x104, "unknown_fc[8]"),
518
+ (0x108, "unknown_fc[C]"),
519
+ (0x10C, "unknown_fc[10]"),
520
+ (0x110, "slot_id"),
521
+ (0x114, "unknown_114"),
522
+ (0x118, "fuse_timer"),
523
+ (0x11C, "unknown_11c"),
524
+ (0x120, "unknown_120"),
525
+ (0x124, "owner_team"),
526
+ ]);
 
 
 
527
  }
528
 
529
  // --- CTaskOilDrum-specific fields ---
530
  if name == "CTaskOilDrum" {
531
+ show_game_task_raw_fields(ui, addr, "CTaskOilDrum", 0x110, &[
532
+ (0x0FC, "triggered"),
533
+ (0x100, "slot_id"),
534
+ (0x104, "unknown_104"),
535
+ (0x108, "health"),
536
+ (0x10C, "roll_counter"),
537
+ ]);
538
+ }
539
+
540
+ // --- CTaskCrate-specific fields ---
541
+ if name == "CTaskCrate" {
542
+ show_game_task_raw_fields(ui, addr, "CTaskCrate", 0x4B0, &[
543
+ (0x0FC, "unknown_fc"),
544
+ (0x100, "slot_id"),
545
+ (0x104, "unknown_104"),
546
+ (0x108, "unknown_108"),
547
+ (0x10C, "timer"),
548
+ // scheme_data starts at 0x110 (0xE5 DWORDs)
549
+ (0x124, "crate_type"),
550
+ (0x24C, "heal_amount"),
551
+ (0x264, "extra_init_flag"),
552
+ (0x4A4, "unknown_4a4"),
553
+ (0x4A8, "sequence_ref"),
554
+ (0x4AC, "unknown_4ac"),
555
+ ]);
556
  }
557
 
558
  // --- CTaskCloud-specific fields ---
 
654
 
655
  // --- CTaskWorm-specific fields ---
656
  if name == "CTaskWorm" || (*task).class_type == openwa_core::game::ClassType::Worm {
657
+ // Summary header with key info
658
  let worm = addr as *const CTaskWorm;
659
+ let name_bytes = &(*worm).worm_name;
660
+ let nul = name_bytes.iter().position(|&b| b == 0).unwrap_or(name_bytes.len());
661
+ let worm_name = std::str::from_utf8(&name_bytes[..nul]).unwrap_or("?");
662
+ ui.label(format!("Worm: \"{}\" state={:#04X} team={} idx={}",
663
+ worm_name, (*worm).state(), (*worm).team_index, (*worm).worm_index));
664
+ ui.separator();
665
+
666
+ show_game_task_raw_fields(ui, addr, "CTaskWorm", 0x3FC, &[
667
+ (0x0FC, "team_index"),
668
+ (0x100, "worm_index"),
669
+ (0x104, "unknown_104"),
670
+ (0x110, "spawn_params[0]"),
671
+ (0x114, "spawn_params[1]"),
672
+ (0x118, "spawn_params[2]"),
673
+ (0x11C, "spawn_params[3]"),
674
+ (0x120, "spawn_params[4]"),
675
+ (0x124, "spawn_params[5]"),
676
+ (0x128, "spawn_params[6]"),
677
+ (0x12C, "spawn_params[7]"),
678
+ (0x130, "spawn_params[8]"),
679
+ (0x134, "spawn_params[9]"),
680
+ (0x158, "slot_id"),
681
+ (0x164, "stationary_frames"),
682
+ (0x170, "selected_weapon"),
683
+ (0x1A8, "facing_dir_2"),
684
+ (0x1AC, "facing_dir_inv"),
685
+ (0x1EC, "movement_streak"),
686
+ (0x24C, "aim_angle"),
687
+ (0x268, "show_cursor"),
688
+ (0x2F0, "worm_name[0..4]"),
689
+ (0x2F4, "worm_name[4..8]"),
690
+ (0x2F8, "worm_name[8..12]"),
691
+ (0x2FC, "worm_name[12..16]"),
692
+ (0x334, "facing_dir_3"),
693
+ (0x368, "animator"),
694
+ (0x3DC, "facing_direction"),
695
+ (0x3E4, "input_aim_up"),
696
+ (0x3E8, "input_aim_down"),
697
+ (0x3EC, "input_move_left"),
698
+ (0x3F0, "input_move_right"),
699
+ ]);
700
+ }
701
+
702
+ // --- CTaskMissile-specific fields ---
703
+ if name == "CTaskMissile" {
704
+ use openwa_core::task::CTaskMissile;
705
+ let m = &*(addr as *const CTaskMissile);
706
+ ui.label(format!("Missile: type={:?} slot={} homing={} dir={}",
707
+ m.missile_type(), m.slot_id, m.homing_enabled, m.direction));
708
+ ui.separator();
709
+
710
+ show_game_task_raw_fields(ui, addr, "CTaskMissile", 0x41C, &[
711
+ (0x128, "launch_seed"),
712
+ (0x12C, "slot_id"),
713
+ (0x130, "spawn: owner_id"),
714
+ (0x134, "spawn: [1]"),
715
+ (0x138, "spawn: spawn_x"),
716
+ (0x13C, "spawn: spawn_y"),
717
+ (0x140, "spawn: speed_x"),
718
+ (0x144, "spawn: speed_y"),
719
+ (0x148, "spawn: cursor_x"),
720
+ (0x14C, "spawn: cursor_y"),
721
+ (0x150, "spawn: pellet_idx"),
722
+ (0x154, "spawn: fb_timer"),
723
+ (0x158, "spawn: fb_param"),
724
+ (0x3A0, "launch_speed"),
725
+ (0x3A8, "homing_enabled"),
726
+ (0x3C8, "direction"),
727
+ ]);
728
  }
729
 
730
  }});
crates/openwa-wormkit/src/replacements/mod.rs CHANGED
@@ -18,7 +18,8 @@ pub fn install_all() -> Result<(), String> {
18
  game_session::install()?;
19
  hardware_init::install()?;
20
  frontend::install()?;
21
- scheme::install()?;
 
22
  config::install()?;
23
  weapon::install()?;
24
  team::install()?;
 
18
  game_session::install()?;
19
  hardware_init::install()?;
20
  frontend::install()?;
21
+ // Temporarily disabled: loading custom scheme causes game to play super fast
22
+ // scheme::install()?;
23
  config::install()?;
24
  weapon::install()?;
25
  team::install()?;
crates/openwa-wormkit/src/validation/mod.rs CHANGED
@@ -801,6 +801,7 @@ fn dump_entity_census() {
801
  (va::CTASK_DIRT_VTABLE, "CTaskDirt"),
802
  (va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"),
803
  (va::CTASK_CPU_VTABLE, "CTaskCPU"),
 
804
  (va::CTASK_VTABLE, "CTask"),
805
  (va::CGAMETASK_VTABLE, "CGameTask"),
806
  ];
 
801
  (va::CTASK_DIRT_VTABLE, "CTaskDirt"),
802
  (va::CTASK_SPRITE_ANIM_VTABLE, "CTaskSpriteAnim"),
803
  (va::CTASK_CPU_VTABLE, "CTaskCPU"),
804
+ (va::CTASK_CRATE_VTABLE, "CTaskCrate"),
805
  (va::CTASK_VTABLE, "CTask"),
806
  (va::CGAMETASK_VTABLE, "CGameTask"),
807
  ];