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>
232 lines
11 KiB
Markdown
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`.
|