moirai-for-games 0.1.0

A small deterministic no_std ECS for constrained and headless games
Documentation
//! Change-detection cursor bound to one query spec and world owner.
//!
//! Records the last [`ChangeTick`] observed by a successful
//! added/changed traversal. Typed iterators commit the cursor only after full exhaustion.

use crate::query::QueryError;
use crate::time::ChangeTick;
use crate::world::WorldOwner;

/// Last-observed change tick for one owner-bound added/changed query window.
pub struct QueryCursor {
    owner: WorldOwner,
    fingerprint: u64,
    last_observed: ChangeTick,
}

impl QueryCursor {
    /// Opens a window from the start of change history for a single-component query.
    pub fn from_spec_start<T: 'static>(
        world: &mut crate::world::World,
        spec: &crate::query::QuerySpec,
    ) -> Result<Self, QueryError> {
        let fingerprint = world.query_fingerprint::<T>(spec)?;
        Ok(Self::from_start(world, fingerprint))
    }

    /// Opens a window ending at the world's current change tick for a single-component query.
    pub fn from_spec_now<T: 'static>(
        world: &mut crate::world::World,
        spec: &crate::query::QuerySpec,
    ) -> Result<Self, QueryError> {
        let fingerprint = world.query_fingerprint::<T>(spec)?;
        Self::from_now(world, fingerprint)
    }

    /// Opens a window from the start of change history for a two-component query.
    pub fn from_spec2_start<A: 'static, B: 'static>(
        world: &mut crate::world::World,
        spec: &crate::query::QuerySpec,
    ) -> Result<Self, QueryError> {
        let (plan, _, _) = world.resolve_query2_plan::<A, B>(spec)?;
        Ok(Self::from_start(world, plan.fingerprint))
    }

    /// Opens a window ending at the world's current change tick for a two-component query.
    pub fn from_spec2_now<A: 'static, B: 'static>(
        world: &mut crate::world::World,
        spec: &crate::query::QuerySpec,
    ) -> Result<Self, QueryError> {
        let (plan, _, _) = world.resolve_query2_plan::<A, B>(spec)?;
        Self::from_now(world, plan.fingerprint)
    }

    #[cfg(test)]
    pub(crate) fn for_entities_from_start(
        world: &mut crate::world::World,
        spec: &crate::query::QuerySpec,
    ) -> Result<Self, QueryError> {
        let fingerprint = world.entity_query_fingerprint(spec)?;
        Ok(Self::from_start(world, fingerprint))
    }

    /// Copies the cursor without advancing its observed tick.
    pub fn fork(&self) -> Self {
        Self {
            owner: self.owner.clone(),
            fingerprint: self.fingerprint,
            last_observed: self.last_observed,
        }
    }

    pub(crate) fn from_start(world: &crate::world::World, fingerprint: u64) -> Self {
        Self {
            owner: world.owner_token(),
            fingerprint,
            last_observed: ChangeTick::ZERO,
        }
    }

    pub(crate) fn from_now(
        world: &crate::world::World,
        fingerprint: u64,
    ) -> Result<Self, QueryError> {
        Ok(Self {
            owner: world.owner_token(),
            fingerprint,
            last_observed: world.change_tick(),
        })
    }

    pub(crate) fn validate(
        &self,
        world: &crate::world::World,
        fingerprint: u64,
    ) -> Result<(), QueryError> {
        if !self.owner.same(&world.owner_token()) {
            return Err(QueryError::WrongOwner);
        }
        if self.fingerprint != fingerprint {
            return Err(QueryError::WrongQuery {
                detail: alloc::string::String::from("cursor fingerprint does not match query spec"),
            });
        }
        Ok(())
    }

    /// Lower bound of the active half-open change window `(since, captured_now]`.
    pub fn since(&self) -> ChangeTick {
        self.last_observed
    }

    pub(crate) fn commit(&mut self, captured_now: ChangeTick) {
        self.last_observed = captured_now;
    }
}

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

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

    #[derive(Clone, Copy)]
    struct Velocity(#[allow(dead_code)] i32);

    #[test]
    fn query_cursor_commits_on_exhaustion() {
        let mut builder = WorldBuilder::new();
        builder
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("register");
        let mut world = builder.build().expect("build");
        let entity = world.spawn().expect("spawn");
        world.insert(entity, Position(1)).expect("insert");

        let spec = QuerySpec::new().added::<Position>();
        let mut cursor =
            QueryCursor::from_spec_start::<Position>(&mut world, &spec).expect("cursor");
        let params = QueryParams::new().cursor(&mut cursor);
        let mut query = world.query::<Position>(&spec, params).expect("query");
        assert!(query.next().is_some());
        assert!(query.next().is_none());
        drop(query);
        assert!(cursor.since().raw() > 0);
    }

    #[test]
    fn query_cursor_skips_commit_on_partial_iteration() {
        let mut builder = WorldBuilder::new();
        builder
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("register");
        let mut world = builder.build().expect("build");
        let a = world.spawn().expect("spawn");
        let b = world.spawn().expect("spawn");
        world.insert(a, Position(1)).expect("insert");
        world.insert(b, Position(2)).expect("insert");

        let spec = QuerySpec::new().added::<Position>();
        let mut cursor =
            QueryCursor::from_spec_start::<Position>(&mut world, &spec).expect("cursor");
        let before = cursor.since();
        let params = QueryParams::new().cursor(&mut cursor);
        let mut query = world.query::<Position>(&spec, params).expect("query");
        let _ = query.next();
        drop(query);
        assert_eq!(cursor.since(), before);
    }

    #[test]
    fn from_spec_now_and_from_now_capture_change_tick() {
        let mut builder = WorldBuilder::new();
        builder
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("register");
        let mut world = builder.build().expect("build");
        let entity = world.spawn().expect("spawn");
        world.insert(entity, Position(1)).expect("insert");
        let spec = QuerySpec::new();
        let fingerprint = world.query_fingerprint::<Position>(&spec).expect("fp");
        let cursor = QueryCursor::from_spec_now::<Position>(&mut world, &spec).expect("now");
        assert_eq!(cursor.since(), world.change_tick());
        let direct = QueryCursor::from_now(&world, fingerprint).expect("direct");
        assert_eq!(direct.since(), world.change_tick());
    }

    #[test]
    fn query2_now_and_fork_preserve_fingerprint_and_tick() {
        let mut builder = WorldBuilder::new();
        builder
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("position");
        builder
            .register_component::<Velocity>(ComponentOptions::sparse())
            .expect("velocity");
        let mut world = builder.build().expect("world");
        let entity = world.spawn().expect("spawn");
        world.insert(entity, Position(1)).expect("position");
        world.insert(entity, Velocity(2)).expect("velocity");
        let spec = QuerySpec::new().changed::<Position>();

        let cursor = QueryCursor::from_spec2_now::<Position, Velocity>(&mut world, &spec)
            .expect("query2 now");
        let fork = cursor.fork();
        assert_eq!(cursor.since(), world.change_tick());
        assert_eq!(fork.since(), cursor.since());

        let (plan, _, _) = world
            .resolve_query2_plan::<Position, Velocity>(&spec)
            .expect("plan");
        assert!(fork.validate(&world, plan.fingerprint).is_ok());
    }

    #[test]
    fn validate_rejects_foreign_owner() {
        let mut builder_a = WorldBuilder::new();
        builder_a
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("register");
        let mut world_a = builder_a.build().expect("a");
        let spec = QuerySpec::new();
        let mut cursor =
            QueryCursor::from_spec_start::<Position>(&mut world_a, &spec).expect("cursor");
        let mut builder_b = WorldBuilder::new();
        builder_b
            .register_component::<Position>(ComponentOptions::sparse())
            .expect("register");
        let mut world_b = builder_b.build().expect("b");
        let fingerprint = world_b.query_fingerprint::<Position>(&spec).expect("fp");
        assert!(matches!(
            cursor.validate(&world_b, fingerprint),
            Err(QueryError::WrongOwner)
        ));
        let _ = &mut cursor;
    }
}