1wowlab_engine_macros::define_error! {
2#[derive(Debug)]
4pub struct EncounterConstructionError {
5 #[source]
6 kind: EncounterConstructionErrorKind,
7}
8
9#[derive(Debug, thiserror::Error)]
10enum EncounterConstructionErrorKind {
11 #[error("invalid encounter definition: {0}")]
12 InvalidDefinition(#[from] wowlab_types::sim::EncounterValidationError),
13 #[error("resolved encounter enemy count mismatch: expected {expected}, found {found}")]
14 ResolvedEnemyCountMismatch { expected: usize, found: usize },
15 #[error("resolved encounter enemy index {index} does not fit EnemyIdx")]
16 ResolvedEnemyIndexOutOfRange {
17 index: usize,
18 #[source]
19 source: std::num::TryFromIntError,
20 },
21 #[error(
22 "resolved encounter identity mismatch at position {index}: authored {authored:?}, resolved {resolved:?}"
23 )]
24 ResolvedEnemyIdentityMismatch {
25 index: usize,
26 authored: wowlab_types::sim::EnemyIdx,
27 resolved: wowlab_types::sim::EnemyIdx,
28 },
29 #[error("resolved enemy {enemy:?} level mismatch: expected {expected}, found {found}")]
30 ResolvedEnemyLevelMismatch {
31 enemy: wowlab_types::sim::EnemyIdx,
32 expected: u16,
33 found: u16,
34 },
35 #[error("resolved enemy {enemy:?} does not match its authored identity input")]
36 ResolvedEnemyIdentityMetadataMismatch { enemy: wowlab_types::sim::EnemyIdx },
37 #[error("resolved enemy {enemy:?} does not match its authored runtime metadata")]
38 ResolvedEnemyMetadataMismatch { enemy: wowlab_types::sim::EnemyIdx },
39 #[error("resolved enemy {enemy:?} health model requires max health {expected}, found {found}")]
40 ResolvedHealthModelMismatch {
41 enemy: wowlab_types::sim::EnemyIdx,
42 expected: f64,
43 found: f64,
44 },
45 #[error("resolved enemy {enemy:?} has an empty display name")]
46 EmptyResolvedDisplayName { enemy: wowlab_types::sim::EnemyIdx },
47 #[error("resolved enemy {enemy:?} has invalid {field}: {found}")]
48 InvalidResolvedEnemyStat {
49 enemy: wowlab_types::sim::EnemyIdx,
50 field: &'static str,
51 found: f64,
52 },
53 #[error(
54 "combat creature armor for enemy {enemy:?} does not match resolved armor: expected {expected}, found {found}"
55 )]
56 CombatCreatureArmorMismatch {
57 enemy: wowlab_types::sim::EnemyIdx,
58 expected: f64,
59 found: f64,
60 },
61 #[error("combat armor constant for enemy {enemy:?} must be finite and positive, found {found}")]
62 InvalidCombatArmorConstant {
63 enemy: wowlab_types::sim::EnemyIdx,
64 found: f64,
65 },
66 #[error(
67 "combat armor constant modifier for enemy {enemy:?} must be finite and positive, found {found}"
68 )]
69 InvalidCombatArmorConstantMod {
70 enemy: wowlab_types::sim::EnemyIdx,
71 found: f64,
72 },
73 #[error(
74 "effective combat armor constant for enemy {enemy:?} does not match resolved value: expected {expected}, found {found} from armor_constant {armor_constant} * armor_constant_mod {armor_constant_mod}"
75 )]
76 CombatEffectiveArmorConstantMismatch {
77 enemy: wowlab_types::sim::EnemyIdx,
78 expected: f64,
79 found: f64,
80 armor_constant: f64,
81 armor_constant_mod: f64,
82 },
83}
84}
85
86impl EncounterConstructionError {
87 pub(crate) fn resolved_enemy_count_mismatch(expected: usize, found: usize) -> Self {
88 Self {
89 kind: EncounterConstructionErrorKind::ResolvedEnemyCountMismatch { expected, found },
90 }
91 }
92
93 pub(crate) fn resolved_enemy_index_out_of_range(
94 index: usize,
95 source: std::num::TryFromIntError,
96 ) -> Self {
97 Self {
98 kind: EncounterConstructionErrorKind::ResolvedEnemyIndexOutOfRange { index, source },
99 }
100 }
101
102 pub(crate) fn resolved_enemy_identity_mismatch(
103 index: usize,
104 authored: wowlab_types::sim::EnemyIdx,
105 resolved: wowlab_types::sim::EnemyIdx,
106 ) -> Self {
107 Self {
108 kind: EncounterConstructionErrorKind::ResolvedEnemyIdentityMismatch {
109 index,
110 authored,
111 resolved,
112 },
113 }
114 }
115
116 pub(crate) fn resolved_enemy_level_mismatch(
117 enemy: wowlab_types::sim::EnemyIdx,
118 expected: u16,
119 found: u16,
120 ) -> Self {
121 Self {
122 kind: EncounterConstructionErrorKind::ResolvedEnemyLevelMismatch {
123 enemy,
124 expected,
125 found,
126 },
127 }
128 }
129
130 pub(crate) fn resolved_enemy_identity_metadata_mismatch(
131 enemy: wowlab_types::sim::EnemyIdx,
132 ) -> Self {
133 Self {
134 kind: EncounterConstructionErrorKind::ResolvedEnemyIdentityMetadataMismatch { enemy },
135 }
136 }
137
138 pub(crate) fn resolved_enemy_metadata_mismatch(enemy: wowlab_types::sim::EnemyIdx) -> Self {
139 Self {
140 kind: EncounterConstructionErrorKind::ResolvedEnemyMetadataMismatch { enemy },
141 }
142 }
143
144 pub(crate) fn resolved_health_model_mismatch(
145 enemy: wowlab_types::sim::EnemyIdx,
146 expected: f64,
147 found: f64,
148 ) -> Self {
149 Self {
150 kind: EncounterConstructionErrorKind::ResolvedHealthModelMismatch {
151 enemy,
152 expected,
153 found,
154 },
155 }
156 }
157
158 pub(crate) fn empty_resolved_display_name(enemy: wowlab_types::sim::EnemyIdx) -> Self {
159 Self {
160 kind: EncounterConstructionErrorKind::EmptyResolvedDisplayName { enemy },
161 }
162 }
163
164 pub(crate) fn invalid_resolved_enemy_stat(
165 enemy: wowlab_types::sim::EnemyIdx,
166 field: &'static str,
167 found: f64,
168 ) -> Self {
169 Self {
170 kind: EncounterConstructionErrorKind::InvalidResolvedEnemyStat {
171 enemy,
172 field,
173 found,
174 },
175 }
176 }
177
178 pub(crate) fn combat_creature_armor_mismatch(
179 enemy: wowlab_types::sim::EnemyIdx,
180 expected: f64,
181 found: f64,
182 ) -> Self {
183 Self {
184 kind: EncounterConstructionErrorKind::CombatCreatureArmorMismatch {
185 enemy,
186 expected,
187 found,
188 },
189 }
190 }
191
192 pub(crate) fn invalid_combat_armor_constant(
193 enemy: wowlab_types::sim::EnemyIdx,
194 found: f64,
195 ) -> Self {
196 Self {
197 kind: EncounterConstructionErrorKind::InvalidCombatArmorConstant { enemy, found },
198 }
199 }
200
201 pub(crate) fn invalid_combat_armor_constant_mod(
202 enemy: wowlab_types::sim::EnemyIdx,
203 found: f64,
204 ) -> Self {
205 Self {
206 kind: EncounterConstructionErrorKind::InvalidCombatArmorConstantMod { enemy, found },
207 }
208 }
209
210 pub(crate) fn combat_effective_armor_constant_mismatch(
211 enemy: wowlab_types::sim::EnemyIdx,
212 expected: f64,
213 found: f64,
214 armor_constant: f64,
215 armor_constant_mod: f64,
216 ) -> Self {
217 Self {
218 kind: EncounterConstructionErrorKind::CombatEffectiveArmorConstantMismatch {
219 enemy,
220 expected,
221 found,
222 armor_constant,
223 armor_constant_mod,
224 },
225 }
226 }
227
228 #[cfg(test)]
229 #[must_use]
230 pub(crate) fn is_combat_creature_armor_mismatch(&self) -> bool {
231 matches!(
232 self.kind,
233 EncounterConstructionErrorKind::CombatCreatureArmorMismatch { .. }
234 )
235 }
236
237 #[cfg(test)]
238 #[must_use]
239 pub(crate) fn is_invalid_combat_armor_constant(&self) -> bool {
240 matches!(
241 self.kind,
242 EncounterConstructionErrorKind::InvalidCombatArmorConstant { .. }
243 )
244 }
245
246 #[cfg(test)]
247 #[must_use]
248 pub(crate) fn is_invalid_combat_armor_constant_mod(&self) -> bool {
249 matches!(
250 self.kind,
251 EncounterConstructionErrorKind::InvalidCombatArmorConstantMod { .. }
252 )
253 }
254
255 #[cfg(test)]
256 #[must_use]
257 pub(crate) fn is_combat_effective_armor_constant_mismatch(&self) -> bool {
258 matches!(
259 self.kind,
260 EncounterConstructionErrorKind::CombatEffectiveArmorConstantMismatch { .. }
261 )
262 }
263}
264
265impl From<wowlab_types::sim::EncounterValidationError> for EncounterConstructionError {
266 fn from(error: wowlab_types::sim::EncounterValidationError) -> Self {
267 Self {
268 kind: EncounterConstructionErrorKind::InvalidDefinition(error),
269 }
270 }
271}
272
273wowlab_engine_macros::define_error! {
274#[derive(Debug)]
276#[non_exhaustive]
277pub struct SimRunError {
278 #[source]
279 kind: SimRunErrorKind,
280}
281
282#[derive(Debug, thiserror::Error)]
283enum SimRunErrorKind {
284 #[error("event budget exceeded: simulator processed {count} events without converging")]
285 EventBudgetExceeded { count: u32 },
286 #[error("spec handler failed while processing an event")]
287 Handler {
288 #[source]
289 source: crate::SpecRuntimeError,
290 },
291}
292}
293
294impl SimRunError {
295 #[must_use]
297 pub const fn event_budget_exceeded(count: u32) -> Self {
298 Self {
299 kind: SimRunErrorKind::EventBudgetExceeded { count },
300 }
301 }
302
303 #[must_use]
305 pub const fn handler(source: crate::SpecRuntimeError) -> Self {
306 Self {
307 kind: SimRunErrorKind::Handler { source },
308 }
309 }
310
311 #[must_use]
313 pub const fn event_count(&self) -> Option<u32> {
314 match self.kind {
315 SimRunErrorKind::EventBudgetExceeded { count } => Some(count),
316 SimRunErrorKind::Handler { .. } => None,
317 }
318 }
319}
320
321#[derive(Clone, Copy, Debug, Eq, PartialEq)]
323#[non_exhaustive]
324pub enum EngineConstructionStage {
325 RotationCompile,
326 SpecConstruction,
327}
328
329wowlab_engine_macros::define_error! {
330#[non_exhaustive]
332pub struct EngineConstructionError {
333 #[source]
334 kind: EngineConstructionErrorKind,
335}
336
337#[derive(Debug, thiserror::Error)]
338enum EngineConstructionErrorKind {
339 #[error("rotation compile error: {source}")]
340 RotationCompile {
341 #[source]
342 source: Box<dyn std::error::Error + Send + Sync>,
343 },
344 #[error("spec construction error: {source}")]
345 SpecConstruction {
346 #[source]
347 source: Box<dyn std::error::Error + Send + Sync>,
348 },
349}
350}
351
352impl EngineConstructionError {
353 fn new(
354 stage: EngineConstructionStage,
355 source: impl std::error::Error + Send + Sync + 'static,
356 ) -> Self {
357 let source = Box::new(source);
358 let kind = match stage {
359 EngineConstructionStage::RotationCompile => {
360 EngineConstructionErrorKind::RotationCompile { source }
361 }
362 EngineConstructionStage::SpecConstruction => {
363 EngineConstructionErrorKind::SpecConstruction { source }
364 }
365 };
366
367 Self { kind }
368 }
369
370 #[must_use]
372 pub const fn stage(&self) -> EngineConstructionStage {
373 match self.kind {
374 EngineConstructionErrorKind::RotationCompile { .. } => {
375 EngineConstructionStage::RotationCompile
376 }
377 EngineConstructionErrorKind::SpecConstruction { .. } => {
378 EngineConstructionStage::SpecConstruction
379 }
380 }
381 }
382}
383
384impl std::fmt::Debug for EngineConstructionError {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 f.debug_struct("EngineConstructionError")
387 .field("stage", &self.stage())
388 .finish_non_exhaustive()
389 }
390}
391
392wowlab_engine_macros::define_error! {
393#[non_exhaustive]
395pub struct EngineError {
396 #[source]
397 kind: EngineErrorKind,
398}
399
400#[derive(Debug, thiserror::Error)]
401enum EngineErrorKind {
402 #[error("intent validation error: {message}")]
403 IntentValidation { message: String },
404 #[error("chunk validation error: {message}")]
405 ChunkValidation { message: String },
406 #[error("resolver error: {0}")]
407 Resolver(#[from] crate::resolver::ResolverError),
408 #[error("encounter construction error: {0}")]
409 EncounterConstruction(#[from] EncounterConstructionError),
410 #[error("spec metadata error: {0}")]
411 SpecMetadata(#[from] crate::DeclaredSpecMetadataError),
412 #[error("content catalog error: {0}")]
413 ContentCatalog(#[from] crate::ContentCatalogError),
414 #[error("{context}: {source}")]
415 SpellIdConversion {
416 context: &'static str,
417 #[source]
418 source: crate::SpellIdConversionError,
419 },
420 #[error("{0}")]
421 Construction(#[source] EngineConstructionError),
422 #[error("spec construction error: {message}")]
423 SpecConstruction { message: String },
424 #[error("spec not found: {message}")]
425 SpecNotFound { message: String },
426 #[error("simulation runtime error: {message}")]
427 SimulationRuntime { message: String },
428 #[error("simulation runtime error: {0}")]
429 SimulationRun(#[from] SimRunError),
430}
431}
432
433macro_rules! engine_error_ctor {
434 ($($ctor:ident => $variant:ident),+ $(,)?) => {
435 impl EngineError {
436 $(
437 pub fn $ctor(message: impl Into<String>) -> Self {
438 Self {
439 kind: EngineErrorKind::$variant {
440 message: message.into(),
441 },
442 }
443 }
444 )+
445 }
446 };
447}
448
449impl EngineError {
450 #[must_use]
452 pub fn rotation_compile(source: impl std::error::Error + Send + Sync + 'static) -> Self {
453 Self {
454 kind: EngineErrorKind::Construction(EngineConstructionError::new(
455 EngineConstructionStage::RotationCompile,
456 source,
457 )),
458 }
459 }
460
461 #[must_use]
463 pub fn spec_construction_source(
464 source: impl std::error::Error + Send + Sync + 'static,
465 ) -> Self {
466 Self {
467 kind: EngineErrorKind::Construction(EngineConstructionError::new(
468 EngineConstructionStage::SpecConstruction,
469 source,
470 )),
471 }
472 }
473
474 #[must_use]
476 pub const fn spell_id_conversion(
477 context: &'static str,
478 source: crate::SpellIdConversionError,
479 ) -> Self {
480 Self {
481 kind: EngineErrorKind::SpellIdConversion { context, source },
482 }
483 }
484
485 #[must_use]
486 pub fn is_intent_validation(&self) -> bool {
487 matches!(self.kind, EngineErrorKind::IntentValidation { .. })
488 }
489
490 #[must_use]
491 pub fn is_chunk_validation(&self) -> bool {
492 matches!(self.kind, EngineErrorKind::ChunkValidation { .. })
493 }
494
495 #[must_use]
497 pub fn is_spec_construction(&self) -> bool {
498 match &self.kind {
499 EngineErrorKind::Construction(error) => {
500 error.stage() == EngineConstructionStage::SpecConstruction
501 }
502 EngineErrorKind::SpecConstruction { .. }
503 | EngineErrorKind::SpecMetadata(_)
504 | EngineErrorKind::ContentCatalog(_)
505 | EngineErrorKind::SpellIdConversion { .. } => true,
506 _ => false,
507 }
508 }
509
510 #[must_use]
511 pub fn message(&self) -> Option<&str> {
512 match &self.kind {
513 EngineErrorKind::IntentValidation { message }
514 | EngineErrorKind::ChunkValidation { message }
515 | EngineErrorKind::SpecConstruction { message }
516 | EngineErrorKind::SpecNotFound { message }
517 | EngineErrorKind::SimulationRuntime { message } => Some(message),
518 EngineErrorKind::Resolver(_)
519 | EngineErrorKind::EncounterConstruction(_)
520 | EngineErrorKind::SpecMetadata(_)
521 | EngineErrorKind::ContentCatalog(_)
522 | EngineErrorKind::Construction(_)
523 | EngineErrorKind::SpellIdConversion { .. }
524 | EngineErrorKind::SimulationRun(_) => None,
525 }
526 }
527
528 #[cfg(test)]
529 pub(crate) fn into_encounter_construction(self) -> Option<EncounterConstructionError> {
530 match self.kind {
531 EngineErrorKind::EncounterConstruction(error) => Some(error),
532 _ => None,
533 }
534 }
535}
536
537impl From<wowlab_engine_gamedata::GameDataError> for EngineError {
538 fn from(source: wowlab_engine_gamedata::GameDataError) -> Self {
539 Self::spec_construction_source(source)
540 }
541}
542
543impl std::fmt::Debug for EngineError {
544 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545 if let EngineErrorKind::Construction(error) = &self.kind {
546 return f
547 .debug_struct("EngineError")
548 .field("construction_stage", &error.stage())
549 .finish_non_exhaustive();
550 }
551
552 f.debug_struct("EngineError")
553 .field("kind", &self.kind)
554 .finish()
555 }
556}
557
558impl From<crate::resolver::ResolverError> for EngineError {
559 fn from(error: crate::resolver::ResolverError) -> Self {
560 Self {
561 kind: EngineErrorKind::Resolver(error),
562 }
563 }
564}
565
566impl From<EncounterConstructionError> for EngineError {
567 fn from(error: EncounterConstructionError) -> Self {
568 Self {
569 kind: EngineErrorKind::EncounterConstruction(error),
570 }
571 }
572}
573
574impl From<wowlab_types::sim::EncounterValidationError> for EngineError {
575 fn from(error: wowlab_types::sim::EncounterValidationError) -> Self {
576 EncounterConstructionError::from(error).into()
577 }
578}
579
580impl From<crate::DeclaredSpecMetadataError> for EngineError {
581 fn from(error: crate::DeclaredSpecMetadataError) -> Self {
582 Self {
583 kind: EngineErrorKind::SpecMetadata(error),
584 }
585 }
586}
587
588impl From<crate::ContentCatalogError> for EngineError {
589 fn from(error: crate::ContentCatalogError) -> Self {
590 Self {
591 kind: EngineErrorKind::ContentCatalog(error),
592 }
593 }
594}
595
596impl From<SimRunError> for EngineError {
597 fn from(error: SimRunError) -> Self {
598 Self {
599 kind: EngineErrorKind::SimulationRun(error),
600 }
601 }
602}
603
604engine_error_ctor! {
605 intent_validation => IntentValidation,
606 chunk_validation => ChunkValidation,
607 spec_construction => SpecConstruction,
608 spec_not_found => SpecNotFound,
609 simulation_runtime => SimulationRuntime,
610}
611
612#[cfg(test)]
613mod construction_error_tests {
614 use std::error::Error as _;
615
616 use googletest::prelude::*;
617
618 use super::*;
619
620 #[gtest]
621 fn typed_construction_source_preserves_stage_display_chain_and_redaction() -> Result<()> {
622 let error = EngineError::spec_construction_source(std::io::Error::new(
623 std::io::ErrorKind::InvalidData,
624 "private builder payload",
625 ));
626
627 verify_true!(error.is_spec_construction())?;
628 verify_that!(
629 error.to_string(),
630 eq("spec construction error: private builder payload")
631 )?;
632 let construction = error
633 .source()
634 .and_then(std::error::Error::source)
635 .and_then(|source| source.downcast_ref::<EngineConstructionError>())
636 .or_fail()?;
637
638 verify_that!(
639 construction.stage(),
640 eq(EngineConstructionStage::SpecConstruction)
641 )?;
642 verify_true!(
643 construction
644 .source()
645 .and_then(std::error::Error::source)
646 .is_some_and(<dyn std::error::Error>::is::<std::io::Error>)
647 )?;
648 let debug = format!("{error:?}");
649
650 verify_that!(&debug, contains_substring("Construction"))?;
651
652 verify_that!(&debug, not(contains_substring("private builder payload")))
653 }
654
655 #[gtest]
656 fn game_data_error_conversion_preserves_spec_construction_source() -> Result<()> {
657 let mut builder = wowlab_engine_gamedata::ResolvedGameData::builder();
658
659 builder.insert_base_points(wowlab_types::sim::SpellIdx::from_raw(1), 1, 1.0);
660 let source = builder
661 .build()
662 .require_base_points(wowlab_types::sim::SpellIdx::from_raw(2), 1)
663 .err()
664 .or_fail()?;
665 let error = EngineError::from(source);
666
667 verify_true!(error.is_spec_construction())?;
668
669 verify_true!(
670 error
671 .source()
672 .and_then(std::error::Error::source)
673 .and_then(|source| source.downcast_ref::<EngineConstructionError>())
674 .and_then(std::error::Error::source)
675 .and_then(std::error::Error::source)
676 .is_some_and(<dyn std::error::Error>::is::<wowlab_engine_gamedata::GameDataError>)
677 )
678 }
679}