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            for (_, schedule) in self.gpu_schedules.iter_mut() {
203                schedule.run(&mut self.world, &mut self.resources);
204            }
205            return;
206        }
207
208        if let Some(mut ready) = self.schedules.remove(&SystemStage::Ready) {
209            ready.run(&mut self.world, &mut self.resources);
210        }
211        for (_, schedule) in self.schedules.iter_mut() {
212            schedule.run(&mut self.world, &mut self.resources);
213        }
214    }
215
216    /// `true` once [`AppExit`] has been set — the default loop in
217    /// [`App::run`] checks this after every tick.
218    pub fn should_exit(&self) -> bool {
219        self.resources.get::<AppExit>().0
220    }
221
222    /// Consumes the app and runs it. [`SystemStage::Startup`] runs first,
223    /// exactly once, before anything else. Then, if a runner was installed
224    /// (e.g. by a windowing plugin via [`App::set_runner`]), control is
225    /// handed to it — this call doesn't return until that runner decides
226    /// to stop. Otherwise, falls back to a default headless loop that
227    /// calls [`App::update`] repeatedly until [`App::should_exit`].
228    pub fn run(mut self) {
229        // startup schedules run exactly once, before the main loop
230        if let Some(mut startup) = self.schedules.remove(&SystemStage::Startup) {
231            startup.run(&mut self.world, &mut self.resources);
232        }
233
234        // a windowing plugin (e.g. WindowPlugin) hands control to its own
235        // event loop instead of the default headless polling loop below
236        if let Some(runner) = self.runner.take() {
237            runner(self);
238            return;
239        }
240
241        loop {
242            let was_ready = self.resources.get::<BackendReady>().0;
243            self.update();
244
245            if !was_ready {
246                // no real OS thread to sleep on wasm32 — just busy-poll.
247                // Only reached at all when an app never registers a
248                // windowing plugin, which always overrides this runner.
249                #[cfg(not(target_arch = "wasm32"))]
250                std::thread::sleep(std::time::Duration::from_millis(16));
251                continue;
252            }
253
254            if self.should_exit() {
255                break;
256            }
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::ecs::{
265        commands::Commands,
266        events::{EventReader, EventWriter},
267        local::Local,
268        observers::Trigger,
269        resources::{Read, Write},
270    };
271
272    struct Damage(u32);
273
274    #[derive(Default)]
275    struct Seen(Vec<u32>);
276
277    fn send_once(mut writer: EventWriter<Damage>, mut sent: Local<bool>) {
278        if !*sent {
279            writer.send(Damage(7));
280            *sent = true;
281        }
282    }
283
284    fn record(mut reader: EventReader<Damage>, mut seen: Write<Seen>) {
285        for event in reader.iter() {
286            seen.0.push(event.0);
287        }
288    }
289
290    #[test]
291    fn add_event_called_twice_still_delivers_exactly_once_one_tick_later() {
292        // Registering the same event type twice (e.g. two plugins both
293        // wanting `Damage`) must be a no-op the second time — this is the
294        // regression test for the bug that motivated moving event aging
295        // onto the ordinary PreUpdate schedule instead of a special lane:
296        // double-registering used to age the buffers twice per tick and
297        // silently drop this event before `record` ever saw it.
298        let mut app = App::new()
299            .add_event::<Damage>()
300            .add_event::<Damage>()
301            .insert_resource(Seen::default())
302            .add_system(SystemStage::PreUpdate, record)
303            .add_system(SystemStage::Update, send_once);
304        app.resources.get_mut::<BackendReady>().0 = true;
305
306        app.update(); // tick 1: reader runs before the writer sends this tick
307        assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
308
309        app.update(); // tick 2: reader catches last tick's send, exactly once
310        assert_eq!(app.resources.get::<Seen>().0, vec![7]);
311
312        app.update(); // tick 3: event has aged out
313        assert_eq!(app.resources.get::<Seen>().0, vec![7]);
314    }
315
316    struct Ping(u32);
317
318    fn fire_once(mut commands: Commands, mut sent: Local<bool>) {
319        if !*sent {
320            commands.trigger(Ping(3));
321            *sent = true;
322        }
323    }
324
325    fn on_ping(trigger: Trigger<Ping>, mut seen: Write<Seen>) {
326        seen.0.push(trigger.0);
327    }
328
329    fn on_ping_doubled(trigger: Trigger<Ping>, mut seen: Write<Seen>) {
330        seen.0.push(trigger.0 * 2);
331    }
332
333    #[test]
334    fn observer_fires_the_same_tick_it_is_triggered() {
335        let mut app = App::new()
336            .add_observer(on_ping)
337            .insert_resource(Seen::default())
338            .add_system(SystemStage::Update, fire_once);
339        app.resources.get_mut::<BackendReady>().0 = true;
340
341        app.update();
342        assert_eq!(app.resources.get::<Seen>().0, vec![3]);
343
344        app.update(); // fire_once no longer sends — nothing new triggered
345        assert_eq!(app.resources.get::<Seen>().0, vec![3]);
346    }
347
348    #[test]
349    fn ready_stage_runs_exactly_once_before_the_regular_schedules_that_same_tick() {
350        struct SetupRan;
351
352        fn setup(mut commands: Commands, mut seen: Write<Seen>) {
353            seen.0.push(1);
354            commands.insert_resource(SetupRan);
355        }
356
357        fn depends_on_setup(ran: Option<Read<SetupRan>>, mut seen: Write<Seen>) {
358            if ran.is_some() {
359                seen.0.push(2);
360            }
361        }
362
363        let mut app = App::new()
364            .add_system(SystemStage::Ready, setup)
365            .insert_resource(Seen::default())
366            .add_system(SystemStage::PreUpdate, depends_on_setup);
367
368        // not ready yet — Ready must not run before BackendReady
369        app.update();
370        assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
371
372        app.resources.get_mut::<BackendReady>().0 = true;
373
374        // same tick: setup runs, commands sync, then depends_on_setup
375        // already sees SetupRan — not one tick later
376        app.update();
377        assert_eq!(app.resources.get::<Seen>().0, vec![1, 2]);
378
379        // never runs again
380        app.update();
381        assert_eq!(app.resources.get::<Seen>().0, vec![1, 2, 2]);
382    }
383
384    #[test]
385    fn multiple_observers_for_the_same_event_all_fire() {
386        let mut app = App::new()
387            .add_observer(on_ping)
388            .add_observer(on_ping_doubled)
389            .insert_resource(Seen::default())
390            .add_system(SystemStage::Update, fire_once);
391        app.resources.get_mut::<BackendReady>().0 = true;
392
393        app.update();
394
395        let seen = app.resources.get::<Seen>().0.clone();
396        assert_eq!(seen.len(), 2);
397        assert!(seen.contains(&3));
398        assert!(seen.contains(&6));
399    }
400}