use crate::engine::error::CapacityError;
use crate::engine::types::{EntityCount, EntityID, IndexID, ShardID, VersionID, INDEX_CAP};
use super::entity::Entity;
use super::entity::{make_entity, split_entity};
use super::location::EntityLocation;
#[derive(Default)]
pub struct Entities {
versions: Vec<VersionID>,
pub(super) free_store: Vec<IndexID>,
alive: Vec<bool>,
locations: Vec<EntityLocation>,
}
impl Entities {
fn ensure_capacity(&mut self, additional_entities: EntityCount) -> Result<(), CapacityError> {
if additional_entities == 0 {
return Ok(());
}
let current_entity_count = self.versions.len() as EntityID;
let entities_needed = current_entity_count + (additional_entities as EntityID);
let capacity = INDEX_CAP as EntityID + 1;
if entities_needed > capacity {
return Err(CapacityError {
entities_needed,
capacity,
});
}
self.versions.resize(entities_needed as usize, 0);
self.alive.resize(entities_needed as usize, false);
self.locations
.resize(entities_needed as usize, EntityLocation::default());
for index in current_entity_count..entities_needed {
self.free_store.push(index as IndexID);
}
Ok(())
}
pub(crate) fn spawn(
&mut self,
shard_id: ShardID,
location: EntityLocation,
) -> Result<Entity, CapacityError> {
let index = if let Some(i) = self.free_store.pop() {
i
} else {
let growth = self.versions.len().max(1024);
self.ensure_capacity(growth as EntityCount)?;
match self.free_store.pop() {
Some(i) => i,
None => {
let entities_needed = (self.versions.len() as u64).saturating_add(1);
let capacity = (INDEX_CAP as u64).saturating_add(1);
return Err(CapacityError {
entities_needed,
capacity,
});
}
}
};
let version = self.versions[index as usize];
self.alive[index as usize] = true;
self.locations[index as usize] = location;
Ok(make_entity(shard_id, index, version))
}
pub(crate) fn spawn_many(
&mut self,
shard_id: ShardID,
count: usize,
mut location_for: impl FnMut(usize) -> EntityLocation,
out: &mut Vec<Entity>,
) -> Result<(), CapacityError> {
let free = self.free_store.len();
if free < count {
let deficit = count - free;
let capacity = INDEX_CAP as usize + 1;
let available = capacity.saturating_sub(self.versions.len());
if available < deficit {
return Err(CapacityError {
entities_needed: (self.versions.len() as EntityID) + (deficit as EntityID),
capacity: capacity as EntityID,
});
}
let growth = deficit.max(self.versions.len()).max(1024).min(available);
self.ensure_capacity(growth as EntityCount)?;
}
debug_assert!(self.free_store.len() >= count);
out.reserve(count);
for k in 0..count {
let Some(index) = self.free_store.pop() else {
return Err(CapacityError {
entities_needed: (self.versions.len() as EntityID) + 1,
capacity: INDEX_CAP as EntityID + 1,
});
};
let version = self.versions[index as usize];
self.alive[index as usize] = true;
self.locations[index as usize] = location_for(k);
out.push(make_entity(shard_id, index, version));
}
Ok(())
}
pub(crate) fn despawn(&mut self, entity: Entity) -> bool {
let (_, i, v) = split_entity(entity);
let index = i as usize;
match self.versions.get_mut(index) {
Some(live) if *live == v && self.alive.get(index).copied().unwrap_or(false) => {
*live = live.wrapping_add(1);
if *live == VersionID::MAX {
*live = 0;
}
self.alive[index] = false;
self.locations[index] = EntityLocation::default();
self.free_store.push(i);
true
}
_ => false,
}
}
pub fn is_alive(&self, entity: Entity) -> bool {
let (_, i, v) = split_entity(entity);
let index = i as usize;
index < self.versions.len()
&& self.alive.get(index).copied().unwrap_or(false)
&& self.versions[index] == v
}
pub fn get_location(&self, entity: Entity) -> Option<EntityLocation> {
let (_, i, _) = split_entity(entity);
if self.is_alive(entity) {
Some(self.locations[i as usize])
} else {
None
}
}
#[cfg(test)]
pub(crate) fn doctor_version_for_test(&mut self, index: usize, version: VersionID) {
self.versions[index] = version;
}
#[cfg(test)]
pub(crate) fn version_for_test(&self, index: usize) -> VersionID {
self.versions[index]
}
pub(crate) fn set_location(&mut self, entity: Entity, location: EntityLocation) {
let (_, i, _) = split_entity(entity);
let index = i as usize;
debug_assert!(
self.is_alive(entity),
"set_location was called on a dead or stale entity. Entity: {:?}, Location: {:?}",
entity,
location
);
if index < self.locations.len() {
self.locations[index] = location;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn despawn_version_increment_skips_max() {
let mut pool = Entities::default();
let first = pool.spawn(0, EntityLocation::default()).unwrap();
let index = first.index() as usize;
pool.doctor_version_for_test(index, VersionID::MAX - 1);
let handle = make_entity(0, first.index(), VersionID::MAX - 1);
assert!(pool.despawn(handle));
assert_eq!(
pool.version_for_test(index),
0,
"version increment must skip VersionID::MAX"
);
}
}