condor-for-games 0.4.0

Rust pathfinding library for grids, polygonal scenes, navmeshes, and replanning.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
//! Root-owned structured errors for embedded polygon-pack loading.
//!
//! # Facade role
//!
//! This module is **root-owned** on the public facade (not a re-export from an
//! owner crate). [`crate::polygonal`] uses it to preserve typed parser,
//! validation, and oracle failures while loading embedded public polygon packs.
//! Domain build errors it wraps (`GridBuildError`, `PolygonValidationError`,
//! `NavmeshValidationError`, …) remain owned by their domain crates.
//!
//! # Error layers
//!
//! Pack loading surfaces errors at two layers:
//! - [`ArtifactLoadError`] is the **public load boundary**: which embedded pack
//!   family failed while preserving the concrete source chain.
//! - [`ArtifactDataError`] is the **data/contract failure**: I/O, parse, domain build,
//!   or a typed [`ArtifactContractError`] when a decoded payload violates its schema.
//!
//! Prefer matching on the load-boundary family at API edges, then drill into
//! `ArtifactDataError::Contract` / [`ArtifactContractError::kind`] for stable
//! machine-readable categories without string-parsing display text.
//!
//! The public loader is gated by `polygonal`; nested source variants also follow
//! their domain feature requirements.

use crate::polygonal::PolygonValidationError;
#[cfg(feature = "grid")]
use crate::{FlowFieldBuildError, GridBuildError, GridEditError, GridSearchError};
#[cfg(feature = "navmesh")]
use crate::{
    PreparedNavmeshBuildError,
    navmesh::{DynamicNavmeshError, NavmeshValidationError},
};

/// Concrete parser, I/O, validation, or materialization failure behind a loader.
///
/// Domain build failures (`GridBuild`, `NavmeshValidation`, …) and
/// [`ArtifactContractError`] (via [`Self::Contract`]) both live here so loaders
/// can unify transport/format errors with payload-contract violations.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArtifactDataError {
    /// Filesystem or reader I/O failure while loading pack bytes.
    #[error("I/O failure: {0}")]
    Io(#[from] std::io::Error),
    /// Pack bytes were not valid UTF-8 text.
    #[error("invalid UTF-8: {0}")]
    Utf8(#[from] std::string::FromUtf8Error),
    /// Integer field parse failed in a text pack format.
    #[error("invalid integer: {0}")]
    ParseInt(#[from] std::num::ParseIntError),
    /// Floating-point field parse failed in a text pack format.
    #[error("invalid floating-point number: {0}")]
    ParseFloat(#[from] std::num::ParseFloatError),
    /// TOML decode failed for a pack document.
    #[error("invalid TOML: {0}")]
    Toml(#[from] toml::de::Error),
    /// JSON decode failed for a pack document.
    #[error("invalid JSON: {0}")]
    Json(#[from] serde_json::Error),
    /// Domain grid construction rejected the decoded payload.
    #[cfg(feature = "grid")]
    #[error("grid construction failed: {0}")]
    GridBuild(#[from] GridBuildError),
    /// Domain grid edit rejected a decoded mutation.
    #[cfg(feature = "grid")]
    #[error("grid edit failed: {0}")]
    GridEdit(#[from] GridEditError),
    /// Domain grid search failed while materializing pack-backed work.
    #[cfg(feature = "grid")]
    #[error("grid search failed: {0}")]
    GridSearch(#[from] GridSearchError),
    /// Continuous polygon scene validation failed.
    #[error("polygon validation failed: {0}")]
    PolygonValidation(#[from] PolygonValidationError),
    /// Static navmesh validation failed.
    #[cfg(feature = "navmesh")]
    #[error("navmesh validation failed: {0}")]
    NavmeshValidation(#[from] NavmeshValidationError),
    /// Dynamic navmesh overlay validation failed.
    #[cfg(feature = "navmesh")]
    #[error("dynamic navmesh validation failed: {0}")]
    DynamicNavmesh(#[from] DynamicNavmeshError),
    /// Prepared navmesh preprocess failed after decode.
    #[cfg(feature = "navmesh")]
    #[error("prepared navmesh construction failed: {0}")]
    PreparedNavmesh(#[from] PreparedNavmeshBuildError),
    /// Flow-field build failed for a pack-backed story/fixture.
    #[cfg(feature = "grid")]
    #[error("flow-field construction failed: {0}")]
    FlowField(#[from] FlowFieldBuildError),
    /// Decoded payload violated a stable pack-schema contract.
    #[error(transparent)]
    Contract(Box<ArtifactContractError>),
}

/// Stable category for decoded artifact contract violations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ArtifactContractKind {
    /// Pack format/version field is not supported by this loader.
    UnsupportedVersion,
    /// A required schema field is absent.
    MissingRequiredField,
    /// An identifier field is present but empty or whitespace-only.
    EmptyIdentifier,
    /// A required collection field is empty when at least one entry is required.
    EmptyCollection,
    /// A coordinate or index falls outside the declared map/scene bounds.
    OutOfBounds,
    /// A reference points at a missing or foreign entity id.
    InvalidReference,
    /// Related fields disagree (counts, dimensions, cross-links).
    InconsistentData,
    /// A field is present but not a legal domain value.
    InvalidValue,
}

/// Typed artifact-contract failure with machine-readable identity and values.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ArtifactContractError {
    /// Pack format/version is not supported by this loader revision.
    #[error("unsupported {field} {value} for {artifact}")]
    UnsupportedVersion {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Version field key (for example `format_version`).
        field: String,
        /// Observed unsupported value.
        value: String,
    },
    /// Required schema field is absent from the decoded payload.
    #[error("{artifact} is missing required field {field}")]
    MissingRequiredField {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Missing field key.
        field: String,
    },
    /// Identifier field is present but empty or whitespace-only.
    #[error("{artifact} has an empty identifier in {field}")]
    EmptyIdentifier {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Offending identifier field key.
        field: String,
    },
    /// Collection field is empty when the schema requires entries.
    #[error("{artifact} has an empty collection in {field}")]
    EmptyCollection {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Empty collection field key.
        field: String,
    },
    /// Index or coordinate is outside the declared map/scene bounds.
    #[error("{artifact} field {field} is out of bounds at {location:?}: {value}")]
    OutOfBounds {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Out-of-bounds field key.
        field: String,
        /// Optional list/grid location within the artifact.
        location: ArtifactContractLocation,
        /// Observed out-of-bounds value (display form).
        value: String,
    },
    /// Reference field points at a missing or foreign entity.
    #[error("{artifact} field {field} has an invalid reference at {location:?}: {value}")]
    InvalidReference {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Reference field key.
        field: String,
        /// Optional list/grid location within the artifact.
        location: ArtifactContractLocation,
        /// Observed reference value.
        value: String,
    },
    /// Related fields disagree (counts, dimensions, cross-links).
    #[error("{artifact} fields are inconsistent at {location:?}: {field}={value}")]
    InconsistentData {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Primary field involved in the inconsistency.
        field: String,
        /// Optional list/grid location within the artifact.
        location: ArtifactContractLocation,
        /// Summary of the inconsistent value or pairing.
        value: String,
    },
    /// Field is present but not a legal domain value.
    #[error("{artifact} field {field} has invalid value at {location:?}: {value}")]
    InvalidValue {
        /// Logical pack/artifact name for diagnostics.
        artifact: String,
        /// Invalid field key.
        field: String,
        /// Optional list/grid location within the artifact.
        location: ArtifactContractLocation,
        /// Observed illegal value.
        value: String,
    },
}

/// Optional position of a contract violation within a decoded artifact.
///
/// Fields are independent hints (list index and/or grid row/column). Absent
/// coordinates mean the violation is artifact-wide or the loader did not attach
/// a finer location.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ArtifactContractLocation {
    /// Optional collection/list index within the artifact.
    pub index: Option<usize>,
    /// Optional map/grid row for spatial packs.
    pub row: Option<usize>,
    /// Optional map/grid column for spatial packs.
    pub column: Option<usize>,
}

impl ArtifactContractLocation {
    /// No positional context (artifact-wide or unspecified).
    pub const NONE: Self = Self {
        index: None,
        row: None,
        column: None,
    };

    /// Violation at a single collection index (scenario list, portal list, …).
    pub const fn index(index: usize) -> Self {
        Self {
            index: Some(index),
            row: None,
            column: None,
        }
    }

    /// Violation on a map/grid row without a column.
    pub const fn row(row: usize) -> Self {
        Self {
            index: None,
            row: Some(row),
            column: None,
        }
    }

    /// Violation at a map/grid cell (`row`, `column`).
    pub const fn row_column(row: usize, column: usize) -> Self {
        Self {
            index: None,
            row: Some(row),
            column: Some(column),
        }
    }
}

impl ArtifactContractError {
    /// Stable category for this error, independent of field names and display text.
    ///
    /// Use for routing, metrics, and fixture assertions without matching every
    /// payload-bearing variant arm.
    pub const fn kind(&self) -> ArtifactContractKind {
        match self {
            Self::UnsupportedVersion { .. } => ArtifactContractKind::UnsupportedVersion,
            Self::MissingRequiredField { .. } => ArtifactContractKind::MissingRequiredField,
            Self::EmptyIdentifier { .. } => ArtifactContractKind::EmptyIdentifier,
            Self::EmptyCollection { .. } => ArtifactContractKind::EmptyCollection,
            Self::OutOfBounds { .. } => ArtifactContractKind::OutOfBounds,
            Self::InvalidReference { .. } => ArtifactContractKind::InvalidReference,
            Self::InconsistentData { .. } => ArtifactContractKind::InconsistentData,
            Self::InvalidValue { .. } => ArtifactContractKind::InvalidValue,
        }
    }
}

impl ArtifactDataError {
    /// Contract factory: unsupported `format_version` for a named artifact pack.
    pub(crate) fn unsupported_version(artifact: impl Into<String>, value: impl ToString) -> Self {
        Self::Contract(Box::new(ArtifactContractError::UnsupportedVersion {
            artifact: artifact.into(),
            field: "format_version".into(),
            value: value.to_string(),
        }))
    }

    /// Contract factory: required schema field absent from a decoded pack.
    pub(crate) fn missing_required_field(
        artifact: impl Into<String>,
        field: impl Into<String>,
    ) -> Self {
        Self::Contract(Box::new(ArtifactContractError::MissingRequiredField {
            artifact: artifact.into(),
            field: field.into(),
        }))
    }

    /// Contract factory: identifier field present but empty/whitespace-only.
    pub(crate) fn empty_identifier(artifact: impl Into<String>, field: impl Into<String>) -> Self {
        Self::Contract(Box::new(ArtifactContractError::EmptyIdentifier {
            artifact: artifact.into(),
            field: field.into(),
        }))
    }

    /// Contract factory: field value points at a missing or foreign entity.
    pub(crate) fn invalid_reference(
        artifact: impl Into<String>,
        field: impl Into<String>,
        location: ArtifactContractLocation,
        value: impl ToString,
    ) -> Self {
        Self::Contract(Box::new(ArtifactContractError::InvalidReference {
            artifact: artifact.into(),
            field: field.into(),
            location,
            value: value.to_string(),
        }))
    }

    /// Contract factory: related fields disagree at an optional location.
    pub(crate) fn inconsistent_data(
        artifact: impl Into<String>,
        field: impl Into<String>,
        location: ArtifactContractLocation,
        value: impl ToString,
    ) -> Self {
        Self::Contract(Box::new(ArtifactContractError::InconsistentData {
            artifact: artifact.into(),
            field: field.into(),
            location,
            value: value.to_string(),
        }))
    }

    /// Contract factory: field present but not a legal domain value.
    pub(crate) fn invalid_value(
        artifact: impl Into<String>,
        field: impl Into<String>,
        location: ArtifactContractLocation,
        value: impl ToString,
    ) -> Self {
        Self::Contract(Box::new(ArtifactContractError::InvalidValue {
            artifact: artifact.into(),
            field: field.into(),
            location,
            value: value.to_string(),
        }))
    }
}

/// Public loading boundary: identifies the artifact family and preserves its source.
///
/// Variants name the **loader surface** (atlas maps, benchmark packs, story/fixture
/// packs for polygon/navmesh/MAPF/replanning/any-angle, …). The nested
/// [`ArtifactDataError`] holds parse, I/O, materialization, or contract detail.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArtifactLoadError {
    /// Moving AI `.map`/`.scen` atlas loader surface failed.
    #[error("failed to load Moving AI atlas data: {source}")]
    Atlas {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
    /// Grid benchmark scenario pack loader surface failed.
    #[error("failed to load benchmark scenario data: {source}")]
    Benchmark {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
    /// Continuous polygon scene pack loader surface failed.
    #[error("failed to load polygon scene data: {source}")]
    Polygon {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
    /// Navmesh pack loader surface failed.
    #[error("failed to load navmesh data: {source}")]
    Navmesh {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
    /// Flow-field story/fixture pack loader surface failed.
    #[error("failed to load flow-field data: {source}")]
    FlowField {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
    /// Multi-agent pathfinding pack loader surface failed.
    #[error("failed to load MAPF data: {source}")]
    Mapf {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
    /// Incremental replanning pack loader surface failed.
    #[error("failed to load replanning data: {source}")]
    Replanning {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
    /// Any-angle benchmark pack loader surface failed.
    #[error("failed to load any-angle benchmark data: {source}")]
    AnyAngle {
        /// Nested data/contract failure for this load.
        #[source]
        source: ArtifactDataError,
    },
}

impl ArtifactLoadError {
    /// Load-boundary factory for the polygon pack family.
    pub(crate) const fn polygon(source: ArtifactDataError) -> Self {
        Self::Polygon { source }
    }
}

#[cfg(test)]
mod tests {
    use std::error::Error;

    use super::{
        ArtifactContractError, ArtifactContractLocation, ArtifactDataError, ArtifactLoadError,
    };

    #[test]
    fn benchmark_toml_error_preserves_concrete_source_chain() {
        let parse_error = toml::from_str::<u32>("not = [valid").unwrap_err();
        let error = ArtifactLoadError::Benchmark {
            source: parse_error.into(),
        };

        assert!(matches!(
            &error,
            ArtifactLoadError::Benchmark {
                source: ArtifactDataError::Toml(_)
            }
        ));
        let data_source = error
            .source()
            .expect("loader error should expose data source");
        assert!(
            data_source.source().is_some(),
            "TOML source should be chained"
        );
    }

    #[test]
    fn unsupported_version_exposes_exact_typed_data() {
        let error = ArtifactDataError::unsupported_version("benchmark pack", 7);
        let ArtifactDataError::Contract(contract) = error else {
            panic!("expected contract error");
        };
        assert_eq!(
            *contract,
            ArtifactContractError::UnsupportedVersion {
                artifact: "benchmark pack".into(),
                field: "format_version".into(),
                value: "7".into(),
            }
        );
    }

    #[test]
    fn empty_identifier_exposes_exact_typed_data() {
        let error = ArtifactDataError::empty_identifier("benchmark pack", "pack_id");
        let ArtifactDataError::Contract(contract) = error else {
            panic!("expected contract error");
        };
        assert_eq!(
            *contract,
            ArtifactContractError::EmptyIdentifier {
                artifact: "benchmark pack".into(),
                field: "pack_id".into(),
            }
        );
    }

    #[test]
    fn missing_field_exposes_exact_typed_data() {
        let error = ArtifactDataError::missing_required_field("scenario-1", "source_map");
        let ArtifactDataError::Contract(contract) = error else {
            panic!("expected contract error");
        };
        assert_eq!(
            *contract,
            ArtifactContractError::MissingRequiredField {
                artifact: "scenario-1".into(),
                field: "source_map".into(),
            }
        );
    }

    #[test]
    fn invalid_reference_exposes_exact_typed_data() {
        let error = ArtifactDataError::invalid_reference(
            "mesh-1",
            "portals.left",
            ArtifactContractLocation::index(3),
            "missing-cell",
        );
        let ArtifactDataError::Contract(contract) = error else {
            panic!("expected contract error");
        };
        assert_eq!(
            *contract,
            ArtifactContractError::InvalidReference {
                artifact: "mesh-1".into(),
                field: "portals.left".into(),
                location: ArtifactContractLocation::index(3),
                value: "missing-cell".into(),
            }
        );
    }

    #[test]
    fn invalid_value_exposes_exact_typed_data() {
        let error = ArtifactDataError::invalid_value(
            "scenario-1",
            "movement_model",
            ArtifactContractLocation::NONE,
            "hex",
        );
        let ArtifactDataError::Contract(contract) = error else {
            panic!("expected contract error");
        };
        assert_eq!(
            *contract,
            ArtifactContractError::InvalidValue {
                artifact: "scenario-1".into(),
                field: "movement_model".into(),
                location: ArtifactContractLocation::NONE,
                value: "hex".into(),
            }
        );
    }
}