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}