Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211//! The types every other helm crate agrees on, and no I/O whatsoever.//!//! This crate exists to be the thing the rules are written against. Battle//! value and construction validation have to run in two places — in a browser//! while somebody edits a design, and again on the server that believes the//! answer — so they cannot depend on a filesystem, a database or a network.//! Keeping those types here, with nothing under them, is what makes that//! possible; `scripts/check-boundaries.sh` fails the build if it stops being//! true.//!//! The split that matters most is between what a unit file *declares* and what//! MegaMek *computes*://!//! * [`Unit`] is declared. It comes out of a `.mtf` or `.blk` and says what the//! designer chose.//! * [`ComputedStats`] is derived. Battle value, cost and the Alpha Strike//! conversion appear in no unit file; MegaMek works them out when it loads a//! design.//!//! [`ComputedStats`] is deliberately named for its shape rather than its//! source. Today the Java bridge fills it by asking MegaMek. Later `helm-bv`//! will fill it by computing it. Nothing downstream should be able to tell//! which, and having both fill one type is what makes conformance a diff//! rather than a project.
mod bv;mod catalogue;mod computed;mod design;mod metrics;mod rng;mod structure;mod unit;
pub use bv::{BvBreakdown, RATING_TOLERANCE};pub use catalogue::{Catalogue, EquipmentEntry};pub use computed::{AlphaStrike, ComputedStats};pub use design::{ ArmorKind, C3, CockpitKind, Engine, EngineKind, GyroKind, HeatSinkKind, HeatSinks, StructureKind,};pub use metrics::{ CombatMetrics, FORMATION_RANGES, FORMATION_SINGLE_RANGES, can_make_anti_mek_attacks, can_ride_a_mek, damage_at_range, equipment_list, jumping_mp, max_single_damage_at_range, mounted_items, walking_mp,};pub use rng::Rng;pub use structure::{Shape, max_armor, structure_per_location, structure_total_in};pub use unit::{Format, Mount, Unit};
/// Turn a battle value figure into the integer MegaMek would.////// Java's `Math.round` is `floor(x + 0.5)` - half goes *up*. Rust's/// `f64::round` is half *away from zero*. For a positive figure the two agree,/// and every figure battle value rounds today is positive, so this changes no/// answer as it stands.////// It exists for the one that will not be. A term that comes out negative -/// an explosive penalty on its own, a difference between two conditions -/// rounds one way here and the other way there, and -2.5 becoming -3 instead/// of -2 is a point nobody would find. Rounding in one named place means the/// rule is written down once rather than assumed at eleven call sites.pub fn round_half_up(value: f64) -> i64 { (value + 0.5).floor() as i64}
/// Strip the markers a critical slot puts after an equipment name.////// A `.mtf` writes how a slot is mounted into the slot itself -/// `CLTargeting Computer (OMNIPOD)`, `Medium Laser (R)` - so the text of a/// slot is not the name of the thing in it, and looking one up in the/// catalogue unstripped finds nothing. On an omni that costs the design its/// pod-mounted ammunition, which is most of what it shoots with.////// Only the known markers, never everything after the first bracket: a/// Rifleman RFL-3B carries a `Binary Laser (Blazer) Cannon`, and cutting at/// the bracket leaves `Binary Laser`, which matches nothing and disarms it.pub fn strip_slot_markers(slot: &str) -> String { let mut name = slot.to_string(); for marker in SLOT_MARKERS { while let Some(at) = name.find(marker) { name.replace_range(at..at + marker.len(), " "); } } name.split_whitespace().collect::<Vec<_>>().join(" ")}
/// The markers [`strip_slot_markers`] knows: rear-facing, pod-mounted,/// turret-mounted and armoured.pub const SLOT_MARKERS: [&str; 4] = ["(R)", "(OMNIPOD)", "(T)", "(ARMORED)"];
/// Reduce a name to lowercase alphanumerics.////// 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 — so matching on the raw string finds one and/// misses the others. Normalising both sides is what makes a search for/// "gauss rifle" return all of them.pub fn normalize(name: &str) -> String { name.chars() .filter(|c| c.is_ascii_alphanumeric()) .map(|c| c.to_ascii_lowercase()) .collect()}
/// Where a database's numbers came from.////// Without this, a conformance failure is indistinguishable from a database/// built against a different MegaMek — which is exactly the ambiguity you hit/// while chasing a deviation, and exactly when you can least afford it.#[derive(Debug, Clone, PartialEq, Eq)]pub struct Provenance { /// The MegaMek release the unit files and catalogue were read from. pub megamek_version: String, /// What filled [`ComputedStats`]: `megamek-bridge`, or `helm-bv` once /// that exists. pub stats_producer: String, /// The producer's own version, so a disagreement can be pinned to a /// change on our side rather than theirs. pub rules_version: Option<String>,}
impl Provenance { /// The provenance of a database whose computed values came from MegaMek /// itself by way of the Java bridge. pub fn from_bridge(megamek_version: impl Into<String>) -> Self { Provenance { megamek_version: megamek_version.into(), stats_producer: "megamek-bridge".to_string(), rules_version: None, } }}
/// `weightClass` codes, from MegaMek's `EntityWeightClass`.////// 5 is both `WEIGHT_COLOSSAL` and `WEIGHT_SUPER_HEAVY` upstream, and which/// one it reads as depends on the unit type, so the label carries both rather/// than picking one and being wrong half the time.pub fn weight_class_name(code: i64) -> Option<&'static str> { Some(match code { 0 => "Ultra Light", 1 => "Light", 2 => "Medium", 3 => "Heavy", 4 => "Assault", 5 => "Colossal / Super Heavy", 6 => "Small Craft", 7 => "Small DropShip", 8 => "Medium DropShip", 9 => "Large DropShip", 10 => "Small WarShip", 11 => "Large WarShip", 12 => "Small Support", 13 => "Medium Support", 14 => "Large Support", _ => return None, })}
/// Unit types that are scenery or ordnance rather than something that takes/// the field under its own power.////// These are the names the *files* use. MegaMek's summary spells several of/// them differently — `Gun Emplacement` against `.blk`'s `GunEmplacement` —/// and filtering on its spelling here silently excludes nothing at all.pub const NOT_PLAYABLE: &[&str] = &["GunEmplacement", "BuildingEntity", "HandheldWeapon"];
#[cfg(test)]mod tests { use super::*;
/// The rule, written down: half goes up, which is what `Math.round` does /// and what a battle value is rounded by. #[test] fn half_goes_up() { assert_eq!(round_half_up(1.5), 2); assert_eq!(round_half_up(2.5), 3); assert_eq!(round_half_up(1.49), 1); assert_eq!(round_half_up(1.0), 1); assert_eq!(round_half_up(0.0), 0); }
/// And the case Rust and Java disagree about, which is the whole reason /// this is a function rather than a method call: `f64::round` takes -2.5 /// to -3 and `Math.round` takes it to -2. #[test] fn a_negative_half_goes_up_too_rather_than_away_from_zero() { assert_eq!(round_half_up(-2.5), -2); assert_eq!(round_half_up(-0.5), 0); assert_eq!((-2.5f64).round() as i64, -3, "which is what Rust does"); }
#[test] fn normalize_collapses_megameks_three_spellings() { assert_eq!(normalize("ISGaussRifle"), "isgaussrifle"); assert_eq!(normalize("Gauss Rifle"), "gaussrifle"); assert_eq!(normalize("IS Gauss Ammo"), "isgaussammo"); // The point of the whole exercise: one substring reaches both weapons. assert!(normalize("ISGaussRifle").contains("gaussrifle")); assert!(normalize("CLGaussRifle").contains("gaussrifle")); }
#[test] fn weight_class_5_names_both_meanings() { assert_eq!(weight_class_name(3), Some("Heavy")); assert_eq!(weight_class_name(5), Some("Colossal / Super Heavy")); assert_eq!(weight_class_name(99), None); }}