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        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/// Contains a bones [`World`] for state, [`SystemStages`] for behavior, and a
30/// [`SessionRunner`] to determine how those are used.
31// TODO: add network safety docs for `Session`.
32#[non_exhaustive]
33#[derive(Deref, DerefMut)]
34pub struct Session {
35    /// The ECS world for the core.
36    pub world: World,
37    /// The system stages.
38    #[deref]
39    pub stages: SystemStages,
40    /// Whether or not this session should have it's systems run.
41    pub active: bool,
42    /// Whether or not this session should be rendered.
43    pub visible: bool,
44    /// The priority of this session relative to other sessions in the [`Game`].
45    pub priority: i32,
46    /// The session runner to use for this session.
47    pub runner: Box<dyn SessionRunner>,
48}
49
50impl Default for Session {
51    fn default() -> Self {
52        Self {
53            world: World::default(),
54            stages: default(),
55            active: true,
56            visible: true,
57            priority: 0,
58            runner: Box::new(DefaultSessionRunner),
59        }
60    }
61}
62
63impl Session {
64    /// Set the priority of this session relative to other sessions in the [`Game`].
65    pub fn priority(&mut self, priority: i32) -> &mut Self {
66        self.priority = priority;
67        self
68    }
69    /// Set the [`SessionRunner`] to use for this session.
70    pub fn runner(&mut self, runner: Box<dyn SessionRunner>) -> &mut Self {
71        self.runner = runner;
72        self
73    }
74    /// Set whether or not this session should have it's systems run.
75    pub fn activity(&mut self, active: bool) -> &mut Self {
76        self.active = active;
77        self
78    }
79    /// Set whether or not this session should be rendered.
80    pub fn visibility(&mut self, visible: bool) -> &mut Self {
81        self.visible = visible;
82        self
83    }
84    /// Set this session to be rendered.
85    pub fn visible(&mut self) -> &mut Self {
86        self.visible = true;
87        self
88    }
89    /// Set this session to not be rendered.
90    pub fn invisible(&mut self) -> &mut Self {
91        self.visible = false;
92        self
93    }
94    /// Set this session to have it's systems run.
95    pub fn active(&mut self) -> &mut Self {
96        self.active = true;
97        self
98    }
99    /// Set this session to not have it's systems run.
100    pub fn inactive(&mut self) -> &mut Self {
101        self.active = false;
102        self
103    }
104    /// Install a [`SessionPlugin`].
105    pub fn install_plugin(&mut self, plugin: impl SessionPlugin) -> &mut Self {
106        plugin.install(self);
107        self
108    }
109}
110
111impl std::fmt::Debug for Session {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.debug_struct("Session")
114            .field("world", &self.world)
115            .field("stages", &self.stages)
116            .field("active", &self.active)
117            .field("visible", &self.visible)
118            .field("priority", &self.priority)
119            .field("runner", &"SessionRunner")
120            .finish()
121    }
122}
123
124/// Trait for plugins that can be installed into a [`Session`].
125pub trait SessionPlugin {
126    /// Install the plugin into the [`Session`].
127    fn install(self, session: &mut Session);
128}
129impl<F: FnOnce(&mut Session)> SessionPlugin for F {
130    fn install(self, session: &mut Session) {
131        (self)(session)
132    }
133}
134
135/// Trait for plugins that can be installed into a [`Game`].
136pub trait GamePlugin {
137    /// Install the plugin into the [`Game`].
138    fn install(self, game: &mut Game);
139}
140impl<F: FnOnce(&mut Game)> GamePlugin for F {
141    fn install(self, game: &mut Game) {
142        (self)(game)
143    }
144}
145
146/// A session runner is in charge of advancing a [`Session`] simulation.
147pub trait SessionRunner: Sync + Send + 'static {
148    /// Step the simulation once.
149    ///
150    /// It is the responsibility of the session runner to update the [`Time`] resource if necessary.
151    ///
152    /// If no special behavior is desired, the simplest session runner, and the one that is
153    /// implemented by [`DefaultSessionRunner`] is as follows:
154    ///
155    /// ```
156    /// # use bones_lib::prelude::*;
157    /// # struct Example;
158    /// # impl SessionRunner for Example {
159    /// fn step(&mut self, now: Instant, world: &mut World, stages: &mut SystemStages) {
160    ///     world.resource_mut::<Time>().update_with_instant(now);
161    ///     stages.run(world);
162    /// }
163    /// fn restart_session(&mut self) {}
164    /// fn disable_local_input(&mut self, disable_input: bool) {}
165    /// # }
166    /// ```
167    fn step(&mut self, now: Instant, world: &mut World, stages: &mut SystemStages);
168
169    /// Restart Session Runner. This should reset accumulated time, inputs, etc.
170    ///
171    /// The expectation is that current players using it may continue to, so something like a network
172    /// socket or player info should persist.
173    fn restart_session(&mut self);
174
175    /// Disable the capture of local input by this session.
176    fn disable_local_input(&mut self, input_disabled: bool);
177}
178
179/// The default [`SessionRunner`], which just runs the systems once every time it is run.
180#[derive(Default)]
181pub struct DefaultSessionRunner;
182impl SessionRunner for DefaultSessionRunner {
183    fn step(&mut self, now: instant::Instant, world: &mut World, stages: &mut SystemStages) {
184        world.init_resource::<Time>().update_with_instant(now);
185        stages.run(world);
186    }
187
188    // This is a no-op as no state, but implemented this way in case that changes later.
189    #[allow(clippy::default_constructed_unit_structs)]
190    fn restart_session(&mut self) {
191        *self = DefaultSessionRunner::default();
192    }
193
194    // `DefaultSessionRunner` does not collect input so this impl is not relevant.
195    fn disable_local_input(&mut self, _input_disabled: bool) {}
196}
197
198/// The [`Game`] encompasses a complete bones game's logic, independent of the renderer and IO
199/// implementations.
200///
201/// Games are made up of one or more [`Session`]s, each of which contains it's own [`World`] and
202/// [`SystemStages`]. These different sessions can be used for parts of the game with independent
203/// states, such as the main menu and the gameplay.
204pub struct Game {
205    /// The sessions that make up the game.
206    pub sessions: Sessions,
207    /// The collection of systems that are associated to the game itself, and not a specific
208    /// session.
209    pub systems: GameSystems,
210    /// List of sorted session keys.
211    ///
212    /// These are only guaranteed to be sorted and up-to-date immediately after calling
213    /// [`Game::step()`].
214    pub sorted_session_keys: Vec<Ustr>,
215    /// Collection of resources that will have a shared instance of each be inserted into each
216    /// session automatically.
217    pub shared_resources: Vec<AtomicUntypedResource>,
218}
219
220impl Default for Game {
221    fn default() -> Self {
222        let mut game = Self {
223            sessions: default(),
224            systems: default(),
225            sorted_session_keys: default(),
226            shared_resources: default(),
227        };
228
229        // Init Sessions shared resource so it exists for game step.
230        // (Game's sessions temporarily moved to this resource during execution)
231        game.init_shared_resource::<Sessions>();
232        game
233    }
234}
235
236impl Game {
237    /// Create an empty game with an asset server.
238    pub fn new() -> Self {
239        Self::default()
240    }
241
242    /// Install a [`GamePlugin`].
243    pub fn install_plugin<P: GamePlugin>(&mut self, plugin: P) -> &mut Self {
244        plugin.install(self);
245        self
246    }
247    #[track_caller]
248    /// Get the shared resource of a given type out of this [`Game`]s shared
249    /// resources if it exists.
250    pub fn get_shared_resource<T: HasSchema>(&self) -> Option<Ref<'_, T>> {
251        let res = self
252            .shared_resources
253            .iter()
254            .find(|x| x.schema() == T::schema())?;
255        let borrow = res.borrow();
256
257        if borrow.is_some() {
258            // SOUND: We know the type matches T
259            Some(Ref::map(borrow, |b| unsafe {
260                b.as_ref().unwrap().as_ref().cast_into_unchecked()
261            }))
262        } else {
263            None
264        }
265    }
266
267    #[track_caller]
268    /// Get the mutable shared resource of a given type out of this [`Game`]s
269    /// shared resources if it exists.
270    pub fn get_shared_resource_mut<T: HasSchema>(&self) -> Option<RefMut<'_, T>> {
271        let res = self
272            .shared_resources
273            .iter()
274            .find(|x| x.schema() == T::schema())?;
275        let borrow = res.borrow_mut();
276
277        if borrow.is_some() {
278            // SOUND: We know the type matches T
279            Some(RefMut::map(borrow, |b| unsafe {
280                b.as_mut().unwrap().as_mut().cast_into_mut_unchecked()
281            }))
282        } else {
283            None
284        }
285    }
286
287    #[track_caller]
288    /// Get the shared resource of a given type out of this [`Game`]s shared
289    /// resources. Panics if the resource doesn't exist.
290    pub fn shared_resource<T: HasSchema>(&self) -> Ref<'_, T> {
291        self.get_shared_resource()
292            .expect("shared resource not found")
293    }
294
295    #[track_caller]
296    /// Get the mutable shared resource of a given type out of this [`Game`]s
297    /// shared resources. Panics if it doesn't exist.
298    pub fn shared_resource_mut<T: HasSchema>(&self) -> RefMut<'_, T> {
299        self.get_shared_resource_mut()
300            .expect("shared resource not found")
301    }
302
303    /// Get the shared resource cell of a given type out of this [`Game`]s shared resources.
304    pub fn shared_resource_cell<T: HasSchema>(&self) -> Option<AtomicResource<T>> {
305        let res = self
306            .shared_resources
307            .iter()
308            .find(|x| x.schema() == T::schema())?;
309        Some(AtomicResource::from_untyped(res.clone()).unwrap())
310    }
311
312    /// Initialize a resource that will be shared across game sessions using it's [`Default`] value
313    /// if it is not already initialized, and borrow it for modification.
314    pub fn init_shared_resource<T: HasSchema + Default>(&mut self) -> RefMut<'_, T> {
315        if !self
316            .shared_resources
317            .iter()
318            .any(|x| x.schema() == T::schema())
319        {
320            self.insert_shared_resource(T::default());
321        }
322        self.shared_resource_mut::<T>()
323    }
324
325    /// Insert a resource that will be shared across all game sessions.
326    ///
327    /// > **Note:** This resource will only be visible in sessions that have not already
328    /// > initialized or access a resource of the same type locally.
329    pub fn insert_shared_resource<T: HasSchema>(&mut self, resource: T) {
330        // Update an existing resource of the same type.
331        for r in &mut self.shared_resources {
332            if r.schema() == T::schema() {
333                let mut borrow = r.borrow_mut();
334
335                if let Some(b) = borrow.as_mut().as_mut() {
336                    *b.cast_mut() = resource;
337                } else {
338                    *borrow = Some(SchemaBox::new(resource))
339                }
340                return;
341            }
342        }
343
344        // Or insert a new resource if we couldn't find one
345        self.shared_resources
346            .push(Arc::new(UntypedResource::new(SchemaBox::new(resource))));
347    }
348
349    // /// Remove a shared resource, if it is present in the world.
350    // /// # Panics
351    // /// Panics if the resource is set and it's cell has another handle to it and cannot be
352    // /// unwrapped.
353    // pub fn remove_shared_resource<T: HasSchema>(&mut self) -> Option<T> {
354    //     self.shared_resources
355    //         .iter()
356    //         .position(|x| x.schema() == T::schema())
357    //         .map(|idx| {
358    //             self.shared_resources
359    //                 .remove(idx)
360    //                 .try_into_inner()
361    //                 .unwrap()
362    //                 .into_inner()
363    //         })
364    // }
365
366    /// Step the game simulation.
367    pub fn step(&mut self, now: instant::Instant) {
368        // Pull out the game systems so that we can run them on the game
369        let mut game_systems = std::mem::take(&mut self.systems);
370
371        // Run game startup systems
372        if !game_systems.has_run_startup {
373            for system in &mut game_systems.startup {
374                system(self);
375            }
376            game_systems.has_run_startup = true;
377        }
378
379        // Run the before systems
380        for system in &mut game_systems.before {
381            system(self)
382        }
383
384        // Sort session keys by priority
385        self.sorted_session_keys.clear();
386        self.sorted_session_keys.extend(self.sessions.map.keys());
387        self.sorted_session_keys
388            .sort_by_key(|name| self.sessions.map.get(name).unwrap().priority);
389
390        // For every session
391        for session_name in self.sorted_session_keys.clone() {
392            // Extract the current session
393            let Some(mut current_session) = self.sessions.map.remove(&session_name) else {
394                // This may happen if the session was deleted by another session.
395                continue;
396            };
397
398            // If this session is active
399            let options = if current_session.active {
400                // Run any before session game systems
401                if let Some(systems) = game_systems.before_session.get_mut(&session_name) {
402                    for system in systems {
403                        system(self)
404                    }
405                }
406
407                // Make sure session contains all of the shared resources
408                for r in &self.shared_resources {
409                    if !current_session
410                        .world
411                        .resources
412                        .untyped()
413                        .contains_cell(r.schema().id())
414                    {
415                        current_session
416                            .world
417                            .resources
418                            .untyped()
419                            .insert_cell(r.clone())
420                            .unwrap();
421                    }
422                }
423
424                // Insert the session options
425                current_session.world.resources.insert(SessionOptions {
426                    active: true,
427                    delete: false,
428                    visible: current_session.visible,
429                });
430
431                // Insert the other sessions into the current session's world
432                {
433                    let mut sessions = current_session.world.resource_mut::<Sessions>();
434                    std::mem::swap(&mut *sessions, &mut self.sessions);
435                }
436
437                // Step the current session's simulation using it's session runner
438                current_session.runner.step(
439                    now,
440                    &mut current_session.world,
441                    &mut current_session.stages,
442                );
443
444                // Pull the sessions back out of the world
445                {
446                    let mut sessions = current_session.world.resource_mut::<Sessions>();
447                    std::mem::swap(&mut *sessions, &mut self.sessions);
448                }
449
450                // Pull the current session options back out of the world.
451                *current_session.world.resource::<SessionOptions>()
452            } else {
453                SessionOptions {
454                    active: false,
455                    visible: current_session.visible,
456                    delete: false,
457                }
458            };
459
460            // Delete the session
461            if options.delete {
462                let session_idx = self
463                    .sorted_session_keys
464                    .iter()
465                    .position(|x| x == &session_name)
466                    .unwrap();
467                self.sorted_session_keys.remove(session_idx);
468
469            // Update session options
470            } else {
471                current_session.active = options.active;
472                current_session.visible = options.visible;
473
474                // Insert the current session back into the session list
475                self.sessions.map.insert(session_name, current_session);
476            }
477
478            // Run any after session game systems
479            if let Some(systems) = game_systems.after_session.get_mut(&session_name) {
480                for system in systems {
481                    system(self)
482                }
483            }
484        }
485
486        // Execute Session Commands
487        {
488            let mut session_commands: VecDeque<Box<SessionCommand>> = default();
489            std::mem::swap(&mut session_commands, &mut self.sessions.commands);
490            for command in session_commands.drain(..) {
491                command(&mut self.sessions);
492            }
493        }
494
495        // Run after systems
496        for system in &mut game_systems.after {
497            system(self)
498        }
499
500        // Replace the game systems
501        self.systems = game_systems;
502
503        // Make sure sorted session keys does not include sessions that were deleted during
504        // execution by other sessions.
505        self.sorted_session_keys
506            .retain(|x| self.sessions.iter().any(|(id, _)| id == x));
507    }
508}
509
510/// A system that runs directly on a [`Game`] instead of in a specific [`Session`].
511pub type GameSystem = Box<dyn FnMut(&mut Game) + Sync + Send>;
512
513/// A collection of systems associated directly to a [`Game`] as opposed to a [`Session`].
514#[derive(Default)]
515pub struct GameSystems {
516    /// Flag which indicates whether or not the startup systems have been run yet.
517    pub has_run_startup: bool,
518    /// Startup systems.
519    pub startup: Vec<GameSystem>,
520    /// Game systems that are run before sessions are run.
521    pub before: Vec<GameSystem>,
522    /// Game systems that are run after sessions are run.
523    pub after: Vec<GameSystem>,
524    /// Game systems that are run after a specific session is run.
525    pub after_session: HashMap<Ustr, Vec<GameSystem>>,
526    /// Game systems that are run before a specific session is run.
527    pub before_session: HashMap<Ustr, Vec<GameSystem>>,
528}
529
530impl GameSystems {
531    /// Add a system that will run only once, before all of the other non-startup systems.
532    pub fn add_startup_system<F>(&mut self, system: F) -> &mut Self
533    where
534        F: FnMut(&mut Game) + Sync + Send + 'static,
535    {
536        self.startup.push(Box::new(system));
537        self
538    }
539
540    /// Add a system that will run on every step, before all of the sessions are run.
541    pub fn add_before_system<F>(&mut self, system: F) -> &mut Self
542    where
543        F: FnMut(&mut Game) + Sync + Send + 'static,
544    {
545        self.before.push(Box::new(system));
546        self
547    }
548
549    /// Add a system that will run on every step, after all of the sessions are run.
550    pub fn add_after_system<F>(&mut self, system: F) -> &mut Self
551    where
552        F: FnMut(&mut Game) + Sync + Send + 'static,
553    {
554        self.after.push(Box::new(system));
555        self
556    }
557
558    /// Add a system that will run every time the named session is run, before the session is run.
559    pub fn add_before_session_system<F>(&mut self, session: &str, system: F) -> &mut Self
560    where
561        F: FnMut(&mut Game) + Sync + Send + 'static,
562    {
563        self.before_session
564            .entry(session.into())
565            .or_default()
566            .push(Box::new(system));
567        self
568    }
569
570    /// Add a system that will run every time the named session is run, after the session is run.
571    pub fn add_after_session_system<F>(&mut self, session: &str, system: F) -> &mut Self
572    where
573        F: FnMut(&mut Game) + Sync + Send + 'static,
574    {
575        self.after_session
576            .entry(session.into())
577            .or_default()
578            .push(Box::new(system));
579        self
580    }
581}
582
583/// Type of session command
584pub type SessionCommand = dyn FnOnce(&mut Sessions) + Sync + Send;
585
586/// Container for multiple game sessions.
587///
588/// Each session shares the same [`Entities`].
589#[derive(HasSchema, Default)]
590#[schema(no_clone)]
591pub struct Sessions {
592    map: UstrMap<Session>,
593
594    /// Commands that operate on [`Sessions`], called after all sessions update.
595    /// These may be used to add/delete/modify sessions.
596    ///
597    /// Commands are useful in a situation where you want to remove / recreate a
598    /// session from within it's own system. You cannot do this while the
599    /// `Session` is running.
600    ///
601    /// Commands added inside a session command will not be executed until after
602    /// the frame they were inserted.
603    commands: VecDeque<Box<SessionCommand>>,
604}
605
606/// Resource that allows you to configure the current session.
607#[derive(HasSchema, Default, Debug, Clone, Copy)]
608#[repr(C)]
609pub struct SessionOptions {
610    /// Whether or not this session should be active after this frame.
611    pub active: bool,
612    /// Whether or not this session should be visible.
613    pub visible: bool,
614    /// Whether or not this session should be deleted.
615    pub delete: bool,
616}
617
618impl Sessions {
619    /// Create a new session, and borrow it mutably so it can be modified.
620    #[track_caller]
621    pub fn create<K: TryInto<Ustr>>(&mut self, name: K) -> &mut Session
622    where
623        <K as TryInto<Ustr>>::Error: Debug,
624    {
625        let name = name.try_into().unwrap();
626        // Create a blank session
627        let session = Session::default();
628
629        // Insert it into the map
630        self.map.insert(name, session);
631
632        // And borrow it for the modification
633        self.map.get_mut(&name).unwrap()
634    }
635
636    // TODO: `create_with` should be removed if `create` stays appropriate.
637    /// Create a new session, and borrow it mutably so it can be modified.
638    #[track_caller]
639    pub fn create_with<K: TryInto<Ustr>>(
640        &mut self,
641        name: K,
642        plugin: impl SessionPlugin,
643    ) -> &mut Session
644    where
645        <K as TryInto<Ustr>>::Error: Debug,
646    {
647        let session = self.create(name);
648        session.install_plugin(plugin)
649    }
650
651    /// Delete a session.
652    #[track_caller]
653    pub fn delete<K: TryInto<Ustr>>(&mut self, name: K)
654    where
655        <K as TryInto<Ustr>>::Error: Debug,
656    {
657        self.map.remove(&name.try_into().unwrap());
658    }
659
660    /// Borrow a session from the sessions list.
661    #[track_caller]
662    pub fn get<K: TryInto<Ustr>>(&self, name: K) -> Option<&Session>
663    where
664        <K as TryInto<Ustr>>::Error: Debug,
665    {
666        self.map.get(&name.try_into().unwrap())
667    }
668
669    /// Borrow a session from the sessions list.
670    #[track_caller]
671    pub fn get_mut<K: TryInto<Ustr>>(&mut self, name: K) -> Option<&mut Session>
672    where
673        <K as TryInto<Ustr>>::Error: Debug,
674    {
675        self.map.get_mut(&name.try_into().unwrap())
676    }
677
678    /// Mutably iterate over sessions.
679    pub fn iter_mut(&mut self) -> std::collections::hash_map::IterMut<'_, Ustr, Session> {
680        self.map.iter_mut()
681    }
682
683    /// Iterate over sessions.
684    pub fn iter(&self) -> std::collections::hash_map::Iter<'_, Ustr, Session> {
685        self.map.iter()
686    }
687
688    /// Add a [`SessionCommand`] to queue.
689    pub fn add_command(&mut self, command: Box<SessionCommand>) {
690        self.commands.push_back(command);
691    }
692}