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#[derive(Default)]
19pub struct AppExit(pub bool);
20
21#[derive(Default)]
25pub struct BackendReady(pub bool);
26
27pub 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 pub fn new() -> Self {
75 Self::default()
76 }
77
78 pub fn insert_resource<T: 'static>(mut self, resource: T) -> Self {
80 self.resources.insert(resource);
81 self
82 }
83
84 pub fn remove_resource<T: 'static>(mut self) -> Self {
86 self.resources.remove::<T>();
87 self
88 }
89
90 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 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 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 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 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 pub fn set_runner(mut self, runner: impl FnOnce(App) + 'static) -> Self {
181 self.runner = Some(Box::new(runner));
182 self
183 }
184
185 pub fn with_logging(self) -> Self {
189 tracing_subscriber::fmt().init();
190 self
191 }
192
193 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 pub fn should_exit(&self) -> bool {
219 self.resources.get::<AppExit>().0
220 }
221
222 pub fn run(mut self) {
229 if let Some(mut startup) = self.schedules.remove(&SystemStage::Startup) {
231 startup.run(&mut self.world, &mut self.resources);
232 }
233
234 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 #[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 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(); assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
308
309 app.update(); assert_eq!(app.resources.get::<Seen>().0, vec![7]);
311
312 app.update(); 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(); 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 app.update();
370 assert_eq!(app.resources.get::<Seen>().0, Vec::<u32>::new());
371
372 app.resources.get_mut::<BackendReady>().0 = true;
373
374 app.update();
377 assert_eq!(app.resources.get::<Seen>().0, vec![1, 2]);
378
379 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}