Skip to main content

forge/
provider.rs

1//! `SimProvider` trait and shared comparison types.
2
3use std::collections::BTreeMap;
4
5use crate::run::RunParameters;
6
7#[derive(Clone, Debug)]
8pub(crate) struct ComparisonConfig {
9    pub spec: wowlab_types::game::SpecId,
10    pub parameters: RunParameters,
11    pub race: Option<String>,
12    /// Model live-game bugs (both sides); `false` maps to `SimC` `bugs=0`.
13    pub bugs: bool,
14    /// Run `SimC` with `debug=1` and retain its log for factor-by-factor comparison.
15    pub simc_debug: bool,
16}
17
18#[derive(Clone, Debug)]
19pub(crate) struct SimOutput {
20    pub dps: f64,
21    pub spells: BTreeMap<String, SpellResult>,
22    pub timeline: Vec<CastEntry>,
23    pub encounter_debug: Option<crate::encounter_debug::EncounterDebugReport>,
24    /// Per-source primary-resource income (mean per iteration), by source name.
25    pub resource_gains: BTreeMap<String, ResourceGain>,
26}
27
28impl SimOutput {
29    /// Builds output, merging duplicate spell names by summing DPS and casts.
30    pub(crate) fn new(
31        dps: f64,
32        spells: Vec<(String, SpellResult)>,
33        timeline: Vec<CastEntry>,
34    ) -> Self {
35        let mut merged: BTreeMap<String, SpellResult> = BTreeMap::new();
36
37        for (name, result) in spells {
38            let entry = merged.entry(name).or_default();
39
40            entry.dps += result.dps;
41            entry.casts += result.casts;
42            entry.hits += result.hits;
43            entry.pct += result.pct;
44        }
45
46        let mut spells = merged;
47
48        let mut timeline_casts: BTreeMap<&str, u32> = BTreeMap::new();
49
50        for cast in &timeline {
51            *timeline_casts.entry(cast.spell_name.as_str()).or_insert(0) += 1;
52        }
53
54        for (name, casts) in timeline_casts {
55            if let Some(spell) = spells.get_mut(name) {
56                if spell.casts == 0 {
57                    spell.casts = casts;
58                }
59            } else {
60                let key = String::from(name);
61
62                spells.insert(
63                    key,
64                    SpellResult {
65                        casts,
66                        ..SpellResult::default()
67                    },
68                );
69            }
70        }
71
72        Self {
73            dps,
74            spells,
75            timeline,
76            encounter_debug: None,
77            resource_gains: BTreeMap::new(),
78        }
79    }
80
81    #[must_use]
82    pub(crate) fn with_encounter_debug(
83        mut self,
84        report: crate::encounter_debug::EncounterDebugReport,
85    ) -> Self {
86        self.encounter_debug = Some(report);
87
88        self
89    }
90}
91
92/// One resource-income source: mean gained and overcap waste per iteration.
93#[derive(Clone, Copy, Debug, Default)]
94// #t(rust_similar_structs) Forge report values are detached from engine telemetry accumulator ownership
95pub(crate) struct ResourceGain {
96    pub gained: f64,
97    pub wasted: f64,
98}
99
100#[derive(Clone, Debug, Default)]
101pub(crate) struct SpellResult {
102    pub dps: f64,
103    pub casts: u32,
104    /// Damage events landed: direct hits plus periodic ticks.
105    ///
106    /// Unlike `casts` this is defined for proc, pet, guardian and `DoT` rows.
107    /// It is therefore the only per-event normalizer both sides can be compared on.
108    pub hits: u32,
109    pub pct: f64,
110}
111
112#[derive(Clone, Debug)]
113pub(crate) struct CastEntry {
114    pub time_secs: f64,
115    pub spell_name: String,
116}
117
118pub(crate) trait SimProvider {
119    fn name(&self) -> &str;
120    fn run(&self, config: &ComparisonConfig) -> anyhow::Result<SimOutput>;
121}