Skip to main content

moirai/schedule/
system.rs

1//! Authoring-time system descriptors, flush policy, and opaque runtime handles.
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::any::TypeId;
7
8use crate::event::{ComponentAdded, ComponentRemoved, EventReader, EventReaderStart};
9use crate::query::{PreparedQuery1, PreparedQuery2, QueryError, QueryPolicy, QuerySpec};
10use crate::schedule::condition::Condition;
11use crate::schedule::owner::ScheduleOwner;
12use crate::world::{World, WorldError};
13
14/// When deferred structural commands become visible during Update.
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16pub enum FlushMode {
17    /// Flush once after all Update stages in the pass (default for Render).
18    Final,
19    /// Flush at the end of each Update stage (standard builder default).
20    Stage,
21    /// Flush immediately after this system when running under Update.
22    AfterSystem,
23}
24
25pub(crate) type SystemBody = Box<dyn FnMut(&mut crate::world::World, f32) -> Result<(), String>>;
26pub(crate) type SystemInitializer =
27    Box<dyn for<'world> FnOnce(&mut SystemInitContext<'world>) -> Result<SystemBody, String>>;
28
29pub(crate) enum SystemBodySource {
30    Ready(SystemBody),
31    Initialize(SystemInitializer),
32}
33
34/// Restricted build-time access used to create persistent system-local state.
35///
36/// Initializers may inspect resources and create event readers, but cannot
37/// mutate the world. The context is constructed only while a schedule builds.
38pub struct SystemInitContext<'world> {
39    world: &'world mut World,
40}
41
42impl<'world> SystemInitContext<'world> {
43    pub(crate) fn new(world: &'world mut World) -> Self {
44        Self { world }
45    }
46
47    /// Whether the resource type is registered and present.
48    pub fn contains_resource<R: 'static>(&self) -> bool {
49        self.world.contains_resource::<R>()
50    }
51
52    /// Read-only resource access during initializer execution.
53    pub fn resource<R: 'static>(&self) -> Result<Option<&R>, WorldError> {
54        self.world.resource::<R>()
55    }
56
57    /// Persistent event reader seeded for the compiled system's lifetime.
58    pub fn event_reader<E: Clone + 'static>(
59        &mut self,
60        start: EventReaderStart,
61    ) -> Result<EventReader<E>, WorldError> {
62        self.world.event_reader::<E>(start)
63    }
64
65    /// Persistent component-added lifecycle reader for this system.
66    pub fn on_add_reader<T: 'static>(
67        &mut self,
68        start: EventReaderStart,
69    ) -> Result<EventReader<ComponentAdded>, WorldError> {
70        self.world.on_add_reader::<T>(start)
71    }
72
73    /// Persistent component-removed lifecycle reader for this system.
74    pub fn on_remove_reader<T: 'static>(
75        &mut self,
76        start: EventReaderStart,
77    ) -> Result<EventReader<ComponentRemoved>, WorldError> {
78        self.world.on_remove_reader::<T>(start)
79    }
80
81    /// Resolves and stores a reusable single-component query for this system.
82    pub fn prepare_query1<T: 'static>(
83        &mut self,
84        spec: QuerySpec,
85        policy: QueryPolicy,
86    ) -> Result<PreparedQuery1<T>, QueryError> {
87        self.world.prepare_query1(spec, policy)
88    }
89
90    /// Resolves and stores a reusable two-component query for this system.
91    pub fn prepare_query2<A: 'static, B: 'static>(
92        &mut self,
93        spec: QuerySpec,
94        policy: QueryPolicy,
95    ) -> Result<PreparedQuery2<A, B>, QueryError> {
96        self.world.prepare_query2(spec, policy)
97    }
98}
99
100#[derive(Copy, Clone, Debug, Eq, PartialEq)]
101pub(crate) enum EventRoleKind {
102    Emits,
103    Consumes,
104    ConsumesOnAdd,
105    ConsumesOnRemove,
106}
107
108#[derive(Clone, Debug)]
109pub(crate) struct EventRole {
110    pub type_id: TypeId,
111    pub type_name: &'static str,
112    pub kind: EventRoleKind,
113}
114
115/// Opaque compiled system handle.
116#[derive(Clone, Debug, Eq, PartialEq, Hash)]
117pub struct SystemId {
118    owner: ScheduleOwner,
119    index: u32,
120    generation: u32,
121}
122
123impl SystemId {
124    pub(crate) fn new(owner: ScheduleOwner, index: u32, generation: u32) -> Self {
125        Self {
126            owner,
127            index,
128            generation,
129        }
130    }
131
132    /// Stable compiled index for diagnostics; prefer label lookup for authoring.
133    pub fn index(&self) -> usize {
134        self.index as usize
135    }
136
137    pub(crate) fn validate_owner(
138        &self,
139        owner: &ScheduleOwner,
140        generation: u32,
141    ) -> Result<(), crate::schedule::ScheduleError> {
142        if !self.owner.same(owner) {
143            return Err(crate::schedule::ScheduleError::OwnerMismatch);
144        }
145        if self.generation != generation {
146            return Err(crate::schedule::ScheduleError::StaleHandle);
147        }
148        Ok(())
149    }
150}
151
152/// Authoring-time system-set label.
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct SystemSet {
155    label: String,
156}
157
158impl SystemSet {
159    /// Declares a named group for shared ordering edges and run-if gates.
160    pub fn new(label: impl Into<String>) -> Self {
161        Self {
162            label: label.into(),
163        }
164    }
165
166    /// Set label used by the builder and ordering APIs.
167    pub fn label(&self) -> &str {
168        &self.label
169    }
170}
171
172/// Authoring-time system node: stage placement, ordering, conditions, and event roles.
173pub struct System {
174    pub(crate) name: String,
175    pub(crate) stage_label: String,
176    pub(crate) body: SystemBodySource,
177    pub(crate) enabled: bool,
178    pub(crate) flush_mode: FlushMode,
179    pub(crate) before: Vec<String>,
180    pub(crate) after: Vec<String>,
181    pub(crate) before_sets: Vec<String>,
182    pub(crate) after_sets: Vec<String>,
183    pub(crate) in_set: Option<String>,
184    pub(crate) conditions: Vec<Condition>,
185    pub(crate) required_resources: Vec<TypeId>,
186    pub(crate) event_roles: Vec<EventRole>,
187}
188
189impl System {
190    /// Infallible body wrapper; panics and world errors must be handled inside the closure.
191    pub fn new(
192        name: impl Into<String>,
193        stage: impl Into<String>,
194        body: impl FnMut(&mut crate::world::World, f32) + 'static,
195    ) -> Self {
196        let mut handler = body;
197        Self {
198            name: name.into(),
199            stage_label: stage.into(),
200            body: SystemBodySource::Ready(Box::new(move |world, dt| {
201                handler(world, dt);
202                Ok(())
203            })),
204            enabled: true,
205            flush_mode: FlushMode::Final,
206            before: Vec::new(),
207            after: Vec::new(),
208            before_sets: Vec::new(),
209            after_sets: Vec::new(),
210            in_set: None,
211            conditions: Vec::new(),
212            required_resources: Vec::new(),
213            event_roles: Vec::new(),
214        }
215    }
216
217    /// Fallible body that can abort the stage pass with a detail string.
218    pub fn try_new(
219        name: impl Into<String>,
220        stage: impl Into<String>,
221        body: impl FnMut(&mut crate::world::World, f32) -> Result<(), String> + 'static,
222    ) -> Self {
223        let mut handler = body;
224        Self {
225            name: name.into(),
226            stage_label: stage.into(),
227            body: SystemBodySource::Ready(Box::new(move |world, dt| handler(world, dt))),
228            enabled: true,
229            flush_mode: FlushMode::Final,
230            before: Vec::new(),
231            after: Vec::new(),
232            before_sets: Vec::new(),
233            after_sets: Vec::new(),
234            in_set: None,
235            conditions: Vec::new(),
236            required_resources: Vec::new(),
237            event_roles: Vec::new(),
238        }
239    }
240
241    /// Creates a system whose persistent local state is initialized at build time.
242    pub fn with_local<L: 'static>(
243        name: impl Into<String>,
244        stage: impl Into<String>,
245        init: impl FnOnce(&mut SystemInitContext<'_>) -> Result<L, String> + 'static,
246        run: impl FnMut(&mut World, f32, &mut L) -> Result<(), String> + 'static,
247    ) -> Self {
248        let mut run = run;
249        let initializer = move |context: &mut SystemInitContext<'_>| {
250            let mut local = init(context)?;
251            let body: SystemBody = Box::new(move |world, dt| run(world, dt, &mut local));
252            Ok(body)
253        };
254        Self {
255            name: name.into(),
256            stage_label: stage.into(),
257            body: SystemBodySource::Initialize(Box::new(initializer)),
258            enabled: true,
259            flush_mode: FlushMode::Final,
260            before: Vec::new(),
261            after: Vec::new(),
262            before_sets: Vec::new(),
263            after_sets: Vec::new(),
264            in_set: None,
265            conditions: Vec::new(),
266            required_resources: Vec::new(),
267            event_roles: Vec::new(),
268        }
269    }
270
271    /// Runs before the named system within the same stage.
272    pub fn before(mut self, label: impl Into<String>) -> Self {
273        self.before.push(label.into());
274        self
275    }
276
277    /// Runs after the named system within the same stage.
278    pub fn after(mut self, label: impl Into<String>) -> Self {
279        self.after.push(label.into());
280        self
281    }
282
283    /// Runs before every system in the set that shares this stage.
284    pub fn before_set(mut self, set: &SystemSet) -> Self {
285        self.before_sets.push(set.label.clone());
286        self
287    }
288
289    /// Runs after every system in the set that shares this stage.
290    pub fn after_set(mut self, set: &SystemSet) -> Self {
291        self.after_sets.push(set.label.clone());
292        self
293    }
294
295    /// Membership for set-level ordering edges and shared run-if gates.
296    pub fn in_set(mut self, set: &SystemSet) -> Self {
297        self.in_set = Some(set.label.clone());
298        self
299    }
300
301    /// Skips the system body when the condition evaluates false.
302    pub fn run_if(mut self, condition: Condition) -> Self {
303        self.conditions.push(condition);
304        self
305    }
306
307    /// Build fails unless the resource is present; attaches a world lease lock.
308    pub fn requires_resource<R: 'static>(mut self) -> Self {
309        self.required_resources.push(TypeId::of::<R>());
310        self
311    }
312
313    /// Declares that this system may send events of type `E`.
314    pub fn emits<E: Clone + 'static>(mut self) -> Self {
315        self.push_event_role::<E>(EventRoleKind::Emits);
316        self
317    }
318
319    /// Declares that this system may create readers for and read events of type `E`.
320    pub fn consumes<E: Clone + 'static>(mut self) -> Self {
321        self.push_event_role::<E>(EventRoleKind::Consumes);
322        self
323    }
324
325    /// Declares that this system consumes the added lifecycle channel for `T`.
326    pub fn consumes_on_add<T: 'static>(mut self) -> Self {
327        self.push_event_role::<T>(EventRoleKind::ConsumesOnAdd);
328        self
329    }
330
331    /// Declares that this system consumes the removed lifecycle channel for `T`.
332    pub fn consumes_on_remove<T: 'static>(mut self) -> Self {
333        self.push_event_role::<T>(EventRoleKind::ConsumesOnRemove);
334        self
335    }
336
337    fn push_event_role<T: 'static>(&mut self, kind: EventRoleKind) {
338        let type_id = TypeId::of::<T>();
339        if self
340            .event_roles
341            .iter()
342            .any(|role| role.type_id == type_id && role.kind == kind)
343        {
344            return;
345        }
346        self.event_roles.push(EventRole {
347            type_id,
348            type_name: core::any::type_name::<T>(),
349            kind,
350        });
351    }
352
353    /// Overrides deferred-command flush timing for this system on Update stages.
354    pub fn flush_mode(mut self, mode: FlushMode) -> Self {
355        self.flush_mode = mode;
356        self
357    }
358
359    /// Shorthand for [`FlushMode::AfterSystem`] on Update stages.
360    pub fn flush_after(mut self) -> Self {
361        self.flush_mode = FlushMode::AfterSystem;
362        self
363    }
364
365    /// Registers the system but leaves it disabled until toggled at runtime.
366    pub fn disabled(mut self) -> Self {
367        self.enabled = false;
368        self
369    }
370
371    /// Authoring label and runtime diagnostic name.
372    pub fn name(&self) -> &str {
373        &self.name
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use crate::component::ComponentOptions;
381    use crate::event::EventOptions;
382    use crate::schedule::ScheduleError;
383    use crate::world::WorldBuilder;
384
385    #[test]
386    fn system_id_validate_owner_and_generation() {
387        let owner = ScheduleOwner::new();
388        let id = SystemId::new(owner.clone(), 0, 1);
389        assert!(id.validate_owner(&owner, 1).is_ok());
390        assert!(matches!(
391            id.validate_owner(&ScheduleOwner::new(), 1),
392            Err(ScheduleError::OwnerMismatch)
393        ));
394        assert!(matches!(
395            id.validate_owner(&owner, 0),
396            Err(ScheduleError::StaleHandle)
397        ));
398    }
399
400    #[test]
401    fn system_builder_fluent_api() {
402        let set = SystemSet::new("physics");
403        let _ = System::new("move", "Update", |_world, _dt| {})
404            .before("setup")
405            .after("cleanup")
406            .before_set(&set)
407            .after_set(&set)
408            .in_set(&set)
409            .run_if(Condition::always())
410            .requires_resource::<WorldBuilder>()
411            .emits::<u32>()
412            .consumes::<u32>()
413            .consumes_on_add::<u32>()
414            .consumes_on_remove::<u32>()
415            .flush_mode(FlushMode::Stage)
416            .flush_after()
417            .disabled()
418            .name();
419    }
420
421    #[test]
422    fn init_context_exposes_registered_runtime_state_and_prepared_queries() {
423        struct Position;
424        struct Velocity;
425        #[derive(Clone)]
426        struct Tick;
427
428        let mut builder = WorldBuilder::new();
429        builder
430            .register_component::<Position>(ComponentOptions::sparse())
431            .expect("position");
432        builder
433            .register_component::<Velocity>(ComponentOptions::sparse())
434            .expect("velocity");
435        builder.insert_resource(7_u32);
436        builder
437            .add_event::<Tick>(EventOptions::manual())
438            .expect("event");
439        let mut world = builder.build().expect("world");
440        let mut context = SystemInitContext::new(&mut world);
441
442        assert!(context.contains_resource::<u32>());
443        assert_eq!(context.resource::<u32>().expect("resource"), Some(&7));
444        context
445            .event_reader::<Tick>(EventReaderStart::FromNow)
446            .expect("event reader");
447        context
448            .on_add_reader::<Position>(EventReaderStart::FromNow)
449            .expect("add reader");
450        context
451            .on_remove_reader::<Position>(EventReaderStart::FromNow)
452            .expect("remove reader");
453        context
454            .prepare_query1::<Position>(QuerySpec::new(), QueryPolicy::Prepared)
455            .expect("query1");
456        context
457            .prepare_query2::<Position, Velocity>(QuerySpec::new(), QueryPolicy::Prepared)
458            .expect("query2");
459    }
460
461    #[test]
462    fn duplicate_event_role_is_suppressed() {
463        let system = System::new("writer", "Update", |_world, _dt| {})
464            .emits::<u32>()
465            .emits::<u32>();
466        assert_eq!(system.event_roles.len(), 1);
467    }
468}