Skip to main content

wowlab_engine_ports/
game_bugs.rs

1//! Central registry of modeled live-game bugs.
2
3use crate::EngineError;
4
5/// One deliberately modeled live-game behavior that diverges from spell data.
6#[derive(Debug)]
7pub struct GameBug {
8    pub slug: &'static str,
9    pub description: &'static str,
10}
11
12inventory::collect!(&'static GameBug);
13
14/// Declare a modeled live-game bug and register it with `inventory`.
15#[macro_export]
16macro_rules! define_game_bug {
17    (
18        $(#[$meta:meta])*
19        $vis:vis static $name:ident = ($slug:literal, $description:literal $(,)?);
20    ) => {
21        $(#[$meta])*
22        $vis static $name: $crate::GameBug = $crate::GameBug {
23            slug: $slug,
24            description: $description,
25        };
26        inventory::submit! { &$name }
27    };
28}
29
30/// All registered bugs, sorted by slug.
31#[must_use]
32pub fn registered_game_bugs() -> Vec<&'static GameBug> {
33    let mut bugs: Vec<&'static GameBug> = inventory::iter::<&'static GameBug>
34        .into_iter()
35        .copied()
36        .collect();
37
38    bugs.sort_by_key(|bug| bug.slug);
39
40    bugs
41}
42
43/// Raw bug-toggle input parsed from sim-config settings, unvalidated.
44#[derive(Clone, Debug)]
45pub struct BugSettings {
46    pub default_enabled: bool,
47    pub overrides: Vec<(String, bool)>,
48}
49
50impl BugSettings {
51    /// Resolve one registered bug directly from construction-time settings.
52    #[must_use]
53    pub fn enabled(&self, bug: &GameBug) -> bool {
54        self.overrides
55            .iter()
56            .find(|(slug, _)| slug == bug.slug)
57            .map_or(self.default_enabled, |(_, enabled)| *enabled)
58    }
59}
60
61impl Default for BugSettings {
62    fn default() -> Self {
63        Self {
64            default_enabled: true,
65            overrides: Vec::new(),
66        }
67    }
68}
69
70/// Resolved per-run bug toggles consulted by combat code.
71#[derive(Clone, Debug)]
72pub struct BugToggles {
73    default_enabled: bool,
74    overrides: Vec<(&'static str, bool)>,
75}
76
77impl BugToggles {
78    /// Creates toggles with live-game behavior enabled by default.
79    #[must_use]
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Whether the given bug is modeled in this run.
85    #[must_use]
86    pub fn enabled(&self, bug: &GameBug) -> bool {
87        self.overrides
88            .iter()
89            .find(|(slug, _)| *slug == bug.slug)
90            .map_or(self.default_enabled, |(_, enabled)| *enabled)
91    }
92}
93
94impl Default for BugToggles {
95    fn default() -> Self {
96        Self {
97            default_enabled: true,
98            overrides: Vec::new(),
99        }
100    }
101}
102
103/// Validate raw settings against the registry and freeze them for a run.
104/// # Errors
105/// Returns an error when a configured bug slug is not registered.
106pub fn resolve_bug_toggles(settings: &BugSettings) -> Result<BugToggles, EngineError> {
107    let registry = registered_game_bugs();
108
109    if let Some((unknown, _)) = settings
110        .overrides
111        .iter()
112        .find(|(slug, _)| !registry.iter().any(|bug| bug.slug == slug))
113    {
114        return Err(EngineError::intent_validation(format!(
115            "unknown game bug {unknown:?}; registered bugs: {}",
116            registry
117                .iter()
118                .map(|bug| bug.slug)
119                .collect::<Vec<_>>()
120                .join(", ")
121        )));
122    }
123
124    let overrides = settings
125        .overrides
126        .iter()
127        .filter_map(|(slug, enabled)| {
128            registry
129                .iter()
130                .find(|bug| bug.slug == slug)
131                .map(|bug| (bug.slug, *enabled))
132        })
133        .collect();
134
135    Ok(BugToggles {
136        default_enabled: settings.default_enabled,
137        overrides,
138    })
139}
140
141#[cfg(test)]
142mod tests {
143    use googletest::prelude::*;
144
145    use super::*;
146
147    crate::define_game_bug! {
148        static TEST_BUG = (
149            "engine_ports_test_bug",
150            "test-only modeled behavior",
151        );
152    }
153
154    #[gtest]
155    fn resolved_toggles_apply_default_and_named_override() -> Result<()> {
156        let toggles = resolve_bug_toggles(&BugSettings {
157            default_enabled: true,
158            overrides: vec![(TEST_BUG.slug.to_string(), false)],
159        })
160        .or_fail()?;
161
162        verify_true!(!toggles.enabled(&TEST_BUG))
163    }
164
165    #[gtest]
166    fn unknown_toggle_reports_registered_slugs() -> Result<()> {
167        let error = resolve_bug_toggles(&BugSettings {
168            default_enabled: true,
169            overrides: vec![("unknown_bug".to_string(), false)],
170        })
171        .err()
172        .or_fail()?;
173
174        verify_true!(error.is_intent_validation())?;
175        verify_that!(error.to_string(), contains_substring("unknown_bug"))?;
176
177        verify_that!(error.to_string(), contains_substring(TEST_BUG.slug))
178    }
179}