//! Native headless runner for the simulation. //! //! cargo run --release --example sim_run -- [options] //! //! Options (all optional; a preset gives the defaults, the others override it): //! --scenario NAME big-smart | logistics | ew-adaptive (default big-smart) //! --seed N random seed //! --layout FILE systems from a text file, replacing the preset's; one per line: //! `KIND X Z [HEADING_RAD] [empty]` (`#` starts a comment) //! KIND is a system key (patriot, tor, radar, depot, tzm, ew, ...) //! --raid SPEC replaces the preset's raids; repeatable. SPEC is //! `kind=count,kind=count[;dist=500][;aim=smart|objects|sams][;approach=sector|two|all|waves][;at=TICK]` //! --terrain T flat[:HEIGHT] | hills[:BASE:AMP:PERIOD] //! --ticks N tick limit (20 a second) //! --run-on do not stop when the raids have ended //! --ew / --no-ew EW stations on or off //! --json one JSON object instead of text //! --list list scenarios, system and drone kinds //! Exit code 1 when the run has problems (NaN positions, stuck trucks, a raid that never ended). //! Native timings are for comparing runs on one machine, not an estimate of WASM/GPU cost. mod harness; use airdef::sim::{Aim, Approach, DroneKind, SamKind}; use glam::DVec3; use harness::*; fn die(msg: &str) -> ! { eprintln!("sim_run: {msg}"); std::process::exit(2); } fn parse_raid(spec: &str) -> RaidSpec { let mut r = RaidSpec { kinds: Vec::new(), approach: Approach::Sector, aim: Aim::Smart, dist: 400.0, at: 1 }; for (i, part) in spec.split(';').enumerate() { if i == 0 { for kc in part.split(',').filter(|s| !s.is_empty()) { let (k, c) = kc.split_once('=').unwrap_or_else(|| die(&format!("raid kind `{kc}` needs =count"))); let kind = DroneKind::from_key(k).unwrap_or_else(|| die(&format!("unknown drone kind `{k}` (see --list)"))); r.kinds.push((kind, c.parse().unwrap_or_else(|_| die(&format!("bad count `{c}`"))))); } continue; } let (k, v) = part.split_once('=').unwrap_or_else(|| die(&format!("bad raid option `{part}`"))); match k { "dist" => r.dist = v.parse().unwrap_or_else(|_| die("bad dist")), "at" => r.at = v.parse().unwrap_or_else(|_| die("bad at")), "aim" => { r.aim = match v { "smart" => Aim::Smart, "objects" => Aim::Objects, "sams" => Aim::ObjectsAndSams, _ => die("aim: smart | objects | sams"), } } "approach" => { r.approach = match v { "sector" => Approach::Sector, "two" => Approach::TwoSectors, "all" => Approach::AllSides, "waves" => Approach::Waves, _ => die("approach: sector | two | all | waves"), } } _ => die(&format!("unknown raid option `{k}`")), } } r } fn parse_terrain(t: &str) -> Terrain { let n: Vec<&str> = t.split(':').collect(); let num = |i: usize, d: f64| n.get(i).map_or(d, |s| s.parse().unwrap_or_else(|_| die("bad terrain number"))); match n[0] { "flat" => Terrain::Flat(num(1, 70.0) as i32), "hills" => Terrain::Hills { base: num(1, 70.0) as i32, amp: num(2, 14.0), period: num(3, 90.0) }, _ => die("terrain: flat[:H] | hills[:BASE:AMP:PERIOD]"), } } fn parse_layout(path: &str) -> Vec { let text = std::fs::read_to_string(path).unwrap_or_else(|e| die(&format!("{path}: {e}"))); let mut units = Vec::new(); for (n, line) in text.lines().enumerate() { let line = line.split('#').next().unwrap_or("").trim(); if line.is_empty() { continue; } let w: Vec<&str> = line.split_whitespace().collect(); let bad = || die(&format!("{path}:{}: expected `KIND X Z [HEADING] [empty]`", n + 1)); if w.len() < 3 { bad(); } let kind = SamKind::from_key(w[0]).unwrap_or_else(|| die(&format!("{path}:{}: unknown system `{}`", n + 1, w[0]))); let f = |s: &str| s.parse::().unwrap_or_else(|_| bad()); let heading = w.get(3).filter(|s| **s != "empty").map_or(0.0, |s| f(s)); units.push(Unit { kind, pos: DVec3::new(f(w[1]), 70.0, f(w[2])), heading, empty: w.contains(&"empty") }); } units } pub fn main() { let args: Vec = std::env::args().skip(1).collect(); let mut name = "big-smart".to_string(); let mut i = 0; // The scenario first, so the other options override it whatever their order. while i < args.len() { if args[i] == "--scenario" { name = args.get(i + 1).cloned().unwrap_or_else(|| die("--scenario needs a name")); } i += 1; } let mut sc = preset(&name).unwrap_or_else(|| die(&format!("unknown scenario `{name}`; one of {PRESETS:?}"))); let (mut json, mut raids) = (false, Vec::new()); let mut it = args.iter(); while let Some(arg) = it.next() { let mut value = || it.next().cloned().unwrap_or_else(|| die(&format!("{arg} needs a value"))); match arg.as_str() { "--scenario" => { value(); } "--seed" => sc.seed = value().parse().unwrap_or_else(|_| die("bad seed")), "--ticks" => sc.ticks = value().parse().unwrap_or_else(|_| die("bad ticks")), "--layout" => sc.units = parse_layout(&value()), "--raid" => raids.push(parse_raid(&value())), "--terrain" => sc.terrain = parse_terrain(&value()), "--run-on" => sc.stop_at_end = false, "--ew" => sc.ew = true, "--no-ew" => sc.ew = false, "--json" => json = true, "--list" => { println!("scenarios: {PRESETS:?}"); println!("systems: {:?}", SamKind::ALL.iter().map(|k| k.key()).collect::>()); println!("drones: {:?}", DroneKind::ALL.iter().map(|k| k.spec().key).collect::>()); return; } "-h" | "--help" => { println!("{}", include_str!("main.rs").lines().take_while(|l| l.starts_with("//")).map(|l| l.trim_start_matches("//!").trim_start_matches("//").to_string() + "\n").collect::()); return; } other => die(&format!("unknown option `{other}` (try --help)")), } } if !raids.is_empty() { sc.raids = raids; } let r = run(&sc); if json { println!("{}", r.json()); } else { print!("{}", r.text()); } let problems = r.problems(); if !problems.is_empty() { eprintln!("PROBLEMS: {}", problems.join("; ")); std::process::exit(1); } }