# helm A queryable database of MegaMek's unit library. 10,988 units, indexed, in one SQLite file. Named for the Helm Memory Core, which is also a recovered library that turned out to be worth more than the fortress it was found in. ## Why Answering "which Clan heavy Meks were available by 3050 under 2200 BV" should be a query, not a recollection. This reads MegaMek's unit files and writes a database you can filter on tech base, unit type, era, tonnage, battle value, role, movement, quirks and loadout. ```sql SELECT name, year, mass, bv, role, point_value FROM meks WHERE tech_base = 'Clan' AND weight_class = 3 AND year <= 3050 AND bv BETWEEN 1800 AND 2200 ORDER BY bv DESC; ``` ## Build Everything here reads a MegaMek release, and this repository contains none of one. Get the one it is pinned against: ``` MM_HOME="$(./scripts/fetch-megamek.sh 0.51.0)" ``` That fetches the release tarball, checks it against a digest this repo carries — upstream publishes none — and unpacks it under `~/.cache/helm/megamek`, printing the path. It is the same release, and the same digest, that arena pins for the container a match runs in. ``` cargo build --release ./bridge/dump.sh "$MM_HOME" /tmp/helm-bridge # optional, see below ./target/release/helm build \ --megamek "$MM_HOME" \ --bridge-dir /tmp/helm-bridge \ --megamek-version 0.51.0 \ --out helm.sqlite ``` The install gets you the unit files and the art, which is all the readers need. It does not get you the equipment catalogue: MegaMek computes that when it loads, so it comes out through `bridge/dump.sh`, which runs a JDK in a container. Every command that prices a design needs both. `helm stats helm.sqlite` prints what a database contains. ## Battle value Battle value is computed when MegaMek loads a design and appears in no unit file, so helm borrowed it from the Java bridge. `helm-bv` is the replacement: the same figure, worked out here, so that a damaged unit or a different pilot can be scored rather than only stock designs. It is measured rather than asserted. MegaMek ships an answer for every unit it has, which makes 4,294 Meks an oracle - and `bridge/dump.sh` records the *working* of that calculation as well as its answer, so the defensive and offensive ratings are checked separately. A total that is forty points light could be light on armour or heavy on weapons, and one number cannot say which. ``` helm bv-report --megamek /path/to/megamek --bridge-dir /tmp/helm-bridge ``` That prints how many designs agree and exits non-zero while any of them do not. `crates/helm-bv/conformance.txt` is the same report, committed, so an improvement arrives as a visible diff and a regression cannot arrive quietly. Two more views of the same run, both for deciding what to do next rather than for reporting: helm bv-report ... --clusters # group the failures by what they share helm bv-report ... --unit "Atlas" # one design's working beside MegaMek's helm bv-report ... --rules # how many designs each rule fires on helm bv-report ... --damaged # the same, for designs that were shot at ### Forces inside a `.mul` A `.mul` records no force tree. Each entity carries its own chain — `Steiner Ceremonial|1||Command Lance|2||`, name and id per step, joined by `||`, with two more fields when a force has a camo of its own — and the tree is implied by every unit repeating its ancestry. `Mul::force_tree` rebuilds it, `Mul::loose_units` is everything in no force, and `helm force` prints the two so a MekBay force renders the way MekBay wrote it. Order is the file's: a force appears where its first unit does, which is what MegaMek wrote and what a reader expects to see. Ids are matched, not sorted. The same tree crosses the wasm boundary: `helm_mul_summary` carries `forces` and `loose` beside `units`, and `helm.mjs` exposes them as `force.forces` and `force.loose`, whose indices are into `force.units`. That is what lets a page draw a MekBay force the way MekBay wrote it without parsing XML of its own. A `.mul` is a force in a state rather than a design, so scoring one is its own command - and `--why` says where a damaged machine's value went, term by term, against the design as it left the factory: helm force --megamek --bridge-dir --why A file that arrived from outside is checked rather than trusted. `helm check` says what is wrong with one - a design the library does not have, a crew outside 0-8, a location a shape does not have, more plate or frame than a design was built with - and separates the file's own faults from the units helm cannot score yet, failing only on the first: helm check --megamek --bridge-dir And what it would take to put right - points of plate, points of frame, the equipment shot out and the magazines to fill, location by location: helm repairs --megamek --bridge-dir --why The other direction, for fitting a machine that has already been in a fight into a scenario's budget - damage a design until it is worth a given figure: helm wear "Atlas AS7-D" --to 1500 --megamek --bridge-dir `--write worn.mul` puts the result in a file MegaMek can load, rather than only on the screen. `--battle` does it with damage that has a shape - MegaMek's hit table, fire that clusters, equipment lost when it gets through the plate - so every seed is a different machine worth the same, and none of them is a mission kill: helm wear "Atlas AS7-D" --to 1500 --battle --seed 3 ... `--clusters` is the one that matters. A ranked list of the worst disagreements shows whichever design is strangest; grouping them by what they have in common shows which *rule* is missing, and its second section ranks the failures by how ordinary the design is - a plain biped that disagrees is a bug in arithmetic every design shares. The `.sqlite` is not committed. It is derived, it is 30MB, and MegaMek's data is CC BY-NC-SA 4.0 — build it locally. ## The browser's index The database is the server's and the agent's. A force-building screen wants a different shape: every design it may offer, with the columns a person filters and sorts on, small enough to fetch while somebody reads the page. ``` helm index --megamek /path/to/megamek --bridge-dir /tmp/helm-bridge --out units.json ``` 4,279 canon, valid Meks against 0.51.0 — 1.8MB of JSON, **174KB gzipped**, each one carrying chassis and model as two fields, era, tonnage, movement, armour, battle value, cost, the derived combat metrics, and the sprite that draws it. `--all-types` widens it to the rest of the library. It is a reduction of the same read rather than a second extract with its own truth: the same library, the same producer of computed values, the same `mekset.txt` resolution `helm art` uses. A figure here and a figure in the database cannot disagree, because neither is computed twice. Every file says which MegaMek it describes and what filled its battle values — `megamek-bridge` when a dump is at hand, `helm-bv` when it is not. That is what lets a consumer join it to art: the sprites are served under the same version string, so `//` is an address, and an index that did not name its release would be one whose pictures may not exist. Without `units.jsonl` there is no canon or validity flag, because those are MegaMek's judgement rather than anything a unit file declares — the run says so rather than quietly offering designs the game marks unplayable. ## Deploying the browser artifacts Three files go to the site's assets bucket, under one prefix named after the commit that built them: ``` AWS_PROFILE=lance-blue scripts/deploy.sh 0.51.0 ``` One command: fetch the release if it is not unpacked, dump the bridge if there is no dump for it, build the binary and the wasm, write the index and the catalogue, upload, record the id. The install and the dump are cached under `~/.cache/helm`, so a second run skips to the build; `--megamek` and `--bridge-dir` override either half. | | | fetched | |---|---|---| | `/helm_wasm.wasm` | the rules, compiled for a browser | 222 KB gz | | `//units.json` | the index a force screen filters | 174 KB gz | | `//catalogue.jsonl` | the equipment those rules read | 89 KB gz | Two levels because the artifacts are functions of two different things. The wasm is this repository's code alone; the index and the catalogue are that code over a particular MegaMek. Keyed by one of them, a rebuild silently replaces a file somebody is already fetching, which is exactly what an immutable cache header promises cannot happen — so a prefix is written once, nothing is overwritten, and nothing is ever invalidated. Which prefix the site fetches is `releases.helm` in infra's `releases.auto.tfvars.json`, alongside `api`, `arena` and `site`. This script writes it and applies nothing; `--dry-run` builds everything and uploads nothing. ## Unit art MegaMek ships a picture for every design and no way to tell from the tree which picture belongs to which unit: `data/images/units/` is 6,995 files and `mekset.txt` is the mapping. `helm art` writes it out. ``` helm art --megamek /path/to/megamek --out art.json ``` Against 0.51.0 that resolves 10,623 of 10,988 designs to 5,711 distinct files - every one of the 4,294 Meks, and every file it names is one the release ships. The 365 without art are vehicles, infantry and gun emplacements, which fall back to a generic default this does not implement yet. The mapping follows `MekTileset.entryFor`, which is three lookups rather than one: the exact display name, then the full chassis, then a `default_*` silhouette by shape and weight. Two of its details are quiet if you get them wrong - keys are compared uppercased, and a later entry overrides an earlier one, which is what `include` is for. Guessing a filename from the unit's name instead gets the common cases and then hands `Thor (Summoner) A` the wrong machine. The bytes are not helm's to move. `files` in the output is what a copy of the art tree is expected to carry and `missing_files` is what the set names and the release does not, so a sync that came out short can be told from a release that was short to begin with. ## Hooks ``` prek install ``` Installs the pre-commit and commit-msg hooks from `prek.toml`. Same set as the sibling repos where the file types overlap — whitespace, TOML and XML checks, shellcheck, `cargo fmt` and `cargo clippy` — plus two this repo needs of its own. `crate-boundaries` runs `scripts/check-boundaries.sh` when a `Cargo.toml` changes. That is the file a boundary breaks in, and the breakage otherwise surfaces much later as "why can this not compile to wasm". `commit-scope` checks the subject is Conventional Commits and that its scope names an epic in headquarters' `plan/` — `unit-library`, `unit-search`, `unit-rules` — or one of a short list of non-epic scopes. A commit with no scope passes. `cargo clippy` runs with `-j 2` here where the sibling repos do not bother: a full-parallel cargo build has taken this machine down, and a hook runs on every commit. ## What comes from where `helm-unitfile` reads MegaMek's two formats directly: `.mtf` for Meks, `.blk` for everything else. That is where the declared data comes from — chassis and model, tech base, era, tonnage, engine, movement, armour by location, the equipment list, the quirks, the critical slots. **Battle value, C-bill cost and the Alpha Strike conversion are not in those files.** MegaMek computes them when it loads a design. Reproducing them means reimplementing the construction rules, so until that happens `bridge/` asks MegaMek instead: two small Java programs that read `MekSummaryCache` and the equipment catalogue and write JSON Lines. The bridge is optional and meant to be temporary. Build without `--bridge-dir` and you get every declared column and null in the computed ones. Every computed column is a candidate for a native implementation that would retire a piece of it; that work belongs in this workspace. Both sides fill one type — `helm_core::ComputedStats` — so when a native producer exists, conformance is `ComputedStats::differences`, not a bespoke harness. The database records which producer filled it, in `meta`: without that a conformance failure is indistinguishable from a database built against a different MegaMek. ## MCP server `helm-mcp` exposes the library to an agent over MCP, scoped to BattleMeks. ``` helm-mcp path/to/helm.sqlite # or set HELM_DB ``` Six tools: `list_facets`, `find_units`, `get_unit`, `aggregate`, `resolve_equipment`, `random_unit`. Three properties matter more than the tool list, and the tests are written against them rather than against the shape of the output: - **Every search reports the total, not just what it returned.** An over-broad filter is obvious in one call, and comes back with the dimensions that would actually narrow *this* result. - **A wrong value explains itself.** `weight_class: ["enormous"]` returns the legal values, not an empty list — an empty list is indistinguishable from a filter that is merely too narrow, and costs a round trip to tell apart. - **`random_unit` is deterministic in its seed** and echoes it back. A force you cannot regenerate is one you cannot debug. Filtering runs over `helm-facet`'s predicate in memory rather than in SQL. That is a conformance decision, not a performance one: the predicate is what MegaMek's own filtering is checked against, so there is no query translation in the middle that can drift. ## Layout - `crates/helm-core` — the types every other crate agrees on, and **no I/O at all**. This is what the rules get written against, because battle value and construction validation have to run in a browser as well as on a server. - `crates/helm-unitfile` — the `.mtf` and `.blk` readers. Parsing one design is pure; reading a whole library is behind the `library` feature, which is off for wasm. - `crates/helm-bridge` — a *producer* of `ComputedStats`, and the temporary one. Reads what MegaMek computed. `helm-bv` will sit beside it and fill the same type by computing it. - `crates/helm-db` — schema and SQLite generation. Takes `ComputedStats` and cannot tell who produced them, which is what keeps a battle value dependency out of the database layer. - `crates/helm-cli` — the `helm` binary, and the only crate that picks a producer. - `crates/helm-mcp` — the MCP server. The one crate allowed a heavy dependency graph: nothing depends on it, so rmcp and its async runtime stop there. - `bridge/` — the Java side. Runs in a container, so no JDK is needed. `scripts/check-boundaries.sh` holds that graph in place: no I/O dependency may reach `helm-core`, and no producer may reach `helm-db`. Both boundaries erode silently otherwise, and the failure surfaces much later as "why can this not compile to wasm". ## Tables - `units` — one row per unit. Declared columns always populated; computed columns (`bv`, `cost`, `point_value`, `as_damage`, `as_specials`, `canon`, `invalid`, `weight_class`) populated only with the bridge. - `unit_equipment` — loadout, one row per mount, with its location. - `unit_criticals` — slot contents by location, empty slots included. - `unit_armor` — armour points by location. - `unit_quirks` — one row per quirk. - `unit_fields` — every key the typed columns do not claim: fluff text, manufacturers, system makers, and any field a future MegaMek adds. - `equipment` — the weapon and gear catalogue, with heat, damage and ranges. - `meta` — provenance and counts. Views: `playable` (canon, valid, excluding gun emplacements, buildings and handheld weapons), `meks`, `unit_weapons`, `equipment_primary`. Full-text search over name, chassis and model: ```sql SELECT u.* FROM units_fts f JOIN units u ON u.unit_id = f.rowid WHERE units_fts MATCH '"Timber Wolf"'; ``` ## Two things that will bite you **Equipment names.** MegaMek names the same weapon three ways depending on where you read it — `ISGaussRifle` in a loadout, `Gauss Rifle` in the catalogue, `IS Gauss Ammo` for its ammunition. Matching on the raw string finds one and misses the others. Use `name_norm`, which is the name reduced to lowercase alphanumerics: ```sql -- wrong: misses ISGaussRifle and CLGaussRifle entirely WHERE ue.name LIKE '%Gauss Rifle%' -- right WHERE ue.name_norm LIKE '%gaussrifle%' AND ue.is_ammo = 0 ``` `display_name` is the resolved human name and is never null. `canonical_name` is the name **MegaMek** looks the equipment up by, and it is the column to match on when the question is "what would MegaMek's own filter return". The two spellings are not the ones you would guess: MegaMek's internal name for the Atlas's autocannon is `Autocannon/20` and its display name is `AC/20`, and a single `.mtf` uses each in a different place — `AC/20` in the `Weapons:` block, `Autocannon/20` in the critical slots. **Clan names.** A dual-named design is `chassis (clanname) model` — `Black Hawk (Nova) Prime`, `Mad Cat (Timber Wolf) B`. 434 units have one, and treating the chassis as the whole name loses all of them. ## Licence Code here is this repo's. The unit data it reads is MegaMek's, under CC BY-NC-SA 4.0 — non-commercial and share-alike, which constrains redistributing a built database.