Skip to main content

wowlab_engine_application/settings/
mod.rs

1//! Sim-config parsing and raid/consumable/racial settings resolution.
2
3use std::{collections::BTreeMap, str::FromStr};
4
5use strum::IntoEnumIterator;
6use wowlab_common::sim::intent::{IntentConfigError, SimConfigIntent, parse_sim_config};
7use wowlab_engine_ports::EngineError;
8use wowlab_types::{
9    constants::RACIAL_ON_USE_SPELL_IDS,
10    game::{RaceId, SpecId},
11};
12
13/// Map a race to its modeled on-use racial spell ID.
14#[must_use]
15pub(crate) fn racial_spell_id_for_race(race: RaceId) -> Option<u32> {
16    RACIAL_ON_USE_SPELL_IDS
17        .iter()
18        .find(|(r, _)| *r == race)
19        .map(|(_, id)| *id)
20}
21
22pub(crate) fn parse_config(sim_config: &str) -> Result<SimConfigIntent, IntentConfigError> {
23    parse_sim_config(sim_config)
24}
25
26pub(crate) fn parse_spec(config: &SimConfigIntent) -> Result<SpecId, EngineError> {
27    match SpecId::parse_wow_spec_id(config.spec) {
28        Ok(spec) => Ok(spec),
29        Err(error) => Err(EngineError::intent_validation(error.to_string())),
30    }
31}
32
33pub(crate) fn resolve_bloodlust(settings: &BTreeMap<String, toml::Value>) -> bool {
34    settings
35        .get("bloodlust")
36        .and_then(toml::Value::as_bool)
37        .unwrap_or(false)
38}
39
40pub(crate) fn resolve_raid_events(
41    settings: &BTreeMap<String, toml::Value>,
42) -> Result<Vec<wowlab_engine_ports::RaidEventConfig>, EngineError> {
43    settings.get("raid_events").map_or_else(
44        || Ok(Vec::new()),
45        |value| {
46            value.clone().try_into().map_err(|error| {
47                EngineError::intent_validation(format!(
48                    "settings[\"raid_events\"] is invalid: {error}"
49                ))
50            })
51        },
52    )
53}
54
55pub(crate) fn resolve_external_buffs(
56    settings: &BTreeMap<String, toml::Value>,
57) -> Result<Vec<wowlab_engine_ports::ExternalBuffConfig>, EngineError> {
58    settings.get("external_buffs").map_or_else(
59        || Ok(Vec::new()),
60        |value| {
61            value.clone().try_into().map_err(|error| {
62                EngineError::intent_validation(format!(
63                    "settings[\"external_buffs\"] is invalid: {error}"
64                ))
65            })
66        },
67    )
68}
69
70/// Resolve live-game bug toggles: baseline `bugs` (default true), then `bugs_enable`/`bugs_disable` slug arrays.
71pub(crate) fn resolve_bug_settings(
72    settings: &BTreeMap<String, toml::Value>,
73) -> wowlab_engine_ports::BugSettings {
74    let default_enabled = settings
75        .get("bugs")
76        .and_then(toml::Value::as_bool)
77        .unwrap_or(true);
78    let mut overrides = Vec::new();
79
80    for (key, enabled) in [("bugs_disable", false), ("bugs_enable", true)] {
81        let Some(slugs) = settings.get(key).and_then(toml::Value::as_array) else {
82            continue;
83        };
84
85        for slug in slugs {
86            if let Some(slug) = slug.as_str() {
87                // #t(rust_alloc_in_loop) each override slug becomes an owned settings entry.
88                overrides.push((slug.to_string(), enabled));
89            }
90        }
91    }
92
93    wowlab_engine_ports::BugSettings {
94        default_enabled,
95        overrides,
96    }
97}
98
99pub(crate) fn resolve_cast_latency(
100    settings: &BTreeMap<String, toml::Value>,
101) -> Result<wowlab_engine_ports::CastLatency, EngineError> {
102    fn value(
103        settings: &BTreeMap<String, toml::Value>,
104        key: &str,
105        default: u32,
106    ) -> Result<u32, EngineError> {
107        match settings.get(key) {
108            None => Ok(default),
109            Some(value) => value
110                .as_integer()
111                .and_then(|value| u32::try_from(value).ok())
112                .ok_or_else(|| {
113                    EngineError::intent_validation(format!(
114                        "settings[\"{key}\"] must be a nonnegative integer"
115                    ))
116                }),
117        }
118    }
119
120    let defaults = wowlab_engine_ports::CastLatency::default();
121
122    Ok(wowlab_engine_ports::CastLatency {
123        queue_ms: value(settings, "queue_lag_ms", defaults.queue_ms)?,
124        gcd_ms: value(settings, "gcd_lag_ms", defaults.gcd_ms)?,
125        channel_ms: value(settings, "channel_lag_ms", defaults.channel_ms)?,
126        queue_window_ms: value(settings, "spell_queue_window_ms", defaults.queue_window_ms)?,
127        strict_gcd_queue: settings.get("strict_gcd_queue").map_or(
128            Ok(defaults.strict_gcd_queue),
129            |value| {
130                value.as_bool().ok_or_else(|| {
131                    EngineError::intent_validation(
132                        "settings[\"strict_gcd_queue\"] must be a boolean",
133                    )
134                })
135            },
136        )?,
137    })
138}
139
140fn supported_race_names() -> String {
141    RaceId::iter()
142        .map(|r| r.to_string())
143        .collect::<Vec<_>>()
144        .join(", ")
145}
146
147pub(crate) fn resolve_race_setting(
148    settings: &BTreeMap<String, toml::Value>,
149) -> Result<RaceId, EngineError> {
150    match settings.get("race") {
151        None => Ok(RaceId::Human),
152        Some(v) => match v.as_str() {
153            None => Err(EngineError::intent_validation(format!(
154                "settings[\"race\"] must be a string, got {v:?}; supported: {}",
155                supported_race_names()
156            ))),
157            Some(s) => RaceId::from_str(s).map_err(|e| {
158                EngineError::intent_validation(format!(
159                    "unknown race {s:?}: {e}; supported: {}",
160                    supported_race_names()
161                ))
162            }),
163        },
164    }
165}
166
167mod consumables;
168
169pub(crate) use consumables::{parse_consumable_selection, resolve_consumable_spells};
170
171#[cfg(test)]
172mod tests;