//! `helm wire-check`: what a page would be handed, and what this build says //! the answers are. //! //! The bundle it writes is the input to `crates/helm-wasm/wire.mjs`, which //! scores every design twice - once natively here and once through the wasm //! boundary - and fails on any disagreement. use crate::inputs::install_and_dump; use crate::options::parse_opts; /// Write what a browser needs to score the whole library, and what this build /// says the answers are. /// /// The rules compile twice, natively for the server and to wasm for the page, /// and the whole reason `helm-bv` does no I/O is that both have to agree. /// Nothing proved they did: the smoke test crosses the boundary with a handful /// of designs. This writes the bundle that lets `crates/helm-wasm/wire.mjs` /// check every one of them, which is a different claim entirely. /// /// Three files, all derived and none committed: the catalogue a page loads, /// every design as the text a page would be handed, and this build's answer /// for each, fresh and shot at - because a condition crossing the boundary as /// JSON is its own chance to disagree. pub(crate) fn wire_check(args: &[String]) -> Result<(), String> { let opts = parse_opts(args)?; let (megamek, bridge) = install_and_dump(&opts, "--bridge-dir is required: the catalogue lives there")?; let out = opts .out .ok_or("--out is required: the bundle wire.mjs reads has to go somewhere")?; std::fs::create_dir_all(&out).map_err(|e| format!("{}: {e}", out.display()))?; let catalogue = helm_bridge::read_catalogue(&bridge.join("equipment.jsonl")).map_err(|e| e.to_string())?; let zip = megamek.join(helm_unitfile::LIBRARY_IN_INSTALL); let file = std::fs::File::open(&zip).map_err(|e| format!("{}: {e}", zip.display()))?; let texts = helm_unitfile::read_zip_text(file).map_err(|e| e.to_string())?; // The condition every design is also scored in, chosen to touch the parts // of a `Condition` that cross as JSON: plate, frame, a wrecked slot and an // empty magazine. let damaged = helm_bv::Condition { armor: [("CT".to_string(), 3), ("LT".to_string(), 0)] .into_iter() .collect(), structure: [("RT".to_string(), 2)].into_iter().collect(), destroyed: [("Right Arm".to_string(), [4usize].into_iter().collect())] .into_iter() .collect(), empty_ammo: [("Left Torso".to_string(), [1usize].into_iter().collect())] .into_iter() .collect(), // Nothing swapped: what a slot holds is a `.mul`'s business, and this // sweep reads `.mtf` files. loaded: Default::default(), }; let mut designs = String::new(); let mut native = String::new(); let mut written = 0; for (path, text) in &texts { let Ok(unit) = helm_unitfile::parse_mtf(path, text) else { continue; }; let Ok(fresh) = helm_bv::battle_value(&unit, &catalogue) else { // Declined by decision or not yet written: a page will not be // asked about it either. continue; }; let hurt = helm_bv::battle_value_in(&unit, &catalogue, &damaged).ok(); let name = unit.display_name(); designs.push_str(&serde_json::json!({ "name": name, "mtf": text }).to_string()); designs.push('\n'); native.push_str( &serde_json::json!({ "name": name, "fresh": fresh, "damaged": hurt }).to_string(), ); native.push('\n'); written += 1; } let write = |file: &str, body: &str| -> Result<(), String> { let path = out.join(file); std::fs::write(&path, body).map_err(|e| format!("{}: {e}", path.display())) }; write("designs.jsonl", &designs)?; write("native.jsonl", &native)?; write( "condition.json", &serde_json::json!({ "armor": damaged.armor, "structure": damaged.structure, "destroyed": damaged.destroyed, "emptyAmmo": damaged.empty_ammo, }) .to_string(), )?; println!( "wrote {} ({written} designs, {:.1} MB)", out.display(), designs.len() as f64 / 1_048_576.0 ); println!( "check it with: node crates/helm-wasm/wire.mjs {} {}", bridge.join("equipment.jsonl").display(), out.display() ); Ok(()) }