Skip to main content

wowlab_engine_adapter_data/bridge/
overlay.rs

1use std::sync::Arc;
2
3use wowlab_engine_ports::{DataResolver, PermanentEnchantQuery, ResolverError, SpellId};
4use wowlab_types::{
5    data::{
6        ChallengeModeHealthFlat, ContentTuningFlat, ContentTuningXDifficultyFlat,
7        ContentTuningXExpectedFlat, CreatureDifficultyFlat, CreatureFlat, ExpansionTraitTreeFlat,
8        ExpectedStatFlat, ExpectedStatModFlat, ItemDataFlat, ItemScalingData, PermanentEnchantFlat,
9        PowerTypeFlat, SpecDataFlat, SpellDataFlat, SpellEffect, TraitTreeFlat,
10    },
11    sim::{FastMap, IntMap},
12};
13
14use crate::lookup_key::SpellEffectKey;
15
16macro_rules! overlay_forward {
17    ($(fn $method:ident($($argument:ident: $argument_ty:ty),* $(,)?) -> $output:ty;)+) => {
18        $(
19            async fn $method(
20                &self,
21                $($argument: $argument_ty),*
22            ) -> Result<$output, ResolverError> {
23                self.base.$method($($argument),*).await
24            }
25        )+
26    };
27}
28
29/// Applies explicit spell, effect, and rotation overrides to a base resolver.
30// docref:start data-resolution-overlay-struct
31pub struct OverlayResolver<R> {
32    base: R,
33    spell_overrides: IntMap<i32, SpellDataFlat>,
34    effect_overrides: FastMap<SpellEffectKey, SpellEffect>,
35    rotation_overrides: FastMap<String, String>,
36}
37// docref:end data-resolution-overlay-struct
38
39impl<R> OverlayResolver<R> {
40    /// Wraps a base resolver without adding overrides.
41    #[must_use]
42    pub fn new(base: R) -> Self {
43        Self {
44            base,
45            spell_overrides: IntMap::default(),
46            effect_overrides: FastMap::default(),
47            rotation_overrides: FastMap::default(),
48        }
49    }
50
51    /// Adds or replaces a complete spell override.
52    #[must_use]
53    pub fn with_spell(mut self, spell: SpellDataFlat) -> Self {
54        self.spell_overrides.insert(spell.id, spell);
55
56        self
57    }
58
59    /// Adds an effect override keyed by its one-based resolver index.
60    #[must_use]
61    pub fn with_effect(mut self, spell_id: i32, effect: SpellEffect) -> Self {
62        let effect_index = u8::try_from(effect.index)
63            .unwrap_or(u8::MAX)
64            .saturating_add(1);
65
66        debug_assert!(
67            effect_index >= 1,
68            "effect_overrides keys are 1-based; 0 is reserved for 'no effect'"
69        );
70        self.effect_overrides
71            .insert(SpellEffectKey::new(spell_id, effect_index), effect);
72
73        self
74    }
75
76    /// Adds or replaces a rotation-script override.
77    #[must_use]
78    pub fn with_rotation_script(
79        mut self,
80        id: impl Into<String>,
81        script: impl Into<String>,
82    ) -> Self {
83        self.rotation_overrides.insert(id.into(), script.into());
84
85        self
86    }
87
88    fn override_effect(&self, spell_id: i32, effect_index: u8) -> Option<SpellEffect> {
89        if let Some(effect) = self
90            .effect_overrides
91            .get(&SpellEffectKey::new(spell_id, effect_index))
92        {
93            return Some(effect.clone());
94        }
95
96        let spell = self.spell_overrides.get(&spell_id)?;
97
98        spell.effects.iter().find_map(|effect| {
99            let one_based = u8::try_from(effect.index)
100                .unwrap_or(u8::MAX)
101                .saturating_add(1);
102
103            (one_based == effect_index).then(|| effect.clone())
104        })
105    }
106
107    fn apply_effect_overrides(&self, spell_id: i32, effects: &mut [SpellEffect]) {
108        for effect in effects {
109            let one_based = u8::try_from(effect.index)
110                .unwrap_or(u8::MAX)
111                .saturating_add(1);
112
113            if let Some(overridden) = self.override_effect(spell_id, one_based) {
114                *effect = overridden;
115            }
116        }
117    }
118}
119
120impl<R> std::fmt::Debug for OverlayResolver<R>
121where
122    R: std::fmt::Debug,
123{
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("OverlayResolver")
126            .field("base", &self.base)
127            .field("spell_overrides", &self.spell_overrides.len())
128            .field("effect_overrides", &self.effect_overrides.len())
129            .field("rotation_overrides", &self.rotation_overrides.len())
130            .finish()
131    }
132}
133
134impl<R> DataResolver for OverlayResolver<R>
135where
136    R: DataResolver,
137{
138    async fn get_spell(&self, spell_id: SpellId) -> Result<SpellDataFlat, ResolverError> {
139        if let Some(overridden) = self.spell_overrides.get(&spell_id.as_i32()) {
140            return Ok(overridden.clone());
141        }
142
143        self.base.get_spell(spell_id).await
144    }
145
146    async fn get_spells(&self, spell_ids: &[SpellId]) -> Result<Vec<SpellDataFlat>, ResolverError> {
147        let base_ids = spell_ids
148            .iter()
149            .copied()
150            .filter(|spell_id| !self.spell_overrides.contains_key(&spell_id.as_i32()))
151            .collect::<Vec<_>>();
152        let mut base_spells = self.base.get_spells(&base_ids).await?.into_iter();
153
154        spell_ids
155            .iter()
156            .map(|spell_id| {
157                self.spell_overrides
158                    .get(&spell_id.as_i32())
159                    .cloned()
160                    .or_else(|| base_spells.next())
161                    .ok_or_else(|| ResolverError::spell_not_found(*spell_id))
162            })
163            .collect()
164    }
165
166    async fn get_spell_effect(
167        &self,
168        spell_id: SpellId,
169        effect_index: u8,
170    ) -> Result<SpellEffect, ResolverError> {
171        if effect_index == 0 {
172            return Err(ResolverError::spell_effect_not_found(
173                spell_id,
174                effect_index,
175            ));
176        }
177
178        if let Some(overridden) = self.override_effect(spell_id.as_i32(), effect_index) {
179            return Ok(overridden);
180        }
181
182        self.base.get_spell_effect(spell_id, effect_index).await
183    }
184
185    async fn get_spell_effects(
186        &self,
187        spell_id: SpellId,
188    ) -> Result<Vec<SpellEffect>, ResolverError> {
189        let mut effects = match self.spell_overrides.get(&spell_id.as_i32()) {
190            Some(overridden) => overridden.effects.clone(),
191            None => self.base.get_spell_effects(spell_id).await?,
192        };
193
194        self.apply_effect_overrides(spell_id.as_i32(), &mut effects);
195
196        Ok(effects)
197    }
198
199    overlay_forward! {
200        fn get_item(item_id: i32) -> ItemDataFlat;
201        fn find_consumable_items(name_token: &str, subclass: i32) -> Vec<ItemDataFlat>;
202        fn get_scaling_data() -> Arc<ItemScalingData>;
203        fn get_power_types() -> Vec<PowerTypeFlat>;
204        fn get_spec(spec_id: i32) -> SpecDataFlat;
205        fn get_trait_tree(spec_id: i32) -> TraitTreeFlat;
206        fn get_expansion_trait_tree(
207            expansion_id: i32,
208            system: &str,
209        ) -> ExpansionTraitTreeFlat;
210        fn get_spell_overrides(spec_id: i32) -> Vec<(SpellId, SpellId)>;
211        fn get_specialization_spells(spec_id: i32) -> Vec<SpellId>;
212        fn get_racial_spells(race_id: i32, class_id: i32) -> Vec<SpellId>;
213        fn get_expected_stats(expansion_id: i32, lvl: i32) -> ExpectedStatFlat;
214    }
215
216    async fn get_rotation_script(&self, rotation_id: &str) -> Result<String, ResolverError> {
217        if let Some(script) = self.rotation_overrides.get(rotation_id) {
218            return Ok(script.clone());
219        }
220
221        self.base.get_rotation_script(rotation_id).await
222    }
223
224    overlay_forward! {
225        fn get_item_damage_scaling(
226            item_level: i32,
227            weapon_type: &str,
228        ) -> wowlab_types::data::ItemDamageScalingFlat;
229        fn get_enchantment(
230            enchantment_id: i32,
231        ) -> wowlab_engine_ports::EnchantmentRow;
232        fn find_permanent_enchant(
233            query: &PermanentEnchantQuery,
234        ) -> Option<PermanentEnchantFlat>;
235        fn search_spells(
236            query: &str,
237            limit: u32,
238        ) -> Vec<wowlab_engine_ports::SpellSearchResult>;
239        fn get_creature(creature_id: i32) -> CreatureFlat;
240        fn get_creature_difficulties(creature_id: i32) -> Vec<CreatureDifficultyFlat>;
241        fn get_content_tuning(content_tuning_id: i32) -> ContentTuningFlat;
242        fn get_content_tuning_x_difficulty(
243            content_tuning_id: i32,
244        ) -> Vec<ContentTuningXDifficultyFlat>;
245        fn get_content_tuning_x_expected(
246            content_tuning_id: i32,
247        ) -> Vec<ContentTuningXExpectedFlat>;
248        fn get_expected_stat_mod(expected_stat_mod_id: i32) -> ExpectedStatModFlat;
249        fn get_challenge_mode_health(keystone_level: i32) -> ChallengeModeHealthFlat;
250    }
251}
252
253#[cfg(test)]
254mod tests;