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, 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    /// Registers `system` to run on `stage`, every tick that stage runs.
111    /// See [`SystemStage`] for what each stage is for and when it runs.
112    /// `system` may be a bare system, or one wrapped with
113    /// `.after(...)`/`.before(...)` to order it relative to another system
114    /// on the same stage — see
115    /// [`IntoSystemConfig`](crate::ecs::system_param::IntoSystemConfig).
116    pub fn add_system<S, Params>(
117        mut self,
118        stage: SystemStage,
119        system: impl Into<SystemConfig<S, Params>>,
120    ) -> Self
121    where
122        Params: 'static,
123        S: IntoSystem<Params> + 'static,
124    {
125        Self::add_system_to(&mut self.schedules, stage, system);
126        self
127    }
128
129    pub(crate) fn add_gpu_system<S, Params>(
130        mut self,
131        stage: SystemStage,
132        system: impl Into<SystemConfig<S, Params>>,
133    ) -> Self
134    where
135        Params: 'static,
136        S: IntoSystem<Params> + 'static,
137    {
138        Self::add_system_to(&mut self.gpu_schedules, stage, system);
139        self
140    }
141
142    /// Registers event type `T`, making [`EventReader<T>`](crate::ecs::events::EventReader)/
143    /// [`EventWriter<T>`](crate::ecs::events::EventWriter) usable as system
144    /// parameters. Idempotent — calling this twice for the same `T` (e.g.
145    /// from two different plugins that both want it) is a no-op the second
146    /// time, not a double registration.
147    pub fn add_event<T: 'static + Send + Sync>(mut self) -> Self {
148        if !self.resources.contains::<Events<T>>() {
149            self.resources.insert(Events::<T>::default());
150            self = self.add_system(SystemStage::PreUpdate, age_events::<T>);
151        }
152        self
153    }
154
155    /// Registers `observer` to run whenever [`Commands::trigger`](crate::ecs::commands::Commands::trigger)
156    /// sends an `E`, once the current stage finishes syncing (same tick,
157    /// not deferred to the next one). Multiple observers can be registered
158    /// for the same `E` — every one of them runs.
159    pub fn add_observer<E: 'static + Send + Sync, Params: 'static>(
160        mut self,
161        observer: impl IntoObserverSystem<E, Params> + 'static,
162    ) -> Self {
163        if !self.resources.contains::<Observers<E>>() {
164            self.resources.insert(Observers::<E>::default());
165        }
166        self.resources.get_mut::<Observers<E>>().0.push(Box::new(observer.into_observer_system()));
167        self
168    }
169
170    /// Overrides how the main loop is driven — e.g. a windowing plugin
171    /// installs one that hands control to its own event loop instead of
172    /// the default headless polling loop.
173    pub fn set_runner(mut self, runner: impl FnOnce(App) + 'static) -> Self {
174        self.runner = Some(Box::new(runner));
175        self
176    }
177
178    /// Initializes a `tracing_subscriber` formatter so `tracing::info!`/
179    /// `warn!`/`error!` calls made throughout the engine actually print
180    /// somewhere.
181    pub fn with_logging(self) -> Self {
182        tracing_subscriber::fmt().init();
183        self
184    }
185
186    /// Runs every schedule for a single tick: while the GPU backend isn't
187    /// ready yet, only the internal `gpu_schedules` run; once it is,
188    /// [`SystemStage::Ready`] runs (if anything is still registered there,
189    /// exactly once ever), then every other stage runs in order. Called
190    /// automatically by the default loop in [`App::run`] — call it
191    /// yourself only if you're driving the loop from somewhere else (e.g.
192    /// inside a custom runner installed via [`App::set_runner`]).
193    pub fn update(&mut self) {
194        if !self.resources.get::<BackendReady>().0 {
195            println!("Backend Pending");
196            for (_, schedule) in self.gpu_schedules.iter_mut() {
197                schedule.run(&mut self.world, &mut self.resources);
198            }
199            return;
200        }
201
202        println!("Backend Obtained, Running Systems");
203        if let Some(mut ready) = self.schedules.remove(&SystemStage::Ready) {
204            ready.run(&mut self.world, &mut self.resources);
205        }
206        for (_, schedule) in self.schedules.iter_mut() {
207            schedule.run(&mut self.world, &mut self.resources);
208        }
209    }
210
211    /// `true` once [`AppExit`] has been set — the default loop in
212    /// [`App::run`] checks this after every tick.
213    pub fn should_exit(&self) -> bool {
214        self.resources.get::<AppExit>().0
215    }
216
217    /// Consumes the app and runs it. [`SystemStage::Startup`] runs first,
218    /// exactly once, before anything else. Then, if a runner was installed
219    /// (e.g. by a windowing plugin via [`App::set_runner`]), control is
220    /// handed to it — this call doesn't return until that runner decides
221    /// to stop. Otherwise, falls back to a default headless loop that
222    /// calls [`App::update`] repeatedly until [`App::should_exit`].
223    pub fn run(mut self) {
224        // startup schedules run exactly once, before the main loop
225        if let Some(mut startup) = self.schedules.remove(&SystemStage::Startup) {
226            startup.run(&mut self.world, &mut self.resources);
227        }
228
229        // a windowing plugin (e.g. WindowPlugin) hands control to its own
230        // event loop instead of the default headless polling loop below
231        if let Some(runner) = self.runner.take() {
232            runner(self);
233            return;
234        }
235
236        loop {
237            let was_ready = self.resources.get::<BackendReady>().0;
238            self.update();
239
240            if !was_ready {
241                // no real OS thread to sleep on wasm32 — just busy-poll.
242                // Only reached at all when an app never registers a
243                // windowing plugin, which always overrides this runner.
244                #[cfg(not(target_arch = "wasm32"))]
245                std::thread::sleep(std::time::Duration::from_millis(16));
246                continue;
247            }
248
249            if self.should_exit() {
250                break;
251            }
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::ecs::{
260        commands::Commands,
261        events::{EventReader, EventWriter},
262        local::Local,
263        observers::Trigger,
264        resources::{Read, Write},
265    };
266
267    struct Damage(u32);
268
269    #[derive(Default)]
270    struct Seen(Vec<u32>);
271
272    fn send_once(mut writer: EventWriter<Damage>, mut sent: Local<bool>) {
273        if !*sent {
274            writer.send(Damage(7));
275            *sent = true;
276        }
277    }
278
279    fn record(mut reader: EventReader<Damage>, mut seen: Write<Seen>) {
280        for event in reader.iter() {
281            seen.0.push(event.0);
282        }
283    }
284
285    #[test]
286    fn add_event_called_twice_still_delivers_exactly_once_one_tick_later() {
287        // Registering the same event type twice (e.g. two plugins both
288        // wanting `Damage`) must be a no-op the second time — this is the
289        // regression test for the bug that motivated moving event aging
290        // onto the ordinary PreUpdate schedule instead of a special lane:
291        // double-registering used to age the buffers twice per tick and
292        // silently drop this event before `record` ever saw it.
293        let mut app = App::new()
294            .add_event::<Damage>()
295            .add_event::<Damage>()
296            .insert_resource(Seen::default())
297            .add_system(SystemStage::PreUpdate, record)
298            .add_system(SystemStage::Update, send_once);
299        app.resources.get_mut::<BackendReady>().0 = true;
300
301        app.update(); // tick 1: reader runs before the writer sends this tick
302        assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
303
304        app.update(); // tick 2: reader catches last tick's send, exactly once
305        assert_eq!(app.resources.get::<Seen>().0, vec![7]);
306
307        app.update(); // tick 3: event has aged out
308        assert_eq!(app.resources.get::<Seen>().0, vec![7]);
309    }
310
311    struct Ping(u32);
312
313    fn fire_once(mut commands: Commands, mut sent: Local<bool>) {
314        if !*sent {
315            commands.trigger(Ping(3));
316            *sent = true;
317        }
318    }
319
320    fn on_ping(trigger: Trigger<Ping>, mut seen: Write<Seen>) {
321        seen.0.push(trigger.0);
322    }
323
324    fn on_ping_doubled(trigger: Trigger<Ping>, mut seen: Write<Seen>) {
325        seen.0.push(trigger.0 * 2);
326    }
327
328    #[test]
329    fn observer_fires_the_same_tick_it_is_triggered() {
330        let mut app = App::new()
331            .add_observer(on_ping)
332            .insert_resource(Seen::default())
333            .add_system(SystemStage::Update, fire_once);
334        app.resources.get_mut::<BackendReady>().0 = true;
335
336        app.update();
337        assert_eq!(app.resources.get::<Seen>().0, vec![3]);
338
339        app.update(); // fire_once no longer sends — nothing new triggered
340        assert_eq!(app.resources.get::<Seen>().0, vec![3]);
341    }
342
343    #[test]
344    fn ready_stage_runs_exactly_once_before_the_regular_schedules_that_same_tick() {
345        struct SetupRan;
346
347        fn setup(mut commands: Commands, mut seen: Write<Seen>) {
348            seen.0.push(1);
349            commands.insert_resource(SetupRan);
350        }
351
352        fn depends_on_setup(ran: Option<Read<SetupRan>>, mut seen: Write<Seen>) {
353            if ran.is_some() {
354                seen.0.push(2);
355            }
356        }
357
358        let mut app = App::new()
359            .add_system(SystemStage::Ready, setup)
360            .insert_resource(Seen::default())
361            .add_system(SystemStage::PreUpdate, depends_on_setup);
362
363        // not ready yet — Ready must not run before BackendReady
364        app.update();
365        assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
366
367        app.resources.get_mut::<BackendReady>().0 = true;
368
369        // same tick: setup runs, commands sync, then depends_on_setup
370        // already sees SetupRan — not one tick later
371        app.update();
372        assert_eq!(app.resources.get::<Seen>().0, vec![1, 2]);
373
374        // never runs again
375        app.update();
376        assert_eq!(app.resources.get::<Seen>().0, vec![1, 2, 2]);
377    }
378
379    #[test]
380    fn multiple_observers_for_the_same_event_all_fire() {
381        let mut app = App::new()
382            .add_observer(on_ping)
383            .add_observer(on_ping_doubled)
384            .insert_resource(Seen::default())
385            .add_system(SystemStage::Update, fire_once);
386        app.resources.get_mut::<BackendReady>().0 = true;
387
388        app.update();
389
390        let seen = app.resources.get::<Seen>().0.clone();
391        assert_eq!(seen.len(), 2);
392        assert!(seen.contains(&3));
393        assert!(seen.contains(&6));
394    }
395}