temper 0.2.0

A simulation-first Rust game engine
Documentation
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Entity {
    index: u32,
    generation: u32,
}

impl Entity {
    #[must_use]
    pub(crate) fn new(index: u32, generation: u32) -> Self {
        Self { index, generation }
    }

    #[must_use]
    pub fn index(&self) -> u32 {
        self.index
    }

    #[must_use]
    pub fn generation(&self) -> u32 {
        self.generation
    }
}

#[derive(Debug)]
struct EntitySlot {
    generation: u32,
    alive: bool,
}

#[derive(Debug, Default)]
pub struct EntityAllocator {
    slots: Vec<EntitySlot>,
    free: Vec<u32>,
}

impl EntityAllocator {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    pub fn spawn(&mut self) -> Entity {
        if let Some(index) = self.free.pop() {
            let slot = &mut self.slots[index as usize];
            slot.alive = true;
            Entity::new(index, slot.generation)
        } else {
            let index = self.slots.len() as u32;
            self.slots.push(EntitySlot {
                generation: 0,
                alive: true,
            });
            Entity::new(index, 0)
        }
    }

    pub fn despawn(&mut self, entity: Entity) -> bool {
        let Some(slot) = self.slots.get_mut(entity.index as usize) else {
            return false;
        };

        if !slot.alive || slot.generation != entity.generation {
            return false;
        };

        slot.alive = false;
        slot.generation = slot.generation.wrapping_add(1);
        self.free.push(entity.index);
        true
    }

    pub fn is_alive(&self, entity: Entity) -> bool {
        self.slots
            .get(entity.index as usize)
            .is_some_and(|slot| slot.alive && slot.generation == entity.generation)
    }

    pub fn alive_count(&self) -> usize {
        self.slots.len() - self.free.len()
    }
}

#[cfg(test)]
pub mod tests {
    use super::*;

    #[test]
    fn spawned_entity_is_alive() {
        let mut entities = EntityAllocator::new();
        let entity = entities.spawn();
        assert!(entities.is_alive(entity));
    }

    #[test]
    fn despawn_entity_is_not_alive() {
        let mut entities = EntityAllocator::new();
        let entity = entities.spawn();
        assert!(entities.despawn(entity));
        assert!(!entities.is_alive(entity));
    }

    #[test]
    fn despawn_rejects_stale_entity() {
        let mut entities = EntityAllocator::new();
        let entity = entities.spawn();
        entities.despawn(entity);
        assert!(!entities.despawn(entity));
    }

    #[test]
    fn allocator_reuses_index_with_new_generation() {
        let mut entities = EntityAllocator::new();
        let first = entities.spawn();
        assert!(entities.despawn(first));
        let second = entities.spawn();
        assert_ne!(first.generation(), second.generation());
        assert_eq!(first.index(), second.index());
    }

    #[test]
    fn despawn_rejects_unknown_entity() {
        let mut entities = EntityAllocator::new();
        assert_eq!(
            entities.despawn(Entity {
                index: 0,
                generation: 0
            }),
            false
        );
    }

    #[test]
    fn reused_slot_invalidates_old_entity() {
        let mut entities = EntityAllocator::new();

        let first = entities.spawn();
        assert!(entities.despawn(first));

        let second = entities.spawn();

        assert_eq!(first.index(), second.index());
        assert_ne!(first.generation(), second.generation());
        assert!(!entities.is_alive(first));
        assert!(entities.is_alive(second));
    }
}