paavohtl commited on
Commit
531fddf
Β·
2 Parent(s): 8e90aec27b9967

Merge branch 'master' into worktree-debug-cli

Browse files
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -175,19 +175,14 @@ pub struct DDGame {
175
  /// Initialized to 0x10000 (1.0). Not read by the render method β€” may be
176
  /// target/previous value for interpolation, or used by update logic.
177
  pub team_health_ratio_2: [i32; 6],
178
- /// 0x4620-0x64D7: Unknown
179
- ///
180
- /// Known landmarks:
181
- /// - 0x64D8: cleared by init
182
- pub _unknown_4620: [u8; 0x64D8 - 0x4620],
183
- /// 0x64D8: Cleared by InitFields. Purpose unknown.
184
- pub init_field_64d8: u32,
185
- /// 0x64DC-0x72A3: Unknown
186
- pub _unknown_64dc: [u8; 0x72A4 - 0x64DC],
187
- /// 0x72A4: Cleared by InitFields. Purpose unknown.
188
- pub init_field_72a4: u32,
189
- /// 0x72A8-0x72D7: Unknown
190
- pub _unknown_72a8: [u8; 0x72D8 - 0x72A8],
191
 
192
  /// 0x72D8: Game speed multiplier (Fixed-point, 0x10000 = 1.0x).
193
  pub game_speed: i32,
@@ -415,8 +410,10 @@ pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
415
  *(base.add(off) as *mut u32) = 0;
416
  }
417
 
418
- (*ddgame).init_field_64d8 = 0;
419
- (*ddgame).init_field_72a4 = 0;
 
 
420
 
421
  // InitRenderIndices β€” original sets ESI = ddgame + 0x72D8, now uses typed DDGame ptr
422
  ddgame_init_render_indices(ddgame);
@@ -1431,17 +1428,17 @@ pub mod offsets {
1431
  /// GetAmmo/AddAmmo/SubtractAmmo.
1432
  pub const TEAM_ARENA_STATE: usize = 0x4628;
1433
 
1434
- // === Team block array (7 Γ— FullTeamBlock, stride 0x51C) ===
1435
  /// Start of team block array within DDGame (7 blocks, stride 0x51C).
1436
  /// Derived: entry_ptr(team=0) - 0x598 = 0x4628 - 0x598 = 0x4090.
1437
  /// Runtime-confirmed: block[0] is zeroed preamble, blocks[1-6] hold team data.
1438
  pub const TEAM_BLOCKS: usize = 0x4090;
1439
 
1440
- /// Byte offset from TeamArenaState base back to FullTeamBlock array start.
1441
- /// `blocks_ptr = (tws_base as *const c_char).sub(ARENA_TO_BLOCKS) as *const FullTeamBlock`
1442
  ///
1443
  /// entry_ptr(0) = DDGame+0x4628 = TEAM_BLOCKS + 0x598.
1444
- /// 0x598 = sizeof(FullTeamBlock) + 0x7C = one block + offset into TeamBlockHeader.
1445
  pub const ARENA_TO_BLOCKS: usize = 0x598;
1446
 
1447
  // === FUN_00526120 init offsets (stride 0x194, 10 entries) ===
@@ -1639,7 +1636,7 @@ const _: () = assert!(core::mem::size_of::<SoundQueueEntry>() == 0x24);
1639
  ///
1640
  /// WA supports up to 8 playable worms per team. The original code accesses
1641
  /// worms via raw pointer arithmetic from the team entry pointer, using
1642
- /// stride 0x9C. This means the 8th worm crosses the FullTeamBlock boundary
1643
  /// into the next block's header slot β€” see `TeamArenaRef::team_worm()`.
1644
  ///
1645
  /// Field offsets confirmed by runtime memory dump (validator DLL):
@@ -1678,7 +1675,7 @@ pub struct WormEntry {
1678
 
1679
  const _: () = assert!(core::mem::size_of::<WormEntry>() == 0x9C);
1680
 
1681
- /// Team-level metadata stored at slot 0 of each FullTeamBlock (0x9C bytes).
1682
  ///
1683
  /// This struct overlays the same memory as a WormEntry but interprets the
1684
  /// high offsets (0x6C+) as team metadata rather than worm data. The low
@@ -1688,9 +1685,9 @@ const _: () = assert!(core::mem::size_of::<WormEntry>() == 0x9C);
1688
  /// Accessed via `TeamArenaRef::team_header()` and `team_header_b()`.
1689
  #[repr(C)]
1690
  #[derive(Clone, Copy)]
1691
- pub struct TeamBlockHeader {
1692
  /// 0x00-0x5F: Opaque β€” may hold 8th worm data from previous team.
1693
- pub _worm_overlap: [u8; 0x60],
1694
  /// 0x60-0x6B: Unknown padding.
1695
  pub _unknown_60: [u8; 0x0C],
1696
  /// 0x6C: Team eliminated flag (nonzero = eliminated).
@@ -1701,8 +1698,11 @@ pub struct TeamBlockHeader {
1701
  pub active_worm: i32,
1702
  /// 0x78: Number of worms on this team.
1703
  pub worm_count: i32,
1704
- /// 0x7C: Unknown.
1705
- pub _unknown_7c: [u8; 4],
 
 
 
1706
  /// 0x80: Alliance ID for ammo/delay table indexing (GetAmmo/AddAmmo/SubtractAmmo).
1707
  /// Teams with the same weapon_alliance share ammo pools. Distinct from
1708
  /// `alliance` at 0x70 which is used by CountTeamsByAlliance.
@@ -1713,27 +1713,27 @@ pub struct TeamBlockHeader {
1713
  pub _unknown_98: [u8; 4],
1714
  }
1715
 
1716
- const _: () = assert!(core::mem::size_of::<TeamBlockHeader>() == 0x9C);
1717
 
1718
- /// Union for slot 0 of a FullTeamBlock.
1719
  ///
1720
  /// This slot is dual-purpose: its high offsets store team metadata
1721
- /// (`TeamBlockHeader`), while its low offsets may contain the 8th worm
1722
  /// of the previous team (`WormEntry`). The two uses don't conflict
1723
  /// because worm data occupies 0x00-0x5F and header data starts at 0x6C.
1724
  #[repr(C)]
1725
  #[derive(Clone, Copy)]
1726
- pub union TeamBlockSlot0 {
1727
  /// View as worm data (used when the previous team has 8 worms).
1728
  pub worm: core::mem::ManuallyDrop<WormEntry>,
1729
  /// View as team metadata (eliminated, alliance, worm_count, etc.).
1730
- pub team: core::mem::ManuallyDrop<TeamBlockHeader>,
1731
  }
1732
 
1733
  /// Full per-team data block (0x51C bytes, 7 blocks in DDGame).
1734
  ///
1735
- /// Each block starts with a `TeamBlockSlot0` union (0x9C bytes) that serves
1736
- /// dual purpose: its high offsets hold team metadata (`TeamBlockHeader`),
1737
  /// while its low offsets may contain the 8th worm of the previous team.
1738
  /// The remaining 7 worm slots follow, then 0x3C bytes of metadata.
1739
  ///
@@ -1742,19 +1742,19 @@ pub union TeamBlockSlot0 {
1742
  /// which uses raw pointer arithmetic and naturally crosses block boundaries.
1743
  ///
1744
  /// **Header access**: Use `TeamArenaRef::team_header(idx)` to get the
1745
- /// `TeamBlockHeader` for a team (reads from `blocks[idx+1].header.team`).
1746
  #[repr(C)]
1747
- pub struct FullTeamBlock {
1748
- /// 0x000-0x09B: Header slot (union of TeamBlockHeader and WormEntry).
1749
  /// Team metadata at high offsets; may hold 8th worm data at low offsets.
1750
- pub header: TeamBlockSlot0,
1751
  /// 0x09C-0x4DF: 7 worm entries (slots 1-7, stride 0x9C)
1752
  pub worms: [WormEntry; 7],
1753
  /// 0x4E0-0x51B: Team metadata (0x3C bytes)
1754
- pub _metadata: [u8; 0x3C],
1755
  }
1756
 
1757
- const _: () = assert!(core::mem::size_of::<FullTeamBlock>() == 0x51C);
1758
 
1759
  /// Team arena state area within DDGame (at DDGame + 0x4628).
1760
  ///
@@ -1769,8 +1769,26 @@ const _: () = assert!(core::mem::size_of::<FullTeamBlock>() == 0x51C);
1769
  /// The original code accesses these as two arrays at different base offsets
1770
  /// (0x1EB4 and 0x1FD0) using the same index `alliance_id * 142 + weapon_id`.
1771
  /// Since 0x1FD0 - 0x1EB4 = 71 * 4, this is equivalent to accessing
1772
- /// `weapon_slots[alliance * 142 + weapon]` for ammo and
1773
- /// `weapon_slots[alliance * 142 + 71 + weapon]` for delay.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1774
  #[repr(C)]
1775
  pub struct TeamArenaState {
1776
  /// 0x0000-0x1EAF: Per-team data region (opaque).
@@ -1778,20 +1796,19 @@ pub struct TeamArenaState {
1778
  /// This region contains 7 team entries at stride 0x51C (1-indexed, index 0
1779
  /// is preamble). The original WA.exe accesses team data via raw pointer
1780
  /// arithmetic relative to the arena base. In our Rust code, team data is
1781
- /// accessed through `TeamArenaRef`, `FullTeamBlock`, and `TeamBlockHeader`,
1782
  /// so this region is treated as opaque padding.
1783
  ///
1784
  /// The 7th entry (team 6, 1-indexed) only has 8 bytes before team_count;
1785
  /// the rest overlaps with weapon_slots.
1786
- pub _teams_region: [u8; 0x1EB0],
1787
  /// 0x1EB0: Number of teams in the game (used by team iteration loops)
1788
  pub team_count: i32,
1789
- /// 0x1EB4: Interleaved ammo/delay slots.
1790
- /// Per alliance: [ammo_0..ammo_70, delay_0..delay_70] = 142 i32 entries.
1791
- /// 6 alliances * 142 = 852 total entries.
1792
  /// Ammo: -1 = unlimited, 0 = none, >0 = count.
1793
  /// Delay: nonzero = weapon on delay.
1794
- pub weapon_slots: [i32; 852],
1795
  /// 0x2C04: Padding between weapon_slots end and game_mode_flag.
1796
  /// weapon_slots: 852 * 4 = 3408 = 0xD50. 0x1EB4 + 0xD50 = 0x2C04.
1797
  pub _pad_2c04: [u8; 8],
@@ -1851,20 +1868,19 @@ pub const GAME_PHASE_SUDDEN_DEATH: i32 = 0x1E4; // 484
1851
  pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
1852
 
1853
  impl TeamArenaState {
1854
- /// Get ammo count for a weapon slot (by flat index).
1855
- pub fn get_ammo(&self, index: usize) -> i32 {
1856
- self.weapon_slots[index]
1857
  }
1858
 
1859
- /// Get mutable reference to ammo count for a weapon slot.
1860
- pub fn ammo_mut(&mut self, index: usize) -> &mut i32 {
1861
- &mut self.weapon_slots[index]
1862
  }
1863
 
1864
- /// Get delay flag for a weapon slot (by flat index).
1865
- /// Delay is at +71 offset from the ammo index within the same alliance block.
1866
- pub fn get_delay(&self, index: usize) -> i32 {
1867
- self.weapon_slots[index + 71]
1868
  }
1869
  }
1870
 
@@ -1872,7 +1888,7 @@ impl TeamArenaState {
1872
  ///
1873
  /// Wraps the raw `base: u32` from trampoline register captures and provides
1874
  /// accessor methods that encapsulate the backward pointer arithmetic to reach
1875
- /// FullTeamBlock worm data. The FullTeamBlock array lives 0x598 bytes before
1876
  /// the TeamArenaState in DDGame memory.
1877
  ///
1878
  /// # Safety
@@ -1882,18 +1898,29 @@ impl TeamArenaState {
1882
  #[repr(transparent)]
1883
  #[derive(Clone, Copy)]
1884
  pub struct TeamArenaRef {
1885
- base: *const u8,
1886
  }
1887
 
1888
  impl TeamArenaRef {
1889
- /// Wrap a raw base pointer (for non-trampoline contexts like validation).
1890
  ///
1891
  /// # Safety
1892
- /// `base` must point to DDGame + TEAM_ARENA_STATE (0x4628).
1893
  #[inline]
1894
  pub unsafe fn from_raw(base: u32) -> Self {
1895
  Self {
1896
- base: base as *const u8,
 
 
 
 
 
 
 
 
 
 
 
1897
  }
1898
  }
1899
 
@@ -1909,10 +1936,10 @@ impl TeamArenaRef {
1909
  &mut *(self.base as *mut TeamArenaState)
1910
  }
1911
 
1912
- /// Get pointer to the FullTeamBlock array base.
1913
  #[inline]
1914
- pub unsafe fn blocks(&self) -> *const FullTeamBlock {
1915
- self.base.sub(offsets::ARENA_TO_BLOCKS) as *const FullTeamBlock
1916
  }
1917
 
1918
  /// Get the team header (metadata) for a team.
@@ -1920,15 +1947,21 @@ impl TeamArenaRef {
1920
  /// Returns `&block[team_idx+1].header.team`, which holds team metadata:
1921
  /// worm_count, eliminated flag, weapon_alliance, team_name.
1922
  #[inline]
1923
- pub unsafe fn team_header(&self, team_idx: usize) -> &TeamBlockHeader {
1924
  &(*self.blocks().add(team_idx + 1)).header.team
1925
  }
1926
 
 
 
 
 
 
 
1927
  /// Get a playable worm entry by 1-indexed worm number (1..=8).
1928
  ///
1929
  /// Uses raw pointer arithmetic matching the original WA code:
1930
  /// `base + team_idx * 0x51C + worm_num * 0x9C - 0x598`.
1931
- /// This naturally crosses FullTeamBlock boundaries when worm_num = 8,
1932
  /// since the 8th worm's early fields (state, health) spill into the
1933
  /// next block's header slot. The header metadata lives at high offsets
1934
  /// (0x6C+) that don't conflict with worm data (0x00-0x5F).
@@ -1951,7 +1984,7 @@ impl TeamArenaRef {
1951
  /// **Note**: For accessing worms, prefer `team_worm()` which handles
1952
  /// 8-worm teams correctly via cross-boundary pointer arithmetic.
1953
  #[inline]
1954
- pub unsafe fn team_and_header(&self, team_idx: usize) -> (&FullTeamBlock, &TeamBlockHeader) {
1955
  let blocks = self.blocks();
1956
  let block = &*blocks.add(team_idx);
1957
  let header = &(*blocks.add(team_idx + 1)).header.team;
@@ -1963,14 +1996,14 @@ impl TeamArenaRef {
1963
  /// Pattern B indexes from block[i+2] for 0-indexed team `i`:
1964
  /// `base + 0x510 + i*0x51C` = `blocks[i+2].header.team + 0x70`.
1965
  #[inline]
1966
- pub unsafe fn team_header_b(&self, team_idx: usize) -> &TeamBlockHeader {
1967
  &(*self.blocks().add(team_idx + 2)).header.team
1968
  }
1969
 
1970
  /// Get mutable team header for Pattern B access.
1971
  #[inline]
1972
- pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamBlockHeader {
1973
- &mut (*(self.blocks() as *mut FullTeamBlock).add(team_idx + 2))
1974
  .header
1975
  .team
1976
  }
@@ -1984,9 +2017,10 @@ impl TeamArenaRef {
1984
  /// then 71 delay slots (stride 142 per alliance).
1985
  /// Ammo: `weapon_slots[alliance_id * 142 + weapon_id]`
1986
  /// Delay: `weapon_slots[alliance_id * 142 + 71 + weapon_id]`
 
1987
  #[inline]
1988
- pub unsafe fn ammo_index(&self, team_index: usize, weapon_id: u32) -> usize {
1989
  let alliance_id = self.team_header(team_index).weapon_alliance as usize;
1990
- alliance_id * 142 + weapon_id as usize
1991
  }
1992
  }
 
175
  /// Initialized to 0x10000 (1.0). Not read by the render method β€” may be
176
  /// target/previous value for interpolation, or used by update logic.
177
  pub team_health_ratio_2: [i32; 6],
178
+ /// 0x4620-0x4627: Unknown
179
+ pub _unknown_4620: [u8; 0x4628 - 0x4620],
180
+ /// 0x4628: Team arena state β€” per-team data, ammo, delays, alliance tracking.
181
+ /// Note: fields previously named init_field_64d8 (= team_count at arena+0x1EB0)
182
+ /// and init_field_72a4 (= weapon_slots entry at arena+0x2A7C) are inside this struct.
183
+ pub team_arena: TeamArenaState,
184
+ /// 0x7270-0x72D7: Unknown
185
+ pub _unknown_7270: [u8; 0x72D8 - 0x7270],
 
 
 
 
 
186
 
187
  /// 0x72D8: Game speed multiplier (Fixed-point, 0x10000 = 1.0x).
188
  pub game_speed: i32,
 
410
  *(base.add(off) as *mut u32) = 0;
411
  }
412
 
413
+ // init_field_64d8 = TeamArenaState.team_count (arena+0x1EB0)
414
+ (*ddgame).team_arena.team_count = 0;
415
+ // init_field_72a4 = weapon_slots flat[754] = alliance 5, ammo[44]
416
+ (*ddgame).team_arena.weapon_slots.teams[5].ammo[44] = 0;
417
 
418
  // InitRenderIndices β€” original sets ESI = ddgame + 0x72D8, now uses typed DDGame ptr
419
  ddgame_init_render_indices(ddgame);
 
1428
  /// GetAmmo/AddAmmo/SubtractAmmo.
1429
  pub const TEAM_ARENA_STATE: usize = 0x4628;
1430
 
1431
+ // === Team block array (7 Γ— TeamBlock, stride 0x51C) ===
1432
  /// Start of team block array within DDGame (7 blocks, stride 0x51C).
1433
  /// Derived: entry_ptr(team=0) - 0x598 = 0x4628 - 0x598 = 0x4090.
1434
  /// Runtime-confirmed: block[0] is zeroed preamble, blocks[1-6] hold team data.
1435
  pub const TEAM_BLOCKS: usize = 0x4090;
1436
 
1437
+ /// Byte offset from TeamArenaState base back to TeamBlock array start.
1438
+ /// `blocks_ptr = (tws_base as *const c_char).sub(ARENA_TO_BLOCKS) as *const TeamBlock`
1439
  ///
1440
  /// entry_ptr(0) = DDGame+0x4628 = TEAM_BLOCKS + 0x598.
1441
+ /// 0x598 = sizeof(TeamBlock) + 0x7C = one block + offset into TeamHeader.
1442
  pub const ARENA_TO_BLOCKS: usize = 0x598;
1443
 
1444
  // === FUN_00526120 init offsets (stride 0x194, 10 entries) ===
 
1636
  ///
1637
  /// WA supports up to 8 playable worms per team. The original code accesses
1638
  /// worms via raw pointer arithmetic from the team entry pointer, using
1639
+ /// stride 0x9C. This means the 8th worm crosses the TeamBlock boundary
1640
  /// into the next block's header slot β€” see `TeamArenaRef::team_worm()`.
1641
  ///
1642
  /// Field offsets confirmed by runtime memory dump (validator DLL):
 
1675
 
1676
  const _: () = assert!(core::mem::size_of::<WormEntry>() == 0x9C);
1677
 
1678
+ /// Team-level metadata stored at slot 0 of each TeamBlock (0x9C bytes).
1679
  ///
1680
  /// This struct overlays the same memory as a WormEntry but interprets the
1681
  /// high offsets (0x6C+) as team metadata rather than worm data. The low
 
1685
  /// Accessed via `TeamArenaRef::team_header()` and `team_header_b()`.
1686
  #[repr(C)]
1687
  #[derive(Clone, Copy)]
1688
+ pub struct TeamHeader {
1689
  /// 0x00-0x5F: Opaque β€” may hold 8th worm data from previous team.
1690
+ pub worm_overlap: [u8; 0x60],
1691
  /// 0x60-0x6B: Unknown padding.
1692
  pub _unknown_60: [u8; 0x0C],
1693
  /// 0x6C: Team eliminated flag (nonzero = eliminated).
 
1698
  pub active_worm: i32,
1699
  /// 0x78: Number of worms on this team.
1700
  pub worm_count: i32,
1701
+ /// 0x7C: Per-team turn action flags (bitfield).
1702
+ /// Skip Go (weapon 57) toggles a bit here based on the weapon's fire_params.
1703
+ /// Bit is set to mark the team should skip; in game_version > 0x1C, toggling
1704
+ /// again clears it.
1705
+ pub turn_action_flags: u32,
1706
  /// 0x80: Alliance ID for ammo/delay table indexing (GetAmmo/AddAmmo/SubtractAmmo).
1707
  /// Teams with the same weapon_alliance share ammo pools. Distinct from
1708
  /// `alliance` at 0x70 which is used by CountTeamsByAlliance.
 
1713
  pub _unknown_98: [u8; 4],
1714
  }
1715
 
1716
+ const _: () = assert!(core::mem::size_of::<TeamHeader>() == 0x9C);
1717
 
1718
+ /// Union for slot 0 of a TeamBlock.
1719
  ///
1720
  /// This slot is dual-purpose: its high offsets store team metadata
1721
+ /// (`TeamHeader`), while its low offsets may contain the 8th worm
1722
  /// of the previous team (`WormEntry`). The two uses don't conflict
1723
  /// because worm data occupies 0x00-0x5F and header data starts at 0x6C.
1724
  #[repr(C)]
1725
  #[derive(Clone, Copy)]
1726
+ pub union TeamSlot0 {
1727
  /// View as worm data (used when the previous team has 8 worms).
1728
  pub worm: core::mem::ManuallyDrop<WormEntry>,
1729
  /// View as team metadata (eliminated, alliance, worm_count, etc.).
1730
+ pub team: core::mem::ManuallyDrop<TeamHeader>,
1731
  }
1732
 
1733
  /// Full per-team data block (0x51C bytes, 7 blocks in DDGame).
1734
  ///
1735
+ /// Each block starts with a `TeamSlot0` union (0x9C bytes) that serves
1736
+ /// dual purpose: its high offsets hold team metadata (`TeamHeader`),
1737
  /// while its low offsets may contain the 8th worm of the previous team.
1738
  /// The remaining 7 worm slots follow, then 0x3C bytes of metadata.
1739
  ///
 
1742
  /// which uses raw pointer arithmetic and naturally crosses block boundaries.
1743
  ///
1744
  /// **Header access**: Use `TeamArenaRef::team_header(idx)` to get the
1745
+ /// `TeamHeader` for a team (reads from `blocks[idx+1].header.team`).
1746
  #[repr(C)]
1747
+ pub struct TeamBlock {
1748
+ /// 0x000-0x09B: Header slot (union of TeamHeader and WormEntry).
1749
  /// Team metadata at high offsets; may hold 8th worm data at low offsets.
1750
+ pub header: TeamSlot0,
1751
  /// 0x09C-0x4DF: 7 worm entries (slots 1-7, stride 0x9C)
1752
  pub worms: [WormEntry; 7],
1753
  /// 0x4E0-0x51B: Team metadata (0x3C bytes)
1754
+ pub trailer: [u8; 0x3C],
1755
  }
1756
 
1757
+ const _: () = assert!(core::mem::size_of::<TeamBlock>() == 0x51C);
1758
 
1759
  /// Team arena state area within DDGame (at DDGame + 0x4628).
1760
  ///
 
1769
  /// The original code accesses these as two arrays at different base offsets
1770
  /// (0x1EB4 and 0x1FD0) using the same index `alliance_id * 142 + weapon_id`.
1771
  /// Since 0x1FD0 - 0x1EB4 = 71 * 4, this is equivalent to accessing
1772
+ /// `weapon_slots.teams[alliance].ammo[weapon]` and
1773
+ /// `weapon_slots.teams[alliance].delay[weapon]`.
1774
+
1775
+ /// Per-alliance weapon ammo and delay data (0x238 = 568 bytes per alliance).
1776
+ #[repr(C)]
1777
+ pub struct TeamWeaponSlots {
1778
+ /// Ammo counts per weapon (71 entries). -1 = unlimited, 0 = none, >0 = count.
1779
+ pub ammo: [i32; 71],
1780
+ /// Delay flags per weapon (71 entries). Nonzero = weapon on cooldown.
1781
+ pub delay: [i32; 71],
1782
+ }
1783
+ const _: () = assert!(core::mem::size_of::<TeamWeaponSlots>() == 0x238);
1784
+
1785
+ /// Weapon slot data for all 6 alliances (0xD50 = 3408 bytes total).
1786
+ #[repr(C)]
1787
+ pub struct WeaponSlots {
1788
+ pub teams: [TeamWeaponSlots; 6],
1789
+ }
1790
+ const _: () = assert!(core::mem::size_of::<WeaponSlots>() == 852 * 4);
1791
+
1792
  #[repr(C)]
1793
  pub struct TeamArenaState {
1794
  /// 0x0000-0x1EAF: Per-team data region (opaque).
 
1796
  /// This region contains 7 team entries at stride 0x51C (1-indexed, index 0
1797
  /// is preamble). The original WA.exe accesses team data via raw pointer
1798
  /// arithmetic relative to the arena base. In our Rust code, team data is
1799
+ /// accessed through `TeamArenaRef`, `TeamBlock`, and `TeamHeader`,
1800
  /// so this region is treated as opaque padding.
1801
  ///
1802
  /// The 7th entry (team 6, 1-indexed) only has 8 bytes before team_count;
1803
  /// the rest overlaps with weapon_slots.
1804
+ pub team_blocks_region: [u8; 0x1EB0],
1805
  /// 0x1EB0: Number of teams in the game (used by team iteration loops)
1806
  pub team_count: i32,
1807
+ /// 0x1EB4: Per-alliance weapon ammo and delay data.
1808
+ /// 6 alliances, each with 71 ammo + 71 delay i32 entries.
 
1809
  /// Ammo: -1 = unlimited, 0 = none, >0 = count.
1810
  /// Delay: nonzero = weapon on delay.
1811
+ pub weapon_slots: WeaponSlots,
1812
  /// 0x2C04: Padding between weapon_slots end and game_mode_flag.
1813
  /// weapon_slots: 852 * 4 = 3408 = 0xD50. 0x1EB4 + 0xD50 = 0x2C04.
1814
  pub _pad_2c04: [u8; 8],
 
1868
  pub const GAME_PHASE_NORMAL_MIN: i32 = -2;
1869
 
1870
  impl TeamArenaState {
1871
+ /// Get ammo count for a weapon by alliance and weapon ID.
1872
+ pub fn get_ammo(&self, alliance: usize, weapon_id: usize) -> i32 {
1873
+ self.weapon_slots.teams[alliance].ammo[weapon_id]
1874
  }
1875
 
1876
+ /// Get mutable reference to ammo count.
1877
+ pub fn ammo_mut(&mut self, alliance: usize, weapon_id: usize) -> &mut i32 {
1878
+ &mut self.weapon_slots.teams[alliance].ammo[weapon_id]
1879
  }
1880
 
1881
+ /// Get delay flag for a weapon by alliance and weapon ID.
1882
+ pub fn get_delay(&self, alliance: usize, weapon_id: usize) -> i32 {
1883
+ self.weapon_slots.teams[alliance].delay[weapon_id]
 
1884
  }
1885
  }
1886
 
 
1888
  ///
1889
  /// Wraps the raw `base: u32` from trampoline register captures and provides
1890
  /// accessor methods that encapsulate the backward pointer arithmetic to reach
1891
+ /// TeamBlock worm data. The TeamBlock array lives 0x598 bytes before
1892
  /// the TeamArenaState in DDGame memory.
1893
  ///
1894
  /// # Safety
 
1898
  #[repr(transparent)]
1899
  #[derive(Clone, Copy)]
1900
  pub struct TeamArenaRef {
1901
+ base: *mut u8,
1902
  }
1903
 
1904
  impl TeamArenaRef {
1905
+ /// Wrap a raw integer pointer (for usercall trampoline register captures).
1906
  ///
1907
  /// # Safety
1908
+ /// `base` must point to a valid TeamArenaState (DDGame + 0x4628).
1909
  #[inline]
1910
  pub unsafe fn from_raw(base: u32) -> Self {
1911
  Self {
1912
+ base: base as *mut u8,
1913
+ }
1914
+ }
1915
+
1916
+ /// Wrap a typed pointer to TeamArenaState.
1917
+ ///
1918
+ /// # Safety
1919
+ /// `arena` must point to a valid TeamArenaState within a live DDGame.
1920
+ #[inline]
1921
+ pub unsafe fn from_ptr(arena: *mut TeamArenaState) -> Self {
1922
+ Self {
1923
+ base: arena as *mut u8,
1924
  }
1925
  }
1926
 
 
1936
  &mut *(self.base as *mut TeamArenaState)
1937
  }
1938
 
1939
+ /// Get pointer to the TeamBlock array base.
1940
  #[inline]
1941
+ pub unsafe fn blocks(&self) -> *mut TeamBlock {
1942
+ self.base.sub(offsets::ARENA_TO_BLOCKS) as *mut TeamBlock
1943
  }
1944
 
1945
  /// Get the team header (metadata) for a team.
 
1947
  /// Returns `&block[team_idx+1].header.team`, which holds team metadata:
1948
  /// worm_count, eliminated flag, weapon_alliance, team_name.
1949
  #[inline]
1950
+ pub unsafe fn team_header(&self, team_idx: usize) -> &TeamHeader {
1951
  &(*self.blocks().add(team_idx + 1)).header.team
1952
  }
1953
 
1954
+ /// Get a mutable team header for a team.
1955
+ #[inline]
1956
+ pub unsafe fn team_header_mut(&self, team_idx: usize) -> &mut TeamHeader {
1957
+ &mut (*self.blocks().add(team_idx + 1)).header.team
1958
+ }
1959
+
1960
  /// Get a playable worm entry by 1-indexed worm number (1..=8).
1961
  ///
1962
  /// Uses raw pointer arithmetic matching the original WA code:
1963
  /// `base + team_idx * 0x51C + worm_num * 0x9C - 0x598`.
1964
+ /// This naturally crosses TeamBlock boundaries when worm_num = 8,
1965
  /// since the 8th worm's early fields (state, health) spill into the
1966
  /// next block's header slot. The header metadata lives at high offsets
1967
  /// (0x6C+) that don't conflict with worm data (0x00-0x5F).
 
1984
  /// **Note**: For accessing worms, prefer `team_worm()` which handles
1985
  /// 8-worm teams correctly via cross-boundary pointer arithmetic.
1986
  #[inline]
1987
+ pub unsafe fn team_and_header(&self, team_idx: usize) -> (&TeamBlock, &TeamHeader) {
1988
  let blocks = self.blocks();
1989
  let block = &*blocks.add(team_idx);
1990
  let header = &(*blocks.add(team_idx + 1)).header.team;
 
1996
  /// Pattern B indexes from block[i+2] for 0-indexed team `i`:
1997
  /// `base + 0x510 + i*0x51C` = `blocks[i+2].header.team + 0x70`.
1998
  #[inline]
1999
+ pub unsafe fn team_header_b(&self, team_idx: usize) -> &TeamHeader {
2000
  &(*self.blocks().add(team_idx + 2)).header.team
2001
  }
2002
 
2003
  /// Get mutable team header for Pattern B access.
2004
  #[inline]
2005
+ pub unsafe fn team_header_b_mut(&self, team_idx: usize) -> &mut TeamHeader {
2006
+ &mut (*(self.blocks() as *mut TeamBlock).add(team_idx + 2))
2007
  .header
2008
  .team
2009
  }
 
2017
  /// then 71 delay slots (stride 142 per alliance).
2018
  /// Ammo: `weapon_slots[alliance_id * 142 + weapon_id]`
2019
  /// Delay: `weapon_slots[alliance_id * 142 + 71 + weapon_id]`
2020
+ /// Returns (alliance_id, weapon_id) for accessing weapon slots.
2021
  #[inline]
2022
+ pub unsafe fn weapon_slot_key(&self, team_index: usize, weapon_id: u32) -> (usize, usize) {
2023
  let alliance_id = self.team_header(team_index).weapon_alliance as usize;
2024
+ (alliance_id, weapon_id as usize)
2025
  }
2026
  }
crates/openwa-core/src/engine/game_state_init.rs CHANGED
@@ -385,11 +385,11 @@ pub unsafe fn check_weapon_avail(ddgame: *mut DDGame, weapon_index: u32) -> i32
385
  _ => {}
386
  }
387
 
388
- // Step 2: Branch on weapon name pointer (non-null = weapon defined).
389
  let weapon_table = (*ddgame).weapon_table;
390
- let name_ptr = (*weapon_table).entries[weapon_index as usize].name1;
391
 
392
- if (*ddgame).level_width_raw == 0 || !name_ptr.is_null() {
393
  // Main path: check super weapon flag
394
  let super_result = is_super_weapon(weapon_index, (*ddgame).version_flag_3);
395
  if super_result != 0 && (*gi).super_weapon_allowed == 0 {
 
385
  _ => {}
386
  }
387
 
388
+ // Step 2: Branch on weapon defined flag (nonzero = weapon exists in table).
389
  let weapon_table = (*ddgame).weapon_table;
390
+ let defined = (*weapon_table).entries[weapon_index as usize].defined;
391
 
392
+ if (*ddgame).level_width_raw == 0 || defined != 0 {
393
  // Main path: check super weapon flag
394
  let super_result = is_super_weapon(weapon_index, (*ddgame).version_flag_3);
395
  if super_result != 0 && (*gi).super_weapon_allowed == 0 {
crates/openwa-core/src/engine/mod.rs CHANGED
@@ -9,8 +9,8 @@ pub mod net_bridge;
9
  pub mod net_wrapper;
10
 
11
  pub use ddgame::{
12
- DDGame, FullTeamBlock, RenderEntry, SoundQueueEntry, TeamArenaRef, TeamArenaState,
13
- TeamBlockHeader, TeamBlockSlot0, TeamIndexMap, WormEntry, GAME_PHASE_NORMAL_MIN,
14
  GAME_PHASE_SUDDEN_DEATH,
15
  };
16
  pub use ddgame_wrapper::DDGameWrapper;
 
9
  pub mod net_wrapper;
10
 
11
  pub use ddgame::{
12
+ DDGame, RenderEntry, SoundQueueEntry, TeamArenaRef, TeamArenaState, TeamBlock,
13
+ TeamHeader, TeamIndexMap, TeamSlot0, WormEntry, GAME_PHASE_NORMAL_MIN,
14
  GAME_PHASE_SUDDEN_DEATH,
15
  };
16
  pub use ddgame_wrapper::DDGameWrapper;
crates/openwa-core/src/game/weapon.rs CHANGED
@@ -104,35 +104,61 @@ impl TryFrom<u32> for Weapon {
104
  /// 71 standard entries (indices 0..70), matching the `Weapon` enum.
105
  /// Source: wkJellyWorm/src/CustomWeapons.h (WeaponStruct).
106
  ///
107
- /// Only 3 fields are confirmed; the rest is opaque. wkJellyWorm copies
108
- /// entire entries via memcpy when creating custom weapons.
109
  #[repr(C)]
110
  pub struct WeaponEntry {
111
  /// +0x00: Pointer to primary weapon name string.
112
- /// Non-null means the weapon is defined/available.
113
  pub name1: *const c_char,
114
  /// +0x04: Pointer to secondary weapon name string.
115
  pub name2: *const c_char,
116
- /// +0x08: Weapon panel row index (0..12).
117
- pub panel_row: i32,
118
  /// +0x0C: Unknown.
119
  pub _unknown_0c: i32,
120
- /// +0x10-0x1CF: Unknown fields (113 Γ— i32).
121
- pub _unknown_10: [u8; 0x1D0 - 0x10],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
  const _: () = assert!(core::mem::size_of::<WeaponEntry>() == 0x1D0);
124
 
125
- /// Weapon table header (0x10 = 16 bytes before the first entry).
126
  ///
127
- /// The table is allocated by `InitWeaponTable` (0x53CAB0) and stored
128
- /// at DDGame+0x510. Layout: 16-byte header + 71 Γ— WeaponEntry.
 
 
 
 
 
 
 
 
 
 
 
129
  #[repr(C)]
130
  pub struct WeaponTable {
131
- /// +0x00-0x0F: Header (purpose unknown).
132
- pub _header: [u8; 0x10],
133
- /// +0x10: Weapon entries array (71 standard weapons).
134
  pub entries: [WeaponEntry; 71],
135
  }
136
- const _: () = assert!(
137
- core::mem::size_of::<WeaponTable>() == 0x10 + 71 * 0x1D0
138
- );
 
104
  /// 71 standard entries (indices 0..70), matching the `Weapon` enum.
105
  /// Source: wkJellyWorm/src/CustomWeapons.h (WeaponStruct).
106
  ///
107
+ /// Known fields from wkJellyWorm and InitWeaponTable (0x53CAB0) analysis.
108
+ /// wkJellyWorm copies entire entries via memcpy when creating custom weapons.
109
  #[repr(C)]
110
  pub struct WeaponEntry {
111
  /// +0x00: Pointer to primary weapon name string.
 
112
  pub name1: *const c_char,
113
  /// +0x04: Pointer to secondary weapon name string.
114
  pub name2: *const c_char,
115
+ /// +0x08: Panel state (init: 0xFFFFFFFF). wkJellyWorm calls this `panelRow`.
116
+ pub panel_state: i32,
117
  /// +0x0C: Unknown.
118
  pub _unknown_0c: i32,
119
+ /// +0x10: Weapon defined flag. Nonzero = weapon exists in table.
120
+ /// Checked by DDGame__CheckWeaponAvail to determine if weapon is valid.
121
+ pub defined: i32,
122
+ /// +0x14-0x23: Unknown.
123
+ pub _unknown_14: [u8; 0x24 - 0x14],
124
+ /// +0x24: Availability flag. Init: 0xFFFFFFFF, then set to 0 (unavailable)
125
+ /// or 1 (available) per weapon. Weapon::None, SkipGo, Surrender default to 0.
126
+ pub availability: i32,
127
+ /// +0x28: Enabled flag (init: 1).
128
+ pub enabled: i32,
129
+ /// +0x2C-0x2F: Unknown.
130
+ pub _unknown_2c: [u8; 4],
131
+ /// +0x30: Weapon fire type (1=projectile, 2=rope, 3=grenade, 4=special).
132
+ /// Read by FireWeapon to dispatch to the correct handler.
133
+ pub fire_type: i32,
134
+ /// +0x34: Fire subtype for weapon types 3 (grenade/mortar) and 4 (special).
135
+ pub fire_subtype_34: i32,
136
+ /// +0x38: Fire subtype for weapon types 1 (projectile) and 2 (rope).
137
+ pub fire_subtype_38: i32,
138
+ /// +0x3C: Fire parameters sub-structure. Pointer to this field is passed
139
+ /// to fire sub-functions (PlacedExplosive, Projectile, CreateWeaponProjectile, etc.).
140
+ pub fire_params: WeaponFireParams,
141
  }
142
  const _: () = assert!(core::mem::size_of::<WeaponEntry>() == 0x1D0);
143
 
144
+ /// Weapon fire parameters β€” embedded at WeaponEntry+0x3C.
145
  ///
146
+ /// Pointer to this struct is passed to all fire dispatch sub-functions.
147
+ /// Internal layout is mostly unknown (0x194 bytes).
148
+ #[repr(C)]
149
+ pub struct WeaponFireParams {
150
+ /// +0x00 (WeaponEntry+0x3C): First param DWORD.
151
+ pub _data: [u8; 0x1D0 - 0x3C],
152
+ }
153
+ const _: () = assert!(core::mem::size_of::<WeaponFireParams>() == 0x1D0 - 0x3C);
154
+
155
+ /// Weapon table β€” flat array of 71 entries, no header.
156
+ ///
157
+ /// Allocated by `InitWeaponTable` (0x53CAB0), stored at DDGame+0x510.
158
+ /// Total size: 71 Γ— 0x1D0 = 0x80B0 bytes.
159
  #[repr(C)]
160
  pub struct WeaponTable {
161
+ /// Weapon entries array (71 standard weapons, indices 0..70).
 
 
162
  pub entries: [WeaponEntry; 71],
163
  }
164
+ const _: () = assert!(core::mem::size_of::<WeaponTable>() == 71 * 0x1D0);
 
 
crates/openwa-core/src/task/base.rs CHANGED
@@ -139,6 +139,25 @@ impl SharedDataTable {
139
  h
140
  }
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  /// Iterate all nodes across all 256 buckets.
143
  ///
144
  /// # Safety
 
139
  h
140
  }
141
 
142
+ /// Look up an entity by key pair. Returns the entity pointer, or null.
143
+ ///
144
+ /// Pure Rust equivalent of `FUN_004FDF90` (SharedData__Lookup).
145
+ /// fastcall(ECX=key_esi, EDX=key_edi, stack=task) in the original.
146
+ ///
147
+ /// # Safety
148
+ /// The table and all linked nodes must be valid.
149
+ pub unsafe fn lookup(&self, key_esi: u32, key_edi: u32) -> *mut u8 {
150
+ let bucket = Self::bucket_for(key_esi, key_edi) as usize;
151
+ let mut node = *self.buckets.add(bucket);
152
+ while !node.is_null() {
153
+ if (*node).key_edi == key_edi && (*node).key_esi == key_esi {
154
+ return (*node).entity;
155
+ }
156
+ node = (*node).next;
157
+ }
158
+ core::ptr::null_mut()
159
+ }
160
+
161
  /// Iterate all nodes across all 256 buckets.
162
  ///
163
  /// # Safety
crates/openwa-core/src/task/game_task.rs CHANGED
@@ -13,8 +13,15 @@ use crate::fixed::Fixed;
13
  pub struct CGameTask {
14
  /// 0x00-0x2F: Base CTask fields
15
  pub base: CTask,
16
- /// 0x30-0x83: Unknown gameplay fields (84 bytes)
17
- pub _unknown_30: [u8; 0x54],
 
 
 
 
 
 
 
18
  /// 0x84: X position in fixed-point
19
  pub pos_x: Fixed,
20
  /// 0x88: Y position in fixed-point
 
13
  pub struct CGameTask {
14
  /// 0x00-0x2F: Base CTask fields
15
  pub base: CTask,
16
+ /// 0x30-0x83: Subclass-specific data (84 bytes). Each CGameTask derivative
17
+ /// uses this region differently:
18
+ /// - CTaskWorm: weapon fire state (+0x30 type, +0x34/+0x38 subtypes, +0x3C flag)
19
+ /// - CTaskFilter: boolean message subscription table (+0x30..+0x93)
20
+ /// - CTaskMissile: spawn/physics parameters
21
+ /// - CTaskTeam: secondary vtable pointer (+0x30)
22
+ /// - CTaskCloud: parallax scroll depth (+0x30)
23
+ /// Access via subclass accessor methods, not directly.
24
+ pub subclass_data: [u8; 0x54],
25
  /// 0x84: X position in fixed-point
26
  pub pos_x: Fixed,
27
  /// 0x88: Y position in fixed-point
crates/openwa-core/src/task/mod.rs CHANGED
@@ -3,14 +3,14 @@ pub mod bit_grid;
3
  mod cloud;
4
  mod filter;
5
  mod fire;
6
- mod game_task;
7
  mod mine_oil_drum;
8
  mod missile;
9
  mod overlays;
10
  mod supply_crate;
11
  mod team;
12
  mod turn_game;
13
- mod worm;
14
 
15
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
16
  pub use cloud::CTaskCloud;
 
3
  mod cloud;
4
  mod filter;
5
  mod fire;
6
+ pub mod game_task;
7
  mod mine_oil_drum;
8
  mod missile;
9
  mod overlays;
10
  mod supply_crate;
11
  mod team;
12
  mod turn_game;
13
+ pub mod worm;
14
 
15
  pub use base::{CTask, CTaskBfsIter, SharedDataIter, SharedDataNode, SharedDataTable};
16
  pub use cloud::CTaskCloud;
crates/openwa-core/src/task/worm.rs CHANGED
@@ -1,5 +1,6 @@
1
  use super::base::CTask;
2
  use super::game_task::CGameTask;
 
3
 
4
  /// Virtual method table for CTaskWorm (vtable at 0x6644C8, 20 slots).
5
  ///
@@ -75,7 +76,7 @@ const _: () = assert!(core::mem::size_of::<CTaskWormVTable>() == 20 * 4);
75
  /// Vtable at 0x6644C8. Class type byte: 0x12.
76
  ///
77
  /// # Important fields in the CGameTask base
78
- /// The worm state field lives at **offset +0x44** (inside `base._unknown_30`).
79
  /// Use [`CTaskWorm::state`] to read it without pointer arithmetic.
80
  ///
81
  /// Source: Ghidra decompilation of 0x50BFB0, vtable analysis of 0x6644C8,
@@ -151,8 +152,12 @@ pub struct CTaskWorm {
151
  pub _unknown_338: [u8; 0x368 - 0x338],
152
  /// 0x368: Animator / controller object (dispatched via vtable for state animations)
153
  pub animator: *mut u8,
154
- /// 0x36C–0x3DB: Unknown (rope anchor, weapon-specific data, etc.)
155
- pub _unknown_36c: [u8; 0x3DC - 0x36C],
 
 
 
 
156
  /// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
157
  pub facing_direction: i32,
158
  /// 0x3E0–0x3E3: Unknown
@@ -172,17 +177,30 @@ pub struct CTaskWorm {
172
  const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
173
 
174
  impl CTaskWorm {
175
- /// Returns the worm's current state code (lives at offset +0x44, inside the
176
- /// CGameTask base's `_unknown_30` padding region).
177
  ///
178
  /// Known states: `0x65`=idle, `0x67`=active turn, `0x7F`=drowning,
179
  /// `0x80`=hurt, `0x81`/`0x86`=dead, `0x87`=dead variant, `0x8B`=unknown.
180
  pub fn state(&self) -> u32 {
181
- // SAFETY: offset 0x44 is within CGameTask._unknown_30 (0x30..0x84).
182
- // Aligned to 4 bytes; repr(C) guarantees no reordering.
183
  unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
184
  }
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  /// Returns a reference to the vtable.
187
  ///
188
  /// # Safety
 
1
  use super::base::CTask;
2
  use super::game_task::CGameTask;
3
+ use crate::game::weapon::WeaponEntry;
4
 
5
  /// Virtual method table for CTaskWorm (vtable at 0x6644C8, 20 slots).
6
  ///
 
76
  /// Vtable at 0x6644C8. Class type byte: 0x12.
77
  ///
78
  /// # Important fields in the CGameTask base
79
+ /// The worm state field lives at **offset +0x44** (inside `base.subclass_data`).
80
  /// Use [`CTaskWorm::state`] to read it without pointer arithmetic.
81
  ///
82
  /// Source: Ghidra decompilation of 0x50BFB0, vtable analysis of 0x6644C8,
 
152
  pub _unknown_338: [u8; 0x368 - 0x338],
153
  /// 0x368: Animator / controller object (dispatched via vtable for state animations)
154
  pub animator: *mut u8,
155
+ /// 0x36C: Active weapon entry pointer. Points to `&WeaponTable.entries[selected_weapon]`.
156
+ /// Contains fire type (+0x30), subtypes (+0x34/+0x38), and completion flag (+0x3C).
157
+ /// Used by WeaponRelease: `MOV EAX, [EDI+0x36C]` before calling FireWeapon.
158
+ pub active_weapon_entry: *mut WeaponEntry,
159
+ /// 0x370–0x3DB: Unknown (rope anchor, weapon-specific data, etc.)
160
+ pub _unknown_370: [u8; 0x3DC - 0x370],
161
  /// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
162
  pub facing_direction: i32,
163
  /// 0x3E0–0x3E3: Unknown
 
177
  const _: () = assert!(core::mem::size_of::<CTaskWorm>() == 0x3FC);
178
 
179
  impl CTaskWorm {
180
+ /// Returns the worm's current state code (lives at offset +0x44, inside
181
+ /// `base.subclass_data`).
182
  ///
183
  /// Known states: `0x65`=idle, `0x67`=active turn, `0x7F`=drowning,
184
  /// `0x80`=hurt, `0x81`/`0x86`=dead, `0x87`=dead variant, `0x8B`=unknown.
185
  pub fn state(&self) -> u32 {
 
 
186
  unsafe { *((self as *const CTaskWorm as *const u8).add(0x44) as *const u32) }
187
  }
188
 
189
+ // Weapon fire dispatch state:
190
+ // - Fire type/subtypes live in the WeaponEntry (via active_weapon_entry at +0x36C)
191
+ // - Completion flag lives in CGameTask.subclass_data[12] (this object, at +0x3C)
192
+
193
+ /// Weapon fire completion flag at CGameTask+0x3C (subclass_data[12]).
194
+ /// Set to 0 before FireWeapon dispatch, 1 after.
195
+ pub fn fire_complete(&self) -> i32 {
196
+ i32::from_ne_bytes(self.base.subclass_data[12..16].try_into().unwrap())
197
+ }
198
+
199
+ /// Set the weapon fire completion flag.
200
+ pub fn set_fire_complete(&mut self, value: i32) {
201
+ self.base.subclass_data[12..16].copy_from_slice(&value.to_ne_bytes());
202
+ }
203
+
204
  /// Returns a reference to the vtable.
205
  ///
206
  /// # Safety
crates/openwa-debugui/src/app.rs CHANGED
@@ -84,6 +84,25 @@ unsafe fn get_ddgame() -> Option<*const DDGame> {
84
  Some(ddgame_ptr)
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  /// Read child task pointers from a CTask's children array.
88
  ///
89
  /// The array is **sparse**: slots are nulled when a child is removed rather than
@@ -221,6 +240,14 @@ impl eframe::App for DebugApp {
221
  self.nav_history.retain(|a| live_addrs.contains(a));
222
 
223
  egui::TopBottomPanel::top("toolbar").show(ctx, |ui| {
 
 
 
 
 
 
 
 
224
  ui.horizontal(|ui| {
225
  ui.checkbox(&mut self.show_transient, "Show transient");
226
  });
 
84
  Some(ddgame_ptr)
85
  }
86
 
87
+ /// Unlock all weapons: set ammo to unlimited (-1) and delays to 0 for all teams.
88
+ unsafe fn cheat_unlock_all_weapons() {
89
+ let Some(ddgame) = get_ddgame() else {
90
+ log::push("[Cheats] Not in game");
91
+ return;
92
+ };
93
+ let ddgame = ddgame as *mut DDGame;
94
+ let arena = &mut (*ddgame).team_arena;
95
+ for team in &mut arena.weapon_slots.teams {
96
+ for ammo in &mut team.ammo {
97
+ *ammo = -1; // unlimited
98
+ }
99
+ for delay in &mut team.delay {
100
+ *delay = 0; // no delay
101
+ }
102
+ }
103
+ log::push("[Cheats] All weapons unlocked (infinite ammo, no delays)");
104
+ }
105
+
106
  /// Read child task pointers from a CTask's children array.
107
  ///
108
  /// The array is **sparse**: slots are nulled when a child is removed rather than
 
240
  self.nav_history.retain(|a| live_addrs.contains(a));
241
 
242
  egui::TopBottomPanel::top("toolbar").show(ctx, |ui| {
243
+ egui::menu::bar(ui, |ui| {
244
+ ui.menu_button("Cheats", |ui| {
245
+ if ui.button("Unlock all weapons").clicked() {
246
+ unsafe { cheat_unlock_all_weapons() };
247
+ ui.close_menu();
248
+ }
249
+ });
250
+ });
251
  ui.horizontal(|ui| {
252
  ui.checkbox(&mut self.show_transient, "Show transient");
253
  });
crates/openwa-wormkit/src/replacements/weapon.rs CHANGED
@@ -1,14 +1,20 @@
1
- //! Weapon ammo hooks.
2
  //!
3
  //! Replaces WA.exe functions that manage weapon ammo in the TeamArenaState area (DDGame + 0x4628):
4
  //! - GetAmmo (0x5225E0): query ammo count with delay/phase checks
5
  //! - AddAmmo (0x522640): add ammo to a weapon slot
6
  //! - SubtractAmmo (0x522680): decrement ammo count
7
  //! - CountAliveWorms (0x5225A0): check if >1 worm alive on team
 
 
 
8
 
9
  use openwa_core::address::va;
10
  use openwa_core::engine::ddgame::{self, TeamArenaRef};
 
11
  use openwa_core::game::Weapon;
 
 
12
 
13
  use crate::hook::{self, usercall_trampoline};
14
 
@@ -24,14 +30,14 @@ unsafe extern "cdecl" fn add_ammo_impl(
24
  arena: TeamArenaRef,
25
  weapon_id: u32,
26
  ) {
27
- let idx = arena.ammo_index(team_index as usize, weapon_id);
28
  let state = arena.state_mut();
29
- let ammo = state.get_ammo(idx);
30
  if ammo >= 0 {
31
  if amount < 0 {
32
- *state.ammo_mut(idx) = -1; // set unlimited
33
  } else {
34
- *state.ammo_mut(idx) = ammo + amount;
35
  }
36
  }
37
  }
@@ -46,11 +52,11 @@ usercall_trampoline!(fn trampoline_add_ammo; impl_fn = add_ammo_impl;
46
  // RET 0x4
47
 
48
  unsafe extern "cdecl" fn subtract_ammo_impl(team_index: u32, arena: TeamArenaRef, weapon_id: u32) {
49
- let idx = arena.ammo_index(team_index as usize, weapon_id);
50
  let state = arena.state_mut();
51
- let ammo = state.get_ammo(idx);
52
  if ammo > 0 {
53
- *state.ammo_mut(idx) = ammo - 1;
54
  }
55
  }
56
 
@@ -68,11 +74,11 @@ unsafe extern "cdecl" fn get_ammo_impl(
68
  arena: TeamArenaRef,
69
  weapon_id: u32,
70
  ) -> u32 {
71
- let idx = arena.ammo_index(team_index as usize, weapon_id);
72
  let state = arena.state();
73
 
74
  // Check weapon delay
75
- if state.get_delay(idx) != 0 {
76
  if state.game_mode_flag == 0 {
77
  return 0;
78
  }
@@ -92,7 +98,7 @@ unsafe extern "cdecl" fn get_ammo_impl(
92
  }
93
  }
94
 
95
- state.get_ammo(idx) as u32
96
  }
97
 
98
  usercall_trampoline!(fn trampoline_get_ammo; impl_fn = get_ammo_impl;
@@ -123,6 +129,439 @@ unsafe extern "cdecl" fn count_alive_worms_impl(team_index: u32, arena: TeamAren
123
  usercall_trampoline!(fn trampoline_count_alive_worms; impl_fn = count_alive_worms_impl;
124
  regs = [eax, ecx]);
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  // ============================================================
127
  // Hook installation
128
  // ============================================================
@@ -144,6 +583,13 @@ pub fn install() -> Result<(), String> {
144
  va::COUNT_ALIVE_WORMS,
145
  trampoline_count_alive_worms as *const (),
146
  )?;
 
 
 
 
 
 
 
147
  }
148
 
149
  Ok(())
 
1
+ //! Weapon hooks.
2
  //!
3
  //! Replaces WA.exe functions that manage weapon ammo in the TeamArenaState area (DDGame + 0x4628):
4
  //! - GetAmmo (0x5225E0): query ammo count with delay/phase checks
5
  //! - AddAmmo (0x522640): add ammo to a weapon slot
6
  //! - SubtractAmmo (0x522680): decrement ammo count
7
  //! - CountAliveWorms (0x5225A0): check if >1 worm alive on team
8
+ //! - FireWeapon (0x51EE60): passthrough with weapon type logging
9
+
10
+ use core::sync::atomic::{AtomicU32, Ordering};
11
 
12
  use openwa_core::address::va;
13
  use openwa_core::engine::ddgame::{self, TeamArenaRef};
14
+ use openwa_core::game::weapon::{WeaponEntry, WeaponFireParams};
15
  use openwa_core::game::Weapon;
16
+ use openwa_core::log::log_line;
17
+ use openwa_core::task::worm::CTaskWorm;
18
 
19
  use crate::hook::{self, usercall_trampoline};
20
 
 
30
  arena: TeamArenaRef,
31
  weapon_id: u32,
32
  ) {
33
+ let (alliance, wid) = arena.weapon_slot_key(team_index as usize, weapon_id);
34
  let state = arena.state_mut();
35
+ let ammo = state.get_ammo(alliance, wid);
36
  if ammo >= 0 {
37
  if amount < 0 {
38
+ *state.ammo_mut(alliance, wid) = -1;
39
  } else {
40
+ *state.ammo_mut(alliance, wid) = ammo + amount;
41
  }
42
  }
43
  }
 
52
  // RET 0x4
53
 
54
  unsafe extern "cdecl" fn subtract_ammo_impl(team_index: u32, arena: TeamArenaRef, weapon_id: u32) {
55
+ let (alliance, wid) = arena.weapon_slot_key(team_index as usize, weapon_id);
56
  let state = arena.state_mut();
57
+ let ammo = state.get_ammo(alliance, wid);
58
  if ammo > 0 {
59
+ *state.ammo_mut(alliance, wid) = ammo - 1;
60
  }
61
  }
62
 
 
74
  arena: TeamArenaRef,
75
  weapon_id: u32,
76
  ) -> u32 {
77
+ let (alliance, wid) = arena.weapon_slot_key(team_index as usize, weapon_id);
78
  let state = arena.state();
79
 
80
  // Check weapon delay
81
+ if state.get_delay(alliance, wid) != 0 {
82
  if state.game_mode_flag == 0 {
83
  return 0;
84
  }
 
98
  }
99
  }
100
 
101
+ state.get_ammo(alliance, wid) as u32
102
  }
103
 
104
  usercall_trampoline!(fn trampoline_get_ammo; impl_fn = get_ammo_impl;
 
129
  usercall_trampoline!(fn trampoline_count_alive_worms; impl_fn = count_alive_worms_impl;
130
  regs = [eax, ecx]);
131
 
132
+ // ============================================================
133
+ // FireWeapon replacement (0x51EE60)
134
+ // ============================================================
135
+ // Convention: usercall(EAX=worm, ECX=local_struct) + 1 stack(worm), RET 0x4.
136
+ // Note: EAX = *(CTaskWorm+0x36C) = worm self-pointer, so EAX == stack param.
137
+ //
138
+ // Weapon launch data offsets (relative to worm/EAX):
139
+ // +0x30 = weapon type (1-4)
140
+ // +0x34 = subtype for types 3,4
141
+ // +0x38 = subtype for types 1,2
142
+ // +0x3C = params base
143
+ //
144
+ // worm = CTaskWorm pointer (ESI in original).
145
+ // local_struct = stack-local buffer from WeaponRelease (ECX at call site).
146
+ //
147
+ // Sub-functions are usercall: ESI=worm, ECX=local_struct (for some).
148
+ // We capture all three in our naked trampoline.
149
+
150
+ static ORIG_FIRE_WEAPON: AtomicU32 = AtomicU32::new(0);
151
+
152
+ /// Naked trampoline for FireWeapon.
153
+ /// Must save ALL callee-saved registers (ESI, EDI, EBX, EBP) because the
154
+ /// Rust cdecl impl may clobber them, and WeaponRelease (our caller) depends
155
+ /// on them being preserved.
156
+ #[unsafe(naked)]
157
+ unsafe extern "C" fn trampoline_fire_weapon() {
158
+ core::arch::naked_asm!(
159
+ // Save all callee-saved + EDX
160
+ "push ebx",
161
+ "push esi",
162
+ "push edi",
163
+ "push ebp",
164
+ "push edx",
165
+ // Push cdecl args: (weapon_ctx=EAX, local_struct=ECX, worm)
166
+ // Stack: 5 pushes (20) + ret (4) = 24 to stack param
167
+ "push [esp+24]", // worm
168
+ "push ecx", // local_struct
169
+ "push eax", // weapon_ctx
170
+ "call {impl_fn}",
171
+ "add esp, 12",
172
+ // Restore everything
173
+ "pop edx",
174
+ "pop ebp",
175
+ "pop edi",
176
+ "pop esi",
177
+ "pop ebx",
178
+ "ret 0x4",
179
+ impl_fn = sym fire_weapon_impl,
180
+ );
181
+ }
182
+
183
+ /// Rust implementation of FireWeapon dispatch.
184
+ ///
185
+ /// `entry`: EAX = active WeaponEntry pointer (from CTaskWorm+0x36C).
186
+ /// `local_struct`: ECX = stack-local buffer from WeaponRelease.
187
+ /// `worm`: stack param = CTaskWorm pointer (ESI in original).
188
+ ///
189
+ /// Completion flag at worm+0x3C (CGameTask.subclass_data[12]).
190
+ /// Params pointer at entry+0x3C (WeaponEntry.fire_complete) β€” different object, same offset.
191
+ unsafe extern "cdecl" fn fire_weapon_impl(
192
+ entry: *const WeaponEntry, local_struct: u32, worm: *mut CTaskWorm,
193
+ ) {
194
+ use openwa_core::rebase::rb;
195
+
196
+ let weapon_type = (*entry).fire_type;
197
+ let subtype_34 = (*entry).fire_subtype_34;
198
+ let subtype_38 = (*entry).fire_subtype_38;
199
+ let fire_params = &raw const (*entry).fire_params;
200
+ let w = worm as u32;
201
+
202
+ // Log weapon fire
203
+ let weapon_id = (*worm).selected_weapon;
204
+ let weapon_name = Weapon::try_from(weapon_id)
205
+ .map(|wp| format!("{:?}", wp))
206
+ .unwrap_or_else(|id| format!("Unknown({})", id));
207
+ let _ = log_line(&format!(
208
+ "[Weapon] FireWeapon: {} (id={}) type={} sub34={} sub38={}",
209
+ weapon_name, weapon_id, weapon_type, subtype_34, subtype_38
210
+ ));
211
+
212
+ (*worm).set_fire_complete(0);
213
+
214
+ match weapon_type {
215
+ 1 => match subtype_38 {
216
+ 1 => call_fire_stdcall1(w, fire_params, rb(0x51EC80)), // PlacedExplosive
217
+ 2 => call_fire_stdcall3(w, fire_params, local_struct, rb(0x51DFB0)), // Projectile
218
+ 3 => call_fire_thiscall2(w, fire_params, local_struct, rb(0x51E0F0)), // CreateWeaponProjectile
219
+ 4 => call_fire_stdcall2(w, fire_params, local_struct, rb(0x51ED90)), // Shotgun
220
+ _ => {}
221
+ },
222
+ 2 => match subtype_38 {
223
+ 1 => call_fire_stdcall3(w, fire_params, local_struct, rb(0x51E1C0)), // RopeType1
224
+ 2 => call_fire_thiscall2(w, fire_params, local_struct, rb(0x51E0F0)), // CreateWeaponProjectile
225
+ 3 => call_fire_stdcall3(w, fire_params, local_struct, rb(0x51E240)), // RopeType3
226
+ _ => {}
227
+ },
228
+ 3 => {
229
+ // GrenadeMortar receives &fire_subtype_34 as its params pointer
230
+ let subtype_34_ptr = &raw const (*entry).fire_subtype_34 as *const WeaponFireParams;
231
+ call_fire_stdcall3(w, subtype_34_ptr, local_struct, rb(0x51E2C0));
232
+ }
233
+ 4 => {
234
+ let subtype_38_ptr = &raw const (*entry).fire_subtype_38 as u32;
235
+ fire_weapon_special(subtype_34, subtype_38_ptr, worm, local_struct, entry);
236
+ }
237
+ _ => {}
238
+ }
239
+
240
+ (*worm).set_fire_complete(1);
241
+ }
242
+
243
+ // ── Sub-function bridges ────────────────────────────────────
244
+ // All preserve ESI=worm for usercall sub-functions.
245
+
246
+ // ── Sub-function bridges ────────────────────────────────────
247
+ // All bridges set ESI=worm AND EDI=worm before calling, since
248
+ // sub-functions are usercall and read both registers implicitly.
249
+ // Stack offsets are carefully calculated for each bridge.
250
+
251
+ // ── Sub-function bridges ────────────────────────────────────
252
+ // All bridges save/restore ESI+EDI, set ESI=EDI=worm, then call.
253
+ // This preserves LLVM's callee-saved registers while providing
254
+ // the usercall context that sub-functions expect.
255
+
256
+ /// Bridge: PlacedExplosive β€” stdcall(fire_params), RET 0x4.
257
+ #[unsafe(naked)]
258
+ unsafe extern "C" fn call_fire_stdcall1(
259
+ _worm: u32, _fire_params: *const WeaponFireParams, _addr: u32,
260
+ ) {
261
+ core::arch::naked_asm!(
262
+ "push ebx",
263
+ "push esi",
264
+ "push edi",
265
+ "mov esi, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
266
+ "mov edi, [esp+16]",
267
+ "mov ebx, [esp+24]", // addr
268
+ "push [esp+20]", // params
269
+ "call ebx",
270
+ "pop edi",
271
+ "pop esi",
272
+ "pop ebx",
273
+ "ret",
274
+ );
275
+ }
276
+
277
+ /// Bridge: Projectile/Rope/Grenade β€” stdcall(worm, fire_params, local_struct), RET 0xC.
278
+ #[unsafe(naked)]
279
+ unsafe extern "C" fn call_fire_stdcall3(
280
+ _worm: u32, _fire_params: *const WeaponFireParams, _local: u32, _addr: u32,
281
+ ) {
282
+ core::arch::naked_asm!(
283
+ "push ebx",
284
+ "push esi",
285
+ "push edi",
286
+ "mov esi, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
287
+ "mov edi, [esp+16]",
288
+ "mov ebx, [esp+28]", // addr (saves=12 + ret=4 + 3 args=12 = 28)
289
+ "push [esp+24]", // local_struct
290
+ "push [esp+24]", // params (shifted +4)
291
+ "push [esp+24]", // worm (shifted +8)
292
+ "call ebx",
293
+ "pop edi",
294
+ "pop esi",
295
+ "pop ebx",
296
+ "ret",
297
+ );
298
+ }
299
+
300
+ /// Bridge: CreateWeaponProjectile β€” thiscall(ECX=worm, fire_params, local_struct), RET 0x8.
301
+ #[unsafe(naked)]
302
+ unsafe extern "C" fn call_fire_thiscall2(
303
+ _worm: u32, _fire_params: *const WeaponFireParams, _local: u32, _addr: u32,
304
+ ) {
305
+ core::arch::naked_asm!(
306
+ "push ebx",
307
+ "push esi",
308
+ "push edi",
309
+ "mov esi, [esp+16]", // worm
310
+ "mov edi, [esp+16]",
311
+ "mov ecx, [esp+16]", // ECX = worm (this)
312
+ "mov ebx, [esp+28]", // addr
313
+ "push [esp+24]", // local_struct
314
+ "push [esp+24]", // params (shifted +4)
315
+ "call ebx",
316
+ "pop edi",
317
+ "pop esi",
318
+ "pop ebx",
319
+ "ret",
320
+ );
321
+ }
322
+
323
+ /// Bridge: Shotgun β€” stdcall(fire_params, local_struct), RET 0x8.
324
+ #[unsafe(naked)]
325
+ unsafe extern "C" fn call_fire_stdcall2(
326
+ _worm: u32, _fire_params: *const WeaponFireParams, _local: u32, _addr: u32,
327
+ ) {
328
+ core::arch::naked_asm!(
329
+ "push ebx",
330
+ "push esi",
331
+ "push edi",
332
+ "mov esi, [esp+16]", // worm
333
+ "mov edi, [esp+16]",
334
+ "mov ebx, [esp+28]", // addr
335
+ "push [esp+24]", // local_struct
336
+ "push [esp+24]", // params (shifted +4)
337
+ "call ebx",
338
+ "pop edi",
339
+ "pop esi",
340
+ "pop ebx",
341
+ "ret",
342
+ );
343
+ }
344
+
345
+ /// Type 4 (special) weapon dispatch.
346
+ /// Type 4 (special) weapon dispatch.
347
+ ///
348
+ /// EAX at entry = weapon entry pointer (unchanged from type-4 switch).
349
+ /// Some handlers explicitly set EAX=worm or EAX=*(worm+0x44).
350
+ /// Handlers without explicit MOV EAX inherit the entry pointer.
351
+ unsafe fn fire_weapon_special(
352
+ subtype: i32, params_38: u32, worm: *mut CTaskWorm, local_struct: u32,
353
+ entry: *const WeaponEntry,
354
+ ) {
355
+ use openwa_core::rebase::rb;
356
+ let w = worm as u32;
357
+ let e = entry as u32;
358
+
359
+ match subtype {
360
+ 1 => fire_worm_vtable_0xe(w, 0x6C), // Blowtorch
361
+ 2 => call_fire_usercall(e, w, rb(0x51E3E0)), // Drill (EAX=entry)
362
+ 3 => call_fire_stdcall3(w, params_38 as *const WeaponFireParams, local_struct, rb(0x51E350)), // Girder
363
+ 4 => fire_worm_vtable_0xe(w, 0x6D), // Baseball Bat
364
+ 5 => fire_worm_vtable_0xe(w, 0x75), // Fire Punch
365
+ 6 => fire_worm_vtable_0xe(w, 0x70), // Dragon Ball
366
+ 8 => fire_worm_vtable_0xe(w, 0x6E), // Kamikaze
367
+ 9 => call_fire_usercall_stdcall1(e, w, local_struct, rb(0x51E480)), // Prod (EAX=entry)
368
+ 10 => call_fire_usercall(w, w, rb(0x51E710)), // Air Strike (EAX=worm)
369
+ 11 => fire_worm_vtable_0xe(w, 0x71), // Scales of Justice
370
+ 13 => fire_send_team_message(worm, 0x2B), // Napalm Strike
371
+ 14 => call_fire_usercall(w, w, rb(0x51E670)), // Mail/Mine/Mole (EAX=worm)
372
+ 16 => {
373
+ // Teleport: MOV EAX,[ESI+0x44] (worm state) before check
374
+ let worm_state = (*worm).state();
375
+ let result = call_fire_usercall_ret(worm_state, w, rb(0x516930));
376
+ if result != 0 {
377
+ call_fire_usercall(result as u32, w, rb(0x51EB00));
378
+ } else {
379
+ fire_worm_vtable_0xe(w, 0x74);
380
+ }
381
+ }
382
+ 17 => call_fire_usercall(w, w, rb(0x51E920)), // Freeze (EAX=worm)
383
+ 18 => fire_worm_vtable_0xe(w, 0x72), // Suicide Bomber
384
+ 19 => fire_skip_go(worm, entry), // Skip Go (pure Rust)
385
+ 20 => call_fire_usercall(w, w, rb(0x51E600)), // Surrender (EAX=worm)
386
+ 21 => fire_select_worm(worm), // Select Worm (pure Rust)
387
+ 22 => call_fire_usercall(e, w, rb(0x51EC30)), // Jet Pack (EAX=entry)
388
+ 23 => fire_worm_vtable_0xe(w, 0x78), // Magic Bullet
389
+ 24 => call_fire_usercall(e, w, rb(0x51EA60)), // Low Grav (EAX=entry)
390
+ _ => {}
391
+ }
392
+ }
393
+
394
+ // ── Pure Rust fire handlers (no bridge needed) ──────────────
395
+
396
+ /// Send a message to the worm's CTaskTeam entity via SharedData lookup.
397
+ ///
398
+ /// Pattern shared by Napalm Strike (msg 0x2B), Surrender (msg 0x29), etc.
399
+ /// Looks up entity with key (0, 0x14=CTaskTeam) in worm's shared data table,
400
+ /// then calls entity->vtable[2] (HandleMessage) with the given message type.
401
+ ///
402
+ /// The 0x40C-byte local buffer is passed as data pointer; team_index is written
403
+ /// at buf+0x08 to identify which team fired.
404
+ unsafe fn fire_send_team_message(worm: *mut CTaskWorm, msg_type: u32) {
405
+ use openwa_core::task::SharedDataTable;
406
+
407
+ let task = &(*worm).base.base; // CTask base
408
+ let table = SharedDataTable::from_task(task);
409
+
410
+ // Lookup CTaskTeam (key_esi=0, key_edi=0x14)
411
+ let team_entity = table.lookup(0, 0x14);
412
+ if team_entity.is_null() {
413
+ return;
414
+ }
415
+
416
+ // Prepare local buffer (0x40C bytes, mostly zero)
417
+ let mut buf = [0u8; 0x40C];
418
+ let team_index = (*worm).team_index;
419
+ buf[8..12].copy_from_slice(&team_index.to_ne_bytes());
420
+
421
+ // Call entity->vtable[2] = HandleMessage(sender=worm, msg, size=4, data=&buf)
422
+ let vtable = *(team_entity as *const u32);
423
+ let handle_message: unsafe extern "thiscall" fn(*mut u8, *mut CTaskWorm, u32, u32, *const u8) =
424
+ core::mem::transmute(*(vtable as *const u32).add(2));
425
+ handle_message(team_entity, worm, msg_type, 4, buf.as_ptr());
426
+ }
427
+
428
+ /// Select Worm (subtype 21) β€” pure Rust replacement for 0x51EBE0.
429
+ ///
430
+ /// Sends message 0x5D to CTaskTeam with buf = [8, team_index, ...].
431
+ unsafe fn fire_select_worm(worm: *mut CTaskWorm) {
432
+ use openwa_core::task::SharedDataTable;
433
+
434
+ let task = &(*worm).base.base;
435
+ let table = SharedDataTable::from_task(task);
436
+ let team_entity = table.lookup(0, 0x14);
437
+ if team_entity.is_null() {
438
+ return;
439
+ }
440
+
441
+ let mut buf = [0u8; 0x40C];
442
+ // buf[0..4] = 8 (constant), buf[4..8] = team_index
443
+ buf[0..4].copy_from_slice(&8u32.to_ne_bytes());
444
+ buf[4..8].copy_from_slice(&(*worm).team_index.to_ne_bytes());
445
+
446
+ let vtable = *(team_entity as *const u32);
447
+ let handle_message: unsafe extern "thiscall" fn(*mut u8, *mut CTaskWorm, u32, u32, *const u8) =
448
+ core::mem::transmute(*(vtable as *const u32).add(2));
449
+ handle_message(team_entity, worm, 0x5D, 0x408, buf.as_ptr());
450
+ }
451
+
452
+ /// Skip Go (subtype 19) β€” pure Rust replacement for 0x51E8C0.
453
+ ///
454
+ /// Toggles a bit in the team's `TeamHeader.turn_action_flags` (+0x7C).
455
+ /// Bit position comes from weapon entry's fire_params.
456
+ /// In game_version > 0x1C: toggles (set/clear). Otherwise: always sets.
457
+ unsafe fn fire_skip_go(worm: *const CTaskWorm, entry: *const WeaponEntry) {
458
+ use openwa_core::engine::ddgame::TeamArenaRef;
459
+ use openwa_core::engine::DDGame;
460
+
461
+ let ddgame = (*worm).base.base.ddgame as *mut DDGame;
462
+ let game_version = (*(*ddgame).game_info).game_version;
463
+ let team_index = (*worm).team_index as usize;
464
+
465
+ let bit_index = (*entry).fire_params._data[0] & 0x1F;
466
+ let bit = 1u32 << bit_index;
467
+
468
+ let arena = TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
469
+ let header = arena.team_header_mut(team_index);
470
+ let flags = header.turn_action_flags;
471
+
472
+ if game_version > 0x1C && (flags & bit) != 0 {
473
+ header.turn_action_flags = flags & !bit;
474
+ } else {
475
+ header.turn_action_flags = flags | bit;
476
+ }
477
+ }
478
+
479
+ // ── Naked asm bridges ───────────────────────────────────────
480
+
481
+ /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET.
482
+ /// Args: (eax_val, worm, addr). Saves/restores ESI+EDI. Uses EBX to hold addr.
483
+ #[unsafe(naked)]
484
+ unsafe extern "C" fn call_fire_usercall(_eax: u32, _worm: u32, _addr: u32) {
485
+ core::arch::naked_asm!(
486
+ "push ebx",
487
+ "push esi",
488
+ "push edi",
489
+ "mov eax, [esp+16]", // eax_val (3 saves=12 + ret=4 = 16)
490
+ "mov esi, [esp+20]", // worm
491
+ "mov edi, [esp+20]",
492
+ "mov ebx, [esp+24]", // addr
493
+ "call ebx",
494
+ "pop edi",
495
+ "pop esi",
496
+ "pop ebx",
497
+ "ret",
498
+ );
499
+ }
500
+
501
+ /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm), plain RET, returns EAX.
502
+ #[unsafe(naked)]
503
+ unsafe extern "C" fn call_fire_usercall_ret(_eax: u32, _worm: u32, _addr: u32) -> i32 {
504
+ core::arch::naked_asm!(
505
+ "push ebx",
506
+ "push esi",
507
+ "push edi",
508
+ "mov eax, [esp+16]",
509
+ "mov esi, [esp+20]",
510
+ "mov edi, [esp+20]",
511
+ "mov ebx, [esp+24]",
512
+ "call ebx",
513
+ "pop edi",
514
+ "pop esi",
515
+ "pop ebx",
516
+ "ret",
517
+ );
518
+ }
519
+
520
+ /// Bridge: usercall(EAX=eax_val, ESI=worm, EDI=worm) + stdcall(1 param), RET 0x4.
521
+ #[unsafe(naked)]
522
+ unsafe extern "C" fn call_fire_usercall_stdcall1(
523
+ _eax: u32, _worm: u32, _param: u32, _addr: u32,
524
+ ) {
525
+ core::arch::naked_asm!(
526
+ "push ebx",
527
+ "push esi",
528
+ "push edi",
529
+ "mov eax, [esp+16]", // eax_val
530
+ "mov esi, [esp+20]", // worm
531
+ "mov edi, [esp+20]",
532
+ "mov ebx, [esp+28]", // addr (3 saves=12 + ret=4 + 3 args=12 = 28)
533
+ "push [esp+24]", // param
534
+ "call ebx",
535
+ "pop edi",
536
+ "pop esi",
537
+ "pop ebx",
538
+ "ret",
539
+ );
540
+ }
541
+
542
+ /// Call worm->vtable[0xE](msg_id) β€” thiscall(ECX=worm, stack=msg_id), RET 0x4.
543
+ /// Original: `(**(code **)(*worm + 0x38))(msg_id)`.
544
+ /// Uses naked bridge to avoid LLVM stack tracking issues with push/RET mismatch.
545
+ #[unsafe(naked)]
546
+ unsafe extern "C" fn fire_worm_vtable_0xe(_worm: u32, _msg_id: u32) {
547
+ core::arch::naked_asm!(
548
+ "push ebx",
549
+ "push esi",
550
+ "push edi",
551
+ "mov ecx, [esp+16]", // worm (3 saves=12 + ret=4 = 16)
552
+ "mov esi, [esp+16]", // ESI = worm
553
+ "mov edi, [esp+16]", // EDI = worm
554
+ "mov ebx, [ecx]", // vtable
555
+ "mov ebx, [ebx+0x38]",// vtable[0xE]
556
+ "push [esp+20]", // msg_id
557
+ "call ebx", // thiscall: ECX=worm, RET 0x4 cleans msg_id
558
+ "pop edi",
559
+ "pop esi",
560
+ "pop ebx",
561
+ "ret",
562
+ );
563
+ }
564
+
565
  // ============================================================
566
  // Hook installation
567
  // ============================================================
 
583
  va::COUNT_ALIVE_WORMS,
584
  trampoline_count_alive_worms as *const (),
585
  )?;
586
+
587
+ let trampoline = hook::install(
588
+ "FireWeapon",
589
+ va::FIRE_WEAPON,
590
+ trampoline_fire_weapon as *const (),
591
+ )?;
592
+ ORIG_FIRE_WEAPON.store(trampoline as u32, Ordering::Relaxed);
593
  }
594
 
595
  Ok(())
crates/openwa-wormkit/src/validation/mod.rs CHANGED
@@ -1529,7 +1529,7 @@ unsafe fn dump_missile_raw(ptr: u32) {
1529
  let base = ptr as *const u32;
1530
  let sections: &[(usize, usize, &str)] = &[
1531
  (0x000, 0x030, "CTask base"),
1532
- (0x030, 0x084, "CGameTask _unknown_30"),
1533
  (0x084, 0x098, "CGameTask pos/speed (0x84–0x97)"),
1534
  (0x098, 0x0FC, "CGameTask _unknown_98"),
1535
  (0x0FC, 0x130, "CTaskMissile 0xFC–0x12F"),
 
1529
  let base = ptr as *const u32;
1530
  let sections: &[(usize, usize, &str)] = &[
1531
  (0x000, 0x030, "CTask base"),
1532
+ (0x030, 0x084, "CGameTask subclass_data"),
1533
  (0x084, 0x098, "CGameTask pos/speed (0x84–0x97)"),
1534
  (0x098, 0x0FC, "CGameTask _unknown_98"),
1535
  (0x0FC, 0x130, "CTaskMissile 0xFC–0x12F"),
docs/re-notes/weapon-system.md ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Weapon System
2
+
3
+ Comprehensive reference for the Worms Armageddon 3.8.1 weapon system internals.
4
+
5
+ ## Overview
6
+
7
+ The weapon system spans multiple subsystems: a **weapon table** defining all 71 weapons, an **ammo/delay tracker** per team/alliance, a **firing dispatch** that routes weapon activation to type-specific handlers, **CTaskMissile** objects for in-flight projectiles, and an **explosion system** for terrain damage.
8
+
9
+ ### Object Relationships
10
+
11
+ ```mermaid
12
+ graph TD
13
+ DDGame["DDGame (0x98B8 bytes)"]
14
+ WT["WeaponTable<br/>DDGame+0x510"]
15
+ TAS["TeamArenaState<br/>DDGame+0x4628"]
16
+ WP["WeaponPanel<br/>DDGame+0x548"]
17
+ CTW["CTaskWorm<br/>(per-live-worm task)"]
18
+ CTM["CTaskMissile<br/>(per-projectile task)"]
19
+ EXP["CreateExplosion"]
20
+ LAND["PCLandscape"]
21
+
22
+ DDGame --> WT
23
+ DDGame --> TAS
24
+ DDGame --> WP
25
+ CTW -->|"FireWeapon"| CTM
26
+ CTW -->|"GetAmmo/SubtractAmmo"| TAS
27
+ CTM -->|"impact"| EXP
28
+ EXP -->|"terrain damage"| LAND
29
+ WP -->|"reads availability"| WT
30
+ WP -->|"reads ammo"| TAS
31
+ ```
32
+
33
+ ### Firing Sequence
34
+
35
+ ```mermaid
36
+ sequenceDiagram
37
+ participant P as Player Input
38
+ participant W as CTaskWorm
39
+ participant FR as WeaponRelease
40
+ participant FW as FireWeapon
41
+ participant CW as CreateWeaponProjectile
42
+ participant M as CTaskMissile
43
+ participant E as CreateExplosion
44
+ participant L as PCLandscape
45
+
46
+ P->>W: Fire key pressed
47
+ W->>FR: WeaponRelease(0x51C3D0)<br/>position, angle
48
+ FR->>FW: FireWeapon(0x51EE60)<br/>weapon type dispatch
49
+ FW->>CW: CreateWeaponProjectile(0x51E0F0)
50
+ CW->>M: CTaskMissile__Constructor(0x507D10)<br/>allocate 0x40C bytes
51
+ loop Every frame
52
+ M->>M: PhysicsUpdate(0x508C90)<br/>gravity, wind, collision
53
+ end
54
+ M->>E: CreateExplosion(0x548080)
55
+ E->>L: ApplyExplosion(0x57C820)<br/>terrain bitmap damage
56
+ ```
57
+
58
+ ## WeaponEntry & WeaponTable
59
+
60
+ ### WeaponTable (DDGame+0x510)
61
+
62
+ Allocated by `InitWeaponTable` (0x53CAB0, stdcall(wrapper), RET 0x4). Called once
63
+ during `CTaskGameState__vmethod_7` at game start.
64
+
65
+ ```
66
+ +0x00 [WeaponEntry; 71] entries Standard weapons (0..70)
67
+ Total: 71 Γ— 0x1D0 = 0x80B0 bytes (no header)
68
+ ```
69
+
70
+ ### WeaponEntry (0x1D0 = 464 bytes per entry)
71
+
72
+ Source: `wkJellyWorm/src/CustomWeapons.h` (WeaponStruct) + InitWeaponTable analysis.
73
+
74
+ ```
75
+ +0x00 *const c_char name1 Primary weapon name string
76
+ +0x04 *const c_char name2 Secondary weapon name string
77
+ +0x08 i32 panel_state Panel state (init: 0xFFFFFFFF). wkJellyWorm: panelRow
78
+ +0x0C i32 unknown_0c Unknown
79
+ +0x10 i32 defined Nonzero = weapon exists. Checked by CheckWeaponAvail.
80
+ +0x14 [u8; 0x10] unknown_14 Unknown
81
+ +0x24 i32 availability Init: 0xFFFFFFFF, then 0 or 1. None/SkipGo/Surrender = 0.
82
+ +0x28 i32 enabled Init: 1
83
+ +0x2C [u8; 0x1A4] unknown_2c Remaining fields (opaque)
84
+ ```
85
+
86
+ **InitWeaponTable per-entry initialization:**
87
+ - +0x08 = 0xFFFFFFFF (panel_state)
88
+ - +0x24 = 0xFFFFFFFF (availability, then overwritten per-weapon)
89
+ - +0x28 = 1 (enabled)
90
+ - Weapon 0 (None), 57 (SkipGo), 58 (Surrender): availability forced to 0
91
+
92
+ **Rust types:** `WeaponEntry`, `WeaponTable` in `openwa-core/src/game/weapon.rs`.
93
+
94
+ ### Weapon Enum (71 entries)
95
+
96
+ Defined in `openwa-core/src/game/weapon.rs`. Key weapon IDs:
97
+
98
+ | ID | Hex | Name | Category |
99
+ |----|-----|------|----------|
100
+ | 0 | 0x00 | None | - |
101
+ | 1 | 0x01 | Bazooka | Projectile |
102
+ | 2 | 0x02 | HomingMissile | Projectile (homing) |
103
+ | 6 | 0x06 | Grenade | Thrown |
104
+ | 10 | 0x0A | Earthquake | Special (net-disabled) |
105
+ | 24 | 0x18 | SuperSheep | Animal (restricted) |
106
+ | 25 | 0x19 | AquaSheep | Animal (restricted) |
107
+ | 37 | 0x25 | NinjaRope | Rope |
108
+ | 40 | 0x28 | Teleport | Utility (immune to sudden death) |
109
+ | 54 | 0x36 | Donkey | Special (disable flag) |
110
+ | 55 | 0x37 | NuclearTest | Special (net-disabled) |
111
+ | 56 | 0x38 | Armageddon | Special (net-disabled) |
112
+ | 59 | 0x3B | SelectWorm | Action (needs >1 alive worm) |
113
+ | 66 | 0x42 | Invisibility | Power-up (mode flag) |
114
+ | 69 | 0x45 | DoubleTurnTime | Power-up (threshold) |
115
+
116
+ ## Ammunition & Delay System
117
+
118
+ ### Storage: TeamArenaState (DDGame+0x4628)
119
+
120
+ Ammo and delay are stored in a flat array indexed by alliance and weapon ID.
121
+
122
+ ```
123
+ Flat index = alliance_id Γ— 142 + weapon_id
124
+
125
+ Ammo: TeamArenaState + 0x1EB4 + flat_index Γ— 4
126
+ Delay: TeamArenaState + 0x1EB4 + (flat_index + 71) Γ— 4
127
+
128
+ Values: -1 = unlimited, 0 = none, >0 = count
129
+ Delay: 0 = available, >0 = turns until available
130
+ ```
131
+
132
+ 6 alliances Γ— 142 entries (71 ammo + 71 delay) = 852 total slots.
133
+
134
+ Alliance ID is read from the team's sentinel worm field at offset +0x80
135
+ (`sentinel_weapon_alliance`).
136
+
137
+ ### Functions
138
+
139
+ | Function | Address | Convention | Parameters |
140
+ |----------|---------|------------|------------|
141
+ | GetAmmo | 0x5225E0 | usercall | EAX=team_index, ESI=arena_base, EDX=weapon_id β†’ EAX=ammo |
142
+ | AddAmmo | 0x522640 | usercall | EAX=team_index, EDX=amount, stack(base, weapon_id), RET 0x8 |
143
+ | SubtractAmmo | 0x522680 | usercall | EAX=team_index, ECX=arena_base, stack(weapon_id), RET 0x4 |
144
+ | SubtractAmmo_v2 | 0x558E80 | fastcall | ECX=param1, EDX=CTaskTeam* |
145
+ | CountAliveWorms | 0x5225A0 | usercall | EAX=team_index, ECX=arena_base β†’ EAX=count |
146
+
147
+ **GetAmmo special rules:**
148
+ - Returns 0 if weapon delay > 0 (weapon on cooldown)
149
+ - Sudden death (game_phase β‰₯ 484): returns 0 for all except Teleport (0x28)
150
+ - SelectWorm (0x3B): returns 0 if team has ≀1 alive worm
151
+ - Game mode flag at TeamArenaState+0x2C0C can override delay checks
152
+
153
+ **SubtractAmmo call sites (verified):**
154
+ - `CTaskWorm__HandleMessage` (0x512C27): subtracts Teleport (0x28) ammo
155
+ - `CTaskTeam_vt2__vmethod_2` (0x558443): subtracts the team's selected weapon
156
+ - `FUN_0055f8c0` (3 sites): subtracts DamageX2 (0x43), CrateSpy (0x44), and others
157
+
158
+ Our hook reportedly never triggered β€” this was because the test replay (bots)
159
+ doesn't use Teleport, DamageX2, or CrateSpy. The hook implementation is correct.
160
+
161
+ The second variant at 0x558E80 (fastcall) has 3 xrefs from `CTaskTeam_vt2__vmethod_2`.
162
+
163
+ **Port status:** All 4 functions ported in `replacements/weapon.rs`.
164
+
165
+ ## Weapon Availability
166
+
167
+ ### DDGame__CheckWeaponAvail (0x53FFC0)
168
+
169
+ Convention: fastcall(DDGame), ESI=weapon_id. Ported as `check_weapon_avail`
170
+ in `openwa-core/src/engine/game_state_init.rs`.
171
+
172
+ **Per-weapon disable rules:**
173
+
174
+ | Weapon(s) | Condition | Result |
175
+ |-----------|-----------|--------|
176
+ | Earthquake (10), NuclearTest (55), Armageddon (56) | `net_config_2 != 0 && net_weapon_exception == 0` | Disabled in network games |
177
+ | Donkey (54) | `donkey_disabled != 0` | GameInfo+0xD94C flag |
178
+ | Invisibility (66) | `invisibility_mode == 0 && network_ecx == 0` | Needs network or mode flag |
179
+ | DoubleTurnTime (69) | `game_version > 0xD1 && threshold > 0x7FFF` | GameInfo+0xD932 threshold |
180
+
181
+ **General availability path:**
182
+ 1. Check weapon table entry: name1 non-null = weapon defined
183
+ 2. If `level_width_raw == 0` OR weapon defined: check super weapon rules
184
+ 3. `IsSuperWeapon` (0x565960): if super and `super_weapon_allowed == 0`, disable (unless game_version < 0x2A)
185
+ 4. `supersheep_restricted` (DDGame+0x7E25): gates SuperSheep/AquaSheep check
186
+ 5. `aquasheep_is_supersheep` (GameInfo+0xD956): when set, AquaSheep slot becomes SuperSheep
187
+
188
+ ### DDGame__IsSuperWeapon (0x565960)
189
+
190
+ Convention: fastcall(weapon_id). Returns 1 for super weapons:
191
+
192
+ Super weapon IDs: 16, 19, 29-31, 36, 41-42, 45-46, 49-51, 54-56, 60-61
193
+
194
+ ## FireWeapon Dispatch (0x51EE60)
195
+
196
+ Convention: thiscall. `in_EAX` = weapon context structure where:
197
+ - +0x30: weapon type (1-4)
198
+ - +0x34: subtype for type 3 and type 4
199
+ - +0x38: subtype for type 1 and type 2
200
+ - +0x3C: parameters base (passed to sub-functions)
201
+
202
+ `param_2` = DDGameWrapper pointer. Sets `param_2[0xF] = 1` on completion.
203
+
204
+ ### Type 1: Projectile Weapons
205
+
206
+ | Subtype | Function | Address | Weapons |
207
+ |---------|----------|---------|---------|
208
+ | 1 | FireWeapon__PlacedExplosive | 0x51EC80 | Dynamite, Mine, Petrol Bomb |
209
+ | 2 | FireWeapon__Projectile | 0x51DFB0 | Bazooka, Mortar, Longbow |
210
+ | 3 | CreateWeaponProjectile | 0x51E0F0 | Homing Missile, Homing Pigeon |
211
+ | 4 | FireWeapon__Shotgun | 0x51ED90 | Shotgun, Handgun, Uzi, Minigun |
212
+
213
+ ### Type 2: Rope Weapons
214
+
215
+ | Subtype | Function | Address | Weapons |
216
+ |---------|----------|---------|---------|
217
+ | 1 | FireWeapon__RopeType1 | 0x51E1C0 | Ninja Rope (attach) |
218
+ | 2 | CreateWeaponProjectile | 0x51E0F0 | Ninja Rope (projectile mode) |
219
+ | 3 | FireWeapon__RopeType3 | 0x51E240 | Bungee |
220
+
221
+ ### Type 3: Thrown Weapons
222
+
223
+ | Subtype | Function | Address | Weapons |
224
+ |---------|----------|---------|---------|
225
+ | (all) | FireWeapon__GrenadeMortar | 0x51E2C0 | Grenade, Cluster Bomb, Banana Bomb, Holy Grenade |
226
+
227
+ ### Type 4: Special Weapons
228
+
229
+ | Subtype | Function | Address | Description |
230
+ |---------|----------|---------|-------------|
231
+ | 1 | vtable[0xE](0x6C) | (indirect) | Blowtorch |
232
+ | 2 | FireWeapon__Special_Type2 | 0x51E3E0 | Pneumatic Drill |
233
+ | 3 | FireWeapon__Special_Type3 | 0x51E350 | Girder |
234
+ | 4 | vtable[0xE](0x6D) | (indirect) | Baseball Bat |
235
+ | 5 | vtable[0xE](0x75) | (indirect) | Fire Punch |
236
+ | 6 | vtable[0xE](0x70) | (indirect) | Dragon Ball |
237
+ | 8 | vtable[0xE](0x6E) | (indirect) | Kamikaze |
238
+ | 9 | FireWeapon__Special_Type9 | 0x51E480 | Prod |
239
+ | 10 | FireWeapon__Special_Type10 | 0x51E710 | Air Strike |
240
+ | 11 | vtable[0xE](0x71) | (indirect) | Scales of Justice |
241
+ | 13 | FireWeapon__Special_Type13 | 0x51E5C0 | Napalm Strike |
242
+ | 14 | FireWeapon__Special_Type14 | 0x51E670 | Mail/Mine/Mole Strike |
243
+ | 16 | FUN_0051EB00 (conditional) | 0x51EB00 | Teleport (checks FUN_516930 first) |
244
+ | 17 | FireWeapon__Special_Type17 | 0x51E920 | Freeze |
245
+ | 18 | vtable[0xE](0x72) | (indirect) | Suicide Bomber |
246
+ | 19 | FireWeapon__Special_Type19 | 0x51E8C0 | Skip Go |
247
+ | 20 | FireWeapon__Special_Type20 | 0x51E600 | Surrender |
248
+ | 21 | FireWeapon__Special_Type21 | 0x51EBE0 | Select Worm |
249
+ | 22 | FireWeapon__Special_Type22 | 0x51EC30 | Jet Pack |
250
+ | 23 | vtable[0xE](0x78) | (indirect) | Magic Bullet |
251
+ | 24 | FireWeapon__Special_Type24 | 0x51EA60 | Low Gravity / Fast Walk |
252
+
253
+ ## CTaskMissile (0x40C bytes)
254
+
255
+ Extends CGameTask (0xFC base). Represents an in-flight projectile.
256
+
257
+ ```
258
+ Constructor: 0x507D10 (stdcall, 4 params)
259
+ Vtable: 0x664438
260
+ Class type: 0x0B
261
+ Allocation: 0x40C bytes via wa_malloc
262
+ ```
263
+
264
+ ### Structure Layout
265
+
266
+ ```
267
+ +0x000 CGameTask base (0xFC bytes)
268
+ +0x084 speed_x Fixed16.16 horizontal velocity
269
+ +0x088 speed_y Fixed16.16 vertical velocity
270
+ +0x090 pos_x Fixed16.16 world X
271
+ +0x094 pos_y Fixed16.16 world Y
272
+
273
+ +0x128 u32 launch_seed Position-derived seed
274
+ +0x12C u32 slot_id Object pool slot index
275
+
276
+ +0x130 spawn_params [11 DWORDs = 44 bytes]
277
+ [0] owner_id Team that fired
278
+ [2-3] spawn_x/y Fixed16.16 launch position
279
+ [4-5] speed_x/y Fixed16.16 initial velocity
280
+ [8] pellet_index Cluster volley index
281
+
282
+ +0x15C weapon_data [94 DWORDs = 376 bytes]
283
+ [0x0F] gravity_pct 100 = 1.0Γ— gravity
284
+ [0x10] bounce_pct Bounce coefficient
285
+ [0x12] friction_pct Friction coefficient
286
+ [0x1A] missile_type 2=Standard, 3=Homing, 4=Sheep, 5=Cluster
287
+ [0x1C] render_timer Fuse countdown
288
+
289
+ +0x2D4 render_data [42 DWORDs = 168 bytes]
290
+ Physics parameters (shifted copy of weapon_data)
291
+
292
+ +0x3C8 i32 direction -1 (left) or +1 (right)
293
+ ```
294
+
295
+ ### Missile Type Enum
296
+
297
+ | Value | Type | Behavior |
298
+ |-------|------|----------|
299
+ | 2 | Standard | Straight trajectory with gravity |
300
+ | 3 | Homing | Tracks target position |
301
+ | 4 | Sheep | Walking behavior on terrain |
302
+ | 5 | Cluster | Splits into sub-munitions on impact |
303
+
304
+ ### Key Methods
305
+
306
+ | Method | Address | Description |
307
+ |--------|---------|-------------|
308
+ | Constructor | 0x507D10 | Initialize physics, copy weapon data from scheme |
309
+ | Init | 0x508640 | Reset physics state |
310
+ | PhysicsUpdate | 0x508C90 | Per-frame trajectory, gravity, wind, collision |
311
+ | HandleMessage | 0x50B400 | Event processing (detonation triggers, etc.) |
312
+ | Free | 0x508330 | Cleanup and pool return |
313
+ | vt12 | 0x508C70 | Vtable slot 12 |
314
+ | vt18_GetField130 | 0x50BFA0 | Returns spawn_params pointer |
315
+
316
+ ### Missile Count Limit
317
+
318
+ DDGame+0x72A4 tracks active missile count. `CreateWeaponProjectile` checks:
319
+ - If count + 7 < 0x2BD (701): create missile normally
320
+ - If exceeded and game_version < 0x3C: set error code 6 at DDGame+0x4624, load
321
+ string resource 0x70F as error message at DDGame+0x7EF4
322
+ - Otherwise: silently fail
323
+
324
+ ## Projectile Creation (0x51E0F0)
325
+
326
+ `CreateWeaponProjectile`: thiscall(this=context, param_2=missile_params, param_3=launch_params).
327
+
328
+ 1. Read DDGame via `this+0x2C` (CTask.ddgame field)
329
+ 2. Check missile count limit (DDGame+0x72A4 + 7 < 0x2BD)
330
+ 3. Call `FUN_004FDF90(this)` β€” pool slot allocator
331
+ 4. `wa_malloc(0x40C)` + `memset(0, 0x3EC)`
332
+ 5. `CTaskMissile__Constructor(slot, missile_params, launch_params)`
333
+ 6. Return allocated missile pointer
334
+
335
+ ## Explosion & Terrain Damage
336
+
337
+ | Function | Address | Convention | Description |
338
+ |----------|---------|------------|-------------|
339
+ | CreateExplosion | 0x548080 | usercall(ESI=context) | Spawns explosion task via pool allocator + vtable dispatch |
340
+ | PCLandscape_ApplyExplosion | 0x57C820 | (unknown) | Modifies terrain bitmap, creates crater |
341
+
342
+ CreateExplosion is a thin wrapper: allocates via `FUN_004FDF90`, then calls
343
+ `vtable[2]()` on the allocated object. The actual explosion logic (radius,
344
+ damage, terrain removal) lives in the explosion task's ProcessFrame.
345
+
346
+ ## Weapon Panel UI
347
+
348
+ | Function | Address | Description |
349
+ |----------|---------|-------------|
350
+ | DDGame__InitWeaponPanel_Maybe | 0x567770 | Initialize panel structures and availability grid |
351
+ | PrepareWeaponPanel | 0x568220 | Main panel renderer (~2000 lines). Iterates weapons by row, checks availability, renders tiles |
352
+ | WeaponPanelDrawTile | 0x567AA0 | Render individual weapon tile |
353
+ | WeaponPanelDescription | 0x567F80 | Show ammo/delay info on hover |
354
+ | WeaponPanelCheckClick | 0x5690B0 | Handle weapon selection clicks |
355
+
356
+ **Panel geometry:** 5 columns Γ— 13 rows. Each tile is 29px wide.
357
+ Custom weapons (wkJellyWorm): dynamic columns up to 256 weapons.
358
+
359
+ ## Weapon Crate System
360
+
361
+ - `CTaskCrate__Constructor` (0x502490): creates a weapon crate in the game world
362
+ - Crate probability per weapon defined in scheme (`WeaponSettings.crate_probability`, 0-100%)
363
+ - Crate drops award random weapons weighted by probability
364
+
365
+ ## Scheme Integration
366
+
367
+ Weapon configuration flows from `.wsc` scheme files into GameInfo, then into the
368
+ weapon table and ammo arrays at game start.
369
+
370
+ ### WeaponSettings (4 bytes per weapon)
371
+
372
+ ```
373
+ +0x00 u8 ammo 0=none, 1-10=count, 0x80+=infinite
374
+ +0x01 u8 power 0-20 (max varies per weapon)
375
+ +0x02 u8 delay Turn cooldown before available
376
+ +0x03 u8 crate_probability 0-100 percentage
377
+ ```
378
+
379
+ ### Scheme Formats
380
+
381
+ | Version | Payload | Weapons |
382
+ |---------|---------|---------|
383
+ | V1 | 0xD8 bytes | 45 standard weapons |
384
+ | V2 | 0x124 bytes | V1 + 19 super weapons |
385
+ | V3 | 0x192 bytes | V2 + 110 extended options (physics tweaks) |
386
+
387
+ **Port status:** Scheme parsing fully ported in `openwa-core/src/game/scheme.rs`
388
+ (10 hooks in `replacements/scheme.rs`).
389
+
390
+ ## Complete Function Address Table
391
+
392
+ ### Weapon Table & Initialization
393
+
394
+ | Function | Address | Convention | Port Status |
395
+ |----------|---------|------------|-------------|
396
+ | InitWeaponTable | 0x53CAB0 | stdcall(wrapper), RET 0x4 | Hooked (passthrough) |
397
+ | DDGame__CheckWeaponAvail | 0x53FFC0 | fastcall(DDGame) | **Ported** |
398
+ | DDGame__IsSuperWeapon | 0x565960 | fastcall(weapon_id) | **Ported** |
399
+ | DDGame__LoadHudAndWeaponSprites | 0x53D0E0 | stdcall(wrapper, ...) | Bridge call |
400
+
401
+ ### Ammo System
402
+
403
+ | Function | Address | Convention | Port Status |
404
+ |----------|---------|------------|-------------|
405
+ | GetAmmo | 0x5225E0 | usercall(EAX,ESI,EDX) | **Ported** |
406
+ | AddAmmo | 0x522640 | usercall(EAX,EDX,stack), RET 0x8 | **Ported** |
407
+ | SubtractAmmo | 0x522680 | usercall(EAX,ECX,stack), RET 0x4 | **Ported** (may not trigger) |
408
+ | SubtractAmmo_v2 | 0x558E80 | fastcall(ECX,EDX) | Unported |
409
+ | CountAliveWorms | 0x5225A0 | usercall(EAX,ECX) | **Ported** |
410
+
411
+ ### Firing & Projectiles
412
+
413
+ | Function | Address | Convention | Port Status |
414
+ |----------|---------|------------|-------------|
415
+ | WeaponRelease | 0x51C3D0 | thiscall(CTaskWorm*, ...) | Unported |
416
+ | FireWeapon | 0x51EE60 | thiscall, EAX=weapon_ctx | Unported |
417
+ | CreateWeaponProjectile | 0x51E0F0 | thiscall(context, params, launch) | Unported |
418
+ | CTaskMissile__Constructor | 0x507D10 | stdcall, 4 params | Unported |
419
+ | CTaskMissile__PhysicsUpdate | 0x508C90 | (unknown) | Unported |
420
+ | CTaskMissile__HandleMessage | 0x50B400 | thiscall | Unported |
421
+ | CreateExplosion | 0x548080 | usercall(ESI=context) | Unported |
422
+ | PCLandscape_ApplyExplosion | 0x57C820 | (unknown) | Unported |
423
+
424
+ ### Fire Dispatch Targets
425
+
426
+ | Function | Address | Weapon Category |
427
+ |----------|---------|-----------------|
428
+ | FireWeapon__Projectile | 0x51DFB0 | Bazooka-class |
429
+ | FireWeapon__PlacedExplosive | 0x51EC80 | Dynamite/Mine/Petrol |
430
+ | FireWeapon__Shotgun | 0x51ED90 | Shotgun/Handgun/Uzi |
431
+ | FireWeapon__RopeType1 | 0x51E1C0 | Ninja Rope attach |
432
+ | FireWeapon__RopeType3 | 0x51E240 | Bungee |
433
+ | FireWeapon__GrenadeMortar | 0x51E2C0 | Grenade/Cluster/Banana |
434
+ | FireWeapon__Special_Type2 | 0x51E3E0 | Pneumatic Drill |
435
+ | FireWeapon__Special_Type3 | 0x51E350 | Girder |
436
+ | FireWeapon__Special_Type9 | 0x51E480 | Prod |
437
+ | FireWeapon__Special_Type10 | 0x51E710 | Air Strike |
438
+ | FireWeapon__Special_Type13 | 0x51E5C0 | Napalm Strike |
439
+ | FireWeapon__Special_Type14 | 0x51E670 | Mail/Mine/Mole Strike |
440
+
441
+ ### Weapon Panel
442
+
443
+ | Function | Address | Convention | Port Status |
444
+ |----------|---------|------------|-------------|
445
+ | DDGame__InitWeaponPanel_Maybe | 0x567770 | thiscall | Unported |
446
+ | PrepareWeaponPanel | 0x568220 | cdecl | Unported |
447
+ | WeaponPanelDrawTile | 0x567AA0 | (unknown) | Unported |
448
+ | WeaponPanelDescription | 0x567F80 | (unknown) | Unported |
449
+ | WeaponPanelCheckClick | 0x5690B0 | (unknown) | Unported |
450
+
451
+ ### Scheme
452
+
453
+ | Function | Address | Convention | Port Status |
454
+ |----------|---------|------------|-------------|
455
+ | Scheme__CheckWeaponLimits | 0x4D50E0 | stdcall | **Ported** |
456
+ | Scheme__ValidateExtendedOptions | 0x4D5110 | stdcall | **Ported** |
457
+ | Scheme__ReadFile | 0x4D3890 | stdcall | **Ported** |
458
+
459
+ ## Port Recommendations
460
+
461
+ ### Already Done
462
+ - Weapon enum and struct types (`WeaponEntry`, `WeaponTable`)
463
+ - Ammo system (4 hooks: GetAmmo, AddAmmo, SubtractAmmo, CountAliveWorms)
464
+ - Availability checks (2 hooks: CheckWeaponAvail, IsSuperWeapon)
465
+ - Scheme parsing (10 hooks covering all scheme I/O)
466
+ - Team/worm state queries (8 hooks in replacements/team.rs)
467
+
468
+ ### Next Priority
469
+ 1. **InitWeaponTable internals** β€” populate WeaponEntry fields from scheme data
470
+ 2. **SubtractAmmo investigation** β€” verify hook triggers, check v2 variant
471
+ 3. **Weapon panel init** β€” DDGame__InitWeaponPanel_Maybe (self-contained)
472
+
473
+ ### Medium Term
474
+ 4. **FireWeapon dispatch** β€” the type/subtype routing logic
475
+ 5. **CreateWeaponProjectile** β€” missile allocation and pool management
476
+ 6. **WeaponRelease** β€” coordinate transform and launch logic
477
+
478
+ ### Long Term
479
+ 7. **CTaskMissile physics** β€” gravity, wind, collision, homing AI
480
+ 8. **Individual weapon behaviors** β€” each Fire__* handler
481
+ 9. **Explosion system** β€” terrain damage, damage calculation
482
+ 10. **Weapon panel rendering** β€” PrepareWeaponPanel (~2000 lines)