use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use serde_json::Value;
use crate::asset::{Asset, Role};
use crate::clip::ClipBinding;
use crate::components::{Instance, Mesh, Transform};
use crate::json::JsonError;
use crate::ledger::Measure;
use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
use crate::world::{Component, Entity, Resource, World};
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum Domain {
Bool,
Enum(Vec<String>),
}
impl Domain {
pub fn len(&self) -> usize {
match self {
Domain::Bool => 2,
Domain::Enum(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn name(&self, i: u32) -> Option<&str> {
match self {
Domain::Bool => ["off", "on"].get(i as usize).copied(),
Domain::Enum(v) => v.get(i as usize).map(String::as_str),
}
}
pub fn parse(&self, text: &str) -> Option<u32> {
let n = self.len() as u32;
if let Ok(i) = text.parse::<u32>() {
return (i < n).then_some(i);
}
(0..n).find(|&i| self.name(i) == Some(text))
}
fn full_mask(&self) -> u64 {
match self.len() {
n @ 0..=63 => (1u64 << n) - 1,
_ => u64::MAX,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Scope {
Global,
Local(Entity),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct StateAxis {
pub key: String,
pub domain: Domain,
pub default: Option<u32>,
pub scope: Scope,
pub class: String,
pub source: String,
}
impl Component for StateAxis {
const NAME: &'static str = "core.state_axis";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
if self.key.is_empty() || self.key.contains([':', ';']) {
return Err(format!(
"key {:?} is empty or holds ':' or ';' (the assign syntax)",
self.key
));
}
let n = self.domain.len();
if !(1..=64).contains(&n) {
return Err(format!("{}: {n} values, the domain holds 1..=64", self.key));
}
if let Domain::Enum(names) = &self.domain {
for (i, a) in names.iter().enumerate() {
if a.is_empty() || a.contains([':', ';']) || names[..i].contains(a) {
return Err(format!(
"{}: value name {a:?} empty, repeated or holding ':' or ';'",
self.key
));
}
}
}
if let Some(d) = self.default
&& d as usize >= n
{
return Err(format!("{}: default {d} outside its {n} values", self.key));
}
Ok(())
}
fn references(&self, out: &mut Vec<Entity>) {
if let Scope::Local(owner) = self.scope {
out.push(owner);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct Literal {
pub axis: Entity,
pub values: u64,
}
#[derive(Clone, Copy, Debug)]
pub struct Resolved<'a>(&'a BTreeMap<Entity, u32>);
impl Resolved<'_> {
pub fn reduce(&self, clause: &Clause) -> Option<Clause> {
clause.reduce(self.0)
}
pub fn has(&self, axis: Entity) -> bool {
self.0.contains_key(&axis)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Clause(pub Vec<Literal>);
impl Clause {
fn reduce(&self, values: &BTreeMap<Entity, u32>) -> Option<Clause> {
let mut rest = Vec::new();
for l in &self.0 {
match values.get(&l.axis) {
Some(&v) if 1u64.checked_shl(v).is_none_or(|bit| l.values & bit == 0) => return None,
Some(_) => {}
None => rest.push(*l),
}
}
Some(Clause(rest))
}
fn verify(&self, w: &World) -> Result<(), String> {
for l in &self.0 {
let axis = w
.get::<StateAxis>(l.axis)
.ok_or_else(|| format!("literal on {:?}, which is not a live axis", l.axis))?;
if l.values == 0 {
return Err(format!("{}: empty value mask", axis.key));
}
if l.values & !axis.domain.full_mask() != 0 {
return Err(format!(
"{}: mask {:#x} has values beyond its {}",
axis.key,
l.values,
axis.domain.len()
));
}
}
Ok(())
}
fn axes(&self, out: &mut Vec<Entity>) {
out.extend(self.0.iter().map(|l| l.axis));
}
}
fn placed(e: Entity, w: &World) -> Result<(), String> {
if w.has::<Transform>(e) {
Ok(())
} else {
Err("on an entity without a transform: only placed things carry state".into())
}
}
fn is_prototype(e: Entity, w: &World) -> bool {
w.has::<Mesh>(e) || w.has::<crate::MeshFile>(e)
}
fn is_choosable_member(e: Entity, w: &World) -> bool {
w.iter::<Asset>().any(|(_, a)| {
a.members
.iter()
.any(|&(m, r)| m == e && matches!(r, Role::Part | Role::Alternative))
})
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Condition(pub Vec<Clause>);
impl Component for Condition {
const NAME: &'static str = "core.condition";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
if !is_prototype(e, w) && !is_choosable_member(e, w) {
placed(e, w).map_err(|_| {
"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()
})?;
}
self.0.iter().try_for_each(|c| c.verify(w))
}
fn references(&self, out: &mut Vec<Entity>) {
self.0.iter().for_each(|c| c.axes(out));
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct PrimitiveCondition(pub Vec<Option<Condition>>);
impl Component for PrimitiveCondition {
const NAME: &'static str = "core.primitive_condition";
const GEOMETRY: crate::Geometry = crate::Geometry::PerPrimitive;
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
Some(crate::remap_list(&self.0, from).map(PrimitiveCondition))
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
let mesh = w.get::<Mesh>(e).ok_or("on an entity without a core.mesh")?;
if mesh.primitives().len() != self.0.len() {
return Err(format!(
"{} conditions for {} primitives",
self.0.len(),
mesh.primitives().len()
));
}
self.0
.iter()
.flatten()
.flat_map(|c| c.0.iter())
.try_for_each(|c| c.verify(w))
}
fn references(&self, out: &mut Vec<Entity>) {
self.0
.iter()
.flatten()
.for_each(|c| c.0.iter().for_each(|cl| cl.axes(out)));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum Override {
Transform(Transform),
Variant(u32),
Prototype(Entity),
}
impl Override {
fn kind(&self) -> &'static str {
match self {
Override::Transform(_) => "transform",
Override::Variant(_) => "variant",
Override::Prototype(_) => "prototype",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct StateOverride(pub Vec<(Clause, Override)>);
impl Component for StateOverride {
const NAME: &'static str = "core.state_override";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
placed(e, w)?;
for (c, o) in &self.0 {
c.verify(w)?;
match o {
Override::Transform(t) => t.verify(e, w)?,
Override::Variant(_) | Override::Prototype(_) if !w.has::<Instance>(e) => {
return Err(format!("{} override on an entity without an instance", o.kind()));
}
_ => {}
}
}
Ok(())
}
fn references(&self, out: &mut Vec<Entity>) {
for (c, o) in &self.0 {
c.axes(out);
if let Override::Prototype(p) = o {
out.push(*p);
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct StateAssignment(pub BTreeMap<String, u32>);
impl Resource for StateAssignment {
const NAME: &'static str = "core.state_assignment";
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
}
const REMOVED: &[&str] = &[
StateAxis::NAME,
Condition::NAME,
PrimitiveCondition::NAME,
StateOverride::NAME,
StateAssignment::NAME,
];
const MODIFIED: &[&str] = &[
Transform::NAME,
Instance::NAME,
Mesh::NAME,
Asset::NAME,
ClipBinding::NAME,
];
enum Reduced {
Always,
Never,
Some(Vec<Clause>),
}
fn reduce_condition(c: &Condition, values: &BTreeMap<Entity, u32>) -> Reduced {
let reduced: Vec<Clause> = c.0.iter().filter_map(|cl| cl.reduce(values)).collect();
if reduced.iter().any(|cl| cl.0.is_empty()) {
Reduced::Always
} else if reduced.is_empty() {
Reduced::Never
} else {
Reduced::Some(reduced)
}
}
fn remove_not_present(w: &mut World, e: Entity, log: &mut PassLog, why: &str) {
let member = is_choosable_member(e, w);
if member || (is_prototype(e, w) && !w.has::<Transform>(e)) {
let roots: Vec<Entity> = w
.iter::<Asset>()
.filter(|(_, a)| a.members.iter().any(|&(m, _)| m == e))
.map(|(r, _)| r)
.collect();
for r in roots {
if let Some(a) = w.get_mut::<Asset>(r) {
a.members.retain(|&(m, _)| m != e);
}
}
let placements: Vec<Entity> = w
.iter::<Instance>()
.filter(|(p, i)| i.prototype == e && *p != e)
.map(|(p, _)| p)
.collect();
for p in placements {
crate::ledger::despawn(w, p, log, "its prototype is not present in the resolved state");
log.count("placements of an absent member, despawned", 1);
}
if member {
log.count("asset members despawned", 1);
}
}
crate::ledger::despawn(w, e, log, why);
}
#[derive(Clone, Debug, Default)]
pub struct ResolveState {
pub assign: BTreeMap<String, String>,
pub assign_class: BTreeMap<String, String>,
pub keep: BTreeSet<String>,
}
pub const RESOLVE_STATE: PassSpec = PassSpec {
name: "resolve-state",
options: &[
OptionSpec::optional(
"assign",
"time:noon",
"key:value items, `;` between: each axis's value",
"no axis assigned by its key: each takes its class's assignment, else is kept, else resolves by its default",
),
OptionSpec::optional(
"assign-class",
"event:off",
"class:value items, `;` between: the value of every axis of that class nothing else gives one",
"no class assigned",
),
OptionSpec::optional(
"keep",
"time",
"axis keys, `;` between: the axes left unresolved",
"every axis resolved",
),
],
make: resolve_state,
};
fn resolve_state(g: &Given) -> Result<Box<dyn Pass>, String> {
let items = |v: Option<&str>| -> Vec<String> {
v.unwrap_or_default()
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
};
let pairs = |option: &str, what: &str| -> Result<BTreeMap<String, String>, String> {
let mut out = BTreeMap::new();
for item in items(g.optional(option)) {
let (key, value) = item
.split_once(':')
.ok_or_else(|| format!("{option} item {item:?} is not {what}"))?;
if out
.insert(key.trim().to_owned(), value.trim().to_owned())
.is_some()
{
return Err(format!("{option}: {} twice", key.trim()));
}
}
Ok(out)
};
let assign = pairs("assign", "key:value")?;
let assign_class = pairs("assign-class", "class:value")?;
let mut keep = BTreeSet::new();
for key in items(g.optional("keep")) {
if !keep.insert(key.clone()) {
return Err(format!("keep: {key} twice"));
}
}
if let Some(k) = keep.iter().find(|k| assign.contains_key(*k)) {
return Err(format!("{k} is both assigned and kept"));
}
let pass = ResolveState {
assign,
assign_class,
keep,
};
for (option, value) in pass.options() {
if crate::pass::carries(&value) {
continue;
}
let items: Vec<&str> = value.split(';').collect();
let item = (1..=items.len())
.find(|&n| !crate::pass::carries(&items[..n].join(";")))
.map_or(value.as_str(), |n| items[n - 1]);
return Err(format!(
"{option} item {item:?}: the pipeline text cannot carry it (a ',' at its end or before '=', or a brace)"
));
}
Ok(Box::new(pass))
}
impl Pass for ResolveState {
fn name(&self) -> &'static str {
"resolve-state"
}
fn options(&self) -> Options {
let mut o = Options::new();
if !self.assign.is_empty() {
let items: Vec<String> = self.assign.iter().map(|(k, v)| format!("{k}:{v}")).collect();
o.insert("assign".to_owned(), items.join(";"));
}
if !self.assign_class.is_empty() {
let items: Vec<String> = self
.assign_class
.iter()
.map(|(k, v)| format!("{k}:{v}"))
.collect();
o.insert("assign-class".to_owned(), items.join(";"));
}
if !self.keep.is_empty() {
let keys: Vec<&str> = self.keep.iter().map(String::as_str).collect();
o.insert("keep".to_owned(), keys.join(";"));
}
o
}
fn removes(&self) -> &[&'static str] {
REMOVED
}
fn modifies(&self) -> &[&'static str] {
MODIFIED
}
fn despawns_entities(&self) -> bool {
true
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
let axes: Vec<(Entity, StateAxis)> = w.iter::<StateAxis>().map(|(e, a)| (e, a.clone())).collect();
let by_key: BTreeMap<&str, (Entity, &StateAxis)> =
axes.iter().map(|(e, a)| (a.key.as_str(), (*e, a))).collect();
if by_key.len() != axes.len() {
return Err("two axes share a key".into());
}
let mut values: BTreeMap<Entity, u32> = axes
.iter()
.filter_map(|(e, a)| a.default.map(|d| (*e, d)))
.collect();
let mut explicit = BTreeSet::new();
let resource = w.remove_resource::<StateAssignment>().unwrap_or_default();
for (k, &v) in &resource.0 {
let Some(&(e, a)) = by_key.get(k.as_str()) else {
log.count("assignment keys without an axis", 1);
continue;
};
if v as usize >= a.domain.len() {
return Err(format!(
"state assignment {k}={v} is outside its {} values",
a.domain.len()
));
}
values.insert(e, v);
explicit.insert(e);
}
for (k, text) in &self.assign {
let Some(&(e, a)) = by_key.get(k.as_str()) else {
log.count(&format!("assigned {k}, which no axis has: ignored"), 1);
continue;
};
let v = a
.domain
.parse(text)
.ok_or_else(|| format!("assign {k}:{text} is not one of its values"))?;
values.insert(e, v);
explicit.insert(e);
}
let mut kept = BTreeSet::new();
let mut kept_assignment = StateAssignment::default();
for k in &self.keep {
let Some(&(e, a)) = by_key.get(k.as_str()) else {
log.count(&format!("kept {k}, which no axis has: ignored"), 1);
continue;
};
if let Scope::Local(_) = a.scope {
return Err(format!(
"keep names {k}, which is scoped to an entity: only global axes are kept"
));
}
values.remove(&e);
kept.insert(e);
if let Some(&v) = resource.0.get(k) {
kept_assignment.0.insert(k.clone(), v);
}
log.count(&format!("axis {k} kept unresolved"), 1);
}
for (class, text) in &self.assign_class {
let mut any = false;
for (e, a) in axes
.iter()
.filter(|(e, a)| &a.class == class && !explicit.contains(e) && !kept.contains(e))
{
let v = a
.domain
.parse(text)
.ok_or_else(|| format!("assign-class {class}:{text} is not a value of axis {}", a.key))?;
values.insert(*e, v);
any = true;
}
if any {
log.count(&format!("axes of class {class} assigned {text}"), 1);
} else {
log.count(
&format!("assigned class {class}, which no axis it falls to has: ignored"),
1,
);
}
}
let mut unresolved = BTreeSet::new();
for (e, a) in &axes {
if values.contains_key(e) || kept.contains(e) {
continue;
}
if let Scope::Local(_) = a.scope {
return Err(format!(
"{}: no known default and no value, and scoped to an entity (only global axes stay unresolved): \
assign it",
a.key
));
}
unresolved.insert(*e);
log.count(
&format!("axis {} unresolved: no known default, not assigned", a.key),
1,
);
}
kept.extend(unresolved.iter().copied());
if !kept_assignment.0.is_empty() {
log.count(
"snapshot values kept for kept axes",
kept_assignment.0.len() as u64,
);
w.insert_resource(kept_assignment);
}
let named_by = |e: Entity, clauses: &mut dyn Iterator<Item = &Clause>| {
clauses
.flat_map(|c| c.0.iter().map(|l| l.axis))
.find(|a| !values.contains_key(a) && !kept.contains(a))
.map_or(Ok(()), |a| {
Err(format!("{e:?}: a literal names {a:?}, which is not an axis"))
})
};
for (e, c) in w.iter::<Condition>() {
named_by(e, &mut c.0.iter())?;
}
for (e, o) in w.iter::<StateOverride>() {
named_by(e, &mut o.0.iter().map(|(c, _)| c))?;
}
for (e, pc) in w.iter::<PrimitiveCondition>() {
named_by(e, &mut pc.0.iter().flatten().flat_map(|c| c.0.iter()))?;
}
let axis_of: BTreeMap<Entity, &StateAxis> = axes.iter().map(|(e, a)| (*e, a)).collect();
let label = |e: &Entity| {
let a = axis_of[e];
match values.get(e) {
Some(&v) => format!("{}={}", a.key, a.domain.name(v).unwrap_or("?")),
None if unresolved.contains(e) => format!("{} (unresolved)", a.key),
None => format!("{} (kept)", a.key),
}
};
let conditions: Vec<(Entity, Condition)> =
w.iter::<Condition>().map(|(e, c)| (e, c.clone())).collect();
for (e, c) in &conditions {
let reduced = reduce_condition(c, &values);
let present = !matches!(reduced, Reduced::Never);
if c.0.is_empty() {
log.count("never present", 1);
}
let mut mentioned = Vec::new();
c.references(&mut mentioned);
mentioned.sort();
mentioned.dedup();
for a in &mentioned {
log.count(
&format!("{} {}", label(a), if present { "kept" } else { "despawned" }),
1,
);
}
match reduced {
Reduced::Always => {
w.remove::<Condition>(*e);
}
Reduced::Some(r) => {
w.insert(*e, Condition(r));
log.count("conditions kept on unresolved axes", 1);
}
Reduced::Never => remove_not_present(w, *e, log, "not present in the resolved state"),
}
}
let primitive_conditions: Vec<(Entity, PrimitiveCondition)> = w
.iter::<PrimitiveCondition>()
.map(|(e, c)| (e, c.clone()))
.collect();
for (e, pc) in primitive_conditions {
if !w.is_alive(e) {
continue;
}
let mut keep: Vec<usize> = Vec::new();
let mut conditions: Vec<Option<Condition>> = Vec::new();
for (i, c) in pc.0.iter().enumerate() {
let Some(c) = c else {
keep.push(i);
conditions.push(None);
continue;
};
if c.0.is_empty() {
log.count("primitives never present", 1);
}
let mut mentioned = Vec::new();
c.references(&mut mentioned);
mentioned.sort();
mentioned.dedup();
let reduced = reduce_condition(c, &values);
let present = !matches!(reduced, Reduced::Never);
for a in &mentioned {
log.count(
&format!(
"{} primitives {}",
label(a),
if present { "kept" } else { "dropped" }
),
1,
);
}
match reduced {
Reduced::Always => {
keep.push(i);
conditions.push(None);
}
Reduced::Some(r) => {
keep.push(i);
conditions.push(Some(Condition(r)));
log.count("primitive conditions kept on unresolved axes", 1);
}
Reduced::Never => {}
}
}
if keep.is_empty() {
remove_not_present(
w,
e,
log,
"every primitive's condition fails in the resolved state",
);
continue;
}
let dropped = pc.0.len() - keep.len();
if dropped > 0 {
w.rebuild_primitives(e, |mesh| {
let primitives = mesh.primitives();
let kept = keep
.iter()
.map(|&i| {
primitives.get(i).cloned().ok_or_else(|| {
format!(
"{e:?}: {} primitive conditions for {} primitives",
pc.0.len(),
primitives.len()
)
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok((kept, keep.clone()))
})?;
log.dropped(
Measure::Primitives,
"primitive condition fails in the resolved state",
dropped as u64,
);
log.count("primitives dropped by their condition", dropped as u64);
}
if conditions.iter().all(Option::is_none) {
w.remove::<PrimitiveCondition>(e);
} else {
w.insert(e, PrimitiveCondition(conditions));
}
}
let overrides: Vec<(Entity, StateOverride)> =
w.iter::<StateOverride>().map(|(e, o)| (e, o.clone())).collect();
for (e, o) in overrides {
w.remove::<StateOverride>(e);
let mut reduced: Vec<(Clause, Override)> = Vec::new();
for (c, ov) in &o.0 {
if let Some(r) = c.reduce(&values) {
let last = r.0.is_empty();
reduced.push((r, *ov));
if last {
break;
}
}
}
let ov = match reduced.as_slice() {
[] => continue,
[(c, ov)] if c.0.is_empty() => ov,
_ => {
w.insert(e, StateOverride(reduced));
log.count("overrides kept on unresolved axes", 1);
continue;
}
};
match *ov {
Override::Transform(t) => {
w.insert(e, t);
}
Override::Variant(v) => {
w.get_mut::<Instance>(e)
.ok_or("variant override without an instance")?
.variant = v;
}
Override::Prototype(p) => {
w.get_mut::<Instance>(e)
.ok_or("prototype override without an instance")?
.prototype = p;
}
}
log.count(&format!("override {}", ov.kind()), 1);
}
for e in w.restate_components(&Resolved(&values)) {
if w.is_alive(e) {
remove_not_present(w, e, log, "not present in the resolved state");
}
}
for (e, _) in &axes {
if !kept.contains(e) {
w.remove::<StateAxis>(*e);
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SelectOutfit {
pub choices: BTreeMap<String, String>,
}
pub const SELECT_OUTFIT: PassSpec = PassSpec {
name: "select-outfit",
options: &[OptionSpec::required(
"choices",
"axis:value",
"key:value items, `;` between, at least one: the outfit axes' values",
)],
make: select_outfit,
};
fn select_outfit(g: &Given) -> Result<Box<dyn Pass>, String> {
let mut choices = BTreeMap::new();
for item in g
.required("choices")
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
{
let (key, value) = item
.split_once(':')
.ok_or_else(|| format!("choices item {item:?} is not key:value"))?;
if choices
.insert(key.trim().to_owned(), value.trim().to_owned())
.is_some()
{
return Err(format!("choices: {key} twice"));
}
}
if choices.is_empty() {
return Err("choices: no key:value item".into());
}
Ok(Box::new(SelectOutfit { choices }))
}
impl Pass for SelectOutfit {
fn name(&self) -> &'static str {
"select-outfit"
}
fn options(&self) -> Options {
let items: Vec<String> = self.choices.iter().map(|(k, v)| format!("{k}:{v}")).collect();
Options::from([("choices".to_owned(), items.join(";"))])
}
fn requires(&self) -> &[&'static str] {
&[StateAxis::NAME]
}
fn removes(&self) -> &[&'static str] {
REMOVED
}
fn modifies(&self) -> &[&'static str] {
MODIFIED
}
fn despawns_entities(&self) -> bool {
true
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
let axes: Vec<&StateAxis> = w.iter::<StateAxis>().map(|(_, a)| a).collect();
if let Some(k) = self.choices.keys().find(|k| !axes.iter().any(|a| &a.key == *k)) {
let mut there: Vec<&str> = axes.iter().map(|a| a.key.as_str()).collect();
there.sort_unstable();
let shown = there.iter().take(20).copied().collect::<Vec<_>>().join(", ");
return Err(format!(
"choice {k}: no axis has that key (the world's: {shown}{})",
if there.len() > 20 {
format!(" and {} more", there.len() - 20)
} else {
String::new()
}
));
}
let keep = axes
.iter()
.filter(|a| a.scope == Scope::Global && !self.choices.contains_key(&a.key))
.map(|a| a.key.clone())
.collect();
ResolveState {
assign: self.choices.clone(),
assign_class: BTreeMap::new(),
keep,
}
.run(w, log)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_domain_beyond_the_word_does_not_overflow_a_shift() {
let big = Domain::Enum((0..65).map(|i| format!("v{i}")).collect());
assert_eq!(big.full_mask(), u64::MAX);
assert_eq!(Domain::Bool.full_mask(), 0b11);
let mut w = World::new();
let axis = w.spawn();
let clause = Clause(vec![Literal {
axis,
values: u64::MAX,
}]);
assert_eq!(clause.reduce(&BTreeMap::from([(axis, 64)])), None);
assert_eq!(
clause.reduce(&BTreeMap::from([(axis, 63)])),
Some(Clause(Vec::new()))
);
}
use crate::components::Name;
use crate::pass::{Error, Mode, PassManager, PassRegistry, Pipeline, Target};
fn axis(w: &mut World, key: &str) -> Entity {
let e = w.spawn();
w.insert(
e,
StateAxis {
key: key.into(),
domain: Domain::Bool,
default: Some(0),
scope: Scope::Global,
class: "event".into(),
source: format!("event flag {}", key.rsplit('.').next().unwrap()),
},
);
e
}
fn on(a: Entity) -> Literal {
Literal {
axis: a,
values: 0b10,
}
}
fn off(a: Entity) -> Literal {
Literal {
axis: a,
values: 0b01,
}
}
fn placement(w: &mut World, proto: Entity, name: &str) -> Entity {
let e = w.spawn();
w.insert(e, Transform::IDENTITY);
w.insert(
e,
Instance {
prototype: proto,
variant: 0,
},
);
w.insert(e, Name(name.into()));
e
}
struct Castle {
w: World,
intact: Entity,
burnt: Entity,
f8301: Entity,
}
fn castle() -> Castle {
let mut w = World::new();
let f8301 = axis(&mut w, "fromsoft.flag.8301");
let f8302 = axis(&mut w, "fromsoft.flag.8302");
let f8306 = axis(&mut w, "fromsoft.flag.8306");
let proto = w.spawn();
w.insert(proto, Name("m003312".into()));
let intact = placement(&mut w, proto, "m003312_3802");
w.insert(intact, Condition(vec![Clause(vec![off(f8306), off(f8302)])]));
let burnt = placement(&mut w, proto, "m003412_3902");
w.insert(burnt, Condition(vec![Clause(vec![on(f8302)])]));
let only8301 = placement(&mut w, proto, "m300102_3700");
w.insert(only8301, Condition(vec![Clause(vec![on(f8301)])]));
Castle {
w,
intact,
burnt,
f8301,
}
}
fn run(w: &mut World, assign: &str) -> PassLog {
let opts = if assign.is_empty() {
Options::new()
} else {
Options::from([("assign".to_owned(), assign.to_owned())])
};
let mut log = PassLog::default();
RESOLVE_STATE.instance(&opts).unwrap().run(w, &mut log).unwrap();
log
}
fn names(w: &World) -> Vec<String> {
w.iter::<Instance>()
.map(|(e, _)| w.get::<Name>(e).unwrap().0.clone())
.collect()
}
fn run_keep(w: &mut World, assign: &str, keep: &str) -> Result<PassLog, String> {
let mut opts = Options::from([("keep".to_owned(), keep.to_owned())]);
if !assign.is_empty() {
opts.insert("assign".to_owned(), assign.to_owned());
}
let mut log = PassLog::default();
RESOLVE_STATE.instance(&opts)?.run(w, &mut log)?;
Ok(log)
}
#[derive(Clone, Debug, PartialEq)]
struct Parts(Vec<(&'static str, Option<Clause>)>);
impl Component for Parts {
const NAME: &'static str = "test.parts";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn references(&self, out: &mut Vec<Entity>) {
for (_, c) in &self.0 {
if let Some(c) = c {
out.extend(c.0.iter().map(|l| l.axis));
}
}
}
fn restate(&self, resolved: &Resolved<'_>) -> crate::world::Restated<Self> {
use crate::world::Restated;
let mut parts = Vec::new();
for (name, clause) in &self.0 {
let clause = match clause {
None => None,
Some(c) => match resolved.reduce(c) {
None => continue,
Some(r) if r.0.is_empty() => None,
Some(r) => Some(r),
},
};
parts.push((*name, clause));
}
if parts.is_empty() {
Restated::Absent
} else if parts == self.0 {
Restated::Unchanged
} else {
Restated::Replaced(Parts(parts))
}
}
}
#[test]
fn a_clause_inside_a_component_is_reduced_by_resolving_state() {
let mut c = castle();
let f8302 =
c.w.iter::<StateAxis>()
.find(|(_, a)| a.key == "fromsoft.flag.8302")
.unwrap()
.0;
let record = c.w.spawn();
c.w.insert(
record,
Parts(vec![
("always", None),
("needs 8301", Some(Clause(vec![on(c.f8301)]))),
("not 8301", Some(Clause(vec![off(c.f8301)]))),
(
"8301 off and 8302 on",
Some(Clause(vec![off(c.f8301), on(f8302)])),
),
]),
);
let only = c.w.spawn();
c.w.insert(only, Parts(vec![("needs 8301", Some(Clause(vec![on(c.f8301)])))]));
run_keep(&mut c.w, "", "fromsoft.flag.8302").unwrap();
assert_eq!(
c.w.get::<Parts>(record),
Some(&Parts(vec![
("always", None),
("not 8301", None),
("8301 off and 8302 on", Some(Clause(vec![on(f8302)]))),
]))
);
assert!(!c.w.is_alive(only));
let mut all = castle();
let record = all.w.spawn();
all.w.insert(
record,
Parts(vec![("not 8301", Some(Clause(vec![off(all.f8301)])))]),
);
run(&mut all.w, "");
assert_eq!(all.w.get::<Parts>(record), Some(&Parts(vec![("not 8301", None)])));
}
#[test]
fn kept_axes_stay_with_the_conditions_reduced_to_them() {
let mut c = castle();
let f8302 =
c.w.iter::<StateAxis>()
.find(|(_, a)| a.key == "fromsoft.flag.8302")
.unwrap()
.0;
let log = run_keep(&mut c.w, "", "fromsoft.flag.8302").unwrap();
assert_eq!(log.counts["axis fromsoft.flag.8302 kept unresolved"], 1);
assert_eq!(
c.w.get::<Condition>(c.intact),
Some(&Condition(vec![Clause(vec![off(f8302)])]))
);
assert_eq!(
c.w.get::<Condition>(c.burnt),
Some(&Condition(vec![Clause(vec![on(f8302)])]))
);
assert!(!c.w.is_alive(c.f8301) || !c.w.has::<StateAxis>(c.f8301));
assert_eq!(names(&c.w), vec!["m003312_3802", "m003412_3902"]);
let keys: Vec<String> = c.w.iter::<StateAxis>().map(|(_, a)| a.key.clone()).collect();
assert_eq!(keys, vec!["fromsoft.flag.8302"]);
run(&mut c.w, "fromsoft.flag.8302:on");
assert_eq!(names(&c.w), vec!["m003412_3902"]);
assert!(c.w.iter::<Condition>().next().is_none());
}
#[test]
fn overrides_on_kept_axes_stay_in_order() {
for (assign, want) in [("", None), ("a:on", Some(1))] {
let mut w = World::new();
let a = axis(&mut w, "a");
let k = axis(&mut w, "k");
let proto = w.spawn();
let e = placement(&mut w, proto, "p");
w.insert(
e,
StateOverride(vec![
(Clause(vec![on(a)]), Override::Variant(1)),
(Clause(vec![on(k), off(a)]), Override::Variant(2)),
(Clause(vec![]), Override::Variant(3)),
(Clause(vec![on(k)]), Override::Variant(4)),
]),
);
run_keep(&mut w, assign, "k").unwrap();
match want {
None => assert_eq!(
w.get::<StateOverride>(e),
Some(&StateOverride(vec![
(Clause(vec![on(k)]), Override::Variant(2)),
(Clause(vec![]), Override::Variant(3)),
]))
),
Some(v) => {
assert!(!w.has::<StateOverride>(e));
assert_eq!(w.get::<Instance>(e).unwrap().variant, v);
}
}
}
}
#[test]
fn a_kept_axis_keeps_its_snapshot_value() {
let mut c = castle();
c.w.insert_resource(StateAssignment(BTreeMap::from([
("fromsoft.flag.8302".to_owned(), 1),
("fromsoft.flag.8306".to_owned(), 0),
])));
let log = run_keep(&mut c.w, "", "fromsoft.flag.8302").unwrap();
assert_eq!(log.counts["snapshot values kept for kept axes"], 1);
assert_eq!(
c.w.resource::<StateAssignment>(),
Some(&StateAssignment(BTreeMap::from([(
"fromsoft.flag.8302".to_owned(),
1
)])))
);
run(&mut c.w, "");
assert_eq!(names(&c.w), vec!["m003412_3902"]);
}
#[test]
fn a_missing_axis_is_counted_an_assigned_and_kept_one_is_an_error() {
let mut plain = castle();
run(&mut plain.w, "");
let mut c = castle();
let log = run_keep(&mut c.w, "fromsoft.flag.7777:on", "fromsoft.flag.9999").unwrap();
assert_eq!(names(&c.w), names(&plain.w));
assert_eq!(
log.counts["kept fromsoft.flag.9999, which no axis has: ignored"],
1
);
assert_eq!(
log.counts["assigned fromsoft.flag.7777, which no axis has: ignored"],
1
);
assert!(!c.w.has_component_named(StateAxis::NAME));
let both = Options::from([
("keep".to_owned(), "fromsoft.flag.8302".to_owned()),
("assign".to_owned(), "fromsoft.flag.8302:on".to_owned()),
]);
assert!(RESOLVE_STATE.instance(&both).is_err());
}
#[test]
fn resolve_state_refuses_what_its_text_form_cannot_carry() {
let opts = |k: &str, v: &str| Options::from([(k.to_owned(), v.to_owned())]);
for (option, value, item) in [
("assign", "a:1,", "a:1,"),
("assign", "a:1,b=2", "a:1,b=2"),
("assign", "a:1,;b=c:2", "a:1,"),
("keep", "a;z,", "z,"),
] {
let error = RESOLVE_STATE.instance(&opts(option, value)).err().expect(value);
assert!(error.contains(&format!("{item:?}")), "{value}: {error}");
}
assert!(RESOLVE_STATE.instance(&opts("keep", "k,;x")).is_ok());
let accepted = RESOLVE_STATE
.instance(&opts("assign", "fromsoft.flag.8302:on;gta.ipl.x:off"))
.unwrap();
let text = crate::pass::pass_text(accepted.as_ref());
let mut r = PassRegistry::new();
crate::register(&mut r);
let again = r.parse(&text).unwrap();
assert_eq!(again.passes[0].options(), accepted.options());
}
#[test]
fn castle_defaults_keep_the_intact_walls() {
let mut c = castle();
assert_eq!(c.w.verify_components(), vec![]);
let log = run(&mut c.w, "");
assert_eq!(names(&c.w), ["m003312_3802"]);
assert!(c.w.is_alive(c.intact) && !c.w.is_alive(c.burnt));
assert_eq!(log.counts["fromsoft.flag.8302=off kept"], 1);
assert_eq!(log.counts["fromsoft.flag.8302=off despawned"], 1);
assert_eq!(log.counts["fromsoft.flag.8301=off despawned"], 1);
for name in [StateAxis::NAME, Condition::NAME, StateOverride::NAME] {
assert!(!c.w.has_component_named(name), "{name} left");
}
}
#[test]
fn castle_burnt_by_assignment() {
let mut c = castle();
run(&mut c.w, "fromsoft.flag.8302:on");
assert_eq!(names(&c.w), ["m003412_3902"]);
}
#[test]
fn assign_beats_resource_beats_default() {
let mut c = castle();
c.w.insert_resource(StateAssignment(BTreeMap::from([
("fromsoft.flag.8301".to_owned(), 1),
("fromsoft.flag.8302".to_owned(), 1),
])));
run(&mut c.w, "fromsoft.flag.8302:0");
assert_eq!(names(&c.w), ["m003312_3802", "m300102_3700"]);
assert!(c.w.resource::<StateAssignment>().is_none());
assert!(
c.w.is_alive(c.f8301) && !c.w.has::<StateAxis>(c.f8301),
"axes lose their component, the entity stays"
);
}
#[test]
fn a_class_assignment_reaches_only_the_axes_nothing_else_set() {
let lod_axis = |w: &mut World, key: &str, levels: usize| {
let e = w.spawn();
w.insert(
e,
StateAxis {
key: key.into(),
domain: Domain::Enum((0..levels).map(|i| format!("lod{i}")).collect()),
default: None,
scope: Scope::Global,
class: "lod".into(),
source: key.into(),
},
);
e
};
let world = || {
let mut w = World::new();
let proto = w.spawn();
w.insert(proto, Name("proto".into()));
let (a, b) = (lod_axis(&mut w, "lod.a", 3), lod_axis(&mut w, "lod.b", 2));
for (axis, level, name) in [(a, 2, "a2"), (b, 0, "b0")] {
let p = placement(&mut w, proto, name);
w.insert(
p,
Condition(vec![Clause(vec![Literal {
axis,
values: 1 << level,
}])]),
);
}
w
};
let resolve = |w: &mut World, assign: &[(&str, &str)]| {
ResolveState {
assign: assign.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
assign_class: [("lod".to_owned(), "lod2".to_owned())].into(),
..Default::default()
}
.run(w, &mut PassLog::default())
};
let mut w = world();
resolve(&mut w, &[("lod.b", "lod0")]).unwrap();
let mut left: Vec<String> = w.iter::<Name>().map(|(_, n)| n.0.clone()).collect();
left.sort();
assert_eq!(left, ["a2", "b0", "proto"]);
let e = resolve(&mut world(), &[]).unwrap_err();
assert!(
e.contains("assign-class lod:lod2 is not a value of axis lod.b"),
"{e}"
);
}
#[test]
fn a_save_with_unrelated_flags_is_fine() {
let mut c = castle();
c.w.insert_resource(StateAssignment(BTreeMap::from([
("fromsoft.flag.8302".to_owned(), 1),
("fromsoft.flag.11110100".to_owned(), 1),
("fromsoft.flag.6000".to_owned(), 0),
])));
let log = run(&mut c.w, "");
assert_eq!(names(&c.w), ["m003412_3902"]);
assert_eq!(log.counts["assignment keys without an axis"], 2);
}
#[test]
fn run_on_an_unverified_world_errs_instead_of_panicking() {
let mut w = World::new();
let proto = w.spawn();
let not_axis = w.spawn();
let e = placement(&mut w, proto, "p");
w.insert(
e,
Condition(vec![Clause(vec![Literal {
axis: not_axis,
values: 1,
}])]),
);
let err = RESOLVE_STATE
.instance(&Options::new())
.unwrap()
.run(&mut w, &mut PassLog::default())
.unwrap_err();
assert!(err.contains("not an axis"), "{err}");
let mut w = World::new();
let e = placement(&mut w, proto, "p");
w.insert(
e,
StateOverride(vec![(
Clause(vec![Literal {
axis: not_axis,
values: 1,
}]),
Override::Variant(1),
)]),
);
assert!(
RESOLVE_STATE
.instance(&Options::new())
.unwrap()
.run(&mut w, &mut PassLog::default())
.is_err()
);
}
#[test]
fn an_unknown_default_stays_unresolved_unless_assigned() {
let unknown = |c: &mut Castle| {
let (e, _) =
c.w.iter::<StateAxis>()
.find(|(_, a)| a.key == "fromsoft.flag.8302")
.unwrap();
c.w.get_mut::<StateAxis>(e).unwrap().default = None;
e
};
let mut c = castle();
let f8302 = unknown(&mut c);
assert_eq!(c.w.verify_components(), vec![]);
let log = run(&mut c.w, "");
assert_eq!(names(&c.w), ["m003312_3802", "m003412_3902"]);
assert_eq!(
log.counts["axis fromsoft.flag.8302 unresolved: no known default, not assigned"],
1
);
assert_eq!(log.counts["fromsoft.flag.8302 (unresolved) kept"], 2);
assert_eq!(log.counts["conditions kept on unresolved axes"], 2);
let axes: Vec<&str> = c.w.iter::<StateAxis>().map(|(_, a)| a.key.as_str()).collect();
assert_eq!(axes, ["fromsoft.flag.8302"]);
assert_eq!(
c.w.get::<Condition>(c.intact),
Some(&Condition(vec![Clause(vec![off(f8302)])]))
);
assert_eq!(c.w.verify_components(), vec![]);
run(&mut c.w, "fromsoft.flag.8302:on");
assert_eq!(names(&c.w), ["m003412_3902"]);
assert!(!c.w.has_component_named(StateAxis::NAME));
for given in ["assign", "snapshot"] {
let mut c = castle();
unknown(&mut c);
if given == "snapshot" {
c.w.insert_resource(StateAssignment(BTreeMap::from([(
"fromsoft.flag.8302".to_owned(),
0,
)])));
run(&mut c.w, "");
} else {
run(&mut c.w, "fromsoft.flag.8302:off");
}
assert_eq!(names(&c.w), ["m003312_3802"], "{given}");
assert!(!c.w.has_component_named(StateAxis::NAME), "{given}");
}
let mut c = castle();
let e = unknown(&mut c);
let door = c.intact;
c.w.get_mut::<StateAxis>(e).unwrap().scope = Scope::Local(door);
let err = RESOLVE_STATE
.instance(&Options::new())
.unwrap()
.run(&mut c.w, &mut PassLog::default())
.unwrap_err();
assert!(err.contains("assign it"), "{err}");
}
#[test]
fn values_outside_the_domain_and_bad_options_are_errors() {
for bad in ["fromsoft.flag.8302:maybe", "fromsoft.flag.8302:2"] {
let mut c = castle();
let opts = Options::from([("assign".to_owned(), bad.to_owned())]);
assert!(
RESOLVE_STATE
.instance(&opts)
.unwrap()
.run(&mut c.w, &mut PassLog::default())
.is_err(),
"{bad}"
);
}
let mut c = castle();
c.w.insert_resource(StateAssignment(BTreeMap::from([(
"fromsoft.flag.8302".to_owned(),
2,
)])));
assert!(
RESOLVE_STATE
.instance(&Options::new())
.unwrap()
.run(&mut c.w, &mut PassLog::default())
.is_err()
);
assert!(
RESOLVE_STATE
.instance(&Options::from([("assign".to_owned(), "x".to_owned())]))
.is_err()
);
assert!(
RESOLVE_STATE
.instance(&Options::from([("colour".to_owned(), "red".to_owned())]))
.is_err()
);
}
#[test]
fn empty_clause_always_zero_clauses_never() {
let mut w = World::new();
let proto = w.spawn();
let always = placement(&mut w, proto, "always");
w.insert(always, Condition(vec![Clause::default()]));
let never = placement(&mut w, proto, "never");
w.insert(never, Condition(vec![]));
assert_eq!(w.verify_components(), vec![]);
let log = run(&mut w, "");
assert_eq!(names(&w), ["always"]);
assert_eq!(log.counts["never present"], 1);
}
#[test]
fn overrides_first_match_applies() {
let mut w = World::new();
let door = axis(&mut w, "fromsoft.door.1110432");
let proto = w.spawn();
let open = w.spawn();
w.insert(open, Name("door open".into()));
let e = placement(&mut w, proto, "door");
let mut moved = Transform::IDENTITY;
moved.cols[3] = [1.0, 0.0, 0.0];
w.insert(
e,
StateOverride(vec![
(Clause(vec![on(door)]), Override::Prototype(open)),
(Clause(vec![on(door)]), Override::Transform(moved)),
(Clause::default(), Override::Variant(3)),
]),
);
assert_eq!(w.verify_components(), vec![]);
let mut a = w.clone();
let log = run(&mut a, "fromsoft.door.1110432:on");
assert_eq!(a.get::<Instance>(e).unwrap().prototype, open);
assert_eq!(
*a.get::<Transform>(e).unwrap(),
Transform::IDENTITY,
"only the first match applies"
);
assert_eq!(log.counts["override prototype"], 1);
run(&mut w, "");
assert_eq!(
*w.get::<Instance>(e).unwrap(),
Instance {
prototype: proto,
variant: 3
}
);
}
#[test]
fn verify_rejects_each_rule() {
let failing = |build: &dyn Fn(&mut World) -> (&'static str, Entity, String)| {
let mut w = World::new();
let (component, entity, message) = build(&mut w);
assert_eq!(
w.verify_components(),
[(component, entity, message)],
"one failure, the rule's own"
);
};
let mut good = StateAxis {
key: "k".into(),
domain: Domain::Bool,
default: Some(0),
scope: Scope::Global,
class: "event".into(),
source: String::new(),
};
let with_axis = move |w: &mut World, a: StateAxis| {
let e = w.spawn();
w.insert(e, a);
e
};
failing(&|w| {
let e = with_axis(
w,
StateAxis {
key: String::new(),
..good.clone()
},
);
(
"core.state_axis",
e,
r#"key "" is empty or holds ':' or ';' (the assign syntax)"#.to_owned(),
)
});
failing(&|w| {
let e = with_axis(
w,
StateAxis {
default: Some(2),
..good.clone()
},
);
(
"core.state_axis",
e,
"k: default 2 outside its 2 values".to_owned(),
)
});
failing(&|w| {
let e = with_axis(
w,
StateAxis {
domain: Domain::Enum(vec![]),
default: Some(0),
..good.clone()
},
);
(
"core.state_axis",
e,
"k: 0 values, the domain holds 1..=64".to_owned(),
)
});
failing(&|w| {
let names = (0..65).map(|i| format!("v{i}")).collect();
let e = with_axis(
w,
StateAxis {
domain: Domain::Enum(names),
..good.clone()
},
);
(
"core.state_axis",
e,
"k: 65 values, the domain holds 1..=64".to_owned(),
)
});
failing(&|w| {
let e = with_axis(
w,
StateAxis {
domain: Domain::Enum(vec!["a".into(), "a".into()]),
..good.clone()
},
);
(
"core.state_axis",
e,
r#"k: value name "a" empty, repeated or holding ':' or ';'"#.to_owned(),
)
});
failing(&|w| {
let e = with_axis(
w,
StateAxis {
domain: Domain::Enum(vec!["a:b".into()]),
..good.clone()
},
);
(
"core.state_axis",
e,
r#"k: value name "a:b" empty, repeated or holding ':' or ';'"#.to_owned(),
)
});
failing(&|w| {
let e = with_axis(
w,
StateAxis {
key: "k;x".into(),
..good.clone()
},
);
(
"core.state_axis",
e,
r#"key "k;x" is empty or holds ':' or ';' (the assign syntax)"#.to_owned(),
)
});
failing(&|w| {
let gone = w.spawn();
w.despawn(gone);
let e = with_axis(
w,
StateAxis {
scope: Scope::Local(gone),
..good.clone()
},
);
(
"core.state_axis",
e,
format!("refers to {gone:?}, which is not alive"),
)
});
let cond = |w: &mut World, lit: Literal, transform: bool| {
let e = w.spawn();
if transform {
w.insert(e, Transform::IDENTITY);
}
w.insert(e, Condition(vec![Clause(vec![lit])]));
e
};
failing(&|w| {
let a = with_axis(w, good.clone());
let e = cond(w, Literal { axis: a, values: 0 }, true);
("core.condition", e, "k: empty value mask".to_owned())
});
failing(&|w| {
let a = with_axis(w, good.clone());
let e = cond(
w,
Literal {
axis: a,
values: 0b100,
},
true,
);
(
"core.condition",
e,
"k: mask 0x4 has values beyond its 2".to_owned(),
)
});
failing(&|w| {
let not_axis = w.spawn();
w.insert(not_axis, Name("x".into()));
let e = cond(
w,
Literal {
axis: not_axis,
values: 1,
},
true,
);
(
"core.condition",
e,
format!("literal on {not_axis:?}, which is not a live axis"),
)
});
failing(&|w| {
let a = with_axis(w, good.clone());
let e = cond(w, on(a), false);
(
"core.condition",
e,
"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(),
)
});
failing(&|w| {
let e = w.spawn();
w.insert(e, Transform::IDENTITY);
w.insert(e, StateOverride(vec![(Clause::default(), Override::Variant(1))]));
(
"core.state_override",
e,
"variant override on an entity without an instance".to_owned(),
)
});
failing(&|w| {
let proto = w.spawn();
let gone = w.spawn();
w.despawn(gone);
let e = placement(w, proto, "p");
w.insert(
e,
StateOverride(vec![(Clause::default(), Override::Prototype(gone))]),
);
(
"core.state_override",
e,
format!("refers to {gone:?}, which is not alive"),
)
});
failing(&|w| {
let proto = w.spawn();
let e = placement(w, proto, "p");
let mut t = Transform::IDENTITY;
t.cols[3][0] = f32::NAN;
w.insert(
e,
StateOverride(vec![(Clause::default(), Override::Transform(t))]),
);
(
"core.state_override",
e,
"transform: value 9 is NaN, not finite".to_owned(),
)
});
good.domain = Domain::Enum((0..64).map(|i| format!("v{i}")).collect());
let mut w = World::new();
let a = with_axis(&mut w, good);
let e = w.spawn();
w.insert(e, Transform::IDENTITY);
w.insert(
e,
Condition(vec![Clause(vec![Literal {
axis: a,
values: u64::MAX,
}])]),
);
assert_eq!(w.verify_components(), vec![]);
}
#[test]
fn references_reach_axes_and_targets() {
let c = castle();
let reached = c.w.closure([c.intact]);
let mut keys: Vec<&str> =
c.w.iter::<StateAxis>()
.filter(|(e, _)| reached.contains(e))
.map(|(_, a)| a.key.as_str())
.collect();
keys.sort_unstable();
assert_eq!(keys, ["fromsoft.flag.8302", "fromsoft.flag.8306"]);
}
fn registry() -> PassRegistry {
let mut r = PassRegistry::new();
r.add(&RESOLVE_STATE);
r
}
fn skate() -> Target {
Target::new("skate")
.legal(Transform::NAME)
.legal(Instance::NAME)
.legal(Name::NAME)
.illegal(Condition::NAME)
.illegal(StateOverride::NAME)
.illegal(StateAxis::NAME)
}
fn dump(w: &World) -> String {
let mut s = String::new();
for e in w.entities() {
for c in w.components_json(e).unwrap() {
s += &format!("{e:?} {} {} {:?}\n", c.name, c.version, c.value);
}
}
for r in w.resources_json().unwrap() {
s += &format!("resource {} {} {:?}\n", r.name, r.version, r.value);
}
s
}
#[test]
fn stateless_world_is_untouched() {
let mut c = castle();
run(&mut c.w, "");
let before = dump(&c.w);
let m = PassManager::new(registry());
m.convert(&mut c.w, &Pipeline::default(), &skate(), Mode::Partial)
.unwrap();
assert_eq!(dump(&c.w), before);
let resolve = m.registry.parse("resolve-state").unwrap();
m.convert(&mut c.w, &resolve, &skate(), Mode::Partial).unwrap();
assert_eq!(dump(&c.w), before);
}
#[test]
fn a_writer_that_cannot_toggle_needs_resolve_state_stated() {
let m = PassManager::new(registry());
let err = m
.convert(&mut castle().w, &Pipeline::default(), &skate(), Mode::Partial)
.unwrap_err();
assert!(
matches!(&err, Error::Illegal { name, lowered_by, .. } if name == Condition::NAME && lowered_by.is_empty()),
"{err}"
);
let mut c = castle();
let resolve = m.registry.parse("resolve-state").unwrap();
let report = m.convert(&mut c.w, &resolve, &skate(), Mode::Partial).unwrap();
assert_eq!(report.pipeline, "resolve-state");
assert_eq!(names(&c.w), ["m003312_3802"]);
}
}