File size: 7,865 Bytes
106f839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/usr/bin/env python3
"""Analyze a raw weapon table dump from the debug CLI.

Usage:
    python tools/analyze_weapon_table.py /tmp/weapon_table.bin

Reads 71 x 0x1D0 byte entries and prints field analysis.
"""

import struct
import sys
from pathlib import Path

ENTRY_SIZE = 0x1D0
ENTRY_COUNT = 71
FIRE_PARAMS_OFFSET = 0x3C

WEAPON_NAMES = [
    "None", "Bazooka", "HomingMissile", "Mortar", "HomingPigeon",
    "SheepLauncher", "Grenade", "ClusterBomb", "BananaBomb", "BattleAxe",
    "Earthquake", "Shotgun", "Handgun", "Uzi", "Minigun",
    "Longbow", "FirePunch", "DragonBall", "Kamikaze", "SuicideBomber",
    "Prod", "Dynamite", "Mine", "Sheep", "SuperSheep",
    "AquaSheep", "MoleBomb", "AirStrike", "NapalmStrike", "MailStrike",
    "MineStrike", "MoleSquadron", "BlowTorch", "PneumaticDrill", "Girder",
    "BaseballBat", "GirderPack", "NinjaRope", "Bungee", "Parachute",
    "Teleport", "ScalesOfJustice", "SuperBanana", "HolyGrenade", "FlameThrower",
    "SalvationArmy", "MbBomb", "PetrolBomb", "Skunk", "MingVase",
    "SheepStrike", "CarpetBomb", "MadCow", "OldWoman", "Donkey",
    "NuclearTest", "Armageddon", "SkipGo", "Surrender", "SelectWorm",
    "Freeze", "MagicBullet", "JetPack", "LowGravity", "FastWalk",
    "LaserSight", "Invisibility", "DamageX2", "CrateSpy", "DoubleTurnTime",
    "CrateShower",
]

FIRE_TYPE_NAMES = {0: "none", 1: "projectile", 2: "rope", 3: "grenade", 4: "special"}

# Known WeaponEntry fields (offset -> name)
KNOWN_FIELDS = {
    0x00: "name1_ptr",
    0x04: "name2_ptr",
    0x08: "panel_state",
    0x0C: "_unknown_0c",
    0x10: "defined",
    0x24: "availability",
    0x28: "enabled",
    0x30: "fire_type",
    0x34: "fire_subtype_34",
    0x38: "fire_subtype_38",
}


def read_entries(data: bytes) -> list[bytes]:
    assert len(data) == ENTRY_SIZE * ENTRY_COUNT, f"Expected {ENTRY_SIZE * ENTRY_COUNT} bytes, got {len(data)}"
    return [data[i * ENTRY_SIZE:(i + 1) * ENTRY_SIZE] for i in range(ENTRY_COUNT)]


def dword_at(entry: bytes, offset: int) -> int:
    return struct.unpack_from("<I", entry, offset)[0]


def sdword_at(entry: bytes, offset: int) -> int:
    return struct.unpack_from("<i", entry, offset)[0]


def print_header_fields(entries: list[bytes]):
    """Print known header fields (0x00-0x3B) for all weapons."""
    print("=" * 80)
    print("WEAPON ENTRY HEADER (0x00-0x3B)")
    print("=" * 80)

    fmt = "{:<4} {:<16} {:>4} {:>4} {:>4} {:>12} {:>4}  {:>4} {:>4} {:>4}"
    print(fmt.format("ID", "Name", "panl", "unk0C", "def", "avail", "enab", "ftyp", "sub34", "sub38"))
    print("-" * 80)

    for i, entry in enumerate(entries):
        name = WEAPON_NAMES[i]
        panel = sdword_at(entry, 0x08)
        unk0c = sdword_at(entry, 0x0C)
        defined = sdword_at(entry, 0x10)
        avail = sdword_at(entry, 0x24)
        enabled = sdword_at(entry, 0x28)
        ftype = sdword_at(entry, 0x30)
        sub34 = sdword_at(entry, 0x34)
        sub38 = sdword_at(entry, 0x38)

        type_name = FIRE_TYPE_NAMES.get(ftype, f"?{ftype}")
        print(f"{i:<4} {name:<16} {panel:>4} {unk0c:>4} {defined:>4} {avail:>12} {enabled:>4}  {type_name:<11} {sub34:>4} {sub38:>4}")


def print_unknown_header_region(entries: list[bytes]):
    """Print the unknown 0x14-0x23 region."""
    print("\n" + "=" * 80)
    print("UNKNOWN HEADER REGION (0x14-0x23)")
    print("=" * 80)

    for i, entry in enumerate(entries):
        vals = [dword_at(entry, off) for off in range(0x14, 0x24, 4)]
        if any(v != 0 for v in vals):
            print(f"{i:<4} {WEAPON_NAMES[i]:<16} " + " ".join(f"{v:08X}" for v in vals))
    print("(only non-zero entries shown)")


def analyze_fire_params(entries: list[bytes]):
    """Analyze fire_params region (0x3C-0x1CF) across all weapons."""
    print("\n" + "=" * 80)
    print("FIRE PARAMS ANALYSIS (0x3C-0x1CF, 0x194 bytes = 101 DWORDs)")
    print("=" * 80)

    num_dwords = (ENTRY_SIZE - FIRE_PARAMS_OFFSET) // 4

    # For each DWORD offset, collect all values
    interesting = []
    all_zero = []

    for di in range(num_dwords):
        byte_off = FIRE_PARAMS_OFFSET + di * 4
        values = [dword_at(entry, byte_off) for entry in entries]
        unique = set(values)
        nonzero = [v for v in values if v != 0]

        if len(unique) == 1 and values[0] == 0:
            all_zero.append(di)
        elif len(unique) <= 3:
            interesting.append((di, byte_off, values, "few_values"))
        else:
            interesting.append((di, byte_off, values, "varied"))

    print(f"\nAll-zero DWORDs ({len(all_zero)} of {num_dwords}): indices {all_zero[:20]}{'...' if len(all_zero) > 20 else ''}")
    print(f"Non-trivial DWORDs: {len(interesting)}")

    # Print interesting fields grouped by pattern
    print("\n--- Fields with few distinct values (likely flags/enums) ---")
    for di, byte_off, values, kind in interesting:
        if kind != "few_values":
            continue
        unique = sorted(set(values))
        # Which weapons have each value?
        val_weapons = {}
        for i, v in enumerate(values):
            val_weapons.setdefault(v, []).append(i)

        print(f"\n  +0x{byte_off:03X} (fire_params[{di - FIRE_PARAMS_OFFSET // 4}]):")
        for v in unique:
            weapons = val_weapons[v]
            names = [WEAPON_NAMES[w] for w in weapons[:8]]
            suffix = f" +{len(weapons)-8} more" if len(weapons) > 8 else ""
            print(f"    {v:>10} ({v:#010x}): {', '.join(names)}{suffix}")

    print("\n--- Fields with many distinct values (likely parameters) ---")
    print(f"{'Offset':<10} {'Min':>10} {'Max':>10} {'Unique':>6}  Sample values (Bazooka, Grenade, Shotgun, NinjaRope)")
    print("-" * 90)

    for di, byte_off, values, kind in interesting:
        if kind != "varied":
            continue
        unique_count = len(set(values))
        min_v = min(values)
        max_v = max(values)
        samples = [values[1], values[6], values[11], values[37]]  # Bazooka, Grenade, Shotgun, NinjaRope

        sample_str = ", ".join(f"{v:>8}" for v in samples)
        print(f"+0x{byte_off:03X}     {min_v:>10} {max_v:>10} {unique_count:>6}  [{sample_str}]")


def print_fire_params_by_type(entries: list[bytes]):
    """Group weapons by fire_type and print fire_params for each group."""
    groups: dict[int, list[int]] = {}
    for i, entry in enumerate(entries):
        ftype = sdword_at(entry, 0x30)
        groups.setdefault(ftype, []).append(i)

    print("\n" + "=" * 80)
    print("FIRE PARAMS BY WEAPON TYPE")
    print("=" * 80)

    for ftype in sorted(groups.keys()):
        if ftype == 0:
            continue
        type_name = FIRE_TYPE_NAMES.get(ftype, f"type_{ftype}")
        weapon_ids = groups[ftype]
        print(f"\n--- Type {ftype} ({type_name}): {len(weapon_ids)} weapons ---")
        print(f"Weapons: {', '.join(WEAPON_NAMES[w] for w in weapon_ids)}")

        # Find fields that vary within this type group
        num_dwords = (ENTRY_SIZE - FIRE_PARAMS_OFFSET) // 4
        for di in range(min(num_dwords, 40)):  # First 40 DWORDs for readability
            byte_off = FIRE_PARAMS_OFFSET + di * 4
            values = {w: dword_at(entries[w], byte_off) for w in weapon_ids}
            unique = set(values.values())
            if len(unique) > 1 or (len(unique) == 1 and list(unique)[0] != 0):
                val_str = ", ".join(f"{WEAPON_NAMES[w][:8]}={values[w]}" for w in weapon_ids[:6])
                print(f"  +0x{byte_off:03X} fp[{di}]: {val_str}")


def main():
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <weapon_table.bin>")
        sys.exit(1)

    data = Path(sys.argv[1]).read_bytes()
    entries = read_entries(data)

    print_header_fields(entries)
    print_unknown_header_region(entries)
    analyze_fire_params(entries)
    print_fire_params_by_type(entries)


if __name__ == "__main__":
    main()