Skip to main content

wowlab_engine_combat/state/program/
buffer_bindings.rs

1//! Finalized combat identities bound to construction-validated buffer handles.
2
3use wowlab_engine_domain::rotation::{
4    AuraSlotHandle, BufferRequirements, ContextSchema, CooldownSlotHandle, HistorySlotHandle,
5    ItemSlotHandle, ResourceSlotHandle, SpellSlotHandle, SwingSlotHandle, TalentSlotHandle,
6    UnitSlotHandle, WeaponImbueSlotHandle,
7};
8use wowlab_types::{
9    combat::ResourceType,
10    sim::{ActorId, AuraKey, AuraOn, PetIdx, SpellIdx},
11};
12
13use crate::{
14    builder::draft::DraftDefinitions,
15    keys::UnitKey,
16    state::{LocalAuraIdx, LocalSpellIdx},
17};
18
19#[derive(Debug, thiserror::Error)]
20pub(crate) enum BufferBindingError {
21    #[error("missing frozen {kind} binding for spell {spell_id}")]
22    MissingSpell { kind: &'static str, spell_id: u32 },
23    #[error("missing frozen swing binding at local index {index}")]
24    MissingSwing { index: usize },
25    #[error("missing frozen target-unit binding")]
26    MissingTargetUnit,
27    #[error("missing frozen resource binding for {resource}")]
28    MissingResource { resource: ResourceType },
29    #[error("missing frozen talent binding for {name}")]
30    MissingTalent { name: Box<str> },
31    #[error("missing frozen item binding for {key}")]
32    MissingItem { key: Box<str> },
33    #[error("missing frozen weapon-imbue binding for {key}")]
34    MissingWeaponImbue { key: Box<str> },
35    #[error("frozen aura {aura_id} ({on:?}) has no finalized local identity")]
36    MissingAuraIdentity { aura_id: u32, on: AuraOn },
37    #[error("finalized aura count {count} exceeds the local identity range")]
38    AuraIdentityOverflow { count: usize },
39    #[error("unsupported finalized concrete aura binding {key:?}")]
40    UnsupportedAuraBinding { key: AuraKey },
41    #[error("duplicate finalized concrete aura binding coordinate for {key:?}")]
42    DuplicateAuraBinding { key: AuraKey },
43}
44
45/// One fully finalized concrete aura identity.
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub(crate) struct ConcreteAuraBinding {
48    pub(crate) local: LocalAuraIdx,
49    pub(crate) key: AuraKey,
50    pub(crate) handle: AuraSlotHandle,
51    index: ConcreteAuraBindingIdx,
52}
53
54/// Stable finalized identity for one concrete aura binding.
55#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
56pub(crate) struct ConcreteAuraBindingIdx(usize);
57
58impl ConcreteAuraBindingIdx {
59    #[expect(
60        clippy::trivially_copy_pass_by_ref,
61        reason = "C-CONV requires borrowed as_ receivers"
62    )]
63    pub(crate) const fn as_usize(&self) -> usize {
64        self.0
65    }
66}
67
68#[derive(Debug)]
69pub(crate) struct BufferBindings {
70    spells: Box<[SpellSlotHandle]>,
71    cooldowns: Box<[CooldownSlotHandle]>,
72    history: Box<[HistorySlotHandle]>,
73    auras: Box<[ConcreteAuraBinding]>,
74    aura_coordinates: Box<[Option<ConcreteAuraBinding>]>,
75    aura_shape_len: usize,
76    enemy_count: usize,
77    aura_ons: Box<[AuraOn]>,
78    resources: Box<[(ResourceType, ResourceSlotHandle)]>,
79    talents: Box<[(Box<str>, TalentSlotHandle)]>,
80    items: Box<[(Box<str>, ItemSlotHandle)]>,
81    swings: Box<[SwingSlotHandle]>,
82    weapon_imbues: Box<[(Box<str>, WeaponImbueSlotHandle)]>,
83    target_unit: UnitSlotHandle,
84}
85
86impl BufferBindings {
87    // #t(fn: rust_max_fn_lines) binding every frozen slot family is one construction-validated finalization transaction.
88    // #t(fn: rust_cyclomatic_complexity) each branch binds one required frozen slot family and returns its precise construction error.
89    pub(crate) fn build(
90        schema: &ContextSchema,
91        requirements: &BufferRequirements,
92        definitions: &DraftDefinitions,
93        talent_ranks: &[(String, u8)],
94        initial_weapon_imbues: &[(&'static str, bool)],
95    ) -> Result<Self, BufferBindingError> {
96        let finalizer = schema.buffer_finalizer();
97        let mut spells = Vec::with_capacity(definitions.spells.len());
98        let mut cooldowns = Vec::with_capacity(definitions.spells.len());
99        let mut history = Vec::with_capacity(definitions.spells.len());
100
101        for spell in &definitions.spells {
102            let key = SpellIdx::from_raw(spell.spell_id);
103
104            let missing = |kind| BufferBindingError::MissingSpell {
105                kind,
106                spell_id: spell.spell_id,
107            };
108
109            spells.push(finalizer.bind_spell(key).ok_or_else(|| missing("spell"))?);
110            cooldowns.push(
111                finalizer
112                    .bind_cooldown(key)
113                    .ok_or_else(|| missing("cooldown"))?,
114            );
115            history.push(
116                finalizer
117                    .bind_history(key)
118                    .ok_or_else(|| missing("history"))?,
119            );
120        }
121
122        let aura_shape_len = aura_shape_len(definitions.enemies.len());
123        let aura_ons: Box<[_]> = definitions.auras.iter().map(|aura| aura.on).collect();
124        let mut auras = Vec::with_capacity(requirements.aura_keys().len());
125        let mut aura_coordinates =
126            vec![None; definitions.auras.len() * aura_shape_len * AURA_FAMILY_COUNT];
127
128        for &key in requirements.aura_keys() {
129            let handle = finalizer
130                .bind_aura(key)
131                .ok_or(BufferBindingError::UnsupportedAuraBinding { key })?;
132            let raw = definitions
133                .auras
134                .iter()
135                .position(|aura| aura.aura_id == key.aura().as_u32())
136                .ok_or(BufferBindingError::MissingAuraIdentity {
137                    aura_id: key.aura().as_u32(),
138                    on: key.on(),
139                })?;
140            let Ok(raw) = u8::try_from(raw) else {
141                return Err(BufferBindingError::AuraIdentityOverflow {
142                    count: definitions.auras.len(),
143                });
144            };
145            let local = LocalAuraIdx::new(raw);
146            let definition_on = aura_ons.get(local.as_usize()).copied().ok_or(
147                BufferBindingError::MissingAuraIdentity {
148                    aura_id: key.aura().as_u32(),
149                    on: key.on(),
150                },
151            )?;
152            let binding = ConcreteAuraBinding {
153                local,
154                key,
155                handle,
156                index: ConcreteAuraBindingIdx(auras.len()),
157            };
158            let coordinate = aura_coordinate(
159                local,
160                key,
161                definition_on,
162                definitions.enemies.len(),
163                aura_shape_len,
164            )
165            .ok_or(BufferBindingError::UnsupportedAuraBinding { key })?;
166            let entry = aura_coordinates
167                .get_mut(coordinate)
168                .ok_or(BufferBindingError::UnsupportedAuraBinding { key })?;
169
170            if entry.is_some() {
171                return Err(BufferBindingError::DuplicateAuraBinding { key });
172            }
173
174            *entry = Some(binding);
175            auras.push(binding);
176        }
177
178        let mut resources: Vec<_> = requirements
179            .resource_keys()
180            .iter()
181            .map(|&resource| {
182                finalizer
183                    .bind_resource(resource)
184                    .map(|handle| (resource, handle))
185                    .ok_or(BufferBindingError::MissingResource { resource })
186            })
187            .collect::<Result<_, _>>()?;
188
189        resources.sort_unstable_by_key(|(resource, _)| u8::from(*resource));
190        let mut talents: Vec<_> = talent_ranks
191            .iter()
192            .map(|(name, _)| {
193                finalizer
194                    .bind_talent(name)
195                    .map(|handle| (Box::<str>::from(name.as_str()), handle))
196                    .ok_or_else(|| BufferBindingError::MissingTalent {
197                        name: Box::from(name.as_str()),
198                    })
199            })
200            .collect::<Result<_, _>>()?;
201
202        talents.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
203        let mut items: Vec<_> = definitions
204            .item_use_spells
205            .iter()
206            .map(|(slot, _)| {
207                let key = slot.slug();
208
209                finalizer
210                    .bind_item(key)
211                    .map(|handle| (Box::<str>::from(key), handle))
212                    .ok_or_else(|| BufferBindingError::MissingItem {
213                        key: Box::from(key),
214                    })
215            })
216            .collect::<Result<_, _>>()?;
217
218        items.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
219        let swings = (0..definitions.auto_attacks.len())
220            .map(|index| {
221                let key = crate::keys::auto_attack_key(index);
222
223                finalizer
224                    .bind_swing(&key)
225                    .ok_or(BufferBindingError::MissingSwing { index })
226            })
227            .collect::<Result<Vec<_>, _>>()?;
228        let mut weapon_imbues: Vec<_> = initial_weapon_imbues
229            .iter()
230            .map(|(key, _)| {
231                finalizer
232                    .bind_weapon_imbue(key)
233                    .map(|handle| (Box::<str>::from(*key), handle))
234                    .ok_or_else(|| BufferBindingError::MissingWeaponImbue {
235                        key: Box::from(*key),
236                    })
237            })
238            .collect::<Result<_, _>>()?;
239
240        weapon_imbues.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
241        let target_unit = finalizer
242            .bind_unit(UnitKey::TARGET)
243            .ok_or(BufferBindingError::MissingTargetUnit)?;
244
245        Ok(Self {
246            spells: spells.into_boxed_slice(),
247            cooldowns: cooldowns.into_boxed_slice(),
248            history: history.into_boxed_slice(),
249            auras: auras.into_boxed_slice(),
250            aura_coordinates: aura_coordinates.into_boxed_slice(),
251            aura_shape_len,
252            enemy_count: definitions.enemies.len(),
253            aura_ons,
254            resources: resources.into_boxed_slice(),
255            talents: talents.into_boxed_slice(),
256            items: items.into_boxed_slice(),
257            swings: swings.into_boxed_slice(),
258            weapon_imbues: weapon_imbues.into_boxed_slice(),
259            target_unit,
260        })
261    }
262
263    pub(crate) fn spell(&self, local: LocalSpellIdx) -> SpellSlotHandle {
264        // BOUNDS: local spell indices are constructed from the definitions used to build this array.
265        self.spells[local.as_usize()]
266    }
267
268    pub(crate) fn cooldown(&self, local: LocalSpellIdx) -> CooldownSlotHandle {
269        // BOUNDS: local spell indices are constructed from the definitions used to build this array.
270        self.cooldowns[local.as_usize()]
271    }
272
273    pub(crate) fn history(&self, local: LocalSpellIdx) -> HistorySlotHandle {
274        // BOUNDS: local spell indices are constructed from the definitions used to build this array.
275        self.history[local.as_usize()]
276    }
277
278    pub(crate) fn aura(&self, local: LocalAuraIdx, key: AuraKey) -> Option<ConcreteAuraBinding> {
279        let on = *self.aura_ons.get(local.as_usize())?;
280        let coordinate = aura_coordinate(local, key, on, self.enemy_count, self.aura_shape_len)?;
281        let binding = self.aura_coordinates.get(coordinate).copied().flatten();
282
283        #[cfg(not(feature = "rotation-optimizations"))]
284        return binding.filter(|binding| binding.local == local && binding.key == key);
285
286        #[cfg(feature = "rotation-optimizations")]
287        binding
288    }
289
290    pub(crate) fn aura_bindings(&self) -> impl Iterator<Item = ConcreteAuraBinding> + '_ {
291        self.auras.iter().copied()
292    }
293
294    pub(crate) const fn aura_binding_count(&self) -> usize {
295        self.auras.len()
296    }
297
298    #[cfg(feature = "rotation-optimizations")]
299    pub(crate) const fn aura_local_count(&self) -> usize {
300        self.aura_ons.len()
301    }
302
303    pub(crate) fn resource(&self, key: ResourceType) -> Option<ResourceSlotHandle> {
304        self.resources
305            .binary_search_by_key(&u8::from(key), |(candidate, _)| u8::from(*candidate))
306            .ok()
307            // BOUNDS: successful binary search returns an index into this exact slice.
308            .map(|index| self.resources[index].1)
309    }
310
311    pub(crate) fn talent(&self, key: &str) -> Option<TalentSlotHandle> {
312        self.talents
313            .binary_search_by(|(candidate, _)| candidate.as_ref().cmp(key))
314            .ok()
315            // BOUNDS: successful binary search returns an index into this exact slice.
316            .map(|index| self.talents[index].1)
317    }
318
319    pub(crate) fn item(&self, key: &str) -> Option<ItemSlotHandle> {
320        self.items
321            .binary_search_by(|(candidate, _)| candidate.as_ref().cmp(key))
322            .ok()
323            // BOUNDS: successful binary search returns an index into this exact slice.
324            .map(|index| self.items[index].1)
325    }
326
327    pub(crate) fn swing(&self, index: usize) -> Option<SwingSlotHandle> {
328        self.swings.get(index).copied()
329    }
330
331    pub(crate) fn weapon_imbue(&self, key: &str) -> Option<WeaponImbueSlotHandle> {
332        self.weapon_imbues
333            .binary_search_by(|(candidate, _)| candidate.as_ref().cmp(key))
334            .ok()
335            // BOUNDS: successful binary search returns an index into this exact slice.
336            .map(|index| self.weapon_imbues[index].1)
337    }
338
339    pub(crate) const fn target_unit(&self) -> UnitSlotHandle {
340        self.target_unit
341    }
342}
343
344#[cfg(test)]
345#[path = "buffer_bindings/tests.rs"]
346mod tests;
347
348impl ConcreteAuraBinding {
349    pub(crate) const fn index(self) -> ConcreteAuraBindingIdx {
350        self.index
351    }
352}
353
354const NON_PET_ACTOR_COUNT: usize = 2;
355const PET_ACTOR_START: usize = NON_PET_ACTOR_COUNT;
356const AURA_FAMILY_COUNT: usize = 3;
357const PET_SOURCE_AFFECTED_COUNT: usize = 3;
358const PLAYER_COORDINATE: usize = 0;
359const EXTERNAL_COORDINATE: usize = 1;
360const PRIMARY_PET_COORDINATE: usize = 1;
361const SELF_PET_COORDINATE: usize = 2;
362const PET_ENEMY_COORDINATE_START: usize = PET_SOURCE_AFFECTED_COUNT;
363const DEFINITION_AURA_FAMILY: usize = 0;
364const PLAYER_AURA_FAMILY: usize = 1;
365const PET_AURA_FAMILY: usize = 2;
366
367const fn aura_shape_len(enemy_count: usize) -> usize {
368    let actor_count = NON_PET_ACTOR_COUNT + PetIdx::MAX + enemy_count;
369
370    (NON_PET_ACTOR_COUNT + enemy_count) * actor_count
371        + PetIdx::MAX * (PET_SOURCE_AFFECTED_COUNT + enemy_count)
372}
373
374fn actor_coordinate(actor: ActorId, enemy_count: usize) -> Option<usize> {
375    match actor {
376        ActorId::Player => Some(PLAYER_COORDINATE),
377        ActorId::External => Some(EXTERNAL_COORDINATE),
378        ActorId::Pet(pet) => {
379            (pet.as_usize() < PetIdx::MAX).then_some(PET_ACTOR_START + pet.as_usize())
380        }
381        ActorId::Enemy(enemy) => (enemy.as_usize() < enemy_count)
382            .then_some(PET_ACTOR_START + PetIdx::MAX + enemy.as_usize()),
383    }
384}
385
386fn aura_shape_coordinate(key: AuraKey, enemy_count: usize) -> Option<usize> {
387    let actor_count = NON_PET_ACTOR_COUNT + PetIdx::MAX + enemy_count;
388
389    match key.source() {
390        ActorId::Player => actor_coordinate(key.affected(), enemy_count),
391        ActorId::External => {
392            actor_coordinate(key.affected(), enemy_count).map(|index| actor_count + index)
393        }
394        ActorId::Enemy(enemy) if enemy.as_usize() < enemy_count => {
395            actor_coordinate(key.affected(), enemy_count)
396                .map(|index| (NON_PET_ACTOR_COUNT + enemy.as_usize()) * actor_count + index)
397        }
398        ActorId::Pet(pet) if pet.as_usize() < PetIdx::MAX => {
399            let affected = match key.affected() {
400                ActorId::Player => PLAYER_COORDINATE,
401                ActorId::Pet(affected) if affected == PetIdx::PRIMARY => PRIMARY_PET_COORDINATE,
402                ActorId::Pet(affected) if affected == pet => SELF_PET_COORDINATE,
403                ActorId::Enemy(enemy) if enemy.as_usize() < enemy_count => {
404                    PET_ENEMY_COORDINATE_START + enemy.as_usize()
405                }
406                ActorId::External | ActorId::Pet(_) | ActorId::Enemy(_) => return None,
407            };
408            let non_pet_len = (NON_PET_ACTOR_COUNT + enemy_count) * actor_count;
409
410            Some(
411                non_pet_len + pet.as_usize() * (PET_SOURCE_AFFECTED_COUNT + enemy_count) + affected,
412            )
413        }
414        ActorId::Enemy(_) | ActorId::Pet(_) => None,
415    }
416}
417
418fn aura_coordinate(
419    local: LocalAuraIdx,
420    key: AuraKey,
421    definition_on: AuraOn,
422    enemy_count: usize,
423    shape_len: usize,
424) -> Option<usize> {
425    let family = if key.on() == definition_on {
426        DEFINITION_AURA_FAMILY
427    } else if key.on() == AuraOn::Player {
428        PLAYER_AURA_FAMILY
429    } else if key.on() == AuraOn::Pet && definition_on != AuraOn::Pet {
430        PET_AURA_FAMILY
431    } else {
432        return None;
433    };
434    let shape = aura_shape_coordinate(key, enemy_count)?;
435
436    Some((local.as_usize() * AURA_FAMILY_COUNT + family) * shape_len + shape)
437}