moirai-for-games 0.1.0

A small deterministic no_std ECS for constrained and headless games
Documentation
//! Entity-id query iterators and lightweight entity views.
//!
//! [`QueryIds`] and [`QueryEntities`] materialize id-only query results. [`EntityRef`] exposes
//! read-only component access for one entity during entity-only traversal.

use alloc::string::String;
use alloc::vec::Vec;
use core::any::type_name;

use crate::entity::EntityId;
use crate::time::ChangeTick;
use crate::world::{World, WorldError};

/// Read-only view of one entity in the world backing an entity query.
#[derive(Clone, Copy)]
pub struct EntityRef<'w> {
    pub(crate) world: &'w World,
    pub(crate) entity: EntityId,
}

impl<'w> EntityRef<'w> {
    /// Entity id for this view.
    pub fn id(self) -> EntityId {
        self.entity
    }

    /// Whether the entity currently has the registered component type.
    pub fn has<T: 'static>(self) -> Result<bool, WorldError> {
        let id =
            self.world
                .registry_id_of::<T>()
                .ok_or_else(|| WorldError::UnregisteredComponent {
                    name: String::from(type_name::<T>()),
                })?;
        Ok(self.world.entity_has_component(self.entity, id.index()))
    }

    /// Borrows the component value when present.
    pub fn get<T: 'static>(self) -> Result<Option<&'w T>, WorldError> {
        self.world.get::<T>(self.entity)
    }
}

/// Iterator over entity ids produced by an entity-only query.
pub struct QueryIds<'w, 'c> {
    pub(crate) world: &'w World,
    pub(crate) ids: Vec<EntityId>,
    pub(crate) index: usize,
    pub(crate) exhausted: bool,
    pub(crate) fingerprint: u64,
    pub(crate) captured_now: ChangeTick,
    pub(crate) cursor_committed: bool,
    pub(crate) cursor: Option<&'c mut crate::query::QueryCursor>,
}

impl QueryIds<'_, '_> {
    fn commit_cursor_if_needed(&mut self) {
        if self.cursor_committed {
            return;
        }
        let world = self.world;
        let fingerprint = self.fingerprint;
        let cursor = self
            .cursor
            .as_mut()
            .filter(|cursor| cursor.validate(world, fingerprint).is_ok());
        if let Some(cursor) = cursor {
            cursor.commit(self.captured_now);
        }
        self.cursor_committed = true;
    }
}

impl Iterator for QueryIds<'_, '_> {
    type Item = EntityId;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(&entity) = self.ids.get(self.index) {
            self.index += 1;
            return Some(entity);
        }
        self.exhausted = true;
        self.commit_cursor_if_needed();
        None
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let remaining = self.ids.len().saturating_sub(self.index);
        (remaining, Some(remaining))
    }
}

impl ExactSizeIterator for QueryIds<'_, '_> {}

impl Drop for QueryIds<'_, '_> {
    fn drop(&mut self) {
        if self.exhausted {
            self.commit_cursor_if_needed();
        }
    }
}

/// Iterator over [`EntityRef`] views produced by an entity-only query.
pub struct QueryEntities<'w, 'c> {
    pub(crate) inner: QueryIds<'w, 'c>,
}

impl<'w> Iterator for QueryEntities<'w, '_> {
    type Item = EntityRef<'w>;

    fn next(&mut self) -> Option<Self::Item> {
        let entity = self.inner.next()?;
        Some(EntityRef {
            world: self.inner.world,
            entity,
        })
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        self.inner.size_hint()
    }
}

impl ExactSizeIterator for QueryEntities<'_, '_> {}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::component::ComponentOptions;
    use crate::query::{QueryCursor, QueryParams, QuerySpec};
    use crate::world::WorldBuilder;

    #[derive(Clone, Copy)]
    struct Position;

    #[derive(Clone, Copy)]
    struct Unregistered;

    #[test]
    fn entity_ref_has_reports_unregistered_component() {
        let mut builder = WorldBuilder::new();
        builder
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("position");
        let mut world = builder.build().expect("world");
        let entity = world.spawn().expect("spawn");
        world.insert(entity, Position).expect("position");
        let entity_ref = EntityRef {
            world: &world,
            entity,
        };

        assert!(matches!(
            entity_ref.has::<Unregistered>(),
            Err(WorldError::UnregisteredComponent { .. })
        ));
    }

    #[test]
    fn exhausted_query_ids_commit_the_entity_cursor() {
        let mut builder = WorldBuilder::new();
        builder
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("position");
        let mut world = builder.build().expect("world");
        let entity = world.spawn().expect("spawn");
        world.insert(entity, Position).expect("position");
        let spec = QuerySpec::new().added::<Position>();
        let mut cursor = QueryCursor::for_entities_from_start(&mut world, &spec).expect("cursor");
        let before = cursor.since();

        let ids = world
            .query_ids(&spec, QueryParams::new().cursor(&mut cursor))
            .expect("ids")
            .collect::<Vec<_>>();

        assert_eq!(ids, alloc::vec![entity]);
        assert!(cursor.since() > before);
    }
}