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;