diff --git a/Cargo.toml b/Cargo.toml index ab560a2..f70f85a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aery" -version = "0.2.0" +version = "0.3.0" edition = "2021" authors = ["iiYese iiyese@outlook.com"] repository = "https://github.com/iiYese/aery" @@ -12,6 +12,7 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bevy = "0.10" +bevy = "0.11" indexmap = "1.9.3" -aery_macros = { path = "macros", version = "0.1.0-dev" } +aery_macros = { path = "macros", version = "0.2.0-dev" } +aquamarine = "0.3.2" diff --git a/README.md b/README.md index dce0fd9..aff053d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,31 @@ ## Aery -Non-fragmenting (slight misnomer) ZST relations for Bevy. +A plugin that adds a subset of Entity Relationship features to Bevy using Non-fragmenting +ZST relations. [![Crates.io](https://img.shields.io/crates/v/aery)](https://crates.io/crates/aery) [![Docs.rs](https://img.shields.io/docsrs/aery)](https://docs.rs/aery/latest/aery/) +### Currently supported: +- ZST relations +- Fragmenting on (relation) type +- Cleanup policies +- Declarative APIs for: + - Joining + - Traversing + - Spawning + +### API tour: +Non exhaustive. Covers most common parts. + ```rust use bevy::prelude::*; use aery::prelude::*; fn main() { App::new() - .add_plugin(Aery) - .add_startup_system(setup) - .add_system(sys) + .add_plugins(Aery) + .add_systems(Startup, setup) + .add_systems(Update, sys) .run(); } @@ -23,53 +36,60 @@ struct Foo; struct Bar; #[derive(Relation)] -struct R0; +#[cleanup(policy = "Recursive")] +struct ChildOf; #[derive(Relation)] -#[cleanup(policy = "Recursive")] -struct R1; +#[multi] +struct Bag; +// Spawning entities with relations fn setup(mut commands: Commands) { - let (root, foo0, foo1, bar0, bar1) = ( - commands.spawn(Foo).id(), - commands.spawn(Foo).id(), - commands.spawn(Foo).id(), - commands.spawn(Bar).id(), - commands.spawn(Bar).id(), - ); + // A hierarchy of Foos with (chocolate? OwO) Bars in their Bags + commands.add(|wrld: &mut World| { + wrld.spawn(Foo) + .scope::(|_, mut child| { + child.insert(Foo); + child.scope_target::(|_, mut bag| { bag.insert(Bar); }); + }) + .scope::(|_, mut child| { + child.insert(Foo); + child.scope_target::(|_, mut bag| { bag.insert(Bar); }); + }); + }) +} - commands.set::(foo0, bar0); - commands.set::(foo1, bar1); - commands.set::(foo0, root); - commands.set::(foo1, root); +// Listening for relation events +fn alert(mut events: EventReader) { + for event in events.iter() { + if event.matches(Wc, TargetOp::Set, ChildOf, Wc) { + println!("{:?} was added as a child of {:?}", event.host, event.target); + } + } } +// Relation Queries fn sys( - foos: Query<(&Foo, Relations<(R0, R1)>)>, + foos: Query<(&Foo, Relations<(Bag, ChildOf)>)>, + roots: Query>, bars: Query<&Bar>, - r1_roots: Query> ) { foos.ops() - .join::(&bars) - .breadth_first::(r1_roots.iter()) - .for_each(|foo_ancestor, foo, bar| { + .join::(&bars) + .traverse::(roots.iter()) + .for_each(|foo_parent, foo, bar| { // .. }) } ``` -### What is supported: -- ZST relations -- Fragmenting on (relation) type -- Declarative joining & traversing -- Explicit despawn cleanup - -### What is not supported: -- Fragmenting on target -- Target querying -- Implicit despawn cleanup - ### Version table | Bevy version | Aery verison | |--------------|--------------| +| 0.11 | 0.3 | | 0.10 | 0.1 - 0.2 | + +### Credits +- [Sander Mertens](https://github.com/SanderMertens): +Responsible for pioneering Entity Relationships in ECS and the author of Flecs which Aery has taken +a lot of inspiration from. diff --git a/macros/Cargo.toml b/macros/Cargo.toml index c54b38c..c2d0972 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aery_macros" description = "Proc macros for Aery." -version = "0.1.0-dev" +version = "0.2.0-dev" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/macros/src/lib.rs b/macros/src/lib.rs index d61e487..5dc643e 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -3,12 +3,16 @@ use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use syn::{parse_macro_input, DeriveInput, Ident, LitStr, Result}; -#[proc_macro_derive(Relation, attributes(multi, cleanup))] +#[proc_macro_derive(Relation, attributes(multi, symmetric, cleanup))] pub fn relation_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ident = &ast.ident; let exclusive = !ast.attrs.iter().any(|attr| attr.path().is_ident("multi")); + let symmetric = ast + .attrs + .iter() + .any(|attr| attr.path().is_ident("symmetric")); let cleanup = match parse_cleanup_attr(&ast) { Ok(cleanup) => cleanup, @@ -19,7 +23,10 @@ pub fn relation_derive(input: TokenStream) -> TokenStream { impl Relation for #ident { const CLEANUP_POLICY: CleanupPolicy = CleanupPolicy::#cleanup; const EXCLUSIVE: bool = #exclusive; + const SYMMETRIC: bool = #symmetric; } + + const _: () = <#ident as aery::relation::ZstOrPanic>::ZST_OR_PANIC; }; output.into() diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..252796d --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,1291 @@ +use crate::{ + events::{CleanupEvent, TargetEvent, TargetOp}, + relation::{CleanupPolicy, Edges, Participant, Relation, RelationId, RootMarker, ZstOrPanic}, +}; + +use bevy::{ + ecs::{ + entity::Entity, + system::{Command, Resource}, + world::{EntityMut, World}, + }, + log::warn, + utils::{HashMap, HashSet}, +}; + +use indexmap::IndexSet; +use std::{collections::VecDeque, marker::PhantomData}; + +pub(crate) fn refragment(world: &mut World, entity: Entity) { + let Some(mut edges) = world.get_mut::(entity) else { + return + }; + + let (has_hosts, has_targets) = ( + edges.hosts[R::CLEANUP_POLICY as usize] + .get(&RelationId::of::()) + .map(|hosts| !hosts.is_empty()) + .unwrap_or(false), + edges.targets[R::CLEANUP_POLICY as usize] + .get(&RelationId::of::()) + .map(|targets| !targets.is_empty()) + .unwrap_or(false), + ); + + if !has_hosts { + edges.hosts[R::CLEANUP_POLICY as usize].remove(&RelationId::of::()); + } + + if !has_targets { + edges.targets[R::CLEANUP_POLICY as usize].remove(&RelationId::of::()); + } + + if edges + .targets + .iter() + .chain(edges.hosts.iter()) + .all(HashMap::is_empty) + { + world.entity_mut(entity).remove::(); + } + + match (has_hosts, has_targets) { + (_, true) => { + world + .entity_mut(entity) + .remove::>() + .insert(Participant:: { + _phantom: PhantomData, + }); + } + (true, false) => { + world + .entity_mut(entity) + .remove::>() + .insert(RootMarker:: { + _phantom: PhantomData, + }); + } + (false, false) => { + world + .entity_mut(entity) + .remove::<(Participant, RootMarker)>(); + } + } +} + +#[derive(Resource, Default)] +pub(crate) struct RefragmentHooks { + hooks: HashMap, +} + +/// Command to set a relationship target for an entity. If either of the participants do not exist +/// or the host tries to target itself the operation will be ignored and logged. +pub struct Set +where + R: Relation, +{ + pub host: Entity, + pub target: Entity, + pub _phantom: PhantomData, +} + +impl Set { + pub fn new(host: Entity, target: Entity) -> Self { + Self { + host, + target, + _phantom: PhantomData, + } + } +} + +impl Command for Set +where + R: Relation, +{ + #[allow(clippy::let_unit_value)] + fn apply(self, world: &mut World) { + let _ = R::ZST_OR_PANIC; + + if self.host == self.target { + warn!( + "{host:?} Tried to target to itself with {rel}. \ + Self referential relations are not allowed. \ + Ignoring.", + host = self.host, + rel = std::any::type_name::(), + ); + return; + } + + if world.get_entity(self.target).is_none() { + warn!( + "{host:?} tried to target {target:?} with {rel}. \ + {target:?} does not exist. \ + Ignoring.", + host = self.host, + target = self.target, + rel = std::any::type_name::(), + ); + return; + } + + if world.get_entity(self.host).is_none() { + warn!( + "{host:?} tried to target {target:?} with {rel}. \ + {host:?} does not exist. \ + Ignoring.", + host = self.host, + target = self.target, + rel = std::any::type_name::(), + ); + return; + } + + world + .resource_mut::() + .hooks + .insert(RelationId::of::(), refragment::); + + let mut target_edges = world + .get_mut::(self.target) + .map(|mut edges| std::mem::take(&mut *edges)) + .unwrap_or_default(); + + target_edges.hosts[R::CLEANUP_POLICY as usize] + .entry(RelationId::of::()) + .or_default() + .insert(self.host); + + let symmetric_set = R::SYMMETRIC + && !target_edges.targets[R::CLEANUP_POLICY as usize] + .entry(RelationId::of::()) + .or_default() + .contains(&self.host); + + if !target_edges.targets[R::CLEANUP_POLICY as usize].contains_key(&RelationId::of::()) { + world.entity_mut(self.target).insert(RootMarker:: { + _phantom: PhantomData, + }); + } + + world.entity_mut(self.target).insert(target_edges); + + let mut host_edges = world + .entity_mut(self.host) + .remove::>() + .get_mut::() + .map(|mut edges| std::mem::take(&mut *edges)) + .unwrap_or_default(); + + let old = host_edges.targets[R::CLEANUP_POLICY as usize] + .get(&RelationId::of::()) + .and_then(|targets| targets.first()) + .copied(); + + host_edges.targets[R::CLEANUP_POLICY as usize] + .entry(RelationId::of::()) + .or_default() + .insert(self.target); + + world.entity_mut(self.host).insert(( + host_edges, + Participant:: { + _phantom: PhantomData, + }, + )); + + world.send_event(TargetEvent { + host: self.host, + target_op: TargetOp::Set, + target: self.target, + relation_id: RelationId::of::(), + }); + + if symmetric_set { + Command::apply( + Set:: { + host: self.target, + target: self.host, + _phantom: PhantomData, + }, + world, + ); + } + + if let Some(old) = old.filter(|old| R::EXCLUSIVE && self.target != *old) { + Command::apply( + UnsetAsymmetric:: { + host: self.host, + target: old, + _phantom: PhantomData, + }, + world, + ); + } + } +} + +/// Command to remove relationships between entities +/// This operation is not noisy so if either participant does not exist or +/// the relation does not exist nothing happens. +pub struct Unset +where + R: Relation, +{ + pub host: Entity, + pub target: Entity, + pub _phantom: PhantomData, +} + +impl Unset { + pub fn new(host: Entity, target: Entity) -> Self { + Self { + host, + target, + _phantom: PhantomData, + } + } +} + +impl Command for Unset { + #[allow(clippy::let_unit_value)] + fn apply(self, world: &mut World) { + let _ = R::ZST_OR_PANIC; + + Command::apply( + UnsetAsymmetric:: { + host: self.host, + target: self.target, + _phantom: PhantomData, + }, + world, + ); + + if R::SYMMETRIC { + Command::apply( + UnsetAsymmetric:: { + host: self.target, + target: self.host, + _phantom: PhantomData, + }, + world, + ); + } + } +} + +struct UnsetAsymmetric { + host: Entity, + target: Entity, + _phantom: PhantomData, +} + +impl Command for UnsetAsymmetric { + fn apply(self, world: &mut World) { + let Some(refragment) = world + .resource::() + .hooks + .get(&RelationId::of::()) + .copied() + else { + return + }; + + let Some(mut host_edges) = world + .get_mut::(self.host) + .map(|mut edges| std::mem::take(&mut *edges)) + else { + return + }; + + let Some(mut target_edges) = world + .get_mut::(self.target) + .map(|mut edges| std::mem::take(&mut *edges)) + else { + world.entity_mut(self.host).insert(host_edges); + return + }; + + host_edges.targets[R::CLEANUP_POLICY as usize] + .entry(RelationId::of::()) + .and_modify(|hosts| { + hosts.remove(&self.target); + }); + + target_edges.hosts[R::CLEANUP_POLICY as usize] + .entry(RelationId::of::()) + .and_modify(|hosts| { + hosts.remove(&self.host); + }); + + let target_orphaned = target_edges.hosts[R::CLEANUP_POLICY as usize] + .get(&RelationId::of::()) + .map_or(false, IndexSet::is_empty); + + world.entity_mut(self.host).insert(host_edges); + world.entity_mut(self.target).insert(target_edges); + + if target_orphaned + && matches!( + R::CLEANUP_POLICY, + CleanupPolicy::Counted | CleanupPolicy::Total + ) + { + Command::apply( + CheckedDespawn { + entity: self.target, + }, + world, + ); + } + + world.send_event(TargetEvent { + host: self.host, + target_op: TargetOp::Unset, + target: self.target, + relation_id: RelationId::of::(), + }); + + refragment(world, self.host); + refragment(world, self.target); + } +} + +/// Command for entities to untarget all of their relations of a given type. +pub struct UnsetAll +where + R: Relation, +{ + pub entity: Entity, + pub _phantom: PhantomData, +} + +impl Command for UnsetAll { + #[allow(clippy::let_unit_value)] + fn apply(self, world: &mut World) { + while let Some(target) = world + .get::(self.entity) + .and_then(|edges| edges.targets[R::CLEANUP_POLICY as usize].get(&RelationId::of::())) + .and_then(|targets| targets.first()) + .copied() + { + let _ = R::ZST_OR_PANIC; + + Command::apply( + Unset:: { + target, + host: self.entity, + _phantom: PhantomData, + }, + world, + ); + } + } +} + +/// Command for entities to remove themselves as the target of all relations of a given type. +pub struct Withdraw +where + R: Relation, +{ + pub entity: Entity, + pub _phantom: PhantomData, +} + +impl Command for Withdraw { + #[allow(clippy::let_unit_value)] + fn apply(self, world: &mut World) { + while let Some(host) = world + .get::(self.entity) + .and_then(|edges| edges.hosts[R::CLEANUP_POLICY as usize].get(&RelationId::of::())) + .and_then(|targets| targets.first()) + .copied() + { + let _ = R::ZST_OR_PANIC; + + Command::apply( + Unset:: { + host, + target: self.entity, + _phantom: PhantomData, + }, + world, + ); + } + } +} + +/// Command to despawn entities with rleations. Despawning via any other method can lead to +/// dangling which will not produce correct behavior! +pub struct CheckedDespawn { + pub entity: Entity, +} + +impl Command for CheckedDespawn { + fn apply(self, world: &mut World) { + let mut to_refrag = HashMap::>::new(); + let mut to_despawn = HashSet::::from([self.entity]); + let mut queue = VecDeque::from([self.entity]); + + let mut graph = world.query::<&mut Edges>(); + + while let Some(curr) = queue.pop_front() { + let Ok(edges) = graph + .get_mut(world, curr) + .map(|mut edges| std::mem::take(&mut *edges)) + else { + continue + }; + + // Total relations + for (typeid, target) in edges.targets[CleanupPolicy::Total as usize] + .iter() + .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) + { + let Ok(mut target_edges) = graph.get_mut(world, *target) else { + continue + }; + + let Some(target_hosts) = target_edges + .hosts[CleanupPolicy::Total as usize] + .get_mut(typeid) + else { + continue + }; + + target_hosts.remove(&curr); + + if target_hosts.is_empty() { + queue.push_back(*target); + to_despawn.insert(*target); + } + } + + for (typeid, host) in edges.hosts[CleanupPolicy::Total as usize] + .iter() + .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) + { + let Ok(mut host_edges) = graph.get_mut(world, *host) else { + continue + }; + + host_edges.targets[CleanupPolicy::Total as usize] + .entry(*typeid) + .and_modify(|bucket| { + bucket.remove(&curr); + }); + + queue.push_back(*host); + to_despawn.insert(*host); + } + + // Recursive relations + for (typeid, target) in edges.targets[CleanupPolicy::Recursive as usize] + .iter() + .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) + { + let Ok(mut target_edges) = graph.get_mut(world, *target) else { + continue + }; + + let Some(target_hosts) = target_edges + .hosts[CleanupPolicy::Recursive as usize] + .get_mut(typeid) + else { + continue + }; + + target_hosts.remove(&curr); + to_refrag.entry(*typeid).or_default().insert(*target); + } + + for (typeid, host) in edges.hosts[CleanupPolicy::Recursive as usize] + .iter() + .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) + { + let Ok(mut host_edges) = graph.get_mut(world, *host) else { + continue + }; + + host_edges.targets[CleanupPolicy::Recursive as usize] + .entry(*typeid) + .and_modify(|bucket| { + bucket.remove(&curr); + }); + + queue.push_back(*host); + to_despawn.insert(*host); + } + + // Counted relations + for (typeid, target) in edges.targets[CleanupPolicy::Counted as usize] + .iter() + .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) + { + let Ok(mut target_edges) = graph.get_mut(world, *target) else { + continue + }; + + let Some(target_hosts) = target_edges + .hosts[CleanupPolicy::Counted as usize] + .get_mut(typeid) + else { + continue + }; + + target_hosts.remove(&curr); + + if target_hosts.is_empty() { + queue.push_back(*target); + to_despawn.insert(*target); + } + } + + for (typeid, host) in edges.hosts[CleanupPolicy::Counted as usize] + .iter() + .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) + { + let Ok(mut host_edges) = graph.get_mut(world, *host) else { + continue + }; + + host_edges.targets[CleanupPolicy::Counted as usize] + .entry(*typeid) + .and_modify(|bucket| { + bucket.remove(&curr); + }); + + to_refrag.entry(*typeid).or_default().insert(*host); + } + + // Orphaning relations + for (typeid, target) in edges.targets[CleanupPolicy::Orphan as usize] + .iter() + .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) + { + let Ok(mut target_edges) = graph.get_mut(world, *target) else { + continue + }; + + target_edges.hosts[CleanupPolicy::Orphan as usize] + .entry(*typeid) + .and_modify(|bucket| { + bucket.remove(&curr); + }); + + to_refrag.entry(*typeid).or_default().insert(*target); + } + + for (typeid, host) in edges.hosts[CleanupPolicy::Orphan as usize] + .iter() + .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) + { + let Ok(mut host_edges) = graph.get_mut(world, *host) else { + continue + }; + + host_edges.targets[CleanupPolicy::Orphan as usize] + .entry(*typeid) + .and_modify(|bucket| { + bucket.remove(&curr); + }); + + to_refrag.entry(*typeid).or_default().insert(*host); + } + } + + for entity in to_despawn { + world.despawn(entity); + world.send_event(CleanupEvent { entity }); + } + + for (typeid, entities) in to_refrag { + let refrag = world + .resource::() + .hooks + .get(&typeid) + .copied() + .unwrap(); + + for entity in entities { + refrag(world, entity); + } + } + } +} + +/// An extension API for `EntityMut<'_>` to sugar using relation commands. +/// Since changing relations can trigger cleanup procedures that might despawn the `Entity` referred +/// to by `EntytMut<'_>` each method is consuming and returns an `Option>`. +/// +/// For convenience this trait is also implemented for `Option>`. Where the methods +/// are essentially their non-option equivalent wrapped in an implicit [`Option::and_then`] call. +/// `Option::>::set` will emit a warning if called on an option that is `None`. +/// All of the other functions will not emit a warning as unsetting relations that don't exist and +/// despawning entities that don't exist are not considered an error. +/// +/// See [`Scope`] for extension APIs that can operate on relation participants including spawning +/// them. +pub trait RelationCommands<'a>: Sized { + fn set(self, target: Entity) -> Option>; + fn unset(self, target: Entity) -> Option>; + fn unset_all(self) -> Option>; + fn withdraw(self) -> Option>; + fn checked_despawn(self); +} + +#[rustfmt::skip] +#[allow(clippy::let_unit_value)] +impl<'a> RelationCommands<'a> for EntityMut<'a> { + fn set(self, target: Entity) -> Option { + let _ = R::ZST_OR_PANIC; + + let id = self.id(); + let world = self.into_world_mut(); + + Command::apply( + Set:: { host: id, target, _phantom: PhantomData }, + world, + ); + + world.get_entity_mut(id) + } + + fn unset(self, target: Entity) -> Option { + let _ = R::ZST_OR_PANIC; + + let id = self.id(); + let world = self.into_world_mut(); + + Command::apply( + Unset:: { host: id, target, _phantom: PhantomData }, + world, + ); + + world.get_entity_mut(id) + } + + fn unset_all(self) -> Option { + let _ = R::ZST_OR_PANIC; + + let id = self.id(); + let world = self.into_world_mut(); + + Command::apply( + UnsetAll:: { entity: id, _phantom: PhantomData }, + world, + ); + + world.get_entity_mut(id) + } + + fn withdraw(self) -> Option { + let _ = R::ZST_OR_PANIC; + + let id = self.id(); + let world = self.into_world_mut(); + + Command::apply( + Withdraw:: { entity: id, _phantom: PhantomData }, + world, + ); + + world.get_entity_mut(id) + } + + fn checked_despawn(self) { + let id = self.id(); + let world = self.into_world_mut(); + Command::apply(CheckedDespawn { entity: id }, world); + } +} + +impl<'a> RelationCommands<'a> for Option> { + fn set(self, target: Entity) -> Self { + match self { + Some(entity_mut) => entity_mut.set::(target), + None => { + warn!("Tried to set relation on an entity that doesn't exist. Ignoring.",); + None + } + } + } + + // Unsetting an entity that doesn't exist is not considered an error + fn unset(self, target: Entity) -> Self { + self.and_then(|entity_mut| entity_mut.unset::(target)) + } + + // Unsetting an entity that doesn't exist is not considered an error + fn unset_all(self) -> Self { + self.and_then(|entity_mut| entity_mut.unset_all::()) + } + + // Unsetting an entity that doesn't exist is not considered an error + fn withdraw(self) -> Self { + self.and_then(|entity_mut| entity_mut.withdraw::()) + } + + fn checked_despawn(self) { + if let Some(entity_mut) = self { + entity_mut.checked_despawn() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{self as aery, prelude::*}; + use std::array::from_fn; + + fn has_edges(world: &World, entity: Entity) -> bool { + world.get::(entity).is_some() + } + + fn is_root(world: &World, entity: Entity) -> bool { + world.get::>(entity).is_some() + } + + fn is_participant(world: &World, entity: Entity) -> bool { + world.get::>(entity).is_some() + } + + fn targeting(world: &World, host: Entity, target: Entity) -> bool { + let host_is_targeting = world + .get::(host) + .map(|edges| &edges.targets[R::CLEANUP_POLICY as usize]) + .and_then(|bucket| bucket.get(&RelationId::of::())) + .map_or(false, |set| set.contains(&target)); + + let target_is_hosted = world + .get::(target) + .map(|edges| &edges.hosts[R::CLEANUP_POLICY as usize]) + .and_then(|bucket| bucket.get(&RelationId::of::())) + .map_or(false, |set| set.contains(&host)); + + if host_is_targeting != target_is_hosted { + panic!("Asymmetric edge info"); + } + + host_is_targeting + } + + #[test] + fn set_unset() { + #[derive(Relation)] + struct R; + + let mut world = World::new(); + world.init_resource::(); + let [host, target] = from_fn(|_| world.spawn_empty().id()); + + world.entity_mut(host).set::(target); + assert!(targeting::(&world, host, target)); + assert!(is_participant::(&world, host)); + assert!(is_root::(&world, target)); + + world.entity_mut(host).unset::(target); + assert!(!has_edges(&world, target)); + assert!(!has_edges(&world, host)); + assert!(!is_participant::(&world, host)); + assert!(!is_root::(&world, target)); + } + + #[test] + fn exclusive() { + #[derive(Relation)] + struct R; + + let mut world = World::new(); + world.init_resource::(); + let [host, t0, t1] = from_fn(|_| world.spawn_empty().id()); + + // Before overwrite + world.entity_mut(host).set::(t0); + + assert!(targeting::(&world, host, t0)); + assert!(is_participant::(&world, host)); + assert!(is_root::(&world, t0)); + + // After overwrite + world.entity_mut(host).set::(t1); + + assert!(targeting::(&world, host, t1)); + assert!(is_participant::(&world, host)); + assert!(is_root::(&world, t1)); + + assert!(!has_edges(&world, t0)); + assert!(!is_root::(&world, t0)); + } + + #[derive(Relation)] + #[cleanup(policy = "Orphan")] + struct Orphan; + + #[derive(Relation)] + #[cleanup(policy = "Counted")] + struct Counted; + + #[derive(Relation)] + #[cleanup(policy = "Recursive")] + struct Recursive; + + #[derive(Relation)] + #[cleanup(policy = "Total")] + struct Total; + + #[derive(Debug)] + struct TestEdges { + orphan: Entity, + counted: Entity, + recursive: Entity, + total: Entity, + } + + #[derive(Debug)] + struct Test { + center: Entity, + targets: TestEdges, + hosts: TestEdges, + } + + impl Test { + fn new(world: &mut World) -> Self { + let test = Self { + center: world.spawn_empty().id(), + targets: TestEdges { + orphan: world.spawn_empty().id(), + counted: world.spawn_empty().id(), + recursive: world.spawn_empty().id(), + total: world.spawn_empty().id(), + }, + hosts: TestEdges { + orphan: world.spawn_empty().id(), + counted: world.spawn_empty().id(), + recursive: world.spawn_empty().id(), + total: world.spawn_empty().id(), + }, + }; + + world + .entity_mut(test.hosts.orphan) + .set::(test.center); + world + .entity_mut(test.center) + .set::(test.targets.orphan); + + world + .entity_mut(test.hosts.counted) + .set::(test.center); + world + .entity_mut(test.center) + .set::(test.targets.counted); + + world + .entity_mut(test.hosts.recursive) + .set::(test.center); + world + .entity_mut(test.center) + .set::(test.targets.recursive); + + world.entity_mut(test.hosts.total).set::(test.center); + world + .entity_mut(test.center) + .set::(test.targets.total); + + test + } + + fn assert_unchanged(&self, world: &World) { + assert!(targeting::(world, self.hosts.orphan, self.center)); + assert!(targeting::(world, self.center, self.targets.orphan)); + assert!(is_participant::(world, self.hosts.orphan,)); + assert!(is_root::(world, self.targets.orphan)); + + assert!(targeting::(world, self.hosts.counted, self.center)); + assert!(targeting::( + world, + self.center, + self.targets.counted + )); + assert!(is_participant::(world, self.hosts.counted,)); + assert!(is_root::(world, self.targets.counted)); + + assert!(targeting::( + world, + self.hosts.recursive, + self.center + )); + assert!(targeting::( + world, + self.center, + self.targets.recursive + )); + assert!(is_participant::(world, self.hosts.recursive,)); + assert!(is_root::(world, self.targets.recursive)); + + assert!(targeting::(world, self.hosts.total, self.center)); + assert!(targeting::(world, self.center, self.targets.total)); + assert!(is_participant::(world, self.hosts.total)); + assert!(is_root::(world, self.targets.total)); + + assert!(is_participant::(world, self.center)); + assert!(is_participant::(world, self.center)); + assert!(is_participant::(world, self.center)); + assert!(is_participant::(world, self.center)); + } + + fn assert_cleaned(&self, world: &World) { + assert!(world.get_entity(self.center).is_none()); + + assert!(!has_edges(world, self.hosts.orphan)); + assert!(!has_edges(world, self.targets.orphan)); + assert!(!is_participant::(world, self.hosts.orphan)); + assert!(!is_root::(world, self.targets.orphan)); + + assert!(world.get_entity(self.targets.counted).is_none()); + assert!(!has_edges(world, self.hosts.counted)); + assert!(!is_participant::(world, self.hosts.counted,)); + + assert!(world.get_entity(self.hosts.recursive).is_none()); + assert!(!has_edges(world, self.targets.recursive)); + assert!(!is_root::(world, self.targets.recursive)); + + assert!(world.get_entity(self.hosts.total).is_none()); + assert!(world.get_entity(self.targets.total).is_none()); + } + } + + #[test] + fn orphan_in_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Orphan")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .checked_despawn(); + + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + fn orphan_out_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Orphan")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + world.entity_mut(test.center).set::(e); + + world.entity_mut(e).checked_despawn(); + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + fn counted_in_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Counted")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .checked_despawn(); + + test.assert_cleaned(&world); + } + + #[test] + fn counted_out_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Counted")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + world.entity_mut(test.center).set::(e); + + world.entity_mut(e).checked_despawn(); + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + fn recursive_in_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Recursive")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .checked_despawn(); + + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + fn recursive_out_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Recursive")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + world.entity_mut(test.center).set::(e); + + world.entity_mut(e).checked_despawn(); + test.assert_cleaned(&world); + } + + #[test] + fn total_in_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Total")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .checked_despawn(); + + test.assert_cleaned(&world); + } + + #[test] + fn total_out_despawned() { + #[derive(Relation)] + #[cleanup(policy = "Total")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + world.entity_mut(test.center).set::(e); + + world.entity_mut(e).checked_despawn(); + test.assert_cleaned(&world); + } + + #[test] + fn orphan_in_unset() { + #[derive(Relation)] + #[cleanup(policy = "Orphan")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .unset::(test.center); + + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + fn orphan_out_unset() { + #[derive(Relation)] + #[cleanup(policy = "Orphan")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + + world + .entity_mut(test.center) + .set::(e) + .unwrap() + .unset::(e); + + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + fn counted_in_unset() { + #[derive(Relation)] + #[cleanup(policy = "Counted")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .unset::(test.center); + + test.assert_cleaned(&world); + } + + #[test] + fn counted_out_unset() { + #[derive(Relation)] + #[cleanup(policy = "Counted")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + + world + .entity_mut(test.center) + .set::(e) + .unwrap() + .unset::(e); + + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + fn recursive_in_unset() { + #[derive(Relation)] + #[cleanup(policy = "Recursive")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .unset::(test.center); + + test.assert_unchanged(&world); + assert!(!is_participant::(&world, test.center)); + } + + #[test] + #[should_panic] + fn recursive_out_unset() { + #[derive(Relation)] + #[cleanup(policy = "Recursive")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + + world + .entity_mut(test.center) + .set::(e) + .unwrap() + .unset::(e); + + test.assert_cleaned(&world); + } + + #[test] + fn total_in_unset() { + #[derive(Relation)] + #[cleanup(policy = "Total")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + world + .spawn_empty() + .set::(test.center) + .unwrap() + .unset::(test.center); + + test.assert_cleaned(&world); + } + + #[test] + #[should_panic] + fn total_out_unset() { + #[derive(Relation)] + #[cleanup(policy = "Total")] + struct R; + + let mut world = World::new(); + world.init_resource::(); + + let test = Test::new(&mut world); + + let e = world.spawn_empty().id(); + + world + .entity_mut(test.center) + .set::(e) + .unwrap() + .unset::(e); + + test.assert_cleaned(&world); + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..6a42866 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,107 @@ +use crate::{relation::RelationId, Var}; + +use bevy::ecs::{entity::Entity, event::Event}; +use std::cmp::PartialEq; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TargetOp { + Set, + Unset, +} + +impl From for Var { + fn from(op: TargetOp) -> Self { + Self::Val(op) + } +} + +/// An event to notify when a target has changed for a relation. +#[derive(Event, Clone, Copy, Debug)] +pub struct TargetEvent { + pub host: Entity, + pub target: Entity, + pub target_op: TargetOp, + pub relation_id: RelationId, +} + +impl TargetEvent { + /// Function to check if an event meets a critera. + /// ``` + /// use bevy::prelude::*; + /// use aery::prelude::*; + /// + /// #[derive(Component)] + /// struct A; + /// + /// #[derive(Component)] + /// struct B; + /// + /// #[derive(Relation)] + /// struct R; + /// + /// fn sys( + /// mut events: EventReader, + /// a: Query<(Entity, &A)>, + /// b: Query<(Entity, &B)>, + /// ) { + /// let (foo, _) = a.single(); + /// let (bar, _) = b.single(); + /// + /// for event in events.iter() { + /// // Anything Set anything to anything else + /// if event.matches(Wc, TargetOp::Set, Wc, Wc) { + /// if let Ok(a) = a.get(event.host) { + /// // Do something if it was a host with an `A` component + /// } + /// + /// if let Ok(a) = a.get(event.target) { + /// // Do something if it was a target with an `A` component + /// } + /// } + /// + /// // foo Set an `R` to bar + /// if event.matches(foo, TargetOp::Set, R, bar) { + /// // .. + /// } + /// + /// // foo Set an `R` to something + /// if event.matches(foo, TargetOp::Set, R, Wc) { + /// // .. + /// } + /// + /// // foo Set something to something + /// if event.matches(foo, TargetOp::Set, Wc, Wc) { + /// // .. + /// } + /// + /// // foo did anything + /// if event.matches(foo, Wc, Wc, Wc) { + /// // .. + /// } + /// + /// // this is useless lol + /// if event.matches(Wc, Wc, Wc, Wc) { + /// // .. + /// } + /// } + /// } + /// ``` + pub fn matches( + self, + host: impl Into>, + target_op: impl Into>, + rel_var: impl Into>, + target: impl Into>, + ) -> bool { + host.into().eq(&self.host) + && target_op.into().eq(&self.target_op) + && rel_var.into().eq(&self.relation_id) + && target.into().eq(&self.target) + } +} + +/// An event to notify when an entity was despawned as the resultof a cleanup policy. +#[derive(Event)] +pub struct CleanupEvent { + pub entity: Entity, +} diff --git a/src/lib.rs b/src/lib.rs index 6b2048a..ff8e8fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,31 @@ #![allow(clippy::type_complexity)] #![allow(clippy::too_many_arguments)] +#![allow(clippy::let_unit_value)] //! # Aery -//! Non-fragmenting ZST relations for Bevy featuring ergonomic query operations and cleanup -//! policies. -//! # Examples +//! A plugin that adds a subset of Entity Relationship features to Bevy using Non-fragmenting +//! ZST relations. +//! +//! ### Currently supported: +//! - ZST relations +//! - Fragmenting on (relation) type +//! - Cleanup policies +//! - Declarative APIs for: +//! - Joining +//! - Traversing +//! - Spawning +//! +//! # API tour: +//! Non exhaustive. Covers most common parts. //! ``` //! use bevy::prelude::*; //! use aery::prelude::*; //! //! fn main() { //! App::new() -//! .add_plugin(Aery) -//! .add_startup_system(setup) -//! .add_system(sys) +//! .add_plugins(Aery) +//! .add_systems(Startup, setup) +//! .add_systems(Update, sys) //! .run(); //! } //! @@ -24,65 +36,124 @@ //! struct Bar; //! //! #[derive(Relation)] -//! struct R0; +//! #[cleanup(policy = "Recursive")] +//! struct ChildOf; //! //! #[derive(Relation)] -//! #[cleanup(policy = "Recursive")] -//! struct R1; +//! #[multi] +//! struct Bag; //! +//! // Spawning entities with relations //! fn setup(mut commands: Commands) { -//! let (root, foo0, foo1, bar0, bar1) = ( -//! commands.spawn(Foo).id(), -//! commands.spawn(Foo).id(), -//! commands.spawn(Foo).id(), -//! commands.spawn(Bar).id(), -//! commands.spawn(Bar).id(), -//! ); +//! // A hierarchy of Foos with (chocolate? OwO) Bars in their Bags +//! commands.add(|wrld: &mut World| { +//! wrld.spawn(Foo) +//! .scope::(|_, mut child| { +//! child.insert(Foo); +//! child.scope_target::(|_, mut bag| { bag.insert(Bar); }); +//! }) +//! .scope::(|_, mut child| { +//! child.insert(Foo); +//! child.scope_target::(|_, mut bag| { bag.insert(Bar); }); +//! }); +//! }) +//! } //! -//! commands.set::(foo0, bar0); -//! commands.set::(foo1, bar1); -//! commands.set::(foo0, root); -//! commands.set::(foo1, root); +//! // Listening for relation events +//! fn alert(mut events: EventReader) { +//! for event in events.iter() { +//! if event.matches(Wc, TargetOp::Set, ChildOf, Wc) { +//! println!("{:?} was added as a child of {:?}", event.host, event.target); +//! } +//! } //! } //! +//! // Relation Queries //! fn sys( -//! foos: Query<(&Foo, Relations<(R0, R1)>)>, +//! foos: Query<(&Foo, Relations<(Bag, ChildOf)>)>, +//! roots: Query>, //! bars: Query<&Bar>, -//! r1_roots: Query> //! ) { //! foos.ops() -//! .join::(&bars) -//! .breadth_first::(r1_roots.iter()) -//! .for_each(|foo_ancestor, foo, bar| { +//! .join::(&bars) +//! .traverse::(roots.iter()) +//! .for_each(|foo_parent, foo, bar| { //! // .. //! }) //! } //! ``` +pub mod commands; +pub mod events; pub mod operations; pub mod relation; +pub mod scope; pub mod tuple_traits; -pub mod prelude { - pub use crate::{ - operations::{ - AeryQueryExt, BreadthFirst, ControlFlow, ForEachPermutations, - ForEachPermutations3Arity, Join, Relations, - }, - relation::{CheckedDespawn, CleanupPolicy, Participates, Relation, RelationCommands, Root}, - tuple_traits::{Joinable, RelationSet}, - Aery, - }; - pub use aery_macros::*; +use commands::RefragmentHooks; +use events::{CleanupEvent, TargetEvent}; + +use bevy::{ + app::{App, Plugin}, + ecs::entity::Entity, +}; + +/// A type to enable wildcard APIs +pub enum Var { + /// Sepcific value. + Val(T), + /// Wildcard. Will match anything. + Wc, } -use bevy::app::{App, Plugin}; -use relation::RefragmentHooks; +impl PartialEq for Var { + fn eq(&self, other: &T) -> bool { + match self { + Self::Val(v) if v == other => true, + Self::Wc => true, + _ => false, + } + } +} + +impl From> for Var { + fn from(value: Option) -> Self { + match value { + Some(value) => Self::Val(value), + None => Self::Wc, + } + } +} + +impl From for Var { + fn from(value: Entity) -> Self { + Self::Val(value) + } +} pub struct Aery; impl Plugin for Aery { fn build(&self, app: &mut App) { - app.init_resource::(); + app.init_resource::() + .add_event::() + .add_event::(); } } + +pub mod prelude { + pub use super::Var::{self, Wc}; + pub use crate::{ + commands::{RelationCommands, Set, Unset}, + events::{TargetEvent, TargetOp}, + operations::{ + AeryQueryExt, ControlFlow, ForEachPermutations, ForEachPermutations3Arity, Join, + Relations, Traverse, + }, + relation::{CheckRelations, CleanupPolicy, Participates, Relation, Root, ZstOrPanic}, + scope::Scope, + tuple_traits::{Joinable, RelationSet}, + Aery, + }; + pub use aery_macros::*; +} diff --git a/src/operations.rs b/src/operations.rs index 98475ab..4d66cb0 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -1,5 +1,5 @@ use crate::{ - relation::{EdgeWQ, Relation}, + relation::{EdgeWQ, EdgeWQItem, Relation, ZstOrPanic}, tuple_traits::*, }; use bevy::ecs::{ @@ -66,29 +66,19 @@ pub struct Relations { } /// Struct that is used to track metadata for relation operations. -pub struct Operations { +pub struct Operations { control: Control, joined_types: PhantomData, joined_queries: JoinedQueries, traversal: Traversal, -} - -/// Struct that is used to track metadata for breadth first traversals. -pub struct BreadthFirstTraversal -where - T: Relation, - E: Borrow, - I: IntoIterator, -{ - roots: I, - _phantom: PhantomData<(T, E)>, + starts: Starts, } /// An extension trait to turn `Query<(X, Relations)>`s into [`Operations`]s which have the /// trait implementations to build relation operations. This query is called the "control query". /// The [`RelationSet`] `R` from this query is what is used for joins and traversals any `T` in a -/// subsequent `.join::(_)` or `.beadth_first::(_)` call must be present in `R`. -/// Also see [`Join`] and [`BreadthFirst`]. +/// subsequent `.join::(_)` or `.traverse::(_)` call must be present in `R`. +/// Also see [`Join`] and [`Traverse`]. pub trait AeryQueryExt { /// Provides read only access to the left portion of the [`Query`] tuple. fn ops(&self) -> Operations<&Self>; @@ -102,35 +92,68 @@ where F: ReadOnlyWorldQuery, R: RelationSet + Send + Sync, { + #[allow(clippy::let_unit_value)] fn ops(&self) -> Operations<&Self> { + let _ = R::ZST_OR_PANIC; + Operations { control: self, joined_types: PhantomData, joined_queries: (), traversal: (), + starts: (), } } + #[allow(clippy::let_unit_value)] fn ops_mut(&mut self) -> Operations<&mut Self> { + let _ = R::ZST_OR_PANIC; + Operations { control: self, joined_types: PhantomData, joined_queries: (), traversal: (), + starts: (), } } } -/// A trait to implement the `breadth_first` functionality of the operations API. Any `T` in -/// `breadth_first::(roots)` must be present in the [`RelationSet`] of the control query. -/// Diamonds are impossible with `Exclusive` relations where the edges face bottom up instead of -/// top down. For this reason bottom up graphs are the only pattern that is recognized for -/// traversal. -/// Uses: +pub trait Traversal { + fn iter<'a>(edges: &EdgeWQItem<'a>) -> EdgeIter<'a>; +} + +pub struct BreadthFirstAscent(PhantomData); +pub struct BreadthFirstDescent(PhantomData); + +impl Traversal for BreadthFirstAscent { + fn iter<'a>(edges: &EdgeWQItem<'a>) -> EdgeIter<'a> { + edges.edges.iter_targets::() + } +} + +impl Traversal for BreadthFirstDescent { + fn iter<'a>(edges: &EdgeWQItem<'a>) -> EdgeIter<'a> { + edges.edges.iter_hosts::() + } +} + +/// The traversal functionality of the operations API. Any `T` in `traverse::(roots)` must +/// be present in the [`RelationSet`] of the control query. Diamonds are impossible with `Exclusive` +/// relations where the edges face bottom up instead of top down. For this reason all of Aery's +/// APIs are opinionated with implicit defaults to prefer bottom up edges. +/// +/// To descend is to traverse hosts and to ascend is to traverse targets. Descent is breadth first +/// and since relations support multi arity ascent is also breadth first. Ascending exclusive +/// relations is to ascend parents as the "breadth" is always `1`. +/// +/// Traversals will not check for cycles or diamonds (possible with multi relations). Cycles will +/// infinite loop and entities may be traversed multiple times for diamonds. +/// +/// See [`Join`] for joining queries and: /// - [`ForEachPermutations`] for operations with just traversals. /// - [`ForEachPermutations3Arity`] for operations with traversals and joins. /// -/// See [`Join`] for joining queries. /// # Illustration: /// ``` /// use bevy::prelude::*; @@ -140,21 +163,26 @@ where /// struct A; /// /// #[derive(Relation)] +/// #[cleanup(policy = "Recursive")] /// struct R; /// +/// #[derive(Relation)] +/// #[cleanup(policy = "Orphan")] +/// struct O; +/// /// fn setup(mut commands: Commands) { -/// let [e0, e1, e2, e3, e4, e5, e6] = std::array::from_fn(|_| commands.spawn_empty().id()); -/// -/// for (from, to) in [ -/// (e1, e0), -/// (e2, e0), -/// (e3, e1), -/// (e4, e1), -/// (e5, e2), -/// (e6, e2) -/// ] { -/// commands.set::(from, to); -/// } +/// commands.add(|wrld: &mut World| { +/// wrld.spawn_empty() +/// .scope::(|parent, ent1| { +/// ent1.set::(parent) +/// .scope::(|parent, ent3| {}) +/// .scope::(|parent, ent4| { ent4.set::(parent); }); +/// }) +/// .scope::(|_, ent2| { +/// ent2.scope::(|_, ent5| {}) +/// .scope::(|_, ent6| {}); +/// }); +/// }); /// /// // Will construct the following graph: /// // @@ -168,73 +196,70 @@ where /// } /// /// fn sys(a: Query<(&A, Relations)>, roots: Query>) { -/// a.ops().breadth_first::(roots.iter()).for_each(|a_ancestor, a| { +/// a.ops().traverse::(roots.iter()).for_each(|a_ancestor, a| { /// // Will traverse in the following order: -/// // (a_ancestor, a): (0, 1) -/// // (a_ancestor, a): (0, 2) -/// // (a_ancestor, a): (1, 3) -/// // (a_ancestor, a): (1, 4) -/// // (a_ancestor, a): (2, 5) -/// // (a_ancestor, a): (2, 6) +/// // (a_ancestor, a) == (0, 1) +/// // (a_ancestor, a) == (0, 2) +/// // (a_ancestor, a) == (1, 3) +/// // (a_ancestor, a) == (1, 4) +/// // (a_ancestor, a) == (2, 5) +/// // (a_ancestor, a) == (2, 6) /// }) /// } /// ``` -/// What the Archetypes/Tables should look like: -/// -/// | entityid | A | `Root` | -/// |-----------|---|-----------| -/// | 0 | _ | _ | -/// -/// *Note:* `Root<_>` markers are automatically added and removed by the provided commands for -/// convenient traversing. -/// -/// | entityid | A | R | -/// |-----------|---|---| -/// | 1 | _ | 0 | -/// | 2 | _ | 0 | -/// | 3 | _ | 1 | -/// | 4 | _ | 1 | -/// | 5 | _ | 2 | -/// | 6 | _ | 2 | -pub trait BreadthFirst +pub trait Traverse where E: Borrow, I: IntoIterator, { type Out; - fn breadth_first(self, roots: I) -> Self::Out; + type OutDown; + + fn traverse(self, starts: I) -> Self::OutDown; + fn traverse_targets(self, starts: I) -> Self::Out; } -impl BreadthFirst +impl Traverse for Operations where E: Borrow, I: IntoIterator, { type Out = - Operations>; + Operations, I>; - fn breadth_first(self, roots: I) -> Self::Out { + type OutDown = + Operations, I>; + + fn traverse(self, starts: I) -> Self::OutDown { Operations { control: self.control, joined_types: self.joined_types, joined_queries: self.joined_queries, - traversal: BreadthFirstTraversal { - roots, - _phantom: PhantomData, - }, + traversal: BreadthFirstDescent::(PhantomData), + starts, + } + } + + fn traverse_targets(self, starts: I) -> Self::Out { + Operations { + control: self.control, + joined_types: self.joined_types, + joined_queries: self.joined_queries, + traversal: BreadthFirstAscent::(PhantomData), + starts, } } } -/// A trait to implement the `join` functionality of the operations API. Any `T` in -/// `join::(query)` must be present in the [`RelationSet`] of the control query. The type of -/// join performed is what's known as an "inner join" which produces permutations of all matched -/// entiteis. +/// The `join` functionality of the operations API. Any `T` in `join::(query)` must be present +/// in the [`RelationSet`] of the control query. The type of join performed is what's known as an +/// "inner join" which produces permutations of all matched entiteis. +/// +/// See [`Traverse`] for traversing hierarchies and: /// - [`ForEachPermutations`] for operations with just joins. /// - [`ForEachPermutations3Arity`] for operations with joins and traversals. /// -/// See [`BreadthFirst`] for traversing hierarchies. /// # Illustration: /// ``` /// use bevy::prelude::*; @@ -308,7 +333,7 @@ where /// | 8 | `"corge"` | /// | 9 | `"grault"` | /// -/// **then `a.ops().join::(&b).join::(&c).for_each(|a, (b, c)| {})`:** +/// **then `a.ops().join::(&b).join::(&c)`:** /// /// | a | b | c | /// |-------|-----------|-----------| @@ -323,8 +348,8 @@ where fn join(self, item: Item) -> Self::Out; } -impl Join - for Operations +impl Join + for Operations where Item: for<'a> Joinable<'a, 1>, JoinedTypes: Append, @@ -335,6 +360,7 @@ where ::Out, ::Out, Traversal, + Roots, >; fn join(self, item: Item) -> Self::Out { @@ -343,56 +369,325 @@ where joined_types: PhantomData, joined_queries: Append::append(self.joined_queries, item), traversal: self.traversal, + starts: self.starts, } } } -/// Control flow enum for [`ForEachPermutations`] and [`ForEachPermutations3Arity`]. +#[cfg_attr(doc, aquamarine::aquamarine)] +/// Control flow enum for [`ForEachPermutations`] and [`ForEachPermutations3Arity`]. The closures +/// accepted by both return `impl Into` with `()` being turned into +/// `ControlFlow::Continue` to save you from typing `return ControlFlow::Continue` in all your +/// functions. +/// +/// ``` +///# use bevy::prelude::*; +///# use aery::prelude::*; +///# +///# #[derive(Component)] +///# struct A { +///# // .. +///# } +///# +///# #[derive(Component)] +///# struct B { +///# // .. +///# } +///# +///# #[derive(Relation)] +///# struct R; +///# +///# fn predicate(a: &A, b: &B) -> bool { +///# true +///# } +///# +/// fn sys(a: Query<(&A, Relations)>, b: Query<&B>) { +/// a.ops().join::(&b).for_each(|a, b| { +/// if predicate(a, b) { +/// return ControlFlow::Exit; +/// } +/// +/// // Return types still need to be the same so explicitly providing this is nessecary +/// // when doing any controlflow manipulation. +/// ControlFlow::Continue +/// }) +/// } +/// ``` +/// +/// ## FastForward Illustration: /// ``` /// use bevy::prelude::*; /// use aery::prelude::*; +/// +/// #[derive(Component)] +/// struct A; +/// /// #[derive(Component)] -/// struct A { -/// // .. +/// struct B(usize); +/// +/// #[derive(Component)] +/// struct C(usize); +/// +/// #[derive(Component)] +/// struct D(usize); +/// +/// #[derive(Relation)] +/// struct R0; +/// +/// #[derive(Relation)] +/// struct R1; +/// +/// #[derive(Relation)] +/// struct R2; +/// +/// fn setup(mut commands: Commands) { +/// let bs = std::array::from_fn::<_, 3, _>(|n| commands.spawn(B(n)).id()); +/// let cs = std::array::from_fn::<_, 3, _>(|n| commands.spawn(C(n)).id()); +/// let ds = std::array::from_fn::<_, 3, _>(|n| commands.spawn(D(n)).id()); +/// +/// let a = commands.spawn(A).id(); +/// +/// for id in bs { +/// commands.add(Set::::new(a, id)); +/// } +/// +/// for id in cs { +/// commands.add(Set::::new(a, id)); +/// } +/// +/// for id in ds { +/// commands.add(Set::::new(a, id)); +/// } /// } /// +/// fn ff_sys( +/// a: Query<(&A, Relations<(R0, R1, R2)>)>, +/// b: Query<&B>, +/// c: Query<&C>, +/// d: Query<&D> +/// ) { +/// a.ops() +/// .join::(&b) +/// .join::(&c) +/// .join::(&d) +/// .for_each(|a, (b, c, d)| { +/// if c.0 == 1 { +/// ControlFlow::FastForward(1) +/// } +/// else { +/// println!("({}, {}, {})", b.0, c.0, d.0); +/// ControlFlow::Continue +/// } +/// }); +/// } +/// ``` +/// ### Output of ff_sys: +/// ```ignore +/// (0, 0, 0) +/// (0, 0, 1) +/// (0, 0, 2) +/// // Skipped: +/// // (0, 1, 0) +/// // (0, 1, 1) +/// // (0, 1, 2) +/// (0, 2, 0) +/// (0, 2, 1) +/// (0, 2, 2) +/// (1, 0, 0) +/// (1, 0, 1) +/// (1, 0, 2) +/// // Skipped: +/// // (1, 1, 0) +/// // (1, 1, 1) +/// // (1, 1, 2) +/// (1, 2, 0) +/// (1, 2, 1) +/// (1, 2, 2) +/// (2, 0, 0) +/// (2, 0, 1) +/// (2, 0, 2) +/// // Skipped: +/// // (2, 1, 0) +/// // (2, 1, 1) +/// // (2, 1, 2) +/// (2, 2, 0) +/// (2, 2, 1) +/// (2, 2, 2) +/// ``` +/// ## Walk Illustration: +/// ``` +/// use bevy::prelude::*; +/// use aery::prelude::*; +/// +/// #[derive(Component)] +/// struct A(usize); +/// /// #[derive(Component)] -/// struct B { -/// // .. +/// struct B(usize); +/// +/// #[derive(Relation)] +/// struct R0; +/// +/// #[derive(Relation)] +/// struct R1; +/// +/// fn setup(mut commands: Commands) { +/// commands.add(|wrld: &mut World| { +/// wrld.spawn(A(0)) +/// .scope::(|_, mut ent1| { +/// ent1.insert(A(1)); +/// ent1.scope_target::(|_, mut ent| { ent.insert(B(0)); }) +/// .scope_target::(|_, mut ent| { ent.insert(B(1)); }); +/// }) +/// .scope::(|_, mut ent2| { +/// ent2.insert(A(2)); +/// ent2.scope_target::(|_, mut ent| { ent.insert(B(3)); }) +/// .scope_target::(|_, mut ent| { ent.insert(B(4)); }); +/// }) +/// .scope::(|_, mut ent3| { +/// ent3.insert(A(3)); +/// ent3.scope_target::(|_, mut ent| { ent.insert(B(5)); }) +/// .scope_target::(|_, mut ent| { ent.insert(B(6)); }); +/// }); +/// }); /// } /// +/// fn walk_sys( +/// roots: Query>, +/// a: Query<(&A, Relations<(R0, R1)>)>, +/// b: Query<&B>, +/// ) { +/// a.ops() +/// .join::(&b) +/// .traverse::(roots.iter()) +/// .for_each(|a, a_child, b| { +/// if a_child.0 == 2 { +/// ControlFlow::Walk +/// } +/// else { +/// println!("({}, {}, {})", a.0, a_child.0, b.0); +/// ControlFlow::Continue +/// } +/// }); +/// } +/// ``` +/// ### Output of walk_sys: +/// ```ignore +/// (0, 1, 0) +/// (0, 1, 1) +/// // Skipped: +/// // (0, 2, 3) +/// // (0, 2, 4) +/// (0, 3, 5) +/// (0, 3, 6) +/// ``` +/// ## Probe Illustration: +/// ``` +/// use bevy::prelude::*; +/// use aery::prelude::*; +/// +/// #[derive(Component)] +/// struct A(usize); +/// /// #[derive(Relation)] /// struct R; /// -/// fn predicate(a: &A, b: &B) -> bool { -/// # true // amongus -/// // .. +/// fn setup(mut commands: Commands) { +/// commands.add(|wrld: &mut World| { +/// wrld.spawn(A(0)) +/// .scope::(|_, mut ent1| { +/// ent1.insert(A(1)); +/// ent1.scope_target::(|_, mut ent4| { ent4.insert(A(4)); }) +/// .scope_target::(|_, mut ent5| { ent5.insert(A(5)); }); +/// }) +/// .scope::(|_, mut ent2| { +/// ent2.insert(A(2)); +/// ent2.scope_target::(|_, mut ent6| { ent6.insert(A(6)); }) +/// .scope_target::(|_, mut ent7| { ent7.insert(A(7)); }); +/// }) +/// .scope::(|_, mut ent3| { +/// ent3.insert(A(3)); +/// ent3.scope_target::(|_, mut ent8| { ent8.insert(A(8)); }) +/// .scope_target::(|_, mut ent9| { ent9.insert(A(9)); }); +/// }); +/// }); /// } /// -/// fn sys(a: Query<(&A, Relations)>, b: Query<&B>) { -/// a.ops().join::(&b).for_each(|a, b| { -/// if predicate(a, b) { -/// return ControlFlow::Exit; -/// } +/// fn noprobe(query: Query<(&A, Relations)>, roots: Query>) { +/// query.ops().traverse::(roots.iter()).for_each(|a, a_child| { +/// // .. +/// }) +/// } /// -/// // `()` impls Into for convenience. Return types still need to be the same -/// // so explicitly providing this is nessecary when doing any controlflow manipulation. -/// ControlFlow::Continue +/// fn probe(query: Query<(&A, Relations)>, roots: Query>) { +/// query.ops().traverse::(roots.iter()).for_each(|a, a_child| { +/// if (a_child.0 == 2) { +/// ControlFlow::Probe +/// } +/// else { +/// ControlFlow::Continue +/// } /// }) /// } /// ``` +/// ### Traversal of noprobe: +/// Pink means traversed. +/// ```mermaid +/// flowchart BT +/// classDef pink fill:#f66 +/// +/// E1:::pink --R--> E0:::pink +/// E2:::pink --R--> E0:::pink +/// E3:::pink --R--> E0:::pink +/// +/// E4:::pink --R--> E1:::pink +/// E5:::pink --R--> E1:::pink +/// +/// E6:::pink --R--> E2:::pink +/// E7:::pink --R--> E2:::pink +/// +/// E8:::pink --R--> E3:::pink +/// E9:::pink --R--> E3:::pink +/// ``` +/// +/// ### Traversal of probe: +/// Pink means traversed. +/// ```mermaid +/// flowchart BT +/// classDef pink fill:#f66 +/// +/// E1:::pink --R--> E0:::pink +/// E2:::pink --R--> E0:::pink +/// E3 --R--> E0:::pink +/// +/// E4 --R--> E1:::pink +/// E5 --R--> E1:::pink +/// +/// E6:::pink --R--> E2:::pink +/// E7:::pink --R--> E2:::pink +/// +/// E8:::pink --R--> E3 +/// E9:::pink --R--> E3 +/// ``` pub enum ControlFlow { /// Continue to next permutation. Continue, /// Stop iterating permutatiosn and exit loop. Exit, - /// FastForward(n) will advance the nth join to the next match skipping any premutations + /// FastForward(n) will advance the nth join to the next match skipping any premutations. /// inbetween where it currently is and the next permutation where it was supposed to advance. + /// Has no effect for operations with no joins. FastForward(usize), - /// Walks to the next entity in the traversal skipping any remaining permutations to iterate. - /// - For beadth first traversals this is the next entity in the walk path. - /// - Otherwise it's a linear traversal through the query items and this is the next entity. + /// Walks to the next entity in the "traversal" skipping any remaining permutations to iterate. + /// - For operations with *traversals* this is the next entity in the traversal path. + /// - Otherwise when there are only joins it's a linear traversal through the query items + /// and this is just the next entity in the control query. Walk, + /// Skips: + /// - Any remaining join permutations. + /// - Any remaining entities on the current breadth. + /// - Entities on the breadth of the next depth that are before the current child/ancestor. + Probe, } impl From<()> for ControlFlow { @@ -401,23 +696,26 @@ impl From<()> for ControlFlow { } } -/// A for each trait to get around lending Iterators not being expressible with current GATs. +/// A trait to iterate relation queries. +/// /// When iterating: /// - Diamonds /// - Cycles /// - Joins where multiple entities have the same target /// /// References to the same entities components can be produced more than once which is why this -/// problem cannot be solved with `unsafe`. So to work around this "lifetime trapping" is used -/// instead. The closure parameters cannot escape the closure. +/// problem cannot be solved with `unsafe`. It requires lending iterators which are not expressable +/// with current GATs so to work around this "lifetime trapping" is used instead. +/// The closure parameters cannot escape the closure. /// /// For any control query `Query<(X, Relations<..>)>`: /// - If there is only joins: Permutations of valid entities from the joined queries will be looped /// through. The left parameter will be the `X` item and the right parameter will be a tuple of all /// the query fetch parameters from the joined queries. /// - If there is only hierarchy traversals: Traversable ancestor-descendant permutations that -/// belong to the control query will be looped through. The left parameter will be the `X` item of -/// an ancestor and the right parameter will be the `X` item of an immediate descendant. +/// belong to the control query will be looped through. For descents the left parameter will be the +/// `X` item of an ancestor and the right parameter will be the `X` item of an immediate descendant. +/// For ascents this is the other way around. /// /// See [`ControlFlow`] for control flow options and [`ForEachPermutations3Arity`] for the loop /// behavior of operations with hierarchy traversals and 1 or more join. @@ -467,7 +765,7 @@ where { ControlFlow::Continue => {} ControlFlow::Exit => return, - ControlFlow::Walk => break, + ControlFlow::Walk | ControlFlow::Probe => break, ControlFlow::FastForward(n) if n < N => { matches[n] = false; } @@ -514,7 +812,7 @@ where { ControlFlow::Continue => {} ControlFlow::Exit => return, - ControlFlow::Walk => break, + ControlFlow::Walk | ControlFlow::Probe => break, ControlFlow::FastForward(n) if n < N => { matches[n] = false; } @@ -526,12 +824,12 @@ where } impl ForEachPermutations<0> - for Operations<&'_ Query<'_, '_, (Q, Relations), F>, (), (), BreadthFirstTraversal> + for Operations<&'_ Query<'_, '_, (Q, Relations), F>, (), (), T, I> where Q: WorldQuery, R: RelationSet, F: ReadOnlyWorldQuery, - T: Relation, + T: Traversal, E: Borrow, I: IntoIterator, { @@ -544,28 +842,33 @@ where Func: for<'f, 'l, 'r> FnMut(&'f mut Self::Left<'l>, Self::Right<'r>) -> Ret, { let mut queue = self - .traversal - .roots + .starts .into_iter() .map(|e| *e.borrow()) .collect::>(); - while let Some(entity) = queue.pop_front() { + 'queue: while let Some(entity) = queue.pop_front() { let Ok((mut control, relations)) = self.control.get(entity) else { continue }; - for e in relations.edges.edges.iter_hosts::() { + for e in T::iter(&relations.edges) { let Ok(joined_queries) = self.control.get(e) else { continue }; - if let ControlFlow::Exit = func(&mut control, joined_queries.0).into() { - return; + match func(&mut control, joined_queries.0).into() { + ControlFlow::Exit => return, + ControlFlow::Probe => { + queue.clear(); + queue.push_back(e); + continue 'queue; + } + _ => {} } } - for e in relations.edges.edges.iter_hosts::() { + for e in T::iter(&relations.edges) { queue.push_back(e); } } @@ -573,17 +876,12 @@ where } impl ForEachPermutations<0> - for Operations< - &'_ mut Query<'_, '_, (Q, Relations), F>, - (), - (), - BreadthFirstTraversal, - > + for Operations<&'_ mut Query<'_, '_, (Q, Relations), F>, (), (), T, I> where Q: WorldQuery, R: RelationSet, F: ReadOnlyWorldQuery, - T: Relation, + T: Traversal, E: Borrow, I: IntoIterator, { @@ -596,13 +894,12 @@ where Func: for<'f, 'l, 'r> FnMut(&'f mut Self::Left<'l>, Self::Right<'r>) -> Ret, { let mut queue = self - .traversal - .roots + .starts .into_iter() .map(|e| *e.borrow()) .collect::>(); - while let Some(entity) = queue.pop_front() { + 'queue: while let Some(entity) = queue.pop_front() { // SAFETY: Self referential relations are impossible so this is always safe. let Ok((mut control, relations)) = (unsafe { self.control.get_unchecked(entity) @@ -610,18 +907,24 @@ where continue }; - for e in relations.edges.edges.iter_hosts::() { + for e in T::iter(&relations.edges) { // SAFETY: Self referential relations are impossible so this is always safe. let Ok(joined_queries) = (unsafe { self.control.get_unchecked(e) }) else { continue }; - if let ControlFlow::Exit = func(&mut control, joined_queries.0).into() { - return; + match func(&mut control, joined_queries.0).into() { + ControlFlow::Exit => return, + ControlFlow::Probe => { + queue.clear(); + queue.push_back(e); + continue 'queue; + } + _ => {} } } - for e in relations.edges.edges.iter_hosts::() { + for e in T::iter(&relations.edges) { queue.push_back(e); } } @@ -630,11 +933,10 @@ where /// A 3 arity version of [`ForEachPermutations`] for when operations feature a traversal with 1 or /// more joins. Will iterate through hierarchy permutations and join permutations together. -/// - The left paramater will be an ancestor entity. -/// - The middle parameter will be a descendant of the ancestor. -/// - The right parameter will be a tuple of all the query fetch parameters from joined queries -/// where the entity being joined on is the descendant. The traversal relation is essentially -/// treated as another join paramter where the query being joined on is the control query. +/// - The left and middle paramaters will be an ancestor/descendant pairs. +/// - The rightmost parameter will be a tuple of all the query fetch parameters from joined queries +/// where the entity being joined on is the same entity that is the middle parameter. This is the +/// ancestor or descendant depending on if the traversal is an ascent or descent. pub trait ForEachPermutations3Arity { type Left<'l>; type Middle<'m>; @@ -651,17 +953,12 @@ pub trait ForEachPermutations3Arity { } impl ForEachPermutations3Arity - for Operations< - &'_ Query<'_, '_, (Q, Relations), F>, - JoinedTypes, - JoinedQueries, - BreadthFirstTraversal, - > + for Operations<&'_ Query<'_, '_, (Q, Relations), F>, JoinedTypes, JoinedQueries, T, I> where Q: WorldQuery, R: RelationSet, F: ReadOnlyWorldQuery, - T: Relation, + T: Traversal, E: Borrow, I: IntoIterator, JoinedTypes: Product, @@ -681,23 +978,25 @@ where ) -> Ret, { let mut queue = self - .traversal - .roots + .starts .into_iter() .map(|e| *e.borrow()) .collect::>(); - while let Some(entity) = queue.pop_front() { - let Ok((mut ancestor, ancestor_edges)) = self.control.get(entity) else { + 'queue: while let Some(entity) = queue.pop_front() { + let Ok((mut left_components, left_edges)) = self.control.get(entity) else { continue }; - for descendant in ancestor_edges.edges.edges.iter_hosts::() { - let Ok((mut descendant, descendant_edges)) = self.control.get(descendant) else { + for mid in T::iter(&left_edges.edges) { + let Ok((mut mid_components, mid_edges)) = self + .control + .get(mid) + else { continue }; - let mut edge_product = JoinedTypes::product(descendant_edges.edges); + let mut edge_product = JoinedTypes::product(mid_edges.edges); let mut matches = [false; N]; while let Some(entities) = edge_product.advance(matches) { @@ -708,8 +1007,8 @@ where } match func( - &mut ancestor, - &mut descendant, + &mut left_components, + &mut mid_components, Joinable::join(&mut self.joined_queries, entities), ) .into() @@ -720,12 +1019,17 @@ where ControlFlow::FastForward(n) if n < N => { matches[n] = false; } + ControlFlow::Probe => { + queue.clear(); + queue.push_back(mid); + continue 'queue; + } _ => {} } } } - for e in ancestor_edges.edges.edges.iter_hosts::() { + for e in T::iter(&left_edges.edges) { queue.push_back(e); } } @@ -733,17 +1037,12 @@ where } impl ForEachPermutations3Arity - for Operations< - &'_ mut Query<'_, '_, (Q, Relations), F>, - JoinedTypes, - JoinedQueries, - BreadthFirstTraversal, - > + for Operations<&'_ mut Query<'_, '_, (Q, Relations), F>, JoinedTypes, JoinedQueries, T, I> where Q: WorldQuery, R: RelationSet, F: ReadOnlyWorldQuery, - T: Relation, + T: Traversal, E: Borrow, I: IntoIterator, JoinedTypes: Product, @@ -763,29 +1062,28 @@ where ) -> Ret, { let mut queue = self - .traversal - .roots + .starts .into_iter() .map(|e| *e.borrow()) .collect::>(); - while let Some(entity) = queue.pop_front() { + 'queue: while let Some(entity) = queue.pop_front() { // SAFETY: Self referential relations are impossible so this is always safe. - let Ok((mut ancestor, ancestor_edges)) = (unsafe { + let Ok((mut left_components, left_edges)) = (unsafe { self.control.get_unchecked(entity) }) else { continue }; - for descendant in ancestor_edges.edges.edges.iter_hosts::() { + for mid in T::iter(&left_edges.edges) { // SAFETY: Self referential relations are impossible so this is always safe. - let Ok((mut descendant, descendant_edges)) = (unsafe { - self.control.get_unchecked(descendant) + let Ok((mut mid_components, mid_edges)) = (unsafe { + self.control.get_unchecked(mid) }) else { continue }; - let mut edge_product = JoinedTypes::product(descendant_edges.edges); + let mut edge_product = JoinedTypes::product(mid_edges.edges); let mut matches = [false; N]; while let Some(entities) = edge_product.advance(matches) { @@ -796,8 +1094,8 @@ where } match func( - &mut ancestor, - &mut descendant, + &mut left_components, + &mut mid_components, Joinable::join(&mut self.joined_queries, entities), ) .into() @@ -808,11 +1106,17 @@ where ControlFlow::FastForward(n) if n < N => { matches[n] = false; } + ControlFlow::Probe => { + queue.clear(); + queue.push_back(mid); + continue 'queue; + } _ => {} } } } - for e in ancestor_edges.edges.edges.iter_hosts::() { + + for e in T::iter(&left_edges.edges) { queue.push_back(e); } } @@ -823,7 +1127,7 @@ where #[allow(dead_code)] #[allow(unused_variables)] mod compile_tests { - use crate::prelude::*; + use crate::{self as aery, prelude::*}; use bevy::prelude::*; #[derive(Component)] @@ -879,31 +1183,28 @@ mod compile_tests { .for_each(|a, (b, c)| {}); } - fn breadth_first_immut(left: Query<(&A, Relations<(R0, R1)>)>) { + fn traverse_immut(left: Query<(&A, Relations<(R0, R1)>)>) { left.ops() - .breadth_first::(None::) + .traverse::(None::) .for_each(|a0, a1| {}); } - fn breadth_first_immut_joined(left: Query<(&A, Relations<(R0, R1)>)>, right: Query<&B>) { + fn traverse_immut_joined(left: Query<(&A, Relations<(R0, R1)>)>, right: Query<&B>) { left.ops() - .breadth_first::(None::) + .traverse::(None::) .join::(&right) .for_each(|a0, a1, b| {}); } - fn breadth_first_mut(mut left: Query<(&mut A, Relations<(R0, R1)>)>) { + fn traverse_mut(mut left: Query<(&mut A, Relations<(R0, R1)>)>) { left.ops_mut() - .breadth_first::(None::) + .traverse::(None::) .for_each(|a0, a1| {}); } - fn breadth_first_mut_joined_mut( - left: Query<(&A, Relations<(R0, R1)>)>, - mut right: Query<&mut B>, - ) { + fn traverse_mut_joined_mut(left: Query<(&A, Relations<(R0, R1)>)>, mut right: Query<&mut B>) { left.ops() - .breadth_first::(None::) + .traverse::(None::) .join::(&mut right) .for_each(|a0, a1, b| {}); } @@ -911,7 +1212,7 @@ mod compile_tests { #[cfg(test)] mod tests { - use crate::prelude::*; + use crate::{self as aery, prelude::*}; use bevy::{app::AppExit, prelude::*}; #[derive(Component)] @@ -964,10 +1265,15 @@ mod tests { let left = world.spawn(S).id(); - world.set::(left, a1); - world.set::(left, b0); - world.set::(left, b2); - world.set::(left, c1); + world + .entity_mut(left) + .set::(a1) + .unwrap() + .set::(b0) + .unwrap() + .set::(b2) + .unwrap() + .set::(c1); world.insert_resource(EntityList { entities }); } @@ -1014,8 +1320,8 @@ mod tests { } App::new() - .add_plugin(Aery) - .add_systems((init, run, test).chain()) + .add_plugins(Aery) + .add_systems(Update, (init, run, test).chain()) .run(); } @@ -1040,11 +1346,17 @@ mod tests { let left = world.spawn(S).id(); - world.set::(left, a0); - world.set::(left, a2); - world.set::(left, b1); - world.set::(left, c0); - world.set::(left, c2); + world + .entity_mut(left) + .set::(a0) + .unwrap() + .set::(a2) + .unwrap() + .set::(b1) + .unwrap() + .set::(c0) + .unwrap() + .set::(c2); world.insert_resource(EntityList { entities }); } @@ -1091,8 +1403,8 @@ mod tests { } App::new() - .add_plugin(Aery) - .add_systems((init, run, test).chain()) + .add_plugins(Aery) + .add_systems(Update, (init, run, test).chain()) .run(); } @@ -1117,15 +1429,25 @@ mod tests { let left = world.spawn(S).id(); - world.set::(left, a0); - world.set::(left, a1); - world.set::(left, a2); - world.set::(left, b0); - world.set::(left, b1); - world.set::(left, b2); - world.set::(left, c0); - world.set::(left, c1); - world.set::(left, c2); + world + .entity_mut(left) + .set::(a0) + .unwrap() + .set::(a1) + .unwrap() + .set::(a2) + .unwrap() + .set::(b0) + .unwrap() + .set::(b1) + .unwrap() + .set::(b2) + .unwrap() + .set::(c0) + .unwrap() + .set::(c1) + .unwrap() + .set::(c2); world.insert_resource(EntityList { entities }); } @@ -1165,8 +1487,8 @@ mod tests { } App::new() - .add_plugin(Aery) - .add_systems((init, run, test).chain()) + .add_plugins(Aery) + .add_systems(Update, (init, run, test).chain()) .run(); } @@ -1191,15 +1513,25 @@ mod tests { let left = world.spawn(S).id(); - world.set::(left, a0); - world.set::(left, a1); - world.set::(left, a2); - world.set::(left, b0); - world.set::(left, b1); - world.set::(left, b2); - world.set::(left, c0); - world.set::(left, c1); - world.set::(left, c2); + world + .entity_mut(left) + .set::(a0) + .unwrap() + .set::(a1) + .unwrap() + .set::(a2) + .unwrap() + .set::(b0) + .unwrap() + .set::(b1) + .unwrap() + .set::(b2) + .unwrap() + .set::(c0) + .unwrap() + .set::(c1) + .unwrap() + .set::(c2); world.insert_resource(EntityList { entities }); } @@ -1240,8 +1572,8 @@ mod tests { } App::new() - .add_plugin(Aery) - .add_systems((init, run, test).chain()) + .add_plugins(Aery) + .add_systems(Update, (init, run, test).chain()) .run(); } } diff --git a/src/relation.rs b/src/relation.rs index c30a8b6..0f978bd 100644 --- a/src/relation.rs +++ b/src/relation.rs @@ -1,18 +1,34 @@ +use crate::Var; + use bevy::{ ecs::{ component::Component, entity::Entity, query::{Or, With, WorldQuery}, - system::{Command, Commands, Resource}, - world::World, + world::{EntityMut, EntityRef}, }, - log::warn, - utils::{HashMap, HashSet}, + utils::HashMap, }; use core::any::TypeId; use indexmap::IndexSet; -use std::{collections::VecDeque, marker::PhantomData}; +use std::marker::PhantomData; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RelationId(TypeId); + +impl RelationId { + pub fn of() -> Self { + Self(TypeId::of::()) + } +} + +impl From for Var { + fn from(_: R) -> Self { + let _ = R::ZST_OR_PANIC; + Self::Val(RelationId::of::()) + } +} #[derive(Component)] pub(crate) struct RootMarker { @@ -24,7 +40,9 @@ pub(crate) struct Participant { pub _phantom: PhantomData, } -/// Filter to find roots of a relationship graph. +/// Filter to find roots of a relationship graph for quintessential traversal. +/// A root of any `R` is an entity that is the target of atleast 1 `R` +/// but does not itself target any other entities with `R`. #[derive(WorldQuery)] pub struct Root { filter: With>, @@ -36,13 +54,11 @@ pub struct Participates { filter: Or<(With>, With>)>, } +#[cfg_attr(doc, aquamarine::aquamarine)] /// Supported cleanup patterns. When entities have multiple relations with different cleanup /// policies each relation looks at the graph as if it were the only relation that existed. /// In effect the summation of their cleanup is applied. -/// Cleanup is triggered when the entities participating in a relationship change. Ie. -/// - When an entity is despawned -/// - When the relation is removed -/// # Example +/// ## Illustration /// ``` /// use bevy::prelude::*; /// use aery::prelude::*; @@ -55,71 +71,118 @@ pub struct Participates { /// #[cleanup(policy = "Recursive")] /// struct R; /// -/// fn sys(world: &mut World) { -/// let [e0, e1, e2, e3, e4, e5, e6] = std::array::from_fn(|_| world.spawn_empty().id()); +/// fn sys(wrld: &mut World) { +/// // Creation +/// let root = wrld +/// .spawn_empty() +/// .scope::(|parent, ent1| { +/// ent1.set::(parent) +/// .scope::(|parent, ent3| {}) +/// .scope::(|parent, ent4| { ent4.set::(parent); }); +/// }) +/// .scope::(|_, ent2| { +/// ent2.scope::(|_, ent5| {}) +/// .scope::(|_, ent6| {}); +/// }); +/// +/// // Trigger cleanup +/// root.checked_despawn(); +/// } +/// ``` +/// ## After creation before cleanup: +/// ```mermaid +/// flowchart BT +/// E1 --R--> E0 +/// E1 --O--> E0 +/// +/// E2 --O--> E0 /// -/// world.set::(e1, e0); -/// world.set::(e2, e0); -/// world.set::(e3, e1); -/// world.set::(e4, e1); +/// E3 --O--> E1 /// -/// world.set::(e1, e0); -/// world.set::(e4, e1); -/// world.set::(e5, e2); -/// world.set::(e6, e2); +/// E4 --R--> E1 +/// E4 --O--> E1 /// -/// // Results in: -/// // 0 -/// // // \ -/// // // \ -/// // RO O -/// // // \ -/// // // \ -/// // 1 2 -/// // / \\ / \ -/// // O RO R R -/// // / \\ / \ -/// // 3 4 5 6 +/// E5 --R--> E2 /// -/// world.checked_despawn(e0); +/// E6 --R--> E2 +/// ``` /// -/// // After cleanup: -/// // 2 -/// // / \ -/// // 3 R R -/// // / \ -/// // 5 6 -/// } +/// ## After cleanup: +/// ```mermaid +/// flowchart BT +/// E3 +/// +/// E5 --R--> E2 +/// +/// E6 --R--> E2 /// ``` #[derive(Clone, Copy)] pub enum CleanupPolicy { /// Will do no further cleanup. Orphan, - /// Counted relationships "count" the number of hosts they have. If it ever reaches zero they - /// will delete themselves. This is effectively reference counting. + + /// Entities that are the target of counted relationships *count* the number of hosts they have. + /// If it ever reaches zero they will delete themselves. This is effectively reference counting. Counted, - /// When targets of recursively cleaning relations are deleted they also delete all their - /// hosts. Unsetting a recursively cleaning relation is the same as despawning the host. + + /// When entities that are the target of recursive relationships are despawned they also + /// *recursively* despawn their hosts. Unsetting **does not** trigger recursive cleanup. Recursive, + /// Total performs both counted and recursive cleanup. Total, } -/// The relation trait. This is what controls the cleanup policy and exclusivity of a relation. -pub trait Relation: 'static + Send + Sync { +/// Hack to ensure relation types are indeed ZSTs +pub trait ZstOrPanic: Sized { + const ZST_OR_PANIC: () = { + // TODO: Make diagnostic friendlier when `std::any::type_name` becomes const + // TODO: Use actual type level mechanism and remove hack when possible in stable + if std::mem::size_of::() != 0 { + panic!("Not a ZST") + } + }; +} + +impl ZstOrPanic for T {} + +/// The relation trait. This is what controls the cleanup, exclusivity & symmetry of a relation. +/// Relations can be thought of as arrows. The terms Aery uses for the base and head of this arrow +/// are "host" and "target" respectively. With both the host and target being entities. Both the +/// host and target are "participants". Exclusive relations that face bottom up in hierarchies have +/// many favorable properties so these are the default. +/// +/// Note that relations **must** be a [ZST](https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts). +/// A compile error will be produced if you try to use a relation that isn't one. +/// +/// Aery only supports relations that are non-fragmenting. Ie. an entities archetype is not affected +/// by the targets of its relations. See [this article](https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9) +/// for more information. This isn't necessarily good or bad. There are various tradeoffs but it +/// would be overwhelming to explain them all at once. To keep it quick the archetype fragmentation +/// is comparable to `bevy_hierarchy` if it supported multiple hierarchy types. +pub trait Relation: 'static + Sized + Send + Sync { + /// How to clean up entities and relations when an entity with a relation is despawned + /// or when a relation is unset. const CLEANUP_POLICY: CleanupPolicy = CleanupPolicy::Orphan; + + /// Whether or not an entity is allowed to have more than 1 of this relation type. + /// Entities can still be targeted multiple times by different entities with this relation. + /// Entities cannot however host more than 1 of this relation at a time. + /// Setting an exclusive relation that is already set will unset the existing relation. const EXCLUSIVE: bool = true; + + /// Whether or not a relation is symmetric. Ie: + /// - When `e0 -R-> e1` + /// - Then `e0 <-R- e1` + /// + /// For example it would make sense for a `MarriedTo` relation to be symmetric. + const SYMMETRIC: bool = false; } #[derive(Component, Default, Debug)] pub(crate) struct Edges { - pub hosts: [HashMap>; 4], - pub targets: [HashMap>; 4], -} - -#[derive(WorldQuery)] -pub struct EdgeWQ { - pub(crate) edges: &'static Edges, + pub hosts: [HashMap>; 4], + pub targets: [HashMap>; 4], } type EdgeIter<'a> = std::iter::Flatten< @@ -129,7 +192,7 @@ type EdgeIter<'a> = std::iter::Flatten< impl Edges { pub(crate) fn iter_hosts(&self) -> EdgeIter<'_> { self.hosts[R::CLEANUP_POLICY as usize] - .get(&TypeId::of::()) + .get(&RelationId::of::()) .map(|targets| targets.iter().copied()) .into_iter() .flatten() @@ -137,1169 +200,173 @@ impl Edges { pub(crate) fn iter_targets(&self) -> EdgeIter<'_> { self.targets[R::CLEANUP_POLICY as usize] - .get(&TypeId::of::()) + .get(&RelationId::of::()) .map(|targets| targets.iter().copied()) .into_iter() .flatten() } } -pub(crate) fn refragment(world: &mut World, entity: Entity) { - let Some(mut edges) = world.get_mut::(entity) else { - return - }; - - let (has_hosts, has_targets) = ( - edges.hosts[R::CLEANUP_POLICY as usize] - .get(&TypeId::of::()) - .map(|hosts| !hosts.is_empty()) - .unwrap_or(false), - edges.targets[R::CLEANUP_POLICY as usize] - .get(&TypeId::of::()) - .map(|targets| !targets.is_empty()) - .unwrap_or(false), - ); - - if !has_hosts { - edges.hosts[R::CLEANUP_POLICY as usize].remove(&TypeId::of::()); - } - - if !has_targets { - edges.targets[R::CLEANUP_POLICY as usize].remove(&TypeId::of::()); - } - - if edges - .targets - .iter() - .chain(edges.hosts.iter()) - .all(HashMap::is_empty) - { - world.entity_mut(entity).remove::(); - } - - match (has_hosts, has_targets) { - (_, true) => { - world - .entity_mut(entity) - .remove::>() - .insert(Participant:: { - _phantom: PhantomData, - }); - } - (true, false) => { - world - .entity_mut(entity) - .remove::>() - .insert(RootMarker:: { - _phantom: PhantomData, - }); - } - (false, false) => { - world - .entity_mut(entity) - .remove::<(Participant, RootMarker)>(); - } - } -} - -#[derive(Resource, Default)] -pub(crate) struct RefragmentHooks { - hooks: HashMap, -} - -/// Command to set relationships between entities. If either of the participants do not exist or the -/// host tries to target itself the operation will be ignored and logged. -pub struct Set -where - R: Relation, -{ - pub host: Entity, - pub target: Entity, - pub _phantom: PhantomData, -} - -impl Command for Set -where - R: Relation, -{ - fn write(self, world: &mut World) { - if self.host == self.target { - warn!( - "Tried to set {} from {:?} to itself. \ - Self referential relations are not allowed. \ - Ignoring.", - std::any::type_name::(), - self.host, - ); - return; - } - - if world.get_entity(self.target).is_none() { - warn!( - "Tried to set {rel} from {from:?} to {to:?}. \ - {to:?} does not exist. \ - Ignoring.", - rel = std::any::type_name::(), - from = self.host, - to = self.target, - ); - return; - } - - if world.get_entity(self.host).is_none() { - warn!( - "Tried to set {rel} from {from:?} to {to:?}. \ - {from:?} does not exist. \ - Ignoring.", - rel = std::any::type_name::(), - from = self.host, - to = self.target, - ); - return; - } - - world - .resource_mut::() - .hooks - .insert(TypeId::of::(), refragment::); - - let mut target_edges = world - .get_mut::(self.target) - .map(|mut edges| std::mem::take(&mut *edges)) - .unwrap_or_default(); - - target_edges.hosts[R::CLEANUP_POLICY as usize] - .entry(TypeId::of::()) - .or_default() - .insert(self.host); - - if !target_edges.targets[R::CLEANUP_POLICY as usize].contains_key(&TypeId::of::()) { - world.entity_mut(self.target).insert(RootMarker:: { - _phantom: PhantomData, - }); - } - - world.entity_mut(self.target).insert(target_edges); - - let mut host_edges = world - .entity_mut(self.host) - .remove::>() - .get_mut::() - .map(|mut edges| std::mem::take(&mut *edges)) - .unwrap_or_default(); - - let old = host_edges.targets[R::CLEANUP_POLICY as usize] - .get(&TypeId::of::()) - .and_then(|targets| targets.first()) - .copied(); - - host_edges.targets[R::CLEANUP_POLICY as usize] - .entry(TypeId::of::()) - .or_default() - .insert(self.target); - - world.entity_mut(self.host).insert(( - host_edges, - Participant:: { - _phantom: PhantomData, - }, - )); - - if let Some(old) = old.filter(|old| R::EXCLUSIVE && self.target != *old) { - Command::write( - Unset:: { - host: self.host, - target: old, - _phantom: PhantomData, - }, - world, - ); - } - } -} - -/// Command to remove relationships between entities -/// This operation is not noisy so if either participant does not exist or -/// the relation does not exist nothing happens. -pub struct Unset -where - R: Relation, -{ - pub host: Entity, - pub target: Entity, - pub _phantom: PhantomData, -} - -impl Command for Unset { - fn write(self, world: &mut World) { - Command::write( - UnsetErased { - host: self.host, - target: self.target, - typeid: TypeId::of::(), - policy: R::CLEANUP_POLICY, - }, - world, - ); - } -} - -struct UnsetErased { - host: Entity, - target: Entity, - typeid: TypeId, - policy: CleanupPolicy, -} - -impl Command for UnsetErased { - fn write(self, world: &mut World) { - let Some(refragment) = world - .resource::() - .hooks - .get(&self.typeid) - .copied() - else { - return - }; - - let Some(mut host_edges) = world - .get_mut::(self.host) - .map(|mut edges| std::mem::take(&mut *edges)) - else { - return - }; - - let Some(mut target_edges) = world - .get_mut::(self.target) - .map(|mut edges| std::mem::take(&mut *edges)) - else { - world.entity_mut(self.host).insert(host_edges); - return - }; - - host_edges.targets[self.policy as usize] - .entry(self.typeid) - .and_modify(|hosts| { - hosts.remove(&self.target); - }); - - target_edges.hosts[self.policy as usize] - .entry(self.typeid) - .and_modify(|hosts| { - hosts.remove(&self.host); - }); - - let target_orphaned = target_edges.hosts[self.policy as usize] - .get(&self.typeid) - .map_or(false, IndexSet::is_empty); - - world.entity_mut(self.host).insert(host_edges); - world.entity_mut(self.target).insert(target_edges); - - match self.policy { - CleanupPolicy::Orphan => { - refragment(world, self.host); - refragment(world, self.target); - } - CleanupPolicy::Recursive => { - Command::write(CheckedDespawn { entity: self.host }, world); - refragment(world, self.target); - } - CleanupPolicy::Counted => { - if target_orphaned { - Command::write( - CheckedDespawn { - entity: self.target, - }, - world, - ); - } - refragment(world, self.host); - } - CleanupPolicy::Total => { - Command::write(CheckedDespawn { entity: self.host }, world); - if target_orphaned { - Command::write( - CheckedDespawn { - entity: self.target, - }, - world, - ); - } - } - } - } -} - -/// Command for entities to to unset all targets of a given relation. -pub struct UnsetAll -where - R: Relation, -{ - pub entity: Entity, - pub _phantom: PhantomData, -} - -impl Command for UnsetAll { - fn write(self, world: &mut World) { - while let Some(target) = world - .get::(self.entity) - .and_then(|edges| edges.targets[R::CLEANUP_POLICY as usize].get(&TypeId::of::())) - .and_then(|targets| targets.first()) - .copied() - { - Command::write( - Unset:: { - target, - host: self.entity, - _phantom: PhantomData, - }, - world, - ); - } - } -} - -/// Command for entities to unset themselves from all relations of a given type. -pub struct Withdraw -where - R: Relation, -{ - pub entity: Entity, - pub _phantom: PhantomData, -} - -impl Command for Withdraw { - fn write(self, world: &mut World) { - while let Some(host) = world - .get::(self.entity) - .and_then(|edges| edges.hosts[R::CLEANUP_POLICY as usize].get(&TypeId::of::())) - .and_then(|targets| targets.first()) - .copied() - { - Command::write( - Unset:: { - host, - target: self.entity, - _phantom: PhantomData, - }, - world, - ); - } - } +#[derive(WorldQuery)] +pub struct EdgeWQ { + pub(crate) edges: &'static Edges, } -/// Command to despawn entities with rleations. Despawning via any other method can lead to -/// dangling which will not produce correct behavior! -pub struct CheckedDespawn { - pub entity: Entity, +/// Trait to check what relations exist. +pub trait CheckRelations { + /// Check if another entity is targeting this one via a relation. + /// ``` + ///# use bevy::prelude::*; + ///# use aery::{prelude::*, relation::EdgeWQItem}; + ///# + ///# #[derive(Relation)] + ///# struct R; + ///# + ///# fn foo(entity: EdgeWQItem<'_>, e: Entity) { + /// // Check if entity is the target of `e` via `R` + /// entity.has_host(R, e); + /// + /// // Check if entity is the target of any entity via `R` + /// entity.has_host(R, Wc); + /// + /// // Check if entity is the target of any entity via any relationship + /// entity.has_host(Wc, Wc); + ///# } + /// ``` + fn has_host(&self, relation: impl Into>, host: impl Into>) -> bool; + + /// Check if entity is targeting another via a relation. + /// ``` + ///# use bevy::prelude::*; + ///# use aery::{prelude::*, relation::EdgeWQItem}; + ///# + ///# #[derive(Relation)] + ///# struct R; + ///# + ///# fn foo(entity: EdgeWQItem<'_>, e: Entity) { + /// // Check if entity targets `e` via `R` + /// entity.has_target(R, e); + /// + /// // Check if entity targets of any other entity via `R` + /// entity.has_target(R, Wc); + /// + /// // Check if entity targets of any other entity via any relationship + /// entity.has_target(Wc, Wc); + ///# } + /// ``` + fn has_target( + &self, + relation: impl Into>, + target: impl Into>, + ) -> bool; } -impl Command for CheckedDespawn { - fn write(self, world: &mut World) { - let mut to_refrag = HashMap::>::new(); - let mut to_despawn = HashSet::::from([self.entity]); - let mut queue = VecDeque::from([self.entity]); - - let mut graph = world.query::<&mut Edges>(); - - while let Some(curr) = queue.pop_front() { - let Ok(edges) = graph - .get_mut(world, curr) - .map(|mut edges| std::mem::take(&mut *edges)) - else { - continue - }; - - // Total relations - for (typeid, target) in edges.targets[CleanupPolicy::Total as usize] +#[rustfmt::skip] +impl CheckRelations for Edges { + fn has_host(&self, relation: impl Into>, host: impl Into>) -> bool { + match (relation.into(), host.into()) { + (Var::Val(rel), Var::Val(host)) => self + .hosts .iter() - .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) - { - let Ok(mut target_edges) = graph.get_mut(world, *target) else { - continue - }; - - let Some(target_hosts) = target_edges - .hosts[CleanupPolicy::Total as usize] - .get_mut(typeid) - else { - continue - }; - - target_hosts.remove(&curr); - - if target_hosts.is_empty() { - queue.push_back(*target); - to_despawn.insert(*target); - } - } - - for (typeid, host) in edges.hosts[CleanupPolicy::Total as usize] + .any(|map| map.get(&rel).filter(|set| set.contains(&host)).is_some()), + (Var::Val(rel), Var::Wc) => self + .hosts .iter() - .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) - { - let Ok(mut host_edges) = graph.get_mut(world, *host) else { - continue - }; - - host_edges.targets[CleanupPolicy::Total as usize] - .entry(*typeid) - .and_modify(|bucket| { - bucket.remove(&curr); - }); - - queue.push_back(*host); - to_despawn.insert(*host); - } - - // Recursive relations - for (typeid, target) in edges.targets[CleanupPolicy::Recursive as usize] + .any(|map| map.get(&rel).is_some()), + (Var::Wc, Var::Val(host)) => self + .hosts .iter() - .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) - { - let Ok(mut target_edges) = graph.get_mut(world, *target) else { - continue - }; - - let Some(target_hosts) = target_edges - .hosts[CleanupPolicy::Recursive as usize] - .get_mut(typeid) - else { - continue - }; - - target_hosts.remove(&curr); - to_refrag.entry(*typeid).or_default().insert(*target); - } - - for (typeid, host) in edges.hosts[CleanupPolicy::Recursive as usize] + .flat_map(|map| map.values()) + .any(|set| set.contains(&host)), + (Var::Wc, Var::Wc) => self + .hosts .iter() - .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) - { - let Ok(mut host_edges) = graph.get_mut(world, *host) else { - continue - }; - - host_edges.targets[CleanupPolicy::Recursive as usize] - .entry(*typeid) - .and_modify(|bucket| { - bucket.remove(&curr); - }); - - queue.push_back(*host); - to_despawn.insert(*host); - } + .flat_map(|map| map.keys()) + .next() + .is_some(), + } + } - // Counted relations - for (typeid, target) in edges.targets[CleanupPolicy::Counted as usize] + fn has_target(&self, + relation: impl Into>, + target: impl Into>, + ) + -> bool + { + match (relation.into(), target.into()) { + (Var::Val(rel), Var::Val(target)) => self + .targets .iter() - .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) - { - let Ok(mut target_edges) = graph.get_mut(world, *target) else { - continue - }; - - let Some(target_hosts) = target_edges - .hosts[CleanupPolicy::Counted as usize] - .get_mut(typeid) - else { - continue - }; - - target_hosts.remove(&curr); - - if target_hosts.is_empty() { - queue.push_back(*target); - to_despawn.insert(*target); - } - } - - for (typeid, host) in edges.hosts[CleanupPolicy::Counted as usize] + .any(|map| map.get(&rel).filter(|set| set.contains(&target)).is_some()), + (Var::Val(rel), Var::Wc) => self + .targets .iter() - .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) - { - let Ok(mut host_edges) = graph.get_mut(world, *host) else { - continue - }; - - host_edges.targets[CleanupPolicy::Counted as usize] - .entry(*typeid) - .and_modify(|bucket| { - bucket.remove(&curr); - }); - - to_refrag.entry(*typeid).or_default().insert(*host); - } - - // Orphaning relations - for (typeid, target) in edges.targets[CleanupPolicy::Orphan as usize] + .any(|map| map.get(&rel).is_some()), + (Var::Wc, Var::Val(target)) => self + .targets .iter() - .flat_map(|(typeid, targets)| targets.iter().map(move |target| (typeid, target))) - { - let Ok(mut target_edges) = graph.get_mut(world, *target) else { - continue - }; - - target_edges.hosts[CleanupPolicy::Orphan as usize] - .entry(*typeid) - .and_modify(|bucket| { - bucket.remove(&curr); - }); - - to_refrag.entry(*typeid).or_default().insert(*target); - } - - for (typeid, host) in edges.hosts[CleanupPolicy::Orphan as usize] + .flat_map(|map| map.values()) + .any(|set| set.contains(&target)), + (Var::Wc, Var::Wc) => self + .targets .iter() - .flat_map(|(typeid, hosts)| hosts.iter().map(move |host| (typeid, host))) - { - let Ok(mut host_edges) = graph.get_mut(world, *host) else { - continue - }; - - host_edges.targets[CleanupPolicy::Orphan as usize] - .entry(*typeid) - .and_modify(|bucket| { - bucket.remove(&curr); - }); - - to_refrag.entry(*typeid).or_default().insert(*host); - } - } - - for entity in to_despawn { - world.despawn(entity); - } - - for (typeid, entities) in to_refrag { - let refrag = world - .resource::() - .hooks - .get(&typeid) - .copied() - .unwrap(); - - for entity in entities { - refrag(world, entity); - } + .flat_map(|map| map.keys()) + .next() + .is_some(), } } } -/// Convenience trait to sugar using relation commands. Can be used with `World` or `Commands`. -/// ``` -/// use bevy::prelude::*; -/// use aery::prelude::*; -/// -/// #[derive(Relation)] -/// struct R; -/// -/// fn sys(mut commands: Commands) { -/// let a = commands.spawn_empty().id(); -/// let b = commands.spawn_empty().id(); -/// commands.set::(a, b); -/// } -/// ``` -pub trait RelationCommands { - fn set(&mut self, host: Entity, target: Entity); - fn unset(&mut self, host: Entity, target: Entity); - fn unset_all(&mut self, entity: Entity); - fn withdraw(&mut self, entity: Entity); - fn checked_despawn(&mut self, entity: Entity); -} - -impl RelationCommands for Commands<'_, '_> { - fn set(&mut self, host: Entity, target: Entity) { - self.add(Set:: { - host, - target, - _phantom: PhantomData, - }); - } - - fn unset(&mut self, host: Entity, target: Entity) { - self.add(Unset:: { - host, - target, - _phantom: PhantomData, - }); - } - - fn unset_all(&mut self, entity: Entity) { - self.add(UnsetAll:: { - entity, - _phantom: PhantomData, - }); +impl CheckRelations for EdgeWQItem<'_> { + fn has_host(&self, relation: impl Into>, host: impl Into>) -> bool { + self.edges.has_host(relation, host) } - fn withdraw(&mut self, entity: Entity) { - self.add(Withdraw:: { - entity, - _phantom: PhantomData, - }); - } - - fn checked_despawn(&mut self, entity: Entity) { - self.add(CheckedDespawn { entity }); + fn has_target( + &self, + relation: impl Into>, + target: impl Into>, + ) -> bool { + self.edges.has_target(relation, target) } } -impl RelationCommands for World { - fn set(&mut self, host: Entity, target: Entity) { - Command::write( - Set:: { - host, - target, - _phantom: PhantomData, - }, - self, - ); - } - - fn unset(&mut self, host: Entity, target: Entity) { - Command::write( - Unset:: { - host, - target, - _phantom: PhantomData, - }, - self, - ); - } - - fn unset_all(&mut self, entity: Entity) { - Command::write( - UnsetAll:: { - entity, - _phantom: PhantomData, - }, - self, - ); - } - - fn withdraw(&mut self, entity: Entity) { - Command::write( - Withdraw:: { - entity, - _phantom: PhantomData, - }, - self, - ); +impl CheckRelations for EntityRef<'_> { + fn has_host( + &self, + relation: impl Into>, + target: impl Into>, + ) -> bool { + self.get::() + .map_or(false, |edges| edges.has_host(relation, target)) } - fn checked_despawn(&mut self, entity: Entity) { - Command::write(CheckedDespawn { entity }, self); + fn has_target( + &self, + relation: impl Into>, + target: impl Into>, + ) -> bool { + self.get::() + .map_or(false, |edges| edges.has_target(relation, target)) } } -#[cfg(test)] -mod tests { - use crate::{ - prelude::*, - relation::{Edges, Participant, RefragmentHooks, RootMarker}, - }; - use bevy::prelude::*; - use core::any::TypeId; - use std::array::from_fn; - - fn has_edges(world: &World, entity: Entity) -> bool { - world.get::(entity).is_some() - } - - fn is_root(world: &World, entity: Entity) -> bool { - world.get::>(entity).is_some() - } - - fn is_participant(world: &World, entity: Entity) -> bool { - world.get::>(entity).is_some() - } - - fn targeting(world: &World, host: Entity, target: Entity) -> bool { - let host_is_targeting = world - .get::(host) - .map(|edges| &edges.targets[R::CLEANUP_POLICY as usize]) - .and_then(|bucket| bucket.get(&TypeId::of::())) - .map_or(false, |set| set.contains(&target)); - - let target_is_hosted = world - .get::(target) - .map(|edges| &edges.hosts[R::CLEANUP_POLICY as usize]) - .and_then(|bucket| bucket.get(&TypeId::of::())) - .map_or(false, |set| set.contains(&host)); - - if host_is_targeting != target_is_hosted { - panic!("Asymmetric edge info"); - } - - host_is_targeting - } - - #[test] - fn set_unset() { - #[derive(Relation)] - struct R; - - let mut world = World::new(); - world.init_resource::(); - let [host, target] = from_fn(|_| world.spawn_empty().id()); - - world.set::(host, target); - assert!(targeting::(&world, host, target)); - assert!(is_participant::(&world, host)); - assert!(is_root::(&world, target)); - - world.unset::(host, target); - assert!(!has_edges(&world, target)); - assert!(!has_edges(&world, host)); - assert!(!is_participant::(&world, host)); - assert!(!is_root::(&world, target)); - } - - #[test] - fn exclusive() { - #[derive(Relation)] - struct R; - - let mut world = World::new(); - world.init_resource::(); - let [host, t0, t1] = from_fn(|_| world.spawn_empty().id()); - - // Before overwrite - world.set::(host, t0); - - assert!(targeting::(&world, host, t0)); - assert!(is_participant::(&world, host)); - assert!(is_root::(&world, t0)); - - // After overwrite - world.set::(host, t1); - - assert!(targeting::(&world, host, t1)); - assert!(is_participant::(&world, host)); - assert!(is_root::(&world, t1)); - - assert!(!has_edges(&world, t0)); - assert!(!is_root::(&world, t0)); - } - - #[derive(Relation)] - #[cleanup(policy = "Orphan")] - struct Orphan; - - #[derive(Relation)] - #[cleanup(policy = "Counted")] - struct Counted; - - #[derive(Relation)] - #[cleanup(policy = "Recursive")] - struct Recursive; - - #[derive(Relation)] - #[cleanup(policy = "Total")] - struct Total; - - #[derive(Debug)] - struct TestEdges { - orphan: Entity, - counted: Entity, - recursive: Entity, - total: Entity, - } - - #[derive(Debug)] - struct Test { - center: Entity, - targets: TestEdges, - hosts: TestEdges, - } - - impl Test { - fn new(world: &mut World) -> Self { - let test = Self { - center: world.spawn_empty().id(), - targets: TestEdges { - orphan: world.spawn_empty().id(), - counted: world.spawn_empty().id(), - recursive: world.spawn_empty().id(), - total: world.spawn_empty().id(), - }, - hosts: TestEdges { - orphan: world.spawn_empty().id(), - counted: world.spawn_empty().id(), - recursive: world.spawn_empty().id(), - total: world.spawn_empty().id(), - }, - }; - - world.set::(test.hosts.orphan, test.center); - world.set::(test.center, test.targets.orphan); - - world.set::(test.hosts.counted, test.center); - world.set::(test.center, test.targets.counted); - - world.set::(test.hosts.recursive, test.center); - world.set::(test.center, test.targets.recursive); - - world.set::(test.hosts.total, test.center); - world.set::(test.center, test.targets.total); - - test - } - - fn assert_unchanged(&self, world: &World) { - assert!(targeting::(world, self.hosts.orphan, self.center)); - assert!(targeting::(world, self.center, self.targets.orphan)); - assert!(is_participant::(world, self.hosts.orphan,)); - assert!(is_root::(world, self.targets.orphan)); - - assert!(targeting::(world, self.hosts.counted, self.center)); - assert!(targeting::( - world, - self.center, - self.targets.counted - )); - assert!(is_participant::(world, self.hosts.counted,)); - assert!(is_root::(world, self.targets.counted)); - - assert!(targeting::( - world, - self.hosts.recursive, - self.center - )); - assert!(targeting::( - world, - self.center, - self.targets.recursive - )); - assert!(is_participant::(world, self.hosts.recursive,)); - assert!(is_root::(world, self.targets.recursive)); - - assert!(targeting::(world, self.hosts.total, self.center)); - assert!(targeting::(world, self.center, self.targets.total)); - assert!(is_participant::(world, self.hosts.total)); - assert!(is_root::(world, self.targets.total)); - - assert!(is_participant::(world, self.center)); - assert!(is_participant::(world, self.center)); - assert!(is_participant::(world, self.center)); - assert!(is_participant::(world, self.center)); - } - - fn assert_cleaned(&self, world: &World) { - assert!(world.get_entity(self.center).is_none()); - - assert!(!has_edges(world, self.hosts.orphan)); - assert!(!has_edges(world, self.targets.orphan)); - assert!(!is_participant::(world, self.hosts.orphan)); - assert!(!is_root::(world, self.targets.orphan)); - - assert!(world.get_entity(self.targets.counted).is_none()); - assert!(!has_edges(world, self.hosts.counted)); - assert!(!is_participant::(world, self.hosts.counted,)); - - assert!(world.get_entity(self.hosts.recursive).is_none()); - assert!(!has_edges(world, self.targets.recursive)); - assert!(!is_root::(world, self.targets.recursive)); - - assert!(world.get_entity(self.hosts.total).is_none()); - assert!(world.get_entity(self.targets.total).is_none()); - } - } - - #[test] - fn orphan_in_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Orphan")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - - world.checked_despawn(e); - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn orphan_out_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Orphan")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - - world.checked_despawn(e); - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn counted_in_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Counted")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - - world.checked_despawn(e); - test.assert_cleaned(&world); - } - - #[test] - fn counted_out_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Counted")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - - world.checked_despawn(e); - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn recursive_in_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Recursive")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - - world.checked_despawn(e); - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn recursive_out_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Recursive")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - - world.checked_despawn(e); - test.assert_cleaned(&world); - } - - #[test] - fn total_in_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Total")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - - world.checked_despawn(e); - test.assert_cleaned(&world); - } - - #[test] - fn total_out_despawned() { - #[derive(Relation)] - #[cleanup(policy = "Total")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - - world.checked_despawn(e); - test.assert_cleaned(&world); - } - - #[test] - fn orphan_in_unset() { - #[derive(Relation)] - #[cleanup(policy = "Orphan")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - world.unset::(e, test.center); - - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn orphan_out_unset() { - #[derive(Relation)] - #[cleanup(policy = "Orphan")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - world.unset::(test.center, e); - - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn counted_in_unset() { - #[derive(Relation)] - #[cleanup(policy = "Counted")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - world.unset::(e, test.center); - - test.assert_cleaned(&world); - } - - #[test] - fn counted_out_unset() { - #[derive(Relation)] - #[cleanup(policy = "Counted")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - world.unset::(test.center, e); - - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn recursive_in_unset() { - #[derive(Relation)] - #[cleanup(policy = "Recursive")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - world.unset::(e, test.center); - - test.assert_unchanged(&world); - assert!(!is_participant::(&world, test.center)); - } - - #[test] - fn recursive_out_unset() { - #[derive(Relation)] - #[cleanup(policy = "Recursive")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - world.unset::(test.center, e); - - test.assert_cleaned(&world); - } - - #[test] - fn total_in_unset() { - #[derive(Relation)] - #[cleanup(policy = "Total")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(e, test.center); - world.unset::(e, test.center); - - test.assert_cleaned(&world); - } - - #[test] - fn total_out_unset() { - #[derive(Relation)] - #[cleanup(policy = "Total")] - struct R; - - let mut world = World::new(); - world.init_resource::(); - - let test = Test::new(&mut world); - - let e = world.spawn_empty().id(); - world.set::(test.center, e); - world.unset::(test.center, e); - - test.assert_cleaned(&world); +impl CheckRelations for EntityMut<'_> { + fn has_host( + &self, + relation: impl Into>, + target: impl Into>, + ) -> bool { + self.get::() + .map_or(false, |edges| edges.has_host(relation, target)) + } + + fn has_target( + &self, + relation: impl Into>, + target: impl Into>, + ) -> bool { + self.get::() + .map_or(false, |edges| edges.has_target(relation, target)) } } diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 0000000..64a2994 --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,199 @@ +use crate::{ + commands::{RelationCommands, Set}, + relation::{Relation, ZstOrPanic}, +}; + +use bevy::{ + ecs::{entity::Entity, system::Command, world::EntityMut}, + log::warn, +}; + +use std::marker::PhantomData; + +/// An extension API for `EntityMut<'_>` to make spawning and changing relation graphs easier. +/// All functions will set relations if they do not exist including overwriting exclusive relations! +/// Since changing relations can trigger cleanup procedures that might despawn the `Entity` referred +/// to by `EntytMut<'_>` each method is consuming and returns an `Option>`. +/// +/// For convenience `Scope<'_>` is also implemented for `Option>`. Where the methods +/// are essentially their non-option equivalent wrapped in an implicit [`Option::and_then`] call +/// that emits warnings when the option is `None`. +pub trait Scope<'a>: Sized { + // DEV NOTE: + // + // Will always return `Some` for Slef = `EntityMut` but not for Self = `Option>` + // Can make generic programming slightly annoying to reflect this in the API so is left as is. + // + /// Spawns a host and gives mutable access to it via `EntityMut<'_>`. + /// Hierarchies are bottom up so the host is also the descendant. + fn scope(self, func: impl FnMut(Entity, EntityMut<'_>)) -> Option>; + + /// Spawns a target and gives mutable access to it via `EntityMut<'_>`. + /// Hierarchies are bottom up so the target is also the ancestor. + fn scope_target( + self, + func: impl FnMut(Entity, EntityMut<'_>), + ) -> Option>; + + /// Tries to scope an existing entity as a host. Gives mutable access via `EntityMut<'_>`. + /// A warning is emitted when the entity doesn't exist. + /// Hierarchies are bottom up so the host is also the descendant. + fn scope_entity( + self, + entity: Entity, + func: impl FnMut(EntityMut<'_>), + ) -> Option>; + + /// Tries to scope an existing entity as a target. Gives mutable access via `EntityMut<'_>`. + /// A warning is emitted when the entity doesn't exist. + /// Hierarchies are bottom up so the target is also the ancestor. + fn scope_target_entity( + self, + entity: Entity, + func: impl FnMut(EntityMut<'_>), + ) -> Option>; +} + +impl<'a> Scope<'a> for EntityMut<'a> { + fn scope(self, mut func: impl FnMut(Entity, EntityMut<'_>)) -> Option { + let _ = R::ZST_OR_PANIC; + + let target = self.id(); + let world = self.into_world_mut(); + let host = world.spawn_empty(); + + if let Some(host) = host.set::(target) { + func(target, host); + } + + world.get_entity_mut(target) + } + + fn scope_target( + self, + mut func: impl FnMut(Entity, EntityMut<'_>), + ) -> Option { + let _ = R::ZST_OR_PANIC; + + let host = self.id(); + let world = self.into_world_mut(); + let target = world.spawn_empty().id(); + + Command::apply( + Set:: { + host, + target, + _phantom: PhantomData, + }, + world, + ); + + if let Some(target) = world.get_entity_mut(target) { + func(host, target); + } + + world.get_entity_mut(host) + } + + fn scope_target_entity( + self, + entity: Entity, + mut func: impl FnMut(EntityMut<'_>), + ) -> Option { + let _ = R::ZST_OR_PANIC; + + let host = self.id(); + let world = self.into_world_mut(); + + Command::apply( + Set:: { + host, + target: entity, + _phantom: PhantomData, + }, + world, + ); + + if let Some(target) = world.get_entity_mut(entity) { + func(target) + } else { + warn!( + "Tried to scope {:?} as a target which does not exist. Ignoring.", + entity + ); + }; + + world.get_entity_mut(host) + } + + fn scope_entity( + self, + entity: Entity, + mut func: impl FnMut(EntityMut<'_>), + ) -> Option { + let _ = R::ZST_OR_PANIC; + + let target = self.id(); + let world = self.into_world_mut(); + + if let Some(host) = world + .get_entity_mut(entity) + .and_then(|host| host.set::(target)) + { + func(host); + } else { + warn!( + "Tried to scope {:?} as a host which does not exist. Ignoring.", + entity + ); + } + + world.get_entity_mut(target) + } +} + +impl<'a> Scope<'a> for Option> { + fn scope_target(self, func: impl FnMut(Entity, EntityMut<'_>)) -> Self { + match self { + Some(entity_mut) => entity_mut.scope_target::(func), + None => { + warn!("Tried to scope from an optional entity that doesn't exist. Ignoring.",); + None + } + } + } + + fn scope(self, func: impl FnMut(Entity, EntityMut<'_>)) -> Self { + match self { + Some(entity_mut) => entity_mut.scope::(func), + None => { + warn!("Tried to scope from an optional entity that doesn't exist. Ignoring.",); + None + } + } + } + + fn scope_target_entity( + self, + entity: Entity, + func: impl FnMut(EntityMut<'_>), + ) -> Self { + match self { + Some(entity_mut) => entity_mut.scope_target_entity::(entity, func), + None => { + warn!("Tried to scope from an optional entity that doesn't exist. Ignoring.",); + None + } + } + } + + fn scope_entity(self, entity: Entity, func: impl FnMut(EntityMut<'_>)) -> Self { + match self { + Some(entity_mut) => entity_mut.scope_entity::(entity, func), + None => { + warn!("Tried to scope from an optional entity that doesn't exist. Ignoring.",); + None + } + } + } +} diff --git a/src/tuple_traits.rs b/src/tuple_traits.rs index c052cf4..1866b4b 100644 --- a/src/tuple_traits.rs +++ b/src/tuple_traits.rs @@ -47,7 +47,7 @@ macro_rules! count { ($_:tt $($tail:tt)*) => { 1 + count!($($tail)*) }; } -pub trait RelationSet: Sealed { +pub trait RelationSet: Sized + Sealed { type Filters: ReadOnlyWorldQuery; } @@ -167,7 +167,7 @@ where } } -/*macro_rules! impl_joinable { +macro_rules! impl_joinable { ($(($P:ident, $p:ident, $e:ident, $v:ident)),*) => { impl<'a, $($P),*> Joinable<'a, { count!($($P )*) }> for ($($P,)*) where @@ -198,40 +198,4 @@ where } } -all_tuples!(impl_joinable, 2, 15, P, p, e, v);*/ - -macro_rules! impl_joinable { - ($(($P:ident, $p:ident, $e:ident, $v:ident)),*) => { - impl<'a, P0, $($P),*> Joinable<'a, { 1 + count!($($P )*) }> for (P0, $($P,)*) - where - P0: Sealed + Joinable<'a, 1>, - $($P: Sealed + Joinable<'a, 1>,)* - { - type Out = (>::Out, $(<$P as Joinable<'a, 1>>::Out,)*); - - fn check( - (p0, $($p,)*): &Self, - [e0, $($e,)*]: [Entity; 1 + count!($($P )*)] - ) - -> [bool; 1 + count!($($p )*)] - { - let [v0] = Joinable::check(p0, [e0]); - $(let [$v] = Joinable::check($p, [$e]); )* - [v0, $($v,)*] - } - - fn join( - (p0, $($p,)*): &'a mut Self, - [e0, $($e,)*]: [Entity; 1 + count!($($P )*)] - ) - -> Self::Out - { - let v0 = Joinable::join(p0, [e0]); - $(let $v = Joinable::join($p, [$e]); )* - (v0, $($v,)*) - } - } - } -} - -all_tuples!(impl_joinable, 1, 15, P, p, e, v); +all_tuples!(impl_joinable, 2, 15, P, p, e, v);