feat: map WeaponEntry and WeaponFireParams fields from live memory
Browse filesDumped full WeaponTable (71 entries) via debug CLI and cross-referenced
with CTaskMissile constructor decompilation. Key findings:
- fire_params[0..94] copied verbatim into CTaskMissile.weapon_data
- Fields are polymorphic: different meanings for missile vs hitscan weapons
- Named 28+ fields: gravity_pct, bounce_pct, friction_pct, missile_type,
blast_radius, sprite_id, collision_radius (Fixed), render_size (Fixed)
- Hitscan damage at fp[8] (Shotgun=25, confirmed by gameplay)
- WeaponEntry header: requires_aiming, shot_count, retreat_time, creates_projectile
- MissileType corrected: 1=Homing, 2=Standard, 3=Sheep (was 2/3/4)
Includes tools/analyze_weapon_table.py for offline dump analysis.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/game/weapon.rs +110 -8
- tools/analyze_weapon_table.py +211 -0
|
@@ -1,5 +1,7 @@
|
|
| 1 |
use core::ffi::c_char;
|
| 2 |
|
|
|
|
|
|
|
| 3 |
/// Weapon types. Contiguous range 0-70.
|
| 4 |
///
|
| 5 |
/// Source: wkJellyWorm Constants.h
|
|
@@ -114,13 +116,22 @@ pub struct WeaponEntry {
|
|
| 114 |
pub name2: *const c_char,
|
| 115 |
/// +0x08: Panel state (init: 0xFFFFFFFF). wkJellyWorm calls this `panelRow`.
|
| 116 |
pub panel_state: i32,
|
| 117 |
-
/// +0x0C:
|
| 118 |
-
|
|
|
|
| 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
|
| 123 |
-
pub
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
@@ -141,14 +152,105 @@ pub struct WeaponEntry {
|
|
| 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 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
#[repr(C)]
|
| 149 |
pub struct WeaponFireParams {
|
| 150 |
-
///
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
const _: () = assert!(core::mem::size_of::<WeaponFireParams>() == 0x1D0 - 0x3C);
|
| 154 |
|
|
|
|
| 1 |
use core::ffi::c_char;
|
| 2 |
|
| 3 |
+
use crate::fixed::Fixed;
|
| 4 |
+
|
| 5 |
/// Weapon types. Contiguous range 0-70.
|
| 6 |
///
|
| 7 |
/// Source: wkJellyWorm Constants.h
|
|
|
|
| 116 |
pub name2: *const c_char,
|
| 117 |
/// +0x08: Panel state (init: 0xFFFFFFFF). wkJellyWorm calls this `panelRow`.
|
| 118 |
pub panel_state: i32,
|
| 119 |
+
/// +0x0C: Requires aiming (1 for aimed weapons like Bazooka, 0 for non-aimed like Earthquake).
|
| 120 |
+
/// Runtime-observed: 1 for Bazooka/Grenade/Shotgun, 0 for AirStrike/HomingMissile/Teleport.
|
| 121 |
+
pub requires_aiming: i32,
|
| 122 |
/// +0x10: Weapon defined flag. Nonzero = weapon exists in table.
|
| 123 |
/// Checked by DDGame__CheckWeaponAvail to determine if weapon is valid.
|
| 124 |
pub defined: i32,
|
| 125 |
+
/// +0x14: Shot count per use (1 for most, 2 for Shotgun/Longbow, 5 for GirderPack).
|
| 126 |
+
pub shot_count: i32,
|
| 127 |
+
/// +0x18: Unknown flag (0 or 1). 0 for NinjaRope/Bungee/Parachute/SelectWorm/JetPack/powerups.
|
| 128 |
+
pub _unknown_18: i32,
|
| 129 |
+
/// +0x1C: Retreat timer in ms (0xBB8=3000 for most, 0x1388=5000 for Dynamite/Mine/MingVase,
|
| 130 |
+
/// -1 for non-retreating, -1000 for PneumaticDrill/Teleport, 0 for utility/powerups).
|
| 131 |
+
pub retreat_time: i32,
|
| 132 |
+
/// +0x20: Creates projectile flag (1 for weapons that fire a physical object, 0 for utility).
|
| 133 |
+
/// 0 for Prod/Girder/NinjaRope/Parachute/Teleport/utility weapons.
|
| 134 |
+
pub creates_projectile: i32,
|
| 135 |
/// +0x24: Availability flag. Init: 0xFFFFFFFF, then set to 0 (unavailable)
|
| 136 |
/// or 1 (available) per weapon. Weapon::None, SkipGo, Surrender default to 0.
|
| 137 |
pub availability: i32,
|
|
|
|
| 152 |
}
|
| 153 |
const _: () = assert!(core::mem::size_of::<WeaponEntry>() == 0x1D0);
|
| 154 |
|
| 155 |
+
/// Weapon fire parameters — embedded at WeaponEntry+0x3C (0x194 = 404 bytes, 101 DWORDs).
|
| 156 |
///
|
| 157 |
/// Pointer to this struct is passed to all fire dispatch sub-functions.
|
| 158 |
+
/// The first 94 DWORDs (0x178 bytes) are copied verbatim into CTaskMissile.weapon_data
|
| 159 |
+
/// by CTaskMissile__Constructor. The remaining 7 DWORDs are WeaponEntry-only metadata.
|
| 160 |
+
///
|
| 161 |
+
/// For single-shot projectiles, CTaskMissile.render_data[N] = weapon_data[N+3].
|
| 162 |
+
/// For cluster sub-pellets, render_data[N] = weapon_data[N+52].
|
| 163 |
+
///
|
| 164 |
+
/// Field names confirmed by cross-referencing live memory dumps (debug CLI),
|
| 165 |
+
/// CTaskMissile constructor decompilation, and render_data physics usage.
|
| 166 |
#[repr(C)]
|
| 167 |
pub struct WeaponFireParams {
|
| 168 |
+
/// [0] +0x3C: Polymorphic.
|
| 169 |
+
/// Missile: pellet count (Bazooka=2, Mortar=2).
|
| 170 |
+
/// Hitscan: shots per trigger (Shotgun=1, Handgun=6, Uzi=10, Minigun=20).
|
| 171 |
+
/// Airstrike: number of projectiles (AirStrike=5).
|
| 172 |
+
pub shot_count: i32,
|
| 173 |
+
/// [1] +0x40: Polymorphic.
|
| 174 |
+
/// Missile: spread angle for multi-pellet (Mortar=100).
|
| 175 |
+
/// Hitscan: spread cone (Shotgun=500, Handgun=500).
|
| 176 |
+
/// Airstrike: spacing (NapalmStrike=48).
|
| 177 |
+
pub spread: i32,
|
| 178 |
+
/// [2] +0x44: Polymorphic.
|
| 179 |
+
/// Missile: cluster flag (Grenade=1, ClusterBomb=1, BananaBomb=1).
|
| 180 |
+
/// Hitscan: fire delay between shots (Handgun=10, Uzi=15, Minigun=25).
|
| 181 |
+
pub _fp_02: i32,
|
| 182 |
+
/// [3] +0x48: Polymorphic.
|
| 183 |
+
/// Missile: collision radius (Bazooka=2.10, HomingPigeon=66.1).
|
| 184 |
+
/// Hitscan: always Fixed(1) — flag.
|
| 185 |
+
pub collision_radius: Fixed,
|
| 186 |
+
/// [4] +0x4C: Missile only: unknown (HomingMissile=5, HomingPigeon=25, Grenade=10).
|
| 187 |
+
pub _fp_04: i32,
|
| 188 |
+
/// [5] +0x50: Polymorphic.
|
| 189 |
+
/// Missile: explosion damage (Bazooka=100, Grenade=100, Mortar=0).
|
| 190 |
+
/// Hitscan: max range (all=66.1 as Fixed16.16).
|
| 191 |
+
pub _fp_05: i32,
|
| 192 |
+
/// [6] +0x54: Polymorphic.
|
| 193 |
+
/// Missile: blast radius (Bazooka=50, Mortar=15, Grenade=50).
|
| 194 |
+
/// Hitscan: impact radius (Shotgun=5, Minigun=20).
|
| 195 |
+
pub _fp_06: i32,
|
| 196 |
+
/// [7] +0x58: Hitscan only: unknown (Shotgun=100, Handgun=50, Uzi/Minigun=100).
|
| 197 |
+
pub _fp_07: i32,
|
| 198 |
+
/// [8] +0x5C: Hitscan only: damage per hit (Shotgun=25, Handgun/Uzi/Minigun=5).
|
| 199 |
+
pub _fp_08: i32,
|
| 200 |
+
/// [9] +0x60: Missile only: sprite/animation ID (Bazooka=48, Grenade=50, HomingPigeon=175).
|
| 201 |
+
pub sprite_id: i32,
|
| 202 |
+
/// [10] +0x64: Shot type/impact type (Missile: Bazooka=2, Grenade=1. Hitscan: Shotgun=2, Uzi=5).
|
| 203 |
+
pub impact_type: i32,
|
| 204 |
+
/// [11] +0x68: Polymorphic.
|
| 205 |
+
/// Missile: unknown (Bazooka=131).
|
| 206 |
+
/// Hitscan: max range in pixels (all=32767).
|
| 207 |
+
pub _fp_11: i32,
|
| 208 |
+
/// [12] +0x6C: Trail effect (Bazooka=50, Mortar=50, most weapons=0).
|
| 209 |
+
pub trail_effect: i32,
|
| 210 |
+
/// [13] +0x70: Gravity percentage (100=normal, 0=no gravity).
|
| 211 |
+
/// → render_data[0x0C] → CGameTask.gravity_factor.
|
| 212 |
+
pub gravity_pct: i32,
|
| 213 |
+
/// [14] +0x74: Wind influence (Bazooka=50, AquaSheep=200).
|
| 214 |
+
pub wind_influence: i32,
|
| 215 |
+
/// [15] +0x78: Bounce percentage (100=normal elastic, 0=no bounce).
|
| 216 |
+
/// → render_data[0x0D] → CGameTask.bounce_factor.
|
| 217 |
+
pub bounce_pct: i32,
|
| 218 |
+
/// [16] +0x7C: Unknown (Bazooka=100, most=0).
|
| 219 |
+
pub _fp_16: i32,
|
| 220 |
+
/// [17] +0x80: Unknown (AirStrike=10, MoleSquadron=50).
|
| 221 |
+
pub _fp_17: i32,
|
| 222 |
+
/// [18] +0x84: Friction percentage (100=normal, 0=no friction).
|
| 223 |
+
/// → render_data[0x0F] → CGameTask.friction_factor.
|
| 224 |
+
pub friction_pct: i32,
|
| 225 |
+
/// [19] +0x88: Explosion delay/fuse timer (Grenade=5000, Dynamite=5000).
|
| 226 |
+
pub explosion_delay: i32,
|
| 227 |
+
/// [20] +0x8C: Fuse timer (Bazooka=9000, HomingMissile=10000, SheepLauncher=20000).
|
| 228 |
+
pub fuse_timer: i32,
|
| 229 |
+
/// [21-25] +0x90-0xA0: Various parameters. Remaining primary fields.
|
| 230 |
+
pub _fp_21_25: [i32; 5],
|
| 231 |
+
/// [26] +0xA4: Missile type discriminator.
|
| 232 |
+
/// 0=None, 1=Homing, 2=Standard, 3=Sheep, 5=SheepLauncher/Cluster.
|
| 233 |
+
/// → render_data[0x17] → CTaskMissile behavior dispatch.
|
| 234 |
+
pub missile_type: i32,
|
| 235 |
+
/// [27] +0xA8: Render size. Bazooka=64.0, Grenade≈66.1.
|
| 236 |
+
pub render_size: Fixed,
|
| 237 |
+
/// [28] +0xAC: Render timer/fuse (Bazooka=1, HomingMissile=58, HomingPigeon=175).
|
| 238 |
+
pub render_timer: i32,
|
| 239 |
+
/// [29-33] +0xB0-0xC0: Homing parameters (only nonzero for homing weapons).
|
| 240 |
+
/// [29]=homing_strength, [30]=homing_sprite, [31]=homing_turn_rate,
|
| 241 |
+
/// [32]=homing_accel, [33]=homing_speed.
|
| 242 |
+
pub homing_params: [i32; 5],
|
| 243 |
+
/// [34-36] +0xC4-0xCC: Additional parameters.
|
| 244 |
+
pub _fp_34_36: [i32; 3],
|
| 245 |
+
/// [37-51] +0xD0-0x10C: Reserved / weapon-specific extended params.
|
| 246 |
+
pub _fp_37_51: [i32; 15],
|
| 247 |
+
/// [52-93] +0x10C-0x1B0: Cluster sub-pellet parameters (mirrors primary [0-41]).
|
| 248 |
+
/// When pellet_index > 0, render_data copies from here instead.
|
| 249 |
+
pub cluster_params: [i32; 42],
|
| 250 |
+
/// [94-100] +0x1B0-0x1CC: WeaponEntry-only metadata (NOT copied to CTaskMissile).
|
| 251 |
+
/// [94]+0x1C8: Power percentage (Bazooka=100, Shotgun=10, NinjaRope=100).
|
| 252 |
+
/// [95]+0x1CC: Unknown (Bazooka=100, Grenade=70, Shotgun=20).
|
| 253 |
+
pub entry_metadata: [i32; 7],
|
| 254 |
}
|
| 255 |
const _: () = assert!(core::mem::size_of::<WeaponFireParams>() == 0x1D0 - 0x3C);
|
| 256 |
|
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Analyze a raw weapon table dump from the debug CLI.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
python tools/analyze_weapon_table.py /tmp/weapon_table.bin
|
| 6 |
+
|
| 7 |
+
Reads 71 x 0x1D0 byte entries and prints field analysis.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import struct
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
ENTRY_SIZE = 0x1D0
|
| 15 |
+
ENTRY_COUNT = 71
|
| 16 |
+
FIRE_PARAMS_OFFSET = 0x3C
|
| 17 |
+
|
| 18 |
+
WEAPON_NAMES = [
|
| 19 |
+
"None", "Bazooka", "HomingMissile", "Mortar", "HomingPigeon",
|
| 20 |
+
"SheepLauncher", "Grenade", "ClusterBomb", "BananaBomb", "BattleAxe",
|
| 21 |
+
"Earthquake", "Shotgun", "Handgun", "Uzi", "Minigun",
|
| 22 |
+
"Longbow", "FirePunch", "DragonBall", "Kamikaze", "SuicideBomber",
|
| 23 |
+
"Prod", "Dynamite", "Mine", "Sheep", "SuperSheep",
|
| 24 |
+
"AquaSheep", "MoleBomb", "AirStrike", "NapalmStrike", "MailStrike",
|
| 25 |
+
"MineStrike", "MoleSquadron", "BlowTorch", "PneumaticDrill", "Girder",
|
| 26 |
+
"BaseballBat", "GirderPack", "NinjaRope", "Bungee", "Parachute",
|
| 27 |
+
"Teleport", "ScalesOfJustice", "SuperBanana", "HolyGrenade", "FlameThrower",
|
| 28 |
+
"SalvationArmy", "MbBomb", "PetrolBomb", "Skunk", "MingVase",
|
| 29 |
+
"SheepStrike", "CarpetBomb", "MadCow", "OldWoman", "Donkey",
|
| 30 |
+
"NuclearTest", "Armageddon", "SkipGo", "Surrender", "SelectWorm",
|
| 31 |
+
"Freeze", "MagicBullet", "JetPack", "LowGravity", "FastWalk",
|
| 32 |
+
"LaserSight", "Invisibility", "DamageX2", "CrateSpy", "DoubleTurnTime",
|
| 33 |
+
"CrateShower",
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
FIRE_TYPE_NAMES = {0: "none", 1: "projectile", 2: "rope", 3: "grenade", 4: "special"}
|
| 37 |
+
|
| 38 |
+
# Known WeaponEntry fields (offset -> name)
|
| 39 |
+
KNOWN_FIELDS = {
|
| 40 |
+
0x00: "name1_ptr",
|
| 41 |
+
0x04: "name2_ptr",
|
| 42 |
+
0x08: "panel_state",
|
| 43 |
+
0x0C: "_unknown_0c",
|
| 44 |
+
0x10: "defined",
|
| 45 |
+
0x24: "availability",
|
| 46 |
+
0x28: "enabled",
|
| 47 |
+
0x30: "fire_type",
|
| 48 |
+
0x34: "fire_subtype_34",
|
| 49 |
+
0x38: "fire_subtype_38",
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def read_entries(data: bytes) -> list[bytes]:
|
| 54 |
+
assert len(data) == ENTRY_SIZE * ENTRY_COUNT, f"Expected {ENTRY_SIZE * ENTRY_COUNT} bytes, got {len(data)}"
|
| 55 |
+
return [data[i * ENTRY_SIZE:(i + 1) * ENTRY_SIZE] for i in range(ENTRY_COUNT)]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def dword_at(entry: bytes, offset: int) -> int:
|
| 59 |
+
return struct.unpack_from("<I", entry, offset)[0]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def sdword_at(entry: bytes, offset: int) -> int:
|
| 63 |
+
return struct.unpack_from("<i", entry, offset)[0]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def print_header_fields(entries: list[bytes]):
|
| 67 |
+
"""Print known header fields (0x00-0x3B) for all weapons."""
|
| 68 |
+
print("=" * 80)
|
| 69 |
+
print("WEAPON ENTRY HEADER (0x00-0x3B)")
|
| 70 |
+
print("=" * 80)
|
| 71 |
+
|
| 72 |
+
fmt = "{:<4} {:<16} {:>4} {:>4} {:>4} {:>12} {:>4} {:>4} {:>4} {:>4}"
|
| 73 |
+
print(fmt.format("ID", "Name", "panl", "unk0C", "def", "avail", "enab", "ftyp", "sub34", "sub38"))
|
| 74 |
+
print("-" * 80)
|
| 75 |
+
|
| 76 |
+
for i, entry in enumerate(entries):
|
| 77 |
+
name = WEAPON_NAMES[i]
|
| 78 |
+
panel = sdword_at(entry, 0x08)
|
| 79 |
+
unk0c = sdword_at(entry, 0x0C)
|
| 80 |
+
defined = sdword_at(entry, 0x10)
|
| 81 |
+
avail = sdword_at(entry, 0x24)
|
| 82 |
+
enabled = sdword_at(entry, 0x28)
|
| 83 |
+
ftype = sdword_at(entry, 0x30)
|
| 84 |
+
sub34 = sdword_at(entry, 0x34)
|
| 85 |
+
sub38 = sdword_at(entry, 0x38)
|
| 86 |
+
|
| 87 |
+
type_name = FIRE_TYPE_NAMES.get(ftype, f"?{ftype}")
|
| 88 |
+
print(f"{i:<4} {name:<16} {panel:>4} {unk0c:>4} {defined:>4} {avail:>12} {enabled:>4} {type_name:<11} {sub34:>4} {sub38:>4}")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def print_unknown_header_region(entries: list[bytes]):
|
| 92 |
+
"""Print the unknown 0x14-0x23 region."""
|
| 93 |
+
print("\n" + "=" * 80)
|
| 94 |
+
print("UNKNOWN HEADER REGION (0x14-0x23)")
|
| 95 |
+
print("=" * 80)
|
| 96 |
+
|
| 97 |
+
for i, entry in enumerate(entries):
|
| 98 |
+
vals = [dword_at(entry, off) for off in range(0x14, 0x24, 4)]
|
| 99 |
+
if any(v != 0 for v in vals):
|
| 100 |
+
print(f"{i:<4} {WEAPON_NAMES[i]:<16} " + " ".join(f"{v:08X}" for v in vals))
|
| 101 |
+
print("(only non-zero entries shown)")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def analyze_fire_params(entries: list[bytes]):
|
| 105 |
+
"""Analyze fire_params region (0x3C-0x1CF) across all weapons."""
|
| 106 |
+
print("\n" + "=" * 80)
|
| 107 |
+
print("FIRE PARAMS ANALYSIS (0x3C-0x1CF, 0x194 bytes = 101 DWORDs)")
|
| 108 |
+
print("=" * 80)
|
| 109 |
+
|
| 110 |
+
num_dwords = (ENTRY_SIZE - FIRE_PARAMS_OFFSET) // 4
|
| 111 |
+
|
| 112 |
+
# For each DWORD offset, collect all values
|
| 113 |
+
interesting = []
|
| 114 |
+
all_zero = []
|
| 115 |
+
|
| 116 |
+
for di in range(num_dwords):
|
| 117 |
+
byte_off = FIRE_PARAMS_OFFSET + di * 4
|
| 118 |
+
values = [dword_at(entry, byte_off) for entry in entries]
|
| 119 |
+
unique = set(values)
|
| 120 |
+
nonzero = [v for v in values if v != 0]
|
| 121 |
+
|
| 122 |
+
if len(unique) == 1 and values[0] == 0:
|
| 123 |
+
all_zero.append(di)
|
| 124 |
+
elif len(unique) <= 3:
|
| 125 |
+
interesting.append((di, byte_off, values, "few_values"))
|
| 126 |
+
else:
|
| 127 |
+
interesting.append((di, byte_off, values, "varied"))
|
| 128 |
+
|
| 129 |
+
print(f"\nAll-zero DWORDs ({len(all_zero)} of {num_dwords}): indices {all_zero[:20]}{'...' if len(all_zero) > 20 else ''}")
|
| 130 |
+
print(f"Non-trivial DWORDs: {len(interesting)}")
|
| 131 |
+
|
| 132 |
+
# Print interesting fields grouped by pattern
|
| 133 |
+
print("\n--- Fields with few distinct values (likely flags/enums) ---")
|
| 134 |
+
for di, byte_off, values, kind in interesting:
|
| 135 |
+
if kind != "few_values":
|
| 136 |
+
continue
|
| 137 |
+
unique = sorted(set(values))
|
| 138 |
+
# Which weapons have each value?
|
| 139 |
+
val_weapons = {}
|
| 140 |
+
for i, v in enumerate(values):
|
| 141 |
+
val_weapons.setdefault(v, []).append(i)
|
| 142 |
+
|
| 143 |
+
print(f"\n +0x{byte_off:03X} (fire_params[{di - FIRE_PARAMS_OFFSET // 4}]):")
|
| 144 |
+
for v in unique:
|
| 145 |
+
weapons = val_weapons[v]
|
| 146 |
+
names = [WEAPON_NAMES[w] for w in weapons[:8]]
|
| 147 |
+
suffix = f" +{len(weapons)-8} more" if len(weapons) > 8 else ""
|
| 148 |
+
print(f" {v:>10} ({v:#010x}): {', '.join(names)}{suffix}")
|
| 149 |
+
|
| 150 |
+
print("\n--- Fields with many distinct values (likely parameters) ---")
|
| 151 |
+
print(f"{'Offset':<10} {'Min':>10} {'Max':>10} {'Unique':>6} Sample values (Bazooka, Grenade, Shotgun, NinjaRope)")
|
| 152 |
+
print("-" * 90)
|
| 153 |
+
|
| 154 |
+
for di, byte_off, values, kind in interesting:
|
| 155 |
+
if kind != "varied":
|
| 156 |
+
continue
|
| 157 |
+
unique_count = len(set(values))
|
| 158 |
+
min_v = min(values)
|
| 159 |
+
max_v = max(values)
|
| 160 |
+
samples = [values[1], values[6], values[11], values[37]] # Bazooka, Grenade, Shotgun, NinjaRope
|
| 161 |
+
|
| 162 |
+
sample_str = ", ".join(f"{v:>8}" for v in samples)
|
| 163 |
+
print(f"+0x{byte_off:03X} {min_v:>10} {max_v:>10} {unique_count:>6} [{sample_str}]")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def print_fire_params_by_type(entries: list[bytes]):
|
| 167 |
+
"""Group weapons by fire_type and print fire_params for each group."""
|
| 168 |
+
groups: dict[int, list[int]] = {}
|
| 169 |
+
for i, entry in enumerate(entries):
|
| 170 |
+
ftype = sdword_at(entry, 0x30)
|
| 171 |
+
groups.setdefault(ftype, []).append(i)
|
| 172 |
+
|
| 173 |
+
print("\n" + "=" * 80)
|
| 174 |
+
print("FIRE PARAMS BY WEAPON TYPE")
|
| 175 |
+
print("=" * 80)
|
| 176 |
+
|
| 177 |
+
for ftype in sorted(groups.keys()):
|
| 178 |
+
if ftype == 0:
|
| 179 |
+
continue
|
| 180 |
+
type_name = FIRE_TYPE_NAMES.get(ftype, f"type_{ftype}")
|
| 181 |
+
weapon_ids = groups[ftype]
|
| 182 |
+
print(f"\n--- Type {ftype} ({type_name}): {len(weapon_ids)} weapons ---")
|
| 183 |
+
print(f"Weapons: {', '.join(WEAPON_NAMES[w] for w in weapon_ids)}")
|
| 184 |
+
|
| 185 |
+
# Find fields that vary within this type group
|
| 186 |
+
num_dwords = (ENTRY_SIZE - FIRE_PARAMS_OFFSET) // 4
|
| 187 |
+
for di in range(min(num_dwords, 40)): # First 40 DWORDs for readability
|
| 188 |
+
byte_off = FIRE_PARAMS_OFFSET + di * 4
|
| 189 |
+
values = {w: dword_at(entries[w], byte_off) for w in weapon_ids}
|
| 190 |
+
unique = set(values.values())
|
| 191 |
+
if len(unique) > 1 or (len(unique) == 1 and list(unique)[0] != 0):
|
| 192 |
+
val_str = ", ".join(f"{WEAPON_NAMES[w][:8]}={values[w]}" for w in weapon_ids[:6])
|
| 193 |
+
print(f" +0x{byte_off:03X} fp[{di}]: {val_str}")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def main():
|
| 197 |
+
if len(sys.argv) < 2:
|
| 198 |
+
print(f"Usage: {sys.argv[0]} <weapon_table.bin>")
|
| 199 |
+
sys.exit(1)
|
| 200 |
+
|
| 201 |
+
data = Path(sys.argv[1]).read_bytes()
|
| 202 |
+
entries = read_entries(data)
|
| 203 |
+
|
| 204 |
+
print_header_fields(entries)
|
| 205 |
+
print_unknown_header_region(entries)
|
| 206 |
+
analyze_fire_params(entries)
|
| 207 |
+
print_fire_params_by_type(entries)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
main()
|