diff --git a/mvp-effects.md b/mvp-effects.md index 13bb610..e69d67e 100644 --- a/mvp-effects.md +++ b/mvp-effects.md @@ -11,13 +11,13 @@ # Event Effects - [x] `EventSend` -# Query Effects -- [ ] `QueryPut` -- [ ] `QueryWith` +# Components Effects +- [x] `ComponentsPut` +- [x] `ComponentsWith` -# QueryEntity Effects -- [ ] `QueryEntityPut` -- [ ] `QueryEntityWith` +# EntityComponents Effects +- [ ] `EntityComponentsPut` +- [ ] `EntityComponentsWith` # Command Effects - [ ] `InsertResource` diff --git a/src/effects/components.rs b/src/effects/components.rs new file mode 100644 index 0000000..8fda8ac --- /dev/null +++ b/src/effects/components.rs @@ -0,0 +1,296 @@ +use std::marker::PhantomData; + +use bevy::ecs::query::{QueryFilter, ReadOnlyQueryData, WorldQuery}; +use bevy::ecs::system::SystemParam; +use bevy::prelude::*; +use bevy::utils::all_tuples; + +use crate::Effect; + +/// [`Effect`] that sets `Component`s of all entities in a query to the provided `Component` tuple. +/// +/// Can be parameterized by a `QueryFilter` to narrow down the components updated. +pub struct ComponentsPut +where + C: Clone, + Filter: QueryFilter, +{ + components: C, + filter: PhantomData, +} + +impl ComponentsPut +where + C: Clone, + Filter: QueryFilter, +{ + /// Construct a new [`ComponentsPut`]. + pub fn new(components: C) -> Self { + ComponentsPut { + components, + filter: PhantomData, + } + } +} + +macro_rules! impl_effect_for_components_put { + ($(($C:ident, $c:ident, $r:ident)),*) => { + impl<$($C,)* Filter> Effect for ComponentsPut<($($C,)*), Filter> + where + $($C: Component + Clone),*, + Filter: QueryFilter + 'static, + { + type MutParam = Query<'static, 'static, ($(&'static mut $C,)*), Filter>; + + fn affect(self, param: &mut ::Item<'_, '_>) { + let ($($c,)*) = self.components; + param.par_iter_mut().for_each(|($(mut $r,)*)| { + $(*$r = $c.clone();)* + }); + } + } + } +} + +all_tuples!(impl_effect_for_components_put, 1, 15, C, c, r); + +/// [`Effect`] that transforms `Component`s of all entities in a query with the provided function. +/// +/// Can be parameterized by a `ReadOnlyQueryData` to access additional query data in the function. +/// +/// Can be parameterized by a `QueryFilter` to narrow down the components updated. +pub struct ComponentsWith +where + F: for<'a> Fn(C, ::Item<'a>) -> C + Send + Sync, + C: Clone, + Data: ReadOnlyQueryData, + Filter: QueryFilter, +{ + f: F, + components: PhantomData, + data: PhantomData, + filter: PhantomData, +} + +impl ComponentsWith +where + F: for<'a> Fn(C, ::Item<'a>) -> C + Send + Sync, + C: Clone, + Data: ReadOnlyQueryData, + Filter: QueryFilter, +{ + /// Construct a new [`ComponentsWith`]. + pub fn new(f: F) -> Self { + ComponentsWith { + f, + components: PhantomData, + data: PhantomData, + filter: PhantomData, + } + } +} + +macro_rules! impl_effect_for_components_with { + ($(($C:ident, $c:ident, $r:ident)),*) => { + impl Effect for ComponentsWith + where + F: for<'a> Fn(($($C,)*), ::Item<'a>) -> ($($C,)*) + Send + Sync, + $($C: Component + Clone),*, + Data: ReadOnlyQueryData + 'static, + Filter: QueryFilter + 'static, + { + type MutParam = Query<'static, 'static, (($(&'static mut $C,)*), Data), Filter>; + + fn affect(self, param: &mut ::Item<'_, '_>) { + param.par_iter_mut().for_each(|(($(mut $r,)*), data)| { + let cloned = ($($r.clone(),)*); + let ($($c,)*) = (self.f)(cloned, data); + $(*$r = $c;)* + }); + } + } + } +} + +all_tuples!(impl_effect_for_components_with, 1, 15, C, c, r); + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use proptest_derive::Arbitrary; + + use super::*; + use crate::effects::one_way_fn::OneWayFn; + use crate::system_combinators::affect; + + #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Component, Arbitrary)] + struct NumberComponent(u128); + + #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Component)] + struct MarkerComponent; + + #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Arbitrary)] + struct BundleToBeMarked { + initial: B, + to_be_marked: bool, + } + + fn spawn_bundle_and_mark( + world: &mut World, + BundleToBeMarked { + initial, + to_be_marked, + }: BundleToBeMarked, + ) -> Entity { + let mut entity_commands = world.spawn(initial); + + if to_be_marked { + entity_commands.insert(MarkerComponent); + } + + entity_commands.id() + } + + proptest! { + #[test] + fn components_put_overwrites_initial_state( + test_bundles in proptest::collection::vec(any::, NumberComponent<1>)>>(), 0..16), + put: (NumberComponent<0>, NumberComponent<1>) + ) { + let mut app = App::new(); + + let entities = test_bundles + .iter() + .copied() + .map(|bundle| spawn_bundle_and_mark(app.world_mut(), bundle)) + .collect::>(); + + app.add_systems( + Update, + (move || ComponentsPut::<_, With>::new(put)).pipe(affect), + ); + + app.update(); + + for ( + BundleToBeMarked { + initial, + to_be_marked, + }, + entity, + ) in test_bundles.into_iter().zip(entities) + { + let actual0 = app.world().get::>(entity).unwrap(); + let actual1 = app.world().get::>(entity).unwrap(); + + let (expected0, expected1) = if to_be_marked { put } else { initial }; + + prop_assert_eq!(actual0, &expected0); + prop_assert_eq!(actual1, &expected1); + } + } + + #[test] + fn components_with_correctly_executes_one_way_function( + test_bundles in proptest::collection::vec(any::, NumberComponent<1>)>>(), 0..16), + f0: OneWayFn, + f1: OneWayFn + ) { + let mut app = App::new(); + + let entities = test_bundles + .iter() + .copied() + .map(|bundle| spawn_bundle_and_mark(app.world_mut(), bundle)) + .collect::>(); + + app.add_systems( + Update, + (move || { + ComponentsWith::<_, _, (), With>::new( + move |(NumberComponent(n0), NumberComponent(n1)), _| { + ( + NumberComponent::<0>(f0.call(n0)), + NumberComponent::<1>(f1.call(n1)), + ) + }, + ) + }) + .pipe(affect), + ); + + app.update(); + + for ( + BundleToBeMarked { + initial: (NumberComponent(initial0), NumberComponent(initial1)), + to_be_marked, + }, + entity, + ) in test_bundles.into_iter().zip(entities) + { + let actual0 = app.world().get::>(entity).unwrap(); + let actual1 = app.world().get::>(entity).unwrap(); + + let (expected0, expected1) = if to_be_marked { + ( + NumberComponent(f0.call(initial0)), + NumberComponent(f1.call(initial1)), + ) + } else { + (NumberComponent(initial0), NumberComponent(initial1)) + }; + + prop_assert_eq!(actual0, &expected0); + prop_assert_eq!(actual1, &expected1); + } + } + + #[test] + fn read_only_query_data_paramaterizes_components_with_function( + initial_bundles in proptest::collection::vec(any::, NumberComponent<1>)>>(), 0..16), + f: OneWayFn, + ) { + let mut app = App::new(); + + let entities = initial_bundles + .iter() + .copied() + .map(|bundle| spawn_bundle_and_mark(app.world_mut(), bundle)) + .collect::>(); + + app.add_systems( + Update, + (move || { + ComponentsWith::<_, _, &NumberComponent<0>, With>::new( + move |_, NumberComponent(to_read)| (NumberComponent::<1>(f.call(*to_read)),), + ) + }) + .pipe(affect), + ); + + app.update(); + + for ( + BundleToBeMarked { + initial: (expected_read_component, NumberComponent(initial_written_component)), + to_be_marked, + }, + entity, + ) in initial_bundles.into_iter().zip(entities) + { + let actual_read_component = app.world().get::>(entity).unwrap(); + let actual_written_component = app.world().get::>(entity).unwrap(); + + let expected_written_component = if to_be_marked { + NumberComponent(f.call(expected_read_component.0)) + } else { + NumberComponent(initial_written_component) + }; + + prop_assert_eq!(actual_read_component, &expected_read_component); + prop_assert_eq!(actual_written_component, &expected_written_component); + } + } + } +} diff --git a/src/effects/mod.rs b/src/effects/mod.rs index 091bd3c..8bc5ff5 100644 --- a/src/effects/mod.rs +++ b/src/effects/mod.rs @@ -8,5 +8,8 @@ pub use resource::{ResPut, ResWith}; mod event; pub use event::EventSend; +mod components; +pub use components::{ComponentsPut, ComponentsWith}; + #[cfg(test)] mod one_way_fn; diff --git a/src/prelude.rs b/src/prelude.rs index 04d0923..9ff4026 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,5 +1,5 @@ //! `use bevy_pipe_affect::prelude::*;` to import common items. -pub use crate::effects::{EventSend, ResPut, ResWith}; +pub use crate::effects::{ComponentsPut, ComponentsWith, EventSend, ResPut, ResWith}; pub use crate::system_combinators::{affect, and_compose}; pub use crate::{Effect, EffectOut};