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