use std::fmt;
use crate::EcsError;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Entity {
index: u32,
generation: u32,
}
impl Entity {
#[must_use]
pub const fn index(self) -> u32 {
self.index
}
#[must_use]
pub const fn generation(self) -> u32 {
self.generation
}
#[must_use]
pub const fn to_bits(self) -> u64 {
(self.generation as u64).wrapping_shl(32) | self.index as u64
}
#[must_use]
pub const fn from_bits(bits: u64) -> Self {
Self {
index: bits as u32,
generation: bits.wrapping_shr(32) as u32,
}
}
}
impl fmt::Debug for Entity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Entity({self})")
}
}
impl fmt::Display for Entity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}v{}", self.index, self.generation)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Location {
pub(crate) archetype: usize,
pub(crate) row: usize,
}
#[derive(Debug)]
struct Meta {
generation: u32,
location: Option<Location>,
}
#[derive(Debug, Default)]
pub(crate) struct Entities {
meta: Vec<Meta>,
free: Vec<u32>,
len: usize,
}
impl Entities {
pub(crate) const fn len(&self) -> usize {
self.len
}
pub(crate) fn alloc(&mut self, location: Location) -> Result<Entity, EcsError> {
let index = match self.free.pop() {
Some(index) => index,
None => {
let slots = self.meta.len();
let index = u32::try_from(slots).map_err(|_| EcsError::CapacityExhausted)?;
let oom = |_| EcsError::OutOfMemory {
entities: slots.saturating_add(1),
};
self.meta.try_reserve(1).map_err(oom)?;
let free_room = slots.saturating_add(1).saturating_sub(self.free.len());
self.free.try_reserve(free_room).map_err(oom)?;
self.meta.push(Meta {
generation: 0,
location: None,
});
index
}
};
let meta = self.meta_mut(index).ok_or(EcsError::CapacityExhausted)?;
meta.location = Some(location);
let generation = meta.generation;
self.len = self.len.saturating_add(1);
Ok(Entity { index, generation })
}
pub(crate) fn location(&self, entity: Entity) -> Option<Location> {
self.meta
.get(usize::try_from(entity.index).ok()?)
.filter(|m| m.generation == entity.generation)
.and_then(|m| m.location)
}
pub(crate) fn relocate(&mut self, entity: Entity, location: Location) -> bool {
match self
.meta_mut(entity.index)
.filter(|m| m.generation == entity.generation)
.and_then(|m| m.location.as_mut())
{
Some(slot) => {
*slot = location;
true
}
None => false,
}
}
pub(crate) fn free(&mut self, entity: Entity) -> Option<Location> {
let meta = self
.meta_mut(entity.index)
.filter(|m| m.generation == entity.generation)?;
let location = meta.location.take()?;
match meta.generation.checked_add(1) {
Some(next) => {
meta.generation = next;
self.free.push(entity.index);
}
None => meta.generation = u32::MAX,
}
self.len = self.len.saturating_sub(1);
Some(location)
}
fn meta_mut(&mut self, index: u32) -> Option<&mut Meta> {
self.meta.get_mut(usize::try_from(index).ok()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
const AT: Location = Location {
archetype: 0,
row: 0,
};
#[test]
fn bits_round_trip() {
let e = Entity {
index: 7,
generation: u32::MAX,
};
assert_eq!(e.to_bits(), 0xFFFF_FFFF_0000_0007);
assert_eq!(Entity::from_bits(e.to_bits()), e);
assert_eq!(format!("{e:?}"), "Entity(7v4294967295)");
}
#[test]
fn reuse_bumps_generation() {
let mut entities = Entities::default();
let a = entities.alloc(AT).unwrap();
assert_eq!(entities.free(a), Some(AT));
let b = entities.alloc(AT).unwrap();
assert_eq!((a.index(), a.generation()), (b.index(), 0));
assert_eq!(b.generation(), 1);
assert_eq!(entities.location(a), None);
assert_eq!(entities.free(a), None);
assert!(!entities.relocate(a, AT));
assert_eq!(entities.len(), 1);
}
#[test]
fn exhausted_generation_retires_the_slot() {
let mut entities = Entities::default();
let a = entities.alloc(AT).unwrap();
entities.meta[0].generation = u32::MAX;
let last = Entity {
index: a.index(),
generation: u32::MAX,
};
assert_eq!(entities.free(last), Some(AT));
let b = entities.alloc(AT).unwrap();
assert_ne!(b.index(), a.index());
assert_eq!(entities.location(last), None);
assert_eq!(entities.free(last), None);
assert_eq!(entities.len(), 1);
}
}