Skip to main content

pebble/
app.rs

1use std::collections::BTreeMap;
2
3use crate::ecs::{
4    commands::{ResourceCommandQueue, TriggerQueue},
5    events::{Events, age_events},
6    observers::{IntoObserverSystem, Observers},
7    plugin::Plugin,
8    resources::Resources,
9    schedule::Schedule,
10    system::SystemStage,
11    system_param::{IntoSystem, SystemChain, SystemConfig},
12};
13
14/// Set this to `true` (e.g. `commands.insert_resource(AppExit(true))`) to
15/// stop the default headless polling loop after the current tick. Has no
16/// effect on a windowing plugin's own runner — closing the window is what
17/// stops that one.
18#[derive(Default)]
19pub struct AppExit(pub bool);
20
21/// Whether the GPU backend has finished initializing. `false` until a
22/// plugin (e.g. `GraphicsPlugin`) acquires one and flips it — until then,
23/// [`App::update`] only runs `gpu_schedules`, not the regular stages.
24#[derive(Default)]
25pub struct BackendReady(pub bool);
26
27/// The central application object: owns the ECS world, resources, and every
28/// registered system, organized into [`SystemStage`]s.
29///
30/// Built by chaining `.add_plugin(...)`/`.add_system(...)`/etc. calls —
31/// every builder method takes `self` by value and returns `Self`, so a
32/// typical setup reads as one expression ending in [`App::run`]:
33///
34/// ```ignore
35/// App::new()
36///     .add_plugin(GraphicsPlugin)
37///     .add_system(SystemStage::Ready, setup)
38///     .add_system(SystemStage::Update, my_game_logic)
39///     .run();
40/// ```
41pub struct App {
42    world: hecs::World,
43    resources: Resources,
44    schedules: BTreeMap<SystemStage, Schedule>,
45    pub(crate) gpu_schedules: BTreeMap<SystemStage, Schedule>,
46    runner: Option<Box<dyn FnOnce(App)>>,
47}
48
49impl Default for App {
50    fn default() -> Self {
51        let mut resources = Resources::default();
52        resources.insert(hecs::CommandBuffer::default());
53        resources.insert(ResourceCommandQueue::default());
54        resources.insert(TriggerQueue::default());
55        resources.insert(AppExit::default());
56        resources.insert(BackendReady::default());
57
58        Self {
59            world: hecs::World::default(),
60            schedules: BTreeMap::new(),
61            gpu_schedules: BTreeMap::new(),
62            resources,
63            runner: None,
64        }
65    }
66}
67
68impl App {
69    /// Creates a fresh `App` with no plugins, systems, or resources beyond
70    /// the small set every app needs internally (a command buffer,
71    /// [`AppExit`], [`BackendReady`]). Nothing is registered automatically
72    /// — windowing, the GPU backend, `Time` are all opt-in via
73    /// `.add_plugin(...)`.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Inserts a resource, replacing any existing value of the same type.
79    pub fn insert_resource<T: 'static>(mut self, resource: T) -> Self {
80        self.resources.insert(resource);
81        self
82    }
83
84    /// Removes a resource, if present. A no-op if it wasn't there.
85    pub fn remove_resource<T: 'static>(mut self) -> Self {
86        self.resources.remove::<T>();
87        self
88    }
89
90    /// Runs a [`Plugin`]'s `build`, which may insert resources, register
91    /// systems, or add further plugins of its own.
92    pub fn add_plugin<P: Plugin>(self, plugin: P) -> Self {
93        plugin.build(self)
94    }
95
96    fn add_system_to<S, Params>(
97        schedules: &mut BTreeMap<SystemStage, Schedule>,
98        stage: SystemStage,
99        system: impl Into<SystemConfig<S, Params>>,
100    ) where
101        Params: 'static,
102        S: IntoSystem<Params> + 'static,
103    {
104        schedules
105            .entry(stage)
106            .or_insert_with(Schedule::default)
107            .add_system(system);
108    }
109
110    fn add_systems_to(schedules: &mut BTreeMap<SystemStage, Schedule>, stage: SystemStage, chain: SystemChain) {
111        schedules
112            .entry(stage)
113            .or_insert_with(Schedule::default)
114            .add_systems(chain);
115    }
116
117    /// Registers `system` to run on `stage`, every tick that stage runs.
118    /// See [`SystemStage`] for what each stage is for and when it runs.
119    /// `system` may be a bare system, or one wrapped with
120    /// `.after(...)`/`.before(...)`/`.priority(...)` to order/prioritize it
121    /// relative to another system on the same stage — see
122    /// [`IntoSystemConfig`](crate::ecs::system_param::IntoSystemConfig).
123    pub fn add_system<S, Params>(mut self, stage: SystemStage, system: impl Into<SystemConfig<S, Params>>) -> Self
124    where
125        Params: 'static,
126        S: IntoSystem<Params> + 'static,
127    {
128        Self::add_system_to(&mut self.schedules, stage, system);
129        self
130    }
131
132    /// Registers every system in `chain` — built by calling `.chain()` on a
133    /// tuple of systems, see [`Chain`](crate::ecs::system_param::Chain) —
134    /// to run on `stage`, in that exact relative order.
135    pub fn add_systems(mut self, stage: SystemStage, chain: SystemChain) -> Self {
136        Self::add_systems_to(&mut self.schedules, stage, chain);
137        self
138    }
139
140    pub(crate) fn add_gpu_system<S, Params>(mut self, stage: SystemStage, system: impl Into<SystemConfig<S, Params>>) -> Self
141    where
142        Params: 'static,
143        S: IntoSystem<Params> + 'static,
144    {
145        Self::add_system_to(&mut self.gpu_schedules, stage, system);
146        self
147    }
148
149    /// Registers event type `T`, making [`EventReader<T>`](crate::ecs::events::EventReader)/
150    /// [`EventWriter<T>`](crate::ecs::events::EventWriter) usable as system
151    /// parameters. Idempotent — calling this twice for the same `T` (e.g.
152    /// from two different plugins that both want it) is a no-op the second
153    /// time, not a double registration.
154    pub fn add_event<T: 'static + Send + Sync>(mut self) -> Self {
155        if !self.resources.contains::<Events<T>>() {
156            self.resources.insert(Events::<T>::default());
157            self = self.add_system(SystemStage::PreUpdate, age_events::<T>);
158        }
159        self
160    }
161
162    /// Registers `observer` to run whenever [`Commands::trigger`](crate::ecs::commands::Commands::trigger)
163    /// sends an `E`, once the current stage finishes syncing (same tick,
164    /// not deferred to the next one). Multiple observers can be registered
165    /// for the same `E` — every one of them runs.
166    pub fn add_observer<E: 'static + Send + Sync, Params: 'static>(
167        mut self,
168        observer: impl IntoObserverSystem<E, Params> + 'static,
169    ) -> Self {
170        if !self.resources.contains::<Observers<E>>() {
171            self.resources.insert(Observers::<E>::default());
172        }
173        self.resources.get_mut::<Observers<E>>().0.push(Box::new(observer.into_observer_system()));
174        self
175    }
176
177    /// Overrides how the main loop is driven — e.g. a windowing plugin
178    /// installs one that hands control to its own event loop instead of
179    /// the default headless polling loop.
180    pub fn set_runner(mut self, runner: impl FnOnce(App) + 'static) -> Self {
181        self.runner = Some(Box::new(runner));
182        self
183    }
184
185    /// Initializes a `tracing_subscriber` formatter so `tracing::info!`/
186    /// `warn!`/`error!` calls made throughout the engine actually print
187    /// somewhere.
188    pub fn with_logging(self) -> Self {
189        tracing_subscriber::fmt().init();
190        self
191    }
192
193    /// Runs every schedule for a single tick: while the GPU backend isn't
194    /// ready yet, only the internal `gpu_schedules` run; once it is,
195    /// [`SystemStage::Ready`] runs (if anything is still registered there,
196    /// exactly once ever), then every other stage runs in order. Called
197    /// automatically by the default loop in [`App::run`] — call it
198    /// yourself only if you're driving the loop from somewhere else (e.g.
199    /// inside a custom runner installed via [`App::set_runner`]).
200    pub fn update(&mut self) {
201        if !self.resources.get::<BackendReady>().0 {
202            println!("Backend Pending");
203            for (_, schedule) in self.gpu_schedules.iter_mut() {
204                schedule.run(&mut self.world, &mut self.resources);
205            }
206            return;
207        }
208
209        println!("Backend Obtained, Running Systems");
210        if let Some(mut ready) = self.schedules.remove(&SystemStage::Ready) {
211            ready.run(&mut self.world, &mut self.resources);
212        }
213        for (_, schedule) in self.schedules.iter_mut() {
214            schedule.run(&mut self.world, &mut self.resources);
215        }
216    }
217
218    /// `true` once [`AppExit`] has been set — the default loop in
219    /// [`App::run`] checks this after every tick.
220    pub fn should_exit(&self) -> bool {
221        self.resources.get::<AppExit>().0
222    }
223
224    /// Consumes the app and runs it. [`SystemStage::Startup`] runs first,
225    /// exactly once, before anything else. Then, if a runner was installed
226    /// (e.g. by a windowing plugin via [`App::set_runner`]), control is
227    /// handed to it — this call doesn't return until that runner decides
228    /// to stop. Otherwise, falls back to a default headless loop that
229    /// calls [`App::update`] repeatedly until [`App::should_exit`].
230    pub fn run(mut self) {
231        // startup schedules run exactly once, before the main loop
232        if let Some(mut startup) = self.schedules.remove(&SystemStage::Startup) {
233            startup.run(&mut self.world, &mut self.resources);
234        }
235
236        // a windowing plugin (e.g. WindowPlugin) hands control to its own
237        // event loop instead of the default headless polling loop below
238        if let Some(runner) = self.runner.take() {
239            runner(self);
240            return;
241        }
242
243        loop {
244            let was_ready = self.resources.get::<BackendReady>().0;
245            self.update();
246
247            if !was_ready {
248                // no real OS thread to sleep on wasm32 — just busy-poll.
249                // Only reached at all when an app never registers a
250                // windowing plugin, which always overrides this runner.
251                #[cfg(not(target_arch = "wasm32"))]
252                std::thread::sleep(std::time::Duration::from_millis(16));
253                continue;
254            }
255
256            if self.should_exit() {
257                break;
258            }
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::ecs::{
267        commands::Commands,
268        events::{EventReader, EventWriter},
269        local::Local,
270        observers::Trigger,
271        resources::{Read, Write},
272    };
273
274    struct Damage(u32);
275
276    #[derive(Default)]
277    struct Seen(Vec<u32>);
278
279    fn send_once(mut writer: EventWriter<Damage>, mut sent: Local<bool>) {
280        if !*sent {
281            writer.send(Damage(7));
282            *sent = true;
283        }
284    }
285
286    fn record(mut reader: EventReader<Damage>, mut seen: Write<Seen>) {
287        for event in reader.iter() {
288            seen.0.push(event.0);
289        }
290    }
291
292    #[test]
293    fn add_event_called_twice_still_delivers_exactly_once_one_tick_later() {
294        // Registering the same event type twice (e.g. two plugins both
295        // wanting `Damage`) must be a no-op the second time — this is the
296        // regression test for the bug that motivated moving event aging
297        // onto the ordinary PreUpdate schedule instead of a special lane:
298        // double-registering used to age the buffers twice per tick and
299        // silently drop this event before `record` ever saw it.
300        let mut app = App::new()
301            .add_event::<Damage>()
302            .add_event::<Damage>()
303            .insert_resource(Seen::default())
304            .add_system(SystemStage::PreUpdate, record)
305            .add_system(SystemStage::Update, send_once);
306        app.resources.get_mut::<BackendReady>().0 = true;
307
308        app.update(); // tick 1: reader runs before the writer sends this tick
309        assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
310
311        app.update(); // tick 2: reader catches last tick's send, exactly once
312        assert_eq!(app.resources.get::<Seen>().0, vec![7]);
313
314        app.update(); // tick 3: event has aged out
315        assert_eq!(app.resources.get::<Seen>().0, vec![7]);
316    }
317
318    struct Ping(u32);
319
320    fn fire_once(mut commands: Commands, mut sent: Local<bool>) {
321        if !*sent {
322            commands.trigger(Ping(3));
323            *sent = true;
324        }
325    }
326
327    fn on_ping(trigger: Trigger<Ping>, mut seen: Write<Seen>) {
328        seen.0.push(trigger.0);
329    }
330
331    fn on_ping_doubled(trigger: Trigger<Ping>, mut seen: Write<Seen>) {
332        seen.0.push(trigger.0 * 2);
333    }
334
335    #[test]
336    fn observer_fires_the_same_tick_it_is_triggered() {
337        let mut app = App::new()
338            .add_observer(on_ping)
339            .insert_resource(Seen::default())
340            .add_system(SystemStage::Update, fire_once);
341        app.resources.get_mut::<BackendReady>().0 = true;
342
343        app.update();
344        assert_eq!(app.resources.get::<Seen>().0, vec![3]);
345
346        app.update(); // fire_once no longer sends — nothing new triggered
347        assert_eq!(app.resources.get::<Seen>().0, vec![3]);
348    }
349
350    #[test]
351    fn ready_stage_runs_exactly_once_before_the_regular_schedules_that_same_tick() {
352        struct SetupRan;
353
354        fn setup(mut commands: Commands, mut seen: Write<Seen>) {
355            seen.0.push(1);
356            commands.insert_resource(SetupRan);
357        }
358
359        fn depends_on_setup(ran: Option<Read<SetupRan>>, mut seen: Write<Seen>) {
360            if ran.is_some() {
361                seen.0.push(2);
362            }
363        }
364
365        let mut app = App::new()
366            .add_system(SystemStage::Ready, setup)
367            .insert_resource(Seen::default())
368            .add_system(SystemStage::PreUpdate, depends_on_setup);
369
370        // not ready yet — Ready must not run before BackendReady
371        app.update();
372        assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
373
374        app.resources.get_mut::<BackendReady>().0 = true;
375
376        // same tick: setup runs, commands sync, then depends_on_setup
377        // already sees SetupRan — not one tick later
378        app.update();
379        assert_eq!(app.resources.get::<Seen>().0, vec![1, 2]);
380
381        // never runs again
382        app.update();
383        assert_eq!(app.resources.get::<Seen>().0, vec![1, 2, 2]);
384    }
385
386    #[test]
387    fn multiple_observers_for_the_same_event_all_fire() {
388        let mut app = App::new()
389            .add_observer(on_ping)
390            .add_observer(on_ping_doubled)
391            .insert_resource(Seen::default())
392            .add_system(SystemStage::Update, fire_once);
393        app.resources.get_mut::<BackendReady>().0 = true;
394
395        app.update();
396
397        let seen = app.resources.get::<Seen>().0.clone();
398        assert_eq!(seen.len(), 2);
399        assert!(seen.contains(&3));
400        assert!(seen.contains(&6));
401    }
402}