# MECHSPECSHEET Generates a stock-mech specification spreadsheet directly from the FireStorm source tree, so the numbers come from the data the game actually loads instead of being transcribed by hand. python3 generate-mech-specs.py # writes mech-specifications-5.1.0b.xlsx python3 verify-mech-specs.py # checks the result, prints PASS/ISSUES Both scripts are standard library only. An `.xlsx` is a zip of XML, and only `xl/worksheets/sheet1.xml` is rewritten, so the template's fonts, column widths and drawings survive untouched. No `openpyxl` needed. ## Contents | File | What it is | |---|---| | `generate-mech-specs.py` | The extractor. Reads the roster from `core.build`, parses each chassis, writes the workbook. | | `verify-mech-specs.py` | Five independent checks on a generated workbook. | | `template/mech-spec-template.xlsx` | The original 5.0.7D workbook, kept as the layout template. Its single hand-made Battlemaster row is the calibration reference. | | `mech-specifications-5.1.0b.xlsx` | Current output: 65 stock chassis, 55 columns. | ## Current output 65 chassis (37 Inner Sphere, 28 Clan; 23 Assault, 17 Heavy, 13 Medium, 12 Light). Every non-weapon column is populated for every chassis. The roster is whatever `Content/core.build` registers, via lines of the form `instance=mechs\\.instance`. The six staged V4H chassis (Champion, Dasher, Griffin, Jenner IIC, Marauder, Thunderbolt) are **not** included because they are not registered yet; they will appear automatically once they are, with no change to this tool. ## Column reference Columns 1-24 and 51-55 come from the template. Columns 25-50 are the weapon grid, widened from the original (see *Layout changes*). | # | Column | Source | |---|---|---| | 1 | Mech Chassis | Portrait filename in `hsh/Mechs/` (these are the localized display names), title-cased; falls back to the folder name | | 2 | Is_Playable | `y` for everything in `core.build` | | 3 | Release Version | Constant `SOURCE_VERSION` in the script (currently `5.1.0b`) | | 4 | Technology | `.data` `TechType` -> IS / Clan | | 5 | Weight (tons) | `.data` `MaxVehicleTonnage` | | 6 | Class | Derived from tonnage: <=35 Light, <=55 Medium, <=75 Heavy, else Assault | | 7 | External Armor Type | `.subsystems` Armor block `ArmorType` | | 8 | Internal Structure Type | `.subsystems` Armor block `InternalType` | | 9 | Armor Points | **Installed**: sum of the Armor block's per-zone tons x points-per-ton (see *Armour*) | | 10 | JumpJets | JumpJetSubsystem present in `.subsystems` | | 11 | Light Amplification | `.instance` `DoesHaveLightAmp` (default 1) | | 12-15 | ECM / AMS / LAMS / Beagle | Corresponding subsystem present in `.subsystems` | | 16 | # of Flushes | `.instance` `MaxCoolant` / 5 | | 17 | Heat Sinks | Count of heatsink subsystems, doubled if they are DoubleHeatSink | | 18 | Top Speed Normal (kph) | `min(MinMaxSpeed + MPSPerUpgrade * EngineUpgrades, MaxSpeed) * 3.6` | | 19 | Top Speed Gimped (kph) | `.data` `MaxGimpSpeed * 3.6` | | 20 | Acceleration | `.data` `Acceleration` | | 21 | Deceleration | `.data` `Decceleration` (double-c is canonical in the engine) | | 22 | Turn Rate (rad/sec) | `.data` `TopSpeedTurnRate` (degrees) x pi/180 | | 23 | Twist Range (degrees) | `.torso` `TwistRadius` x 2, macros resolved through `MechTorso.defines` | | 24 | Twist Speed | `.torso` `TwistSpeed`, same macro resolution | | 25-50 | Weapon grid | `.subsystems` weapon blocks, placed by `InternalLocation` | | 51-54 | Rating bars | `.instance` `PowerRating` / `ArmorRating` / `SpeedRating` / `HeatRating` | | 55 | (template note) | Left blank | Weapon cells read `Weapon Name (ammo)`, with ` (Rear)` appended when the block sets `WeaponFacing=1`. ## Data sources All under `Gameleap/mw4/Content/`: | File | Provides | |---|---| | `core.build` | The roster. Presence here is what makes a chassis playable. | | `Mechs//.data` | Tonnage, tech, heat, movement, `CanLoad*` flags | | `Mechs//.instance` | Rating bars, coolant, LightAmp fitted | | `Mechs//.subsystems` | **The stock loadout**: armour type and tonnage, heat sinks, engine upgrades, weapons, electronics | | `Mechs//.engine` | `MPSPerUpgrade` for the speed calculation | | `Mechs//.torso` | Twist range and speed (usually macro references) | | `Subsystems/Armor.data` | Points per ton by armour type | | `Defines/MechTorso.defines` | Torso macro values | | `../hsh/Mechs/*.bmp` | Display names | `.damage` is deliberately **not** used for armour. See below. ## Armour: why tons, not multipliers The Armor block in `.subsystems` looks like per-zone multipliers: ArmorType=Standard InternalType=EndoSteel LeftArm=1.45 CenterFrontTorso=2.05 They are **tons of armour**, not multipliers. The proof is in `MechLab.cpp` `GetCurrentMechArmorData`: tonnage_array[i] = armor_array[i] / *points_per; so `points = tons x points_per`, with the rate chosen by armour type from `Content/Subsystems/Armor.data`: PointsPerStandardTon=32 PointsPerFerroTon=38 PointsPerReflectiveTon=30 PointsPerReactiveTon=30 PointsPerSolarianTon=60 Battlemaster: 13.0 tons x 32 = **416 points**. Cross-check against `MechLab.cpp:796`, which is also the formula written into the template's own header: armor_bar = (TotalArmor / 535) * 100 416/535 x 100 = 77.8%, against a stored `ArmorRating` of 79. 50 of the 65 chassis agree within 5 points, which is good corroboration. An earlier attempt summed `MaxArmorValue` from `.damage` scaled by those numbers. That yields armour **capacity** (1024 for the Battlemaster), not what is fitted. `.damage` `MaxArmorValue` is the per-zone cap the mechlab will not let you exceed; it is not the stock allocation. ## Judgement calls **Columns 10-15 mean *installed*, not *can load*.** The Battlemaster's `.data` says `CanLoadECM=Yes`, `CanLoadAMS=Yes`, `CanLoadLAMS=Yes`, `CanLoadBeagle=Yes`, yet the template sample marks all four `n`. The template is right: these describe the stock loadout, so they are read from `.subsystems`, not `.data`. **Column 7 follows the data, not the sample.** The template sample says Ferro-Fibrous for the Battlemaster; `battlemaster.subsystems` says `ArmorType=Standard`. Internal structure (Endo Steel) matched exactly, so the sample cell looks like a guess. The data wins. **Columns 51-54 are pulled, not recomputed.** These are the authored bar values from `.instance`. The template header carries formulas for them, but the stored values are what the game ships. Note that MechLab *recomputes* the bars live while you edit a mech (`MechLab.cpp:794-796`), so the stored values are the selection-screen figures, and the two can drift: 15 of 65 chassis diverge from the `/535` armour formula by more than 5 points, Fafnir worst at 38. That is a property of the source data, not an extraction error. **Column 3 says `5.1.0b`.** This is the current source tree, not the 5.0.7D drop the template came from. Change `SOURCE_VERSION` if you need a different stamp. ## Layout changes The template gives each arm three weapon slots. The **Gladiator mounts six Clan Medium Pulse Lasers in its right arm**, so three were being silently dropped. Before widening anything, the maximum was measured per location across all 65 chassis: | Location | Template slots | Max used | Worst case | |---|---|---|---| | Head | 1 | 1 | Atlas | | Left arm | 3 | 3 | Deimos | | **Right arm** | **3** | **6** | **Gladiator** | | Left torso | 4 | 4 | Mauler | | Right torso | 4 | 4 | Chimera | | Centre torso | 2 | 2 | Annihilator | | Special 1 | 3 | 2 | Ares | | Special 2 | 3 | 3 | Ares | Only the right arm overflowed. `Right_Arm_4/5/6` were inserted after `Right_Arm_3`, following the existing naming, and everything from the old column 32 onward shifted right by three. The workbook is now 55 columns. If a future chassis overflows another location, the generator prints a `overflowed ... -- widen LOC_COLS` warning naming the mech and location. Widen the entry in `LOC_COLS`, add matching `NEW_HEADERS`, and bump `NCOLS`. ## Weapon names Weapon labels are derived from the subsystem filename, since the `.subsystems` blocks carry no display string. Three rules cover almost everything: camelCase -> spaced ClanMediumPulseLaser -> Clan Medium Pulse Laser ACRONYM+Word -> split ClanERSmallLaser -> Clan ER Small Laser letters+digits -> split SRM6 -> SRM 6 Two source files are entirely upper case and cannot be split by any general rule, so they are special-cased: `CLANLRM10` and `ERPPC`. 44 distinct weapon names are produced; if a new one reads oddly, check the filename first. ## Pitfalls **Folder case.** `core.build` writes `mechs\annihilator\...` but the folder is `Annihilator`. The generator builds a case-insensitive directory index; file lookups inside a chassis folder are case-insensitive too. **Encoding.** Content files are CRLF and contain legacy high bytes. They are read as `latin-1`. Do not decode them as UTF-8. **Engine upgrades matter.** Top speed is not `MaxSpeed`. It is `MinMaxSpeed + MPSPerUpgrade x EngineUpgrades`, capped at `MaxSpeed`. The Battlemaster ships with `EngineUpgrades=5`, which is exactly what turns 65.0 kph into the template's 90.02 kph. Getting this wrong is silently plausible, which is why the sample row is worth calibrating against. **Display names are not folder names.** Folder `Blackhawk` is displayed as "Black Hawk", `Madcat` as "Mad Cat". The portrait filenames in `hsh/Mechs/` carry the localized names, so they are used as the name source. ## Verification `verify-mech-specs.py` runs five checks: 1. **Layout** - weapon and rating headers are where they should be. 2. **Fill rates** - every non-weapon column populated for every chassis. 3. **Calibration** - the generated Battlemaster row against the template's hand-made sample, weapon cells only. This is the important one: those 11 cells are an independent statement of the loadout, so if they still match, `.subsystems` is being read correctly. 4. **Weapon names** - flags camelCase remnants and long capital runs that suggest an unsplit filename. 5. **Armour cross-check** - installed points against the stored `ArmorRating` bar via the `/535` formula, reported as a distribution. Expected result today: `PASS`, 55 columns x 65 chassis, all 11 sample weapon cells matching, 44 clean weapon names, 50/65 armour bars within 5 points. ## Updating Re-run the generator. It re-reads the roster each time, so: - registering the six new chassis in `core.build` adds six rows automatically - editing any `.subsystems` loadout is picked up with no code change - adding a weapon needs no change unless its filename defeats the naming rules Then run the verifier and check it still reports `PASS`.