Skip to main content

primase_core/
state.rs

1//! World state: variables a level can be in (a game's event flags, a door's open state, a level's instance group, an
2//! object's active flag) and what depends on them, resolved to one concrete world by [`ResolveState`].
3//!
4//! - An **axis** ([`StateAxis`]) is an entity: a stable `key`, a small domain (bool, or up to 64 named values), a
5//!   default where the source states one (none where a script the reader doesn't read sets it), a scope (the whole
6//!   world, or one entity such as a door) and a class tools act on (`door`, `ipl`, ...).
7//!   Axes are variables, so independent toggles never multiply into enumerated variants.
8//! - A **condition** ([`Condition`]) on a placed entity, on a prototype or on a part or alternative of an asset
9//!   (`core.asset`: an outfit's pieces), says when it exists: an OR of clauses, each an AND of literals "axis is one of these values"
10//!   (a value mask). No condition: always present. A [`PrimitiveCondition`] says the same of each primitive of one
11//!   mesh (a hat drawn as one primitive of the body's mesh), and follows the primitives through every edit
12//!   ([`Component::remap_primitives`]).
13//! - An **override** ([`StateOverride`]) on a placed entity changes its transform, material variant or prototype in
14//!   the states its clause matches. Poses (a door's open animation) stay in the dialect that owns the clip: its own pass
15//!   lowers them into these overrides first.
16//! - Values come from [`ResolveState`]'s `assign` option, else the [`StateAssignment`] resource (a save snapshot), else
17//!   each axis's default. An axis with none of them (its default unknown, nothing assigned) stays unresolved, with
18//!   what depends on it: a value the source doesn't state is not chosen for the caller.
19//!
20//! Writers that cannot toggle declare `core.condition` and `core.state_override` illegal: a program that writes to one
21//! states `resolve-state` before it. A world without state components is never touched.
22
23use std::collections::{BTreeMap, BTreeSet};
24
25use serde::Serialize;
26use serde_json::Value;
27
28use crate::asset::{Asset, Role};
29use crate::clip::ClipBinding;
30use crate::components::{Instance, Mesh, Transform};
31use crate::json::JsonError;
32use crate::ledger::Measure;
33use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
34use crate::world::{Component, Entity, Resource, World};
35
36/// The values an axis can take.
37#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
38pub enum Domain {
39    /// `off` (0) and `on` (1).
40    Bool,
41    /// Named values, 1..=64 of them, by index.
42    Enum(Vec<String>),
43}
44
45impl Domain {
46    /// The number of values.
47    pub fn len(&self) -> usize {
48        match self {
49            Domain::Bool => 2,
50            Domain::Enum(v) => v.len(),
51        }
52    }
53
54    pub fn is_empty(&self) -> bool {
55        self.len() == 0
56    }
57
58    /// The name of value `i` (`off`/`on` for a bool).
59    pub fn name(&self, i: u32) -> Option<&str> {
60        match self {
61            Domain::Bool => ["off", "on"].get(i as usize).copied(),
62            Domain::Enum(v) => v.get(i as usize).map(String::as_str),
63        }
64    }
65
66    /// A value from its name or its index.
67    pub fn parse(&self, text: &str) -> Option<u32> {
68        let n = self.len() as u32;
69        if let Ok(i) = text.parse::<u32>() {
70            return (i < n).then_some(i);
71        }
72        (0..n).find(|&i| self.name(i) == Some(text))
73    }
74
75    /// The mask of every value.
76    fn full_mask(&self) -> u64 {
77        // a domain of more than 64 values is a verifier's error ([`StateAxis`]), not a shift past the word
78        match self.len() {
79            n @ 0..=63 => (1u64 << n) - 1,
80            _ => u64::MAX,
81        }
82    }
83}
84
85/// Whether an axis belongs to the whole world or to one entity (a door's own open state).
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
87pub enum Scope {
88    Global,
89    Local(Entity),
90}
91
92/// A state variable, on its own entity.
93#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
94pub struct StateAxis {
95    /// Stable and unique in the world (`<dialect>.flag.8302`, `<dialect>.ipl.<name>`): assignments, reports and
96    /// the text form name axes by it.
97    pub key: String,
98    pub domain: Domain,
99    /// The value without an assignment: the game's own start state, where the source states it (an authored flag, a
100    /// stored active state). `None`: unknown (a script decides it, and the reader doesn't read that script);
101    /// [`ResolveState`] leaves the axis unresolved unless it is assigned.
102    pub default: Option<u32>,
103    pub scope: Scope,
104    /// What kind of state it is (`event`, `door`, `ipl`, `entity_set`, `active`, `time`, ...), so tools can act on a
105    /// class ("open every door").
106    pub class: String,
107    /// Where it came from (`event flag 8302`, an IPL name).
108    pub source: String,
109}
110
111impl Component for StateAxis {
112    const NAME: &'static str = "core.state_axis";
113    const GEOMETRY: crate::Geometry = crate::Geometry::None;
114    fn to_json(&self) -> Result<Option<Value>, JsonError> {
115        crate::json::to_value(self).map(Some)
116    }
117    fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
118        if self.key.is_empty() || self.key.contains([':', ';']) {
119            return Err(format!(
120                "key {:?} is empty or holds ':' or ';' (the assign syntax)",
121                self.key
122            ));
123        }
124        let n = self.domain.len();
125        if !(1..=64).contains(&n) {
126            return Err(format!("{}: {n} values, the domain holds 1..=64", self.key));
127        }
128        if let Domain::Enum(names) = &self.domain {
129            for (i, a) in names.iter().enumerate() {
130                if a.is_empty() || a.contains([':', ';']) || names[..i].contains(a) {
131                    return Err(format!(
132                        "{}: value name {a:?} empty, repeated or holding ':' or ';'",
133                        self.key
134                    ));
135                }
136            }
137        }
138        if let Some(d) = self.default
139            && d as usize >= n
140        {
141            return Err(format!("{}: default {d} outside its {n} values", self.key));
142        }
143        // a local axis's owner is a reference: the world checks that it is alive
144        Ok(())
145    }
146    fn references(&self, out: &mut Vec<Entity>) {
147        if let Scope::Local(owner) = self.scope {
148            out.push(owner);
149        }
150    }
151}
152
153/// "`axis` has one of the values in `values`" (bit `i` set: value `i`).
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
155pub struct Literal {
156    pub axis: Entity,
157    pub values: u64,
158}
159
160/// The values `resolve-state` gave the axes it resolved, for a component that holds [`Clause`]s of its own
161/// ([`crate::world::Component::restate`]); an axis it kept or could not resolve has no value here.
162#[derive(Clone, Copy, Debug)]
163pub struct Resolved<'a>(&'a BTreeMap<Entity, u32>);
164
165impl Resolved<'_> {
166    /// `clause` with its literals on resolved axes evaluated, as for a `core.condition`: `None` when one of them fails,
167    /// else the literals on the other axes (empty: the clause holds whatever they are).
168    pub fn reduce(&self, clause: &Clause) -> Option<Clause> {
169        clause.reduce(self.0)
170    }
171    /// Whether `axis` was resolved.
172    pub fn has(&self, axis: Entity) -> bool {
173        self.0.contains_key(&axis)
174    }
175}
176
177/// An AND of literals; an empty clause holds in every state.
178#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
179pub struct Clause(pub Vec<Literal>);
180
181impl Clause {
182    /// The clause with its literals on the axes in `values` evaluated: `None` when one of them fails, else the
183    /// literals on the other axes (empty: the clause holds whatever they are).
184    fn reduce(&self, values: &BTreeMap<Entity, u32>) -> Option<Clause> {
185        let mut rest = Vec::new();
186        for l in &self.0 {
187            match values.get(&l.axis) {
188                // a value past the mask's 64 bits is in no literal's set
189                Some(&v) if 1u64.checked_shl(v).is_none_or(|bit| l.values & bit == 0) => return None,
190                Some(_) => {}
191                None => rest.push(*l),
192            }
193        }
194        Some(Clause(rest))
195    }
196
197    fn verify(&self, w: &World) -> Result<(), String> {
198        for l in &self.0 {
199            let axis = w
200                .get::<StateAxis>(l.axis)
201                .ok_or_else(|| format!("literal on {:?}, which is not a live axis", l.axis))?;
202            if l.values == 0 {
203                return Err(format!("{}: empty value mask", axis.key));
204            }
205            if l.values & !axis.domain.full_mask() != 0 {
206                return Err(format!(
207                    "{}: mask {:#x} has values beyond its {}",
208                    axis.key,
209                    l.values,
210                    axis.domain.len()
211                ));
212            }
213        }
214        Ok(())
215    }
216
217    fn axes(&self, out: &mut Vec<Entity>) {
218        out.extend(self.0.iter().map(|l| l.axis));
219    }
220}
221
222fn placed(e: Entity, w: &World) -> Result<(), String> {
223    if w.has::<Transform>(e) {
224        Ok(())
225    } else {
226        Err("on an entity without a transform: only placed things carry state".into())
227    }
228}
229
230/// Whether `e` is a prototype: it has a mesh (loaded or still in its file), so placements of it may share it.
231fn is_prototype(e: Entity, w: &World) -> bool {
232    w.has::<Mesh>(e) || w.has::<crate::MeshFile>(e)
233}
234
235/// Whether `e` is a part or an alternative of some asset: a member whose presence a choice decides.
236fn is_choosable_member(e: Entity, w: &World) -> bool {
237    w.iter::<Asset>().any(|(_, a)| {
238        a.members
239            .iter()
240            .any(|&(m, r)| m == e && matches!(r, Role::Part | Role::Alternative))
241    })
242}
243
244/// When a placed entity, a prototype (an entity with a mesh, which placements share) or a part or alternative member
245/// of an asset exists: an OR of clauses. Zero clauses: never (resolve-state counts these).
246#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
247pub struct Condition(pub Vec<Clause>);
248
249impl Component for Condition {
250    const NAME: &'static str = "core.condition";
251    const GEOMETRY: crate::Geometry = crate::Geometry::None;
252    /// No local-space geometry.
253    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
254        crate::world::Linearity::Invariant
255    }
256    fn to_json(&self) -> Result<Option<Value>, JsonError> {
257        crate::json::to_value(self).map(Some)
258    }
259    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
260        if !is_prototype(e, w) && !is_choosable_member(e, w) {
261            placed(e, w).map_err(|_| {
262                "on an entity without a transform that is neither a prototype (a mesh) nor a part or alternative of an asset: only placed things, prototypes and chosen members carry state"
263                    .to_owned()
264            })?;
265        }
266        self.0.iter().try_for_each(|c| c.verify(w))
267    }
268    fn references(&self, out: &mut Vec<Entity>) {
269        self.0.iter().for_each(|c| c.axes(out));
270    }
271}
272
273/// When each primitive of the entity's `core.mesh` is drawn: one entry per primitive, `None` for one drawn in every
274/// state. A primitive whose condition fails in the resolved state is dropped from the mesh (declared as a drop); the
275/// condition follows its primitive through every edit that splits, drops or reorders primitives, as every
276/// per-primitive component does.
277#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
278pub struct PrimitiveCondition(pub Vec<Option<Condition>>);
279
280impl Component for PrimitiveCondition {
281    const NAME: &'static str = "core.primitive_condition";
282    const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
283    /// No local-space geometry.
284    fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
285        crate::world::Linearity::Invariant
286    }
287    fn to_json(&self) -> Result<Option<Value>, JsonError> {
288        crate::json::to_value(self).map(Some)
289    }
290    fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
291        Some(crate::remap_list(&self.0, from).map(PrimitiveCondition))
292    }
293    /// One entry per primitive of the entity's mesh, which it has; every literal names a live axis.
294    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
295        let mesh = w.get::<Mesh>(e).ok_or("on an entity without a core.mesh")?;
296        if mesh.primitives().len() != self.0.len() {
297            return Err(format!(
298                "{} conditions for {} primitives",
299                self.0.len(),
300                mesh.primitives().len()
301            ));
302        }
303        self.0
304            .iter()
305            .flatten()
306            .flat_map(|c| c.0.iter())
307            .try_for_each(|c| c.verify(w))
308    }
309    fn references(&self, out: &mut Vec<Entity>) {
310        self.0
311            .iter()
312            .flatten()
313            .for_each(|c| c.0.iter().for_each(|cl| cl.axes(out)));
314    }
315}
316
317/// What an override changes.
318#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
319pub enum Override {
320    Transform(Transform),
321    /// `Instance::variant`, against the prototype's material variants.
322    Variant(u32),
323    /// `Instance::prototype`.
324    Prototype(Entity),
325}
326
327impl Override {
328    fn kind(&self) -> &'static str {
329        match self {
330            Override::Transform(_) => "transform",
331            Override::Variant(_) => "variant",
332            Override::Prototype(_) => "prototype",
333        }
334    }
335}
336
337/// Changes to a placed entity by state, in order: the first clause that holds applies; none: unchanged.
338#[derive(Clone, Debug, Default, PartialEq, Serialize)]
339pub struct StateOverride(pub Vec<(Clause, Override)>);
340
341impl Component for StateOverride {
342    const NAME: &'static str = "core.state_override";
343    const GEOMETRY: crate::Geometry = crate::Geometry::None;
344    fn to_json(&self) -> Result<Option<Value>, JsonError> {
345        crate::json::to_value(self).map(Some)
346    }
347    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
348        placed(e, w)?;
349        for (c, o) in &self.0 {
350            c.verify(w)?;
351            match o {
352                Override::Transform(t) => t.verify(e, w)?,
353                Override::Variant(_) | Override::Prototype(_) if !w.has::<Instance>(e) => {
354                    return Err(format!("{} override on an entity without an instance", o.kind()));
355                }
356                _ => {}
357            }
358        }
359        Ok(())
360    }
361    fn references(&self, out: &mut Vec<Entity>) {
362        for (c, o) in &self.0 {
363            c.axes(out);
364            if let Override::Prototype(p) = o {
365                out.push(*p);
366            }
367        }
368    }
369}
370
371/// Values for axes by key (a save snapshot): above each axis's default, below `resolve-state`'s `assign` option.
372#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
373pub struct StateAssignment(pub BTreeMap<String, u32>);
374
375impl Resource for StateAssignment {
376    const NAME: &'static str = "core.state_assignment";
377    fn to_json(&self) -> Result<Option<Value>, JsonError> {
378        crate::json::to_value(self).map(Some)
379    }
380}
381
382/// What resolving state removes: the model's components and the assignment resource.
383const REMOVED: &[&str] = &[
384    StateAxis::NAME,
385    Condition::NAME,
386    PrimitiveCondition::NAME,
387    StateOverride::NAME,
388    StateAssignment::NAME,
389];
390
391/// What resolving state changes in what stays: placements, their prototypes, meshes that lose primitives, assets that
392/// lose members.
393const MODIFIED: &[&str] = &[
394    Transform::NAME,
395    Instance::NAME,
396    Mesh::NAME,
397    Asset::NAME,
398    ClipBinding::NAME,
399];
400
401/// A condition with its literals on resolved axes evaluated.
402enum Reduced {
403    /// A clause holds whatever the unresolved axes are: no condition is left.
404    Always,
405    /// No clause can hold.
406    Never,
407    /// The clauses that may still hold, reduced to their literals on unresolved axes.
408    Some(Vec<Clause>),
409}
410
411fn reduce_condition(c: &Condition, values: &BTreeMap<Entity, u32>) -> Reduced {
412    let reduced: Vec<Clause> = c.0.iter().filter_map(|cl| cl.reduce(values)).collect();
413    if reduced.iter().any(|cl| cl.0.is_empty()) {
414        Reduced::Always
415    } else if reduced.is_empty() {
416        Reduced::Never
417    } else {
418        Reduced::Some(reduced)
419    }
420}
421
422/// Take `e`, which is not present in the resolved state, out of the world. A part or alternative of an asset also
423/// leaves its asset's member list, and the placements of a prototype go with it (counted): a prototype nothing draws
424/// any more leaves nothing referring to it. Any other entity is despawned as it is, and what still references it is
425/// left to the verifier.
426fn remove_not_present(w: &mut World, e: Entity, log: &mut PassLog, why: &str) {
427    let member = is_choosable_member(e, w);
428    if member || (is_prototype(e, w) && !w.has::<Transform>(e)) {
429        let roots: Vec<Entity> = w
430            .iter::<Asset>()
431            .filter(|(_, a)| a.members.iter().any(|&(m, _)| m == e))
432            .map(|(r, _)| r)
433            .collect();
434        for r in roots {
435            if let Some(a) = w.get_mut::<Asset>(r) {
436                a.members.retain(|&(m, _)| m != e);
437            }
438        }
439        let placements: Vec<Entity> = w
440            .iter::<Instance>()
441            .filter(|(p, i)| i.prototype == e && *p != e)
442            .map(|(p, _)| p)
443            .collect();
444        for p in placements {
445            crate::ledger::despawn(w, p, log, "its prototype is not present in the resolved state");
446            log.count("placements of an absent member, despawned", 1);
447        }
448        if member {
449            log.count("asset members despawned", 1);
450        }
451    }
452    crate::ledger::despawn(w, e, log, why);
453}
454
455/// `resolve-state{assign=key:value;key:value}`: one concrete world, as far as the values are known. Each axis takes
456/// its `assign` value, else the [`StateAssignment`] resource's, else its class's `assign-class` value
457/// (`assign-class=class:value;...`: a program's choice for every axis of a kind, a level of detail where the source
458/// picks by screen size), else its default (values by name or index). Keys
459/// without an axis, in `assign`, `keep` or the resource, are counted by key and ignored: a program states its choices
460/// once for every world it converts, and a region, level or save holds axes for some of them only. A value outside an
461/// axis's domain is an error. Placed entities whose condition does not hold are despawned, then each remaining
462/// entity's first matching override applies. Every resolved axis, every condition and override on resolved axes only,
463/// and the assignment resource are removed. A despawned entity that something else still references (a self-placed
464/// mesh used as another instance's prototype) leaves that reference dangling, and the verifier after the pass fails on
465/// it.
466///
467/// An item the pipeline text form cannot carry back ([`crate::pass::carries`]: a ',' at the end, or one before an '=') is
468/// refused by name, so its text reads back as the same options.
469///
470/// `keep=key;key` leaves those axes unresolved, for a consumer that switches them itself (a time of day). An axis
471/// whose default is unknown and that nothing assigns stays unresolved the same way (counted, `axis <key> unresolved`):
472/// choosing a value for it would assert a state the source doesn't state. An unresolved axis stays, and so do the
473/// conditions and overrides that depend on it, reduced to their literals on unresolved axes (every literal on a
474/// resolved axis evaluated: a clause one of them fails is dropped, a clause left with no literal holds in every
475/// state). An entity whose condition keeps no clause is despawned, one with a clause that always holds loses its
476/// condition; an override list keeps its order up to the first clause that always holds, which applies now when it is
477/// the first. The snapshot's ([`StateAssignment`]) values for kept axes stay, as the resource's only entries. A key
478/// both kept and assigned is an error, and so is an axis scoped to an entity (which a resolved condition may despawn)
479/// that would stay unresolved: only global axes stay.
480///
481/// A part or alternative of an asset whose condition fails leaves the asset's member list and is despawned with the
482/// placements of it (`asset members despawned`, `placements of an absent member, despawned`). A primitive whose
483/// [`PrimitiveCondition`] fails is dropped from its mesh, every per-primitive component following (declared as a
484/// primitive drop; counted per axis as `<axis>=<value> primitives dropped`); a mesh left with no primitive is
485/// not present: its entity is removed as a member would be.
486#[derive(Clone, Debug, Default)]
487pub struct ResolveState {
488    /// key -> value text, as given.
489    pub assign: BTreeMap<String, String>,
490    /// class -> value text, for every axis of that class nothing else gives a value.
491    pub assign_class: BTreeMap<String, String>,
492    /// The keys of the axes left unresolved.
493    pub keep: BTreeSet<String>,
494}
495
496/// `resolve-state`'s declaration ([`crate::pass::PassRegistry::add`]).
497pub const RESOLVE_STATE: PassSpec = PassSpec {
498    name: "resolve-state",
499    options: &[
500        OptionSpec::optional(
501            "assign",
502            "time:noon",
503            "key:value items, `;` between: each axis's value",
504            "no axis assigned by its key: each takes its class's assignment, else is kept, else resolves by its default",
505        ),
506        OptionSpec::optional(
507            "assign-class",
508            "event:off",
509            "class:value items, `;` between: the value of every axis of that class nothing else gives one",
510            "no class assigned",
511        ),
512        OptionSpec::optional(
513            "keep",
514            "time",
515            "axis keys, `;` between: the axes left unresolved",
516            "every axis resolved",
517        ),
518    ],
519    make: resolve_state,
520};
521
522fn resolve_state(g: &Given) -> Result<Box<dyn Pass>, String> {
523    let items = |v: Option<&str>| -> Vec<String> {
524        v.unwrap_or_default()
525            .split(';')
526            .map(str::trim)
527            .filter(|s| !s.is_empty())
528            .map(str::to_owned)
529            .collect()
530    };
531    // `key:value` items into a map, each key once
532    let pairs = |option: &str, what: &str| -> Result<BTreeMap<String, String>, String> {
533        let mut out = BTreeMap::new();
534        for item in items(g.optional(option)) {
535            let (key, value) = item
536                .split_once(':')
537                .ok_or_else(|| format!("{option} item {item:?} is not {what}"))?;
538            if out
539                .insert(key.trim().to_owned(), value.trim().to_owned())
540                .is_some()
541            {
542                return Err(format!("{option}: {} twice", key.trim()));
543            }
544        }
545        Ok(out)
546    };
547    let assign = pairs("assign", "key:value")?;
548    let assign_class = pairs("assign-class", "class:value")?;
549    let mut keep = BTreeSet::new();
550    for key in items(g.optional("keep")) {
551        if !keep.insert(key.clone()) {
552            return Err(format!("keep: {key} twice"));
553        }
554    }
555    if let Some(k) = keep.iter().find(|k| assign.contains_key(*k)) {
556        return Err(format!("{k} is both assigned and kept"));
557    }
558    let pass = ResolveState {
559        assign,
560        assign_class,
561        keep,
562    };
563    // its text form must read back as it: the first item (in the order it is written) whose option the pipeline text
564    // cannot carry (a value ending in ',', a key or value with '=' after a comma) is refused by name
565    for (option, value) in pass.options() {
566        if crate::pass::carries(&value) {
567            continue;
568        }
569        let items: Vec<&str> = value.split(';').collect();
570        let item = (1..=items.len())
571            .find(|&n| !crate::pass::carries(&items[..n].join(";")))
572            .map_or(value.as_str(), |n| items[n - 1]);
573        return Err(format!(
574            "{option} item {item:?}: the pipeline text cannot carry it (a ',' at its end or before '=', or a brace)"
575        ));
576    }
577    Ok(Box::new(pass))
578}
579
580impl Pass for ResolveState {
581    fn name(&self) -> &'static str {
582        "resolve-state"
583    }
584    fn options(&self) -> Options {
585        let mut o = Options::new();
586        if !self.assign.is_empty() {
587            let items: Vec<String> = self.assign.iter().map(|(k, v)| format!("{k}:{v}")).collect();
588            o.insert("assign".to_owned(), items.join(";"));
589        }
590        if !self.assign_class.is_empty() {
591            let items: Vec<String> = self
592                .assign_class
593                .iter()
594                .map(|(k, v)| format!("{k}:{v}"))
595                .collect();
596            o.insert("assign-class".to_owned(), items.join(";"));
597        }
598        if !self.keep.is_empty() {
599            let keys: Vec<&str> = self.keep.iter().map(String::as_str).collect();
600            o.insert("keep".to_owned(), keys.join(";"));
601        }
602        o
603    }
604    /// The state model's components, and the `core.state_assignment` resource it applied.
605    fn removes(&self) -> &[&'static str] {
606        REMOVED
607    }
608    fn modifies(&self) -> &[&'static str] {
609        MODIFIED
610    }
611    /// Entities not present in the resolved state are despawned (declared as drops).
612    fn despawns_entities(&self) -> bool {
613        true
614    }
615    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
616        // the value of every axis: assign option > assignment resource > its class's assign-class > default
617        let axes: Vec<(Entity, StateAxis)> = w.iter::<StateAxis>().map(|(e, a)| (e, a.clone())).collect();
618        let by_key: BTreeMap<&str, (Entity, &StateAxis)> =
619            axes.iter().map(|(e, a)| (a.key.as_str(), (*e, a))).collect();
620        if by_key.len() != axes.len() {
621            return Err("two axes share a key".into());
622        }
623        let mut values: BTreeMap<Entity, u32> = axes
624            .iter()
625            .filter_map(|(e, a)| a.default.map(|d| (*e, d)))
626            .collect();
627        // the axes the snapshot or a key assignment set: a class assignment doesn't reach them
628        let mut explicit = BTreeSet::new();
629        // a save holds every flag the game knows, a world has axes for a few: keys without an axis are counted
630        let resource = w.remove_resource::<StateAssignment>().unwrap_or_default();
631        for (k, &v) in &resource.0 {
632            let Some(&(e, a)) = by_key.get(k.as_str()) else {
633                log.count("assignment keys without an axis", 1);
634                continue;
635            };
636            if v as usize >= a.domain.len() {
637                return Err(format!(
638                    "state assignment {k}={v} is outside its {} values",
639                    a.domain.len()
640                ));
641            }
642            values.insert(e, v);
643            explicit.insert(e);
644        }
645        // a program's choices name axes some of its worlds lack: counted by key
646        for (k, text) in &self.assign {
647            let Some(&(e, a)) = by_key.get(k.as_str()) else {
648                log.count(&format!("assigned {k}, which no axis has: ignored"), 1);
649                continue;
650            };
651            let v = a
652                .domain
653                .parse(text)
654                .ok_or_else(|| format!("assign {k}:{text} is not one of its values"))?;
655            values.insert(e, v);
656            explicit.insert(e);
657        }
658        // the kept axes: no value, their literals stay
659        let mut kept = BTreeSet::new();
660        // the snapshot's values for kept axes stay with them, for whoever resolves them later
661        let mut kept_assignment = StateAssignment::default();
662        for k in &self.keep {
663            let Some(&(e, a)) = by_key.get(k.as_str()) else {
664                log.count(&format!("kept {k}, which no axis has: ignored"), 1);
665                continue;
666            };
667            // a resolved condition may despawn the entity a local axis is scoped to
668            if let Scope::Local(_) = a.scope {
669                return Err(format!(
670                    "keep names {k}, which is scoped to an entity: only global axes are kept"
671                ));
672            }
673            values.remove(&e);
674            kept.insert(e);
675            if let Some(&v) = resource.0.get(k) {
676                kept_assignment.0.insert(k.clone(), v);
677            }
678            log.count(&format!("axis {k} kept unresolved"), 1);
679        }
680        // a program's choice for a kind of axis, over the source's default only: the value is read against each axis
681        // it falls to, not those a key assignment, the snapshot or keep settled
682        for (class, text) in &self.assign_class {
683            let mut any = false;
684            for (e, a) in axes
685                .iter()
686                .filter(|(e, a)| &a.class == class && !explicit.contains(e) && !kept.contains(e))
687            {
688                let v = a
689                    .domain
690                    .parse(text)
691                    .ok_or_else(|| format!("assign-class {class}:{text} is not a value of axis {}", a.key))?;
692                values.insert(*e, v);
693                any = true;
694            }
695            if any {
696                log.count(&format!("axes of class {class} assigned {text}"), 1);
697            } else {
698                log.count(
699                    &format!("assigned class {class}, which no axis it falls to has: ignored"),
700                    1,
701                );
702            }
703        }
704        // the axes nothing gave a value (their default unknown) stay as the kept ones do
705        let mut unresolved = BTreeSet::new();
706        for (e, a) in &axes {
707            if values.contains_key(e) || kept.contains(e) {
708                continue;
709            }
710            if let Scope::Local(_) = a.scope {
711                return Err(format!(
712                    "{}: no known default and no value, and scoped to an entity (only global axes stay unresolved): \
713                     assign it",
714                    a.key
715                ));
716            }
717            unresolved.insert(*e);
718            log.count(
719                &format!("axis {} unresolved: no known default, not assigned", a.key),
720                1,
721            );
722        }
723        kept.extend(unresolved.iter().copied());
724        if !kept_assignment.0.is_empty() {
725            log.count(
726                "snapshot values kept for kept axes",
727                kept_assignment.0.len() as u64,
728            );
729            w.insert_resource(kept_assignment);
730        }
731        // run() may see an unverified world: every literal must name an axis
732        let named_by = |e: Entity, clauses: &mut dyn Iterator<Item = &Clause>| {
733            clauses
734                .flat_map(|c| c.0.iter().map(|l| l.axis))
735                .find(|a| !values.contains_key(a) && !kept.contains(a))
736                .map_or(Ok(()), |a| {
737                    Err(format!("{e:?}: a literal names {a:?}, which is not an axis"))
738                })
739        };
740        for (e, c) in w.iter::<Condition>() {
741            named_by(e, &mut c.0.iter())?;
742        }
743        for (e, o) in w.iter::<StateOverride>() {
744            named_by(e, &mut o.0.iter().map(|(c, _)| c))?;
745        }
746        for (e, pc) in w.iter::<PrimitiveCondition>() {
747            named_by(e, &mut pc.0.iter().flatten().flat_map(|c| c.0.iter()))?;
748        }
749        let axis_of: BTreeMap<Entity, &StateAxis> = axes.iter().map(|(e, a)| (*e, a)).collect();
750        let label = |e: &Entity| {
751            let a = axis_of[e];
752            match values.get(e) {
753                Some(&v) => format!("{}={}", a.key, a.domain.name(v).unwrap_or("?")),
754                None if unresolved.contains(e) => format!("{} (unresolved)", a.key),
755                None => format!("{} (kept)", a.key),
756            }
757        };
758
759        // conditions: despawn what does not exist in this state
760        let conditions: Vec<(Entity, Condition)> =
761            w.iter::<Condition>().map(|(e, c)| (e, c.clone())).collect();
762        for (e, c) in &conditions {
763            let reduced = reduce_condition(c, &values);
764            let present = !matches!(reduced, Reduced::Never);
765            if c.0.is_empty() {
766                log.count("never present", 1);
767            }
768            let mut mentioned = Vec::new();
769            c.references(&mut mentioned);
770            mentioned.sort();
771            mentioned.dedup();
772            for a in &mentioned {
773                log.count(
774                    &format!("{} {}", label(a), if present { "kept" } else { "despawned" }),
775                    1,
776                );
777            }
778            match reduced {
779                Reduced::Always => {
780                    w.remove::<Condition>(*e);
781                }
782                Reduced::Some(r) => {
783                    w.insert(*e, Condition(r));
784                    log.count("conditions kept on unresolved axes", 1);
785                }
786                Reduced::Never => remove_not_present(w, *e, log, "not present in the resolved state"),
787            }
788        }
789
790        // primitive conditions: drop the primitives that do not exist in this state
791        let primitive_conditions: Vec<(Entity, PrimitiveCondition)> = w
792            .iter::<PrimitiveCondition>()
793            .map(|(e, c)| (e, c.clone()))
794            .collect();
795        for (e, pc) in primitive_conditions {
796            if !w.is_alive(e) {
797                continue;
798            }
799            let mut keep: Vec<usize> = Vec::new();
800            let mut conditions: Vec<Option<Condition>> = Vec::new();
801            for (i, c) in pc.0.iter().enumerate() {
802                let Some(c) = c else {
803                    keep.push(i);
804                    conditions.push(None);
805                    continue;
806                };
807                if c.0.is_empty() {
808                    log.count("primitives never present", 1);
809                }
810                let mut mentioned = Vec::new();
811                c.references(&mut mentioned);
812                mentioned.sort();
813                mentioned.dedup();
814                let reduced = reduce_condition(c, &values);
815                let present = !matches!(reduced, Reduced::Never);
816                for a in &mentioned {
817                    log.count(
818                        &format!(
819                            "{} primitives {}",
820                            label(a),
821                            if present { "kept" } else { "dropped" }
822                        ),
823                        1,
824                    );
825                }
826                match reduced {
827                    Reduced::Always => {
828                        keep.push(i);
829                        conditions.push(None);
830                    }
831                    Reduced::Some(r) => {
832                        keep.push(i);
833                        conditions.push(Some(Condition(r)));
834                        log.count("primitive conditions kept on unresolved axes", 1);
835                    }
836                    Reduced::Never => {}
837                }
838            }
839            if keep.is_empty() {
840                // nothing of it is drawn in this state: the entity is not present
841                remove_not_present(
842                    w,
843                    e,
844                    log,
845                    "every primitive's condition fails in the resolved state",
846                );
847                continue;
848            }
849            let dropped = pc.0.len() - keep.len();
850            if dropped > 0 {
851                w.rebuild_primitives(e, |mesh| {
852                    let primitives = mesh.primitives();
853                    // one condition per primitive is the verifier's rule, which an unverified world may break
854                    let kept = keep
855                        .iter()
856                        .map(|&i| {
857                            primitives.get(i).cloned().ok_or_else(|| {
858                                format!(
859                                    "{e:?}: {} primitive conditions for {} primitives",
860                                    pc.0.len(),
861                                    primitives.len()
862                                )
863                            })
864                        })
865                        .collect::<Result<Vec<_>, _>>()?;
866                    Ok((kept, keep.clone()))
867                })?;
868                log.dropped(
869                    Measure::Primitives,
870                    "primitive condition fails in the resolved state",
871                    dropped as u64,
872                );
873                log.count("primitives dropped by their condition", dropped as u64);
874            }
875            if conditions.iter().all(Option::is_none) {
876                w.remove::<PrimitiveCondition>(e);
877            } else {
878                w.insert(e, PrimitiveCondition(conditions));
879            }
880        }
881
882        // overrides on what remains
883        let overrides: Vec<(Entity, StateOverride)> =
884            w.iter::<StateOverride>().map(|(e, o)| (e, o.clone())).collect();
885        for (e, o) in overrides {
886            w.remove::<StateOverride>(e);
887            // in order, the clauses that may still hold, up to the first that always holds
888            let mut reduced: Vec<(Clause, Override)> = Vec::new();
889            for (c, ov) in &o.0 {
890                if let Some(r) = c.reduce(&values) {
891                    let last = r.0.is_empty();
892                    reduced.push((r, *ov));
893                    if last {
894                        break;
895                    }
896                }
897            }
898            let ov = match reduced.as_slice() {
899                [] => continue,
900                [(c, ov)] if c.0.is_empty() => ov,
901                _ => {
902                    w.insert(e, StateOverride(reduced));
903                    log.count("overrides kept on unresolved axes", 1);
904                    continue;
905                }
906            };
907            match *ov {
908                Override::Transform(t) => {
909                    w.insert(e, t);
910                }
911                Override::Variant(v) => {
912                    w.get_mut::<Instance>(e)
913                        .ok_or("variant override without an instance")?
914                        .variant = v;
915                }
916                Override::Prototype(p) => {
917                    w.get_mut::<Instance>(e)
918                        .ok_or("prototype override without an instance")?
919                        .prototype = p;
920                }
921            }
922            log.count(&format!("override {}", ov.kind()), 1);
923        }
924
925        // components that hold clauses of their own, reduced as the conditions are
926        for e in w.restate_components(&Resolved(&values)) {
927            if w.is_alive(e) {
928                remove_not_present(w, e, log, "not present in the resolved state");
929            }
930        }
931        for (e, _) in &axes {
932            if !kept.contains(e) {
933                w.remove::<StateAxis>(*e);
934            }
935        }
936        Ok(())
937    }
938}
939
940/// `select-outfit{choices=key:value;key:value}` (required): resolves the named axes only, to the named values (by name
941/// or index), and leaves every other global axis unresolved, with the conditions and primitive conditions that
942/// depend on it reduced as [`ResolveState`] reduces them. It is what a recipe of chosen ids replays: a program (a
943/// website) picks some outfits and accessories from a catalog and the rest stays open. Everything else is as for
944/// `resolve-state`, whose report it gives (the members and primitives dropped, counted per axis and value), with the
945/// differences that a key naming no axis is an error naming the axes there are, and that an axis scoped to an entity
946/// and not named resolves by its default (an error when it has none) as it cannot stay.
947#[derive(Clone, Debug, PartialEq, Eq)]
948pub struct SelectOutfit {
949    /// key -> value text, as given; never empty.
950    pub choices: BTreeMap<String, String>,
951}
952
953/// `select-outfit`'s declaration ([`crate::pass::PassRegistry::add`]).
954pub const SELECT_OUTFIT: PassSpec = PassSpec {
955    name: "select-outfit",
956    options: &[OptionSpec::required(
957        "choices",
958        "axis:value",
959        "key:value items, `;` between, at least one: the outfit axes' values",
960    )],
961    make: select_outfit,
962};
963
964fn select_outfit(g: &Given) -> Result<Box<dyn Pass>, String> {
965    let mut choices = BTreeMap::new();
966    for item in g
967        .required("choices")
968        .split(';')
969        .map(str::trim)
970        .filter(|s| !s.is_empty())
971    {
972        let (key, value) = item
973            .split_once(':')
974            .ok_or_else(|| format!("choices item {item:?} is not key:value"))?;
975        if choices
976            .insert(key.trim().to_owned(), value.trim().to_owned())
977            .is_some()
978        {
979            return Err(format!("choices: {key} twice"));
980        }
981    }
982    if choices.is_empty() {
983        return Err("choices: no key:value item".into());
984    }
985    Ok(Box::new(SelectOutfit { choices }))
986}
987
988impl Pass for SelectOutfit {
989    fn name(&self) -> &'static str {
990        "select-outfit"
991    }
992    fn options(&self) -> Options {
993        let items: Vec<String> = self.choices.iter().map(|(k, v)| format!("{k}:{v}")).collect();
994        Options::from([("choices".to_owned(), items.join(";"))])
995    }
996    fn requires(&self) -> &[&'static str] {
997        &[StateAxis::NAME]
998    }
999    fn removes(&self) -> &[&'static str] {
1000        REMOVED
1001    }
1002    fn modifies(&self) -> &[&'static str] {
1003        MODIFIED
1004    }
1005    fn despawns_entities(&self) -> bool {
1006        true
1007    }
1008    fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
1009        let axes: Vec<&StateAxis> = w.iter::<StateAxis>().map(|(_, a)| a).collect();
1010        if let Some(k) = self.choices.keys().find(|k| !axes.iter().any(|a| &a.key == *k)) {
1011            let mut there: Vec<&str> = axes.iter().map(|a| a.key.as_str()).collect();
1012            there.sort_unstable();
1013            let shown = there.iter().take(20).copied().collect::<Vec<_>>().join(", ");
1014            return Err(format!(
1015                "choice {k}: no axis has that key (the world's: {shown}{})",
1016                if there.len() > 20 {
1017                    format!(" and {} more", there.len() - 20)
1018                } else {
1019                    String::new()
1020                }
1021            ));
1022        }
1023        let keep = axes
1024            .iter()
1025            .filter(|a| a.scope == Scope::Global && !self.choices.contains_key(&a.key))
1026            .map(|a| a.key.clone())
1027            .collect();
1028        ResolveState {
1029            assign: self.choices.clone(),
1030            assign_class: BTreeMap::new(),
1031            keep,
1032        }
1033        .run(w, log)
1034    }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039    use super::*;
1040
1041    /// A domain of more than 64 values (the verifier refuses it) and a value past the word are not a shift past it.
1042    #[test]
1043    fn a_domain_beyond_the_word_does_not_overflow_a_shift() {
1044        let big = Domain::Enum((0..65).map(|i| format!("v{i}")).collect());
1045        assert_eq!(big.full_mask(), u64::MAX);
1046        assert_eq!(Domain::Bool.full_mask(), 0b11);
1047        let mut w = World::new();
1048        let axis = w.spawn();
1049        let clause = Clause(vec![Literal {
1050            axis,
1051            values: u64::MAX,
1052        }]);
1053        assert_eq!(clause.reduce(&BTreeMap::from([(axis, 64)])), None);
1054        assert_eq!(
1055            clause.reduce(&BTreeMap::from([(axis, 63)])),
1056            Some(Clause(Vec::new()))
1057        );
1058    }
1059    use crate::components::Name;
1060    use crate::pass::{Error, Mode, PassManager, PassRegistry, Pipeline, Target};
1061
1062    fn axis(w: &mut World, key: &str) -> Entity {
1063        let e = w.spawn();
1064        w.insert(
1065            e,
1066            StateAxis {
1067                key: key.into(),
1068                domain: Domain::Bool,
1069                default: Some(0),
1070                scope: Scope::Global,
1071                class: "event".into(),
1072                source: format!("event flag {}", key.rsplit('.').next().unwrap()),
1073            },
1074        );
1075        e
1076    }
1077
1078    fn on(a: Entity) -> Literal {
1079        Literal {
1080            axis: a,
1081            values: 0b10,
1082        }
1083    }
1084    fn off(a: Entity) -> Literal {
1085        Literal {
1086            axis: a,
1087            values: 0b01,
1088        }
1089    }
1090
1091    fn placement(w: &mut World, proto: Entity, name: &str) -> Entity {
1092        let e = w.spawn();
1093        w.insert(e, Transform::IDENTITY);
1094        w.insert(
1095            e,
1096            Instance {
1097                prototype: proto,
1098                variant: 0,
1099            },
1100        );
1101        w.insert(e, Name(name.into()));
1102        e
1103    }
1104
1105    /// A source's castle: intact walls (entity group 1117201: disabled while 8306 or 8302 is on), the same walls
1106    /// with charcoal (1117204: disabled while 8302 is off), and a piece shown only with 8301 on (1117203).
1107    struct Castle {
1108        w: World,
1109        intact: Entity,
1110        burnt: Entity,
1111        f8301: Entity,
1112    }
1113
1114    fn castle() -> Castle {
1115        let mut w = World::new();
1116        let f8301 = axis(&mut w, "fromsoft.flag.8301");
1117        let f8302 = axis(&mut w, "fromsoft.flag.8302");
1118        let f8306 = axis(&mut w, "fromsoft.flag.8306");
1119        let proto = w.spawn();
1120        w.insert(proto, Name("m003312".into()));
1121        let intact = placement(&mut w, proto, "m003312_3802");
1122        w.insert(intact, Condition(vec![Clause(vec![off(f8306), off(f8302)])]));
1123        let burnt = placement(&mut w, proto, "m003412_3902");
1124        w.insert(burnt, Condition(vec![Clause(vec![on(f8302)])]));
1125        let only8301 = placement(&mut w, proto, "m300102_3700");
1126        w.insert(only8301, Condition(vec![Clause(vec![on(f8301)])]));
1127        Castle {
1128            w,
1129            intact,
1130            burnt,
1131            f8301,
1132        }
1133    }
1134
1135    fn run(w: &mut World, assign: &str) -> PassLog {
1136        let opts = if assign.is_empty() {
1137            Options::new()
1138        } else {
1139            Options::from([("assign".to_owned(), assign.to_owned())])
1140        };
1141        let mut log = PassLog::default();
1142        RESOLVE_STATE.instance(&opts).unwrap().run(w, &mut log).unwrap();
1143        log
1144    }
1145
1146    fn names(w: &World) -> Vec<String> {
1147        w.iter::<Instance>()
1148            .map(|(e, _)| w.get::<Name>(e).unwrap().0.clone())
1149            .collect()
1150    }
1151
1152    fn run_keep(w: &mut World, assign: &str, keep: &str) -> Result<PassLog, String> {
1153        let mut opts = Options::from([("keep".to_owned(), keep.to_owned())]);
1154        if !assign.is_empty() {
1155            opts.insert("assign".to_owned(), assign.to_owned());
1156        }
1157        let mut log = PassLog::default();
1158        RESOLVE_STATE.instance(&opts)?.run(w, &mut log)?;
1159        Ok(log)
1160    }
1161
1162    /// A record of parts, each with a clause of its own (as a collision file's parts are): not `core.condition`, so it is
1163    /// the component's own [`Component::restate`] that `resolve-state` runs.
1164    #[derive(Clone, Debug, PartialEq)]
1165    struct Parts(Vec<(&'static str, Option<Clause>)>);
1166    impl Component for Parts {
1167        const NAME: &'static str = "test.parts";
1168        const GEOMETRY: crate::Geometry = crate::Geometry::None;
1169        fn references(&self, out: &mut Vec<Entity>) {
1170            for (_, c) in &self.0 {
1171                if let Some(c) = c {
1172                    out.extend(c.0.iter().map(|l| l.axis));
1173                }
1174            }
1175        }
1176        fn restate(&self, resolved: &Resolved<'_>) -> crate::world::Restated<Self> {
1177            use crate::world::Restated;
1178            let mut parts = Vec::new();
1179            for (name, clause) in &self.0 {
1180                let clause = match clause {
1181                    None => None,
1182                    Some(c) => match resolved.reduce(c) {
1183                        None => continue,
1184                        Some(r) if r.0.is_empty() => None,
1185                        Some(r) => Some(r),
1186                    },
1187                };
1188                parts.push((*name, clause));
1189            }
1190            if parts.is_empty() {
1191                Restated::Absent
1192            } else if parts == self.0 {
1193                Restated::Unchanged
1194            } else {
1195                Restated::Replaced(Parts(parts))
1196            }
1197        }
1198    }
1199
1200    /// A clause a component holds is reduced as a condition is: a part whose clause fails leaves the record, one that always
1201    /// holds loses its clause, a literal on a kept axis stays, and a record left with no part is despawned with the entity
1202    /// that has only it. With 8302 kept the clause on it stays, as a condition's would.
1203    #[test]
1204    fn a_clause_inside_a_component_is_reduced_by_resolving_state() {
1205        let mut c = castle();
1206        let f8302 =
1207            c.w.iter::<StateAxis>()
1208                .find(|(_, a)| a.key == "fromsoft.flag.8302")
1209                .unwrap()
1210                .0;
1211        let record = c.w.spawn();
1212        c.w.insert(
1213            record,
1214            Parts(vec![
1215                ("always", None),
1216                ("needs 8301", Some(Clause(vec![on(c.f8301)]))),
1217                ("not 8301", Some(Clause(vec![off(c.f8301)]))),
1218                (
1219                    "8301 off and 8302 on",
1220                    Some(Clause(vec![off(c.f8301), on(f8302)])),
1221                ),
1222            ]),
1223        );
1224        let only = c.w.spawn();
1225        c.w.insert(only, Parts(vec![("needs 8301", Some(Clause(vec![on(c.f8301)])))]));
1226        run_keep(&mut c.w, "", "fromsoft.flag.8302").unwrap();
1227        // 8301 is off by default: the part needing it leaves, the other clauses keep what is still open
1228        assert_eq!(
1229            c.w.get::<Parts>(record),
1230            Some(&Parts(vec![
1231                ("always", None),
1232                ("not 8301", None),
1233                ("8301 off and 8302 on", Some(Clause(vec![on(f8302)]))),
1234            ]))
1235        );
1236        // a record none of whose parts is present is not
1237        assert!(!c.w.is_alive(only));
1238        // and with everything resolved no clause on an axis is left to name a removed one
1239        let mut all = castle();
1240        let record = all.w.spawn();
1241        all.w.insert(
1242            record,
1243            Parts(vec![("not 8301", Some(Clause(vec![off(all.f8301)])))]),
1244        );
1245        run(&mut all.w, "");
1246        assert_eq!(all.w.get::<Parts>(record), Some(&Parts(vec![("not 8301", None)])));
1247    }
1248
1249    /// Keeping 8302 (8301 and 8306 at their defaults, off): the intact walls keep only their 8302 literal, the
1250    /// charcoal walls their whole condition, the 8301 piece is despawned; 8302 stays an axis, the others go. A later
1251    /// resolve-state with 8302 on finishes the job as resolving everything at once would.
1252    #[test]
1253    fn kept_axes_stay_with_the_conditions_reduced_to_them() {
1254        let mut c = castle();
1255        let f8302 =
1256            c.w.iter::<StateAxis>()
1257                .find(|(_, a)| a.key == "fromsoft.flag.8302")
1258                .unwrap()
1259                .0;
1260        let log = run_keep(&mut c.w, "", "fromsoft.flag.8302").unwrap();
1261        assert_eq!(log.counts["axis fromsoft.flag.8302 kept unresolved"], 1);
1262        assert_eq!(
1263            c.w.get::<Condition>(c.intact),
1264            Some(&Condition(vec![Clause(vec![off(f8302)])]))
1265        );
1266        assert_eq!(
1267            c.w.get::<Condition>(c.burnt),
1268            Some(&Condition(vec![Clause(vec![on(f8302)])]))
1269        );
1270        assert!(!c.w.is_alive(c.f8301) || !c.w.has::<StateAxis>(c.f8301));
1271        assert_eq!(names(&c.w), vec!["m003312_3802", "m003412_3902"]);
1272        let keys: Vec<String> = c.w.iter::<StateAxis>().map(|(_, a)| a.key.clone()).collect();
1273        assert_eq!(keys, vec!["fromsoft.flag.8302"]);
1274        run(&mut c.w, "fromsoft.flag.8302:on");
1275        assert_eq!(names(&c.w), vec!["m003412_3902"]);
1276        assert!(c.w.iter::<Condition>().next().is_none());
1277    }
1278
1279    /// An override list keeps its order on the kept axis up to the clause that always holds; when a resolved axis
1280    /// makes the first clause always hold, it applies now.
1281    #[test]
1282    fn overrides_on_kept_axes_stay_in_order() {
1283        for (assign, want) in [("", None), ("a:on", Some(1))] {
1284            let mut w = World::new();
1285            let a = axis(&mut w, "a");
1286            let k = axis(&mut w, "k");
1287            let proto = w.spawn();
1288            let e = placement(&mut w, proto, "p");
1289            w.insert(
1290                e,
1291                StateOverride(vec![
1292                    (Clause(vec![on(a)]), Override::Variant(1)),
1293                    (Clause(vec![on(k), off(a)]), Override::Variant(2)),
1294                    (Clause(vec![]), Override::Variant(3)),
1295                    (Clause(vec![on(k)]), Override::Variant(4)),
1296                ]),
1297            );
1298            run_keep(&mut w, assign, "k").unwrap();
1299            match want {
1300                None => assert_eq!(
1301                    w.get::<StateOverride>(e),
1302                    Some(&StateOverride(vec![
1303                        (Clause(vec![on(k)]), Override::Variant(2)),
1304                        (Clause(vec![]), Override::Variant(3)),
1305                    ]))
1306                ),
1307                Some(v) => {
1308                    assert!(!w.has::<StateOverride>(e));
1309                    assert_eq!(w.get::<Instance>(e).unwrap().variant, v);
1310                }
1311            }
1312        }
1313    }
1314
1315    /// A save snapshot's value for a kept axis stays (as the only entry of core.state_assignment), so resolving the
1316    /// axis later reads it: 8302 saved on, kept, then resolved without an assignment leaves the charcoal walls.
1317    #[test]
1318    fn a_kept_axis_keeps_its_snapshot_value() {
1319        let mut c = castle();
1320        c.w.insert_resource(StateAssignment(BTreeMap::from([
1321            ("fromsoft.flag.8302".to_owned(), 1),
1322            ("fromsoft.flag.8306".to_owned(), 0),
1323        ])));
1324        let log = run_keep(&mut c.w, "", "fromsoft.flag.8302").unwrap();
1325        assert_eq!(log.counts["snapshot values kept for kept axes"], 1);
1326        assert_eq!(
1327            c.w.resource::<StateAssignment>(),
1328            Some(&StateAssignment(BTreeMap::from([(
1329                "fromsoft.flag.8302".to_owned(),
1330                1
1331            )])))
1332        );
1333        run(&mut c.w, "");
1334        assert_eq!(names(&c.w), vec!["m003412_3902"]);
1335    }
1336
1337    /// Keeping or assigning an axis the world lacks is counted by key and changes nothing (a program's choices cover
1338    /// worlds that hold some of their axes only); keeping one that is also assigned is an error.
1339    #[test]
1340    fn a_missing_axis_is_counted_an_assigned_and_kept_one_is_an_error() {
1341        let mut plain = castle();
1342        run(&mut plain.w, "");
1343        let mut c = castle();
1344        let log = run_keep(&mut c.w, "fromsoft.flag.7777:on", "fromsoft.flag.9999").unwrap();
1345        assert_eq!(names(&c.w), names(&plain.w));
1346        assert_eq!(
1347            log.counts["kept fromsoft.flag.9999, which no axis has: ignored"],
1348            1
1349        );
1350        assert_eq!(
1351            log.counts["assigned fromsoft.flag.7777, which no axis has: ignored"],
1352            1
1353        );
1354        assert!(!c.w.has_component_named(StateAxis::NAME));
1355        let both = Options::from([
1356            ("keep".to_owned(), "fromsoft.flag.8302".to_owned()),
1357            ("assign".to_owned(), "fromsoft.flag.8302:on".to_owned()),
1358        ]);
1359        assert!(RESOLVE_STATE.instance(&both).is_err());
1360    }
1361
1362    /// An item the pipeline text cannot carry is refused by name; what is accepted prints as text that parses back to
1363    /// the same options.
1364    #[test]
1365    fn resolve_state_refuses_what_its_text_form_cannot_carry() {
1366        let opts = |k: &str, v: &str| Options::from([(k.to_owned(), v.to_owned())]);
1367        for (option, value, item) in [
1368            ("assign", "a:1,", "a:1,"),
1369            ("assign", "a:1,b=2", "a:1,b=2"),
1370            ("assign", "a:1,;b=c:2", "a:1,"),
1371            ("keep", "a;z,", "z,"),
1372        ] {
1373            let error = RESOLVE_STATE.instance(&opts(option, value)).err().expect(value);
1374            assert!(error.contains(&format!("{item:?}")), "{value}: {error}");
1375        }
1376        // a ',' with a part after it that holds no '=' is carried
1377        assert!(RESOLVE_STATE.instance(&opts("keep", "k,;x")).is_ok());
1378        let accepted = RESOLVE_STATE
1379            .instance(&opts("assign", "fromsoft.flag.8302:on;gta.ipl.x:off"))
1380            .unwrap();
1381        let text = crate::pass::pass_text(accepted.as_ref());
1382        let mut r = PassRegistry::new();
1383        crate::register(&mut r);
1384        let again = r.parse(&text).unwrap();
1385        assert_eq!(again.passes[0].options(), accepted.options());
1386    }
1387
1388    #[test]
1389    fn castle_defaults_keep_the_intact_walls() {
1390        let mut c = castle();
1391        assert_eq!(c.w.verify_components(), vec![]);
1392        let log = run(&mut c.w, "");
1393        assert_eq!(names(&c.w), ["m003312_3802"]);
1394        assert!(c.w.is_alive(c.intact) && !c.w.is_alive(c.burnt));
1395        assert_eq!(log.counts["fromsoft.flag.8302=off kept"], 1);
1396        assert_eq!(log.counts["fromsoft.flag.8302=off despawned"], 1);
1397        assert_eq!(log.counts["fromsoft.flag.8301=off despawned"], 1);
1398        for name in [StateAxis::NAME, Condition::NAME, StateOverride::NAME] {
1399            assert!(!c.w.has_component_named(name), "{name} left");
1400        }
1401    }
1402
1403    #[test]
1404    fn castle_burnt_by_assignment() {
1405        let mut c = castle();
1406        run(&mut c.w, "fromsoft.flag.8302:on");
1407        assert_eq!(names(&c.w), ["m003412_3902"]);
1408    }
1409
1410    #[test]
1411    fn assign_beats_resource_beats_default() {
1412        let mut c = castle();
1413        c.w.insert_resource(StateAssignment(BTreeMap::from([
1414            ("fromsoft.flag.8301".to_owned(), 1),
1415            ("fromsoft.flag.8302".to_owned(), 1),
1416        ])));
1417        run(&mut c.w, "fromsoft.flag.8302:0");
1418        assert_eq!(names(&c.w), ["m003312_3802", "m300102_3700"]);
1419        assert!(c.w.resource::<StateAssignment>().is_none());
1420        assert!(
1421            c.w.is_alive(c.f8301) && !c.w.has::<StateAxis>(c.f8301),
1422            "axes lose their component, the entity stays"
1423        );
1424    }
1425
1426    /// A class assignment falls back after the key assignments, the snapshot and keep: its value is read only against
1427    /// the axes it falls to (here A, of three levels; B, of two, is assigned lod0 by key, so lod2 is never read against
1428    /// it), and an invalid value for an axis it does fall to is an error.
1429    #[test]
1430    fn a_class_assignment_reaches_only_the_axes_nothing_else_set() {
1431        let lod_axis = |w: &mut World, key: &str, levels: usize| {
1432            let e = w.spawn();
1433            w.insert(
1434                e,
1435                StateAxis {
1436                    key: key.into(),
1437                    domain: Domain::Enum((0..levels).map(|i| format!("lod{i}")).collect()),
1438                    default: None,
1439                    scope: Scope::Global,
1440                    class: "lod".into(),
1441                    source: key.into(),
1442                },
1443            );
1444            e
1445        };
1446        let world = || {
1447            let mut w = World::new();
1448            let proto = w.spawn();
1449            w.insert(proto, Name("proto".into()));
1450            let (a, b) = (lod_axis(&mut w, "lod.a", 3), lod_axis(&mut w, "lod.b", 2));
1451            // a placement for A at lod2 and one for B at lod0
1452            for (axis, level, name) in [(a, 2, "a2"), (b, 0, "b0")] {
1453                let p = placement(&mut w, proto, name);
1454                w.insert(
1455                    p,
1456                    Condition(vec![Clause(vec![Literal {
1457                        axis,
1458                        values: 1 << level,
1459                    }])]),
1460                );
1461            }
1462            w
1463        };
1464        let resolve = |w: &mut World, assign: &[(&str, &str)]| {
1465            ResolveState {
1466                assign: assign.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
1467                assign_class: [("lod".to_owned(), "lod2".to_owned())].into(),
1468                ..Default::default()
1469            }
1470            .run(w, &mut PassLog::default())
1471        };
1472        let mut w = world();
1473        resolve(&mut w, &[("lod.b", "lod0")]).unwrap();
1474        let mut left: Vec<String> = w.iter::<Name>().map(|(_, n)| n.0.clone()).collect();
1475        left.sort();
1476        assert_eq!(left, ["a2", "b0", "proto"]);
1477        // without B's key assignment, lod2 falls to B too, which has no lod2
1478        let e = resolve(&mut world(), &[]).unwrap_err();
1479        assert!(
1480            e.contains("assign-class lod:lod2 is not a value of axis lod.b"),
1481            "{e}"
1482        );
1483    }
1484
1485    #[test]
1486    fn a_save_with_unrelated_flags_is_fine() {
1487        let mut c = castle();
1488        c.w.insert_resource(StateAssignment(BTreeMap::from([
1489            ("fromsoft.flag.8302".to_owned(), 1),
1490            ("fromsoft.flag.11110100".to_owned(), 1),
1491            ("fromsoft.flag.6000".to_owned(), 0),
1492        ])));
1493        let log = run(&mut c.w, "");
1494        assert_eq!(names(&c.w), ["m003412_3902"]);
1495        assert_eq!(log.counts["assignment keys without an axis"], 2);
1496    }
1497
1498    #[test]
1499    fn run_on_an_unverified_world_errs_instead_of_panicking() {
1500        let mut w = World::new();
1501        let proto = w.spawn();
1502        let not_axis = w.spawn();
1503        let e = placement(&mut w, proto, "p");
1504        w.insert(
1505            e,
1506            Condition(vec![Clause(vec![Literal {
1507                axis: not_axis,
1508                values: 1,
1509            }])]),
1510        );
1511        let err = RESOLVE_STATE
1512            .instance(&Options::new())
1513            .unwrap()
1514            .run(&mut w, &mut PassLog::default())
1515            .unwrap_err();
1516        assert!(err.contains("not an axis"), "{err}");
1517        let mut w = World::new();
1518        let e = placement(&mut w, proto, "p");
1519        w.insert(
1520            e,
1521            StateOverride(vec![(
1522                Clause(vec![Literal {
1523                    axis: not_axis,
1524                    values: 1,
1525                }]),
1526                Override::Variant(1),
1527            )]),
1528        );
1529        assert!(
1530            RESOLVE_STATE
1531                .instance(&Options::new())
1532                .unwrap()
1533                .run(&mut w, &mut PassLog::default())
1534                .is_err()
1535        );
1536    }
1537
1538    /// An axis whose default is unknown stays unresolved unless assigned: with 8302's default unknown, the walls on it
1539    /// keep their 8302 literals (8306 resolved off), the 8301 piece goes, and 8302 stays an axis (counted); assigned,
1540    /// or given by the snapshot, it resolves as a known default would. Scoped to an entity, it must be assigned.
1541    #[test]
1542    fn an_unknown_default_stays_unresolved_unless_assigned() {
1543        let unknown = |c: &mut Castle| {
1544            let (e, _) =
1545                c.w.iter::<StateAxis>()
1546                    .find(|(_, a)| a.key == "fromsoft.flag.8302")
1547                    .unwrap();
1548            c.w.get_mut::<StateAxis>(e).unwrap().default = None;
1549            e
1550        };
1551        let mut c = castle();
1552        let f8302 = unknown(&mut c);
1553        assert_eq!(c.w.verify_components(), vec![]);
1554        let log = run(&mut c.w, "");
1555        assert_eq!(names(&c.w), ["m003312_3802", "m003412_3902"]);
1556        assert_eq!(
1557            log.counts["axis fromsoft.flag.8302 unresolved: no known default, not assigned"],
1558            1
1559        );
1560        assert_eq!(log.counts["fromsoft.flag.8302 (unresolved) kept"], 2);
1561        assert_eq!(log.counts["conditions kept on unresolved axes"], 2);
1562        let axes: Vec<&str> = c.w.iter::<StateAxis>().map(|(_, a)| a.key.as_str()).collect();
1563        assert_eq!(axes, ["fromsoft.flag.8302"]);
1564        assert_eq!(
1565            c.w.get::<Condition>(c.intact),
1566            Some(&Condition(vec![Clause(vec![off(f8302)])]))
1567        );
1568        assert_eq!(c.w.verify_components(), vec![]);
1569        // a later resolve-state with a value finishes the job
1570        run(&mut c.w, "fromsoft.flag.8302:on");
1571        assert_eq!(names(&c.w), ["m003412_3902"]);
1572        assert!(!c.w.has_component_named(StateAxis::NAME));
1573
1574        for given in ["assign", "snapshot"] {
1575            let mut c = castle();
1576            unknown(&mut c);
1577            if given == "snapshot" {
1578                c.w.insert_resource(StateAssignment(BTreeMap::from([(
1579                    "fromsoft.flag.8302".to_owned(),
1580                    0,
1581                )])));
1582                run(&mut c.w, "");
1583            } else {
1584                run(&mut c.w, "fromsoft.flag.8302:off");
1585            }
1586            assert_eq!(names(&c.w), ["m003312_3802"], "{given}");
1587            assert!(!c.w.has_component_named(StateAxis::NAME), "{given}");
1588        }
1589
1590        let mut c = castle();
1591        let e = unknown(&mut c);
1592        let door = c.intact;
1593        c.w.get_mut::<StateAxis>(e).unwrap().scope = Scope::Local(door);
1594        let err = RESOLVE_STATE
1595            .instance(&Options::new())
1596            .unwrap()
1597            .run(&mut c.w, &mut PassLog::default())
1598            .unwrap_err();
1599        assert!(err.contains("assign it"), "{err}");
1600    }
1601
1602    #[test]
1603    fn values_outside_the_domain_and_bad_options_are_errors() {
1604        for bad in ["fromsoft.flag.8302:maybe", "fromsoft.flag.8302:2"] {
1605            let mut c = castle();
1606            let opts = Options::from([("assign".to_owned(), bad.to_owned())]);
1607            assert!(
1608                RESOLVE_STATE
1609                    .instance(&opts)
1610                    .unwrap()
1611                    .run(&mut c.w, &mut PassLog::default())
1612                    .is_err(),
1613                "{bad}"
1614            );
1615        }
1616        let mut c = castle();
1617        // a resource value outside the axis's domain is an error (only keys without an axis are tolerated)
1618        c.w.insert_resource(StateAssignment(BTreeMap::from([(
1619            "fromsoft.flag.8302".to_owned(),
1620            2,
1621        )])));
1622        assert!(
1623            RESOLVE_STATE
1624                .instance(&Options::new())
1625                .unwrap()
1626                .run(&mut c.w, &mut PassLog::default())
1627                .is_err()
1628        );
1629        assert!(
1630            RESOLVE_STATE
1631                .instance(&Options::from([("assign".to_owned(), "x".to_owned())]))
1632                .is_err()
1633        );
1634        assert!(
1635            RESOLVE_STATE
1636                .instance(&Options::from([("colour".to_owned(), "red".to_owned())]))
1637                .is_err()
1638        );
1639    }
1640
1641    #[test]
1642    fn empty_clause_always_zero_clauses_never() {
1643        let mut w = World::new();
1644        let proto = w.spawn();
1645        let always = placement(&mut w, proto, "always");
1646        w.insert(always, Condition(vec![Clause::default()]));
1647        let never = placement(&mut w, proto, "never");
1648        w.insert(never, Condition(vec![]));
1649        assert_eq!(w.verify_components(), vec![]);
1650        let log = run(&mut w, "");
1651        assert_eq!(names(&w), ["always"]);
1652        assert_eq!(log.counts["never present"], 1);
1653    }
1654
1655    #[test]
1656    fn overrides_first_match_applies() {
1657        let mut w = World::new();
1658        let door = axis(&mut w, "fromsoft.door.1110432");
1659        let proto = w.spawn();
1660        let open = w.spawn();
1661        w.insert(open, Name("door open".into()));
1662        let e = placement(&mut w, proto, "door");
1663        let mut moved = Transform::IDENTITY;
1664        moved.cols[3] = [1.0, 0.0, 0.0];
1665        w.insert(
1666            e,
1667            StateOverride(vec![
1668                (Clause(vec![on(door)]), Override::Prototype(open)),
1669                (Clause(vec![on(door)]), Override::Transform(moved)),
1670                (Clause::default(), Override::Variant(3)),
1671            ]),
1672        );
1673        assert_eq!(w.verify_components(), vec![]);
1674        let mut a = w.clone();
1675        let log = run(&mut a, "fromsoft.door.1110432:on");
1676        assert_eq!(a.get::<Instance>(e).unwrap().prototype, open);
1677        assert_eq!(
1678            *a.get::<Transform>(e).unwrap(),
1679            Transform::IDENTITY,
1680            "only the first match applies"
1681        );
1682        assert_eq!(log.counts["override prototype"], 1);
1683        run(&mut w, "");
1684        assert_eq!(
1685            *w.get::<Instance>(e).unwrap(),
1686            Instance {
1687                prototype: proto,
1688                variant: 3
1689            }
1690        );
1691    }
1692
1693    /// One failing world per rule `verify()` enforces, each asserting the exact (component, entity, message) that
1694    /// rule reports: a world that fails some other rule does not pass here.
1695    #[test]
1696    fn verify_rejects_each_rule() {
1697        let failing = |build: &dyn Fn(&mut World) -> (&'static str, Entity, String)| {
1698            let mut w = World::new();
1699            let (component, entity, message) = build(&mut w);
1700            assert_eq!(
1701                w.verify_components(),
1702                [(component, entity, message)],
1703                "one failure, the rule's own"
1704            );
1705        };
1706        let mut good = StateAxis {
1707            key: "k".into(),
1708            domain: Domain::Bool,
1709            default: Some(0),
1710            scope: Scope::Global,
1711            class: "event".into(),
1712            source: String::new(),
1713        };
1714        let with_axis = move |w: &mut World, a: StateAxis| {
1715            let e = w.spawn();
1716            w.insert(e, a);
1717            e
1718        };
1719        // the axis
1720        failing(&|w| {
1721            let e = with_axis(
1722                w,
1723                StateAxis {
1724                    key: String::new(),
1725                    ..good.clone()
1726                },
1727            );
1728            (
1729                "core.state_axis",
1730                e,
1731                r#"key "" is empty or holds ':' or ';' (the assign syntax)"#.to_owned(),
1732            )
1733        });
1734        failing(&|w| {
1735            let e = with_axis(
1736                w,
1737                StateAxis {
1738                    default: Some(2),
1739                    ..good.clone()
1740                },
1741            );
1742            (
1743                "core.state_axis",
1744                e,
1745                "k: default 2 outside its 2 values".to_owned(),
1746            )
1747        });
1748        failing(&|w| {
1749            let e = with_axis(
1750                w,
1751                StateAxis {
1752                    domain: Domain::Enum(vec![]),
1753                    default: Some(0),
1754                    ..good.clone()
1755                },
1756            );
1757            (
1758                "core.state_axis",
1759                e,
1760                "k: 0 values, the domain holds 1..=64".to_owned(),
1761            )
1762        });
1763        failing(&|w| {
1764            let names = (0..65).map(|i| format!("v{i}")).collect();
1765            let e = with_axis(
1766                w,
1767                StateAxis {
1768                    domain: Domain::Enum(names),
1769                    ..good.clone()
1770                },
1771            );
1772            (
1773                "core.state_axis",
1774                e,
1775                "k: 65 values, the domain holds 1..=64".to_owned(),
1776            )
1777        });
1778        failing(&|w| {
1779            let e = with_axis(
1780                w,
1781                StateAxis {
1782                    domain: Domain::Enum(vec!["a".into(), "a".into()]),
1783                    ..good.clone()
1784                },
1785            );
1786            (
1787                "core.state_axis",
1788                e,
1789                r#"k: value name "a" empty, repeated or holding ':' or ';'"#.to_owned(),
1790            )
1791        });
1792        failing(&|w| {
1793            let e = with_axis(
1794                w,
1795                StateAxis {
1796                    domain: Domain::Enum(vec!["a:b".into()]),
1797                    ..good.clone()
1798                },
1799            );
1800            (
1801                "core.state_axis",
1802                e,
1803                r#"k: value name "a:b" empty, repeated or holding ':' or ';'"#.to_owned(),
1804            )
1805        });
1806        failing(&|w| {
1807            let e = with_axis(
1808                w,
1809                StateAxis {
1810                    key: "k;x".into(),
1811                    ..good.clone()
1812                },
1813            );
1814            (
1815                "core.state_axis",
1816                e,
1817                r#"key "k;x" is empty or holds ':' or ';' (the assign syntax)"#.to_owned(),
1818            )
1819        });
1820        failing(&|w| {
1821            let gone = w.spawn();
1822            w.despawn(gone);
1823            let e = with_axis(
1824                w,
1825                StateAxis {
1826                    scope: Scope::Local(gone),
1827                    ..good.clone()
1828                },
1829            );
1830            (
1831                "core.state_axis",
1832                e,
1833                format!("refers to {gone:?}, which is not alive"),
1834            )
1835        });
1836        // literals
1837        let cond = |w: &mut World, lit: Literal, transform: bool| {
1838            let e = w.spawn();
1839            if transform {
1840                w.insert(e, Transform::IDENTITY);
1841            }
1842            w.insert(e, Condition(vec![Clause(vec![lit])]));
1843            e
1844        };
1845        failing(&|w| {
1846            let a = with_axis(w, good.clone());
1847            let e = cond(w, Literal { axis: a, values: 0 }, true);
1848            ("core.condition", e, "k: empty value mask".to_owned())
1849        });
1850        failing(&|w| {
1851            let a = with_axis(w, good.clone());
1852            let e = cond(
1853                w,
1854                Literal {
1855                    axis: a,
1856                    values: 0b100,
1857                },
1858                true,
1859            );
1860            (
1861                "core.condition",
1862                e,
1863                "k: mask 0x4 has values beyond its 2".to_owned(),
1864            )
1865        });
1866        failing(&|w| {
1867            let not_axis = w.spawn();
1868            w.insert(not_axis, Name("x".into()));
1869            let e = cond(
1870                w,
1871                Literal {
1872                    axis: not_axis,
1873                    values: 1,
1874                },
1875                true,
1876            );
1877            (
1878                "core.condition",
1879                e,
1880                format!("literal on {not_axis:?}, which is not a live axis"),
1881            )
1882        });
1883        failing(&|w| {
1884            let a = with_axis(w, good.clone());
1885            let e = cond(w, on(a), false);
1886            (
1887                "core.condition",
1888                e,
1889                "on an entity without a transform that is neither a prototype (a mesh) nor a part or alternative of an asset: only placed things, prototypes and chosen members carry state".to_owned(),
1890            )
1891        });
1892        // overrides
1893        failing(&|w| {
1894            let e = w.spawn();
1895            w.insert(e, Transform::IDENTITY);
1896            w.insert(e, StateOverride(vec![(Clause::default(), Override::Variant(1))]));
1897            (
1898                "core.state_override",
1899                e,
1900                "variant override on an entity without an instance".to_owned(),
1901            )
1902        });
1903        failing(&|w| {
1904            let proto = w.spawn();
1905            let gone = w.spawn();
1906            w.despawn(gone);
1907            let e = placement(w, proto, "p");
1908            w.insert(
1909                e,
1910                StateOverride(vec![(Clause::default(), Override::Prototype(gone))]),
1911            );
1912            (
1913                "core.state_override",
1914                e,
1915                format!("refers to {gone:?}, which is not alive"),
1916            )
1917        });
1918        failing(&|w| {
1919            let proto = w.spawn();
1920            let e = placement(w, proto, "p");
1921            let mut t = Transform::IDENTITY;
1922            t.cols[3][0] = f32::NAN;
1923            w.insert(
1924                e,
1925                StateOverride(vec![(Clause::default(), Override::Transform(t))]),
1926            );
1927            (
1928                "core.state_override",
1929                e,
1930                "transform: value 9 is NaN, not finite".to_owned(),
1931            )
1932        });
1933        // and a 64-value enum with a full mask is fine
1934        good.domain = Domain::Enum((0..64).map(|i| format!("v{i}")).collect());
1935        let mut w = World::new();
1936        let a = with_axis(&mut w, good);
1937        let e = w.spawn();
1938        w.insert(e, Transform::IDENTITY);
1939        w.insert(
1940            e,
1941            Condition(vec![Clause(vec![Literal {
1942                axis: a,
1943                values: u64::MAX,
1944            }])]),
1945        );
1946        assert_eq!(w.verify_components(), vec![]);
1947    }
1948
1949    #[test]
1950    fn references_reach_axes_and_targets() {
1951        let c = castle();
1952        let reached = c.w.closure([c.intact]);
1953        let mut keys: Vec<&str> =
1954            c.w.iter::<StateAxis>()
1955                .filter(|(e, _)| reached.contains(e))
1956                .map(|(_, a)| a.key.as_str())
1957                .collect();
1958        keys.sort_unstable();
1959        assert_eq!(keys, ["fromsoft.flag.8302", "fromsoft.flag.8306"]);
1960    }
1961
1962    fn registry() -> PassRegistry {
1963        let mut r = PassRegistry::new();
1964        r.add(&RESOLVE_STATE);
1965        r
1966    }
1967
1968    fn skate() -> Target {
1969        Target::new("skate")
1970            .legal(Transform::NAME)
1971            .legal(Instance::NAME)
1972            .legal(Name::NAME)
1973            .illegal(Condition::NAME)
1974            .illegal(StateOverride::NAME)
1975            .illegal(StateAxis::NAME)
1976    }
1977
1978    fn dump(w: &World) -> String {
1979        let mut s = String::new();
1980        for e in w.entities() {
1981            for c in w.components_json(e).unwrap() {
1982                s += &format!("{e:?} {} {} {:?}\n", c.name, c.version, c.value);
1983            }
1984        }
1985        for r in w.resources_json().unwrap() {
1986            s += &format!("resource {} {} {:?}\n", r.name, r.version, r.value);
1987        }
1988        s
1989    }
1990
1991    /// A world without state components needs no pass and comes out byte for byte the same; `resolve-state` stated on
1992    /// it changes nothing.
1993    #[test]
1994    fn stateless_world_is_untouched() {
1995        let mut c = castle();
1996        run(&mut c.w, "");
1997        let before = dump(&c.w);
1998        let m = PassManager::new(registry());
1999        m.convert(&mut c.w, &Pipeline::default(), &skate(), Mode::Partial)
2000            .unwrap();
2001        assert_eq!(dump(&c.w), before);
2002        let resolve = m.registry.parse("resolve-state").unwrap();
2003        m.convert(&mut c.w, &resolve, &skate(), Mode::Partial).unwrap();
2004        assert_eq!(dump(&c.w), before);
2005    }
2006
2007    /// A writer that cannot toggle refuses state until the program states `resolve-state` before it.
2008    #[test]
2009    fn a_writer_that_cannot_toggle_needs_resolve_state_stated() {
2010        let m = PassManager::new(registry());
2011        let err = m
2012            .convert(&mut castle().w, &Pipeline::default(), &skate(), Mode::Partial)
2013            .unwrap_err();
2014        assert!(
2015            matches!(&err, Error::Illegal { name, lowered_by, .. } if name == Condition::NAME && lowered_by.is_empty()),
2016            "{err}"
2017        );
2018        let mut c = castle();
2019        let resolve = m.registry.parse("resolve-state").unwrap();
2020        let report = m.convert(&mut c.w, &resolve, &skate(), Mode::Partial).unwrap();
2021        assert_eq!(report.pipeline, "resolve-state");
2022        assert_eq!(names(&c.w), ["m003312_3802"]);
2023    }
2024}