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

232 lines
11 KiB
Markdown

# 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`.