wowlab_engine_adapter_data/bridge/
overlay.rs1use 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
29pub 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}
37impl<R> OverlayResolver<R> {
40 #[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 #[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 #[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 #[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;