Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
8.1 kB · 296 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297//! Fixtures are trimmed from real MegaMek files. They keep the parts that are//! actually awkward to read - the clan name, the weapons count block, a crit//! section, a value with a second colon in it - rather than a tidy sample.
use helm_unitfile::{Format, parse};
const ATLAS: &str = "\# MegaMek Data (C) by The MegaMek Team, CC BY-NC-SA 4.0.generator:MegaMek Suite 0.51.0chassis:Atlasmodel:AS7-Dmul id:140
Config:Bipedtechbase:Inner Sphereera:2755source:TR:3039rules level:1role:Juggernaut
quirk:battle_fists_laquirk:command_mech
mass:100engine:300 Fusion Enginestructure:IS Standard
heat sinks:20 Singlewalk mp:3jump mp:0
armor:Standard(Inner Sphere)LA armor:34CT armor:47RTC armor:14
Weapons:3Medium Laser, Left ArmAC/20, Right TorsoMedium Laser (R), Center Torso
Left Arm:ShoulderMedium Laser-Empty-
systemmanufacturer:CHASSIS:Foundationmanufacturer:Yori 'Mech Works|Defiance Industries";
// The Clan-named case. MegaMek renders these "chassis (clanname) model", and// getting it wrong silently breaks every join against MegaMek's own naming.const RYOKEN: &str = "\chassis:Ryoken III-XPclanname:Skinwalkermodel:BConfig:Bipedtechbase:Clanera:3135mass:55walk mp:6jump mp:0armor:Standard(Clan)Weapons:0";
const CONDOR: &str = "\#Saved from version 0.51.0<UnitType>Tank</UnitType>
<Name>Condor HoverBall</Name>
<Model>
</Model>
<year>2564</year>
<type>IS Level 2</type>
<role>Striker</role>
<motion_type>Hover</motion_type>
<cruiseMP>9</cruiseMP>
<armor>4242424240</armor>
<Body Equipment>IS Ammo AC/5ISCASE</Body Equipment>
<Turret Equipment>Machine GunAutocannon/5Machine Gun</Turret Equipment>
<tonnage>50.0</tonnage>";
#[test]fn reads_an_mtf() { let u = parse("meks/Atlas AS7-D.mtf", ATLAS.as_bytes()).expect("parses"); assert_eq!(u.format_str, Format::Mtf.as_str()); assert_eq!(u.chassis, "Atlas"); assert_eq!(u.model, "AS7-D"); assert_eq!(u.name, "Atlas AS7-D"); assert_eq!(u.mul_id, Some(140)); assert_eq!(u.unit_type.as_deref(), Some("Mek")); assert_eq!(u.config.as_deref(), Some("Biped")); assert_eq!(u.tech_base.as_deref(), Some("Inner Sphere")); assert_eq!(u.year, Some(2755)); assert_eq!(u.mass, Some(100.0)); assert_eq!(u.walk_mp, Some(3)); assert_eq!(u.role.as_deref(), Some("Juggernaut")); // "source:TR:3039" - the value keeps its own colon. assert_eq!(u.source.as_deref(), Some("TR:3039"));}
#[test]fn armor_type_is_not_an_armor_location() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!(u.armor.as_deref(), Some("Standard(Inner Sphere)")); assert_eq!(u.armor_locations.get("LA"), Some(&34)); assert_eq!(u.armor_locations.get("CT"), Some(&47)); assert_eq!(u.armor_locations.get("RTC"), Some(&14)); assert!(!u.armor_locations.contains_key("ARMOR"));}
#[test]fn reads_the_weapons_block() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!(u.equipment.len(), 3); assert_eq!(u.equipment[0].name, "Medium Laser"); assert_eq!(u.equipment[0].location.as_deref(), Some("Left Arm")); assert!(!u.equipment[0].rear); // The (R) suffix marks a rear mount and is not part of the name. assert_eq!(u.equipment[2].name, "Medium Laser"); assert!(u.equipment[2].rear);}
#[test]fn reads_crit_slots_including_empties() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); let la = u.criticals.get("Left Arm").expect("left arm section"); assert_eq!(la, &["Shoulder", "Medium Laser", "-Empty-"]);}
#[test]fn collects_quirks_rather_than_overwriting_them() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!(u.quirks, vec!["battle_fists_la", "command_mech"]);}
#[test]fn keeps_unclaimed_fields() { let u = parse("a.mtf", ATLAS.as_bytes()).unwrap(); assert_eq!( u.fields.get("manufacturer").map(String::as_str), Some("Yori 'Mech Works|Defiance Industries") ); // A repeated key joins rather than letting the last one win. assert!(u.fields.contains_key("systemmanufacturer"));}
#[test]fn clan_name_goes_in_the_display_name() { let u = parse("meks/Ryoken III-XP B.mtf", RYOKEN.as_bytes()).unwrap(); assert_eq!(u.clan_name.as_deref(), Some("Skinwalker")); assert_eq!(u.name, "Ryoken III-XP (Skinwalker) B");}
#[test]fn reads_a_blk() { let u = parse("vehicles/Condor HoverBall.blk", CONDOR.as_bytes()).unwrap(); assert_eq!(u.format_str, Format::Blk.as_str()); assert_eq!(u.chassis, "Condor HoverBall"); // An empty <Model> block leaves the name as the chassis alone. assert_eq!(u.model, ""); assert_eq!(u.name, "Condor HoverBall"); assert_eq!(u.unit_type.as_deref(), Some("Tank")); assert_eq!(u.year, Some(2564)); assert_eq!(u.mass, Some(50.0)); assert_eq!(u.motion_type.as_deref(), Some("Hover")); assert_eq!(u.walk_mp, Some(9));}
#[test]fn blk_equipment_blocks_carry_their_location() { let u = parse("a.blk", CONDOR.as_bytes()).unwrap(); assert_eq!(u.equipment.len(), 5); let turret: Vec<&str> = u .equipment .iter() .filter(|m| m.location.as_deref() == Some("Turret")) .map(|m| m.name.as_str()) .collect(); assert_eq!(turret, vec!["Machine Gun", "Autocannon/5", "Machine Gun"]);}
#[test]fn blk_armor_array_gets_location_names() { let u = parse("a.blk", CONDOR.as_bytes()).unwrap(); assert_eq!(u.armor_locations.get("FRONT"), Some(&42)); assert_eq!(u.armor_locations.get("TURRET"), Some(&40)); assert_eq!(u.armor_locations.values().sum::<i64>(), 208);}
// A shipped file opens its <UnitType> block with a blank line. Joining the// block body verbatim made its unit type "\nTank", which then matched no// filter and quietly escaped every unit-type exclusion.#[test]fn blank_lines_around_a_block_body_are_not_part_of_the_value() { let blk = "<UnitType>\n\nTank\n\n</UnitType>\n\n<Name>\n\nScrapper\n</Name>\n"; let u = parse("a.blk", blk.as_bytes()).unwrap(); assert_eq!(u.unit_type.as_deref(), Some("Tank")); assert_eq!(u.chassis, "Scrapper");}
#[test]fn rejects_a_file_with_no_unit_in_it() { assert!(parse("empty.mtf", b"# just a comment\n").is_err()); assert!(parse("notes.txt", b"chassis:Atlas\n").is_err());}
/// A patchwork design names an armour type per location, in front of the/// points: `LT armor:Clan Standard(Clan):26`.////// Reading this wrongly is silent and total. Insisting on a bare integer drops/// every location of every patchwork design, so the unit parses fine and comes/// out with almost no armour - which was true of all five of them in MegaMek/// 0.51.0 until this was fixed, and moved total armour from 28 to 247 on a/// Zeus-X.const PATCHWORK: &str = "\chassis:Zeus-Xmodel:ZEU-Xmass:80armor:PatchworkLA armor:IS Reactive(Inner Sphere):26RA armor:IS Reactive(Inner Sphere):26CT armor:Clan Standard(Clan):38HD armor:9";
#[test]fn patchwork_armor_keeps_both_its_points_and_its_type() { let u = parse("x.mtf", PATCHWORK.as_bytes()).unwrap(); assert_eq!(u.armor.as_deref(), Some("Patchwork")); assert_eq!(u.armor_locations.get("LA"), Some(&26)); assert_eq!(u.armor_locations.get("CT"), Some(&38)); assert_eq!(u.total_armor(), 99);
assert_eq!( u.armor_types.get("LA").map(String::as_str), Some("IS Reactive(Inner Sphere)") ); assert_eq!(u.armor_kind_at("LA"), helm_core::ArmorKind::Reactive); assert_eq!(u.armor_kind_at("CT"), helm_core::ArmorKind::Standard);
// A location that names no type falls back to the design's, which for a // patchwork design is Patchwork itself rather than a real armour. assert_eq!(u.armor_kind_at("HD"), helm_core::ArmorKind::Patchwork); assert!(!u.armor_types.contains_key("HD"));}
/// The ordinary case must not have grown a type map.#[test]fn a_design_with_one_armour_type_records_no_per_location_types() { let u = parse("x.mtf", ATLAS.as_bytes()).unwrap(); assert!(u.armor_types.is_empty()); assert_eq!(u.armor_kind_at("LA"), helm_core::ArmorKind::Standard);}