Merge branch 'master' into worktree-debug-cli
Browse files- crates/openwa-core/src/engine/ddgame.rs +106 -72
- crates/openwa-core/src/engine/game_state_init.rs +3 -3
- crates/openwa-core/src/engine/mod.rs +2 -2
- crates/openwa-core/src/game/weapon.rs +42 -16
- crates/openwa-core/src/task/base.rs +19 -0
- crates/openwa-core/src/task/game_task.rs +9 -2
- crates/openwa-core/src/task/mod.rs +2 -2
- crates/openwa-core/src/task/worm.rs +25 -7
- crates/openwa-debugui/src/app.rs +27 -0
- crates/openwa-wormkit/src/replacements/weapon.rs +457 -11
- crates/openwa-wormkit/src/validation/mod.rs +1 -1
- docs/re-notes/weapon-system.md +482 -0
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-
|
| 179 |
-
|
| 180 |
-
///
|
| 181 |
-
///
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 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 |
-
|
| 419 |
-
(*ddgame).
|
|
|
|
|
|
|
| 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 Γ
|
| 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
|
| 1441 |
-
/// `blocks_ptr = (tws_base as *const c_char).sub(ARENA_TO_BLOCKS) as *const
|
| 1442 |
///
|
| 1443 |
/// entry_ptr(0) = DDGame+0x4628 = TEAM_BLOCKS + 0x598.
|
| 1444 |
-
/// 0x598 = sizeof(
|
| 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
|
| 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
|
| 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
|
| 1692 |
/// 0x00-0x5F: Opaque β may hold 8th worm data from previous team.
|
| 1693 |
-
pub
|
| 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:
|
| 1705 |
-
|
|
|
|
|
|
|
|
|
|
| 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::<
|
| 1717 |
|
| 1718 |
-
/// Union for slot 0 of a
|
| 1719 |
///
|
| 1720 |
/// This slot is dual-purpose: its high offsets store team metadata
|
| 1721 |
-
/// (`
|
| 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
|
| 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<
|
| 1731 |
}
|
| 1732 |
|
| 1733 |
/// Full per-team data block (0x51C bytes, 7 blocks in DDGame).
|
| 1734 |
///
|
| 1735 |
-
/// Each block starts with a `
|
| 1736 |
-
/// dual purpose: its high offsets hold team metadata (`
|
| 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 |
-
/// `
|
| 1746 |
#[repr(C)]
|
| 1747 |
-
pub struct
|
| 1748 |
-
/// 0x000-0x09B: Header slot (union of
|
| 1749 |
/// Team metadata at high offsets; may hold 8th worm data at low offsets.
|
| 1750 |
-
pub header:
|
| 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
|
| 1755 |
}
|
| 1756 |
|
| 1757 |
-
const _: () = assert!(core::mem::size_of::<
|
| 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
|
| 1773 |
-
/// `weapon_slots[alliance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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`, `
|
| 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
|
| 1787 |
/// 0x1EB0: Number of teams in the game (used by team iteration loops)
|
| 1788 |
pub team_count: i32,
|
| 1789 |
-
/// 0x1EB4:
|
| 1790 |
-
///
|
| 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:
|
| 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
|
| 1855 |
-
pub fn get_ammo(&self,
|
| 1856 |
-
self.weapon_slots[
|
| 1857 |
}
|
| 1858 |
|
| 1859 |
-
/// Get mutable reference to ammo count
|
| 1860 |
-
pub fn ammo_mut(&mut self,
|
| 1861 |
-
&mut self.weapon_slots[
|
| 1862 |
}
|
| 1863 |
|
| 1864 |
-
/// Get delay flag for a weapon
|
| 1865 |
-
|
| 1866 |
-
|
| 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 |
-
///
|
| 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: *
|
| 1886 |
}
|
| 1887 |
|
| 1888 |
impl TeamArenaRef {
|
| 1889 |
-
/// Wrap a raw
|
| 1890 |
///
|
| 1891 |
/// # Safety
|
| 1892 |
-
/// `base` must point to
|
| 1893 |
#[inline]
|
| 1894 |
pub unsafe fn from_raw(base: u32) -> Self {
|
| 1895 |
Self {
|
| 1896 |
-
base: base as *
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1897 |
}
|
| 1898 |
}
|
| 1899 |
|
|
@@ -1909,10 +1936,10 @@ impl TeamArenaRef {
|
|
| 1909 |
&mut *(self.base as *mut TeamArenaState)
|
| 1910 |
}
|
| 1911 |
|
| 1912 |
-
/// Get pointer to the
|
| 1913 |
#[inline]
|
| 1914 |
-
pub unsafe fn blocks(&self) -> *
|
| 1915 |
-
self.base.sub(offsets::ARENA_TO_BLOCKS) as *
|
| 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) -> &
|
| 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
|
| 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) -> (&
|
| 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) -> &
|
| 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
|
| 1973 |
-
&mut (*(self.blocks() as *mut
|
| 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
|
| 1989 |
let alliance_id = self.team_header(team_index).weapon_alliance as usize;
|
| 1990 |
-
alliance_id
|
| 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
|
| 389 |
let weapon_table = (*ddgame).weapon_table;
|
| 390 |
-
let
|
| 391 |
|
| 392 |
-
if (*ddgame).level_width_raw == 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 {
|
|
|
|
| 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,
|
| 13 |
-
|
| 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 |
-
///
|
| 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:
|
| 117 |
-
pub
|
| 118 |
/// +0x0C: Unknown.
|
| 119 |
pub _unknown_0c: i32,
|
| 120 |
-
/// +0x10
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
const _: () = assert!(core::mem::size_of::<WeaponEntry>() == 0x1D0);
|
| 124 |
|
| 125 |
-
/// Weapon
|
| 126 |
///
|
| 127 |
-
///
|
| 128 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
#[repr(C)]
|
| 130 |
pub struct WeaponTable {
|
| 131 |
-
///
|
| 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:
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 176 |
-
///
|
| 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
|
| 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
|
| 28 |
let state = arena.state_mut();
|
| 29 |
-
let ammo = state.get_ammo(
|
| 30 |
if ammo >= 0 {
|
| 31 |
if amount < 0 {
|
| 32 |
-
*state.ammo_mut(
|
| 33 |
} else {
|
| 34 |
-
*state.ammo_mut(
|
| 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
|
| 50 |
let state = arena.state_mut();
|
| 51 |
-
let ammo = state.get_ammo(
|
| 52 |
if ammo > 0 {
|
| 53 |
-
*state.ammo_mut(
|
| 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
|
| 72 |
let state = arena.state();
|
| 73 |
|
| 74 |
// Check weapon delay
|
| 75 |
-
if state.get_delay(
|
| 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(
|
| 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
|
| 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)
|