Skip to main content

basalt_api/
system.rs

1//! System registration types for the plugin API.
2//!
3//! These types allow plugins to register tick-based systems without
4//! depending on the ECS storage engine. The server provides a
5//! [`SystemContext`] implementation that wraps the real ECS.
6
7use std::any::{Any, TypeId};
8use std::collections::HashSet;
9use std::time::Duration;
10
11pub use crate::budget::TickBudget;
12pub use crate::components::{EntityId, Phase};
13
14/// Abstract interface for system runners.
15///
16/// Extends [`WorldHandle`](crate::world::handle::WorldHandle) with
17/// ECS storage methods (spawn, despawn, component access). System
18/// closures receive this trait object instead of a raw `&mut Ecs`,
19/// keeping the ECS as an implementation detail.
20///
21/// Pure world ops (`get_block`, `set_block`, `check_overlap`, ...)
22/// come from `WorldHandle` and are not redeclared here. ECS methods
23/// use `TypeId` + `dyn Any` internally; typed access is provided via
24/// [`SystemContextExt`].
25pub trait SystemContext: crate::world::handle::WorldHandle {
26    /// Spawns a new entity and returns its unique ID.
27    fn spawn(&mut self) -> EntityId;
28
29    /// Removes an entity and all its components.
30    fn despawn(&mut self, entity: EntityId);
31
32    /// Sets a component value for an entity (type-erased).
33    fn set_component(
34        &mut self,
35        entity: EntityId,
36        type_id: TypeId,
37        component: Box<dyn Any + Send + Sync>,
38    );
39
40    /// Returns all entity IDs that have a component of the given type.
41    fn entities_with(&self, type_id: TypeId) -> Vec<EntityId>;
42
43    /// Returns a reference to a component as `dyn Any`.
44    fn get_component(&self, entity: EntityId, type_id: TypeId) -> Option<&dyn Any>;
45
46    /// Returns a mutable reference to a component as `dyn Any`.
47    fn get_component_mut(&mut self, entity: EntityId, type_id: TypeId) -> Option<&mut dyn Any>;
48
49    /// Returns the CPU budget for the current system invocation.
50    ///
51    /// Budget-aware systems call this to check remaining time and yield
52    /// early when expired. Systems that ignore the budget run to completion.
53    fn budget(&self) -> &TickBudget;
54}
55
56/// Typed convenience methods on [`SystemContext`].
57///
58/// Implemented for `dyn SystemContext` so callers can write
59/// `ctx.get::<Position>(id)` instead of raw `get_component` + downcast.
60pub trait SystemContextExt {
61    /// Returns a typed component reference.
62    fn get<T: crate::components::Component>(&self, entity: EntityId) -> Option<&T>;
63
64    /// Returns a typed mutable component reference.
65    fn get_mut<T: crate::components::Component>(&mut self, entity: EntityId) -> Option<&mut T>;
66
67    /// Sets a typed component value for an entity.
68    fn set<T: crate::components::Component>(&mut self, entity: EntityId, component: T);
69
70    /// Returns all entity IDs that have a component of type `T`.
71    fn query<T: crate::components::Component>(&self) -> Vec<EntityId>;
72}
73
74impl<S: SystemContext + ?Sized> SystemContextExt for S {
75    fn get<T: crate::components::Component>(&self, entity: EntityId) -> Option<&T> {
76        self.get_component(entity, TypeId::of::<T>())
77            .and_then(|any| any.downcast_ref::<T>())
78    }
79
80    fn get_mut<T: crate::components::Component>(&mut self, entity: EntityId) -> Option<&mut T> {
81        self.get_component_mut(entity, TypeId::of::<T>())
82            .and_then(|any| any.downcast_mut::<T>())
83    }
84
85    fn set<T: crate::components::Component>(&mut self, entity: EntityId, component: T) {
86        self.set_component(entity, TypeId::of::<T>(), Box::new(component));
87    }
88
89    fn query<T: crate::components::Component>(&self) -> Vec<EntityId> {
90        self.entities_with(TypeId::of::<T>())
91    }
92}
93
94/// A registered system with its metadata.
95pub struct SystemDescriptor {
96    /// Human-readable name for logging.
97    pub name: String,
98    /// Which tick phase this system runs in.
99    pub phase: Phase,
100    /// Frequency divisor: runs when `tick_count % every == 0`.
101    pub every: u64,
102    /// Component access declaration.
103    pub access: SystemAccess,
104    /// Optional CPU budget per invocation. `None` means unlimited.
105    pub budget: Option<Duration>,
106    /// The system function (type-erased).
107    pub runner: Box<dyn SystemRunner>,
108}
109
110/// Trait for system execution functions.
111///
112/// Implemented by closures wrapped via [`SystemBuilder`].
113pub trait SystemRunner: Send {
114    /// Runs the system for one tick with a context.
115    fn run(&mut self, ctx: &mut dyn SystemContext);
116}
117
118/// Blanket implementation for closures.
119impl<F: FnMut(&mut dyn SystemContext) + Send> SystemRunner for F {
120    fn run(&mut self, ctx: &mut dyn SystemContext) {
121        self(ctx);
122    }
123}
124
125/// Component access declaration for a system.
126///
127/// Tracks which component types a system reads and writes.
128/// Two systems conflict if one writes a component the other
129/// reads or writes.
130#[derive(Debug, Clone)]
131pub struct SystemAccess {
132    /// Component types this system reads.
133    pub reads: HashSet<TypeId>,
134    /// Component types this system writes.
135    pub writes: HashSet<TypeId>,
136}
137
138impl SystemAccess {
139    /// Creates an empty access declaration.
140    pub fn new() -> Self {
141        Self {
142            reads: HashSet::new(),
143            writes: HashSet::new(),
144        }
145    }
146
147    /// Returns whether this system conflicts with another.
148    ///
149    /// Two systems conflict if one writes a component type that
150    /// the other reads or writes.
151    pub fn conflicts_with(&self, other: &SystemAccess) -> bool {
152        for w in &self.writes {
153            if other.reads.contains(w) || other.writes.contains(w) {
154                return true;
155            }
156        }
157        for w in &other.writes {
158            if self.reads.contains(w) {
159                return true;
160            }
161        }
162        false
163    }
164}
165
166impl Default for SystemAccess {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// Builder for declaring a system's metadata and component access.
173pub struct SystemBuilder {
174    name: String,
175    phase: Phase,
176    every: u64,
177    access: SystemAccess,
178    budget: Option<Duration>,
179}
180
181impl SystemBuilder {
182    /// Creates a new system builder with the given name.
183    pub fn new(name: &str) -> Self {
184        Self {
185            name: name.to_string(),
186            phase: Phase::Simulate,
187            every: 1,
188            access: SystemAccess::new(),
189            budget: None,
190        }
191    }
192
193    /// Sets which tick phase this system runs in.
194    pub fn phase(mut self, phase: Phase) -> Self {
195        self.phase = phase;
196        self
197    }
198
199    /// Sets the frequency divisor.
200    ///
201    /// The system runs when `tick_count % every == 0`.
202    /// Default is 1 (every tick).
203    pub fn every(mut self, every: u64) -> Self {
204        self.every = every;
205        self
206    }
207
208    /// Declares that this system reads a component type.
209    pub fn reads<T: crate::components::Component>(mut self) -> Self {
210        self.access.reads.insert(TypeId::of::<T>());
211        self
212    }
213
214    /// Declares that this system writes a component type.
215    pub fn writes<T: crate::components::Component>(mut self) -> Self {
216        self.access.writes.insert(TypeId::of::<T>());
217        self
218    }
219
220    /// Sets the CPU budget for this system in milliseconds.
221    ///
222    /// When set, the system can check `ctx.budget().is_expired()` to
223    /// yield early. Systems without a budget get an unlimited one.
224    pub fn budget_ms(mut self, ms: u64) -> Self {
225        self.budget = Some(Duration::from_millis(ms));
226        self
227    }
228
229    /// Finalizes the builder and registers the system with a runner.
230    pub fn run<F: FnMut(&mut dyn SystemContext) + Send + 'static>(
231        self,
232        runner: F,
233    ) -> SystemDescriptor {
234        SystemDescriptor {
235            name: self.name,
236            phase: self.phase,
237            every: self.every,
238            access: self.access,
239            budget: self.budget,
240            runner: Box::new(runner),
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::components::{Position, Velocity};
249
250    #[test]
251    fn system_builder_defaults() {
252        let desc = SystemBuilder::new("test").run(|_ctx| {});
253        assert_eq!(desc.name, "test");
254        assert_eq!(desc.phase, Phase::Simulate);
255        assert_eq!(desc.every, 1);
256    }
257
258    #[test]
259    fn system_builder_with_access() {
260        let desc = SystemBuilder::new("physics")
261            .phase(Phase::Simulate)
262            .every(1)
263            .reads::<Position>()
264            .writes::<Position>()
265            .writes::<Velocity>()
266            .run(|_ctx| {});
267        assert!(desc.access.reads.contains(&TypeId::of::<Position>()));
268        assert!(desc.access.writes.contains(&TypeId::of::<Position>()));
269        assert!(desc.access.writes.contains(&TypeId::of::<Velocity>()));
270    }
271
272    #[test]
273    fn access_conflict_detection() {
274        let mut a = SystemAccess::new();
275        a.writes.insert(TypeId::of::<Position>());
276
277        let mut b = SystemAccess::new();
278        b.reads.insert(TypeId::of::<Position>());
279
280        assert!(a.conflicts_with(&b));
281        assert!(b.conflicts_with(&a));
282    }
283
284    #[test]
285    fn no_conflict_for_disjoint_access() {
286        let mut a = SystemAccess::new();
287        a.reads.insert(TypeId::of::<Position>());
288
289        let mut b = SystemAccess::new();
290        b.reads.insert(TypeId::of::<Velocity>());
291
292        assert!(!a.conflicts_with(&b));
293    }
294
295    #[test]
296    fn read_read_no_conflict() {
297        let mut a = SystemAccess::new();
298        a.reads.insert(TypeId::of::<Position>());
299
300        let mut b = SystemAccess::new();
301        b.reads.insert(TypeId::of::<Position>());
302
303        assert!(!a.conflicts_with(&b));
304    }
305}