1use 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#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
38pub enum Domain {
39 Bool,
41 Enum(Vec<String>),
43}
44
45impl Domain {
46 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 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 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 fn full_mask(&self) -> u64 {
77 match self.len() {
79 n @ 0..=63 => (1u64 << n) - 1,
80 _ => u64::MAX,
81 }
82 }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
87pub enum Scope {
88 Global,
89 Local(Entity),
90}
91
92#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
94pub struct StateAxis {
95 pub key: String,
98 pub domain: Domain,
99 pub default: Option<u32>,
103 pub scope: Scope,
104 pub class: String,
107 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
155pub struct Literal {
156 pub axis: Entity,
157 pub values: u64,
158}
159
160#[derive(Clone, Copy, Debug)]
163pub struct Resolved<'a>(&'a BTreeMap<Entity, u32>);
164
165impl Resolved<'_> {
166 pub fn reduce(&self, clause: &Clause) -> Option<Clause> {
169 clause.reduce(self.0)
170 }
171 pub fn has(&self, axis: Entity) -> bool {
173 self.0.contains_key(&axis)
174 }
175}
176
177#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
179pub struct Clause(pub Vec<Literal>);
180
181impl Clause {
182 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 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
230fn is_prototype(e: Entity, w: &World) -> bool {
232 w.has::<Mesh>(e) || w.has::<crate::MeshFile>(e)
233}
234
235fn 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#[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 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#[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 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 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#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
319pub enum Override {
320 Transform(Transform),
321 Variant(u32),
323 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#[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#[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
382const REMOVED: &[&str] = &[
384 StateAxis::NAME,
385 Condition::NAME,
386 PrimitiveCondition::NAME,
387 StateOverride::NAME,
388 StateAssignment::NAME,
389];
390
391const MODIFIED: &[&str] = &[
394 Transform::NAME,
395 Instance::NAME,
396 Mesh::NAME,
397 Asset::NAME,
398 ClipBinding::NAME,
399];
400
401enum Reduced {
403 Always,
405 Never,
407 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
422fn 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#[derive(Clone, Debug, Default)]
487pub struct ResolveState {
488 pub assign: BTreeMap<String, String>,
490 pub assign_class: BTreeMap<String, String>,
492 pub keep: BTreeSet<String>,
494}
495
496pub 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 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 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 fn removes(&self) -> &[&'static str] {
606 REMOVED
607 }
608 fn modifies(&self) -> &[&'static str] {
609 MODIFIED
610 }
611 fn despawns_entities(&self) -> bool {
613 true
614 }
615 fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
616 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 let mut explicit = BTreeSet::new();
629 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 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 let mut kept = BTreeSet::new();
660 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
948pub struct SelectOutfit {
949 pub choices: BTreeMap<String, String>,
951}
952
953pub 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 #[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 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 #[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 #[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 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 assert!(!c.w.is_alive(only));
1238 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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 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 #[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 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 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 #[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 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 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 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 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 #[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 #[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}