Skip to main content

wowlab_engine_domain/rotation/context/
draft.rs

1//! Mutable schema draft used while resolving rotation fields.
2
3use wowlab_buffer_contract::SLOT_ALIGNMENT;
4use wowlab_types::sim::{
5    AuraOn, AuraProjectionKey, Condition, FastMap, RotationAction as AstAction, SpellIdx,
6};
7
8use super::{
9    schema::{ContextField, ContextSchema},
10    slots::{align_up, ensure_slot},
11    spell_usable::{build_cooldown_bypass_offsets, build_spell_usable_offsets},
12};
13use crate::rotation::{
14    buffer::{
15        AuraSlot, BufferOffsets, BufferRequirements, ByteOffset, CooldownSlot, DescriptorId,
16        DescriptorTable, EquipmentSlot, EvalKind, FrozenBufferLayout, HeroTreeSlot, HistorySlot,
17        ItemSlot, ResourceSlot, SetBonusSlot, SlotDescriptor, SlotKind, SlotMaps, SpellSlot,
18        SwingSlot, TalentSlot, UnitSlot, WeaponImbueSlot,
19    },
20    condition::{
21        ResolvedFieldRead, ResolvedKey, domain, resolve_field_read, spell_usable_condition,
22    },
23    error::Result,
24    expr::{FieldType, FieldTypeLayout as _},
25    resolver::{SpecResolver, SpellAuraPolicyKind},
26};
27
28pub(crate) fn schema_draft(table: &DescriptorTable) -> SchemaDraft<'_> {
29    SchemaDraft::new(table)
30}
31
32pub(crate) struct SchemaDraft<'a> {
33    table: &'a DescriptorTable,
34    fields: Vec<ContextField>,
35    current_offset: usize,
36    user_var_offsets: FastMap<String, usize>,
37    user_var_types: FastMap<String, FieldType>,
38
39    slots: SlotMaps<usize>,
40    player_slot: usize,
41    combat_slot: usize,
42    pet_slot: usize,
43
44    standalone_keys: Vec<(DescriptorId, Option<ResolvedKey>, Option<AuraOn>, usize)>,
45
46    spell_usable_pending: Vec<(SpellIdx, usize)>,
47}
48
49impl std::fmt::Debug for SchemaDraft<'_> {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("SchemaDraft")
52            .field("current_offset", &self.current_offset)
53            .field("fields_count", &self.fields.len())
54            .finish()
55    }
56}
57
58impl<'a> SchemaDraft<'a> {
59    fn new(table: &'a DescriptorTable) -> Self {
60        let mut singletons: Vec<&SlotDescriptor> = inventory::iter::<SlotDescriptor>()
61            .filter(|s| s.kind == SlotKind::Singleton)
62            .collect();
63
64        singletons.sort_by_key(|s| s.name);
65
66        let mut current_offset: usize = 0;
67        let mut player_slot: usize = 0;
68        let mut combat_slot: usize = 0;
69        let mut pet_slot: usize = 0;
70
71        for slot in &singletons {
72            current_offset = align_up(current_offset, SLOT_ALIGNMENT);
73
74            match slot.name {
75                domain::PLAYER => player_slot = current_offset,
76                domain::COMBAT => combat_slot = current_offset,
77                domain::PET => pet_slot = current_offset,
78                _ => {}
79            }
80
81            current_offset += slot.size;
82        }
83
84        Self {
85            table,
86            fields: Vec::new(),
87            current_offset,
88            user_var_offsets: FastMap::default(),
89            user_var_types: FastMap::default(),
90            slots: SlotMaps::default(),
91            player_slot,
92            combat_slot,
93            pet_slot,
94            standalone_keys: Vec::new(),
95            spell_usable_pending: Vec::new(),
96        }
97    }
98
99    // #t(fn: rust_cyclomatic_complexity) dispatches across all slot kinds by design
100    pub(super) fn add_resolved(&mut self, resolved: &ResolvedFieldRead) -> Option<usize> {
101        if let Some(existing) = self.fields.iter().find(|f| {
102            f.descriptor_id == resolved.descriptor_id
103                && f.key == resolved.key
104                && f.on == resolved.on
105        }) {
106            return Some(existing.offset);
107        }
108
109        let descriptor = self.table.get(resolved.descriptor_id)?;
110
111        let offset = match descriptor.slot_kind {
112            SlotKind::Singleton => {
113                let base = match descriptor.domain {
114                    domain::PLAYER => self.player_slot,
115                    domain::COMBAT => self.combat_slot,
116                    domain::PET => self.pet_slot,
117                    _ => return None,
118                };
119
120                base + descriptor.field_offset
121            }
122            SlotKind::Keyed => {
123                let key = resolved.key.as_ref()?;
124                let size = descriptor.slot_size;
125                // Per-arm match: the slot maps key on disjoint types, so each borrows `off` with one map via direct field borrows.
126                let off = &mut self.current_offset;
127                let base = match (descriptor.domain, key) {
128                    (domain::COOLDOWN, ResolvedKey::Spell(idx)) => {
129                        ensure_slot(&mut self.slots.cooldowns, off, size, *idx)
130                    }
131                    (domain::SPELL, ResolvedKey::Spell(idx)) => {
132                        ensure_slot(&mut self.slots.spells, off, size, *idx)
133                    }
134                    (domain::HISTORY, ResolvedKey::Spell(idx)) => {
135                        ensure_slot(&mut self.slots.history, off, size, *idx)
136                    }
137                    (domain::AURA, ResolvedKey::Aura(idx)) => {
138                        let on = resolved.on.unwrap_or(AuraOn::Player);
139                        let key = AuraProjectionKey::new(*idx, on);
140
141                        ensure_slot(&mut self.slots.aura_projections, off, size, key)
142                    }
143                    (domain::RESOURCE, ResolvedKey::Resource(rt)) => {
144                        ensure_slot(&mut self.slots.resources, off, size, *rt)
145                    }
146                    (domain::TALENT, ResolvedKey::Named(name)) => {
147                        ensure_slot(&mut self.slots.talents, off, size, name.as_str())
148                    }
149                    (domain::HERO_TREE, ResolvedKey::Named(name)) => {
150                        ensure_slot(&mut self.slots.hero_trees, off, size, name.as_str())
151                    }
152                    (domain::ITEM, ResolvedKey::Named(name)) => {
153                        ensure_slot(&mut self.slots.items, off, size, name.as_str())
154                    }
155                    (domain::SWING, ResolvedKey::Named(name)) => {
156                        ensure_slot(&mut self.slots.swings, off, size, name.as_str())
157                    }
158                    (domain::EQUIPMENT, ResolvedKey::Named(name)) => {
159                        ensure_slot(&mut self.slots.equipment, off, size, name.as_str())
160                    }
161                    (domain::SET_BONUS, ResolvedKey::Named(name)) => {
162                        ensure_slot(&mut self.slots.set_bonuses, off, size, name.as_str())
163                    }
164                    (domain::UNIT, ResolvedKey::Named(name)) => {
165                        ensure_slot(&mut self.slots.units, off, size, name.as_str())
166                    }
167                    (domain::WEAPON_IMBUE, ResolvedKey::Named(name)) => {
168                        ensure_slot(&mut self.slots.weapon_imbues, off, size, name.as_str())
169                    }
170                    _ => return None,
171                };
172
173                base + descriptor.field_offset
174            }
175            _ => return None,
176        };
177
178        let field_type = descriptor.field_type;
179
180        self.fields.push(ContextField {
181            descriptor_id: resolved.descriptor_id,
182            offset,
183            field_type,
184            key: resolved.key.clone(),
185            on: resolved.on,
186        });
187
188        if descriptor.eval_kind == EvalKind::SpellUsable
189            && let Some(ResolvedKey::Spell(idx)) = &resolved.key
190        {
191            self.spell_usable_pending.push((*idx, offset));
192        }
193
194        Some(offset)
195    }
196
197    pub(crate) fn add_user_var(&mut self, name: &str, var_type: FieldType) -> usize {
198        if let Some(&offset) = self.user_var_offsets.get(name) {
199            return offset;
200        }
201
202        self.current_offset = align_up(self.current_offset, var_type.alignment());
203        let offset = self.current_offset;
204
205        self.current_offset += var_type.size();
206        self.user_var_offsets.insert(name.to_string(), offset);
207        self.user_var_types.insert(name.to_string(), var_type);
208
209        offset
210    }
211
212    pub(crate) fn register_condition_fields(
213        &mut self,
214        condition: &Condition,
215        resolver: &SpecResolver,
216    ) -> Result<()> {
217        for field in condition.field_reads() {
218            let resolved_read = resolve_field_read(field, self.table, resolver)?;
219
220            self.add_resolved(&resolved_read).ok_or_else(|| {
221                crate::rotation::Error::validation(
222                    "resolved field has no prepared buffer coordinate",
223                )
224            })?;
225        }
226
227        Ok(())
228    }
229
230    pub(crate) fn register_action_fields(
231        &mut self,
232        action: &AstAction,
233        resolver: &SpecResolver,
234    ) -> Result<()> {
235        if !action.enabled() {
236            return Ok(());
237        }
238
239        if let AstAction::Cast { spell, .. } = action {
240            resolver.resolve_spell(spell)?;
241            self.register_condition_fields(&spell_usable_condition(spell), resolver)?;
242        }
243
244        for cond in action.conditions() {
245            self.register_condition_fields(cond, resolver)?;
246        }
247
248        Ok(())
249    }
250
251    pub(crate) fn build_with_resolver(
252        mut self,
253        resolver: Option<&SpecResolver>,
254        requirements: &BufferRequirements,
255    ) -> ContextSchema {
256        if let Some(r) = resolver {
257            self.force_allocate_spec_slots(r);
258        }
259
260        self.allocate_runtime_requirements(requirements);
261
262        let size = self.finalize_size();
263
264        let user_var_offsets = std::mem::take(&mut self.user_var_offsets);
265        let user_var_types = std::mem::take(&mut self.user_var_types);
266        let spell_usable_pending = std::mem::take(&mut self.spell_usable_pending);
267        let fields = std::mem::take(&mut self.fields);
268
269        let buffer_offsets = self.into_buffer_offsets();
270        let spell_usable_offsets =
271            build_spell_usable_offsets(&buffer_offsets, resolver, &spell_usable_pending);
272        let cooldown_bypass_offsets = build_cooldown_bypass_offsets(&buffer_offsets, resolver);
273
274        let layout = FrozenBufferLayout::freeze(buffer_offsets, size);
275
276        ContextSchema {
277            fields,
278            layout,
279            user_var_offsets,
280            user_var_types,
281            spell_usable_offsets,
282            cooldown_bypass_offsets,
283        }
284    }
285
286    fn allocate_runtime_requirements(&mut self, requirements: &BufferRequirements) {
287        macro_rules! allocate {
288            ($field:ident, $size:expr) => {
289                for key in &requirements.$field {
290                    ensure_slot(
291                        &mut self.slots.$field,
292                        &mut self.current_offset,
293                        $size,
294                        *key,
295                    );
296                }
297            };
298        }
299
300        allocate!(cooldowns, CooldownSlot::SIZE);
301        allocate!(auras, AuraSlot::SIZE);
302        allocate!(resources, ResourceSlot::SIZE);
303        allocate!(spells, SpellSlot::SIZE);
304        allocate!(history, HistorySlot::SIZE);
305
306        macro_rules! allocate_named {
307            ($field:ident, $size:expr) => {
308                for key in &requirements.$field {
309                    ensure_slot(
310                        &mut self.slots.$field,
311                        &mut self.current_offset,
312                        $size,
313                        key.as_ref(),
314                    );
315                }
316            };
317        }
318
319        allocate_named!(talents, TalentSlot::SIZE);
320        allocate_named!(hero_trees, HeroTreeSlot::SIZE);
321        allocate_named!(items, ItemSlot::SIZE);
322        allocate_named!(swings, SwingSlot::SIZE);
323        allocate_named!(equipment, EquipmentSlot::SIZE);
324        allocate_named!(set_bonuses, SetBonusSlot::SIZE);
325
326        for key in &requirements.units {
327            self.unit_mut_or_insert(key);
328        }
329
330        allocate_named!(weapon_imbues, WeaponImbueSlot::SIZE);
331    }
332
333    /// Construction-only unit insertion. No equivalent exists on the frozen runtime buffer.
334    fn unit_mut_or_insert(&mut self, key: &str) -> usize {
335        ensure_slot(
336            &mut self.slots.units,
337            &mut self.current_offset,
338            UnitSlot::SIZE,
339            key,
340        )
341    }
342
343    // JIT loads `f64` from the buffer, so the total size must be `f64`-aligned and non-zero.
344    fn finalize_size(&self) -> usize {
345        align_up(self.current_offset.max(SLOT_ALIGNMENT), SLOT_ALIGNMENT)
346    }
347
348    // Force-allocate slots even when no condition reads them, so `SpellUsable` can find their offsets.
349    fn force_allocate_spec_slots(&mut self, r: &SpecResolver) {
350        if let Some(rt) = r.primary_resource_type() {
351            ensure_slot(
352                &mut self.slots.resources,
353                &mut self.current_offset,
354                ResourceSlot::SIZE,
355                rt,
356            );
357        }
358
359        if let Some(rt) = r.secondary_resource_type() {
360            ensure_slot(
361                &mut self.slots.resources,
362                &mut self.current_offset,
363                ResourceSlot::SIZE,
364                rt,
365            );
366        }
367
368        for policy in r.spell_aura_policies() {
369            ensure_slot(
370                &mut self.slots.aura_projections,
371                &mut self.current_offset,
372                AuraSlot::SIZE,
373                AuraProjectionKey::new(policy.aura, policy.on),
374            );
375
376            if let SpellAuraPolicyKind::Override { replacement, .. } = policy.kind {
377                ensure_slot(
378                    &mut self.slots.spells,
379                    &mut self.current_offset,
380                    SpellSlot::SIZE,
381                    replacement,
382                );
383            }
384        }
385    }
386
387    fn into_buffer_offsets(self) -> BufferOffsets {
388        let standalone: FastMap<DescriptorId, ByteOffset> = self
389            .standalone_keys
390            .iter()
391            .map(|(desc_id, _, _, offset)| (*desc_id, ByteOffset::new(*offset)))
392            .collect();
393
394        BufferOffsets {
395            slots: self.slots.into_byte_offsets(),
396            player: ByteOffset::new(self.player_slot),
397            combat: ByteOffset::new(self.combat_slot),
398            pet: ByteOffset::new(self.pet_slot),
399            standalone,
400        }
401    }
402}