Skip to main content

pebble/ecs/
schedule.rs

1use std::any::TypeId;
2
3use crate::ecs::{
4    commands::{ResourceCommandQueue, TriggerQueue},
5    resources::Resources,
6    system_param::{IntoSystem, System, SystemChain, SystemConfig},
7};
8
9/// An ordered list of systems, run together — one `Schedule` backs each
10/// [`SystemStage`](crate::ecs::system::SystemStage). Deferred `Commands`
11/// (entity spawns, resource inserts/removes, triggers) are flushed after
12/// *each* system, not just once at the end — so a system ordered
13/// `.after(...)` another can rely on seeing its `Commands` effects, even
14/// within the same stage.
15///
16/// Systems normally run in the order they were added. Call `.after(...)`/
17/// `.before(...)` directly on a system (see
18/// [`IntoSystemConfig`](crate::ecs::system_param::IntoSystemConfig)) to
19/// constrain it relative to another system known to the schedule — added
20/// earlier or later, registration order doesn't matter, only the
21/// constraint does. `.priority(...)` breaks ties between systems with no
22/// `after`/`before` relationship to each other — higher runs first — but
23/// never overrides an explicit constraint. `.chain()` on a tuple of systems
24/// (see [`Chain`](crate::ecs::system_param::Chain), registered with
25/// [`add_systems`](Schedule::add_systems)) forces them to run in that exact
26/// relative order, and can itself be given `.after(...)`/`.before(...)`/
27/// `.priority(...)`, applied to the whole chain:
28///
29/// ```ignore
30/// schedule
31///     .add_system(spawn_enemies)
32///     .add_system(move_enemies.after(spawn_enemies))
33///     .add_system(render.after(move_enemies))
34///     .add_system(hud.priority(10))
35///     .add_systems((physics_step, resolve_collisions).chain().before(render));
36/// ```
37#[derive(Default)]
38pub struct Schedule {
39    systems: Vec<(TypeId, Box<dyn System>, i32)>,
40    /// `(dependent, dependency)` — `dependent` must run after `dependency`.
41    constraints: Vec<(TypeId, TypeId)>,
42    order: Vec<usize>,
43    order_dirty: bool,
44}
45
46impl Schedule {
47    /// Appends `system` to the end of this schedule. `system` may be a bare
48    /// system, or one wrapped with `.after(...)`/`.before(...)`/`.priority(...)`
49    /// — see [`IntoSystemConfig`](crate::ecs::system_param::IntoSystemConfig).
50    pub fn add_system<S, Params>(&mut self, system: impl Into<SystemConfig<S, Params>>) -> &mut Self
51    where
52        Params: 'static,
53        S: IntoSystem<Params> + 'static,
54    {
55        let (id, system, priority, constraints) = system.into().into_parts();
56        self.systems.push((id, system, priority));
57        self.constraints.extend(constraints);
58        self.order_dirty = true;
59        self
60    }
61
62    /// Appends every system in `chain` (built with `.chain()` on a tuple of
63    /// systems — see [`Chain`](crate::ecs::system_param::Chain)) to the end
64    /// of this schedule.
65    pub fn add_systems(&mut self, chain: SystemChain) -> &mut Self {
66        let (systems, constraints) = chain.into_parts();
67        self.systems.extend(systems);
68        self.constraints.extend(constraints);
69        self.order_dirty = true;
70        self
71    }
72
73    /// Topologically sorts systems to satisfy every `after`/`before`
74    /// constraint. Among systems with no constraint relative to each other,
75    /// higher `priority` runs first; ties within the same priority break by
76    /// original `add_system` order. Panics if the constraints form a cycle;
77    /// silently ignores a constraint that names a system never added to
78    /// this schedule.
79    fn compute_order(&self) -> Vec<usize> {
80        let n = self.systems.len();
81        let index_of = |id: TypeId| self.systems.iter().position(|(sid, _, _)| *sid == id);
82
83        let mut in_degree = vec![0usize; n];
84        let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
85
86        for &(dependent_id, dependency_id) in &self.constraints {
87            if let (Some(dependent), Some(dependency)) =
88                (index_of(dependent_id), index_of(dependency_id))
89                && dependent != dependency
90            {
91                dependents[dependency].push(dependent);
92                in_degree[dependent] += 1;
93            }
94        }
95
96        let mut remaining: Vec<usize> = (0..n).collect();
97        let mut order = Vec::with_capacity(n);
98
99        while !remaining.is_empty() {
100            // among ready systems (in_degree 0), pick the highest priority;
101            // ties keep the first one found, preserving `add_system` order
102            // since `remaining` is only ever shrunk, never reordered.
103            let mut best: Option<(usize, i32)> = None;
104            for (pos, &i) in remaining.iter().enumerate() {
105                if in_degree[i] != 0 {
106                    continue;
107                }
108                let priority = self.systems[i].2;
109                if best.is_none_or(|(_, best_priority)| priority > best_priority) {
110                    best = Some((pos, priority));
111                }
112            }
113            let ready = best.map(|(pos, _)| pos).expect("system ordering constraints form a cycle");
114            let picked = remaining.remove(ready);
115            order.push(picked);
116            for &dependent in &dependents[picked] {
117                in_degree[dependent] -= 1;
118            }
119        }
120
121        order
122    }
123
124    /// Runs every system in order, flushing deferred entity spawns, resource
125    /// commands, and triggered observers after *each* system — not once at
126    /// the end of the whole schedule. This is what lets a system that reads
127    /// a resource another system just inserted via `Commands::insert_resource`
128    /// see it immediately, as long as it's ordered `.after(...)` the system
129    /// that queued it — the two don't need to be in different stages.
130    pub fn run(&mut self, world: &mut hecs::World, resources: &mut Resources) {
131        if self.order_dirty {
132            self.order = self.compute_order();
133            self.order_dirty = false;
134        }
135
136        for &index in &self.order {
137            self.systems[index].1.run(world, &*resources);
138            Self::sync_commands(world, resources);
139        }
140    }
141
142    /// Applies the entity/resource commands and triggered observers queued
143    /// by the system that just ran, so the next system in this schedule
144    /// observes them.
145    fn sync_commands(world: &mut hecs::World, resources: &mut Resources) {
146        // sync entity commands
147        resources.get_mut::<hecs::CommandBuffer>().run_on(world);
148
149        // sync resource commands
150        if resources.contains::<ResourceCommandQueue>() {
151            let commands = std::mem::take(&mut resources.get_mut::<ResourceCommandQueue>().0);
152            for command in commands {
153                command(resources);
154            }
155        }
156
157        // sync triggered observers
158        if resources.contains::<TriggerQueue>() {
159            let triggers = std::mem::take(&mut resources.get_mut::<TriggerQueue>().0);
160            for trigger in triggers {
161                trigger(world, resources);
162            }
163        }
164    }
165}