Data management tools for playing BattleTech
helm TODO.md
72 kB

TODO #

State: the library reads and the MCP answers questions about it. 10,988 units parse with no failures, all of them carry MegaMek's computed figures, and six tools expose the Meks over MCP. What is weak is what can be asked: the filters describe a unit's paperwork — tonnage, era, role — and say almost nothing about how it fights. Battle value and construction are still borrowed from the Java bridge.

The epics these belong to are in headquarters: plan/unit-library.md, plan/unit-search.md, plan/unit-rules.md.

Several items below were prompted by reading MekBay, MegaMek's own web client, for what it lets a person filter on. It is GPL-3.0 and helm is not, so what was taken is the vocabulary and the shape of the problem, never code.

What to do next, in order #

Twenty things, ranked. The tiers are the argument: the first finishes what plan/unit-rules.md already promises, the second widens what helm can score at all, and the third is what makes any of it reach a player. Everything below appears again in its own section with the detail; this is the order to take them in and the reason for it.

Finish what unit-rules promises. The epic's exit criterion is a force's battle value computed the same way in the browser and on the server, agreeing with MegaMek except where a deviation is registered.

  1. Read C3 networks from a .mul and score them. The arithmetic is written; nothing reads which unit is whose master, so helm force scores every unit as though it were alone and a networked lance is reported five percent light.
  2. Double verification across the wire. Named in the epic: the same crate compiled twice, agreeing. The smoke test checks a few designs; the proof is the whole library scored in Node and natively with no disagreement. It is the reason helm-bv does no I/O, and it is currently unproven.
  3. Battle value for a varied unit. Condition models damage and not variation. A player swapping an LRM bin for inferno rounds, or changing an omni pod, is exactly what the epic says players will do - and it is the largest gap between what helm computes and what the product needs.
  4. Pin the rounding. Also named in the epic. Battle value is full of half-point steps and nothing proves the two builds round alike, or that a total does not depend on the order floats were added in.
  5. Settle the two compensating explosive errors. B-Pods against CASE II ammunition: both sides are wrong and the fresh figure is right by luck. Wants two oracle designs before either can be corrected.

Widen what helm can score.

  1. Vehicle battle value. CombatVehicleBVCalculator upstream. Scenarios already carry tanks - MercBrawl, example.mul - and helm check answers "cannot be scored" for every one of them.
  2. Battle armour and infantry. The same argument with different calculators, and the reason LegAttack and SwarmMek are synthesised.
  3. C-bill cost. The second of the three figures borrowed from the bridge, and what anything economic needs.
  4. The Alpha Strike conversion. The third. It already has a registered deviation, so the shape of the work is known.
  5. Retire bridge/ for battle value. Once 8 and 9 exist the JDK container leaves the build, which is plan/unit-library.md's exit criterion.
  6. The equipment MegaMek synthesises while loading. 5,534 units differ from MekSummary.equipmentNames only because of it. It blocks equipment filtering conformance and probably hides battle value bugs.
  7. The nine registered disagreements. Now that they are named they can be taken one at a time; the Super-Griffin and the Jabberwocky are the two most likely to be a rule rather than a broken file.

Make it reach a player. These belong to epics in headquarters as much as to helm.

  1. Decide how a design reaches the page, and build it. The one open decision, and everything in plan/unit-search.md waits behind it.
  2. Compile helm-facet to wasm and filter with it in the browser. The epic says it outright: run the same predicate, do not reimplement. Two filter implementations is how "filtering means the same thing everywhere" dies.
  3. The redistribution question. MegaMek's data is CC BY-NC-SA and serving an index of it to browsers is redistribution. It needs a decision and an attribution mechanism before 13 ships.
  4. Provenance on every record. Which MegaMek, which producer, which RULES_VERSION. The constants exist and nothing writes them into the database or into an ATProto record, so a stored figure cannot be told from one computed against a different MegaMek.
  5. The attribution and the repair list on a match page. plan/after-action.md. The pieces are built and cross the wasm boundary; what is missing is the screen that reads them.
  6. Damage that persists between matches. plan/campaign.md. The mechanical half exists; storing a force's condition as a record and reading it back is the rest, and the construct-write-read round trip supports it.
  7. Formation validation. list_formations and validate_formation against the Campaign Operations blueprints - "is this a legal Fire Lance". Validation before generation, and it needs no faction data: it is role and count, both of which helm holds.
  8. Ingest data/forcegenerator/. 40 era files of per-faction availability weights, factions.xml, and 61 faction rulesets that are a small DSL of their own. This is the foundation for force generation, faction filtering and era-correct scenarios, and nothing above it should be attempted until it exists.

Derived combat metrics #

The highest-value gap. These are what make a filter describe a fighting machine rather than a spec sheet, and every one is computable from data helm already holds: unit_equipment joined to the equipment catalogue, which carries heat, damage and ranges per weapon.

  • The maximum is two points a location for every point of internal
    structure, except the head, which takes nine whatever it is bolted to and
    twelve on a superheavy — so it is not twice the structure total. Front
    and rear plate share a torso's allowance rather than each having one.
    
    The figure is in no unit file, so it cannot be conformed against MegaMek
    the way a battle value can. What holds it is that no design may exceed
    it: over all 4,294 Meks none does and 815 sit at exactly 100, which is
    the check both ways round — a maximum set too low would put designs over
    it, and one set too high would leave nothing at the top.
    
    This is declared data rather than derived from the catalogue, so unlike
    the five above it the column is filled whether or not a build had one.
    It is null for everything that is not a Mek, the table being a Mek's.
    

Filters the data already supports #

Cheap: the columns exist and the MCP does not offer them.

Equipment filtering #

  • Two upstream behaviours are reproduced on purpose and pinned by tests so
    nobody tidies them away. `Ultra` matches `ultraa` with two As, which is
    not a typo: it is matching `ISUltraAC5`, where "ultra" runs into the
    "AC". And a class leaf **sums quantities across matching entries** where
    a name leaf does not, so two medium lasers and a large laser satisfy "at
    least 3 energy weapons" while no name leaf asking for 3 would.
    

Damage at range #

  • This is **maximum** damage where `firepower` is average, and the two are
    not interchangeable: a full LRM 20 salvo is 20 here and 12 there.
    
    The first version scored every ammunition-fed weapon at zero and nothing
    looked broken - an Atlas came out at 20 instead of 72, because a `.mtf`
    weapons block lists no ammunition and the lookup therefore never matched.
    Ammunition is now found in the critical slots, where a `.mtf` actually
    puts it. It moved the count of Meks meeting the Assault Lance criteria
    from 786 to 1,765, so it was not a rounding error.
    
    Two more of the same shape came out of the formation oracle. Artillery
    was scored at zero, because a weapon was recognised by having an average
    damage and artillery has a sentinel instead of one - MegaMek asks
    `instanceof WeaponType`, and so does `EquipmentEntry::is_weapon` now. And
    an omni's ammunition was invisible: a `.mtf` writes `(OMNIPOD)` into the
    slot itself, so the name in the slot is not the name of the thing in it.
    `helm_core::strip_slot_markers` is that cleaning, shared with
    `helm_bv::loadout` rather than written twice.
    
  • Two more of the same shape came with it. A weapon whose damage is a
    sentinel rather than a number - a Snub-Nose PPC does ten, eight and five
    by band - was counted as nothing, so `EquipmentEntry::damage_at` reads
    the bands the way `WeaponType.getDamage` does. And a one-shot launcher
    arrives loaded: its rounds are part of the weapon rather than a bin in a
    slot, so MegaMek makes the ammunition while it reads the design and helm
    found none, which scored a Firestarter FS9-C's six rocket launchers at
    zero.
    
    Measured by the formation oracle: 179 damage disagreements of 42,453
    requirement checks, now 5.
    
  • Deliberately not hardened armour, which costs a Mek a point in the battle
    value calculation and nothing in what MegaMek reports about the design.
    The two figures are not the same figure.
    
  • That took the formation oracle's damage disagreements from 5 to 0, and
    the test no longer forgives any: 42,453 requirement checks over 477 Meks,
    and every figure helm computes for them is MegaMek's.
    
  • Deliberately not the same figure `helm_bv` works out. Battle value runs
    under a setting that says `noModularArmor`, so a Koshi (Mist Lynx) Z is
    scored jumping six there and reported jumping five here. Two figures,
    both right, and a test each.
    

Force value #

  • A `.mul` records the network as pointers rather than as a list - a slave
    names its master's `c3UUID`, and a C3i unit names every other member in
    `<c3iLink>` elements - so `helm_force::c3_bonuses_linked` turns the
    pointers into networks and pays each one for its own members. A force can
    hold more than one: two lances with their own masters are two networks,
    and a chain of masters under a master is one. `helm force` reports the
    share in a column of its own.
    
    A Gunslinger GUN-2ERD and a Malice MAL-YZ wired together are 2,423 and
    2,982 apart and +270 each on the network, which is five percent of both.
    A Peacekeeper carrying a slave set with no master in the force is paid
    nothing, because a set with nobody to talk to is not a network.
    
  • The table is transcribed rather than derived - there is no formula
    behind it - and reading it the wrong way round is easy: `[3][4]` is 1.32,
    not the 1.44 at `[3][3]`. My own first test expectations were wrong that
    way and the implementation was right.
    

Battle value #

The long pole, and the reason helm-core does no I/O. Tracked as unit-rules in headquarters' plan/.

  • Two things fell out. The tech-base marker is spelled in three places -
    `XL (Clan) Engine`, `XL Engine(IS)`, `IS Endo Steel` - and reading only
    the first misses 116 designs. And patchwork armour writes its type in
    front of the points, `LT armor:Clan Standard(Clan):26`, which the parser
    rejected as non-numeric: all five patchwork designs were reading as
    almost unarmoured, a Zeus-X at 28 points where MegaMek says 247. Total
    armour now agrees with MegaMek on all 10,749 designs it reports one for.
    
  • A Shadow Hawk SHD-2H is 1,064 as built and 1,069 with acid rounds in its
    SRM bin. Both figures are MegaMek's, and both are now helm's.
    
    `bridge/DumpMul.java` is the oracle, and it is the first one that reads a
    file rather than shooting a design itself - which makes it the harness
    for anything expressed in a `.mul` rather than derived from a `.mtf`. The
    fixtures in `crates/helm-bv/tests/mul/` are this repository's own,
    because MegaMek ships no file that varies a unit.
    
    An unknown name in a slot is ignored rather than treated as an empty one:
    a newer MegaMek's spelling should cost nothing, and the design's own
    answer is a better guess than a hole. An omni pod swap - different
    equipment rather than different rounds - should ride the same mechanism
    and has not been checked.
    
  • Java's `Math.round` sends a half up; Rust's `f64::round` sends it away
    from zero. Those are the same thing until a figure is negative, and every
    figure rounded today is positive - so the rule is written down once in
    `helm_core::round_half_up` rather than assumed at eleven call sites, and
    a term that comes out negative later cannot quietly disagree by a point.
    
    The test measures rather than asserts a limit: how many designs sit
    within a millionth of a boundary, how many exactly on one, and whether
    any of those is a *point* away from MegaMek - which is what a rounding
    disagreement looks like. None is. The only near-boundary design that
    differs is the Uni ATAE-70, and it is 43 out because its file allocates
    one Thumper's slots to two Thumpers.
    
  • Half done. `helm_core::equipment_list` models the part a Mek's header
    lines imply - one structure entry, one armour entry, every heat sink
    including the ones inside the engine, and a Clan design's structural
    CASE - and designs whose list is MegaMek's went from 1,883 of 10,988 to
    4,860. `crates/helm-facet/tests/loadout.rs` holds the figure and reports
    what makes up the rest, which is now infantry weapons, battle armour
    gear and aerospace bays rather than anything Mek-shaped.
    
    Battle armour too: the jets are declared in `<slotless_equipment>`,
    which nothing read, and the four anti-Mek attacks are invented for a suit
    that can climb a Mek. Which can is `BattleArmor.canMakeAntiMekAttacks`:
    lighter than a heavy suit, not a quad, and with two hands free - two
    armoured gloves or two basic manipulators, or one battle claw, or a
    magnetic clamp. Suits carrying MegaMek's list went from 276 of 1,188 to
    1,184.
    
    Infantry is untouched: a platoon's weapons are declared in a way of
    their own, and all 1,792 differ.
    
    Not yet wired to `UnitFacets::loadout` or the database. A synthesised
    entry has no location and the equipment table stores one row per mount
    with one, so switching the facet over is a change to what a search
    returns and wants its own evidence.
    
  • [~] Defensive battle rating. 4,235 of 4,294 Meks match MegaMek exactly. Armour at 2.5 a point and internal structure at 1.5, each through its own type multiplier, plus the gyro at half a ton, defensive equipment at catalogue value, minus 15 a slot for ammunition that can cook off, all through a movement factor of 1 + TMM/10.

    Every multiplier was fitted against MegaMek across the library rather
    than transcribed. Three came out worth commenting: an Inner Sphere XL
    engine is 0.5 where a Clan XL is 0.75, because the Inner Sphere version
    dies with either side torso; Ferro-Fibrous is 1.0 like standard plate,
    since it buys more *points* and those are already counted; and CASE is
    better modelled as "carries it anywhere" than "carries it in that
    location", because a design that fits CASE fits it where the ammunition
    is.
    
    What is left is a long tail of defensive equipment - 1,655 designs, most
    within 20%. Counting equipment per critical slot rather than per item was
    worth 200 designs on its own: a large shield fills seven slots and was
    being paid for seven times.
    
  • [~] Offensive battle rating. 4,193 of 4,294 match. Weapons at catalogue value, halved once the design runs out of heat, plus ammunition capped by the weapons that eat it, plus the Mek's own mass, all through the speed factor.

    The weapon ordering is the part worth knowing: cold weapons first
    whatever they are worth, and only then by descending battle value. A
    machine gun costs nothing to fire, so it never competes for the heat
    budget, and sorting on value alone charges it as though it did.
    
  • [~] Battle value end to end. 4,166 of 4,294 designs match MegaMek exactly. helm bv-report --clusters groups what is left by what the designs have in common, which is how each of the remaining rules was found - the ranked list of worst offenders only ever shows whatever is rarest. The two ratings are summed and the total rounded once, which is the last place a half point can change an answer.

  • It is one calculation and not two. MegaMek has a `BVCalculator` and a
    subclass per unit type, and `CombatVehicleBVCalculator` changes five
    things: the plate is worth less for being on a vehicle, the frame is a
    tenth of the tonnage in each location that wears armour, the tonnage
    counts for half, there is no heat budget to run out of, and only the
    front hull has a say in which way the machine faces. So `Mek` became
    `Machine`, it carries a `Chassis`, and the five places that differ ask
    it.
    
    What the oracle found that reading the rules would not have: a
    trailer with a stealth system is not hard to hit, because a machine that
    cannot move gets no movement modifier at all; a superheavy hull numbers
    its locations differently and MegaMek's facing rule is written against
    the numbers rather than the locations, which is in
    `crates/helm-bv/UPSTREAM.md`; and a vehicle's ammunition carries no
    explosive penalty - none of the 1,256 reports has the line.
    
  • - **LAMs** (23 designs) are declined rather than scored, by decision:
      lance.blue will not field them. They need their own movement modes -
      airmech and fighter - feeding both the speed factor and a heat
      efficiency three points higher, and a LAM scored as a Mek is wrong
      rather than approximately right. Reinstating them is deleting the
      guard in `battle_value` and writing those modes.
    
  • `crates/helm-wasm/smoke.mjs` drives it from Node, which is the only
    thing that exercises the interface a browser actually calls through:
    
        cargo build -p helm-wasm --target wasm32-unknown-unknown --release
        node crates/helm-wasm/smoke.mjs <equipment.jsonl> <design.mtf>
    
    An Atlas AS7-D comes back 1897 fresh, 1564 with its torsos stripped,
    2504 flown by a veteran and 1631 by a green crew.
    
  • `bridge/DumpDamaged.java` is the oracle, because a `.mtf` describes a
    design as it leaves the factory and nothing else. Six designs in six
    states each, and the rows are worth reading rather than only passing:
    
    | what was done to it | Atlas AS7-D | Locust LCT-1V |
    |---|---|---|
    | nothing | 1897 | 432 |
    | plate stripped | 985 | 208 |
    | one torso opened | 1714 | 390 |
    | half a torso's frame gone | 1877 | 426 |
    | right arm shot off | 1719 | 402 |
    | magazines run dry | 1890 | **451** |
    
    Running a Locust dry *raises* its battle value: an empty bin no longer
    explodes, and the defensive penalty that removes is worth more than the
    rounds were. Reasoning from the rules would have got that backwards, and
    two of these six states found real bugs - a heat sink shot out of an arm
    is cooling the `heat sinks:` line still counts, and a hit anywhere in a
    multi-slot weapon destroys the whole weapon rather than the slot.
    
    **Widened, and it found three more.** Nine designs in fourteen states -
    126 rows - covering the systems whose loss changes a figure: engine, gyro,
    heat sinks, jump jets, arm actuators, MASC, and the leg joints. Six states
    were added at once and three of them disagreed straight away:
    
    - **An engine critical is five points of heat a turn**, and MegaMek takes
      it off the heat efficiency after the movement heat rather than through
      it: `mekHeatEfficiency -= getEngineHits() * 5`. A standard fusion engine
      fills six centre torso slots and all six count. helm charged nothing,
      and an Atlas with its engine hit was 1897 where MegaMek says 1631.
    - **Jets that have been shot off are not jets the design never had.** The
      `jump mp:` line is the fallback for a design carrying no jets this crate
      can resolve, and a Catapult whose jets were destroyed fell through to
      it and was scored as still jumping four.
    - **A hatchet is never dropped for heat.** MegaMek builds its heat list
      from `getTotalWeaponList`, which a physical weapon is not in - it is
      scored with the equipment at full value. A vibroblade *is* in it, added
      by name with the heat its motor makes. helm halved every melee weapon
      once a design ran out of budget, which no fresh design in the library
      is far enough over to show.
    
    One of the six was the oracle's own fault rather than helm's: destroying
    "every heat sink" also destroyed the ones inside the engine, which fill no
    critical slot, cannot be shot out in the game, and have no way to be
    written into a `.mul`. An oracle for a state helm can never be asked about
    is not an oracle. It now damages only what a critical hit could reach.
    
    126 of 126 rows agree, and the undamaged library is unchanged at 4,252.
    
    **Widened again, this time across designs.** `bridge/DumpDamaged.java`
    takes `every:N` and samples the library rather than naming designs, which
    is how the shapes and construction a hand-picked list never covers get
    asked about. At `every:40` that is 1,638 states, and it found two more:
    
    - **A weapon too big for one location** has the rest of itself in the
      next one, and it is one weapon. A Victor VTR-9Ka's autocannon fills
      nine slots of an arm and one of the torso beside it; blowing the arm
      off left helm counting a whole autocannon that was no longer there,
      worth 465 points of offence. The halves are now paired - but only when
      there are exactly two of them and they account for the whole item. A
      Chameleon Light Polarization Shield is spread over half a design a slot
      at a time *by construction*, and pairing on the name alone took a
      Scarecrow's shield out with its arm.
    - **An engine only runs hot when it is hit if it is a fusion or fission
      plant.** An internal combustion engine takes the critical and makes no
      extra heat, so a SecurityMech with its engine shot out is worth what it
      was.
    
  • - **A blown-off arm holding a big gun**, +8 to +9 defensive on a Cerberus
      MR-V3, an Ebony MEB-12, a Huron Warrior and a Thunder Hawk TDK-7S. All
      four carry a Gauss Rifle or a TSEMP Cannon in the arm, which suggests
      an explosive penalty MegaMek keeps charging for a weapon that is gone.
    - **Modular armour on a stripped design**, +97.5 on an Enforcer III
      ENF-7D - exactly 39 points of plate at 2.5. MegaMek counts modular
      armour as part of the location's armour, so stripping the location
      takes it too; helm adds it on top of a location it has already zeroed.
    - **A Devastator DVS-X10 MUSE EARTH's movement gear**, hundreds of points
      out on a hip or leg actuator hit and a few points out on a gyro hit.
      An XTR one-off, so it is likely a movement system helm reads and
      MegaMek recomputes differently once the legs are damaged.
    - **A Scarecrow UCU-F4 run dry**, 3 points of defence. The smallest of
      them and probably an explosive penalty on a magazine one of us empties
      and the other does not.
    
    **Three more, all the same mistake in different clothes: reading what a
    design was built with where MegaMek reads what is left, or the reverse.**
    
    - A **torso-mounted cockpit** counts its centre torso's plate a second
      time, for keeping the pilot alive rather than the Mek - and it has to
      be the plate that is still there. A stripped Enforcer III ENF-7D was
      being paid twice for armour it no longer had, 97.5 points of it.
    - An **armoured system slot** is worth something, and stays worth it when
      the slot is shot out: the plating is on the machine whether or not what
      it was protecting still works. helm stopped counting, so a Banshee
      BNC-11X with its arm blown off came out 36 light.
    - **Modular armour** goes the same way, which is the opposite of what it
      looks like: MegaMek pays for the pack whether or not the location
      holding it is still attached.
    
    And one that was not a rule at all: a Banshee writes `Hip (ARMORED)`, and
    the leg-damage rule matched the bare name. The design walked as though
    its hip were whole and came out three hundred points heavy.
    
  • Same format and same reader, so nothing downstream changes:
    
    |  | bridge dump | shipped |
    |---|---|---|
    | raw | 6.0 MB | 1.7 MB |
    | gzipped | 0.32 MB | 0.09 MB |
    | parsed in a browser | 216 ms | 88 ms |
    
    Checked by scoring the whole library from it: 4,252 of 4,294 designs
    match MegaMek either way, which is the same figure to the design.
    
    That check is a test now rather than something done once. The saving is
    only safe while the fields it drops are fields the rules never read, and
    "never read" is a claim about every rule in the crate rather than the
    ones somebody remembered - so every design is scored twice, once from
    each catalogue, and the two have to agree. The list of kept fields moved
    to `helm-bridge` beside the reader that makes it true.
    
  • `helm force <force.mul>` scores one. Two files MegaMek itself wrote:
    
    |  | fresh | from the file |
    |---|---|---|
    | Shadow Hawk SHD-2H | 1064 | 738 |
    | Griffin GRF-1N | 1272 | 694 |
    | Orion ON1-VA (untouched) | 1328 | 1328 |
    
    Parsed by hand like the other MegaMek formats. Three things the format
    does that a reader has to know: slots are numbered from one where
    everything else here counts from zero, `points` is sometimes a word
    rather than a number - `Destroyed` is none left and `N/A` is a location
    that never had any - and an ejected MekWarrior is written as another
    entity in the same file, so a force has to tell people from machines.
    
  • Two things make the figures steady enough to assert on. Every task is
    timed in each round rather than one task to completion, so a busy minute
    costs all of them a round instead of making one look slow. And each keeps
    its fastest round, because a shared machine has a floor and no ceiling.
    Run-to-run drift is then under a tenth, against a gate of a fifth.
    
    Nine operations now, not five: the repair list, the upload check, and
    both ways of wearing a design down. The two searches are the expensive
    ones by a long way - wearing to a figure is 21 times a design read and a
    battle is 37, because each is dozens of scorings rather than one - which
    is worth having recorded before anything tries to run one per frame.
    
    Held only in release: an unoptimised build is several times slower and
    the two sets of figures are not comparable, so a debug run prints them
    and checks nothing. `HELM_PERF_BLESS=1` re-records, and a commit that
    moves a figure should say why.
    

How a design reaches the page #

The one integration question still open, and the only thing on this list that wants a decision rather than work. A .mul names a design by chassis and model and carries nothing about what is in it, so the page needs the designs from somewhere regardless.

Measured rather than guessed. About three quarters of a .mtf is prose - overview:, history:, deployment: - and stripping it leaves every one of the 4,294 battle values identical:

how per design whole library
.mtf as shipped 8.9 KB median 31.9 MB zip
.mtf with the prose stripped 2.6 KB median 0.56 MB gzipped
a minimal JSON of the fields the rules read 2.0 KB median 0.45 MB gzipped

Which makes "ship the whole library and look up by chassis and model" the option that retires the question instead of answering it: 0.56 MB beside the 0.09 MB catalogue is under 0.7 MB for everything a page could ask about, and it can be fetched in the background while somebody browses.

The JSON form saves 0.6 KB a design and costs a format: a new encoder in helm, a new decoder in helm-wasm, and both able to drift from the .mtf parser that conformance actually tests. Not worth it at that price.

What MegaMek keeps units in #

Four readers, and that is the whole set - megamek/common/loaders in 0.51.0:

format what it is count
.mtf one Mek design, line-oriented text 4,294
.blk every other unit type, from tanks to warships 6,694
.mul a force in a state, XML - not a design format interchange
units.cache a Java-serialised index: name, tonnage, BV, cost 1

Older MegaMek had HeavyMetal Pro importers (.hmp, .hmv); they are gone. MegaMekLab reads and writes .mtf and .blk and has no format of its own, so it is not a fifth thing to support.

What the web UI needs from a .mul #

The format lance.blue works in, so these are the jobs it has to do. Written down because the first of them changes what the writer has to be, and that is not obvious from any one of them.

  • Storing a player's file and handing it back would have been data loss
    with no error and no way to notice. Each unit now keeps the element it
    was read from, and writing starts there and changes only what the model
    says - so a camo, a portrait, an `externalId` and any tag a later
    MegaMek adds all survive. A location says `Destroyed` rather than a
    nought this crate invented, and a slot still says which equipment is in
    it.
    
  • Most of the checks are about what the *reader* forgave. A location index
    it cannot place is skipped, an unparseable figure is left out, more
    structure than a design has is clamped when it is scored - all right for
    a reader, since a force that is nine tenths readable is worth reading,
    and all wrong for an upload, where the nine tenths get stored and nobody
    is told about the last tenth. Two of the checks can only be answered
    from the document, because by the time it is a model the evidence is
    gone.
    
    Three more since: six wounds is a dead MekWarrior rather than a hurt one,
    a slot marked as an empty magazine that holds a laser is a file
    describing something that cannot be, and a slot shot out that was empty
    to begin with is one written against a different design. The magazine one
    matters most because it is silent - the reader drops the slot either way,
    so the design comes out missing a weapon nobody shot.
    
    A complaint says whether it is about the file or about us. A force of
    tanks is a good file helm cannot score yet, and refusing it would be
    charging a player for our own gap - so only the file's own faults fail
    the command.
    
    The guard against the obvious failure - a check that refuses good files -
    is every `.mul` the install ships: 99 machines, no complaint about any of
    them. Four name a design MegaMek does not ship, which is a fault in
    MegaMek's own `example.mul` and is written up in `UPSTREAM.md`.
    
    Not reachable from a page yet: the check needs the design to compare
    against, and how a design gets to the page is the decision still open
    below. Everything here is `helm-core` and `helm-unitfile` only, so it
    crosses when that is settled.
    
  • A rewrite is an *edit* of the document that arrived, never a rebuild of
    one from the model. Four separate losses came from getting that wrong -
    dropped attributes, dropped `<force>` elements, a rewritten version
    string, reordered locations - and each was silent. The conformance test
    now compares the whole document element for element and attribute for
    attribute across every `.mul` the install ships, because comparing the
    *model* only proves the parts helm understands survive, which is the
    easy half.
    
    What the metadata has to carry, so a stored figure can be told from one
    computed against something else: `helm_bv::CONFORMED_TO_MEGAMEK`, the
    MegaMek these rules agree with, and `helm_bv::RULES_VERSION`, the build
    that produced the figure. A conformance test now refuses to let either
    drift from what `conformance.txt` was actually measured against.
    
  • It bisects on plate and then on the frame under it, both spread evenly
    across the locations, because armour is most of the defensive rating and
    is the one term the figure moves smoothly with. It touches no magazine
    and shoots nothing out: an empty bin stops being worth anything *and*
    stops exploding, so that lever does not have a direction and a search
    cannot bisect on it. One point of frame is left everywhere - at nought a
    location is off the machine and takes its weapons with it.
    
    The bisection moves every location at once, so it lands within a step
    that is worth thirty points on a large design; a bounded pass taking
    single points off the thickest location closes that. Over a sample of
    the library at three targets each: 67 of 243 reachable targets hit
    exactly and the worst miss is 5, against 28 and 15 before the refinement.
    A point of armour is worth five to eight points of battle value, so a
    target between two achievable figures cannot be hit at all - `missed_by`
    says so rather than leaving the caller to compare.
    
    Of 321 targets tried, 78 were below what stripping a machine can reach.
    Most of a Mek's battle value is what it carries rather than what protects
    it, so "half of what it is worth" is not always a condition that exists.
    
    **Battle damage, rather than an even shave.** `helm wear ... --battle`
    damages a design the way a fight does: fire lands where MegaMek's own
    2d6 hit table sends it, so the torsos take most of it and the head one
    roll in thirty-six; it clusters, because a gunner with the range keeps
    shooting the same spot; it arrives in the sizes weapons throw, a machine
    gun's 2 and a medium laser's 5 and an autocannon/20's 20 rather than a
    smooth distribution; and when it gets through the plate it takes
    equipment with it on MegaMek's critical roll - nothing on 7 or less, one
    component on 8-9, two on 10-11, three on 12.
    
    This is not a combat simulation and does not want to be. There is no
    attacker, no range and no to-hit roll: what is borrowed is the shape of
    damage, which is what makes two machines at the same figure look like
    two machines rather than two spreadsheet rows.
    
    Nothing it produces is a mission kill. No location is destroyed - the
    frame stops at one point, so no limb is ever blown off - the head is left
    alone below its plate, the engine takes at most one hit of the three that
    destroy it and the gyro one of the two, and magazines are never hit, that
    being an explosion and a dead crew without CASE. 180 battles across the
    library are checked against every one of those.
    
    Guns *do* come off, and should: shooting one off is often the shortest
    way down to a figure, and an Atlas that lost its autocannon and kept its
    lasers is a more interesting unit than one with evenly thinned plate.
    What is refused is the end of that - nothing left to fire, or less than a
    quarter of what it could throw.
    
    A leg joint turned out to be the sharpest lever of the lot, and the least
    obvious one. A hip has no battle value of its own, so shooting one off
    looked free - and it is not free at all, because it halves what the Mek
    walks and movement feeds the defensive factor through the target movement
    modifier and the offensive factor through the speed factor. helm was not
    modelling that: a wrecked hip changed nothing. MegaMek takes an Atlas
    AS7-D from 1897 to 1630 for one hip, and to 1289 for the three actuators
    below it. The rule is now written and the damaged oracle carries two more
    scenarios to hold it: a destroyed hip halves the walk rounding up, two
    hips stop the Mek, every other leg actuator costs a point, and a leg with
    a wrecked hip does not also pay for its remaining actuators. 40 of 40
    oracle rows agree.
    
    A location is only ever damaged behind plate that was breached. The pass
    that puts armour back when a hit overshoots was patching up locations it
    had already been shot through - 62 of 1,760 breached locations, a Javelin
    with one point of frame left behind eight points of armour. It now skips
    any location with a bent frame or a wrecked component, and a test over
    the library holds it there.
    
    The hits land in the sizes they land in, so the figure steps past the
    target as often as onto it. The even search's single-point pass finishes
    the job, in both directions: plate off the thickest location when the
    machine is still worth too much, and back onto the least damaged one when
    a hit overshot. Every seed tried lands within a point of armour's worth.
    
  • Nothing is costed. What a repair takes in time, parts and C-bills is a
    campaign's question; what is missing is a fact about two files.
    
    Writing it found a real bug in the `.mul` writer. The key for rear plate
    was built as `RT` plus the location's first letter, which is right for
    the three torsos and sends a left *arm* to `RTL` - the left torso's rear.
    Reading was harmless, since MegaMek writes no rear plate on a limb.
    Writing was not: a force with a damaged torso came back with rear armour
    tags on two limbs that never had any, and `Bot_North.mul` gained a whole
    `<location index="5">` it did not have. The round trip over the shipped
    files missed it because none of them has rear damage.
    
    The test that holds the mapping is exact: strip a design to nothing and
    the repair list has to come to the armour and structure it was built
    with. Over all 4,294 Meks, every one does - which is what says the keys
    are right for a quad's four legs as well as a biped's arms.
    
  • The terms do not add up to the figure by themselves - each half is a sum
    through a factor, and damage moves both - so the split needs a
    convention: an additive term is valued at the factor the machine ended
    up with, and a factor against the subtotal it started from. That is the
    one split with no cross term left over, and it is what lets a page draw
    a bar per term and have the bars sum to the drop beside them.
    
    A Griffin GRF-1N that came out of a match at 694 from 1272 reports -310
    to its weapons, -257 to its plate and -12 to its frame, which is a
    repair list in the order it matters.
    
    Two things the sweep found. Wrecking a torso or a leg moves the *speed*
    factor and not the movement factor, because what it destroys is jump
    jets: 1,159 designs of 4,294 against 306. And a test over the damaged
    oracle rows alone never exercised either, so a split that valued a
    factor at the wrong moment passed it - the sweep over the library is
    what closes that, and both wrong splits were tried on purpose to see the
    guard fire.
    
  • | | the book | helm |
    |---|---|---|
    | MON-66 Mongoose | DBR 433.3, OBR 325.08, BV 758 | 433.3, 325.1, 758 |
    | TDR-10SE Thunderbolt | DBR 954.85, OBR 1052.98, BV 2008 | 954.9, 1053.0, 2008 |
    | WI-DM DemolitionMech | DBR 328.2 | 328.2 |
    
    Armour, structure, gyro, defensive equipment, the explosive penalty,
    both defensive factors and both speed factors agree. That is the rules,
    MegaMek and this crate arriving at the same numbers by three routes.
    
    Two places the book and MegaMek genuinely differ, where helm follows
    MegaMek because that is what conformance means: the book gives a cockpit
    modifier only for the small cockpit, where MegaMek also has torso-mounted,
    small command console, drone, interface and VRRP; and the book describes a
    design's *armament* where MegaMek scores everything with a battle value,
    so a DemolitionMech's wrecking ball and rock cutter count. The one real
    disagreement is written up in `crates/helm-bv/UPSTREAM.md`.
    
  • - **Injection into real files.** Put an unknown attribute, element,
      comment and text run at every position of every real `.mul` and assert
      each survives. The generator makes plausible documents; this proves the
      files that actually exist tolerate whatever a later MegaMek adds.
    - **A named list of what the writer may touch.** A test asserting it only
      ever modifies `points`, `index`, `isDestroyed`, `isHit`, `shots`,
      `gunnery`, `piloting`, `hits`, `chassis`, `model`, `type` and
      `version`. That turns "do not lose data" from a habit into a check.
    - **Deliberately break the guards.** The performance gate was proved to
      fire by making something slower on purpose; nothing has proved the
      losslessness guards fail when they should.
    - [x] **Construct, write, read.** Every round trip started from a file
      somebody else wrote. `helm wear ... --write worn.mul` builds a `Mul` in
      code and writes it, which is the case a player hits when helm hands
      them a damaged machine to load into MegaMek - and a test wears eleven
      designs down, writes them out, reads them back and checks each is worth
      what it was worn to.
    
  • Its fresh penalty is -2 either side, and the -2 is not the same -2. helm
    charges two B-Pods, one in each leg, and charges nothing for the machine
    gun ammunition because CASE II protects it. MegaMek charges the
    ammunition and nothing for the B-Pods - which is consistent with its own
    `countAsOffensiveWeapon`, where a B-Pod is excluded by name. Emptying the
    bin then removes the whole of MegaMek's penalty and none of helm's.
    
    Two changes are needed and neither can be made alone: the fresh figure is
    right by luck, and correcting one side without the other breaks four
    thousand designs that currently agree. What would settle it is oracle
    coverage of each separately - a design with B-Pods and no ammunition, and
    one with CASE II ammunition and no pods.
    
  • [~] Zero unregistered disagreements. Ten designs of 4,294 still differ, and here they are by name. None is a line unit; all are one-offs, joke designs or IndustrialMeks, which is why they have survived this long - but a disagreement nobody has written down is one nobody can pick up.

    | design | out by | what it carries |
    |---|---|---|
    | Uni ATAE-70 ArtilleryMech | off -43.0 | a Thumper |
    | Super-Griffin GRF-2N-X | off +41.2 | LRM 10, experimental |
    | Jabberwocky JAW-67 MilitiaMech | off +30.8 | LRM 5, industrial |
    | UrbanKnight UM-DKX | off +15.5 | industrial |
    | Tessen TSN-X4R 'Rapunzel' | off -11.7 | one-off |
    | Vulpes VLP-1DX 'Beast' | off -6.5 | one-off |
    | Atlas AS7-00 (Jurn) | def -6.0 | a named character's |
    | Hybrid Rifleman RFL-3N (Sneede) | def -3.6 | one-off, two variants |
    | Hercules HRC-LS-9004 | def +1.3 | Ultra AC/10 and an AMS |
    
    The ArtilleryMech is settled, and it is not our arithmetic. Its file
    declares two Thumpers and allocates fifteen critical slots between them,
    which is one Thumper's worth: the design cannot be built as written.
    MegaMek scores two and helm reads the criticals and finds one, which is
    the 43 points exactly. Written up in `crates/helm-bv/UPSTREAM.md`.
    
  • | what changed | ns per unit | a lance |
    |---|---|---|
    | the pilot | 2 | free |
    | armour or damage | 73,000 | 0.29 ms |
    | the whole loadout | 216,000 | 0.87 ms |
    
    Reseating a pilot recomputes *nothing* - battle value is the design's
    figure times a number from a 9x9 table - so the interactive case that
    sounded most expensive is the one that costs nothing at all. Repairing
    changes the armour but not the loadout, so the expensive half is skipped.
    
    Measured in wasm too, running the identical work under Node 24 from a
    `cdylib` with plain `extern "C"` exports and no bindgen: the arithmetic
    is **1.02x** native and reading a design is **1.31x**, the difference
    being string work. So a lance re-scores in about 0.3 ms in a browser and
    a company in under a millisecond, which is inside a frame either way.
    
    Measured again through the finished boundary, from Node against the
    real module rather than a sketch of one:
    
    | what a page does | cost |
    |---|---|
    | instantiate the module | 3 ms |
    | load the shipped catalogue | 88 ms, once |
    | score a design from its `.mtf` text | 253 us |
    | re-score a *held* design, damaged | 326 us |
    | re-crew a *held* design | **1 us** |
    
    Holding the design is what makes the last row true: reading the `.mtf`
    is most of what scoring costs, and a crew only scales a base that has
    already been worked out. Without a handle the same re-crew was 432us,
    which is the promise of the native benchmark being thrown away at the
    boundary. A lance re-crews in 4us and re-scores for damage in 1.3ms.
    
    Three things came out of writing it. Asking "is this location exposed"
    per item rather than per location made the cheapest rating cost more than
    reading the design; the loadout's mountings were rebuilt on every call
    when both ratings ask several times each; and `has_flag` walked every
    mounting's flag list for a question with eight distinct answers. Together
    those were 3x.
    
  • `helm wire-check` writes what a page would be handed - the catalogue,
    every design as text, and this build's answer for each - and
    `crates/helm-wasm/wire.mjs` scores all of them through the boundary and
    compares. Every design twice, fresh and in one condition that carries
    plate, frame, a wrecked slot and an empty magazine, since a `Condition`
    crosses as JSON and is its own chance to disagree.
    
    4,262 designs, 8,524 figures, none of them differing. Eight seconds for
    the check and forty to build the 39 MB bundle, which is derived and
    committed nowhere.
    

Force generation #

Formations #

  • The requirements themselves do not cross the bridge: each is a Java
    lambda with a display label beside it, and only the label survives. So
    `Rule::parse` reads the label back into a predicate and MegaMek's own
    per-unit answers are the oracle - 42,453 checks over 477 Meks, and every
    requirement helm claims to read means what MegaMek means by it. A label
    it cannot read becomes `Rule::Unreadable`, a force carrying one gets
    `Verdict::Unknown`, and nothing guesses.
    
    Two labels do not describe their requirement and one blueprint cannot be
    answered at all; all three are in `crates/helm-force/UPSTREAM.md`.
    

Faction and era availability #

Beyond Meks #

Housekeeping #

  • A database built from one MegaMek and queried about another answers every
    question and answers them about a library that is not there: designs
    since added are missing and figures since corrected are the old ones. The
    version was already recorded in both places; the comparison just had
    nowhere to be made.