Skip to main content

wowlab_engine/cli/
args.rs

1use clap::{Parser, Subcommand, ValueEnum};
2use wowlab_common::sim::defaults::DEFAULT_ITERATIONS;
3use wowlab_fs::path::PathBuf;
4use wowlab_types::{constants::DEFAULT_DURATION_S, game::SpecId};
5
6#[derive(Debug, Parser)]
7#[command(name = "wowlab-engine")]
8#[command(about = "WoW Lab Simulation Engine")]
9pub(super) struct Args {
10    /// Suppress non-essential output.
11    #[arg(long, short, global = true)]
12    pub quiet: bool,
13
14    #[command(subcommand)]
15    pub command: Command,
16}
17
18#[derive(Debug, Subcommand)]
19#[non_exhaustive]
20pub(super) enum Command {
21    /// Run a simulation.
22    Sim {
23        /// Spec to simulate (manifest slug, e.g. `beast_mastery_hunter`).
24        #[arg(short, long, value_parser = parse_spec)]
25        spec: SpecId,
26
27        /// Fight duration in seconds.
28        #[arg(short, long, default_value_t = DEFAULT_DURATION_S)]
29        duration: f64,
30
31        /// Number of iterations.
32        #[arg(short, long, default_value_t = DEFAULT_ITERATIONS)]
33        iterations: u32,
34
35        /// Number of threads (defaults to available cores).
36        #[arg(long)]
37        threads: Option<usize>,
38
39        /// Random seed.
40        #[arg(long)]
41        seed: Option<u64>,
42
43        /// Output format (text or json).
44        #[arg(short = 'f', long, default_value_t)]
45        format: OutputFormat,
46
47        /// Path to rotation JSON file.
48        #[arg(long)]
49        rotation: Option<PathBuf>,
50    },
51
52    /// Validate a rotation file.
53    Validate {
54        /// Path to rotation JSON file.
55        #[arg(short, long)]
56        rotation: PathBuf,
57
58        /// Spec to validate against.
59        #[arg(short, long, value_parser = parse_spec)]
60        spec: Option<SpecId>,
61    },
62
63    /// Inspect a spec's resolved spells, auras, and resources.
64    Inspect {
65        /// Spec to inspect.
66        #[arg(short, long, value_parser = parse_spec)]
67        spec: SpecId,
68
69        /// Output format (text or json).
70        #[arg(short = 'f', long, default_value_t)]
71        format: OutputFormat,
72
73        /// Filter by spell or aura ID.
74        #[arg(long)]
75        id: Option<u32>,
76    },
77
78    /// Audit all manifests against game data.
79    Audit {
80        /// Path to manifests directory (auto-detected if omitted).
81        #[arg(short, long)]
82        manifests: Option<PathBuf>,
83    },
84
85    /// Show version info.
86    Version,
87}
88
89// #t(rust_string_error) clap value parsers require a displayable string error
90fn parse_spec(s: &str) -> Result<SpecId, String> {
91    SpecId::from_manifest_slug(s).ok_or_else(|| {
92        format!(
93            "unknown spec '{}'. valid specs:\n  {}",
94            s,
95            valid_spec_slugs_for_error().join("\n  ")
96        )
97    })
98}
99
100fn valid_spec_slugs_for_error() -> Vec<&'static str> {
101    use strum::IntoEnumIterator;
102    let mut slugs: Vec<&str> = SpecId::iter()
103        .filter(|s| s.is_dps())
104        .map(SpecId::slug)
105        .collect();
106
107    slugs.sort_unstable();
108
109    slugs
110}
111
112#[derive(Clone, Copy, Debug, Default, ValueEnum)]
113#[non_exhaustive]
114pub(super) enum OutputFormat {
115    #[default]
116    Text,
117    Json,
118}
119
120impl std::fmt::Display for OutputFormat {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        let s = match self {
123            Self::Text => "text",
124            Self::Json => "json",
125        };
126
127        f.write_str(s)
128    }
129}