paavohtl Claude Opus 4.6 (1M context) commited on
Commit
e80ee6b
Β·
1 Parent(s): 5c51f8a

refactor: rename TeamArenaState to TeamArena, use raw pointers for soundness

Browse files

- TeamArenaState β†’ TeamArena; accessor methods now take *mut/*const Self
instead of returning &mut from &self (unsound aliasing)
- Move unrelated DDGame sub-structs out of team_arena.rs:
CoordEntry/CoordList β†’ engine/coord.rs,
RenderEntry β†’ render/mod.rs,
SoundQueueEntry β†’ audio/sound_queue.rs

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

crates/openwa-core/src/audio/mod.rs CHANGED
@@ -4,6 +4,7 @@ pub mod known_sound_id;
4
  pub mod music;
5
  pub mod sound_id;
6
  pub mod sound_ops;
 
7
  pub mod speech;
8
  pub mod speech_ops;
9
  pub mod wav_player;
@@ -20,4 +21,5 @@ pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
20
  pub use known_sound_id::KnownSoundId;
21
  pub use music::{Music, MusicVtable, StreamingAudio};
22
  pub use sound_id::SoundId;
 
23
  pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
 
4
  pub mod music;
5
  pub mod sound_id;
6
  pub mod sound_ops;
7
+ pub mod sound_queue;
8
  pub mod speech;
9
  pub mod speech_ops;
10
  pub mod wav_player;
 
21
  pub use known_sound_id::KnownSoundId;
22
  pub use music::{Music, MusicVtable, StreamingAudio};
23
  pub use sound_id::SoundId;
24
+ pub use sound_queue::SoundQueueEntry;
25
  pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
crates/openwa-core/src/audio/sound_ops.rs CHANGED
@@ -13,8 +13,9 @@
13
  //! - DispatchGlobalSound (0x526270)
14
  //! - PlaySoundPooled_Direct (0x546B50)
15
 
 
16
  use crate::audio::{play_sound, play_sound_pooled, SoundId};
17
- use crate::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
18
  use crate::fixed::Fixed;
19
  use crate::task::worm::CTaskWorm;
20
  use crate::task::{CGameTask, CTask, SoundEmitter};
 
13
  //! - DispatchGlobalSound (0x526270)
14
  //! - PlaySoundPooled_Direct (0x546B50)
15
 
16
+ use crate::audio::SoundQueueEntry;
17
  use crate::audio::{play_sound, play_sound_pooled, SoundId};
18
+ use crate::engine::{DDGame, DDGameWrapper};
19
  use crate::fixed::Fixed;
20
  use crate::task::worm::CTaskWorm;
21
  use crate::task::{CGameTask, CTask, SoundEmitter};
crates/openwa-core/src/audio/sound_queue.rs ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // Sound queue entry β€” 16 entries at DDGame + 0x7F00
3
+ // ============================================================
4
+
5
+ /// Sound queue entry (0x24 = 36 bytes, stride between consecutive entries).
6
+ ///
7
+ /// DDGame maintains a 16-slot sound queue at offset 0x7F00. PlaySoundGlobal
8
+ /// appends entries; PlaySoundLocal additionally marks entries as local and
9
+ /// stores position via the task's secondary vtable.
10
+ #[repr(C)]
11
+ pub struct SoundQueueEntry {
12
+ /// Sound effect ID (SoundId enum value).
13
+ pub sound_id: u32,
14
+ /// Flags / priority (e.g. 3=weapon, 7=explosion).
15
+ pub flags: u32,
16
+ /// Volume (Fixed-point, 0x10000 = 1.0).
17
+ pub volume: u32,
18
+ /// Pitch (Fixed-point, 0x10000 = 1.0).
19
+ pub pitch: u32,
20
+ /// Reserved, always 0.
21
+ pub reserved: u32,
22
+ /// 0 = global, 1 = local (has position tracking).
23
+ pub is_local: u8,
24
+ pub _pad: [u8; 3],
25
+ /// Position X (filled by secondary vtable GetPosition for local sounds).
26
+ pub pos_x: u32,
27
+ /// Position Y (filled by secondary vtable GetPosition for local sounds).
28
+ pub pos_y: u32,
29
+ /// Pointer to task's secondary vtable (at task+0xE8) for position updates.
30
+ pub secondary_vtable: u32,
31
+ }
32
+
33
+ const _: () = assert!(core::mem::size_of::<SoundQueueEntry>() == 0x24);
crates/openwa-core/src/engine/coord.rs ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // Coordinate types β€” DDGame sub-structs for screen and terrain coords
3
+ // ============================================================
4
+
5
+ /// Coordinate entry used in DDGame screen coordinate tables (stride 0x10).
6
+ ///
7
+ /// InitFields zeroes x and y; at runtime they contain fixed-point screen
8
+ /// coordinates used for camera tracking and rendering regions.
9
+ #[repr(C)]
10
+ #[derive(Clone, Copy)]
11
+ pub struct CoordEntry {
12
+ pub x: i32,
13
+ pub y: i32,
14
+ pub _unknown: [u8; 8],
15
+ }
16
+
17
+ const _: () = assert!(core::mem::size_of::<CoordEntry>() == 0x10);
18
+
19
+ // ============================================================
20
+ // CoordList β€” dynamic array of packed terrain coordinates
21
+ // ============================================================
22
+
23
+ /// Packed terrain coordinate entry (8 bytes).
24
+ ///
25
+ /// `coord` packs x and y as `x * 0x10000 + y` (fixed-point).
26
+ /// `flag` is always 1 for populated entries.
27
+ #[repr(C)]
28
+ #[derive(Clone, Copy)]
29
+ pub struct CoordListEntry {
30
+ pub coord: u32,
31
+ pub flag: u32,
32
+ }
33
+ const _: () = assert!(core::mem::size_of::<CoordListEntry>() == 8);
34
+
35
+ /// Dynamic coordinate array header (12 bytes).
36
+ ///
37
+ /// Allocated at DDGame+0x50C during `init_graphics_and_resources`.
38
+ /// Data buffer is a separate allocation of `capacity * 8` bytes.
39
+ /// Used for terrain coordinate lookups (spawning, aiming, collision).
40
+ #[repr(C)]
41
+ pub struct CoordList {
42
+ /// Number of entries currently stored.
43
+ pub count: u32,
44
+ /// Maximum number of entries (600).
45
+ pub capacity: u32,
46
+ /// Pointer to the data buffer (`capacity * sizeof(CoordListEntry)` bytes).
47
+ pub data: *mut CoordListEntry,
48
+ }
49
+ const _: () = assert!(core::mem::size_of::<CoordList>() == 12);
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -2,19 +2,19 @@ use crate::audio::active_sound::ActiveSoundTable;
2
  use crate::audio::dssound::DSSound;
3
  use crate::audio::music::Music;
4
  use crate::audio::speech::SpeechSlotTable;
 
5
  use crate::bitgrid::{CollisionBitGrid, DisplayBitGrid};
6
  use crate::display::dd_display::DDDisplay;
7
  use crate::display::palette::Palette;
8
  use crate::engine::game_info::GameInfo;
9
- use crate::engine::{
10
- CoordEntry, CoordList, RenderEntry, SoundQueueEntry, TeamArenaState, TeamIndexMap,
11
- };
12
  use crate::fixed::Fixed;
13
  use crate::game::weapon::WeaponTable;
14
  use crate::input::keyboard::DDKeyboard;
15
  use crate::render::landscape::PCLandscape;
16
  use crate::render::queue::RenderQueue;
17
  use crate::render::turn_order::TurnOrderWidget;
 
18
  use crate::FieldRegistry;
19
 
20
  /// DDGame β€” the main game engine object.
@@ -183,7 +183,7 @@ pub struct DDGame {
183
  /// 0x4628: Team arena state β€” per-team data, ammo, delays, alliance tracking.
184
  /// Note: fields previously named init_field_64d8 (= team_count at arena+0x1EB0)
185
  /// and init_field_72a4 (= weapon_slots entry at arena+0x2A7C) are inside this struct.
186
- pub team_arena: TeamArenaState,
187
  /// 0x7270-0x72A3: Unknown
188
  pub _unknown_7270: [u8; 0x72A4 - 0x7270],
189
  /// 0x72A4: Object pool counter. Incremented by +7 per CTaskMissile, +2 per CTaskArrow.
@@ -610,7 +610,7 @@ pub mod offsets {
610
  pub const WEAPON_PANEL: usize = 0x548;
611
 
612
  // === Team weapon state (DDGame + 0x4628) ===
613
- /// Base of TeamArenaState sub-struct within DDGame.
614
  /// Callers pass DDGame + TEAM_ARENA_STATE as base pointer to
615
  /// GetAmmo/AddAmmo/SubtractAmmo.
616
  pub const TEAM_ARENA_STATE: usize = 0x4628;
@@ -621,7 +621,7 @@ pub mod offsets {
621
  /// Runtime-confirmed: block[0] is zeroed preamble, blocks[1-6] hold team data.
622
  pub const TEAM_BLOCKS: usize = 0x4090;
623
 
624
- /// Byte offset from TeamArenaState base back to TeamBlock array start.
625
  /// `blocks_ptr = (tws_base as *const c_char).sub(ARENA_TO_BLOCKS) as *const TeamBlock`
626
  ///
627
  /// entry_ptr(0) = DDGame+0x4628 = TEAM_BLOCKS + 0x598.
@@ -772,7 +772,7 @@ impl crate::snapshot::Snapshot for DDGame {
772
  write_indent(w, i)?;
773
  writeln!(w, "task_land = {}", fmt_ptr(self.task_land))?;
774
 
775
- // TeamArenaState summary
776
  write_indent(w, i)?;
777
  writeln!(w, "team_arena.team_count = {}", self.team_arena.team_count)?;
778
  write_indent(w, i)?;
 
2
  use crate::audio::dssound::DSSound;
3
  use crate::audio::music::Music;
4
  use crate::audio::speech::SpeechSlotTable;
5
+ use crate::audio::SoundQueueEntry;
6
  use crate::bitgrid::{CollisionBitGrid, DisplayBitGrid};
7
  use crate::display::dd_display::DDDisplay;
8
  use crate::display::palette::Palette;
9
  use crate::engine::game_info::GameInfo;
10
+ use crate::engine::{CoordEntry, CoordList, TeamArena, TeamIndexMap};
 
 
11
  use crate::fixed::Fixed;
12
  use crate::game::weapon::WeaponTable;
13
  use crate::input::keyboard::DDKeyboard;
14
  use crate::render::landscape::PCLandscape;
15
  use crate::render::queue::RenderQueue;
16
  use crate::render::turn_order::TurnOrderWidget;
17
+ use crate::render::RenderEntry;
18
  use crate::FieldRegistry;
19
 
20
  /// DDGame β€” the main game engine object.
 
183
  /// 0x4628: Team arena state β€” per-team data, ammo, delays, alliance tracking.
184
  /// Note: fields previously named init_field_64d8 (= team_count at arena+0x1EB0)
185
  /// and init_field_72a4 (= weapon_slots entry at arena+0x2A7C) are inside this struct.
186
+ pub team_arena: TeamArena,
187
  /// 0x7270-0x72A3: Unknown
188
  pub _unknown_7270: [u8; 0x72A4 - 0x7270],
189
  /// 0x72A4: Object pool counter. Incremented by +7 per CTaskMissile, +2 per CTaskArrow.
 
610
  pub const WEAPON_PANEL: usize = 0x548;
611
 
612
  // === Team weapon state (DDGame + 0x4628) ===
613
+ /// Base of TeamArena sub-struct within DDGame.
614
  /// Callers pass DDGame + TEAM_ARENA_STATE as base pointer to
615
  /// GetAmmo/AddAmmo/SubtractAmmo.
616
  pub const TEAM_ARENA_STATE: usize = 0x4628;
 
621
  /// Runtime-confirmed: block[0] is zeroed preamble, blocks[1-6] hold team data.
622
  pub const TEAM_BLOCKS: usize = 0x4090;
623
 
624
+ /// Byte offset from TeamArena base back to TeamBlock array start.
625
  /// `blocks_ptr = (tws_base as *const c_char).sub(ARENA_TO_BLOCKS) as *const TeamBlock`
626
  ///
627
  /// entry_ptr(0) = DDGame+0x4628 = TEAM_BLOCKS + 0x598.
 
772
  write_indent(w, i)?;
773
  writeln!(w, "task_land = {}", fmt_ptr(self.task_land))?;
774
 
775
+ // TeamArena summary
776
  write_indent(w, i)?;
777
  writeln!(w, "team_arena.team_count = {}", self.team_arena.team_count)?;
778
  write_indent(w, i)?;
crates/openwa-core/src/engine/ddgame_constructor.rs CHANGED
@@ -16,10 +16,10 @@ use crate::bitgrid::{BitGrid, BitGridBaseVtable, CollisionBitGrid, DisplayBitGri
16
  use crate::display::dd_display::DDDisplay;
17
  use crate::display::gradient::compute_complex_gradient;
18
  use crate::display::palette::Palette;
 
19
  use crate::engine::ddgame_wrapper::DDGameWrapper;
20
  use crate::engine::game_info::GameInfo;
21
  use crate::engine::net_bridge::NetBridge;
22
- use crate::engine::team_arena::{CoordList, CoordListEntry};
23
  use crate::input::keyboard::DDKeyboard;
24
  use crate::rebase::rb;
25
  use crate::render::gfx_dir::{
@@ -63,7 +63,7 @@ pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
63
  *(base.add(off) as *mut u32) = 0;
64
  }
65
 
66
- // init_field_64d8 = TeamArenaState.team_count (arena+0x1EB0)
67
  (*ddgame).team_arena.team_count = 0;
68
  // init_field_72a4 = weapon_slots flat[754] = alliance 5, ammo[44]
69
  (*ddgame).team_arena.weapon_slots.teams[5].ammo[44] = 0;
 
16
  use crate::display::dd_display::DDDisplay;
17
  use crate::display::gradient::compute_complex_gradient;
18
  use crate::display::palette::Palette;
19
+ use crate::engine::coord::{CoordList, CoordListEntry};
20
  use crate::engine::ddgame_wrapper::DDGameWrapper;
21
  use crate::engine::game_info::GameInfo;
22
  use crate::engine::net_bridge::NetBridge;
 
23
  use crate::input::keyboard::DDKeyboard;
24
  use crate::rebase::rb;
25
  use crate::render::gfx_dir::{
 
63
  *(base.add(off) as *mut u32) = 0;
64
  }
65
 
66
+ // init_field_64d8 = TeamArena.team_count (arena+0x1EB0)
67
  (*ddgame).team_arena.team_count = 0;
68
  // init_field_72a4 = weapon_slots flat[754] = alliance 5, ammo[44]
69
  (*ddgame).team_arena.weapon_slots.teams[5].ammo[44] = 0;
crates/openwa-core/src/engine/mod.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  pub mod ddgame;
2
  pub mod ddgame_constructor;
3
  pub mod ddgame_load_fonts;
@@ -12,6 +13,7 @@ pub mod replay;
12
  pub mod team_arena;
13
  pub mod team_ops;
14
 
 
15
  pub use ddgame::DDGame;
16
  pub use ddgame_constructor::{
17
  create_ddgame, ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init,
@@ -23,7 +25,6 @@ pub use game_session::GameSession;
23
  pub use game_timer::GameTimer;
24
  pub use net_wrapper::DDNetGameWrapper;
25
  pub use team_arena::{
26
- CoordEntry, CoordList, CoordListEntry, RenderEntry, SoundQueueEntry, TeamArenaRef,
27
- TeamArenaState, TeamBlock, TeamHeader, TeamIndexMap, TeamSlot0, TeamWeaponSlots, WeaponSlots,
28
  WormEntry, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH,
29
  };
 
1
+ pub mod coord;
2
  pub mod ddgame;
3
  pub mod ddgame_constructor;
4
  pub mod ddgame_load_fonts;
 
13
  pub mod team_arena;
14
  pub mod team_ops;
15
 
16
+ pub use coord::{CoordEntry, CoordList, CoordListEntry};
17
  pub use ddgame::DDGame;
18
  pub use ddgame_constructor::{
19
  create_ddgame, ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init,
 
25
  pub use game_timer::GameTimer;
26
  pub use net_wrapper::DDNetGameWrapper;
27
  pub use team_arena::{
28
+ TeamArena, TeamBlock, TeamHeader, TeamIndexMap, TeamSlot0, TeamWeaponSlots, WeaponSlots,
 
29
  WormEntry, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH,
30
  };
crates/openwa-core/src/engine/team_arena.rs CHANGED
@@ -2,26 +2,14 @@
2
  // Team arena state β€” sub-struct at DDGame + 0x4628
3
  // ============================================================
4
  //
5
- // Extracted from ddgame.rs: team/worm data structures, TeamArenaRef,
6
  // and related helper structs used as DDGame fields.
7
 
8
  use core::ffi::CStr;
9
 
10
- use super::ddgame::offsets;
11
 
12
- /// Coordinate entry used in DDGame screen coordinate tables (stride 0x10).
13
- ///
14
- /// InitFields zeroes x and y; at runtime they contain fixed-point screen
15
- /// coordinates used for camera tracking and rendering regions.
16
- #[repr(C)]
17
- #[derive(Clone, Copy)]
18
- pub struct CoordEntry {
19
- pub x: i32,
20
- pub y: i32,
21
- pub _unknown: [u8; 8],
22
- }
23
-
24
- const _: () = assert!(core::mem::size_of::<CoordEntry>() == 0x10);
25
 
26
  /// Team index permutation map (0x64 = 100 bytes).
27
  ///
@@ -42,86 +30,6 @@ pub struct TeamIndexMap {
42
  }
43
  const _: () = assert!(core::mem::size_of::<TeamIndexMap>() == 0x64);
44
 
45
- /// Render table entry (0x14 = 20 bytes).
46
- ///
47
- /// 14 entries live at DDGame+0x73B0 (stride 0x14). Only the first u32
48
- /// is zeroed during construction; the rest is uninitialized/unknown.
49
- #[repr(C)]
50
- #[derive(Clone, Copy)]
51
- pub struct RenderEntry {
52
- /// Active/state flag (zeroed on init).
53
- pub active: u32,
54
- /// Unknown data.
55
- pub _unknown: [u8; 16],
56
- }
57
- const _: () = assert!(core::mem::size_of::<RenderEntry>() == 0x14);
58
-
59
- // ============================================================
60
- // CoordList β€” dynamic array of packed terrain coordinates
61
- // ============================================================
62
-
63
- /// Packed terrain coordinate entry (8 bytes).
64
- ///
65
- /// `coord` packs x and y as `x * 0x10000 + y` (fixed-point).
66
- /// `flag` is always 1 for populated entries.
67
- #[repr(C)]
68
- #[derive(Clone, Copy)]
69
- pub struct CoordListEntry {
70
- pub coord: u32,
71
- pub flag: u32,
72
- }
73
- const _: () = assert!(core::mem::size_of::<CoordListEntry>() == 8);
74
-
75
- /// Dynamic coordinate array header (12 bytes).
76
- ///
77
- /// Allocated at DDGame+0x50C during `init_graphics_and_resources`.
78
- /// Data buffer is a separate allocation of `capacity * 8` bytes.
79
- /// Used for terrain coordinate lookups (spawning, aiming, collision).
80
- #[repr(C)]
81
- pub struct CoordList {
82
- /// Number of entries currently stored.
83
- pub count: u32,
84
- /// Maximum number of entries (600).
85
- pub capacity: u32,
86
- /// Pointer to the data buffer (`capacity * sizeof(CoordListEntry)` bytes).
87
- pub data: *mut CoordListEntry,
88
- }
89
- const _: () = assert!(core::mem::size_of::<CoordList>() == 12);
90
-
91
- // ============================================================
92
- // Sound queue entry β€” 16 entries at DDGame + 0x7F00
93
- // ============================================================
94
-
95
- /// Sound queue entry (0x24 = 36 bytes, stride between consecutive entries).
96
- ///
97
- /// DDGame maintains a 16-slot sound queue at offset 0x7F00. PlaySoundGlobal
98
- /// appends entries; PlaySoundLocal additionally marks entries as local and
99
- /// stores position via the task's secondary vtable.
100
- #[repr(C)]
101
- pub struct SoundQueueEntry {
102
- /// Sound effect ID (SoundId enum value).
103
- pub sound_id: u32,
104
- /// Flags / priority (e.g. 3=weapon, 7=explosion).
105
- pub flags: u32,
106
- /// Volume (Fixed-point, 0x10000 = 1.0).
107
- pub volume: u32,
108
- /// Pitch (Fixed-point, 0x10000 = 1.0).
109
- pub pitch: u32,
110
- /// Reserved, always 0.
111
- pub reserved: u32,
112
- /// 0 = global, 1 = local (has position tracking).
113
- pub is_local: u8,
114
- pub _pad: [u8; 3],
115
- /// Position X (filled by secondary vtable GetPosition for local sounds).
116
- pub pos_x: u32,
117
- /// Position Y (filled by secondary vtable GetPosition for local sounds).
118
- pub pos_y: u32,
119
- /// Pointer to task's secondary vtable (at task+0xE8) for position updates.
120
- pub secondary_vtable: u32,
121
- }
122
-
123
- const _: () = assert!(core::mem::size_of::<SoundQueueEntry>() == 0x24);
124
-
125
  // ============================================================
126
  // Per-worm and per-team block structs
127
  // ============================================================
@@ -131,7 +39,7 @@ const _: () = assert!(core::mem::size_of::<SoundQueueEntry>() == 0x24);
131
  /// WA supports up to 8 playable worms per team. The original code accesses
132
  /// worms via raw pointer arithmetic from the team entry pointer, using
133
  /// stride 0x9C. This means the 8th worm crosses the TeamBlock boundary
134
- /// into the next block's header slot β€” see `TeamArenaRef::team_worm()`.
135
  ///
136
  /// Field offsets confirmed by runtime memory dump (validator DLL):
137
  /// - state at 0x00: 0x67 = active/selected, 0x65 = idle, 0x80+ = special
@@ -179,7 +87,7 @@ const _: () = assert!(core::mem::size_of::<WormEntry>() == 0x9C);
179
  /// offsets (0x00-0x5F) may contain data from the previous team's 8th worm
180
  /// when that team has 8 worms β€” they are treated as opaque padding.
181
  ///
182
- /// Accessed via `TeamArenaRef::team_header()` and `team_header_b()`.
183
  #[repr(C)]
184
  #[derive(Clone, Copy)]
185
  pub struct TeamHeader {
@@ -235,10 +143,10 @@ pub union TeamSlot0 {
235
  /// The remaining 7 worm slots follow, then 0x3C bytes of metadata.
236
  ///
237
  /// **Block indexing**: Block 0 is unused (preamble, all zeros). Actual team
238
- /// data starts at block 1. Worms are accessed via `TeamArenaRef::team_worm()`
239
  /// which uses raw pointer arithmetic and naturally crosses block boundaries.
240
  ///
241
- /// **Header access**: Use `TeamArenaRef::team_header(idx)` to get the
242
  /// `TeamHeader` for a team (reads from `blocks[idx+1].header.team`).
243
  #[repr(C)]
244
  pub struct TeamBlock {
@@ -286,13 +194,13 @@ const _: () = assert!(core::mem::size_of::<WeaponSlots>() == 852 * 4);
286
  /// `weapon_slots.teams[alliance].ammo[weapon]` and
287
  /// `weapon_slots.teams[alliance].delay[weapon]`.
288
  #[repr(C)]
289
- pub struct TeamArenaState {
290
  /// 0x0000-0x1EAF: Per-team data region (opaque).
291
  ///
292
  /// This region contains 7 team entries at stride 0x51C (1-indexed, index 0
293
  /// is preamble). The original WA.exe accesses team data via raw pointer
294
  /// arithmetic relative to the arena base. In our Rust code, team data is
295
- /// accessed through `TeamArenaRef`, `TeamBlock`, and `TeamHeader`,
296
  /// so this region is treated as opaque padding.
297
  ///
298
  /// The 7th entry (team 6, 1-indexed) only has 8 bytes before team_count;
@@ -333,7 +241,7 @@ pub struct TeamArenaState {
333
  pub last_active_alliance: i32,
334
  }
335
 
336
- const _: () = assert!(core::mem::size_of::<TeamArenaState>() == 0x2C48);
337
 
338
  /// Worm state constants and helpers.
339
  /// TODO: Replace this module with the WormState enum
@@ -361,11 +269,11 @@ pub mod worm {
361
  }
362
  }
363
 
364
- /// Game phase thresholds (stored in TeamArenaState::game_phase).
365
  pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
366
  pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
367
 
368
- impl TeamArenaState {
369
  /// Get ammo count for a weapon by alliance and weapon ID.
370
  pub fn get_ammo(&self, alliance: usize, weapon_id: usize) -> i32 {
371
  self.weapon_slots.teams[alliance].ammo[weapon_id]
@@ -380,82 +288,44 @@ impl TeamArenaState {
380
  pub fn get_delay(&self, alliance: usize, weapon_id: usize) -> i32 {
381
  self.weapon_slots.teams[alliance].delay[weapon_id]
382
  }
383
- }
384
 
385
- /// Typed handle to a TeamArenaState pointer received from WA.exe.
386
- ///
387
- /// Wraps the raw `base: u32` from trampoline register captures and provides
388
- /// accessor methods that encapsulate the backward pointer arithmetic to reach
389
- /// TeamBlock worm data. The TeamBlock array lives 0x598 bytes before
390
- /// the TeamArenaState in DDGame memory.
391
- ///
392
- /// # Safety
393
- /// Must only be constructed from a valid DDGame + TEAM_ARENA_STATE pointer.
394
- /// `repr(transparent)` ensures identical ABI to `*const u8` / `u32` on i686,
395
- /// so it can be received directly from usercall trampoline register captures.
396
- #[repr(transparent)]
397
- #[derive(Clone, Copy)]
398
- pub struct TeamArenaRef {
399
- base: *mut u8,
400
- }
401
 
402
- impl TeamArenaRef {
403
- /// Wrap a raw integer pointer (for usercall trampoline register captures).
404
  ///
405
- /// # Safety
406
- /// `base` must point to a valid TeamArenaState (DDGame + 0x4628).
407
  #[inline]
408
- pub unsafe fn from_raw(base: u32) -> Self {
409
- Self {
410
- base: base as *mut u8,
411
- }
412
  }
413
 
414
- /// Wrap a typed pointer to TeamArenaState.
415
  ///
416
- /// # Safety
417
- /// `arena` must point to a valid TeamArenaState within a live DDGame.
418
- #[inline]
419
- pub unsafe fn from_ptr(arena: *mut TeamArenaState) -> Self {
420
- Self {
421
- base: arena as *mut u8,
422
- }
423
- }
424
-
425
- /// Access the TeamArenaState fields (read-only).
426
  #[inline]
427
- pub unsafe fn state(&self) -> &TeamArenaState {
428
- &*(self.base as *const TeamArenaState)
429
  }
430
 
431
- /// Access the TeamArenaState fields (mutable).
432
- #[inline]
433
- pub unsafe fn state_mut(&self) -> &mut TeamArenaState {
434
- &mut *(self.base as *mut TeamArenaState)
435
- }
436
-
437
- /// Get pointer to the TeamBlock array base.
438
- #[inline]
439
- pub unsafe fn blocks(&self) -> *mut TeamBlock {
440
- self.base.sub(offsets::ARENA_TO_BLOCKS) as *mut TeamBlock
441
- }
442
-
443
- /// Get the team header (metadata) for a team.
444
  ///
445
- /// Returns `&block[team_idx+1].header.team`, which holds team metadata:
446
  /// worm_count, eliminated flag, weapon_alliance, team_name.
447
  #[inline]
448
- pub unsafe fn team_header(&self, team_idx: usize) -> &TeamHeader {
449
- &(*self.blocks().add(team_idx + 1)).header.team
450
  }
451
 
452
- /// Get a mutable team header for a team.
 
 
 
453
  #[inline]
454
- pub unsafe fn team_header_mut(&self, team_idx: usize) -> &mut TeamHeader {
455
- &mut (*self.blocks().add(team_idx + 1)).header.team
456
  }
457
 
458
- /// Get a playable worm entry by 1-indexed worm number (1..=8).
459
  ///
460
  /// Uses raw pointer arithmetic matching the original WA code:
461
  /// `base + team_idx * 0x51C + worm_num * 0x9C - 0x598`.
@@ -464,56 +334,60 @@ impl TeamArenaRef {
464
  /// next block's header slot. The header metadata lives at high offsets
465
  /// (0x6C+) that don't conflict with worm data (0x00-0x5F).
466
  #[inline]
467
- pub unsafe fn team_worm(&self, team_idx: usize, worm_num: usize) -> &WormEntry {
468
- let ptr = self
469
- .base
470
- .add(team_idx * 0x51C)
471
- .add(worm_num * 0x9C)
472
- .sub(0x598);
473
- &*(ptr as *const WormEntry)
474
  }
475
 
476
- /// Get a mutable reference to a specific worm entry.
477
- pub unsafe fn team_worm_mut(&self, team_idx: usize, worm_num: usize) -> &mut WormEntry {
478
- let ptr = self
479
- .base
480
- .add(team_idx * 0x51C)
481
- .add(worm_num * 0x9C)
482
- .sub(0x598);
483
- &mut *(ptr as *mut WormEntry)
484
  }
485
 
486
- /// Get a team's block and its header in one call.
487
  ///
488
- /// Returns `(block[team_idx], &block[team_idx+1].header.team)`.
489
  /// The block contains worm data, and the header (slot 0 of the next block)
490
  /// holds team metadata (worm_count, eliminated flag).
491
  ///
492
  /// **Note**: For accessing worms, prefer `team_worm()` which handles
493
  /// 8-worm teams correctly via cross-boundary pointer arithmetic.
494
  #[inline]
495
- pub unsafe fn team_and_header(&self, team_idx: usize) -> (&TeamBlock, &TeamHeader) {
496
- let blocks = self.blocks();
497
- let block = &*blocks.add(team_idx);
498
- let header = &(*blocks.add(team_idx + 1)).header.team;
 
 
 
499
  (block, header)
500
  }
501
 
502
- /// Get team header for Pattern B access (alliance/active_worm at +0x70/+0x74).
503
  ///
504
  /// Pattern B indexes from block[i+2] for 0-indexed team `i`:
505
  /// `base + 0x510 + i*0x51C` = `blocks[i+2].header.team + 0x70`.
506
  #[inline]
507
- pub unsafe fn team_header_b(&self, team_idx: usize) -> &TeamHeader {
508
- &(*self.blocks().add(team_idx + 2)).header.team
509
  }
510
 
511
- /// Get mutable team header for Pattern B access.
 
 
 
512
  #[inline]
513
- pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamHeader {
514
- &mut (*(self.blocks() as *mut TeamBlock).add(team_idx + 2))
515
- .header
516
- .team
517
  }
518
 
519
  /// Compute the flat index for ammo/delay table access.
@@ -521,21 +395,21 @@ impl TeamArenaRef {
521
  /// Reads the weapon alliance ID from the team header for the given
522
  /// 1-indexed team, then computes `alliance_id * 142 + weapon_id`.
523
  ///
524
- /// The weapon_slots array is interleaved: per alliance, 71 ammo slots
525
- /// then 71 delay slots (stride 142 per alliance).
526
- /// Ammo: `weapon_slots[alliance_id * 142 + weapon_id]`
527
- /// Delay: `weapon_slots[alliance_id * 142 + 71 + weapon_id]`
528
- /// Returns (alliance_id, weapon_id) for accessing weapon slots.
529
  #[inline]
530
- pub unsafe fn weapon_slot_key(&self, team_index: usize, weapon_id: u32) -> (usize, usize) {
531
- let alliance_id = self.team_header(team_index).weapon_alliance as usize;
 
 
 
 
532
  (alliance_id, weapon_id as usize)
533
  }
534
  }
535
 
536
  // ── Snapshot impl ──────────────────────────────────────────
537
 
538
- impl crate::snapshot::Snapshot for WormEntry {
539
  unsafe fn write_snapshot(
540
  &self,
541
  w: &mut dyn core::fmt::Write,
 
2
  // Team arena state β€” sub-struct at DDGame + 0x4628
3
  // ============================================================
4
  //
5
+ // Extracted from ddgame.rs: team/worm data structures, TeamArena,
6
  // and related helper structs used as DDGame fields.
7
 
8
  use core::ffi::CStr;
9
 
10
+ use crate::snapshot::Snapshot;
11
 
12
+ use super::ddgame::offsets;
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  /// Team index permutation map (0x64 = 100 bytes).
15
  ///
 
30
  }
31
  const _: () = assert!(core::mem::size_of::<TeamIndexMap>() == 0x64);
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  // ============================================================
34
  // Per-worm and per-team block structs
35
  // ============================================================
 
39
  /// WA supports up to 8 playable worms per team. The original code accesses
40
  /// worms via raw pointer arithmetic from the team entry pointer, using
41
  /// stride 0x9C. This means the 8th worm crosses the TeamBlock boundary
42
+ /// into the next block's header slot β€” see `TeamArena::team_worm()`.
43
  ///
44
  /// Field offsets confirmed by runtime memory dump (validator DLL):
45
  /// - state at 0x00: 0x67 = active/selected, 0x65 = idle, 0x80+ = special
 
87
  /// offsets (0x00-0x5F) may contain data from the previous team's 8th worm
88
  /// when that team has 8 worms β€” they are treated as opaque padding.
89
  ///
90
+ /// Accessed via `TeamArena::team_header()` and `team_header_b()`.
91
  #[repr(C)]
92
  #[derive(Clone, Copy)]
93
  pub struct TeamHeader {
 
143
  /// The remaining 7 worm slots follow, then 0x3C bytes of metadata.
144
  ///
145
  /// **Block indexing**: Block 0 is unused (preamble, all zeros). Actual team
146
+ /// data starts at block 1. Worms are accessed via `TeamArena::team_worm()`
147
  /// which uses raw pointer arithmetic and naturally crosses block boundaries.
148
  ///
149
+ /// **Header access**: Use `TeamArena::team_header(idx)` to get the
150
  /// `TeamHeader` for a team (reads from `blocks[idx+1].header.team`).
151
  #[repr(C)]
152
  pub struct TeamBlock {
 
194
  /// `weapon_slots.teams[alliance].ammo[weapon]` and
195
  /// `weapon_slots.teams[alliance].delay[weapon]`.
196
  #[repr(C)]
197
+ pub struct TeamArena {
198
  /// 0x0000-0x1EAF: Per-team data region (opaque).
199
  ///
200
  /// This region contains 7 team entries at stride 0x51C (1-indexed, index 0
201
  /// is preamble). The original WA.exe accesses team data via raw pointer
202
  /// arithmetic relative to the arena base. In our Rust code, team data is
203
+ /// accessed through `TeamArena`, `TeamBlock`, and `TeamHeader`,
204
  /// so this region is treated as opaque padding.
205
  ///
206
  /// The 7th entry (team 6, 1-indexed) only has 8 bytes before team_count;
 
241
  pub last_active_alliance: i32,
242
  }
243
 
244
+ const _: () = assert!(core::mem::size_of::<TeamArena>() == 0x2C48);
245
 
246
  /// Worm state constants and helpers.
247
  /// TODO: Replace this module with the WormState enum
 
269
  }
270
  }
271
 
272
+ /// Game phase thresholds (stored in TeamArena::game_phase).
273
  pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
274
  pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
275
 
276
+ impl TeamArena {
277
  /// Get ammo count for a weapon by alliance and weapon ID.
278
  pub fn get_ammo(&self, alliance: usize, weapon_id: usize) -> i32 {
279
  self.weapon_slots.teams[alliance].ammo[weapon_id]
 
288
  pub fn get_delay(&self, alliance: usize, weapon_id: usize) -> i32 {
289
  self.weapon_slots.teams[alliance].delay[weapon_id]
290
  }
 
291
 
292
+ // ── Raw-pointer accessors (safe from noalias miscompilation) ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
+ /// Get pointer to the TeamBlock array base.
 
295
  ///
296
+ /// The TeamBlock array lives 0x598 bytes before TeamArena in DDGame memory.
 
297
  #[inline]
298
+ pub unsafe fn blocks_mut(this: *mut Self) -> *mut TeamBlock {
299
+ (this as *mut u8).sub(offsets::ARENA_TO_BLOCKS) as *mut TeamBlock
 
 
300
  }
301
 
302
+ /// Get pointer to the TeamBlock array base (read-only).
303
  ///
304
+ /// The TeamBlock array lives 0x598 bytes before TeamArena in DDGame memory.
 
 
 
 
 
 
 
 
 
305
  #[inline]
306
+ pub unsafe fn blocks(this: *const Self) -> *const TeamBlock {
307
+ (this as *const u8).sub(offsets::ARENA_TO_BLOCKS) as *const TeamBlock
308
  }
309
 
310
+ /// Get pointer to team header (metadata) for a team.
 
 
 
 
 
 
 
 
 
 
 
 
311
  ///
312
+ /// Returns `blocks[team_idx+1].header.team`, which holds team metadata:
313
  /// worm_count, eliminated flag, weapon_alliance, team_name.
314
  #[inline]
315
+ pub unsafe fn team_header_mut(this: *mut Self, team_idx: usize) -> *mut TeamHeader {
316
+ &raw mut (*Self::blocks_mut(this).add(team_idx + 1)).header.team as *mut TeamHeader
317
  }
318
 
319
+ /// Get pointer to team header (metadata) for a team.
320
+ ///
321
+ /// Returns `blocks[team_idx+1].header.team`, which holds team metadata:
322
+ /// worm_count, eliminated flag, weapon_alliance, team_name.
323
  #[inline]
324
+ pub unsafe fn team_header(this: *const Self, team_idx: usize) -> *const TeamHeader {
325
+ &raw const (*Self::blocks(this).add(team_idx + 1)).header.team as *const TeamHeader
326
  }
327
 
328
+ /// Get pointer to a playable worm entry by 1-indexed worm number (1..=8).
329
  ///
330
  /// Uses raw pointer arithmetic matching the original WA code:
331
  /// `base + team_idx * 0x51C + worm_num * 0x9C - 0x598`.
 
334
  /// next block's header slot. The header metadata lives at high offsets
335
  /// (0x6C+) that don't conflict with worm data (0x00-0x5F).
336
  #[inline]
337
+ pub unsafe fn team_worm_mut(
338
+ this: *mut Self,
339
+ team_idx: usize,
340
+ worm_num: usize,
341
+ ) -> *mut WormEntry {
342
+ let base = this as *mut u8;
343
+ base.add(team_idx * 0x51C).add(worm_num * 0x9C).sub(0x598) as *mut WormEntry
344
  }
345
 
346
+ #[inline]
347
+ pub unsafe fn team_worm(
348
+ this: *const Self,
349
+ team_idx: usize,
350
+ worm_num: usize,
351
+ ) -> *const WormEntry {
352
+ let base = this as *const u8;
353
+ base.add(team_idx * 0x51C).add(worm_num * 0x9C).sub(0x598) as *const WormEntry
354
  }
355
 
356
+ /// Get a team's block pointer and its header pointer in one call.
357
  ///
358
+ /// Returns `(blocks[team_idx], blocks[team_idx+1].header.team)`.
359
  /// The block contains worm data, and the header (slot 0 of the next block)
360
  /// holds team metadata (worm_count, eliminated flag).
361
  ///
362
  /// **Note**: For accessing worms, prefer `team_worm()` which handles
363
  /// 8-worm teams correctly via cross-boundary pointer arithmetic.
364
  #[inline]
365
+ pub unsafe fn team_and_header(
366
+ this: *mut Self,
367
+ team_idx: usize,
368
+ ) -> (*mut TeamBlock, *mut TeamHeader) {
369
+ let blocks = Self::blocks_mut(this);
370
+ let block = blocks.add(team_idx);
371
+ let header = &raw mut (*blocks.add(team_idx + 1)).header.team as *mut TeamHeader;
372
  (block, header)
373
  }
374
 
375
+ /// Get team header pointer for Pattern B access (alliance/active_worm at +0x70/+0x74).
376
  ///
377
  /// Pattern B indexes from block[i+2] for 0-indexed team `i`:
378
  /// `base + 0x510 + i*0x51C` = `blocks[i+2].header.team + 0x70`.
379
  #[inline]
380
+ pub unsafe fn team_header_b_mut(this: *mut Self, team_idx: usize) -> *mut TeamHeader {
381
+ &raw mut (*Self::blocks_mut(this).add(team_idx + 2)).header.team as *mut TeamHeader
382
  }
383
 
384
+ /// Get team header pointer for Pattern B access (alliance/active_worm at +0x70/+0x74).
385
+ ///
386
+ /// Pattern B indexes from block[i+2] for 0-indexed team `i`:
387
+ /// `base + 0x510 + i*0x51C` = `blocks[i+2].header.team + 0x70`.
388
  #[inline]
389
+ pub unsafe fn team_header_b(this: *const Self, team_idx: usize) -> *const TeamHeader {
390
+ &raw const (*Self::blocks(this).add(team_idx + 2)).header.team as *const TeamHeader
 
 
391
  }
392
 
393
  /// Compute the flat index for ammo/delay table access.
 
395
  /// Reads the weapon alliance ID from the team header for the given
396
  /// 1-indexed team, then computes `alliance_id * 142 + weapon_id`.
397
  ///
398
+ /// Returns (alliance_id, weapon_id) for accessing weapon_slots.
 
 
 
 
399
  #[inline]
400
+ pub unsafe fn weapon_slot_key(
401
+ this: *const Self,
402
+ team_index: usize,
403
+ weapon_id: u32,
404
+ ) -> (usize, usize) {
405
+ let alliance_id = (*Self::team_header(this, team_index)).weapon_alliance as usize;
406
  (alliance_id, weapon_id as usize)
407
  }
408
  }
409
 
410
  // ── Snapshot impl ──────────────────────────────────────────
411
 
412
+ impl Snapshot for WormEntry {
413
  unsafe fn write_snapshot(
414
  &self,
415
  w: &mut dyn core::fmt::Write,
crates/openwa-core/src/engine/team_ops.rs CHANGED
@@ -1,6 +1,6 @@
1
  //! Team and worm state accessor functions.
2
  //!
3
- //! Pure Rust reimplementations of WA.exe functions that access the TeamArenaState
4
  //! area (DDGame + 0x4628). Called from hook trampolines in openwa-dll.
5
  //!
6
  //! Original WA functions:
@@ -15,29 +15,28 @@
15
 
16
  use crate::engine::team_arena::worm;
17
 
18
- use super::team_arena::TeamArenaRef;
19
 
20
  /// Count teams by alliance membership β€” port of 0x522030.
21
  ///
22
  /// Updates `state.active_team_count`, `same_alliance_count`, `enemy_team_count`.
23
  /// Uses Pattern B header fields (+0x70/+0x74) for alliance and active_worm.
24
- pub unsafe fn count_teams_by_alliance(arena: TeamArenaRef, alliance_id: i32) {
25
- let state = arena.state_mut();
26
- state.current_alliance = alliance_id;
27
- state.active_team_count = 0;
28
- state.same_alliance_count = 0;
29
- state.enemy_team_count = 0;
30
-
31
- for i in 0..state.team_count as usize {
32
- let header = arena.team_header_b(i);
33
- let team_alliance = header.alliance;
34
- let active_worm = header.active_worm;
35
  if active_worm != 0 && team_alliance >= 0 {
36
- state.active_team_count += 1;
37
  if team_alliance == alliance_id {
38
- state.same_alliance_count += 1;
39
  } else {
40
- state.enemy_team_count += 1;
41
  }
42
  }
43
  }
@@ -46,17 +45,17 @@ pub unsafe fn count_teams_by_alliance(arena: TeamArenaRef, alliance_id: i32) {
46
  /// Sum worm health for a team β€” port of 0x5224D0.
47
  ///
48
  /// Returns 0 if the team is eliminated.
49
- pub unsafe fn get_team_total_health(team_index: u32, arena: TeamArenaRef) -> i32 {
50
- let header = arena.team_header(team_index as usize);
51
 
52
- if header.eliminated != 0 {
53
  return 0;
54
  }
55
 
56
- let worm_count = header.worm_count;
57
  let mut total = 0i32;
58
  for w in 1..=worm_count as usize {
59
- total += arena.team_worm(team_index as usize, w).health;
60
  }
61
  total
62
  }
@@ -67,12 +66,10 @@ pub unsafe fn get_team_total_health(team_index: u32, arena: TeamArenaRef) -> i32
67
  pub unsafe fn is_worm_in_special_state(
68
  team_index: u32,
69
  worm_index: u32,
70
- arena: TeamArenaRef,
71
  ) -> u32 {
72
  if worm::is_special_state(
73
- arena
74
- .team_worm(team_index as usize, worm_index as usize)
75
- .state,
76
  ) {
77
  1
78
  } else {
@@ -87,29 +84,27 @@ pub unsafe fn is_worm_in_special_state(
87
  pub unsafe fn get_worm_position(
88
  team_index: u32,
89
  worm_index: u32,
90
- arena: TeamArenaRef,
91
  out_x: *mut i32,
92
  out_y: *mut i32,
93
  ) {
94
- let worm = arena.team_worm(team_index as usize, worm_index as usize);
95
- *out_x = *(worm._unknown_90.as_ptr() as *const i32);
96
- *out_y = *(worm._unknown_90.as_ptr().add(4) as *const i32);
97
  }
98
 
99
  /// Check if any worm on any team has state 0x64 β€” port of 0x5228D0.
100
  ///
101
  /// Iterates real teams (1-indexed). 11 xrefs in gameplay code.
102
- pub unsafe fn check_worm_state_0x64(arena: TeamArenaRef) -> u32 {
103
- let state = arena.state();
104
-
105
- for i in 1..=state.team_count as usize {
106
- let header = arena.team_header(i);
107
- if header.eliminated != 0 {
108
  continue;
109
  }
110
- let worm_count = header.worm_count;
111
  for w in 1..=worm_count as usize {
112
- if arena.team_worm(i, w).state == 0x64 {
113
  return 1;
114
  }
115
  }
@@ -120,16 +115,16 @@ pub unsafe fn check_worm_state_0x64(arena: TeamArenaRef) -> u32 {
120
  /// Check if any worm on a specific team has state 0x64 β€” port of 0x522930.
121
  ///
122
  /// Per-team version. 1 xref (FUN_00556ad0).
123
- pub unsafe fn check_team_worm_state_0x64(arena: TeamArenaRef, team_idx: u32) -> u32 {
124
- let header = arena.team_header(team_idx as usize);
125
 
126
- if header.eliminated != 0 {
127
  return 0;
128
  }
129
 
130
- let worm_count = header.worm_count;
131
  for w in 1..=worm_count as usize {
132
- if arena.team_worm(team_idx as usize, w).state == 0x64 {
133
  return 1;
134
  }
135
  }
@@ -139,17 +134,15 @@ pub unsafe fn check_team_worm_state_0x64(arena: TeamArenaRef, team_idx: u32) ->
139
  /// Scan all teams for state 0x8b β€” port of 0x522970.
140
  ///
141
  /// 1 xref (FUN_00557310).
142
- pub unsafe fn check_any_worm_state_0x8b(arena: TeamArenaRef) -> u32 {
143
- let state = arena.state();
144
-
145
- for i in 1..=state.team_count as usize {
146
- let header = arena.team_header(i);
147
- if header.eliminated != 0 {
148
  continue;
149
  }
150
- let worm_count = header.worm_count;
151
  for w in 1..=worm_count as usize {
152
- if arena.team_worm(i, w).state == 0x8b {
153
  return 1;
154
  }
155
  }
@@ -163,45 +156,43 @@ pub unsafe fn check_any_worm_state_0x8b(arena: TeamArenaRef) -> u32 {
163
  /// Called on turn transitions and worm selection (Tab).
164
  /// Updates active counters and records last_active_team/alliance.
165
  /// Uses Pattern B headers (0-indexed from `team_idx - 1`).
166
- pub unsafe fn set_active_worm(arena: TeamArenaRef, team_idx: u32, worm_index: i32) {
167
- let state = arena.state_mut();
168
-
169
  // Pattern B header: team_idx is 1-indexed, team_header_b is 0-indexed
170
  // team_idx=1 β†’ team_header_b(0) β†’ blocks[2].header
171
- let header = arena.team_header_b(team_idx as usize - 1);
172
 
173
  if worm_index == 0 {
174
  // Deactivate team
175
- if header.active_worm != 0 {
176
- state.active_worm_count -= 1;
177
- let alliance = header.alliance;
178
  if alliance >= 0 {
179
- state.active_team_count -= 1;
180
- if alliance == state.current_alliance {
181
- state.same_alliance_count -= 1;
182
  } else {
183
- state.enemy_team_count -= 1;
184
  }
185
  }
186
- arena.team_header_b_mut(team_idx as usize - 1).active_worm = 0;
187
  }
188
  } else {
189
  // Activate team β€” only update counters if not already active
190
- if header.active_worm == 0 {
191
- state.active_worm_count += 1;
192
- state.last_active_team = team_idx as i32;
193
- let alliance = header.alliance;
194
- state.last_active_alliance = alliance;
195
  if alliance >= 0 {
196
- state.active_team_count += 1;
197
- if alliance == state.current_alliance {
198
- state.same_alliance_count += 1;
199
  } else {
200
- state.enemy_team_count += 1;
201
  }
202
  }
203
  }
204
  // Always write worm_index (original writes ESI unconditionally)
205
- arena.team_header_b_mut(team_idx as usize - 1).active_worm = worm_index;
206
  }
207
  }
 
1
  //! Team and worm state accessor functions.
2
  //!
3
+ //! Pure Rust reimplementations of WA.exe functions that access the TeamArena
4
  //! area (DDGame + 0x4628). Called from hook trampolines in openwa-dll.
5
  //!
6
  //! Original WA functions:
 
15
 
16
  use crate::engine::team_arena::worm;
17
 
18
+ use super::team_arena::TeamArena;
19
 
20
  /// Count teams by alliance membership β€” port of 0x522030.
21
  ///
22
  /// Updates `state.active_team_count`, `same_alliance_count`, `enemy_team_count`.
23
  /// Uses Pattern B header fields (+0x70/+0x74) for alliance and active_worm.
24
+ pub unsafe fn count_teams_by_alliance(arena: *mut TeamArena, alliance_id: i32) {
25
+ (*arena).current_alliance = alliance_id;
26
+ (*arena).active_team_count = 0;
27
+ (*arena).same_alliance_count = 0;
28
+ (*arena).enemy_team_count = 0;
29
+
30
+ for i in 0..(*arena).team_count as usize {
31
+ let header = TeamArena::team_header_b(arena, i);
32
+ let team_alliance = (*header).alliance;
33
+ let active_worm = (*header).active_worm;
 
34
  if active_worm != 0 && team_alliance >= 0 {
35
+ (*arena).active_team_count += 1;
36
  if team_alliance == alliance_id {
37
+ (*arena).same_alliance_count += 1;
38
  } else {
39
+ (*arena).enemy_team_count += 1;
40
  }
41
  }
42
  }
 
45
  /// Sum worm health for a team β€” port of 0x5224D0.
46
  ///
47
  /// Returns 0 if the team is eliminated.
48
+ pub unsafe fn get_team_total_health(team_index: u32, arena: *const TeamArena) -> i32 {
49
+ let header = TeamArena::team_header(arena, team_index as usize);
50
 
51
+ if (*header).eliminated != 0 {
52
  return 0;
53
  }
54
 
55
+ let worm_count = (*header).worm_count;
56
  let mut total = 0i32;
57
  for w in 1..=worm_count as usize {
58
+ total += (*TeamArena::team_worm(arena, team_index as usize, w)).health;
59
  }
60
  total
61
  }
 
66
  pub unsafe fn is_worm_in_special_state(
67
  team_index: u32,
68
  worm_index: u32,
69
+ arena: *const TeamArena,
70
  ) -> u32 {
71
  if worm::is_special_state(
72
+ (*TeamArena::team_worm(arena, team_index as usize, worm_index as usize)).state,
 
 
73
  ) {
74
  1
75
  } else {
 
84
  pub unsafe fn get_worm_position(
85
  team_index: u32,
86
  worm_index: u32,
87
+ arena: *const TeamArena,
88
  out_x: *mut i32,
89
  out_y: *mut i32,
90
  ) {
91
+ let worm = TeamArena::team_worm(arena, team_index as usize, worm_index as usize);
92
+ *out_x = *((*worm)._unknown_90.as_ptr() as *const i32);
93
+ *out_y = *((*worm)._unknown_90.as_ptr().add(4) as *const i32);
94
  }
95
 
96
  /// Check if any worm on any team has state 0x64 β€” port of 0x5228D0.
97
  ///
98
  /// Iterates real teams (1-indexed). 11 xrefs in gameplay code.
99
+ pub unsafe fn check_worm_state_0x64(arena: *const TeamArena) -> u32 {
100
+ for i in 1..=(*arena).team_count as usize {
101
+ let header = TeamArena::team_header(arena, i);
102
+ if (*header).eliminated != 0 {
 
 
103
  continue;
104
  }
105
+ let worm_count = (*header).worm_count;
106
  for w in 1..=worm_count as usize {
107
+ if (*TeamArena::team_worm(arena, i, w)).state == 0x64 {
108
  return 1;
109
  }
110
  }
 
115
  /// Check if any worm on a specific team has state 0x64 β€” port of 0x522930.
116
  ///
117
  /// Per-team version. 1 xref (FUN_00556ad0).
118
+ pub unsafe fn check_team_worm_state_0x64(arena: *const TeamArena, team_idx: u32) -> u32 {
119
+ let header = TeamArena::team_header(arena, team_idx as usize);
120
 
121
+ if (*header).eliminated != 0 {
122
  return 0;
123
  }
124
 
125
+ let worm_count = (*header).worm_count;
126
  for w in 1..=worm_count as usize {
127
+ if (*TeamArena::team_worm(arena, team_idx as usize, w)).state == 0x64 {
128
  return 1;
129
  }
130
  }
 
134
  /// Scan all teams for state 0x8b β€” port of 0x522970.
135
  ///
136
  /// 1 xref (FUN_00557310).
137
+ pub unsafe fn check_any_worm_state_0x8b(arena: *const TeamArena) -> u32 {
138
+ for i in 1..=(*arena).team_count as usize {
139
+ let header = TeamArena::team_header(arena, i);
140
+ if (*header).eliminated != 0 {
 
 
141
  continue;
142
  }
143
+ let worm_count = (*header).worm_count;
144
  for w in 1..=worm_count as usize {
145
+ if (*TeamArena::team_worm(arena, i, w)).state == 0x8b {
146
  return 1;
147
  }
148
  }
 
156
  /// Called on turn transitions and worm selection (Tab).
157
  /// Updates active counters and records last_active_team/alliance.
158
  /// Uses Pattern B headers (0-indexed from `team_idx - 1`).
159
+ pub unsafe fn set_active_worm(arena: *mut TeamArena, team_idx: u32, worm_index: i32) {
 
 
160
  // Pattern B header: team_idx is 1-indexed, team_header_b is 0-indexed
161
  // team_idx=1 β†’ team_header_b(0) β†’ blocks[2].header
162
+ let header = TeamArena::team_header_b_mut(arena, team_idx as usize - 1);
163
 
164
  if worm_index == 0 {
165
  // Deactivate team
166
+ if (*header).active_worm != 0 {
167
+ (*arena).active_worm_count -= 1;
168
+ let alliance = (*header).alliance;
169
  if alliance >= 0 {
170
+ (*arena).active_team_count -= 1;
171
+ if alliance == (*arena).current_alliance {
172
+ (*arena).same_alliance_count -= 1;
173
  } else {
174
+ (*arena).enemy_team_count -= 1;
175
  }
176
  }
177
+ (*header).active_worm = 0;
178
  }
179
  } else {
180
  // Activate team β€” only update counters if not already active
181
+ if (*header).active_worm == 0 {
182
+ (*arena).active_worm_count += 1;
183
+ (*arena).last_active_team = team_idx as i32;
184
+ let alliance = (*header).alliance;
185
+ (*arena).last_active_alliance = alliance;
186
  if alliance >= 0 {
187
+ (*arena).active_team_count += 1;
188
+ if alliance == (*arena).current_alliance {
189
+ (*arena).same_alliance_count += 1;
190
  } else {
191
+ (*arena).enemy_team_count += 1;
192
  }
193
  }
194
  }
195
  // Always write worm_index (original writes ESI unconditionally)
196
+ (*header).active_worm = worm_index;
197
  }
198
  }
crates/openwa-core/src/game/weapon_fire.rs CHANGED
@@ -11,7 +11,7 @@
11
 
12
  use crate::address::va;
13
  use crate::audio::{KnownSoundId, SoundId};
14
- use crate::engine::{TeamArenaRef, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH};
15
  use crate::fixed::Fixed;
16
  use crate::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
17
  use crate::game::Weapon;
@@ -48,15 +48,14 @@ const _: () = assert!(core::mem::size_of::<WeaponReleaseContext>() == 0x2C);
48
  // __usercall: EAX = team_index, EDX = amount, [ESP+4] = team_info_base, [ESP+8] = weapon_id
49
  // RET 0x8
50
 
51
- pub unsafe fn add_ammo(team_index: u32, amount: i32, arena: TeamArenaRef, weapon_id: u32) {
52
- let (alliance, wid) = arena.weapon_slot_key(team_index as usize, weapon_id);
53
- let state = arena.state_mut();
54
- let ammo = state.get_ammo(alliance, wid);
55
  if ammo >= 0 {
56
  if amount < 0 {
57
- *state.ammo_mut(alliance, wid) = -1;
58
  } else {
59
- *state.ammo_mut(alliance, wid) = ammo + amount;
60
  }
61
  }
62
  }
@@ -67,12 +66,11 @@ pub unsafe fn add_ammo(team_index: u32, amount: i32, arena: TeamArenaRef, weapon
67
  // __usercall: EAX = team_index, ECX = team_info_base, [ESP+4] = weapon_id
68
  // RET 0x4
69
 
70
- pub unsafe fn subtract_ammo(team_index: u32, arena: TeamArenaRef, weapon_id: u32) {
71
- let (alliance, wid) = arena.weapon_slot_key(team_index as usize, weapon_id);
72
- let state = arena.state_mut();
73
- let ammo = state.get_ammo(alliance, wid);
74
  if ammo > 0 {
75
- *state.ammo_mut(alliance, wid) = ammo - 1;
76
  }
77
  }
78
 
@@ -82,31 +80,30 @@ pub unsafe fn subtract_ammo(team_index: u32, arena: TeamArenaRef, weapon_id: u32
82
  // __usercall: EAX = team_index, ESI = team_info_base, EDX = weapon_id
83
  // plain RET, returns EAX = ammo count
84
 
85
- pub unsafe fn get_ammo(team_index: u32, arena: TeamArenaRef, weapon_id: u32) -> u32 {
86
- let (alliance, wid) = arena.weapon_slot_key(team_index as usize, weapon_id);
87
- let state = arena.state();
88
 
89
  // Check weapon delay
90
- if state.get_delay(alliance, wid) != 0 {
91
- if state.game_mode_flag == 0 {
92
  return 0;
93
  }
94
  // In sudden death (phase >= 484), delayed weapons return 0
95
  // unless it's Teleport (weapon 0x28)
96
- if state.game_phase >= GAME_PHASE_SUDDEN_DEATH && weapon_id != Weapon::Teleport as u32 {
97
  return 0;
98
  }
99
  }
100
 
101
  // SelectWorm (0x3B) requires >1 alive worm on the team
102
- if state.game_phase >= GAME_PHASE_NORMAL_MIN
103
  && weapon_id == Weapon::SelectWorm as u32
104
  && count_alive_worms(team_index, arena) == 0
105
  {
106
  return 0;
107
  }
108
 
109
- state.get_ammo(alliance, wid) as u32
110
  }
111
 
112
  // ============================================================
@@ -115,12 +112,12 @@ pub unsafe fn get_ammo(team_index: u32, arena: TeamArenaRef, weapon_id: u32) ->
115
  // __usercall: EAX = team_index, ECX = base
116
  // plain RET, returns EAX = bool (1 if >1 worm alive on team)
117
 
118
- pub unsafe fn count_alive_worms(team_index: u32, arena: TeamArenaRef) -> u32 {
119
- let header = arena.team_header(team_index as usize);
120
- let worm_count = header.worm_count;
121
  let mut alive = 0i32;
122
  for w in 1..=worm_count as usize {
123
- if arena.team_worm(team_index as usize, w).health > 0 {
124
  alive += 1;
125
  }
126
  }
@@ -470,14 +467,14 @@ unsafe fn fire_skip_go(worm: *const CTaskWorm, entry: *const WeaponEntry) {
470
  let bit_index = ((*entry).fire_params.shot_count & 0x1F) as u32;
471
  let bit = 1u32 << bit_index;
472
 
473
- let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
474
- let header = arena.team_header_mut(team_index);
475
- let flags = header.turn_action_flags;
476
 
477
  if game_version > 0x1C && (flags & bit) != 0 {
478
- header.turn_action_flags = flags & !bit;
479
  } else {
480
- header.turn_action_flags = flags | bit;
481
  }
482
  }
483
 
@@ -489,11 +486,11 @@ unsafe fn fire_freeze(worm: *mut CTaskWorm) {
489
  fire_send_team_message(worm, 0x29);
490
 
491
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
492
- let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
493
  let team_index = (*worm).team_index as usize;
494
  let worm_index = (*worm).worm_index as usize;
495
- let entry = arena.team_worm_mut(team_index, worm_index);
496
- entry.turn_action_counter_Maybe += 14;
497
  }
498
 
499
  /// Mail/Mine/Mole (subtype 14) β€” pure Rust replacement for 0x51E670.
@@ -524,11 +521,11 @@ unsafe fn fire_mail_mine_mole(worm: *mut CTaskWorm) {
524
 
525
  fire_send_team_message(worm, 0x28);
526
 
527
- let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
528
  let team_index = (*worm).team_index as usize;
529
  let worm_index = (*worm).worm_index as usize;
530
- let entry = arena.team_worm_mut(team_index, worm_index);
531
- entry.turn_action_counter_Maybe += 7;
532
  }
533
 
534
  /// Teleport (subtype 10) β€” pure Rust port of 0x51E710.
@@ -983,12 +980,12 @@ unsafe fn fire_girder(worm: *mut CTaskWorm) {
983
  girder_visual(landscape, girder_x >> 16, girder_y >> 16, sprite1, sprite2);
984
 
985
  // Increment WormEntry counters
986
- let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
987
  let team_index = (*worm).team_index as usize;
988
  let worm_index = (*worm).worm_index as usize;
989
- let entry = arena.team_worm_mut(team_index, worm_index);
990
- entry.turn_action_counter_Maybe += 3;
991
- entry.effect_counter_04_Maybe += 10;
992
  }
993
  }
994
 
 
11
 
12
  use crate::address::va;
13
  use crate::audio::{KnownSoundId, SoundId};
14
+ use crate::engine::{TeamArena, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH};
15
  use crate::fixed::Fixed;
16
  use crate::game::weapon::{WeaponEntry, WeaponFireParams, WeaponSpawnData};
17
  use crate::game::Weapon;
 
48
  // __usercall: EAX = team_index, EDX = amount, [ESP+4] = team_info_base, [ESP+8] = weapon_id
49
  // RET 0x8
50
 
51
+ pub unsafe fn add_ammo(team_index: u32, amount: i32, arena: *mut TeamArena, weapon_id: u32) {
52
+ let (alliance, wid) = TeamArena::weapon_slot_key(arena, team_index as usize, weapon_id);
53
+ let ammo = (*arena).get_ammo(alliance, wid);
 
54
  if ammo >= 0 {
55
  if amount < 0 {
56
+ *(*arena).ammo_mut(alliance, wid) = -1;
57
  } else {
58
+ *(*arena).ammo_mut(alliance, wid) = ammo + amount;
59
  }
60
  }
61
  }
 
66
  // __usercall: EAX = team_index, ECX = team_info_base, [ESP+4] = weapon_id
67
  // RET 0x4
68
 
69
+ pub unsafe fn subtract_ammo(team_index: u32, arena: *mut TeamArena, weapon_id: u32) {
70
+ let (alliance, wid) = TeamArena::weapon_slot_key(arena, team_index as usize, weapon_id);
71
+ let ammo = (*arena).get_ammo(alliance, wid);
 
72
  if ammo > 0 {
73
+ *(*arena).ammo_mut(alliance, wid) = ammo - 1;
74
  }
75
  }
76
 
 
80
  // __usercall: EAX = team_index, ESI = team_info_base, EDX = weapon_id
81
  // plain RET, returns EAX = ammo count
82
 
83
+ pub unsafe fn get_ammo(team_index: u32, arena: *mut TeamArena, weapon_id: u32) -> u32 {
84
+ let (alliance, wid) = TeamArena::weapon_slot_key(arena, team_index as usize, weapon_id);
 
85
 
86
  // Check weapon delay
87
+ if (*arena).get_delay(alliance, wid) != 0 {
88
+ if (*arena).game_mode_flag == 0 {
89
  return 0;
90
  }
91
  // In sudden death (phase >= 484), delayed weapons return 0
92
  // unless it's Teleport (weapon 0x28)
93
+ if (*arena).game_phase >= GAME_PHASE_SUDDEN_DEATH && weapon_id != Weapon::Teleport as u32 {
94
  return 0;
95
  }
96
  }
97
 
98
  // SelectWorm (0x3B) requires >1 alive worm on the team
99
+ if (*arena).game_phase >= GAME_PHASE_NORMAL_MIN
100
  && weapon_id == Weapon::SelectWorm as u32
101
  && count_alive_worms(team_index, arena) == 0
102
  {
103
  return 0;
104
  }
105
 
106
+ (*arena).get_ammo(alliance, wid) as u32
107
  }
108
 
109
  // ============================================================
 
112
  // __usercall: EAX = team_index, ECX = base
113
  // plain RET, returns EAX = bool (1 if >1 worm alive on team)
114
 
115
+ pub unsafe fn count_alive_worms(team_index: u32, arena: *const TeamArena) -> u32 {
116
+ let header = TeamArena::team_header(arena, team_index as usize);
117
+ let worm_count = (*header).worm_count;
118
  let mut alive = 0i32;
119
  for w in 1..=worm_count as usize {
120
+ if (*TeamArena::team_worm(arena, team_index as usize, w)).health > 0 {
121
  alive += 1;
122
  }
123
  }
 
467
  let bit_index = ((*entry).fire_params.shot_count & 0x1F) as u32;
468
  let bit = 1u32 << bit_index;
469
 
470
+ let arena = &raw mut (*ddgame).team_arena;
471
+ let header = TeamArena::team_header_mut(arena, team_index);
472
+ let flags = (*header).turn_action_flags;
473
 
474
  if game_version > 0x1C && (flags & bit) != 0 {
475
+ (*header).turn_action_flags = flags & !bit;
476
  } else {
477
+ (*header).turn_action_flags = flags | bit;
478
  }
479
  }
480
 
 
486
  fire_send_team_message(worm, 0x29);
487
 
488
  let ddgame = CTask::ddgame_raw(worm as *const CTask);
489
+ let arena = &raw mut (*ddgame).team_arena;
490
  let team_index = (*worm).team_index as usize;
491
  let worm_index = (*worm).worm_index as usize;
492
+ let entry = TeamArena::team_worm_mut(arena, team_index, worm_index);
493
+ (*entry).turn_action_counter_Maybe += 14;
494
  }
495
 
496
  /// Mail/Mine/Mole (subtype 14) β€” pure Rust replacement for 0x51E670.
 
521
 
522
  fire_send_team_message(worm, 0x28);
523
 
524
+ let arena = &raw mut (*ddgame).team_arena;
525
  let team_index = (*worm).team_index as usize;
526
  let worm_index = (*worm).worm_index as usize;
527
+ let entry = TeamArena::team_worm_mut(arena, team_index, worm_index);
528
+ (*entry).turn_action_counter_Maybe += 7;
529
  }
530
 
531
  /// Teleport (subtype 10) β€” pure Rust port of 0x51E710.
 
980
  girder_visual(landscape, girder_x >> 16, girder_y >> 16, sprite1, sprite2);
981
 
982
  // Increment WormEntry counters
983
+ let arena = &raw mut (*ddgame).team_arena;
984
  let team_index = (*worm).team_index as usize;
985
  let worm_index = (*worm).worm_index as usize;
986
+ let entry = TeamArena::team_worm_mut(arena, team_index, worm_index);
987
+ (*entry).turn_action_counter_Maybe += 3;
988
+ (*entry).effect_counter_04_Maybe += 10;
989
  }
990
  }
991
 
crates/openwa-core/src/render/mod.rs CHANGED
@@ -16,3 +16,17 @@ pub use sprite::{Sprite, SpriteFrame, SpriteId};
16
  pub use turn_order::{
17
  AnimatedItemList, TurnOrderAllianceGroup, TurnOrderTeamEntry, TurnOrderWidget,
18
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  pub use turn_order::{
17
  AnimatedItemList, TurnOrderAllianceGroup, TurnOrderTeamEntry, TurnOrderWidget,
18
  };
19
+
20
+ /// Render table entry (0x14 = 20 bytes).
21
+ ///
22
+ /// 14 entries live at DDGame+0x73B0 (stride 0x14). Only the first u32
23
+ /// is zeroed during construction; the rest is uninitialized/unknown.
24
+ #[repr(C)]
25
+ #[derive(Clone, Copy)]
26
+ pub struct RenderEntry {
27
+ /// Active/state flag (zeroed on init).
28
+ pub active: u32,
29
+ /// Unknown data.
30
+ pub _unknown: [u8; 16],
31
+ }
32
+ const _: () = assert!(core::mem::size_of::<RenderEntry>() == 0x14);
crates/openwa-core/src/task/team.rs CHANGED
@@ -66,7 +66,7 @@ pub struct CTaskTeam {
66
  pub _secondary_vtable: u32,
67
  /// 0x34: Unknown β€” observed to hold the same value as `team_index` in all runs.
68
  pub _unknown_34: u32,
69
- /// 0x38: Team number, **1-based**. Matches TeamArenaState slot index.
70
  pub team_index: u32,
71
  /// 0x3C: Unknown β€” observed 0 for team 1, 1 for team 2.
72
  pub _unknown_3c: u32,
 
66
  pub _secondary_vtable: u32,
67
  /// 0x34: Unknown β€” observed to hold the same value as `team_index` in all runs.
68
  pub _unknown_34: u32,
69
+ /// 0x38: Team number, **1-based**. Matches TeamArena slot index.
70
  pub team_index: u32,
71
  /// 0x3C: Unknown β€” observed 0 for team 1, 1 for team 2.
72
  pub _unknown_3c: u32,
crates/openwa-core/src/task/turn_game.rs CHANGED
@@ -152,9 +152,9 @@ pub struct CTaskTurnGame {
152
  pub current_team: u32,
153
  /// 0x130: active worm index within the current team, **0-based** (stride 0x9C).
154
  pub current_worm: u32,
155
- /// 0x134: arena team index used for TeamArenaState lookups.
156
  pub arena_team: u32,
157
- /// 0x138: arena worm index used for TeamArenaState lookups.
158
  pub arena_worm: u32,
159
  pub _unknown_13c: u32,
160
  /// 0x140: set to 1 at construction; role unclear.
 
152
  pub current_team: u32,
153
  /// 0x130: active worm index within the current team, **0-based** (stride 0x9C).
154
  pub current_worm: u32,
155
+ /// 0x134: arena team index used for TeamArena lookups.
156
  pub arena_team: u32,
157
+ /// 0x138: arena worm index used for TeamArena lookups.
158
  pub arena_worm: u32,
159
  pub _unknown_13c: u32,
160
  /// 0x140: set to 1 at construction; role unclear.
crates/openwa-core/src/task/worm.rs CHANGED
@@ -9,7 +9,7 @@ use crate::FieldRegistry;
9
  ///
10
  /// CTaskWorm's `SetState` (vtable slot 14) transitions between these.
11
  /// The state byte lives at CTaskWorm+0x44 (inside `base.subclass_data`).
12
- /// Also stored in WormEntry.state in the TeamArenaState.
13
  ///
14
  /// States 0x68..=0x8A are the "weapon/action active" range β€” checked by
15
  /// `(state - 0x68) < 0x23` in HandleMessage. States 0x80+ are dying/dead.
 
9
  ///
10
  /// CTaskWorm's `SetState` (vtable slot 14) transitions between these.
11
  /// The state byte lives at CTaskWorm+0x44 (inside `base.subclass_data`).
12
+ /// Also stored in WormEntry.state in the TeamArena.
13
  ///
14
  /// States 0x68..=0x8A are the "weapon/action active" range β€” checked by
15
  /// `(state - 0x68) < 0x23` in HandleMessage. States 0x80+ are dying/dead.
crates/openwa-dll/src/replacements/replay_test.rs CHANGED
@@ -13,10 +13,9 @@
13
  use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering};
14
 
15
  use crate::log_line;
16
- use openwa_core::engine::ddgame::offsets;
17
  use openwa_core::engine::game_session;
18
  use openwa_core::engine::DDGame;
19
- use openwa_core::engine::TeamArenaRef;
20
 
21
  // ---------------------------------------------------------------------------
22
  // Fast-forward
@@ -69,9 +68,8 @@ pub unsafe fn on_frame(frame: u32) {
69
  ///
70
  /// Returns (alive_team_count, total_team_count).
71
  unsafe fn count_alive_teams(ddgame: *const DDGame) -> (i32, i32) {
72
- let arena_base = (ddgame as u32) + offsets::TEAM_ARENA_STATE as u32;
73
- let arena = TeamArenaRef::from_raw(arena_base);
74
- let team_count = arena.state().team_count;
75
  if team_count <= 0 || team_count > 6 {
76
  return (0, 0);
77
  }
@@ -80,8 +78,7 @@ unsafe fn count_alive_teams(ddgame: *const DDGame) -> (i32, i32) {
80
  for t in 1..=team_count as usize {
81
  let mut team_has_alive = false;
82
  for w in 1..=8usize {
83
- let worm = arena.team_worm(t, w);
84
- if worm.health > 0 {
85
  team_has_alive = true;
86
  break;
87
  }
 
13
  use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering};
14
 
15
  use crate::log_line;
 
16
  use openwa_core::engine::game_session;
17
  use openwa_core::engine::DDGame;
18
+ use openwa_core::engine::TeamArena;
19
 
20
  // ---------------------------------------------------------------------------
21
  // Fast-forward
 
68
  ///
69
  /// Returns (alive_team_count, total_team_count).
70
  unsafe fn count_alive_teams(ddgame: *const DDGame) -> (i32, i32) {
71
+ let arena = &raw const (*ddgame).team_arena;
72
+ let team_count = (*arena).team_count;
 
73
  if team_count <= 0 || team_count > 6 {
74
  return (0, 0);
75
  }
 
78
  for t in 1..=team_count as usize {
79
  let mut team_has_alive = false;
80
  for w in 1..=8usize {
81
+ if (*TeamArena::team_worm(arena, t, w)).health > 0 {
 
82
  team_has_alive = true;
83
  break;
84
  }
crates/openwa-dll/src/replacements/team.rs CHANGED
@@ -4,13 +4,13 @@
4
  //! This file contains only usercall trampolines and hook installation.
5
 
6
  use openwa_core::engine::team_ops;
7
- use openwa_core::{address::va, engine::TeamArenaRef};
8
 
9
  use crate::hook::{self, usercall_trampoline};
10
 
11
  // ── CountTeamsByAlliance (0x522030): usercall(EAX=base, EDI=alliance_id) ──
12
 
13
- unsafe extern "cdecl" fn count_teams_by_alliance_impl(arena: TeamArenaRef, alliance_id: i32) {
14
  team_ops::count_teams_by_alliance(arena, alliance_id);
15
  }
16
 
@@ -19,7 +19,7 @@ usercall_trampoline!(fn trampoline_count_teams_by_alliance; impl_fn = count_team
19
 
20
  // ── GetTeamTotalHealth (0x5224D0): fastcall(ECX=team_index, EDX=base) ──
21
 
22
- unsafe extern "cdecl" fn get_team_total_health_impl(team_index: u32, arena: TeamArenaRef) -> i32 {
23
  team_ops::get_team_total_health(team_index, arena)
24
  }
25
 
@@ -31,7 +31,7 @@ usercall_trampoline!(fn trampoline_get_team_total_health; impl_fn = get_team_tot
31
  unsafe extern "cdecl" fn is_worm_in_special_state_impl(
32
  team_index: u32,
33
  worm_index: u32,
34
- arena: TeamArenaRef,
35
  ) -> u32 {
36
  team_ops::is_worm_in_special_state(team_index, worm_index, arena)
37
  }
@@ -44,7 +44,7 @@ usercall_trampoline!(fn trampoline_is_worm_in_special_state; impl_fn = is_worm_i
44
  unsafe extern "cdecl" fn get_worm_position_impl(
45
  team_index: u32,
46
  worm_index: u32,
47
- arena: TeamArenaRef,
48
  out_x: *mut i32,
49
  out_y: *mut i32,
50
  ) {
@@ -56,7 +56,7 @@ usercall_trampoline!(fn trampoline_get_worm_position; impl_fn = get_worm_positio
56
 
57
  // ── CheckWormState0x64 (0x5228D0): usercall(EAX=base) ──
58
 
59
- unsafe extern "cdecl" fn check_worm_state_0x64_impl(arena: TeamArenaRef) -> u32 {
60
  team_ops::check_worm_state_0x64(arena)
61
  }
62
 
@@ -66,7 +66,7 @@ usercall_trampoline!(fn trampoline_check_worm_state_0x64; impl_fn = check_worm_s
66
  // ── CheckTeamWormState0x64 (0x522930): usercall(ECX=base, EAX=team_idx) ──
67
 
68
  unsafe extern "cdecl" fn check_team_worm_state_0x64_impl(
69
- arena: TeamArenaRef,
70
  team_idx: u32,
71
  ) -> u32 {
72
  team_ops::check_team_worm_state_0x64(arena, team_idx)
@@ -77,7 +77,7 @@ usercall_trampoline!(fn trampoline_check_team_worm_state_0x64; impl_fn = check_t
77
 
78
  // ── CheckAnyWormState0x8b (0x522970): usercall(EAX=base) ──
79
 
80
- unsafe extern "cdecl" fn check_any_worm_state_0x8b_impl(arena: TeamArenaRef) -> u32 {
81
  team_ops::check_any_worm_state_0x8b(arena)
82
  }
83
 
@@ -86,7 +86,11 @@ usercall_trampoline!(fn trampoline_check_any_worm_state_0x8b; impl_fn = check_an
86
 
87
  // ── SetActiveWorm_Maybe (0x522500): usercall(EAX=base, EDX=team_idx, ESI=worm) ──
88
 
89
- unsafe extern "cdecl" fn set_active_worm_impl(arena: TeamArenaRef, team_idx: u32, worm_index: i32) {
 
 
 
 
90
  team_ops::set_active_worm(arena, team_idx, worm_index);
91
  }
92
 
 
4
  //! This file contains only usercall trampolines and hook installation.
5
 
6
  use openwa_core::engine::team_ops;
7
+ use openwa_core::{address::va, engine::TeamArena};
8
 
9
  use crate::hook::{self, usercall_trampoline};
10
 
11
  // ── CountTeamsByAlliance (0x522030): usercall(EAX=base, EDI=alliance_id) ──
12
 
13
+ unsafe extern "cdecl" fn count_teams_by_alliance_impl(arena: *mut TeamArena, alliance_id: i32) {
14
  team_ops::count_teams_by_alliance(arena, alliance_id);
15
  }
16
 
 
19
 
20
  // ── GetTeamTotalHealth (0x5224D0): fastcall(ECX=team_index, EDX=base) ──
21
 
22
+ unsafe extern "cdecl" fn get_team_total_health_impl(team_index: u32, arena: *mut TeamArena) -> i32 {
23
  team_ops::get_team_total_health(team_index, arena)
24
  }
25
 
 
31
  unsafe extern "cdecl" fn is_worm_in_special_state_impl(
32
  team_index: u32,
33
  worm_index: u32,
34
+ arena: *mut TeamArena,
35
  ) -> u32 {
36
  team_ops::is_worm_in_special_state(team_index, worm_index, arena)
37
  }
 
44
  unsafe extern "cdecl" fn get_worm_position_impl(
45
  team_index: u32,
46
  worm_index: u32,
47
+ arena: *mut TeamArena,
48
  out_x: *mut i32,
49
  out_y: *mut i32,
50
  ) {
 
56
 
57
  // ── CheckWormState0x64 (0x5228D0): usercall(EAX=base) ──
58
 
59
+ unsafe extern "cdecl" fn check_worm_state_0x64_impl(arena: *mut TeamArena) -> u32 {
60
  team_ops::check_worm_state_0x64(arena)
61
  }
62
 
 
66
  // ── CheckTeamWormState0x64 (0x522930): usercall(ECX=base, EAX=team_idx) ──
67
 
68
  unsafe extern "cdecl" fn check_team_worm_state_0x64_impl(
69
+ arena: *mut TeamArena,
70
  team_idx: u32,
71
  ) -> u32 {
72
  team_ops::check_team_worm_state_0x64(arena, team_idx)
 
77
 
78
  // ── CheckAnyWormState0x8b (0x522970): usercall(EAX=base) ──
79
 
80
+ unsafe extern "cdecl" fn check_any_worm_state_0x8b_impl(arena: *mut TeamArena) -> u32 {
81
  team_ops::check_any_worm_state_0x8b(arena)
82
  }
83
 
 
86
 
87
  // ── SetActiveWorm_Maybe (0x522500): usercall(EAX=base, EDX=team_idx, ESI=worm) ──
88
 
89
+ unsafe extern "cdecl" fn set_active_worm_impl(
90
+ arena: *mut TeamArena,
91
+ team_idx: u32,
92
+ worm_index: i32,
93
+ ) {
94
  team_ops::set_active_worm(arena, team_idx, worm_index);
95
  }
96
 
crates/openwa-dll/src/replacements/weapon.rs CHANGED
@@ -16,7 +16,7 @@ use openwa_core::game::weapon::WeaponFireParams;
16
  use openwa_core::game::weapon_fire;
17
  use openwa_core::log::log_line;
18
  use openwa_core::task::worm::CTaskWorm;
19
- use openwa_core::{address::va, engine::TeamArenaRef};
20
 
21
  use crate::hook::{self, usercall_trampoline};
22
 
@@ -25,7 +25,7 @@ use crate::hook::{self, usercall_trampoline};
25
  unsafe extern "cdecl" fn add_ammo_impl(
26
  team_index: u32,
27
  amount: i32,
28
- arena: TeamArenaRef,
29
  weapon_id: u32,
30
  ) {
31
  weapon_fire::add_ammo(team_index, amount, arena, weapon_id);
@@ -36,7 +36,11 @@ usercall_trampoline!(fn trampoline_add_ammo; impl_fn = add_ammo_impl;
36
 
37
  // ── SubtractAmmo (0x522680): usercall(EAX=team, ECX=arena, stack=weapon_id) ──
38
 
39
- unsafe extern "cdecl" fn subtract_ammo_impl(team_index: u32, arena: TeamArenaRef, weapon_id: u32) {
 
 
 
 
40
  weapon_fire::subtract_ammo(team_index, arena, weapon_id);
41
  }
42
 
@@ -47,7 +51,7 @@ usercall_trampoline!(fn trampoline_subtract_ammo; impl_fn = subtract_ammo_impl;
47
 
48
  unsafe extern "cdecl" fn get_ammo_impl(
49
  team_index: u32,
50
- arena: TeamArenaRef,
51
  weapon_id: u32,
52
  ) -> u32 {
53
  weapon_fire::get_ammo(team_index, arena, weapon_id)
@@ -58,7 +62,7 @@ usercall_trampoline!(fn trampoline_get_ammo; impl_fn = get_ammo_impl;
58
 
59
  // ── CountAliveWorms (0x5225A0): usercall(EAX=team, ECX=arena) ──
60
 
61
- unsafe extern "cdecl" fn count_alive_worms_impl(team_index: u32, arena: TeamArenaRef) -> u32 {
62
  weapon_fire::count_alive_worms(team_index, arena)
63
  }
64
 
 
16
  use openwa_core::game::weapon_fire;
17
  use openwa_core::log::log_line;
18
  use openwa_core::task::worm::CTaskWorm;
19
+ use openwa_core::{address::va, engine::TeamArena};
20
 
21
  use crate::hook::{self, usercall_trampoline};
22
 
 
25
  unsafe extern "cdecl" fn add_ammo_impl(
26
  team_index: u32,
27
  amount: i32,
28
+ arena: *mut TeamArena,
29
  weapon_id: u32,
30
  ) {
31
  weapon_fire::add_ammo(team_index, amount, arena, weapon_id);
 
36
 
37
  // ── SubtractAmmo (0x522680): usercall(EAX=team, ECX=arena, stack=weapon_id) ──
38
 
39
+ unsafe extern "cdecl" fn subtract_ammo_impl(
40
+ team_index: u32,
41
+ arena: *mut TeamArena,
42
+ weapon_id: u32,
43
+ ) {
44
  weapon_fire::subtract_ammo(team_index, arena, weapon_id);
45
  }
46
 
 
51
 
52
  unsafe extern "cdecl" fn get_ammo_impl(
53
  team_index: u32,
54
+ arena: *mut TeamArena,
55
  weapon_id: u32,
56
  ) -> u32 {
57
  weapon_fire::get_ammo(team_index, arena, weapon_id)
 
62
 
63
  // ── CountAliveWorms (0x5225A0): usercall(EAX=team, ECX=arena) ──
64
 
65
+ unsafe extern "cdecl" fn count_alive_worms_impl(team_index: u32, arena: *mut TeamArena) -> u32 {
66
  weapon_fire::count_alive_worms(team_index, arena)
67
  }
68
 
crates/openwa-dll/src/snapshot.rs CHANGED
@@ -19,7 +19,7 @@ use openwa_core::task::{CTask, CTaskBfsIter, CTaskMissile, CTaskWorm};
19
  /// Must be called from the DLL while the game is paused (frame breakpoint).
20
  #[cfg(target_arch = "x86")]
21
  pub unsafe fn capture() -> String {
22
- use openwa_core::engine::TeamArenaRef;
23
 
24
  let mut out = String::with_capacity(128 * 1024);
25
 
@@ -62,26 +62,30 @@ pub unsafe fn capture() -> String {
62
  // ── Team blocks + worm entries ──
63
  let team_count = (*ddgame).team_arena.team_count as usize;
64
  let _ = writeln!(out, "[Teams] count={}", team_count);
65
- let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
66
- let blocks = arena.blocks();
67
  for t in 0..team_count {
68
- let header = arena.team_header(t);
69
  let name =
70
- core::ffi::CStr::from_ptr(header.team_name.as_ptr() as *const _).to_string_lossy();
71
  let _ = writeln!(out, "\n [Team {}] \"{}\"", t, name);
72
  let _ = writeln!(
73
  out,
74
  " eliminated={} alliance={} worm_count={} active_worm={}",
75
- header.eliminated, header.alliance, header.worm_count, header.active_worm
 
 
 
76
  );
77
  let _ = writeln!(
78
  out,
79
  " weapon_alliance={} turn_action_flags=0x{:08X}",
80
- header.weapon_alliance, header.turn_action_flags
 
81
  );
82
 
83
  // Worms are in block[t+1].worms[0..worm_count] (1-indexed blocks)
84
- let worm_count = header.worm_count.max(0) as usize;
85
  let block = &*blocks.add(t + 1);
86
  for wi in 0..worm_count.min(7) {
87
  let worm = &block.worms[wi];
 
19
  /// Must be called from the DLL while the game is paused (frame breakpoint).
20
  #[cfg(target_arch = "x86")]
21
  pub unsafe fn capture() -> String {
22
+ use openwa_core::engine::TeamArena;
23
 
24
  let mut out = String::with_capacity(128 * 1024);
25
 
 
62
  // ── Team blocks + worm entries ──
63
  let team_count = (*ddgame).team_arena.team_count as usize;
64
  let _ = writeln!(out, "[Teams] count={}", team_count);
65
+ let arena = &raw mut (*ddgame).team_arena;
66
+ let blocks = TeamArena::blocks_mut(arena);
67
  for t in 0..team_count {
68
+ let header = TeamArena::team_header_mut(arena, t);
69
  let name =
70
+ core::ffi::CStr::from_ptr((*header).team_name.as_ptr() as *const _).to_string_lossy();
71
  let _ = writeln!(out, "\n [Team {}] \"{}\"", t, name);
72
  let _ = writeln!(
73
  out,
74
  " eliminated={} alliance={} worm_count={} active_worm={}",
75
+ (*header).eliminated,
76
+ (*header).alliance,
77
+ (*header).worm_count,
78
+ (*header).active_worm
79
  );
80
  let _ = writeln!(
81
  out,
82
  " weapon_alliance={} turn_action_flags=0x{:08X}",
83
+ (*header).weapon_alliance,
84
+ (*header).turn_action_flags
85
  );
86
 
87
  // Worms are in block[t+1].worms[0..worm_count] (1-indexed blocks)
88
+ let worm_count = (*header).worm_count.max(0) as usize;
89
  let block = &*blocks.add(t + 1);
90
  for wi in 0..worm_count.min(7) {
91
  let worm = &block.worms[wi];