summaryrefslogtreecommitdiff
path: root/engine-ecs/src/lib.rs
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2026-05-21 17:55:20 +0200
committerHampusM <hampus@hampusmat.com>2026-05-21 17:55:20 +0200
commit8022e8998290b067b8aa0cb9cba8ba410826bdab (patch)
tree7171e79ce530e03079046ee8fd12167160c45480 /engine-ecs/src/lib.rs
parent412cee02c252f91bcf0b70a3f5cc5fca6d2b4c62 (diff)
chore: rename ecs* crates to engine-ecs*HEADmaster
Diffstat (limited to 'engine-ecs/src/lib.rs')
-rw-r--r--engine-ecs/src/lib.rs773
1 files changed, 773 insertions, 0 deletions
diff --git a/engine-ecs/src/lib.rs b/engine-ecs/src/lib.rs
new file mode 100644
index 0000000..6450587
--- /dev/null
+++ b/engine-ecs/src/lib.rs
@@ -0,0 +1,773 @@
+#![deny(clippy::all, clippy::pedantic)]
+
+use std::any::{Any, TypeId, type_name};
+use std::fmt::Debug;
+use std::hint::cold_path;
+use std::mem::ManuallyDrop;
+use std::rc::Rc;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use hashbrown::HashMap;
+
+use crate::actions::Action;
+use crate::component::storage::archetype::EntityComponent as ArchetypeEntityComponent;
+use crate::component::storage::{EntityAlreadyExistsError, Storage as ComponentStorage};
+use crate::component::{
+ Component,
+ IntoParts as IntoComponentParts,
+ Parts as ComponentParts,
+ Sequence as ComponentSequence,
+};
+use crate::entity::{Declaration as EntityDeclaration, Handle as EntityHandle};
+use crate::error::{
+ ErrorHandler,
+ Metadata as ErrorMetadata,
+ SourceKind as ErrorSourceKind,
+ err_handler_panic,
+};
+use crate::event::component::Added;
+use crate::event::{Emitted as EmittedEvent, NewEvents, Submitter as EventSubmitter};
+use crate::extension::{Collector as ExtensionCollector, Extension};
+use crate::lock::Lock;
+use crate::pair::{ChildOf, Pair, Wildcard};
+use crate::phase::{
+ HasSystem as PhaseHasSystem,
+ POST_UPDATE as POST_UPDATE_PHASE,
+ PRE_UPDATE as PRE_UPDATE_PHASE,
+ Phase,
+ START as START_PHASE,
+ UPDATE as UPDATE_PHASE,
+};
+use crate::query::flexible::Query as FlexibleQuery;
+use crate::query::{
+ MAX_TERM_CNT as QUERY_MAX_TERM_CNT,
+ TermWithFieldTuple as QueryTermWithFieldTuple,
+ TermWithoutFieldTuple as QueryTermWithoutFieldTuple,
+ Terms as QueryTerms,
+ TermsBuilderInterface,
+};
+use crate::sole::{Single, Sole};
+use crate::stats::Stats;
+use crate::system::observer::{Observer, WrapperComponent as ObserverWrapperComponent};
+use crate::system::{Callbacks, Metadata as SystemMetadata, System, SystemComponent};
+use crate::uid::{Kind as UidKind, Uid};
+
+pub mod actions;
+pub mod component;
+pub mod entity;
+pub mod error;
+pub mod event;
+pub mod extension;
+pub mod pair;
+pub mod phase;
+pub mod query;
+pub mod sole;
+pub mod stats;
+pub mod system;
+pub mod tuple;
+pub mod uid;
+pub mod util;
+
+mod lock;
+
+pub use engine_ecs_macros::{Component, Sole};
+
+pub use crate::query::Query;
+
+#[derive(Debug)]
+pub struct World
+{
+ data: WorldData,
+ stop: AtomicBool,
+ is_first_tick: AtomicBool,
+ error_handler: ErrorHandler,
+}
+
+impl World
+{
+ #[must_use]
+ pub fn new() -> Self
+ {
+ let mut world = Self {
+ data: WorldData::default(),
+ stop: AtomicBool::new(false),
+ is_first_tick: AtomicBool::new(false),
+ error_handler: err_handler_panic,
+ };
+
+ crate::phase::spawn_entities(&mut world);
+
+ world.add_sole(Stats::default()).ok();
+
+ world
+ }
+
+ pub fn set_err_handler(&mut self, err_handler: ErrorHandler)
+ {
+ self.error_handler = err_handler;
+ }
+
+ /// Creates a entity with the given components. A new unique [`Uid`] will be generated
+ /// for this entity.
+ pub fn create_entity<Comps>(&mut self, components: Comps) -> Uid
+ where
+ Comps: ComponentSequence,
+ {
+ let entity_uid = Uid::new_unique(UidKind::Entity);
+
+ self.create_entity_with_uid(entity_uid, components);
+
+ entity_uid
+ }
+
+ /// Creates a entity with the given components. The entity will have the specified
+ /// [`Uid`].
+ #[tracing::instrument(skip_all)]
+ pub fn create_entity_with_uid<Comps>(&mut self, entity_uid: Uid, components: Comps)
+ where
+ Comps: ComponentSequence,
+ {
+ self.create_ent(entity_uid, components.into_parts_array());
+ }
+
+ pub fn add_component(&mut self, entity_id: Uid, component_parts: ComponentParts)
+ {
+ Self::add_entity_components(
+ entity_id,
+ [component_parts],
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
+ }
+
+ pub fn create_declared_entity(&mut self, entity_decl: &EntityDeclaration)
+ {
+ entity_decl.create(self);
+ }
+
+ /// Adds a globally shared singleton value.
+ ///
+ /// # Errors
+ /// Returns `Err` if this [`Sole`] has already been added.
+ pub fn add_sole<SoleT>(&mut self, sole: SoleT) -> Result<(), SoleAlreadyExistsError>
+ where
+ SoleT: Sole,
+ {
+ self.data.sole_storage.insert(sole)
+ }
+
+ pub fn register_observer<'this, SystemImpl, ObserverT>(
+ &'this mut self,
+ observer: ObserverT,
+ ) where
+ ObserverT: Observer<'this, SystemImpl>,
+ {
+ let (wrapper_comp, mut system_callbacks) = observer.finish_observer();
+
+ let ent_id = Uid::new_unique(UidKind::Entity);
+
+ self.create_ent(
+ ent_id,
+ [wrapper_comp.into_parts()].into_iter().chain(
+ ObserverT::observed_events()
+ .into_iter()
+ .map(IntoComponentParts::into_parts),
+ ),
+ );
+
+ system_callbacks.on_created(self, SystemMetadata { ent_id });
+ }
+
+ pub fn register_system<'this, SystemImpl>(
+ &'this mut self,
+ phase_euid: Uid,
+ system: impl System<'this, SystemImpl>,
+ )
+ {
+ let (type_erased_system, mut system_callbacks) = system.finish();
+
+ let system_ent_id =
+ self.create_entity((SystemComponent { system: type_erased_system },));
+
+ system_callbacks.on_created(self, SystemMetadata { ent_id: system_ent_id });
+
+ self.create_entity_with_uid(
+ phase_euid,
+ (Pair::builder()
+ .relation::<PhaseHasSystem>()
+ .target_id(system_ent_id)
+ .build(),),
+ );
+ }
+
+ /// Adds a extensions.
+ pub fn add_extension(&mut self, extension: impl Extension)
+ {
+ let extension_collector = ExtensionCollector::new(self);
+
+ extension.collect(extension_collector);
+ }
+
+ pub fn query<FieldTerms, FieldlessTerms>(
+ &self,
+ ) -> Query<'_, FieldTerms, FieldlessTerms>
+ where
+ FieldTerms: QueryTermWithFieldTuple,
+ FieldlessTerms: QueryTermWithoutFieldTuple,
+ {
+ Query::new(self)
+ }
+
+ pub fn flexible_query<const MAX_TERM_CNT: usize>(
+ &self,
+ terms: QueryTerms<MAX_TERM_CNT>,
+ ) -> FlexibleQuery<'_, MAX_TERM_CNT>
+ {
+ FlexibleQuery::new(self, terms)
+ }
+
+ pub fn get_entity(&self, entity_id: Uid) -> Option<EntityHandle<'_>>
+ {
+ let archetype = self
+ .data
+ .component_storage
+ .get_entity_archetype(entity_id)?;
+
+ let Some(entity) = archetype.get_entity_by_id(entity_id) else {
+ unreachable!("Should exist since archetype was found by entity id");
+ };
+
+ Some(EntityHandle::new(archetype, entity, self))
+ }
+
+ pub fn get_sole<SoleT: Sole>(&self) -> Option<Single<'_, SoleT>>
+ {
+ Some(Single::new(self.data.sole_storage.get::<SoleT>()?))
+ }
+
+ pub fn event_submitter(&self) -> EventSubmitter<'_>
+ {
+ EventSubmitter::new(&self.data.new_events)
+ }
+
+ /// Performs a single tick.
+ /// # Panics
+ /// Will panic if mutable internal lock cannot be acquired.
+ pub fn step(&mut self) -> StepResult
+ {
+ if self.stop.load(Ordering::Relaxed) {
+ return StepResult::Stop;
+ }
+
+ if self
+ .is_first_tick
+ .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
+ .is_ok()
+ {
+ let Some(start_phase_entity) = self.get_entity(*START_PHASE) else {
+ unreachable!();
+ };
+
+ self.run_phase_systems(&start_phase_entity);
+ }
+
+ self.perform_phases();
+
+ self.emit_new_events();
+
+ self.data.component_storage.create_imaginary_archetypes();
+
+ self.perform_queued_actions();
+
+ if self.stop.load(Ordering::Relaxed) {
+ return StepResult::Stop;
+ }
+
+ let Some(mut stats) = self.get_sole::<Stats>() else {
+ unreachable!(); // Reason: is added in World::new
+ };
+
+ stats.current_tick += 1;
+
+ StepResult::Continue
+ }
+
+ /// Starts a loop which calls [`Self::step`] until the world is stopped.
+ pub fn start_loop(&mut self)
+ {
+ while let StepResult::Continue = self.step() {}
+ }
+
+ #[cfg(feature = "vizoxide")]
+ pub fn create_vizoxide_archetype_graph(
+ &self,
+ name: impl AsRef<str>,
+ ) -> Result<vizoxide::Graph, vizoxide::GraphvizError>
+ {
+ use std::borrow::Cow;
+
+ use crate::component::storage::{
+ VizoxideArchetypeGraphEdgeKind,
+ VizoxideArchetypeGraphParams,
+ };
+
+ self.data.component_storage.create_vizoxide_archetype_graph(
+ name,
+ VizoxideArchetypeGraphParams {
+ create_node_name: |archetype, _| {
+ Cow::Owned(format!(
+ "[{}]",
+ archetype
+ .component_ids_sorted()
+ .into_iter()
+ .map(|comp_id| comp_id.to_string())
+ .collect::<Vec<_>>()
+ .join(", ")
+ ))
+ },
+ create_node_cb: |_archetype, archetype_metadata, node_builder| {
+ if archetype_metadata.is_imaginary {
+ return node_builder.attribute("shape", "ellipse");
+ }
+
+ node_builder.attribute("shape", "box")
+ },
+ create_edge_cb: |_, _, edge_kind, edge_builder| {
+ edge_builder.attribute(
+ "color",
+ match edge_kind {
+ VizoxideArchetypeGraphEdgeKind::Add => "green",
+ VizoxideArchetypeGraphEdgeKind::Remove => "red",
+ },
+ )
+ },
+ },
+ )
+ }
+
+ #[tracing::instrument(skip_all)]
+ fn create_ent(
+ &mut self,
+ entity_uid: Uid,
+ components: impl IntoIterator<Item = ComponentParts>,
+ )
+ {
+ debug_assert_eq!(entity_uid.kind(), UidKind::Entity);
+
+ if let Err(EntityAlreadyExistsError) =
+ self.data.component_storage.create_entity(entity_uid)
+ {
+ // This is fine
+ }
+
+ Self::add_entity_components(
+ entity_uid,
+ components,
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
+ }
+
+ fn run_phase_systems(&self, phase_entity: &EntityHandle<'_>)
+ {
+ // The phase's systems are retrieved this way so that the order they are
+ // run is the same order as they were registered, even if they have local
+ // components.
+ for system_entity in phase_entity
+ .get_wildcard_pair_matches::<PhaseHasSystem, Wildcard>()
+ .into_iter()
+ .filter_map(|phase_has_system| phase_has_system.get_target_ent())
+ {
+ let Some(system) = system_entity.get::<SystemComponent>() else {
+ cold_path();
+ continue;
+ };
+
+ // SAFETY: The world lives long enough
+ if let Err(err) = unsafe {
+ system
+ .system
+ .run(self, SystemMetadata { ent_id: system_entity.uid() })
+ } {
+ cold_path();
+
+ (self.error_handler)(
+ err,
+ ErrorMetadata {
+ source_name: system.system.name(),
+ source_kind: ErrorSourceKind::System,
+ },
+ )
+ }
+ }
+ }
+
+ fn perform_child_phases(&self, parent_phase_euid: Uid)
+ {
+ let phase_query = self.flexible_query(
+ QueryTerms::<2>::builder()
+ .with_required([
+ Phase::id(),
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(parent_phase_euid)
+ .build()
+ .id(),
+ ])
+ .build(),
+ );
+
+ for child_phase_entity in &phase_query {
+ self.run_phase_systems(&child_phase_entity);
+ self.perform_child_phases(child_phase_entity.uid());
+ }
+ }
+
+ fn perform_single_phase(&self, phase_entity_id: Uid)
+ {
+ let Some(phase_entity) = self.get_entity(phase_entity_id) else {
+ unreachable!();
+ };
+
+ self.run_phase_systems(&phase_entity);
+ self.perform_child_phases(phase_entity_id);
+ }
+
+ fn perform_phases(&self)
+ {
+ self.perform_single_phase(*PRE_UPDATE_PHASE);
+ self.perform_single_phase(*UPDATE_PHASE);
+ self.perform_single_phase(*POST_UPDATE_PHASE);
+ }
+
+ fn emit_new_events(&self)
+ {
+ loop {
+ let new_events = {
+ let mut new_events_lock = self
+ .data
+ .new_events
+ .write_nonblock()
+ .expect("Failed to acquire read-write lock to new events");
+
+ if new_events_lock.is_empty() {
+ break;
+ }
+
+ new_events_lock.take()
+ };
+
+ for (event_id, event_matches) in new_events {
+ self.emit_event_observers(
+ event_id,
+ &EmittedEvent {
+ event: event_id,
+ match_ids: &event_matches.match_ids,
+ },
+ );
+ }
+ }
+ }
+
+ #[tracing::instrument(skip_all)]
+ fn perform_queued_actions(&mut self)
+ {
+ let mut action_queue_lock = self
+ .data
+ .action_queue
+ .queue
+ .write_nonblock()
+ .unwrap_or_else(|err| {
+ panic!("Failed to take read-write action queue lock: {err}",);
+ });
+
+ for action in action_queue_lock.drain(..) {
+ match action {
+ Action::Spawn(new_entity_uid, components) => {
+ if let Err(err) =
+ self.data.component_storage.create_entity(new_entity_uid)
+ {
+ tracing::warn!("Failed to create entity: {err}");
+ continue;
+ }
+
+ Self::add_entity_components(
+ new_entity_uid,
+ components,
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
+ }
+ Action::Despawn(entity_uid) => {
+ if let Err(err) =
+ self.data.component_storage.remove_entity(entity_uid)
+ {
+ tracing::error!("Failed to despawn entity: {err}");
+ }
+ }
+ Action::AddComponents(entity_uid, components) => {
+ Self::add_entity_components(
+ entity_uid,
+ components,
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
+ }
+ Action::RemoveComponents(entity_uid, component_ids) => {
+ Self::remove_entity_components(
+ entity_uid,
+ component_ids,
+ &mut self.data.component_storage,
+ );
+ }
+ Action::Stop => {
+ self.stop.store(true, Ordering::Relaxed);
+ }
+ }
+ }
+ }
+
+ fn add_entity_components(
+ entity_uid: Uid,
+ components: impl IntoIterator<Item = ComponentParts>,
+ component_storage: &mut ComponentStorage,
+ event_submitter: &EventSubmitter<'_>,
+ )
+ {
+ let component_iter = components.into_iter();
+
+ for component_parts in component_iter {
+ let comp_id = component_parts.id();
+
+ let comp_name = component_parts.name();
+
+ if let Err(err) = component_storage.add_entity_component(
+ entity_uid,
+ (comp_id, comp_name, component_parts.into_data()),
+ ) {
+ tracing::error!("Failed to add component {comp_name} to entity: {err}");
+ continue;
+ }
+
+ if comp_id.kind() == UidKind::Pair {
+ continue;
+ }
+
+ event_submitter.submit_event(
+ &Pair::builder()
+ .relation::<Added>()
+ .target_id(comp_id)
+ .build(),
+ entity_uid,
+ );
+ }
+ }
+
+ fn remove_entity_components(
+ entity_uid: Uid,
+ component_ids: impl IntoIterator<Item = Uid>,
+ component_storage: &mut ComponentStorage,
+ )
+ {
+ let component_id_iter = component_ids.into_iter();
+
+ for component_id in component_id_iter {
+ if let Err(err) =
+ component_storage.remove_entity_component(entity_uid, component_id)
+ {
+ tracing::error!("Failed to remove component to entity: {err}");
+ }
+ }
+ }
+
+ fn emit_event_observers(&self, event_id: Uid, emitted_event: &EmittedEvent<'_>)
+ {
+ assert_eq!(event_id.kind(), UidKind::Pair);
+
+ let query = Query::<(&ObserverWrapperComponent,)>::from_flexible_query(
+ self.flexible_query(
+ QueryTerms::<QUERY_MAX_TERM_CNT>::builder()
+ .with_required([ObserverWrapperComponent::id(), event_id])
+ .build(),
+ ),
+ );
+
+ for (observer_ent_id, (observer,)) in query.iter_with_euids() {
+ if let Err(err) = unsafe {
+ observer.run(
+ self,
+ SystemMetadata { ent_id: observer_ent_id },
+ emitted_event.clone(),
+ )
+ } {
+ cold_path();
+
+ (self.error_handler)(
+ err,
+ ErrorMetadata {
+ source_name: observer.name(),
+ source_kind: ErrorSourceKind::Observer,
+ },
+ )
+ }
+ }
+ }
+}
+
+impl Default for World
+{
+ fn default() -> Self
+ {
+ Self::new()
+ }
+}
+
+/// The result of calling [`World::step`].
+pub enum StepResult
+{
+ /// Another step can be made.
+ Continue,
+
+ /// The world have been stopped so no step can be made again.
+ Stop,
+}
+
+#[derive(Debug, Default)]
+struct WorldData
+{
+ component_storage: ComponentStorage,
+ sole_storage: SoleStorage,
+ action_queue: Rc<ActionQueue>,
+ new_events: Lock<NewEvents>,
+}
+
+#[derive(Debug, Clone)]
+pub struct EntityComponentRef<'a>
+{
+ component_id: Uid,
+ component: &'a ArchetypeEntityComponent,
+ entity_id: Uid,
+}
+
+impl<'a> EntityComponentRef<'a>
+{
+ fn component(&self) -> &'a Lock<Box<dyn Any>>
+ {
+ self.component.component()
+ }
+
+ #[must_use]
+ pub fn id(&self) -> Uid
+ {
+ self.component_id
+ }
+
+ #[must_use]
+ pub fn entity_id(&self) -> Uid
+ {
+ self.entity_id
+ }
+
+ fn new(component_id: Uid, comp: &'a ArchetypeEntityComponent, entity_id: Uid)
+ -> Self
+ {
+ Self {
+ component_id,
+ component: comp,
+ entity_id,
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+struct ActionQueue
+{
+ queue: Lock<Vec<Action>>,
+}
+
+impl ActionQueue
+{
+ fn push(&self, action: Action)
+ {
+ self.queue
+ .write_nonblock()
+ .expect("Failed to aquire read-write lock to action queue")
+ .push(action);
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Sole {0} already exists")]
+pub struct SoleAlreadyExistsError(pub &'static str);
+
+#[derive(Debug)]
+struct StoredSole
+{
+ sole: Arc<Lock<Box<dyn Sole>>>,
+ drop_last: bool,
+}
+
+#[derive(Debug, Default)]
+struct SoleStorage
+{
+ storage: HashMap<TypeId, ManuallyDrop<StoredSole>>,
+}
+
+impl SoleStorage
+{
+ fn get<SoleT: Sole>(&self) -> Option<&Arc<Lock<Box<dyn Sole>>>>
+ {
+ self.storage
+ .get(&TypeId::of::<SoleT>())
+ .map(|sole| &sole.sole)
+ }
+
+ fn insert<SoleT: Sole>(&mut self, sole: SoleT) -> Result<(), SoleAlreadyExistsError>
+ {
+ let sole_type_id = TypeId::of::<SoleT>();
+
+ if self.storage.contains_key(&sole_type_id) {
+ return Err(SoleAlreadyExistsError(type_name::<SoleT>()));
+ }
+
+ let drop_last = sole.drop_last();
+
+ // TODO: Reconsider this maybe?
+ #[allow(clippy::arc_with_non_send_sync)]
+ self.storage.insert(
+ sole_type_id,
+ ManuallyDrop::new(StoredSole {
+ sole: Arc::new(Lock::new(Box::new(sole), type_name::<SoleT>())),
+ drop_last,
+ }),
+ );
+
+ Ok(())
+ }
+}
+
+impl Drop for SoleStorage
+{
+ fn drop(&mut self)
+ {
+ let mut soles_to_drop_last = Vec::new();
+
+ for sole in self.storage.values_mut() {
+ if sole.drop_last {
+ soles_to_drop_last.push(sole);
+ continue;
+ }
+
+ unsafe {
+ ManuallyDrop::drop(sole);
+ }
+ }
+
+ for sole in &mut soles_to_drop_last {
+ unsafe {
+ ManuallyDrop::drop(sole);
+ }
+ }
+ }
+}