Files
firestorm/MECHSPECSHEET/README.md
T
653265c928 Add MECHSPECSHEET: generate the stock-mech spec sheet from the source tree
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>
2026-08-09 20:09:24 -05:00

11 KiB

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:

  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.