Turns the hand-maintained 5.0.7D mech specification spreadsheet into something
generated from Content/, so the numbers come from the data the game loads
rather than being transcribed. 65 stock chassis, 55 columns, every non-weapon
column populated.
Standard library only. An .xlsx is a zip of XML and only sheet1.xml is
rewritten, so the template's formatting and drawings survive untouched.
Two findings worth recording, both in the README:
The Armor block's per-zone values in .subsystems are TONS, not multipliers.
MechLab.cpp GetCurrentMechArmorData does tonnage = points / points_per, so
installed points = tons x points-per-ton, with the rates in
Subsystems/Armor.data (Standard 32, Ferro 38, Reflective/Reactive 30,
Solarian 60). Battlemaster = 13.0 x 32 = 416 points. Cross-checked against
MechLab.cpp:796 armor_bar = (TotalArmor/535)*100: 50 of 65 chassis land within
5 points of their stored ArmorRating. An earlier pass summed MaxArmorValue from
.damage instead, which gives armour CAPACITY (1024 for the Battlemaster), not
what is fitted.
The Gladiator mounts six Clan Medium Pulse Lasers in its right arm, so the
template's three arm slots were silently dropping three weapons. Maximum usage
was measured per location across all 65 chassis before widening anything: only
the right arm overflowed. Right_Arm_4/5/6 inserted, trailing columns shifted
+3. The generator now warns by name if any location overflows again.
Judgement calls, all documented:
- columns 10-15 are "installed in the stock loadout", read from .subsystems,
not the CanLoad* flags in .data. The template sample marks the Battlemaster
n for ECM/AMS/LAMS/Beagle even though it can load all four.
- column 7 follows the data (Standard) over the sample (Ferro-Fibrous).
- rating bars are pulled from .instance rather than recomputed. MechLab
recalculates them live while editing, so stored and computed can drift;
15 chassis diverge by more than 5 points, Fafnir worst at 38.
verify-mech-specs.py runs five checks. The load-bearing one calibrates the
generated Battlemaster row against the template's hand-made sample: those 11
weapon cells are an independent statement of the loadout, and they match
exactly, which is the evidence that .subsystems is being read correctly.
Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
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\<folder>\<base>.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/<dir>/<base>.data |
Tonnage, tech, heat, movement, CanLoad* flags |
Mechs/<dir>/<base>.instance |
Rating bars, coolant, LightAmp fitted |
Mechs/<dir>/<base>.subsystems |
The stock loadout: armour type and tonnage, heat sinks, engine upgrades, weapons, electronics |
Mechs/<dir>/<base>.engine |
MPSPerUpgrade for the speed calculation |
Mechs/<dir>/<base>.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:
- Layout - weapon and rating headers are where they should be.
- Fill rates - every non-weapon column populated for every chassis.
- 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,
.subsystemsis being read correctly. - Weapon names - flags camelCase remnants and long capital runs that suggest an unsplit filename.
- Armour cross-check - installed points against the stored
ArmorRatingbar via the/535formula, 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.buildadds six rows automatically - editing any
.subsystemsloadout 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.