Skip to main content

forge/
compare.rs

1//! Compare subcommand: runs `WoW` Lab engine and `SimC`, then renders a side-by-side comparison.
2
3use anyhow::Result;
4use clap::Args;
5use wowlab_common::output;
6use wowlab_types::game::SpecId;
7
8use crate::{
9    encounter_fixture::EncounterFixture,
10    provider::{ComparisonConfig, SimProvider},
11    render,
12    run::{FightDurationSeconds, IterationCount, RunParameters, RunParametersError},
13    simc::SimcProvider,
14    trace,
15    wowlab::WowlabProvider,
16};
17
18#[derive(Args, Debug)]
19#[expect(
20    clippy::struct_excessive_bools,
21    reason = "independent CLI feature switches are represented directly by clap"
22)]
23pub(crate) struct CompareArgs {
24    /// Spec slug (e.g. `outlaw_rogue`, `fire_mage`).
25    pub spec: String,
26
27    /// Show side-by-side cast timeline.
28    #[arg(long)]
29    pub timeline: bool,
30
31    /// Show per-source resource-income comparison.
32    #[arg(long)]
33    pub resources: bool,
34
35    /// Seconds of timeline to show (default 30).
36    #[arg(long, default_value = "30")]
37    pub timeline_duration: u32,
38
39    /// Fight duration in seconds (default 300).
40    #[arg(long, default_value = "300")]
41    pub duration: u32,
42
43    /// Number of iterations (default 1 for compare).
44    #[arg(long, default_value = "1")]
45    pub iterations: u32,
46
47    /// Enable TRACE-level logging to a file.
48    #[arg(long)]
49    pub trace: bool,
50
51    /// Player race (e.g. `orc`, `troll`, `human`). Defaults to `human`.
52    #[arg(long)]
53    pub race: Option<String>,
54
55    /// Run `WoW` Lab with one of the canonical Phase 8 encounter fixtures.
56    #[arg(long, value_enum)]
57    pub encounter_fixture: Option<EncounterFixture>,
58
59    /// Disable modeled live-game bugs on both sides (`SimC` `bugs=0`).
60    #[arg(long)]
61    pub no_bugs: bool,
62
63    /// Run `SimC` with `debug=1` and retain its log (per-spell static modifiers, buff and cast events).
64    #[arg(long)]
65    pub simc_debug: bool,
66}
67
68impl TryFrom<&CompareArgs> for RunParameters {
69    type Error = RunParametersError;
70
71    fn try_from(args: &CompareArgs) -> Result<Self, Self::Error> {
72        let fight_duration = FightDurationSeconds::try_from(args.duration)?;
73        let iterations = IterationCount::try_from(args.iterations)?;
74
75        Ok(Self::new(iterations, fight_duration))
76    }
77}
78
79pub(crate) fn run(args: &CompareArgs) -> Result<()> {
80    let parameters = RunParameters::try_from(args)?;
81    let spec = SpecId::from_manifest_slug(&args.spec)
82        .ok_or_else(|| anyhow::anyhow!("unknown spec slug: {}", args.spec))?;
83
84    let trace_path = trace::init_trace_log(args.trace, &args.spec)?;
85
86    output::header(&format!("Comparing {} (wowlab vs SimC)", args.spec));
87
88    if let Some(ref path) = trace_path {
89        output::detail(&format!("Trace log: {}", path.display()));
90    }
91
92    output::blank();
93
94    let config = ComparisonConfig {
95        spec,
96        parameters,
97        race: args.race.clone(),
98        bugs: !args.no_bugs,
99        simc_debug: args.simc_debug,
100    };
101
102    let wl_provider = WowlabProvider::new(args.encounter_fixture);
103    let sc_provider = SimcProvider;
104
105    let wl = wl_provider.run(&config)?;
106    let sc = sc_provider.run(&config)?;
107
108    if let Some(fixture) = args.encounter_fixture {
109        render::print_encounter_banner(&args.spec, parameters, fixture.slug());
110    } else {
111        render::print_banner(&args.spec, parameters);
112    }
113
114    render::print_dps_comparison(&wl, &sc, wl_provider.name(), sc_provider.name());
115    render::print_spell_comparison(&wl, &sc);
116
117    if let Some(report) = &wl.encounter_debug {
118        crate::encounter_debug::print_report(report);
119    }
120
121    if args.resources {
122        render::print_resource_comparison(&wl, &sc);
123    }
124
125    if args.timeline {
126        render::print_timeline(&wl, &sc, f64::from(args.timeline_duration));
127    }
128
129    if let Some(ref path) = trace_path {
130        output::blank();
131        output::detail(&format!("Trace log written to: {}", path.display()));
132    }
133
134    Ok(())
135}