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