1use std::sync::Arc;
4
5use wowlab_types::sim::{AuraOn, FastMap};
6
7use crate::rotation::{
8 buffer::{
9 AuraProjectionKey, AuraProjectionSlotHandle, AuraSlot, AuraSlotHandle, BoundDenseBuffer,
10 CooldownSlot, CooldownSlotHandle, DenseBuffer, DescriptorId, DescriptorTable, EvalKind,
11 FrozenBufferLayout, HistorySlotHandle, ItemSlotHandle, LayoutFingerprint, ResourceSlot,
12 ResourceSlotHandle, SpellSlot, SpellSlotHandle, SwingSlotHandle, TalentSlotHandle,
13 UnitSlotHandle, WeaponImbueSlotHandle,
14 },
15 condition::{ResolvedFieldRead, ResolvedKey},
16 error::{Error, Result},
17 expr::{FieldType, FieldTypeLayout as _},
18};
19
20#[derive(Clone, Copy, Debug)]
22pub(crate) struct SpellUsableOffsets {
23 pub(crate) resource_current: Option<usize>,
24 pub(crate) secondary_resource_current: Option<usize>,
25 pub(crate) cooldown_base: Option<usize>,
26 pub(crate) gating_aura_offset: Option<usize>,
27 pub(crate) gating_aura_min_stacks: u8,
28 pub(crate) cost_bypass_aura_offset: Option<usize>,
29 pub(crate) cooldown_bypass_aura_offset: Option<usize>,
30 pub(crate) override_spell_offset: Option<usize>,
31 pub(crate) override_cooldown_base: Option<usize>,
32 pub(crate) override_aura_offset: Option<usize>,
33}
34
35#[derive(Clone, Debug)]
37pub struct ContextSchema {
38 pub(crate) fields: Vec<ContextField>,
39 pub(crate) layout: Arc<FrozenBufferLayout>,
40 pub(super) user_var_offsets: FastMap<String, usize>,
41 pub(super) user_var_types: FastMap<String, FieldType>,
42 pub(super) spell_usable_offsets: FastMap<usize, SpellUsableOffsets>,
43 pub(super) cooldown_bypass_offsets: FastMap<usize, usize>,
44}
45
46#[derive(Clone, Copy, Debug)]
48#[doc(hidden)]
49pub struct BufferFinalizer<'a> {
50 schema: &'a ContextSchema,
51}
52
53#[derive(Clone, Debug)]
55pub(crate) struct ContextField {
56 pub(crate) descriptor_id: DescriptorId,
57 pub(crate) offset: usize,
58 pub(crate) field_type: FieldType,
59 pub(crate) key: Option<ResolvedKey>,
60 pub(crate) on: Option<AuraOn>,
61}
62
63impl ContextSchema {
64 #[must_use]
65 pub fn new_buffer(&self) -> DenseBuffer {
66 DenseBuffer::new(Arc::clone(&self.layout))
67 }
68
69 #[must_use]
71 pub fn new_bound_buffer(&self) -> BoundDenseBuffer {
72 BoundDenseBuffer::from_schema(self.new_buffer())
73 }
74
75 #[must_use]
76 pub fn buffer_size(&self) -> usize {
77 self.layout.byte_len()
78 }
79
80 pub fn layout_fingerprint(&self) -> LayoutFingerprint {
81 self.layout.fingerprint()
82 }
83
84 #[doc(hidden)]
86 #[must_use]
87 pub const fn buffer_finalizer(&self) -> BufferFinalizer<'_> {
88 BufferFinalizer { schema: self }
89 }
90}
91
92impl BufferFinalizer<'_> {
93 #[doc(hidden)]
94 #[must_use]
95 pub fn bind_spell(&self, key: wowlab_types::sim::SpellIdx) -> Option<SpellSlotHandle> {
96 self.schema.layout.spell_handle(key)
97 }
98
99 #[doc(hidden)]
100 #[must_use]
101 pub fn bind_cooldown(&self, key: wowlab_types::sim::SpellIdx) -> Option<CooldownSlotHandle> {
102 self.schema.layout.cooldown_handle(key)
103 }
104
105 #[doc(hidden)]
106 #[must_use]
107 pub fn bind_history(&self, key: wowlab_types::sim::SpellIdx) -> Option<HistorySlotHandle> {
108 self.schema.layout.history_handle(key)
109 }
110
111 #[doc(hidden)]
112 #[must_use]
113 pub fn bind_aura(&self, key: wowlab_types::sim::AuraKey) -> Option<AuraSlotHandle> {
114 self.schema.layout.aura_handle(key)
115 }
116
117 #[doc(hidden)]
118 pub fn visit_aura_projections(
119 &self,
120 mut visit: impl FnMut(AuraProjectionKey, AuraProjectionSlotHandle),
121 ) {
122 for (key, handle) in self.schema.layout.bindings().aura_projections() {
123 visit(key, handle);
124 }
125 }
126
127 #[doc(hidden)]
128 pub fn visit_spells(
129 &self,
130 mut visit: impl FnMut(wowlab_types::sim::SpellIdx, SpellSlotHandle),
131 ) {
132 for (key, handle) in self.schema.layout.bindings().spells() {
133 visit(key, handle);
134 }
135 }
136
137 #[doc(hidden)]
138 pub fn visit_cooldowns(
139 &self,
140 mut visit: impl FnMut(wowlab_types::sim::SpellIdx, CooldownSlotHandle),
141 ) {
142 for (key, handle) in self.schema.layout.bindings().cooldowns() {
143 visit(key, handle);
144 }
145 }
146
147 #[doc(hidden)]
148 pub fn visit_resources(
149 &self,
150 mut visit: impl FnMut(wowlab_types::combat::ResourceType, ResourceSlotHandle),
151 ) {
152 for (key, handle) in self.schema.layout.bindings().resources() {
153 visit(key, handle);
154 }
155 }
156
157 #[doc(hidden)]
158 pub fn visit_units(&self, mut visit: impl FnMut(&str, UnitSlotHandle)) {
159 for (key, handle) in self.schema.layout.bindings().units() {
160 visit(key, handle);
161 }
162 }
163
164 #[doc(hidden)]
165 #[must_use]
166 pub fn bind_resource(
167 &self,
168 key: wowlab_types::combat::ResourceType,
169 ) -> Option<ResourceSlotHandle> {
170 self.schema.layout.resource_handle(key)
171 }
172
173 #[doc(hidden)]
174 #[must_use]
175 pub fn bind_talent(&self, key: &str) -> Option<TalentSlotHandle> {
176 self.schema.layout.talent_handle(key)
177 }
178
179 #[doc(hidden)]
180 #[must_use]
181 pub fn bind_item(&self, key: &str) -> Option<ItemSlotHandle> {
182 self.schema
183 .layout
184 .bindings()
185 .items()
186 .find_map(|(candidate, handle)| (candidate == key).then_some(handle))
187 }
188
189 #[doc(hidden)]
190 #[must_use]
191 pub fn bind_swing(&self, key: &str) -> Option<SwingSlotHandle> {
192 self.schema
193 .layout
194 .bindings()
195 .swings()
196 .find_map(|(candidate, handle)| (candidate == key).then_some(handle))
197 }
198
199 #[doc(hidden)]
200 #[must_use]
201 pub fn bind_unit(&self, key: &str) -> Option<UnitSlotHandle> {
202 self.schema.layout.unit_handle(key)
203 }
204
205 #[doc(hidden)]
206 #[must_use]
207 pub fn bind_weapon_imbue(&self, key: &str) -> Option<WeaponImbueSlotHandle> {
208 self.schema
209 .layout
210 .bindings()
211 .weapon_imbues()
212 .find_map(|(candidate, handle)| (candidate == key).then_some(handle))
213 }
214}
215
216impl ContextSchema {
217 #[must_use]
218 pub fn field_count(&self) -> usize {
219 self.fields.len()
220 }
221
222 #[must_use]
223 pub fn has_spell_usable_fields(&self) -> bool {
224 !self.spell_usable_offsets.is_empty()
225 }
226
227 #[must_use]
228 pub(crate) fn field_offset(&self, resolved: &ResolvedFieldRead) -> Option<usize> {
229 self.fields
230 .iter()
231 .find(|field| {
232 field.descriptor_id == resolved.descriptor_id
233 && field.key == resolved.key
234 && field.on == resolved.on
235 })
236 .map(|field| field.offset)
237 }
238
239 #[must_use]
240 pub(crate) fn user_var_offset(&self, name: &str) -> Option<usize> {
241 self.user_var_offsets.get(name).copied()
242 }
243
244 #[must_use]
245 pub(crate) fn user_var_type(&self, name: &str) -> Option<FieldType> {
246 self.user_var_types.get(name).copied()
247 }
248
249 #[must_use]
250 pub(crate) fn spell_usable_offsets(&self, field_offset: usize) -> Option<&SpellUsableOffsets> {
251 self.spell_usable_offsets.get(&field_offset)
252 }
253
254 #[must_use]
255 pub(crate) fn cooldown_bypass_aura_offset(&self, cooldown_offset: usize) -> Option<usize> {
256 self.cooldown_bypass_offsets.get(&cooldown_offset).copied()
257 }
258
259 pub(in crate::rotation) fn validate(&self, table: &DescriptorTable) -> Result<()> {
260 let byte_len = self.layout.byte_len();
261
262 self.validate_fields(table, byte_len)?;
263 self.validate_variables(byte_len)?;
264 self.validate_spell_usability(byte_len)?;
265
266 self.validate_cooldown_bypasses(byte_len)
267 }
268
269 fn validate_fields(&self, table: &DescriptorTable, byte_len: usize) -> Result<()> {
270 for field in &self.fields {
271 let descriptor = table.get(field.descriptor_id).ok_or_else(|| {
272 Error::validation("prepared rotation references a missing descriptor")
273 })?;
274
275 if descriptor.field_type != field.field_type {
276 return Err(Error::validation(
277 "prepared rotation descriptor has a mismatched field kind",
278 ));
279 }
280
281 validate_coordinate(
282 field.offset,
283 field.field_type.size(),
284 field.field_type.alignment(),
285 byte_len,
286 )?;
287
288 if descriptor.eval_kind == EvalKind::SpellUsable
289 && !self.spell_usable_offsets.contains_key(&field.offset)
290 {
291 return Err(Error::validation(
292 "prepared rotation spell usability field has no composite coordinates",
293 ));
294 }
295 }
296
297 Ok(())
298 }
299
300 fn validate_variables(&self, byte_len: usize) -> Result<()> {
301 for (name, &offset) in &self.user_var_offsets {
302 let field_type =
303 self.user_var_types.get(name).copied().ok_or_else(|| {
304 Error::validation("prepared rotation variable has no field kind")
305 })?;
306
307 validate_coordinate(offset, field_type.size(), field_type.alignment(), byte_len)?;
308 }
309
310 Ok(())
311 }
312
313 fn validate_spell_usability(&self, byte_len: usize) -> Result<()> {
314 for offsets in self.spell_usable_offsets.values() {
315 validate_optional_slot::<ResourceSlot>(offsets.resource_current, byte_len)?;
316 validate_optional_slot::<ResourceSlot>(offsets.secondary_resource_current, byte_len)?;
317 validate_optional_slot::<CooldownSlot>(offsets.cooldown_base, byte_len)?;
318 validate_optional_slot::<AuraSlot>(offsets.gating_aura_offset, byte_len)?;
319 validate_optional_slot::<AuraSlot>(offsets.cost_bypass_aura_offset, byte_len)?;
320 validate_optional_slot::<AuraSlot>(offsets.cooldown_bypass_aura_offset, byte_len)?;
321 validate_optional_slot::<SpellSlot>(offsets.override_spell_offset, byte_len)?;
322 validate_optional_slot::<CooldownSlot>(offsets.override_cooldown_base, byte_len)?;
323 validate_optional_slot::<AuraSlot>(offsets.override_aura_offset, byte_len)?;
324 }
325
326 Ok(())
327 }
328
329 fn validate_cooldown_bypasses(&self, byte_len: usize) -> Result<()> {
330 for (&cooldown, &aura) in &self.cooldown_bypass_offsets {
331 validate_coordinate(
332 cooldown,
333 size_of::<CooldownSlot>(),
334 align_of::<CooldownSlot>(),
335 byte_len,
336 )?;
337 validate_coordinate(
338 aura,
339 size_of::<AuraSlot>(),
340 align_of::<AuraSlot>(),
341 byte_len,
342 )?;
343 }
344
345 Ok(())
346 }
347}
348
349fn validate_optional_slot<T>(offset: Option<usize>, byte_len: usize) -> Result<()> {
350 if let Some(offset) = offset {
351 validate_coordinate(offset, size_of::<T>(), align_of::<T>(), byte_len)?;
352 }
353
354 Ok(())
355}
356
357fn validate_coordinate(
358 offset: usize,
359 width: usize,
360 alignment: usize,
361 byte_len: usize,
362) -> Result<()> {
363 if !offset.is_multiple_of(alignment) {
364 return Err(Error::validation(format!(
365 "prepared rotation buffer coordinate {offset} is misaligned for {alignment}-byte alignment",
366 )));
367 }
368
369 if offset.checked_add(width).is_none_or(|end| end > byte_len) {
370 return Err(Error::validation(format!(
371 "prepared rotation buffer coordinate {offset} with width {width} exceeds layout size {byte_len}",
372 )));
373 }
374
375 Ok(())
376}
377
378#[cfg(test)]
379mod tests {
380 use googletest::prelude::*;
381
382 use super::*;
383 use crate::rotation::{BufferRequirements, SpecResolver, parse_and_validate};
384
385 fn spell_schema() -> (ContextSchema, DescriptorTable) {
386 let rotation = parse_and_validate(
387 r#"{
388 "version":1,
389 "name":"schema-validation",
390 "variables":{"counter":{"type":"int","value":1}},
391 "actions":[{"type":"cast","spell":"primary"}],
392 "lists":{}
393 }"#,
394 )
395 .expect("parse schema fixture");
396 let resolver = SpecResolver::new("schema-validation").spell("primary", 1);
397 let table = DescriptorTable::build();
398 let schema = crate::rotation::lower::prepare(
399 &rotation,
400 &resolver,
401 &table,
402 &BufferRequirements::new(),
403 )
404 .expect("prepare schema fixture");
405
406 (schema, table)
407 }
408
409 #[gtest]
410 fn missing_composite_coordinate_is_rejected() {
411 let (mut schema, table) = spell_schema();
412
413 schema.spell_usable_offsets.clear();
414
415 expect_that!(schema.validate(&table).is_err(), is_true());
416 }
417
418 #[gtest]
419 fn mismatched_coordinate_kind_is_rejected() {
420 let (mut schema, table) = spell_schema();
421 let field = schema
422 .fields
423 .first_mut()
424 .expect("fixture has a context field");
425
426 field.field_type = match field.field_type {
427 FieldType::Bool => FieldType::Float,
428 _ => FieldType::Bool,
429 };
430
431 expect_that!(schema.validate(&table).is_err(), is_true());
432 }
433
434 #[gtest]
435 fn misaligned_coordinate_is_rejected() {
436 let (mut schema, table) = spell_schema();
437 let field = schema
438 .fields
439 .iter_mut()
440 .find(|field| field.field_type.alignment() > 1)
441 .expect("fixture has an aligned context field");
442
443 field.offset += 1;
444
445 expect_that!(schema.validate(&table).is_err(), is_true());
446 }
447
448 #[gtest]
449 fn out_of_layout_coordinate_is_rejected() {
450 let (mut schema, table) = spell_schema();
451 let byte_len = schema.layout.byte_len();
452 let field = schema
453 .fields
454 .first_mut()
455 .expect("fixture has a context field");
456
457 field.offset = byte_len;
458
459 expect_that!(schema.validate(&table).is_err(), is_true());
460 }
461
462 #[gtest]
463 fn variable_coordinate_without_a_kind_is_rejected() {
464 let (mut schema, table) = spell_schema();
465
466 schema.user_var_types.clear();
467
468 expect_that!(schema.validate(&table).is_err(), is_true());
469 }
470}