bones_lib/lib.rs
1//! The core bones library.
2
3#![warn(missing_docs)]
4// This cfg_attr is needed because `rustdoc::all` includes lints not supported on stable
5#![cfg_attr(doc, allow(unknown_lints))]
6#![deny(rustdoc::all)]
7
8#[doc(inline)]
9pub use bones_ecs as ecs;
10
11/// Bones lib prelude
12pub mod prelude {
13 pub use crate::{
14 ecs::prelude::*, instant::Instant, reset::*, time::*, Game, GamePlugin, Session,
15 SessionBuilder, SessionCommand, SessionOptions, SessionPlugin, SessionRunner, Sessions,
16 };
17 pub use ustr::{ustr, Ustr, UstrMap, UstrSet};
18}
19
20pub use instant;
21pub mod reset;
22pub mod time;
23
24use std::{collections::VecDeque, fmt::Debug, sync::Arc};
25use tracing::warn;
26
27use crate::prelude::*;
28
29/// Builder type used to create [`Session`]. If using this directly (as opposed to [`Sessions::create_with`]),
30/// it is important to rember to finish session and add to [`Sessions`] with [`SessionBuilder::finish_and_add`].
31pub struct SessionBuilder {
32 /// Name of session
33 pub name: Ustr,
34 /// System stage builder
35 pub stages: SystemStagesBuilder,
36 /// Whether or not this session should have it's systems run.
37 pub active: bool,
38 /// Whether or not this session should be rendered.
39 pub visible: bool,
40 /// The priority of this session relative to other sessions in the [`Game`].
41 pub priority: i32,
42 /// The session runner to use for this session.
43 pub runner: Box<dyn SessionRunner>,
44
45 /// Tracks if builder has been finished, and warn on drop if not finished.
46 finish_guard: FinishGuard,
47}
48
49impl SessionBuilder {
50 /// Create a new [`SessionBuilder`]. Be sure to add it to [`Sessions`] when finished, with [`SessionBuilder::finish_and_add`], or [`Sessions::create`].
51 ///
52 /// # Panics
53 ///
54 /// Panics if the `name.try_into()` cannot convert into [`Ustr`].
55 pub fn new<N: TryInto<Ustr>>(name: N) -> Self
56 where
57 <N as TryInto<Ustr>>::Error: Debug,
58 {
59 let name = name
60 .try_into()
61 .expect("Session name could not be converted into Ustr.");
62 Self {
63 name,
64 stages: default(),
65 active: true,
66 visible: true,
67 priority: 0,
68 runner: Box::new(DefaultSessionRunner),
69 finish_guard: FinishGuard { finished: false },
70 }
71 }
72
73 /// Create a [`SessionBuilder`] to match existing [`Session`], consuming it. This is useful if a [`GamePlugin`] wishes to modify a session created by another plugin.
74 /// The builder is initialized with settings from session, can be further modified, and then added to session to replace existing.
75 pub fn from_existing_session<N: TryInto<Ustr>>(name: N, session: Session) -> Self
76 where
77 <N as TryInto<Ustr>>::Error: Debug,
78 {
79 let name = name
80 .try_into()
81 .expect("Session name could not be converted into Ustr.");
82
83 Self {
84 name,
85 stages: SystemStagesBuilder::from_stages(session.stages),
86 active: session.active,
87 visible: session.visible,
88 priority: session.priority,
89 runner: session.runner,
90 finish_guard: FinishGuard { finished: false },
91 }
92 }
93
94 /// Get the [`SystemStagesBuilder`] (though the stage build functions are also on [`SessionBuilder`] for convenience).
95 pub fn stages(&mut self) -> &mut SystemStagesBuilder {
96 &mut self.stages
97 }
98
99 /// Whether or not session should run systems.
100 pub fn set_active(&mut self, active: bool) -> &mut Self {
101 self.active = active;
102 self
103 }
104
105 /// Whether or not session should be rendered.
106 pub fn set_visible(&mut self, visible: bool) -> &mut Self {
107 self.visible = visible;
108 self
109 }
110
111 /// The priority of this session relative to other sessions in the [`Game`].
112 pub fn set_priority(&mut self, priority: i32) -> &mut Self {
113 self.priority = priority;
114 self
115 }
116
117 /// Insert a resource.
118 ///
119 /// Note: The resource is not actually initialized in World until first step of [`SystemStages`].
120 /// To mutate or inspect a resource inserted by another [`SessionPlugin`] during session build, use [`SessionBuilder::resource_mut`].
121 pub fn insert_resource<T: HasSchema>(&mut self, resource: T) -> &mut Self {
122 self.stages.insert_startup_resource(resource);
123 self
124 }
125
126 /// Insert a resource using default value (if not found). Returns a mutable ref for modification.
127 ///
128 /// Note: The resource is not actually initialized in World until first step of [`SystemStages`].
129 /// To mutate or inspect a resource inserted by another [`SessionPlugin`] during session build, use [`SessionBuilder::resource_mut`].
130 pub fn init_resource<T: HasSchema + Default>(&mut self) -> RefMut<'_, T> {
131 self.stages.init_startup_resource::<T>()
132 }
133
134 /// Get mutable reference to a resource if it exists.
135 pub fn resource_mut<T: HasSchema>(&self) -> Option<RefMut<'_, T>> {
136 self.stages.startup_resource_mut::<T>()
137 }
138
139 /// Add a system that will run only once, before all of the other non-startup systems.
140 /// If wish to reset startup systems during gameplay and run again, can modify [`SessionStarted`] resource in world.
141 pub fn add_startup_system<Args, S>(&mut self, system: S) -> &mut Self
142 where
143 S: IntoSystem<Args, (), (), Sys = StaticSystem<(), ()>>,
144 {
145 self.stages.add_startup_system(system.system());
146 self
147 }
148
149 /// Add a system that will run each frame until it succeeds (returns Some). Runs before all stages. Uses Option to allow for easy usage of `?`.
150 pub fn add_single_success_system<Args, S>(&mut self, system: S) -> &mut Self
151 where
152 S: IntoSystem<Args, (), Option<()>, Sys = StaticSystem<(), Option<()>>>,
153 {
154 self.stages.add_single_success_system(system.system());
155 self
156 }
157
158 /// Add a [`System`] to the stage with the given label.
159 pub fn add_system_to_stage<Args, S>(&mut self, label: impl StageLabel, system: S) -> &mut Self
160 where
161 S: IntoSystem<Args, (), (), Sys = StaticSystem<(), ()>>,
162 {
163 self.stages.add_system_to_stage(label, system);
164 self
165 }
166
167 /// Insert a new stage, before another existing stage
168 pub fn insert_stage_before<L: StageLabel, S: SystemStage + 'static>(
169 &mut self,
170 label: L,
171 stage: S,
172 ) -> &mut SessionBuilder {
173 self.stages.insert_stage_before(label, stage);
174 self
175 }
176
177 /// Insert a new stage, after another existing stage
178 pub fn insert_stage_after<L: StageLabel, S: SystemStage + 'static>(
179 &mut self,
180 label: L,
181 stage: S,
182 ) -> &mut SessionBuilder {
183 self.stages.insert_stage_after(label, stage);
184
185 self
186 }
187
188 /// Set the session runner for this session.
189 pub fn set_session_runner(&mut self, runner: Box<dyn SessionRunner>) {
190 self.runner = runner;
191 }
192
193 /// Install a plugin.
194 pub fn install_plugin(&mut self, plugin: impl SessionPlugin) -> &mut Self {
195 plugin.install(self);
196 self
197 }
198
199 /// Finalize and add to [`Sessions`].
200 ///
201 /// Alternatively, you may directly pass a [`SessionBuilder`] to [`Sessions::create`] to add and finalize.
202 pub fn finish_and_add(mut self, sessions: &mut Sessions) -> &mut Session {
203 let mut session = Session {
204 world: {
205 let mut w = World::default();
206 w.init_resource::<Time>();
207 w
208 },
209 stages: self.stages.finish(),
210 active: self.active,
211 visible: self.visible,
212 priority: self.priority,
213 runner: self.runner,
214 };
215
216 // mark guard as finished to avoid warning on drop of SessionBuilder.
217 self.finish_guard.finished = true;
218
219 // Insert startup resources so if other sessions look for them, they are present.
220 // Startup systems are deferred until first step of stages.
221 session.stages.handle_startup_resources(&mut session.world);
222
223 sessions.add(self.name, session)
224 }
225}
226
227/// Guard for [`SessionBuilder` ensuring we warn if it goes out of scope without being finished and added to [`Sessions`].
228struct FinishGuard {
229 pub finished: bool,
230}
231
232impl Drop for FinishGuard {
233 fn drop(&mut self) {
234 if !self.finished {
235 warn!("`SessionBuilder` went out of scope. This may have been an acccident in building Session.
236 `finish` the session builder, or directly add to Game's `Sessions` to ensure is created and saved.")
237 }
238 }
239}
240
241/// A bones game. This includes all of the game worlds, and systems.
242///
243/// [`Session`] is not allowed to be constructed directly.
244/// See [`Sessions::create`] or [`SessionBuilder`] for creating a new `Session`.
245#[non_exhaustive]
246#[derive(Deref, DerefMut)]
247pub struct Session {
248 /// The ECS world for the core.
249 pub world: World,
250 /// The system stages.
251 #[deref]
252 pub stages: SystemStages,
253 /// Whether or not this session should have it's systems run.
254 pub active: bool,
255 /// Whether or not this session should be rendered.
256 pub visible: bool,
257 /// The priority of this session relative to other sessions in the [`Game`].
258 pub priority: i32,
259 /// The session runner to use for this session.
260 pub runner: Box<dyn SessionRunner>,
261}
262
263impl std::fmt::Debug for Session {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 f.debug_struct("Session")
266 .field("world", &self.world)
267 .field("stages", &self.stages)
268 .field("active", &self.active)
269 .field("visible", &self.visible)
270 .field("priority", &self.priority)
271 .field("runner", &"SessionRunner")
272 .finish()
273 }
274}
275
276impl Session {
277 /// Snapshot the world state.
278 ///
279 /// This is the same as `core.world.clone()`, but it is more explicit.
280 pub fn snapshot(&self) -> World {
281 self.world.clone()
282 }
283
284 /// Restore the world state.
285 ///
286 /// Re-sets the world state to that of the provided `world`, which may or may not have been
287 /// created with [`snapshot()`][Self::snapshot].
288 ///
289 /// This is the same as doing an [`std::mem::swap`] on `self.world`, but it is more explicit.
290 pub fn restore(&mut self, world: &mut World) {
291 std::mem::swap(&mut self.world, world)
292 }
293
294 /// Set the session runner for this session.
295 pub fn set_session_runner(&mut self, runner: Box<dyn SessionRunner>) {
296 self.runner = runner;
297 }
298}
299
300/// Trait for plugins that can be installed into a [`Session`].
301pub trait SessionPlugin {
302 /// Install the plugin into the [`Session`].
303 fn install(self, session: &mut SessionBuilder);
304}
305impl<F: FnOnce(&mut SessionBuilder)> SessionPlugin for F {
306 fn install(self, session: &mut SessionBuilder) {
307 (self)(session)
308 }
309}
310
311/// Trait for plugins that can be installed into a [`Game`].
312pub trait GamePlugin {
313 /// Install the plugin into the [`Game`].
314 fn install(self, game: &mut Game);
315}
316impl<F: FnOnce(&mut Game)> GamePlugin for F {
317 fn install(self, game: &mut Game) {
318 (self)(game)
319 }
320}
321
322/// A session runner is in charge of advancing a [`Session`] simulation.
323pub trait SessionRunner: Sync + Send + 'static {
324 /// Step the simulation once.
325 ///
326 /// It is the responsibility of the session runner to update the [`Time`] resource if necessary.
327 ///
328 /// If no special behavior is desired, the simplest session runner, and the one that is
329 /// implemented by [`DefaultSessionRunner`] is as follows:
330 ///
331 /// ```
332 /// # use bones_lib::prelude::*;
333 /// # struct Example;
334 /// # impl SessionRunner for Example {
335 /// fn step(&mut self, now: Instant, world: &mut World, stages: &mut SystemStages) {
336 /// world.resource_mut::<Time>().update_with_instant(now);
337 /// stages.run(world);
338 ///
339 /// // Not required for runner - but calling this allows [`ResetWorld`] resource to
340 /// // reset world state from gameplay.
341 /// world.handle_world_reset(stages);
342 ///
343 /// }
344 /// fn restart_session(&mut self) {}
345 /// fn disable_local_input(&mut self, disable_input: bool) {}
346 /// # }
347 /// ```
348 fn step(&mut self, now: Instant, world: &mut World, stages: &mut SystemStages);
349
350 /// Restart Session Runner. This should reset accumulated time, inputs, etc.
351 ///
352 /// The expectation is that current players using it may continue to, so something like a network
353 /// socket or player info should persist.
354 fn restart_session(&mut self);
355
356 /// Disable the capture of local input by this session.
357 fn disable_local_input(&mut self, input_disabled: bool);
358}
359
360/// The default [`SessionRunner`], which just runs the systems once every time it is run.
361#[derive(Default)]
362pub struct DefaultSessionRunner;
363impl SessionRunner for DefaultSessionRunner {
364 fn step(&mut self, now: instant::Instant, world: &mut World, stages: &mut SystemStages) {
365 world.resource_mut::<Time>().update_with_instant(now);
366 stages.run(world);
367
368 // Checks if reset of world has been triggered by [`ResetWorld`] and handles a reset.
369 world.handle_world_reset(stages);
370 }
371
372 // This is a no-op as no state, but implemented this way in case that changes later.
373 #[allow(clippy::default_constructed_unit_structs)]
374 fn restart_session(&mut self) {
375 *self = DefaultSessionRunner::default();
376 }
377
378 // `DefaultSessionRunner` does not collect input so this impl is not relevant.
379 fn disable_local_input(&mut self, _input_disabled: bool) {}
380}
381
382/// The [`Game`] encompasses a complete bones game's logic, independent of the renderer and IO
383/// implementations.
384///
385/// Games are made up of one or more [`Session`]s, each of which contains it's own [`World`] and
386/// [`SystemStages`]. These different sessions can be used for parts of the game with independent
387/// states, such as the main menu and the gameplay.
388pub struct Game {
389 /// The sessions that make up the game.
390 pub sessions: Sessions,
391 /// The collection of systems that are associated to the game itself, and not a specific
392 /// session.
393 pub systems: GameSystems,
394 /// List of sorted session keys.
395 ///
396 /// These are only guaranteed to be sorted and up-to-date immediately after calling
397 /// [`Game::step()`].
398 pub sorted_session_keys: Vec<Ustr>,
399 /// Collection of resources that will have a shared instance of each be inserted into each
400 /// session automatically.
401 pub shared_resources: Vec<AtomicUntypedResource>,
402}
403
404impl Default for Game {
405 fn default() -> Self {
406 let mut game = Self {
407 sessions: default(),
408 systems: default(),
409 sorted_session_keys: default(),
410 shared_resources: default(),
411 };
412
413 // Init Sessions shared resource so it exists for game step.
414 // (Game's sessions temporarily moved to this resource during execution)
415 game.init_shared_resource::<Sessions>();
416 game
417 }
418}
419
420impl Game {
421 /// Create an empty game with an asset server.
422 pub fn new() -> Self {
423 Self::default()
424 }
425
426 /// Install a [`GamePlugin`].
427 pub fn install_plugin<P: GamePlugin>(&mut self, plugin: P) -> &mut Self {
428 plugin.install(self);
429 self
430 }
431 #[track_caller]
432 /// Get the shared resource of a given type out of this [`Game`]s shared
433 /// resources if it exists.
434 pub fn get_shared_resource<T: HasSchema>(&self) -> Option<Ref<'_, T>> {
435 let res = self
436 .shared_resources
437 .iter()
438 .find(|x| x.schema() == T::schema())?;
439 let borrow = res.borrow();
440
441 if borrow.is_some() {
442 // SOUND: We know the type matches T
443 Some(Ref::map(borrow, |b| unsafe {
444 b.as_ref().unwrap().as_ref().cast_into_unchecked()
445 }))
446 } else {
447 None
448 }
449 }
450
451 #[track_caller]
452 /// Get the mutable shared resource of a given type out of this [`Game`]s
453 /// shared resources if it exists.
454 pub fn get_shared_resource_mut<T: HasSchema>(&self) -> Option<RefMut<'_, T>> {
455 let res = self
456 .shared_resources
457 .iter()
458 .find(|x| x.schema() == T::schema())?;
459 let borrow = res.borrow_mut();
460
461 if borrow.is_some() {
462 // SOUND: We know the type matches T
463 Some(RefMut::map(borrow, |b| unsafe {
464 b.as_mut().unwrap().as_mut().cast_into_mut_unchecked()
465 }))
466 } else {
467 None
468 }
469 }
470
471 #[track_caller]
472 /// Get the shared resource of a given type out of this [`Game`]s shared
473 /// resources. Panics if the resource doesn't exist.
474 pub fn shared_resource<T: HasSchema>(&self) -> Ref<'_, T> {
475 self.get_shared_resource()
476 .expect("shared resource not found")
477 }
478
479 #[track_caller]
480 /// Get the mutable shared resource of a given type out of this [`Game`]s
481 /// shared resources. Panics if it doesn't exist.
482 pub fn shared_resource_mut<T: HasSchema>(&self) -> RefMut<'_, T> {
483 self.get_shared_resource_mut()
484 .expect("shared resource not found")
485 }
486
487 /// Get the shared resource cell of a given type out of this [`Game`]s shared resources.
488 pub fn shared_resource_cell<T: HasSchema>(&self) -> Option<AtomicResource<T>> {
489 let res = self
490 .shared_resources
491 .iter()
492 .find(|x| x.schema() == T::schema())?;
493 Some(AtomicResource::from_untyped(res.clone()).unwrap())
494 }
495
496 /// Initialize a resource that will be shared across game sessions using it's [`Default`] value
497 /// if it is not already initialized, and borrow it for modification.
498 pub fn init_shared_resource<T: HasSchema + Default>(&mut self) -> RefMut<'_, T> {
499 if !self
500 .shared_resources
501 .iter()
502 .any(|x| x.schema() == T::schema())
503 {
504 self.insert_shared_resource(T::default());
505 }
506 self.shared_resource_mut::<T>()
507 }
508
509 /// Insert a resource that will be shared across all game sessions.
510 ///
511 /// > **Note:** This resource will only be visible in sessions that have not already
512 /// > initialized or access a resource of the same type locally.
513 pub fn insert_shared_resource<T: HasSchema>(&mut self, resource: T) {
514 // Update an existing resource of the same type.
515 for r in &mut self.shared_resources {
516 if r.schema() == T::schema() {
517 let mut borrow = r.borrow_mut();
518
519 if let Some(b) = borrow.as_mut().as_mut() {
520 *b.cast_mut() = resource;
521 } else {
522 *borrow = Some(SchemaBox::new(resource))
523 }
524 return;
525 }
526 }
527
528 // Or insert a new resource if we couldn't find one
529 self.shared_resources
530 .push(Arc::new(UntypedResource::new(SchemaBox::new(resource))));
531 }
532
533 // /// Remove a shared resource, if it is present in the world.
534 // /// # Panics
535 // /// Panics if the resource is set and it's cell has another handle to it and cannot be
536 // /// unwrapped.
537 // pub fn remove_shared_resource<T: HasSchema>(&mut self) -> Option<T> {
538 // self.shared_resources
539 // .iter()
540 // .position(|x| x.schema() == T::schema())
541 // .map(|idx| {
542 // self.shared_resources
543 // .remove(idx)
544 // .try_into_inner()
545 // .unwrap()
546 // .into_inner()
547 // })
548 // }
549
550 /// Step the game simulation.
551 pub fn step(&mut self, now: instant::Instant) {
552 // Pull out the game systems so that we can run them on the game
553 let mut game_systems = std::mem::take(&mut self.systems);
554
555 // Run game startup systems
556 if !game_systems.has_run_startup {
557 for system in &mut game_systems.startup {
558 system(self);
559 }
560 game_systems.has_run_startup = true;
561 }
562
563 // Run the before systems
564 for system in &mut game_systems.before {
565 system(self)
566 }
567
568 // Sort session keys by priority
569 self.sorted_session_keys.clear();
570 self.sorted_session_keys.extend(self.sessions.map.keys());
571 self.sorted_session_keys
572 .sort_by_key(|name| self.sessions.map.get(name).unwrap().priority);
573
574 // For every session
575 for session_name in self.sorted_session_keys.clone() {
576 // Extract the current session
577 let Some(mut current_session) = self.sessions.map.remove(&session_name) else {
578 // This may happen if the session was deleted by another session.
579 continue;
580 };
581
582 // If this session is active
583 let options = if current_session.active {
584 // Run any before session game systems
585 if let Some(systems) = game_systems.before_session.get_mut(&session_name) {
586 for system in systems {
587 system(self)
588 }
589 }
590
591 // Make sure session contains all of the shared resources
592 for r in &self.shared_resources {
593 if !current_session
594 .world
595 .resources
596 .untyped()
597 .contains_cell(r.schema().id())
598 {
599 current_session
600 .world
601 .resources
602 .untyped()
603 .insert_cell(r.clone())
604 .unwrap();
605 }
606 }
607
608 // Insert the session options
609 current_session.world.resources.insert(SessionOptions {
610 active: true,
611 delete: false,
612 visible: current_session.visible,
613 });
614
615 // Insert the other sessions into the current session's world
616 {
617 let mut sessions = current_session.world.resource_mut::<Sessions>();
618 std::mem::swap(&mut *sessions, &mut self.sessions);
619 }
620
621 // Step the current session's simulation using it's session runner
622 current_session.runner.step(
623 now,
624 &mut current_session.world,
625 &mut current_session.stages,
626 );
627
628 // Pull the sessions back out of the world
629 {
630 let mut sessions = current_session.world.resource_mut::<Sessions>();
631 std::mem::swap(&mut *sessions, &mut self.sessions);
632 }
633
634 // Pull the current session options back out of the world.
635 *current_session.world.resource::<SessionOptions>()
636 } else {
637 SessionOptions {
638 active: false,
639 visible: current_session.visible,
640 delete: false,
641 }
642 };
643
644 // Delete the session
645 if options.delete {
646 let session_idx = self
647 .sorted_session_keys
648 .iter()
649 .position(|x| x == &session_name)
650 .unwrap();
651 self.sorted_session_keys.remove(session_idx);
652
653 // Update session options
654 } else {
655 current_session.active = options.active;
656 current_session.visible = options.visible;
657
658 // Insert the current session back into the session list
659 self.sessions.map.insert(session_name, current_session);
660 }
661
662 // Run any after session game systems
663 if let Some(systems) = game_systems.after_session.get_mut(&session_name) {
664 for system in systems {
665 system(self)
666 }
667 }
668 }
669
670 // Execute Session Commands
671 {
672 let mut session_commands: VecDeque<Box<SessionCommand>> = default();
673 std::mem::swap(&mut session_commands, &mut self.sessions.commands);
674 for command in session_commands.drain(..) {
675 command(&mut self.sessions);
676 }
677 }
678
679 // Run after systems
680 for system in &mut game_systems.after {
681 system(self)
682 }
683
684 // Replace the game systems
685 self.systems = game_systems;
686
687 // Make sure sorted session keys does not include sessions that were deleted during
688 // execution by other sessions.
689 self.sorted_session_keys
690 .retain(|x| self.sessions.iter().any(|(id, _)| id == x));
691 }
692}
693
694/// A system that runs directly on a [`Game`] instead of in a specific [`Session`].
695pub type GameSystem = Box<dyn FnMut(&mut Game) + Sync + Send>;
696
697/// A collection of systems associated directly to a [`Game`] as opposed to a [`Session`].
698#[derive(Default)]
699pub struct GameSystems {
700 /// Flag which indicates whether or not the startup systems have been run yet.
701 pub has_run_startup: bool,
702 /// Startup systems.
703 pub startup: Vec<GameSystem>,
704 /// Game systems that are run before sessions are run.
705 pub before: Vec<GameSystem>,
706 /// Game systems that are run after sessions are run.
707 pub after: Vec<GameSystem>,
708 /// Game systems that are run after a specific session is run.
709 pub after_session: HashMap<Ustr, Vec<GameSystem>>,
710 /// Game systems that are run before a specific session is run.
711 pub before_session: HashMap<Ustr, Vec<GameSystem>>,
712}
713
714impl GameSystems {
715 /// Add a system that will run only once, before all of the other non-startup systems.
716 pub fn add_startup_system<F>(&mut self, system: F) -> &mut Self
717 where
718 F: FnMut(&mut Game) + Sync + Send + 'static,
719 {
720 self.startup.push(Box::new(system));
721 self
722 }
723
724 /// Add a system that will run on every step, before all of the sessions are run.
725 pub fn add_before_system<F>(&mut self, system: F) -> &mut Self
726 where
727 F: FnMut(&mut Game) + Sync + Send + 'static,
728 {
729 self.before.push(Box::new(system));
730 self
731 }
732
733 /// Add a system that will run on every step, after all of the sessions are run.
734 pub fn add_after_system<F>(&mut self, system: F) -> &mut Self
735 where
736 F: FnMut(&mut Game) + Sync + Send + 'static,
737 {
738 self.after.push(Box::new(system));
739 self
740 }
741
742 /// Add a system that will run every time the named session is run, before the session is run.
743 pub fn add_before_session_system<F>(&mut self, session: &str, system: F) -> &mut Self
744 where
745 F: FnMut(&mut Game) + Sync + Send + 'static,
746 {
747 self.before_session
748 .entry(session.into())
749 .or_default()
750 .push(Box::new(system));
751 self
752 }
753
754 /// Add a system that will run every time the named session is run, after the session is run.
755 pub fn add_after_session_system<F>(&mut self, session: &str, system: F) -> &mut Self
756 where
757 F: FnMut(&mut Game) + Sync + Send + 'static,
758 {
759 self.after_session
760 .entry(session.into())
761 .or_default()
762 .push(Box::new(system));
763 self
764 }
765}
766
767/// Type of session command
768pub type SessionCommand = dyn FnOnce(&mut Sessions) + Sync + Send;
769
770/// Container for multiple game sessions.
771///
772/// Each session shares the same [`Entities`].
773#[derive(HasSchema, Default)]
774#[schema(no_clone)]
775pub struct Sessions {
776 map: UstrMap<Session>,
777
778 /// Commands that operate on [`Sessions`], called after all sessions update.
779 /// These may be used to add/delete/modify sessions.
780 ///
781 /// Commands are useful in a situation where you want to remove / recreate
782 /// a session from within it's own system. You cannot do this while the `Session` is running.
783 ///
784 /// Commands added inside a session command will not be executed until next frame.
785 commands: VecDeque<Box<SessionCommand>>,
786}
787
788/// Resource that allows you to configure the current session.
789#[derive(HasSchema, Default, Debug, Clone, Copy)]
790#[repr(C)]
791pub struct SessionOptions {
792 /// Whether or not this session should be active after this frame.
793 pub active: bool,
794 /// Whether or not this session should be visible.
795 pub visible: bool,
796 /// Whether or not this session should be deleted.
797 pub delete: bool,
798}
799
800impl Sessions {
801 /// Create a new session from [`SessionBuilder`], insert into [`Sessions`], and borrow it mutably so it can be modified.
802 /// If session with same name already exists, it will be replaced.
803 pub fn create(&mut self, builder: SessionBuilder) -> &mut Session {
804 builder.finish_and_add(self)
805 }
806
807 /// Create a new session from default [`SessionBuilder`], and modify in closure before it is added to [`Sessions`]. Then borrow it mutably so it can be modified.
808 ///
809 /// If session with same name already exists, it will be replaced.
810 pub fn create_with<N: TryInto<Ustr>>(
811 &mut self,
812 name: N,
813 plugin: impl SessionPlugin,
814 ) -> &mut Session
815 where
816 <N as TryInto<Ustr>>::Error: Debug,
817 {
818 let mut builder = SessionBuilder::new(name);
819 builder.install_plugin(plugin);
820 builder.finish_and_add(self)
821 }
822
823 /// Replaces an existing session after converting it into a [`SessionBuilder`] with same systems, stages, etc. It then is given to `build_function` to extend or modify it.
824 ///
825 /// WARNING - this is intended to allow a [`GamePlugin`] to extend a session created by another plugin during setup. It destroys and re-creates session, and if used during gameplay,
826 /// could break assumptions around determinism and cause desyncs in network play. The sessions [`World`] is not preserved. This is a utility for constructing session, not for runtime mutation.
827 ///
828 /// `None` is returned if the session with name was not found.
829 pub fn modify_and_replace_existing_session<N: TryInto<Ustr>>(
830 &mut self,
831 name: N,
832 build_function: impl FnOnce(&mut SessionBuilder),
833 ) -> Option<&mut Session>
834 where
835 <N as TryInto<Ustr>>::Error: Debug,
836 {
837 let name = name
838 .try_into()
839 .expect("Session name could not be converted into Ustr.");
840
841 let session = self.map.remove(&name)?;
842 let mut builder = SessionBuilder::from_existing_session(name, session);
843 build_function(&mut builder);
844 Some(builder.finish_and_add(self))
845 }
846
847 /// Delete a session.
848 #[track_caller]
849 pub fn delete<K: TryInto<Ustr>>(&mut self, name: K)
850 where
851 <K as TryInto<Ustr>>::Error: Debug,
852 {
853 self.map.remove(&name.try_into().unwrap());
854 }
855
856 /// Borrow a session from the sessions list.
857 #[track_caller]
858 pub fn get<K: TryInto<Ustr>>(&self, name: K) -> Option<&Session>
859 where
860 <K as TryInto<Ustr>>::Error: Debug,
861 {
862 self.map.get(&name.try_into().unwrap())
863 }
864
865 /// Borrow a session from the sessions list.
866 #[track_caller]
867 pub fn get_mut<K: TryInto<Ustr>>(&mut self, name: K) -> Option<&mut Session>
868 where
869 <K as TryInto<Ustr>>::Error: Debug,
870 {
871 self.map.get_mut(&name.try_into().unwrap())
872 }
873
874 /// Mutably iterate over sessions.
875 pub fn iter_mut(&mut self) -> std::collections::hash_map::IterMut<'_, Ustr, Session> {
876 self.map.iter_mut()
877 }
878
879 /// Iterate over sessions.
880 pub fn iter(&self) -> std::collections::hash_map::Iter<'_, Ustr, Session> {
881 self.map.iter()
882 }
883
884 /// Add a [`SessionCommand`] to queue.
885 pub fn add_command(&mut self, command: Box<SessionCommand>) {
886 self.commands.push_back(command);
887 }
888
889 /// Add a [`Session`] to [`Sessions`]. This function is private, used by [`SessionBuilder`] to save the finished `Session`,
890 /// and is not directly useful to user as a `Session` may not be directly constructed.
891 ///
892 /// To build a new session, see [`Sessions::create`] or [`Sessions::create_with`].
893 fn add(&mut self, name: Ustr, session: Session) -> &mut Session {
894 // Insert it into the map
895 self.map.insert(name, session);
896
897 // And borrow it for the modification
898 self.map.get_mut(&name).unwrap()
899 }
900}