feat: replace hardcoded debug UI field labels with registry lookups
Browse filesRemove CGAMETASK_FIELD_LABELS and all per-entity extra_labels arrays
from the debug UI inspector. Field names are now resolved via
registry::field_at_inherited() which walks the CTask inheritance chain
(entity → CGameTask → CTask).
Add #[derive(FieldRegistry)] to CGameTask and CTaskMissile. Add
field_at_inherited() helper for inheritance-aware offset → name lookup.
Entity types without Rust struct definitions (CTaskMine, CTaskOilDrum,
CTaskCrate) will show base class fields only until their structs are
defined — but any new struct fields are automatically picked up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/registry.rs
CHANGED
|
@@ -277,6 +277,37 @@ pub fn all_struct_fields() -> impl Iterator<Item = &'static StructFields> {
|
|
| 277 |
struct_map().values().copied()
|
| 278 |
}
|
| 279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
// =========================================================================
|
| 281 |
// Live object tracker
|
| 282 |
// =========================================================================
|
|
|
|
| 277 |
struct_map().values().copied()
|
| 278 |
}
|
| 279 |
|
| 280 |
+
/// Look up a field at `offset` using inheritance-aware search.
|
| 281 |
+
///
|
| 282 |
+
/// For CTask-derived entities, the inheritance chain is:
|
| 283 |
+
/// entity-specific fields → CGameTask → CTask.
|
| 284 |
+
/// This function tries each registry in order, returning the first match.
|
| 285 |
+
///
|
| 286 |
+
/// `struct_name` is the leaf class name (e.g., "CTaskWorm").
|
| 287 |
+
pub fn field_at_inherited(struct_name: &str, offset: u32) -> Option<&'static FieldEntry> {
|
| 288 |
+
// Inheritance chain for CTask hierarchy. Could be generalized later
|
| 289 |
+
// but this covers all current game entity types.
|
| 290 |
+
const CTASK_CHAIN: &[&str] = &["CGameTask", "CTask"];
|
| 291 |
+
|
| 292 |
+
// Try the leaf struct first
|
| 293 |
+
if let Some(fields) = struct_fields_for(struct_name) {
|
| 294 |
+
if let Some(entry) = fields.field_at(offset) {
|
| 295 |
+
return Some(entry);
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
// Walk the inheritance chain
|
| 300 |
+
for &parent in CTASK_CHAIN {
|
| 301 |
+
if let Some(fields) = struct_fields_for(parent) {
|
| 302 |
+
if let Some(entry) = fields.field_at(offset) {
|
| 303 |
+
return Some(entry);
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
None
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
// =========================================================================
|
| 312 |
// Live object tracker
|
| 313 |
// =========================================================================
|
crates/openwa-core/src/task/game_task.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
use super::base::CTask;
|
| 2 |
use crate::fixed::Fixed;
|
|
|
|
| 3 |
|
| 4 |
/// Game task - extends CTask with physics and gameplay data.
|
| 5 |
///
|
|
@@ -9,6 +10,7 @@ use crate::fixed::Fixed;
|
|
| 9 |
/// Source: wkJellyWorm CGameTask.h
|
| 10 |
///
|
| 11 |
/// Additional vtable (12 methods at offsets 0x1C-0x48 in vtable)
|
|
|
|
| 12 |
#[repr(C)]
|
| 13 |
pub struct CGameTask {
|
| 14 |
/// 0x00-0x2F: Base CTask fields
|
|
|
|
| 1 |
use super::base::CTask;
|
| 2 |
use crate::fixed::Fixed;
|
| 3 |
+
use crate::FieldRegistry;
|
| 4 |
|
| 5 |
/// Game task - extends CTask with physics and gameplay data.
|
| 6 |
///
|
|
|
|
| 10 |
/// Source: wkJellyWorm CGameTask.h
|
| 11 |
///
|
| 12 |
/// Additional vtable (12 methods at offsets 0x1C-0x48 in vtable)
|
| 13 |
+
#[derive(FieldRegistry)]
|
| 14 |
#[repr(C)]
|
| 15 |
pub struct CGameTask {
|
| 16 |
/// 0x00-0x2F: Base CTask fields
|
crates/openwa-core/src/task/missile.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
use super::game_task::CGameTask;
|
| 2 |
use crate::fixed::Fixed;
|
|
|
|
| 3 |
|
| 4 |
/// Projectile / missile entity task.
|
| 5 |
///
|
|
@@ -18,6 +19,7 @@ use crate::fixed::Fixed;
|
|
| 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).
|
|
|
|
| 1 |
use super::game_task::CGameTask;
|
| 2 |
use crate::fixed::Fixed;
|
| 3 |
+
use crate::FieldRegistry;
|
| 4 |
|
| 5 |
/// Projectile / missile entity task.
|
| 6 |
///
|
|
|
|
| 19 |
///
|
| 20 |
/// Source: Ghidra decompilation of 0x507D10 (constructor) and
|
| 21 |
/// wkJellyWorm CTaskMissile.h (field layout reference).
|
| 22 |
+
#[derive(FieldRegistry)]
|
| 23 |
#[repr(C)]
|
| 24 |
pub struct CTaskMissile {
|
| 25 |
/// 0x00–0xFB: CGameTask base (pos at 0x84/0x88, speed at 0x90/0x94).
|
crates/openwa-debugui/src/app.rs
CHANGED
|
@@ -2,6 +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::{
|
| 6 |
CTask, CTaskCloud, CTaskFire, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx,
|
| 7 |
};
|
|
@@ -336,58 +337,18 @@ impl DebugApp {
|
|
| 336 |
// ---------------------------------------------------------------------------
|
| 337 |
|
| 338 |
/// CGameTask field labels for the base class region (0x00..0xFC).
|
| 339 |
-
const CGAMETASK_FIELD_LABELS: &[(usize, &str)] = &[
|
| 340 |
-
// CTask base (0x00..0x30)
|
| 341 |
-
(0x00, "vtable"),
|
| 342 |
-
(0x04, "parent"),
|
| 343 |
-
(0x08, "children_data"),
|
| 344 |
-
(0x0C, "children_watermark"),
|
| 345 |
-
(0x10, "children_capacity"),
|
| 346 |
-
(0x14, "class_type"),
|
| 347 |
-
(0x18, "bit_grid"),
|
| 348 |
-
(0x1C, "shared_data"),
|
| 349 |
-
(0x20, "owns_shared_data"),
|
| 350 |
-
(0x24, "msg_handler"),
|
| 351 |
-
(0x28, "creation_flags"),
|
| 352 |
-
(0x2C, "ddgame"),
|
| 353 |
-
// CGameTask fields (0x30..0xFC)
|
| 354 |
-
(0x84, "pos_x"),
|
| 355 |
-
(0x88, "pos_y"),
|
| 356 |
-
(0x8C, "angle"),
|
| 357 |
-
(0x90, "speed_x"),
|
| 358 |
-
(0x94, "speed_y"),
|
| 359 |
-
// SoundEmitter (0xE8..0xFC)
|
| 360 |
-
(0xE8, "emitter_vtable"),
|
| 361 |
-
(0xEC, "emitter_unk04"),
|
| 362 |
-
(0xF0, "emitter_unk08"),
|
| 363 |
-
(0xF4, "local_snd_count"),
|
| 364 |
-
(0xF8, "emitter_owner"),
|
| 365 |
-
];
|
| 366 |
-
|
| 367 |
/// Display all DWORDs of a CGameTask-derived entity with labelled fields.
|
| 368 |
-
///
|
|
|
|
|
|
|
| 369 |
unsafe fn show_game_task_raw_fields(
|
| 370 |
ui: &mut egui::Ui,
|
| 371 |
addr: u32,
|
| 372 |
type_name: &str,
|
| 373 |
total_size: usize,
|
| 374 |
-
extra_labels: &[(usize, &str)],
|
| 375 |
) {
|
| 376 |
let base = addr as *const u32;
|
| 377 |
|
| 378 |
-
let label_for = |off: usize| -> Option<&str> {
|
| 379 |
-
CGAMETASK_FIELD_LABELS
|
| 380 |
-
.iter()
|
| 381 |
-
.find(|&&(o, _)| o == off)
|
| 382 |
-
.map(|&(_, n)| n)
|
| 383 |
-
.or_else(|| {
|
| 384 |
-
extra_labels
|
| 385 |
-
.iter()
|
| 386 |
-
.find(|&&(o, _)| o == off)
|
| 387 |
-
.map(|&(_, n)| n)
|
| 388 |
-
})
|
| 389 |
-
};
|
| 390 |
-
|
| 391 |
// Sections: CTask base, CGameTask unknowns, pos/speed, more unknowns, emitter,
|
| 392 |
// then type-specific in 0x80-byte chunks to keep each section manageable.
|
| 393 |
let mut sections: Vec<(usize, usize, String)> = vec![
|
|
@@ -434,7 +395,9 @@ unsafe fn show_game_task_raw_fields(
|
|
| 434 |
for i in 0..dwords {
|
| 435 |
let off = start + i * 4;
|
| 436 |
let val = *base.add(off / 4);
|
| 437 |
-
let field_name =
|
|
|
|
|
|
|
| 438 |
|
| 439 |
ui.label(format!("+0x{:03X}", off));
|
| 440 |
// Show as hex + signed decimal
|
|
@@ -592,64 +555,21 @@ impl DebugApp {
|
|
| 592 |
// --- CTaskMine-specific fields ---
|
| 593 |
if name == "CTaskMine" {
|
| 594 |
show_game_task_raw_fields(
|
| 595 |
-
ui,
|
| 596 |
-
addr,
|
| 597 |
-
"CTaskMine",
|
| 598 |
-
0x128,
|
| 599 |
-
&[
|
| 600 |
-
(0x0FC, "unknown_fc[0]"),
|
| 601 |
-
(0x100, "unknown_fc[4]"),
|
| 602 |
-
(0x104, "unknown_fc[8]"),
|
| 603 |
-
(0x108, "unknown_fc[C]"),
|
| 604 |
-
(0x10C, "unknown_fc[10]"),
|
| 605 |
-
(0x110, "slot_id"),
|
| 606 |
-
(0x114, "unknown_114"),
|
| 607 |
-
(0x118, "fuse_timer"),
|
| 608 |
-
(0x11C, "unknown_11c"),
|
| 609 |
-
(0x120, "unknown_120"),
|
| 610 |
-
(0x124, "owner_team"),
|
| 611 |
-
],
|
| 612 |
);
|
| 613 |
}
|
| 614 |
|
| 615 |
// --- CTaskOilDrum-specific fields ---
|
| 616 |
if name == "CTaskOilDrum" {
|
| 617 |
show_game_task_raw_fields(
|
| 618 |
-
ui,
|
| 619 |
-
addr,
|
| 620 |
-
"CTaskOilDrum",
|
| 621 |
-
0x110,
|
| 622 |
-
&[
|
| 623 |
-
(0x0FC, "triggered"),
|
| 624 |
-
(0x100, "slot_id"),
|
| 625 |
-
(0x104, "unknown_104"),
|
| 626 |
-
(0x108, "health"),
|
| 627 |
-
(0x10C, "roll_counter"),
|
| 628 |
-
],
|
| 629 |
);
|
| 630 |
}
|
| 631 |
|
| 632 |
// --- CTaskCrate-specific fields ---
|
| 633 |
if name == "CTaskCrate" {
|
| 634 |
show_game_task_raw_fields(
|
| 635 |
-
ui,
|
| 636 |
-
addr,
|
| 637 |
-
"CTaskCrate",
|
| 638 |
-
0x4B0,
|
| 639 |
-
&[
|
| 640 |
-
(0x0FC, "unknown_fc"),
|
| 641 |
-
(0x100, "slot_id"),
|
| 642 |
-
(0x104, "unknown_104"),
|
| 643 |
-
(0x108, "unknown_108"),
|
| 644 |
-
(0x10C, "timer"),
|
| 645 |
-
// scheme_data starts at 0x110 (0xE5 DWORDs)
|
| 646 |
-
(0x124, "crate_type"),
|
| 647 |
-
(0x24C, "heal_amount"),
|
| 648 |
-
(0x264, "extra_init_flag"),
|
| 649 |
-
(0x4A4, "unknown_4a4"),
|
| 650 |
-
(0x4A8, "sequence_ref"),
|
| 651 |
-
(0x4AC, "unknown_4ac"),
|
| 652 |
-
],
|
| 653 |
);
|
| 654 |
}
|
| 655 |
|
|
@@ -869,44 +789,7 @@ impl DebugApp {
|
|
| 869 |
ui.separator();
|
| 870 |
|
| 871 |
show_game_task_raw_fields(
|
| 872 |
-
ui,
|
| 873 |
-
addr,
|
| 874 |
-
"CTaskWorm",
|
| 875 |
-
0x3FC,
|
| 876 |
-
&[
|
| 877 |
-
(0x0FC, "team_index"),
|
| 878 |
-
(0x100, "worm_index"),
|
| 879 |
-
(0x104, "unknown_104"),
|
| 880 |
-
(0x110, "spawn_params[0]"),
|
| 881 |
-
(0x114, "spawn_params[1]"),
|
| 882 |
-
(0x118, "spawn_params[2]"),
|
| 883 |
-
(0x11C, "spawn_params[3]"),
|
| 884 |
-
(0x120, "spawn_params[4]"),
|
| 885 |
-
(0x124, "spawn_params[5]"),
|
| 886 |
-
(0x128, "spawn_params[6]"),
|
| 887 |
-
(0x12C, "spawn_params[7]"),
|
| 888 |
-
(0x130, "spawn_params[8]"),
|
| 889 |
-
(0x134, "spawn_params[9]"),
|
| 890 |
-
(0x158, "slot_id"),
|
| 891 |
-
(0x164, "stationary_frames"),
|
| 892 |
-
(0x170, "selected_weapon"),
|
| 893 |
-
(0x1A8, "facing_dir_2"),
|
| 894 |
-
(0x1AC, "facing_dir_inv"),
|
| 895 |
-
(0x1EC, "movement_streak"),
|
| 896 |
-
(0x24C, "aim_angle"),
|
| 897 |
-
(0x268, "show_cursor"),
|
| 898 |
-
(0x2F0, "worm_name[0..4]"),
|
| 899 |
-
(0x2F4, "worm_name[4..8]"),
|
| 900 |
-
(0x2F8, "worm_name[8..12]"),
|
| 901 |
-
(0x2FC, "worm_name[12..16]"),
|
| 902 |
-
(0x334, "facing_dir_3"),
|
| 903 |
-
(0x368, "animator"),
|
| 904 |
-
(0x3DC, "facing_direction"),
|
| 905 |
-
(0x3E4, "input_aim_up"),
|
| 906 |
-
(0x3E8, "input_aim_down"),
|
| 907 |
-
(0x3EC, "input_move_left"),
|
| 908 |
-
(0x3F0, "input_move_right"),
|
| 909 |
-
],
|
| 910 |
);
|
| 911 |
}
|
| 912 |
|
|
@@ -924,28 +807,7 @@ impl DebugApp {
|
|
| 924 |
ui.separator();
|
| 925 |
|
| 926 |
show_game_task_raw_fields(
|
| 927 |
-
ui,
|
| 928 |
-
addr,
|
| 929 |
-
"CTaskMissile",
|
| 930 |
-
0x41C,
|
| 931 |
-
&[
|
| 932 |
-
(0x128, "launch_seed"),
|
| 933 |
-
(0x12C, "slot_id"),
|
| 934 |
-
(0x130, "spawn: owner_id"),
|
| 935 |
-
(0x134, "spawn: [1]"),
|
| 936 |
-
(0x138, "spawn: spawn_x"),
|
| 937 |
-
(0x13C, "spawn: spawn_y"),
|
| 938 |
-
(0x140, "spawn: speed_x"),
|
| 939 |
-
(0x144, "spawn: speed_y"),
|
| 940 |
-
(0x148, "spawn: cursor_x"),
|
| 941 |
-
(0x14C, "spawn: cursor_y"),
|
| 942 |
-
(0x150, "spawn: pellet_idx"),
|
| 943 |
-
(0x154, "spawn: fb_timer"),
|
| 944 |
-
(0x158, "spawn: fb_param"),
|
| 945 |
-
(0x3A0, "launch_speed"),
|
| 946 |
-
(0x3A8, "homing_enabled"),
|
| 947 |
-
(0x3C8, "direction"),
|
| 948 |
-
],
|
| 949 |
);
|
| 950 |
}
|
| 951 |
}
|
|
|
|
| 2 |
use openwa_core::address::va;
|
| 3 |
use openwa_core::engine::{DDGame, DDGameWrapper};
|
| 4 |
use openwa_core::rebase::rb;
|
| 5 |
+
use openwa_core::registry;
|
| 6 |
use openwa_core::task::{
|
| 7 |
CTask, CTaskCloud, CTaskFire, CTaskTeam, CTaskTurnGame, CTaskWorm, TurnGameCtx,
|
| 8 |
};
|
|
|
|
| 337 |
// ---------------------------------------------------------------------------
|
| 338 |
|
| 339 |
/// CGameTask field labels for the base class region (0x00..0xFC).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
/// Display all DWORDs of a CGameTask-derived entity with labelled fields.
|
| 341 |
+
///
|
| 342 |
+
/// Field names are resolved from the global registry via inheritance-aware
|
| 343 |
+
/// lookup (entity → CGameTask → CTask). No hardcoded label tables needed.
|
| 344 |
unsafe fn show_game_task_raw_fields(
|
| 345 |
ui: &mut egui::Ui,
|
| 346 |
addr: u32,
|
| 347 |
type_name: &str,
|
| 348 |
total_size: usize,
|
|
|
|
| 349 |
) {
|
| 350 |
let base = addr as *const u32;
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
// Sections: CTask base, CGameTask unknowns, pos/speed, more unknowns, emitter,
|
| 353 |
// then type-specific in 0x80-byte chunks to keep each section manageable.
|
| 354 |
let mut sections: Vec<(usize, usize, String)> = vec![
|
|
|
|
| 395 |
for i in 0..dwords {
|
| 396 |
let off = start + i * 4;
|
| 397 |
let val = *base.add(off / 4);
|
| 398 |
+
let field_name = registry::field_at_inherited(type_name, off as u32)
|
| 399 |
+
.map(|f| f.name)
|
| 400 |
+
.unwrap_or("");
|
| 401 |
|
| 402 |
ui.label(format!("+0x{:03X}", off));
|
| 403 |
// Show as hex + signed decimal
|
|
|
|
| 555 |
// --- CTaskMine-specific fields ---
|
| 556 |
if name == "CTaskMine" {
|
| 557 |
show_game_task_raw_fields(
|
| 558 |
+
ui, addr, "CTaskMine", 0x128,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
);
|
| 560 |
}
|
| 561 |
|
| 562 |
// --- CTaskOilDrum-specific fields ---
|
| 563 |
if name == "CTaskOilDrum" {
|
| 564 |
show_game_task_raw_fields(
|
| 565 |
+
ui, addr, "CTaskOilDrum", 0x110,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
);
|
| 567 |
}
|
| 568 |
|
| 569 |
// --- CTaskCrate-specific fields ---
|
| 570 |
if name == "CTaskCrate" {
|
| 571 |
show_game_task_raw_fields(
|
| 572 |
+
ui, addr, "CTaskCrate", 0x4B0,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
);
|
| 574 |
}
|
| 575 |
|
|
|
|
| 789 |
ui.separator();
|
| 790 |
|
| 791 |
show_game_task_raw_fields(
|
| 792 |
+
ui, addr, "CTaskWorm", 0x3FC,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
);
|
| 794 |
}
|
| 795 |
|
|
|
|
| 807 |
ui.separator();
|
| 808 |
|
| 809 |
show_game_task_raw_fields(
|
| 810 |
+
ui, addr, "CTaskMissile", 0x41C,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
);
|
| 812 |
}
|
| 813 |
}
|