Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173//! What MegaMek works out that no unit file says.//!//! Battle value, C-bill cost and the Alpha Strike conversion are derived from//! a design when MegaMek loads it. They are in no `.mtf` or `.blk`, and//! producing them means applying the construction rules.//!//! This type is named for its shape and not its source, which is the whole//! point of it. Two things fill it://!//! * the Java bridge, by asking MegaMek — what happens today;//! * `helm-bv`, by computing it — what should happen.//!//! Nothing downstream is allowed to tell which, so `helm-db` never grows a//! battle value dependency. And because both producers fill one type,//! conformance is a diff between two `ComputedStats` rather than a bespoke//! comparison harness.
/// A unit's Alpha Strike conversion.////// Kept as MegaMek's own notation rather than exploded — `damage` is a string/// like `5/5/2` and `specials` like `AC2/2/-, IF1, SRCH` — because that/// notation is the game's, and parsing it is a job for whoever needs the parts.#[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct AlphaStrike { pub point_value: Option<i64>, pub unit_type: Option<String>, pub size: Option<i64>, pub tmm: Option<i64>, pub damage: Option<String>, /// Null for 967 units in MegaMek 0.51.0, 887 of them Tanks: /// `ASTurretSummary.getSpecialsDisplayString()` throws on certain turreted /// designs. That is an upstream bug, and it is deviation number one. pub specials: Option<String>,}
impl AlphaStrike { pub fn is_empty(&self) -> bool { self == &AlphaStrike::default() }}
/// The derived figures for one unit.////// Every field is optional: a database built without a producer is a smaller/// answer, not a broken one.#[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct ComputedStats { /// MegaMek's display name, `chassis (clanname) model`. The join key back /// to a [`crate::Unit`]. pub name: String,
pub battle_value: Option<i64>, pub cost: Option<i64>, pub tech_level: Option<String>, pub weight_class: Option<i64>, pub canon: Option<bool>, pub invalid: Option<bool>, pub omni: Option<bool>, pub clan: Option<bool>, /// What the design walks once everything it carries has had its say, /// which is not the figure its file declares - a shield costs a point. pub walk_mp: Option<i64>, /// Derived from walk MP and the engine, so computed rather than declared. pub run_mp: Option<i64>, /// What the design jumps, which is its jets and not its `jump mp:` line. pub jump_mp: Option<i64>,
pub alpha_strike: AlphaStrike,}
impl ComputedStats { pub fn new(name: impl Into<String>) -> Self { ComputedStats { name: name.into(), ..Default::default() } }
/// Fields where two producers disagree, as `(field, left, right)`. /// /// This is the conformance harness in one method: run the bridge and /// `helm-bv` over the same library, and every non-empty result is either a /// bug of ours or a registered deviation of theirs. Fields the other side /// did not populate are not disagreements — a producer that computes only /// battle value should not read as wrong about cost. pub fn differences(&self, other: &ComputedStats) -> Vec<(&'static str, String, String)> { let mut out = Vec::new();
fn cmp<T: PartialEq + std::fmt::Debug>( out: &mut Vec<(&'static str, String, String)>, field: &'static str, a: &Option<T>, b: &Option<T>, ) { if let (Some(a), Some(b)) = (a, b) && a != b { out.push((field, format!("{a:?}"), format!("{b:?}"))); } }
cmp( &mut out, "battle_value", &self.battle_value, &other.battle_value, ); cmp(&mut out, "cost", &self.cost, &other.cost); cmp(&mut out, "tech_level", &self.tech_level, &other.tech_level); cmp( &mut out, "weight_class", &self.weight_class, &other.weight_class, ); cmp(&mut out, "canon", &self.canon, &other.canon); cmp(&mut out, "invalid", &self.invalid, &other.invalid); cmp(&mut out, "omni", &self.omni, &other.omni); cmp(&mut out, "clan", &self.clan, &other.clan); cmp(&mut out, "run_mp", &self.run_mp, &other.run_mp);
let (a, b) = (&self.alpha_strike, &other.alpha_strike); cmp(&mut out, "as.point_value", &a.point_value, &b.point_value); cmp(&mut out, "as.unit_type", &a.unit_type, &b.unit_type); cmp(&mut out, "as.size", &a.size, &b.size); cmp(&mut out, "as.tmm", &a.tmm, &b.tmm); cmp(&mut out, "as.damage", &a.damage, &b.damage); cmp(&mut out, "as.specials", &a.specials, &b.specials);
out }}
#[cfg(test)]mod tests { use super::*;
#[test] fn identical_stats_do_not_differ() { let mut a = ComputedStats::new("Atlas AS7-D"); a.battle_value = Some(1897); let b = a.clone(); assert!(a.differences(&b).is_empty()); }
#[test] fn a_disagreement_names_the_field_and_both_answers() { let mut a = ComputedStats::new("Atlas AS7-D"); a.battle_value = Some(1897); let mut b = ComputedStats::new("Atlas AS7-D"); b.battle_value = Some(1900);
let diff = a.differences(&b); assert_eq!(diff.len(), 1); assert_eq!(diff[0].0, "battle_value"); assert_eq!((diff[0].1.as_str(), diff[0].2.as_str()), ("1897", "1900")); }
// A producer that computes battle value and nothing else must not read as // wrong about every field it declined to fill. #[test] fn a_field_only_one_side_populated_is_not_a_disagreement() { let mut bridge = ComputedStats::new("Atlas AS7-D"); bridge.battle_value = Some(1897); bridge.cost = Some(9_626_000);
let mut ours = ComputedStats::new("Atlas AS7-D"); ours.battle_value = Some(1897);
assert!(bridge.differences(&ours).is_empty()); }}