Skip to content

Commit

Permalink
Merge pull request JoJoJet#58 from RobWalt/feat/without-one
Browse files Browse the repository at this point in the history
solves JoJoJet#50 

feat: Implement `WithoutAny` filter

This PR:

1. implements `WithoutAny`, a `QueryFilter` which is supposed to be the opposite of `WithOne`
2. cleans up some parts of `WithOne` to make it `QueryFilter` only

Note that 2. is a breaking change since we can't use it in the Data position anymore. However, I think this is fine since there is `One` which already fills this gap. So this is really a fix since it clears up the separation of concerns of the two structs.

---

Small pseudo code example:

```rust
struct Food;

trait Fruit {}
struct Banana;
impl Fruit for Banana {}
struct Apple;
impl Fruit for Apple {}

struct Sweets;
struct Cake;

fn eat_unhealthy(
  mut commands: Commands,
  q_non_fruits: Query<Entity, (With<Food>, WithoutAny<&dyn Fruit>)>
) {
  q_non_fruits.iter().for_each(|food| {
    // only sweets and cakes without fruits
    commands.eat(food); 
  });
}  
```
  • Loading branch information
RobWalt authored Oct 28, 2024
2 parents 4328255 + bf3bd03 commit 5df9eb3
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 19 deletions.
113 changes: 94 additions & 19 deletions src/one.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,6 @@ unsafe impl<Trait: ?Sized + TraitQuery> WorldQuery for WithOne<Trait> {
}

const IS_DENSE: bool = false;
// const IS_ARCHETYPAL: bool = false;

#[inline]
unsafe fn set_archetype<'w>(
Expand Down Expand Up @@ -817,25 +816,105 @@ unsafe impl<Trait: ?Sized + TraitQuery> WorldQuery for WithOne<Trait> {
access: &mut bevy_ecs::query::FilteredAccess<ComponentId>,
) {
let mut new_access = access.clone();
let mut not_first = false;
for &component in state.components.iter() {
let mut intermediate = access.clone();
intermediate.and_with(component);
new_access.append_or(&intermediate);
}
*access = new_access;
}

#[inline]
fn init_state(world: &mut World) -> Self::State {
TraitQueryState::init(world)
}

#[inline]
fn get_state(_: &Components) -> Option<Self::State> {
// TODO: fix this https://github.com/bevyengine/bevy/issues/13798
panic!("transmuting and any other operations concerning the state of a query are currently broken and shouldn't be used. See https://github.com/JoJoJet/bevy-trait-query/issues/59");
}

#[inline]
fn matches_component_set(
state: &Self::State,
set_contains_id: &impl Fn(ComponentId) -> bool,
) -> bool {
state.matches_component_set_one(set_contains_id)
}
}

/// SAFETY: read-only access
impl<Trait: ?Sized + TraitQuery> QueryFilter for WithOne<Trait> {
const IS_ARCHETYPAL: bool = false;
unsafe fn filter_fetch(
_fetch: &mut Self::Fetch<'_>,
_entity: Entity,
_table_row: TableRow,
) -> bool {
true
}
}

/// [`WorldQuery`] filter for entities without any [one](crate::One) component
/// implementing a trait.
pub struct WithoutAny<Trait: ?Sized + TraitQuery>(PhantomData<&'static Trait>);

// this takes inspiration from `With` in bevy's main repo
unsafe impl<Trait: ?Sized + TraitQuery> WorldQuery for WithoutAny<Trait> {
type Item<'w> = ();
type Fetch<'w> = ();
type State = TraitQueryState<Trait>;

#[inline]
fn shrink<'wlong: 'wshort, 'wshort>(item: QueryItem<'wlong, Self>) -> QueryItem<'wshort, Self> {
item
}

#[inline]
unsafe fn init_fetch(
_world: UnsafeWorldCell<'_>,
_state: &Self::State,
_last_run: Tick,
_this_run: Tick,
) {
}

const IS_DENSE: bool = false;

#[inline]
unsafe fn set_archetype<'w>(
_fetch: &mut (),
_state: &Self::State,
_archetype: &'w bevy_ecs::archetype::Archetype,
_table: &'w bevy_ecs::storage::Table,
) {
}

#[inline]
unsafe fn set_table(_fetch: &mut (), _state: &Self::State, _table: &bevy_ecs::storage::Table) {}

#[inline]
unsafe fn fetch<'w>(
_fetch: &mut Self::Fetch<'w>,
_entity: Entity,
_table_row: TableRow,
) -> Self::Item<'w> {
}

#[inline]
fn update_component_access(
state: &Self::State,
access: &mut bevy_ecs::query::FilteredAccess<ComponentId>,
) {
for &component in &*state.components {
assert!(
!access.access().has_write(component),
"&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",
std::any::type_name::<Trait>(),
);
if not_first {
let mut intermediate = access.clone();
intermediate.add_read(component);
new_access.append_or(&intermediate);
new_access.extend_access(&intermediate);
} else {
new_access.and_with(component);
new_access.access_mut().add_read(component);
not_first = true;
}
access.and_without(component);
}
*access = new_access;
}

#[inline]
Expand All @@ -854,16 +933,12 @@ unsafe impl<Trait: ?Sized + TraitQuery> WorldQuery for WithOne<Trait> {
state: &Self::State,
set_contains_id: &impl Fn(ComponentId) -> bool,
) -> bool {
state.matches_component_set_one(set_contains_id)
!state.components.iter().any(|&id| set_contains_id(id))
}
}

/// SAFETY: read-only access
unsafe impl<Trait: ?Sized + TraitQuery> QueryData for WithOne<Trait> {
type ReadOnly = Self;
}
unsafe impl<Trait: ?Sized + TraitQuery> ReadOnlyQueryData for WithOne<Trait> {}
impl<Trait: ?Sized + TraitQuery> QueryFilter for WithOne<Trait> {
impl<Trait: ?Sized + TraitQuery> QueryFilter for WithoutAny<Trait> {
const IS_ARCHETYPAL: bool = false;
unsafe fn filter_fetch(
_fetch: &mut Self::Fetch<'_>,
Expand Down
38 changes: 38 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,44 @@ fn print_with_one_filter_info(
output.0.push(Default::default());
}

#[test]
fn without_any_filter() {
let mut world = World::new();
world.init_resource::<Output>();
world
.register_component_as::<dyn Person, Human>()
.register_component_as::<dyn Person, Dolphin>();

world.spawn(Human("Henry".to_owned(), 22));
world.spawn((Human("Henry".to_owned(), 22), Dolphin(22)));
world.spawn(Dolphin(22));
world.spawn(Fem);

let mut schedule = Schedule::default();
schedule.add_systems(print_without_any_filter_info);

schedule.run(&mut world);

assert_eq!(
world.resource::<Output>().0,
&["People that are neither Human or Dolphin:", "3v1", "",]
);
}

// Prints the entity id of every Entity where none of its components implement the trait
fn print_without_any_filter_info(
people: Query<Entity, WithoutAny<dyn Person>>,
mut output: ResMut<Output>,
) {
output
.0
.push("People that are neither Human or Dolphin:".to_string());
for person in (&people).into_iter() {
output.0.push(format!("{person}"));
}
output.0.push(Default::default());
}

#[queryable]
pub trait Messages {
fn send(&mut self, _: &dyn Display);
Expand Down

0 comments on commit 5df9eb3

Please sign in to comment.