Skip to main content

rakata_formats/schema/
mod.rs

1//! What is established about one GFF field, and the vocabulary for saying it.
2//!
3//! The shape follows what a field *is*, rather than growing a slot per defect
4//! as each is found.
5//!
6//! # Nine facts
7//!
8//! Two are about the file: the label and, inside [`Shape`], the GFF type.
9//! Five are about the engine: [`Life`], [`Absent`], `required`,
10//! [`Constraint`] and [`Omission`]. Two are about this crate: the
11//! [`Substitute`] its reader produces where the engine's answer names no
12//! value, and `modelled`, whether this layer holds a member for the label at
13//! all.
14//!
15//! # What is deliberately not here
16//!
17//! **Provenance.** A citation beside an entry enforces nothing: it catches an
18//! empty string and never a false one. [`Life::Unexamined`] is the structural
19//! answer to a field nobody has looked at. Provenance itself lives on the
20//! pages under `docs/src/formats/`, keyed by the same labels these entries
21//! declare.
22//!
23//! **The Rust type.** A field's type decides how to read it, how to write it
24//! and which GFF type it maps to. Recording that mapping makes a closed table
25//! somebody has to maintain, which this layer has already grown twice. Types
26//! state their own mapping through [`GffScalar`] instead.
27
28mod scalar;
29
30pub use scalar::GffScalar;
31
32use crate::gff::Gff;
33use crate::gff::GffLabel;
34use crate::gff::GffValue;
35
36/// One field of one GFF struct, and everything established about it.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct Field {
39    /// The label as it appears in the file.
40    ///
41    /// A built [`GffLabel`] rather than a `&str`, so a declaration naming
42    /// something a GFF cannot hold fails the build at that declaration. Three
43    /// of the corpus's names run past sixteen bytes and the engine truncates
44    /// them; a schema that did not would name a label no file can carry, and
45    /// any rule keyed on it would be silently dead.
46    pub label: GffLabel,
47    /// What the field holds, and for a container what its elements hold.
48    pub shape: Shape,
49    /// Whether the engine reads this label at this path.
50    pub life: Life,
51    /// What happens when the label is missing.
52    pub absent: Absent,
53    /// Whether absence aborts the containing structure rather than defaulting.
54    pub required: bool,
55    /// Whether the engine clamps or truncates the value it reads.
56    pub constraint: Option<Constraint>,
57    /// Whether the engine's own writer leaves this label out at some value.
58    pub omission: Option<Omission>,
59    /// Whether this layer holds a member for the label.
60    ///
61    /// `false` for a label the schema declares and the view has no slot for,
62    /// which is what `gff_entry` means. Those paths are real engine facts a
63    /// reader cannot observe: whatever the engine substitutes for the absent
64    /// label has nowhere to arrive, so a consumer asking "does the fallback
65    /// matter here" gets a different answer than at a modelled path, and the
66    /// difference is not visible from the other seven.
67    pub modelled: bool,
68}
69
70impl Field {
71    /// Whether the writer may leave this label out when it holds `value`.
72    ///
73    /// The engine's own writer omits some labels at some values, and matching
74    /// it is what lets an authored file round-trip against one it wrote. A
75    /// field declaring no omission answers `false`, so a missing opt-in costs
76    /// a redundant label rather than a dropped value.
77    ///
78    /// [`OmitWhen::Matches`] always answers `false`. Its test is the resolved
79    /// value of a sibling, which is not reachable from one entry, and the
80    /// engine-side rule for it has not been traced; the fail-safe direction is
81    /// to write the label.
82    pub fn may_omit(&self, value: &GffValue) -> bool {
83        let Some(omission) = self.omission else {
84            return false;
85        };
86        match omission.when {
87            OmitWhen::AuditedConstant => match self.absent {
88                Absent::Resolves(_, default, _) => same_value(&default.get(), value),
89                // No audited constant to compare against, which the derive
90                // rejects at the declaration. Reachable only from a schema
91                // written by hand.
92                Absent::Silent(..) => false,
93            },
94            OmitWhen::Empty => matches!(value, GffValue::List(items) if items.is_empty()),
95            // Neither decides whether to write the label. `Matches` chains off
96            // a sibling this cannot resolve, and `Elements` is about what goes
97            // in the list rather than whether the list is written.
98            OmitWhen::Matches(_) | OmitWhen::Elements(_) => false,
99        }
100    }
101}
102
103/// Value equality with floats compared bit for bit.
104///
105/// `-0.0 == 0.0` under `PartialEq`, so a writer using it drops a label whose
106/// bits differ from the default and the file stops round-tripping.
107fn same_value(a: &GffValue, b: &GffValue) -> bool {
108    match (a, b) {
109        (GffValue::Single(x), GffValue::Single(y)) => x.to_bits() == y.to_bits(),
110        (GffValue::Double(x), GffValue::Double(y)) => x.to_bits() == y.to_bits(),
111        (GffValue::Vector3(x), GffValue::Vector3(y)) => {
112            x.iter().zip(y).all(|(p, q)| p.to_bits() == q.to_bits())
113        }
114        (GffValue::Vector4(x), GffValue::Vector4(y)) => {
115            x.iter().zip(y).all(|(p, q)| p.to_bits() == q.to_bits())
116        }
117        // Every remaining variant holds either an integer, a byte string or a
118        // composite of those, and their `PartialEq` is exact.
119        _ => a == b,
120    }
121}
122
123/// What a field holds.
124///
125/// One fact rather than the old split across an expected type and a separate
126/// child list, which had nowhere to put a list's element struct id and no way
127/// at all to describe a field whose element schema is chosen at run time.
128#[derive(Debug, Clone, Copy, PartialEq)]
129pub enum Shape {
130    /// A single value of this GFF type.
131    Scalar(GffType),
132    /// A nested struct with its own fields.
133    Struct {
134        /// The child schema, in parts.
135        ///
136        /// A sequence rather than one slice so a type can be its own fields
137        /// plus a flattened child's without copying either. Copying would make
138        /// each shared field two declarations where the audit records one.
139        fields: &'static [&'static [Field]],
140    },
141    /// A list of structs, all carrying `element`, some carrying more.
142    List {
143        /// What every element carries, whichever form it takes, in parts.
144        ///
145        /// Two lists whose elements differ by one label point the shared part
146        /// at one array and add their own: an entry node's links are
147        /// `RepliesList` and a reply node's are `EntriesList`, and the
148        /// twenty-eight they share stay twenty-eight declarations.
149        element: &'static [&'static [Field]],
150        /// The struct id vanilla writes on each element.
151        element_id: ElementId,
152        /// `None` for an ordinary list.
153        split: Option<Split>,
154    },
155}
156
157/// A list whose elements carry extra fields chosen by a sibling of the list.
158///
159/// An addition to a list rather than a kind of list, which is what the GIT
160/// case turned out to need. `UseTemplates` sits at the root beside every
161/// object list and decides, for all of them at once, whether an element points
162/// at a blueprint or carries the object whole. The selector is a sibling, so
163/// resolving a shape needs only the struct the list was found on, and its own
164/// entry says what its absence means rather than this restating it.
165///
166/// Two whole schemas was the first shape and it did not survive contact with
167/// the corpus: a `Creature List` element carries one label in the templated
168/// form only, 106 in the saved form only, and five in both. Those five would
169/// have been restated in each arm, with their own liveness and absent-value,
170/// on every object list using the shape. As an addition they are simply the
171/// element schema, declared once.
172#[derive(Debug, Clone, Copy, PartialEq)]
173pub struct Split {
174    /// Label of the sibling that decides which extras apply.
175    pub selector: GffLabel,
176    /// Extra fields where the selector is set, in parts.
177    ///
178    /// Parts rather than one slice, for the reason every other container
179    /// takes parts: an arm that flattens a block would otherwise lose it, and
180    /// GIT's saved arms flatten four apiece. The arm contributes its whole
181    /// schema here, so the element it shares with the other arm is held as a
182    /// plain member and written by the arm's own codec rather than flattened,
183    /// which is what keeps the common block one declaration.
184    pub when_set: &'static [&'static [Field]],
185    /// Extra fields where it is clear or absent, in parts.
186    pub when_clear: &'static [&'static [Field]],
187}
188
189/// The struct id the engine's own writer puts on a list's elements.
190///
191/// An enum rather than an optional number, because a positional id is a rule
192/// the engine follows rather than the absence of one. Spelling it as `None`
193/// would make absence carry an answer, which is the shape a whole liveness
194/// axis was rebuilt to stop, and it would leave a list whose id nobody
195/// established with no spelling of its own.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum ElementId {
198    /// Nobody has established what an element carries here.
199    ///
200    /// What an entry declaring a label and modelling no element behind it gets.
201    /// There is no element schema to hang an id on and this layer writes no
202    /// element there, so an id would be a claim about an untouched list.
203    ///
204    /// The default, so that an axis with no way of saying this cannot silently
205    /// answer [`Positional`] for every such entry. A modelled list has to
206    /// declare an id and the derive refuses it otherwise, so this variant
207    /// means the entry models nothing rather than that somebody forgot.
208    ///
209    /// [`Positional`]: Self::Positional
210    Unexamined,
211    /// Every element carries this id.
212    Fixed(i32),
213    /// Each element carries its own index in the list.
214    Positional,
215    /// The id is data: the engine reads the element's struct id off the header
216    /// rather than any field, so a writer has to preserve it and the model has
217    /// to carry it somewhere.
218    ///
219    /// `Equip_ItemList` is the case. Its id is the equipment slot, which no
220    /// label repeats, so an element written with the wrong one equips into the
221    /// wrong slot with nothing in the file to say so.
222    Meaningful,
223    /// A value every writer puts there and no loader tests.
224    ///
225    /// Distinct from [`Fixed`](Self::Fixed), which is what a loader *demands*.
226    /// Where nothing checks, two writers can disagree without either being
227    /// wrong, and a corpus that shows them disagreeing is reporting the engine
228    /// rather than a defect. Saying `Fixed` there would claim an enforcement
229    /// nothing performs.
230    ///
231    /// The reason is carried rather than described elsewhere, so a list cannot
232    /// be marked unenforced without saying what was checked.
233    Unenforced {
234        /// The value to write, since something has to be written.
235        writes: i32,
236        /// Why nothing rejects a file that writes something else. Reads on
237        /// from "nothing rejects a file writing something else, because".
238        why: &'static str,
239    },
240}
241
242/// Whether the engine reads a label at a path, and if not, why saying so helps.
243///
244/// A schema answers "what may a legitimate file contain", which is wider than
245/// "what does the engine read": labels the engine ignores still appear in
246/// files the toolset wrote, so leaving them out would make an
247/// unrecognized-field check fire once per shipped resource. Keeping them
248/// without marking them is not much better, because then a diagnostic can only
249/// say a field is unrecognized when the useful message is that setting it does
250/// nothing and here is what the engine reads instead.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum Life {
253    /// The engine reads this label at this path.
254    Live,
255    /// Legitimate file content the engine never reads, with why.
256    ///
257    /// The payload finishes the sentence "setting it has no effect, because
258    /// ...". A payload rather than an optional note, so no marking can exist
259    /// without one.
260    ReadOnlyDead(&'static str),
261    /// The engine writes it and never reads it back.
262    WriteOnlyDead(&'static str),
263    /// Nobody has looked.
264    ///
265    /// The default, and countable. This is the state the old axis lacked, and
266    /// lacking it is what let a thousand entries claim the engine reads a
267    /// field when all anyone knew was that the field existed.
268    Unexamined,
269}
270
271/// What the engine does when the label is missing.
272///
273/// The engine's finding and this crate's substitute are facts about different
274/// actors, and one slot cannot hold both without one standing in for the
275/// other. Splitting them is also what makes a substitute unrepresentable where
276/// the engine already has a value: [`Absent::Resolves`] has no room for one.
277#[derive(Debug, Clone, Copy, PartialEq)]
278pub enum Absent {
279    /// The engine resolves the missing label to a value.
280    ///
281    /// [`Taken`] says whether this crate's reader takes it. There is no slot
282    /// for a value of its own: a reader either takes the engine's or keeps the
283    /// absence, so a third answer stays unrepresentable.
284    Resolves(Resolution, Produced, Taken),
285    /// The engine's answer names no value, so the reader decides for itself.
286    Silent(Silence, Substitute),
287}
288
289/// What this crate's reader does with a value the engine resolves.
290///
291/// Holds no value of its own, which is what keeps the axis closed: a consumer
292/// can read that the reader declined the engine's answer, never an alternative
293/// number in its place.
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub enum Taken {
296    /// The reader hands back the engine's value.
297    Same,
298    /// The model holds the absence instead, because absence and the engine's
299    /// value are both real in the corpus and collapsing them would lose one.
300    PresenceKept,
301}
302
303/// How the engine's value for a missing label arises.
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum Resolution {
306    /// A literal applied at the read site, whatever the object already held.
307    Stamped,
308    /// The object's own constructed value, carried over because the read's
309    /// fallback argument is that member.
310    Constructed,
311}
312
313/// Why the engine's answer names no value.
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum Silence {
316    /// A mechanism rather than a constant: the value chains off a sibling, or
317    /// off whatever the object held, and no literal describes it.
318    NotAConstant,
319    /// The object is allocated without being zeroed and nothing assigns the
320    /// member, so an absent field leaves whatever the memory held.
321    Undefined,
322    /// Somebody looked and the page does not say.
323    Unanswered,
324    /// Nobody has looked.
325    Unexamined,
326}
327
328/// What this crate's reader hands back where the engine's answer names no
329/// value.
330///
331/// This crate's answer, not the engine's. Recording it stops a reader
332/// inventing a number in an `unwrap_or` beside an entry that says no value is
333/// known, which is how twelve non-obvious literals came to sit in readers with
334/// nothing recording them.
335#[derive(Debug, Clone, Copy, PartialEq)]
336pub enum Substitute {
337    /// The field's Rust type decided, so a rebuild regenerates it.
338    ///
339    /// Carries the value anyway. A consumer that can name the state but not
340    /// the value has only "the type decides" to print, which is a cell with
341    /// nothing in it.
342    TypeDefault(Produced),
343    /// A specific value this layer chose. Not an engine fact.
344    Ours(Produced),
345    /// There is nothing to substitute: the model holds the absence itself, so
346    /// a reader hands back "not there" rather than a stand-in.
347    NoValue,
348    /// The reader works it out from sibling fields, and this deliberately does
349    /// not model how.
350    ///
351    /// [`Silence::NotAConstant`] refuses the same thing on the engine's side,
352    /// for the same reason: encoding a derivation would put a small expression
353    /// language in a static table to serve a handful of fields. Three entries
354    /// need it. The derivation lives in the view's own reader, where it can be
355    /// read as code.
356    FromSiblings,
357}
358
359/// A value produced on demand, because a `const` cannot hold a `String`.
360///
361/// # Why this is a type rather than a bare `fn` pointer
362///
363/// Holding the default as a call is what keeps the schema a `const`, and the
364/// pointer that makes that work poisons every trait that reflects over it.
365/// A derived `Debug` prints an address, which changes between runs and says
366/// nothing about the entry; the extraction records values through `Debug`, so
367/// a raw pointer would put addresses in the audit file. A derived `PartialEq`
368/// compares addresses, and identical-code folding merges two closures with the
369/// same body only when optimising, so the answer depends on the build profile.
370///
371/// Fixing those one trait at a time lets the next one through. Wrapping the
372/// pointer once fixes the class: the containing types derive normally and are
373/// right by construction.
374#[derive(Clone, Copy)]
375pub struct Produced(fn() -> GffValue);
376
377impl Produced {
378    /// Wraps a producer. Takes a plain function pointer, so a non-capturing
379    /// closure coerces at the call site.
380    pub const fn new(make: fn() -> GffValue) -> Self {
381        Self(make)
382    }
383
384    /// Runs the producer. A text value allocates on each call.
385    pub fn get(self) -> GffValue {
386        (self.0)()
387    }
388}
389
390impl std::fmt::Debug for Produced {
391    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392        std::fmt::Debug::fmt(&self.get(), f)
393    }
394}
395
396impl PartialEq for Produced {
397    fn eq(&self, other: &Self) -> bool {
398        self.get() == other.get()
399    }
400}
401
402// `Hash` and `Ord` are left off deliberately. Deriving either on an entry then
403// fails to build rather than hashing or ordering an address, which turns the
404// next instance of this trap into a compile error instead of a wrong answer.
405
406/// A range the engine clamps a read value into.
407#[derive(Debug, Clone, Copy, PartialEq)]
408pub enum Constraint {
409    /// Inclusive integer bounds.
410    RangeInt(i64, i64),
411    /// Inclusive float bounds.
412    RangeFloat(f64, f64),
413}
414
415/// The engine's own writer leaves this label out at some value.
416///
417/// Carries the population it was measured against, because the conclusion
418/// without its denominator is an assertion rather than a measurement: a row
419/// means the label was looked for across that many files and appeared in none
420/// of them.
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct Omission {
423    /// The value at which the label is left out.
424    pub when: OmitWhen,
425    /// How many files it was looked for in.
426    pub files: usize,
427}
428
429/// The equality test an omission is decided by.
430#[derive(Debug, Clone, Copy, PartialEq, Eq)]
431pub enum OmitWhen {
432    /// The engine's own absent-value for this field.
433    AuditedConstant,
434    /// An empty container.
435    Empty,
436    /// Elements the writer leaves out, so what it produces is a shorter list
437    /// than the one it read.
438    ///
439    /// Distinct from [`Empty`](Self::Empty): whether to write the label at all
440    /// and which elements go in it are different questions. A faction table is
441    /// the case. `CFactionManager::SaveReputations` emits a `RepList` entry
442    /// only for a pair that is not at the friendly baseline, and a
443    /// toolset-authored `.fac` carries entries that are, so reading one and
444    /// writing it back produces fewer elements without losing anything: an
445    /// entry at the baseline overrides the baseline with itself.
446    ///
447    /// Carries the test in words rather than a predicate. The filter runs on
448    /// the typed element, where the value it tests is reachable, and nothing
449    /// here holds one.
450    Elements(&'static str),
451    /// The resolved value of the named sibling this field chains off.
452    Matches(GffLabel),
453}
454
455/// Associates a schema with the type it describes.
456///
457/// Lets lint and the docs build take a view generically without either naming
458/// the fourteen types, which is what stops a consumer growing its own list of
459/// them.
460pub trait HasSchema {
461    /// The root field schema for this resource type.
462    fn schema() -> &'static [Field];
463
464    /// The same schema in parts, own fields first.
465    ///
466    /// A type that flattens a shared block holds those fields in a later part
467    /// rather than in [`schema`](Self::schema), so a walk wanting everything
468    /// the type declares reads this instead. No view flattens at its root
469    /// today, which is why the two agree; one that did would leave a walk
470    /// built on `schema` quietly short of a block, and every nested descent
471    /// already goes through parts.
472    fn parts() -> &'static [&'static [Field]];
473}
474
475/// Reads a typed view out of a parsed container.
476///
477/// The same reason as [`HasSchema`], one step over. Each view's inherent
478/// `from_gff` carries its own error type, so a caller that does not already
479/// know which view it wants cannot call it, and
480/// [`GffDocument::view`](crate::GffDocument::view) is exactly that caller.
481///
482/// A view that cannot fail names [`Infallible`](std::convert::Infallible) here.
483///
484/// # Only a whole file implements this
485///
486/// [`HasSchema`] is derived onto every modelled struct, nested ones included,
487/// because a schema describes a struct at any depth. This is narrower on
488/// purpose: a container has a magic and a nested struct does not, so only the
489/// root of a format is here. That is what makes [`MAGIC`](Self::MAGIC) a
490/// question every implementor can answer.
491pub trait FromGff: Sized {
492    /// What reading this view out of a tree can fail with.
493    type Error;
494
495    /// The four bytes a container of this format carries.
496    ///
497    /// A GFF says what it is in its own first four bytes, so this is the file's
498    /// claim about itself rather than anything about where it was found: it
499    /// survives a rename, storage in an archive under a resref, and being
500    /// carried inside a save. Extensions do not, and neither does
501    /// [`ResourceType`](rakata_core::ResourceType), which has no entry at all
502    /// for the `.res` a save sidecar uses.
503    ///
504    /// On the trait so it is declared once and read wherever it is needed:
505    /// the view's own reader refuses a container carrying something else, and a
506    /// caller mapping a magic back to a schema looks it up. Two spellings of
507    /// one magic is how those come to disagree.
508    const MAGIC: [u8; 4];
509
510    /// Reads the view. Anything the view does not model stays in `gff`.
511    ///
512    /// # Errors
513    ///
514    /// [`Self::Error`], on terms the implementing view decides. Every view in
515    /// this workspace refuses a container whose file type is not
516    /// [`Self::MAGIC`], and otherwise reports only what the underlying GFF
517    /// read reports: a field of an unexpected type is taken where the type
518    /// tolerates it and dropped where it does not, rather than failing.
519    fn from_gff(gff: &Gff) -> Result<Self, Self::Error>;
520}
521
522mod route;
523pub use route::{routes_in, routes_under, FieldRoute, RouteError, RouteParseError, RouteStep};
524
525mod wire;
526
527/// The GFF wire types a scalar can be.
528///
529/// Emitted from the one declaration in [`gff`](crate::gff) that also produces
530/// the wire enum, the runtime value and the serde DTO.
531pub use wire::{gff_value_type, GffType};
532
533#[cfg(test)]
534mod tests;