paavohtl Claude Sonnet 4.6 commited on
Commit
466d58a
Β·
1 Parent(s): b9cbd32

feat: extract TurnGameCtx struct from CTaskTurnGame._game_ctx

Browse files

Full constructor analysis of CTaskTeam__Constructor_Maybe (0x550EB0) and
CTaskTurnGame__Constructor (0x55B2A0) revealed the layout of all 0xAC bytes:

land_height / land_height_2 (+0x10/+0x14): landscape height as Fixed16.16
from DDGame+0x5E0, duplicated; 696.0 confirmed at runtime.
_sentinel_18/28/38 (+0x18/+0x28/+0x38): always -1 at construction,
evenly spaced 0x10 bytes apart; role unknown.
team_count (+0x4C): *(byte)(GameInfo+0x44C) at construction time.
_slot_d0 (+0xA0): return value of FUN_00525f50(0), observed 15.
_hud_textbox_a/b (+0xA4/+0xA8): DDDisplay textbox handles created by
FUN_005514d0 when DDGame+0x7EF8 != 0 (display active).

Replaces the opaque _game_ctx: [u8; 0xAC] with the named TurnGameCtx struct.
Adds 9 offset checks to validation; 127/127 PASS.

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

crates/openwa-core/src/task.rs CHANGED
@@ -683,6 +683,77 @@ pub struct CTaskFire {
683
 
684
  const _: () = assert!(core::mem::size_of::<CTaskFire>() == 0xD8);
685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  /// Root turn-controller task β€” one instance per game, parent of the entire entity tree.
687
  ///
688
  /// Every worm, team, projectile, and environment task is a child (direct or indirect)
@@ -706,22 +777,9 @@ const _: () = assert!(core::mem::size_of::<CTaskFire>() == 0xD8);
706
  pub struct CTaskTurnGame {
707
  /// 0x00-0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
708
  pub base: CTask,
709
- /// 0x30-0xDB: Intermediate game-context base region (opaque).
710
- ///
711
- /// Initialised by `CTaskTeam__Constructor_Maybe` (0x550EB0) which creates a
712
- /// class_type-5 object with primary vtable 0x669E34 and secondary interface vtable
713
- /// 0x669C44 at +0x30. CTaskTurnGame then overrides the primary vtable and
714
- /// class_type.
715
- ///
716
- /// Known non-zero fields inside this region (all offsets relative to object base):
717
- /// +0x30: secondary interface vtable ptr (0x669C44 Ghidra)
718
- /// +0x40 / +0x44: landscape height as Fixed16.16 (e.g. Fixed(696.0) for 696px map)
719
- /// +0x48 / +0x58 / +0x68: Fixed(-1) β€” boundary sentinels
720
- /// +0x70 / +0x74: Fixed(1.0) β€” x/y scale factors
721
- /// +0x7C: team count (copy of DDGame+0x44C)
722
- /// +0xD0: small integer (β‰ˆ total worm count)
723
- /// +0xD4 / +0xD8: heap pointers (role unknown)
724
- pub _game_ctx: [u8; 0xAC],
725
 
726
  // ---- CTaskTurnGame-specific fields (0xDC onwards) ----
727
 
 
683
 
684
  const _: () = assert!(core::mem::size_of::<CTaskFire>() == 0xD8);
685
 
686
+ /// Embedded intermediate game-context sub-object within `CTaskTurnGame`.
687
+ ///
688
+ /// This is the memory region at `CTaskTurnGame+0x30..+0xDB` (0xAC bytes).
689
+ /// It is initialised by `CTaskTeam__Constructor_Maybe` (0x550EB0), which:
690
+ /// 1. Calls `CTask::Constructor(this, nullptr, ddgame)`
691
+ /// 2. Sets the primary vtable to 0x669E34 and class_type to 5
692
+ /// 3. Sets a **secondary interface vtable** pointer (Ghidra 0x669C44) at +0x30
693
+ /// inside the object (i.e. `TurnGameCtx` base +0x00)
694
+ /// 4. Copies `landscape_height = DDGame+0x5E0` as Fixed16.16 to both
695
+ /// `land_height` and `land_height_2`
696
+ /// 5. Writes -1 sentinels to `_sentinel_18`, `_sentinel_28`, `_sentinel_38`
697
+ /// 6. Writes `team_count = *(byte*)(GameInfo+0x44C)` to `team_count`
698
+ ///
699
+ /// `CTaskTurnGame__Constructor` (0x55B2A0) then overrides the primary vtable
700
+ /// to 0x669F70 and class_type to 6 but **leaves this sub-object intact**.
701
+ ///
702
+ /// The three -1 sentinels at offsets 0x18 / 0x28 / 0x38 are evenly spaced
703
+ /// 0x10 bytes apart. Their role is unknown β€” candidates are water-level
704
+ /// Y-coordinates (initial -1 = "no water"), zone boundaries, or AI markers.
705
+ ///
706
+ /// The final 0x44 bytes (0x68–0xAB, `_unknown_68`) are not initialised by
707
+ /// either constructor; they appear to be set by `FUN_005514d0` or similar
708
+ /// helpers called later. Observed runtime values: +0xA0 = 15, +0xA4/+0xA8
709
+ /// are heap pointers.
710
+ #[repr(C)]
711
+ pub struct TurnGameCtx {
712
+ /// +0x00 (= CTaskTurnGame+0x30): Secondary interface vtable pointer.
713
+ /// Ghidra: 0x00669C44. Set by both constructors; always 0x669C44 at runtime.
714
+ pub _secondary_vtable: u32,
715
+ /// +0x04: Unknown β€” not set by constructors (remains 0).
716
+ pub _unknown_04: u32,
717
+ /// +0x08–0x0F: Unknown β€” explicitly zeroed by constructor.
718
+ pub _unknown_08: [u32; 2],
719
+ /// +0x10: Landscape height as Fixed16.16. `DDGame+0x5E0 << 16`.
720
+ pub land_height: Fixed,
721
+ /// +0x14: Landscape height duplicate β€” same value as `land_height`.
722
+ pub land_height_2: Fixed,
723
+ /// +0x18: Sentinel, always -1 at construction. Role unknown.
724
+ pub _sentinel_18: i32,
725
+ /// +0x1C–0x27: Unknown β€” explicitly zeroed by constructor.
726
+ pub _unknown_1c: [u32; 3],
727
+ /// +0x28: Sentinel, always -1 at construction. Role unknown.
728
+ pub _sentinel_28: i32,
729
+ /// +0x2C–0x37: Unknown β€” explicitly zeroed by constructor.
730
+ pub _unknown_2c: [u32; 3],
731
+ /// +0x38: Sentinel, always -1 at construction. Role unknown.
732
+ pub _sentinel_38: i32,
733
+ /// +0x3C–0x4B: Unknown β€” explicitly zeroed by constructor.
734
+ pub _unknown_3c: [u32; 4],
735
+ /// +0x4C: Number of teams. `*(byte*)(GameInfo+0x44C)` at construction time.
736
+ pub team_count: u32,
737
+ /// +0x50–0x64: Unknown β€” explicitly zeroed by constructor (param_1[0x20..0x25]).
738
+ pub _unknown_50: [u32; 6],
739
+ /// +0x68–0x9F: Unknown β€” all explicitly zeroed by `FUN_005514d0` during game
740
+ /// setup. Purpose unknown; may be per-team or per-worm state slots.
741
+ pub _unknown_68: [u32; 14],
742
+ /// +0xA0: Return value of `FUN_00525f50(0)` stored here during game setup.
743
+ /// Observed value: 15. `FUN_00525f50` is a slot-allocation helper; this may
744
+ /// be a pool slot index or a pre-computed game-state token.
745
+ pub _slot_d0: u32,
746
+ /// +0xA4: `DDDisplay` textbox handle β€” created by `DDDisplay__ConstructTextbox`
747
+ /// with params `(buf, -1280, 2)` if `DDGame+0x7EF8 != 0` (display active).
748
+ /// Likely the HUD timer textbox. NULL when display is disabled.
749
+ pub _hud_textbox_a: u32,
750
+ /// +0xA8: `DDDisplay` textbox handle β€” created with params `(buf, 8, 4)`.
751
+ /// Likely a secondary HUD element. NULL when display is disabled.
752
+ pub _hud_textbox_b: u32,
753
+ }
754
+
755
+ const _: () = assert!(core::mem::size_of::<TurnGameCtx>() == 0xAC);
756
+
757
  /// Root turn-controller task β€” one instance per game, parent of the entire entity tree.
758
  ///
759
  /// Every worm, team, projectile, and environment task is a child (direct or indirect)
 
777
  pub struct CTaskTurnGame {
778
  /// 0x00-0x2F: CTask base (vtable, parent, children, shared_data, ddgame, …)
779
  pub base: CTask,
780
+ /// 0x30–0xDB: Embedded `TurnGameCtx` sub-object (0xAC bytes).
781
+ /// See [`TurnGameCtx`] for field details.
782
+ pub game_ctx: TurnGameCtx,
 
 
 
 
 
 
 
 
 
 
 
 
 
783
 
784
  // ---- CTaskTurnGame-specific fields (0xDC onwards) ----
785
 
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, SharedDataTable};
15
  use openwa_core::ddgame::DDGame;
16
  use openwa_core::ddgame_wrapper::DDGameWrapper;
17
 
@@ -307,7 +307,19 @@ fn validate_struct_offsets(result: &mut ValidationResult) {
307
  check_offset!(result, CTaskTeam, pos_y, 0x408);
308
 
309
  let _ = log_validation("");
 
 
 
 
 
 
 
 
 
 
 
310
  let _ = log_validation(" CTaskTurnGame:");
 
311
  check_offset!(result, CTaskTurnGame, worm_active, 0x108);
312
  check_offset!(result, CTaskTurnGame, current_team, 0x12C);
313
  check_offset!(result, CTaskTurnGame, current_worm, 0x130);
@@ -995,6 +1007,17 @@ fn dump_turngame() {
995
 
996
  // Print known named fields for cross-validation.
997
  let tg = &*(tg_ptr as *const CTaskTurnGame);
 
 
 
 
 
 
 
 
 
 
 
998
  let _ = log_validation(" Known fields:");
999
  let _ = log_validation(&format!(" +0x108 worm_active = {}", tg.worm_active));
1000
  let _ = log_validation(&format!(" +0x12C current_team = {} (1-based, 0=none)", tg.current_team));
@@ -1016,7 +1039,8 @@ fn dump_turngame() {
1016
  // Dump in chunks to keep log lines manageable.
1017
  let base = tg_ptr as *const u8;
1018
  dump_region(base, 0x00, 0x30, "CTaskTurnGame"); // CTask base
1019
- dump_region(base, 0x30, 0xAC, "CTaskTurnGame"); // CTaskTeam region
 
1020
  dump_region(base, 0xDC, 0x80, "CTaskTurnGame"); // 0xDC..0x15B
1021
  dump_region(base, 0x15C, 0x80, "CTaskTurnGame"); // 0x15C..0x1DB
1022
  dump_region(base, 0x1DC, 0x80, "CTaskTurnGame"); // 0x1DC..0x25B
 
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
 
 
307
  check_offset!(result, CTaskTeam, pos_y, 0x408);
308
 
309
  let _ = log_validation("");
310
+ let _ = log_validation(" TurnGameCtx (embedded at CTaskTurnGame+0x30):");
311
+ check_offset!(result, TurnGameCtx, land_height, 0x10);
312
+ check_offset!(result, TurnGameCtx, land_height_2, 0x14);
313
+ check_offset!(result, TurnGameCtx, _sentinel_18, 0x18);
314
+ check_offset!(result, TurnGameCtx, _sentinel_28, 0x28);
315
+ check_offset!(result, TurnGameCtx, _sentinel_38, 0x38);
316
+ check_offset!(result, TurnGameCtx, team_count, 0x4C);
317
+ check_offset!(result, TurnGameCtx, _slot_d0, 0xA0);
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);
324
  check_offset!(result, CTaskTurnGame, current_team, 0x12C);
325
  check_offset!(result, CTaskTurnGame, current_worm, 0x130);
 
1007
 
1008
  // Print known named fields for cross-validation.
1009
  let tg = &*(tg_ptr as *const CTaskTurnGame);
1010
+ let ctx = &tg.game_ctx;
1011
+ let _ = log_validation(" TurnGameCtx fields:");
1012
+ let _ = log_validation(&format!(" +0x40 land_height = {} ({:.4})", ctx.land_height.0, ctx.land_height.to_f32()));
1013
+ let _ = log_validation(&format!(" +0x44 land_height_2 = {} ({:.4})", ctx.land_height_2.0, ctx.land_height_2.to_f32()));
1014
+ let _ = log_validation(&format!(" +0x48 _sentinel_18 = {}", ctx._sentinel_18));
1015
+ let _ = log_validation(&format!(" +0x58 _sentinel_28 = {}", ctx._sentinel_28));
1016
+ let _ = log_validation(&format!(" +0x68 _sentinel_38 = {}", ctx._sentinel_38));
1017
+ let _ = log_validation(&format!(" +0x7C team_count = {}", ctx.team_count));
1018
+ let _ = log_validation(&format!(" +0xD0 _slot_d0 = {}", ctx._slot_d0));
1019
+ let _ = log_validation(&format!(" +0xD4 _hud_textbox_a = 0x{:08X}", ctx._hud_textbox_a));
1020
+ let _ = log_validation(&format!(" +0xD8 _hud_textbox_b = 0x{:08X}", ctx._hud_textbox_b));
1021
  let _ = log_validation(" Known fields:");
1022
  let _ = log_validation(&format!(" +0x108 worm_active = {}", tg.worm_active));
1023
  let _ = log_validation(&format!(" +0x12C current_team = {} (1-based, 0=none)", tg.current_team));
 
1039
  // Dump in chunks to keep log lines manageable.
1040
  let base = tg_ptr as *const u8;
1041
  dump_region(base, 0x00, 0x30, "CTaskTurnGame"); // CTask base
1042
+ dump_region(base, 0x30, 0x38, "TurnGameCtx"); // 0x30..0x67 (vtable + sentinels)
1043
+ dump_region(base, 0x68, 0x74, "TurnGameCtx"); // 0x68..0xDB (team_count + unknowns)
1044
  dump_region(base, 0xDC, 0x80, "CTaskTurnGame"); // 0xDC..0x15B
1045
  dump_region(base, 0x15C, 0x80, "CTaskTurnGame"); // 0x15C..0x1DB
1046
  dump_region(base, 0x1DC, 0x80, "CTaskTurnGame"); // 0x1DC..0x25B