Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373//! The command line: what the flags are, and what `--help` says about them.//!//! Parsed by hand. A derive-macro dependency would cost more to compile than//! it saves to read at this size, and the parser is small enough to hold in//! one screen and test exhaustively - which is what the tests at the bottom//! do, one property over every flag rather than a case per flag.
use std::path::PathBuf;
pub(crate) fn usage() { eprintln!( "helm - a queryable database of MegaMek's unit library
USAGE helm build --megamek <dir> --out <db> [--bridge-dir <dir>] helm stats <db> helm bv-report --megamek <dir> --bridge-dir <dir> helm art --megamek <dir> [--out <file>] helm index --megamek <dir> --bridge-dir <dir> --out <file> [--helm-version <ref>] [--filters <file>] helm force <force.mul> --megamek <dir> --bridge-dir <dir> helm check <force.mul> --megamek <dir> --bridge-dir <dir> helm repairs <force.mul> --megamek <dir> --bridge-dir <dir> helm wear <design> --to <bv> --megamek <dir> --bridge-dir <dir> helm catalogue --bridge-dir <dir> --out <file> helm wire-check --megamek <dir> --bridge-dir <dir> --out <bundle>
BUILD --megamek <dir> A MegaMek install. Reads data/mekfiles/unit_files.zip, or the loose .mtf and .blk files if the zip is absent. --out <db> Where to write the SQLite database. --bridge-dir <dir> Directory holding units.jsonl and equipment.jsonl from the Java bridge. Without it the database is built from the unit files alone and the computed columns - battle value, cost, Alpha Strike - are null. --megamek-version <v> Recorded in the meta table. Read from the install's docs/history.txt when not given.
BV-REPORT How helm-bv's battle values compare with MegaMek's own, design by design. Needs both: the library to compute from, and the bridge dump to check against. Exits non-zero while anything still disagrees. --unit <name> Print one design's working beside MegaMek's instead of the whole report. Matches on any part of the name. --clusters Group the designs that still disagree by what they have in common, worst first. What to fix next, rather than which single design is furthest out. --label <label> List the designs that disagree and carry that label, and which half of the calculation is wrong. `*` lists every design that disagrees. Implies --clusters. --rules How many designs each design-level rule fires on. A rule firing on nothing is a typo or a component this MegaMek does not ship, and both look like an unwritten rule. --bench What a recompute costs, split by what changed. Reseating a pilot and repairing armour are not the same work. --damaged Compare against damaged.jsonl instead of the whole library: what a handful of designs are worth once they have been shot at, which a .mtf cannot say.
CHECK What is wrong with a .mul before anything else touches it: designs that are not in the library, crews outside 0-8, locations a shape does not have, more plate or frame than a design was built with. Exits non-zero when a file has something wrong with it.
WEAR Damage a design until it is worth a given battle value, for fitting a machine that has already been in a fight into a scenario's budget. Plate first, then the frame under it, spread evenly across the locations. --to <bv> The figure to aim for. Required. --battle Damage it in a battle instead of shaving plate evenly: fire lands where the hit table sends it, clusters, and takes equipment out when it gets through. Every seed is a different machine worth the same, and none of them is a mission kill. --seed <n> One battle in particular. Implies --battle. --write <file.mul> Write the worn machine out as a .mul, so it can be loaded into MegaMek rather than only read here.
REPAIRS What a force needs putting right, location by location: points of plate, points of frame, the equipment shot out and the magazines to fill. --why Add what each machine's damage cost it in battle value.
FORCE What each machine in a .mul is worth in the state the file leaves it in, as flown by the crew the file seats. --why For each damaged machine, where its value went: the terms that moved against the design as it left the factory, worst loss first." );}#[derive(Debug, Default)]pub(crate) struct Opts { pub(crate) megamek: Option<PathBuf>, pub(crate) out: Option<PathBuf>, pub(crate) bridge_dir: Option<PathBuf>, pub(crate) version: Option<String>, pub(crate) unit: Option<String>, pub(crate) clusters: bool, /// `index`: every unit type rather than Meks alone. pub(crate) all_types: bool, /// `index`: the release id of the helm that built it. pub(crate) helm_version: Option<String>, /// `index`: a corpus of filters to answer beside the documents, for /// `facets.mjs` to check the wasm module against. pub(crate) filters: Option<PathBuf>, pub(crate) label: Option<String>, pub(crate) rules: bool, pub(crate) bench: bool, pub(crate) damaged: bool, pub(crate) why: bool, pub(crate) to: Option<i64>, pub(crate) out_mul: Option<PathBuf>, pub(crate) battle: bool, pub(crate) seed: Option<u64>,}pub(crate) fn parse_opts(args: &[String]) -> Result<Opts, String> { let mut o = Opts::default(); let mut i = 0; while i < args.len() { let need = |i: usize| -> Result<String, String> { args.get(i + 1) .cloned() .ok_or_else(|| format!("{} needs a value", args[i])) }; match args[i].as_str() { "--megamek" => { o.megamek = Some(PathBuf::from(need(i)?)); i += 2; } "--out" => { o.out = Some(PathBuf::from(need(i)?)); i += 2; } "--bridge-dir" => { o.bridge_dir = Some(PathBuf::from(need(i)?)); i += 2; } "--megamek-version" => { o.version = Some(need(i)?); i += 2; } "--helm-version" => { o.helm_version = Some(need(i)?); i += 2; } "--filters" => { o.filters = Some(PathBuf::from(need(i)?)); i += 2; } "--unit" => { o.unit = Some(need(i)?); i += 2; } "--clusters" => { o.clusters = true; i += 1; } "--all-types" => { o.all_types = true; i += 1; } "--label" => { o.label = Some(need(i)?); o.clusters = true; i += 2; } "--bench" => { o.bench = true; i += 1; } "--write" => { o.out_mul = Some(PathBuf::from(need(i)?)); i += 2; } "--to" => { o.to = Some( need(i)? .parse() .map_err(|_| "--to takes a battle value".to_string())?, ); i += 2; } "--battle" => { o.battle = true; i += 1; } "--seed" => { o.seed = Some( need(i)? .parse() .map_err(|_| "--seed takes a number".to_string())?, ); o.battle = true; i += 2; } "--damaged" => { o.damaged = true; i += 1; } "--why" => { o.why = true; i += 1; } "--rules" => { o.rules = true; i += 1; } other => return Err(format!("unknown flag: {other}")), } } Ok(o)}#[cfg(test)]mod tests { use super::*;
/// Every flag consumes itself and its value and nothing else. /// /// The parser walks the arguments with an index it steps itself, so a /// flag that takes a value and steps one lands on its own value and reads /// it as a flag, and one that takes none and steps two eats whatever /// follows. Both are silent, and the second is how an argument goes /// missing between the command line and the run. /// /// The test is that **exactly one** of `--flag` and `--flag 1` parses. A /// flag that wants a value refuses the first and takes the second; one /// that wants none takes the first and chokes on `1`. A flag that steps /// wrong accepts both, or neither. #[test] fn every_flag_consumes_exactly_what_it_takes() { for flag in FLAGS { // `1` because two of them want a number and the rest do not care. let alone = parse_opts(&[(*flag).to_string()]).is_ok(); let valued = parse_opts(&[(*flag).to_string(), "1".to_string()]).is_ok(); assert!( alone != valued, "{flag}: parses alone = {alone}, parses with a value = {valued}; \ one of those has to be false or it is stepping the cursor wrong" ); } }
/// A flag whose value is missing is an error rather than a default. #[test] fn a_flag_with_no_value_says_so() { for flag in FLAGS { if parse_opts(&[(*flag).to_string()]).is_ok() { continue; } let e = parse_opts(&[(*flag).to_string()]).unwrap_err(); assert!(e.contains(flag), "{flag}'s complaint does not name it: {e}"); } }
#[test] fn an_unknown_flag_is_refused_rather_than_ignored() { let e = parse_opts(&["--wintermute".to_string()]).unwrap_err(); assert!(e.contains("--wintermute"), "{e}"); }
/// Two flags turn another on, and a caller that gave only the one still /// gets the behaviour it asked for. #[test] fn a_flag_that_implies_another_sets_it() { assert!( parse_opts(&["--label".into(), "*".into()]) .unwrap() .clusters ); assert!(parse_opts(&["--seed".into(), "7".into()]).unwrap().battle); }
#[test] fn a_number_that_is_not_one_is_refused() { assert!(parse_opts(&["--to".into(), "lots".into()]).is_err()); assert!(parse_opts(&["--seed".into(), "lots".into()]).is_err()); }
/// Every command `main` dispatches is one `--help` mentions. /// /// Both lists are read out of the source rather than written down here: /// a third copy would drift the same way the first two did, and this way /// adding a command to the dispatch is what makes the test ask about it. /// `helm catalogue` and `helm wire-check` were dispatched and in no usage /// text when this was written. #[test] fn the_usage_text_names_every_command() { let usage = include_str!("options.rs") .split_once("USAGE") .expect("a usage block in this file") .1 .split_once("\n\n") .expect("a usage block that ends") .0; let dispatch = include_str!("main.rs"); let mut found = 0; for line in dispatch.lines() { let Some(rest) = line.trim().strip_prefix("Some(\"") else { continue; }; let Some((command, _)) = rest.split_once('"') else { continue; }; // `-h`, `--help` and `help` are the usage text itself. if command.starts_with('-') || command == "help" { continue; } found += 1; assert!( usage.contains(&format!("helm {command}")), "`helm {command}` is dispatched and not in the usage text" ); } assert!( found > 8, "only {found} commands were found in the dispatch" ); }
/// A required flag says why it is required. /// /// "--bridge-dir is required" tells somebody what to type and not what /// they were missing; three of these said only that, in three different /// commands that want it for three different reasons. #[test] fn every_required_flag_says_what_it_is_for() { for source in [ include_str!("build.rs"), include_str!("force.rs"), include_str!("report.rs"), include_str!("wire.rs"), include_str!("inputs.rs"), ] { for line in source.lines() { let Some(at) = line.find("is required") else { continue; }; let rest = &line[at + "is required".len()..]; assert!( rest.starts_with(':') || rest.starts_with(" <"), "a required flag with no reason: {}", line.trim() ); } } }
/// The flags `parse_opts` knows, for the tests that ask about all of them. const FLAGS: &[&str] = &[ "--megamek", "--out", "--bridge-dir", "--megamek-version", "--helm-version", "--filters", "--unit", "--clusters", "--all-types", "--label", "--bench", "--write", "--to", "--battle", "--seed", "--damaged", "--why", "--rules", ];}