Skip to main content

primase_core/
provenance.rs

1//! Provenance (`core.provenance`): where an entity's content came from, down to the primitive, and which passes
2//! derived it, so a glitch in an output traces back to the archive, the file and the part of the file it was read
3//! from (DESIGN.md ยง4: passes that merge entities merge their provenance).
4//!
5//! - An [`Origin`] is a place in the source data: a file, the archive it is in, and the item inside the file (a
6//!   FLVER mesh, a drawable's model and geometry, a texture's name), in the game's own naming, never a local path.
7//! - [`Provenance::origins`] lists what the entity was made from: one origin for content read as it is, several for a
8//!   merge (a material shared by many files, an image several textures encode to).
9//! - [`Provenance::parts`] gives, per primitive of the entity's `core.mesh`, the index of its own origin. A pass
10//!   that drops, splits or reorders primitives keeps it in step through `World::remap_primitives`, like every
11//!   per-primitive component (`Component::remap_primitives`); the verifier
12//!   refuses a list whose length differs from the mesh's primitive count, naming the pass that broke it.
13//! - [`Provenance::passes`] lists the passes that derived the entity (split, merged or baked it), as pipeline text,
14//!   in the order they ran. Passes that only edit in place are in the run report, not repeated here.
15//!
16//! With levels of detail (`core.lods`) each form carries its own provenance: `select-lod` moves the chosen form's
17//! components onto the prototype, so a form without one would leave the finest form's parts against a coarser mesh
18//! (which the verifier then refuses).
19//!
20//! `core.source` stays the reader's short key; this component is the structured trail.
21
22use std::collections::BTreeSet;
23use std::sync::Arc;
24
25use serde::ser::{Serialize, SerializeMap, Serializer};
26use serde_json::Value;
27
28use crate::components::Mesh;
29use crate::json::JsonError;
30use crate::world::{Component, Entity, World};
31
32/// A place in the source data. `file` is within `archive` (on disk when `archive` is `None`); `item` the part of the
33/// file the content is (`None`: the whole file).
34#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct Origin {
36    pub archive: Option<Arc<str>>,
37    pub file: Arc<str>,
38    pub item: Option<Arc<str>>,
39}
40
41impl Origin {
42    pub fn new(archive: Option<&str>, file: &str, item: Option<&str>) -> Self {
43        Origin {
44            archive: archive.map(Arc::from),
45            file: Arc::from(file),
46            item: item.map(Arc::from),
47        }
48    }
49
50    /// The same file, another item in it (shares the strings).
51    pub fn with_item(&self, item: &str) -> Self {
52        Origin {
53            item: Some(Arc::from(item)),
54            ..self.clone()
55        }
56    }
57}
58
59impl Serialize for Origin {
60    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
61        let n = 1 + usize::from(self.archive.is_some()) + usize::from(self.item.is_some());
62        let mut m = s.serialize_map(Some(n))?;
63        if let Some(a) = &self.archive {
64            m.serialize_entry("archive", &**a)?;
65        }
66        m.serialize_entry("file", &*self.file)?;
67        if let Some(i) = &self.item {
68            m.serialize_entry("item", &**i)?;
69        }
70        m.end()
71    }
72}
73
74/// `core.provenance`: see the module docs.
75#[derive(Clone, Debug, PartialEq, Eq, Default)]
76pub struct Provenance {
77    /// What the entity was made from, without repeats, in first-seen order; never empty.
78    pub origins: Vec<Origin>,
79    /// Per primitive of the entity's `core.mesh`, in draw order, the index in `origins` of its origin. Empty when
80    /// the entity has no mesh or the source does not say which part a primitive came from.
81    pub parts: Vec<u32>,
82    /// The passes that derived the entity, as pipeline text, in the order they ran.
83    pub passes: Vec<Arc<str>>,
84}
85
86impl Serialize for Provenance {
87    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
88        let n = 1 + usize::from(!self.parts.is_empty()) + usize::from(!self.passes.is_empty());
89        let mut m = s.serialize_map(Some(n))?;
90        m.serialize_entry("origins", &self.origins)?;
91        if !self.parts.is_empty() {
92            m.serialize_entry("parts", &self.parts)?;
93        }
94        if !self.passes.is_empty() {
95            let passes: Vec<&str> = self.passes.iter().map(|p| &**p).collect();
96            m.serialize_entry("passes", &passes)?;
97        }
98        m.end()
99    }
100}
101
102impl Component for Provenance {
103    const NAME: &'static str = "core.provenance";
104    const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
105    fn to_json(&self) -> Result<Option<Value>, JsonError> {
106        crate::json::to_value(self).map(Some)
107    }
108    /// Origins present and unrepeated; parts index them and, when the entity has a mesh, number its primitives.
109    /// The parts follow the rebuilt primitives; without parts the provenance holds nothing per primitive and stays.
110    fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
111        if self.parts.is_empty() {
112            return Some(Ok(self.clone()));
113        }
114        Some(crate::remap_list(&self.parts, from).map(|parts| Provenance {
115            parts,
116            ..self.clone()
117        }))
118    }
119    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
120        if self.origins.is_empty() {
121            return Err("no origins".into());
122        }
123        let mut seen = BTreeSet::new();
124        for (i, o) in self.origins.iter().enumerate() {
125            if !seen.insert(o) {
126                return Err(format!("origin {i} repeats an earlier one"));
127            }
128        }
129        if let Some(p) = self.parts.iter().find(|&&p| p as usize >= self.origins.len()) {
130            return Err(format!("part origin {p} of {} origins", self.origins.len()));
131        }
132        if !self.parts.is_empty()
133            && let Some(m) = w.get::<Mesh>(e)
134            && m.primitives().len() != self.parts.len()
135        {
136            return Err(format!(
137                "{} parts for {} primitives",
138                self.parts.len(),
139                m.primitives().len()
140            ));
141        }
142        Ok(())
143    }
144}
145
146impl Provenance {
147    /// Content read as it is from one place.
148    pub fn new(origin: Origin) -> Self {
149        Provenance {
150            origins: vec![origin],
151            ..Provenance::default()
152        }
153    }
154
155    /// The index of `origin`, added when new (a linear search: for a mesh's own few origins; [`Provenance::merge`]
156    /// takes in many at once).
157    pub fn intern(&mut self, origin: Origin) -> u32 {
158        match self.origins.iter().position(|o| *o == origin) {
159            Some(i) => i as u32,
160            None => {
161                self.origins.push(origin);
162                (self.origins.len() - 1) as u32
163            }
164        }
165    }
166
167    /// Another primitive, from `origin`.
168    pub fn push_part(&mut self, origin: Origin) {
169        let i = self.intern(origin);
170        self.parts.push(i);
171    }
172
173    /// Take in `other`'s origins and passes (a merge). Parts are not touched: they follow this entity's mesh.
174    pub fn merge(&mut self, other: &Provenance) {
175        let mut seen: BTreeSet<Origin> = self.origins.iter().cloned().collect();
176        for o in &other.origins {
177            if seen.insert(o.clone()) {
178                self.origins.push(o.clone());
179            }
180        }
181        for p in &other.passes {
182            if !self.passes.contains(p) {
183                self.passes.push(p.clone());
184            }
185        }
186    }
187
188    /// Record that `pass` (its pipeline text) derived the entity; not repeated when it is already the last.
189    pub fn derived_by(&mut self, pass: &str) {
190        if self.passes.last().map(|p| &**p) != Some(pass) {
191            self.passes.push(Arc::from(pass));
192        }
193    }
194}
195
196/// The provenance of an entity `pass` made from `from`: their origins and passes merged, `pass` appended. `None`
197/// when none of them has any (nothing to trace, nothing invented).
198pub fn derive(w: &World, from: &[Entity], pass: &str) -> Option<Provenance> {
199    let mut out: Option<Provenance> = None;
200    for &e in from {
201        if let Some(p) = w.get::<Provenance>(e) {
202            match &mut out {
203                Some(o) => o.merge(p),
204                None => {
205                    out = Some(Provenance {
206                        parts: Vec::new(),
207                        ..p.clone()
208                    })
209                }
210            }
211        }
212    }
213    let mut out = out?;
214    out.derived_by(pass);
215    Some(out)
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::components::{Primitive, Semantic, Stream, Topology};
222    use std::collections::BTreeMap;
223
224    fn prim() -> Primitive {
225        Primitive {
226            topology: Topology::Triangles,
227            streams: BTreeMap::from([(Semantic::Position, Stream::F32x3(vec![[0.; 3]; 3]))]),
228            indices: None,
229            material: None,
230        }
231    }
232
233    fn flver(mesh: u32) -> Origin {
234        Origin::new(
235            Some("map/m11/m11_000123.mapbnd"),
236            "m11_000123.flver",
237            Some(&format!("mesh {mesh}")),
238        )
239    }
240
241    #[test]
242    fn parts_follow_the_mesh_and_the_verifier_holds_them_to_it() {
243        let mut w = World::new();
244        let e = w.spawn();
245        w.insert(e, Mesh::new(vec![prim(), prim(), prim()]));
246        let mut p = Provenance::default();
247        for m in [0, 1, 1] {
248            p.push_part(flver(m));
249        }
250        assert_eq!((p.origins.len(), &p.parts[..]), (2, &[0, 1, 1][..]));
251        w.insert(e, p.clone());
252        assert_eq!(w.verify_components(), []);
253
254        // a pass drops primitive 0 and splits primitive 2 in two, forgetting provenance: the verifier names it
255        *w.get_mut::<Mesh>(e).unwrap() = Mesh::new(vec![prim(), prim(), prim(), prim()]);
256        let failures = w.verify_components();
257        assert_eq!(failures.len(), 1);
258        assert_eq!(failures[0].2, "3 parts for 4 primitives");
259        // remapping with the edit keeps it in step; an index it lacks is refused and changes nothing
260        assert_eq!(
261            w.remap_primitives(e, &[4]).unwrap_err(),
262            "core.provenance: primitive 4 of 3"
263        );
264        w.remap_primitives(e, &[1, 2, 2, 2]).unwrap();
265        assert_eq!(w.get::<Provenance>(e).unwrap().parts, [1, 1, 1, 1]);
266        assert_eq!(w.verify_components(), []);
267        let mut p = w.get::<Provenance>(e).unwrap().clone();
268        p.derived_by("split{max=2}");
269        // provenance without parts holds nothing per primitive: it answers a rebuild with itself
270        let whole = Provenance::new(flver(0));
271        assert_eq!(whole.remap_primitives(&[0, 0]), Some(Ok(whole.clone())));
272
273        // indices out of range, repeated or missing origins are refused
274        for bad in [
275            Provenance {
276                parts: vec![0, 0, 0, 2],
277                ..p.clone()
278            },
279            Provenance {
280                origins: vec![flver(0), flver(0)],
281                parts: vec![],
282                passes: vec![],
283            },
284            Provenance::default(),
285        ] {
286            w.insert(e, bad);
287            assert_eq!(w.verify_components().len(), 1);
288        }
289    }
290
291    #[test]
292    fn derive_merges_origins_and_appends_the_pass() {
293        let mut w = World::new();
294        let a = w.spawn();
295        let b = w.spawn();
296        let c = w.spawn();
297        let mut pa = Provenance::new(flver(0));
298        pa.derived_by("load-textures");
299        w.insert(a, pa);
300        w.insert(b, Provenance::new(flver(1)));
301        let d = derive(&w, &[a, b, c, a], "bake{size=512}").unwrap();
302        assert_eq!(d.origins, [flver(0), flver(1)]);
303        assert_eq!(d.parts, Vec::<u32>::new());
304        assert_eq!(
305            d.passes.iter().map(|p| &**p).collect::<Vec<_>>(),
306            ["load-textures", "bake{size=512}"]
307        );
308        assert_eq!(derive(&w, &[c], "bake"), None);
309        assert_eq!(
310            d.to_json().unwrap().unwrap().to_string(),
311            r#"{"origins":[{"archive":"map/m11/m11_000123.mapbnd","file":"m11_000123.flver","item":"mesh 0"},{"archive":"map/m11/m11_000123.mapbnd","file":"m11_000123.flver","item":"mesh 1"}],"passes":["load-textures","bake{size=512}"]}"#
312        );
313        let bare = Provenance::new(Origin::new(None, "a.ytd", None));
314        assert_eq!(
315            bare.to_json().unwrap().unwrap().to_string(),
316            r#"{"origins":[{"file":"a.ytd"}]}"#
317        );
318    }
319}