syren 0.6.0

A parallel Rust framework for agent-based models with ECS storage, scheduling, messaging, environments, and optional GPU execution.
Documentation
use std::sync::{Arc, RwLock};

use syren::{
    advanced::EntityShards, AccessSets, Bundle, Command, ComponentID, ComponentRegistry,
    ECSManager, ECSReference, ECSResult, Entity, Read, Scheduler, System, Write,
};

#[derive(Clone, Copy, Debug, PartialEq)]
struct Position {
    x: i32,
}

#[derive(Clone, Copy, Debug, PartialEq)]
struct Velocity {
    dx: i32,
}

fn world_with_components() -> (ECSManager, ComponentID, ComponentID) {
    let registry = Arc::new(RwLock::new(ComponentRegistry::new()));
    let (position_id, velocity_id) = {
        let mut reg = registry.write().unwrap();
        let position_id = reg.register::<Position>().unwrap();
        let velocity_id = reg.register::<Velocity>().unwrap();
        reg.freeze();
        (position_id, velocity_id)
    };
    (
        ECSManager::with_registry(EntityShards::new(2).unwrap(), registry),
        position_id,
        velocity_id,
    )
}

fn spawn_position(world: &ECSManager, position_id: ComponentID, x: i32) -> Entity {
    let mut bundle = Bundle::new();
    bundle.insert(position_id, Position { x });
    world.world_ref().defer(Command::Spawn { bundle }).unwrap();
    world.apply_deferred_commands().unwrap().spawned[0].entity
}

fn spawn_position_velocity(
    world: &ECSManager,
    position_id: ComponentID,
    velocity_id: ComponentID,
    x: i32,
    dx: i32,
) -> Entity {
    let ecs = world.world_ref();
    let mut bundle = Bundle::new();
    bundle.insert(position_id, Position { x });
    bundle.insert(velocity_id, Velocity { dx });
    ecs.defer(Command::Spawn { bundle }).unwrap();
    world.apply_deferred_commands().unwrap().spawned[0].entity
}

#[test]
fn entity_aware_write_iteration_returns_matching_entities() {
    let (world, position_id, _) = world_with_components();
    let ecs = world.world_ref();
    let first = spawn_position(&world, position_id, 10);
    let second = spawn_position(&world, position_id, 20);

    let q = ecs
        .query()
        .unwrap()
        .write::<Position>()
        .unwrap()
        .build()
        .unwrap();
    let seen = parking_lot::Mutex::new(Vec::new());
    ecs.for_each_entity_w1::<Position>(q, |entity, position| {
        seen.lock().push((entity, position.x));
        position.x += 1;
    })
    .unwrap();

    let mut seen = seen.into_inner();
    seen.sort_by_key(|(_, x)| *x);
    assert_eq!(seen, vec![(first, 10), (second, 20)]);
    assert_eq!(
        ecs.read_entity_component::<Position>(first, position_id)
            .unwrap(),
        Position { x: 11 }
    );
    assert_eq!(
        ecs.read_entity_component::<Position>(second, position_id)
            .unwrap(),
        Position { x: 21 }
    );
}

#[test]
fn entity_aware_tuple_iteration_returns_matching_entities() {
    let (world, position_id, velocity_id) = world_with_components();
    let ecs = world.world_ref();
    let first = spawn_position_velocity(&world, position_id, velocity_id, 1, 4);
    let second = spawn_position_velocity(&world, position_id, velocity_id, 2, 8);

    let q = ecs
        .query()
        .unwrap()
        .read::<Velocity>()
        .unwrap()
        .write::<Position>()
        .unwrap()
        .build()
        .unwrap();
    let seen = Arc::new(parking_lot::Mutex::new(Vec::new()));
    let seen_capture = Arc::clone(&seen);
    ecs.for_each_entity::<(Read<Velocity>, Write<Position>), _>(
        q,
        move |(entity, velocity, position)| {
            seen_capture.lock().push((entity, velocity.dx, position.x));
            position.x += velocity.dx;
        },
    )
    .unwrap();

    let mut seen = seen.lock().clone();
    seen.sort_by_key(|(_, _, x)| *x);
    assert_eq!(seen, vec![(first, 4, 1), (second, 8, 2)]);
    assert_eq!(
        ecs.read_entity_component::<Position>(first, position_id)
            .unwrap(),
        Position { x: 5 }
    );
    assert_eq!(
        ecs.read_entity_component::<Position>(second, position_id)
            .unwrap(),
        Position { x: 10 }
    );
}

#[test]
fn run_context_is_visible_inside_scheduled_systems() {
    struct CaptureContext {
        access: AccessSets,
        out: Arc<parking_lot::Mutex<Option<(u64, u64, u16)>>>,
    }

    impl System for CaptureContext {
        fn id(&self) -> u16 {
            7
        }

        fn access(&self) -> &AccessSets {
            &self.access
        }

        fn run(&self, ecs: ECSReference<'_>) -> ECSResult<()> {
            let ctx = ecs.run_context();
            *self.out.lock() = Some((ctx.simulation_seed, ctx.tick, ctx.system_id));
            Ok(())
        }
    }

    let (world, _, _) = world_with_components();
    let out = Arc::new(parking_lot::Mutex::new(None));
    let mut scheduler = Scheduler::new();
    scheduler.add_system(CaptureContext {
        access: AccessSets::default(),
        out: Arc::clone(&out),
    });

    scheduler
        .run_with_context(world.world_ref(), 1234, 99)
        .unwrap();

    assert_eq!(*out.lock(), Some((1234, 99, 7)));
}

#[test]
fn fallible_entity_iteration_propagates_error() {
    let (world, position_id, _) = world_with_components();
    let ecs = world.world_ref();
    let _ = spawn_position(&world, position_id, 10);

    let q = ecs
        .query()
        .unwrap()
        .write::<Position>()
        .unwrap()
        .build()
        .unwrap();
    let err = ecs
        .for_each_entity_w1_fallible::<Position>(q, |_entity, _position| {
            Err(syren::ECSError::Execute(
                syren::ExecutionError::InternalExecutionError,
            ))
        })
        .unwrap_err();

    assert!(matches!(
        err,
        syren::ECSError::Execute(syren::ExecutionError::InternalExecutionError)
    ));
}