paavohtl Claude Sonnet 4.6 commited on
Commit
5071a1d
·
1 Parent(s): 0e1013d

feat(debugui): task tree entity census, transient filtering, scrollable inspector

Browse files

- Replace SharedDataTable-based census with full DFS from CTaskTurnGame root,
so parent-hierarchy entities (CTaskTurnGame etc.) are included and the
liveness check no longer wrongly prunes them
- Filter transient entities (CTaskSeaBubble) from census by default; toolbar
checkbox toggles visibility and shows hidden count in the entity label
- Wrap inspector body in a ScrollArea so long entity trees are fully reachable
- Make census ScrollArea fill panel width (auto_shrink disabled, num_columns=3)
- Add CTASK_SEA_BUBBLE_VTABLE (0x669E88) to address.rs

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

crates/openwa-core/src/address.rs CHANGED
@@ -69,6 +69,8 @@ pub mod va {
69
  pub const CTASK_OILDRUM_VTABLE: u32 = 0x0066_4338;
70
  /// CTaskCloud vtable - cloud/airstrike entity (constructor 0x5482E0)
71
  pub const CTASK_CLOUD_VTABLE: u32 = 0x0066_9D38;
 
 
72
  /// Sprite vtable (0x70-byte objects, 8 entries)
73
  pub const SPRITE_VTABLE: u32 = 0x0066_418C;
74
 
 
69
  pub const CTASK_OILDRUM_VTABLE: u32 = 0x0066_4338;
70
  /// CTaskCloud vtable - cloud/airstrike entity (constructor 0x5482E0)
71
  pub const CTASK_CLOUD_VTABLE: u32 = 0x0066_9D38;
72
+ /// CTaskSeaBubble vtable - water bubble particle (constructor 0x554FE0)
73
+ pub const CTASK_SEA_BUBBLE_VTABLE: u32 = 0x0066_9E88;
74
  /// Sprite vtable (0x70-byte objects, 8 entries)
75
  pub const SPRITE_VTABLE: u32 = 0x0066_418C;
76
 
crates/openwa-debugui/src/app.rs CHANGED
@@ -3,7 +3,7 @@ use openwa_core::address::va;
3
  use openwa_core::ddgame::DDGame;
4
  use openwa_core::ddgame_wrapper::DDGameWrapper;
5
  use openwa_core::rebase::rb;
6
- use openwa_core::task::{CTask, CTaskWorm, SharedDataTable};
7
 
8
  use crate::log;
9
 
@@ -23,6 +23,13 @@ const KNOWN_VTABLES: &[(u32, &str)] = &[
23
  (va::CTASK_MINE_VTABLE, "CTaskMine"),
24
  (va::CTASK_OILDRUM_VTABLE, "CTaskOilDrum"),
25
  (va::CTASK_CLOUD_VTABLE, "CTaskCloud"),
 
 
 
 
 
 
 
26
  ];
27
 
28
  fn vtable_name(runtime_vtable: u32) -> Option<&'static str> {
@@ -64,15 +71,6 @@ unsafe fn get_ddgame() -> Option<*const DDGame> {
64
  Some(ddgame_ptr)
65
  }
66
 
67
- /// Returns a SharedDataTable via CTaskLand, or None if map not loaded.
68
- unsafe fn get_shared_table(ddgame: *const DDGame) -> Option<SharedDataTable> {
69
- let task_land = (*ddgame).task_land;
70
- if task_land.is_null() { return None; }
71
- let shared_data = (*(task_land as *const CTask)).shared_data;
72
- if shared_data.is_null() { return None; }
73
- Some(SharedDataTable::from_ptr(shared_data))
74
- }
75
-
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
@@ -89,22 +87,47 @@ unsafe fn read_children(task: *const CTask) -> Vec<u32> {
89
  }
90
 
91
  // ---------------------------------------------------------------------------
92
- // Live entity snapshot (built once per frame)
93
  // ---------------------------------------------------------------------------
94
 
95
- unsafe fn collect_live_entities() -> Vec<(u32, u32)> {
96
- let Some(ddgame) = get_ddgame() else { return Vec::new(); };
97
- let Some(table) = get_shared_table(ddgame) else { return Vec::new(); };
98
- let mut out = Vec::new();
99
- for node in table.iter() {
100
- let entity = (*node).entity as u32;
101
- if entity == 0 { continue; }
102
- let vtable = *(entity as *const u32);
103
- out.push((vtable, entity));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
105
  out
106
  }
107
 
 
 
 
 
 
 
108
  // ---------------------------------------------------------------------------
109
  // DebugApp
110
  // ---------------------------------------------------------------------------
@@ -116,11 +139,13 @@ pub struct DebugApp {
116
  nav_history: Vec<u32>,
117
  /// Whether the log panel should auto-scroll to the bottom.
118
  log_auto_scroll: bool,
 
 
119
  }
120
 
121
  impl Default for DebugApp {
122
  fn default() -> Self {
123
- Self { selected_entity: None, nav_history: Vec::new(), log_auto_scroll: true }
124
  }
125
  }
126
 
@@ -163,6 +188,12 @@ impl eframe::App for DebugApp {
163
  }
164
  self.nav_history.retain(|a| live_addrs.contains(a));
165
 
 
 
 
 
 
 
166
  egui::SidePanel::right("inspector_panel")
167
  .min_width(260.0)
168
  .default_width(300.0)
@@ -202,12 +233,29 @@ impl DebugApp {
202
  return;
203
  }
204
 
205
- ui.label(format!("{} entities total", rows.len()));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  ui.separator();
207
 
208
- egui::ScrollArea::vertical().show(ui, |ui| {
 
 
209
  egui::Grid::new("census_grid")
210
  .striped(true)
 
211
  .min_col_width(60.0)
212
  .show(ui, |ui| {
213
  ui.strong("Type");
@@ -215,7 +263,7 @@ impl DebugApp {
215
  ui.strong("Vtable");
216
  ui.end_row();
217
 
218
- for &(vtable, entity) in rows {
219
  let name = unsafe { entity_type_name(entity) };
220
  let is_selected = self.selected_entity == Some(entity);
221
  if ui.selectable_label(is_selected, name).clicked() {
@@ -263,7 +311,9 @@ impl DebugApp {
263
  return;
264
  }
265
 
266
- unsafe {
 
 
267
  let vtable = *(addr as *const u32);
268
  let name = entity_type_name(addr);
269
 
@@ -375,7 +425,7 @@ impl DebugApp {
375
  });
376
  }
377
 
378
- }
379
  }
380
  }
381
 
 
3
  use openwa_core::ddgame::DDGame;
4
  use openwa_core::ddgame_wrapper::DDGameWrapper;
5
  use openwa_core::rebase::rb;
6
+ use openwa_core::task::{CTask, CTaskWorm};
7
 
8
  use crate::log;
9
 
 
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
+ ];
28
+
29
+ /// Vtables of entities that are created/destroyed every frame (particles,
30
+ /// bubbles, etc.). Filtered from the census by default to reduce noise.
31
+ const TRANSIENT_VTABLES: &[u32] = &[
32
+ va::CTASK_SEA_BUBBLE_VTABLE,
33
  ];
34
 
35
  fn vtable_name(runtime_vtable: u32) -> Option<&'static str> {
 
71
  Some(ddgame_ptr)
72
  }
73
 
 
 
 
 
 
 
 
 
 
74
  /// Read child task pointers from a CTask's children array.
75
  ///
76
  /// The array is **sparse**: slots are nulled when a child is removed rather than
 
87
  }
88
 
89
  // ---------------------------------------------------------------------------
90
+ // Live entity snapshot (built once per frame via full task-tree traversal)
91
  // ---------------------------------------------------------------------------
92
 
93
+ /// Walk up parent pointers from `start` to find the root task (no parent).
94
+ /// Returns None if task_land is null or the chain doesn't terminate within
95
+ /// MAX_DEPTH steps (guard against corrupt/circular pointers).
96
+ unsafe fn find_root_task(ddgame: *const DDGame) -> Option<u32> {
97
+ let task_land = (*ddgame).task_land as u32;
98
+ if task_land == 0 { return None; }
99
+ let mut current = task_land;
100
+ for _ in 0..64 {
101
+ let parent = (*(current as *const CTask)).parent as u32;
102
+ if parent == 0 { return Some(current); }
103
+ current = parent;
104
+ }
105
+ None // chain didn't terminate — corrupt data
106
+ }
107
+
108
+ /// DFS the task tree from `root`, returning (vtable, addr) for every node.
109
+ /// A visited set prevents infinite loops from corrupt/circular pointers.
110
+ unsafe fn collect_task_tree(root: u32) -> Vec<(u32, u32)> {
111
+ let mut out = Vec::new();
112
+ let mut stack = vec![root];
113
+ let mut visited = std::collections::HashSet::new();
114
+ while let Some(addr) = stack.pop() {
115
+ if addr == 0 || !visited.insert(addr) { continue; }
116
+ let vtable = *(addr as *const u32);
117
+ out.push((vtable, addr));
118
+ for child in read_children(addr as *const CTask) {
119
+ if child != 0 { stack.push(child); }
120
+ }
121
  }
122
  out
123
  }
124
 
125
+ unsafe fn collect_live_entities() -> Vec<(u32, u32)> {
126
+ let Some(ddgame) = get_ddgame() else { return Vec::new(); };
127
+ let Some(root) = find_root_task(ddgame) else { return Vec::new(); };
128
+ collect_task_tree(root)
129
+ }
130
+
131
  // ---------------------------------------------------------------------------
132
  // DebugApp
133
  // ---------------------------------------------------------------------------
 
139
  nav_history: Vec<u32>,
140
  /// Whether the log panel should auto-scroll to the bottom.
141
  log_auto_scroll: bool,
142
+ /// Show transient entities (sea bubbles, etc.) in the census.
143
+ show_transient: bool,
144
  }
145
 
146
  impl Default for DebugApp {
147
  fn default() -> Self {
148
+ Self { selected_entity: None, nav_history: Vec::new(), log_auto_scroll: true, show_transient: false }
149
  }
150
  }
151
 
 
188
  }
189
  self.nav_history.retain(|a| live_addrs.contains(a));
190
 
191
+ egui::TopBottomPanel::top("toolbar").show(ctx, |ui| {
192
+ ui.horizontal(|ui| {
193
+ ui.checkbox(&mut self.show_transient, "Show transient");
194
+ });
195
+ });
196
+
197
  egui::SidePanel::right("inspector_panel")
198
  .min_width(260.0)
199
  .default_width(300.0)
 
233
  return;
234
  }
235
 
236
+ let visible: Vec<&(u32, u32)> = if self.show_transient {
237
+ rows.iter().collect()
238
+ } else {
239
+ rows.iter()
240
+ .filter(|&&(vtable, _)| {
241
+ !TRANSIENT_VTABLES.iter().any(|&t| rb(t) == vtable)
242
+ })
243
+ .collect()
244
+ };
245
+
246
+ if visible.len() == rows.len() {
247
+ ui.label(format!("{} entities", rows.len()));
248
+ } else {
249
+ ui.label(format!("{} entities ({} hidden transient)", visible.len(), rows.len() - visible.len()));
250
+ }
251
  ui.separator();
252
 
253
+ egui::ScrollArea::vertical()
254
+ .auto_shrink([false, false])
255
+ .show(ui, |ui| {
256
  egui::Grid::new("census_grid")
257
  .striped(true)
258
+ .num_columns(3)
259
  .min_col_width(60.0)
260
  .show(ui, |ui| {
261
  ui.strong("Type");
 
263
  ui.strong("Vtable");
264
  ui.end_row();
265
 
266
+ for &&(vtable, entity) in &visible {
267
  let name = unsafe { entity_type_name(entity) };
268
  let is_selected = self.selected_entity == Some(entity);
269
  if ui.selectable_label(is_selected, name).clicked() {
 
311
  return;
312
  }
313
 
314
+ egui::ScrollArea::vertical()
315
+ .auto_shrink([false, false])
316
+ .show(ui, |ui| { unsafe {
317
  let vtable = *(addr as *const u32);
318
  let name = entity_type_name(addr);
319
 
 
425
  });
426
  }
427
 
428
+ }});
429
  }
430
  }
431