1use 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#[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#[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 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 self.spells[local.as_usize()]
266 }
267
268 pub(crate) fn cooldown(&self, local: LocalSpellIdx) -> CooldownSlotHandle {
269 self.cooldowns[local.as_usize()]
271 }
272
273 pub(crate) fn history(&self, local: LocalSpellIdx) -> HistorySlotHandle {
274 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 .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 .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 .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 .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}