diff --git a/Cargo.lock b/Cargo.lock index dad1b44b79..b711b0b5d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1435,6 +1435,7 @@ dependencies = [ "paste", "serde", "thiserror", + "tracing", ] [[package]] @@ -1522,6 +1523,7 @@ version = "0.4.0" dependencies = [ "bones_ecs", "instant", + "tracing", "ustr", ] diff --git a/Cargo.toml b/Cargo.toml index 9f6a805e82..fd9c54c44e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ parking_lot = "0.12" smallvec = "1.11" ustr = "0.10" iroh-net = "0.27" +tracing = "0.1" [profile.release] lto = true diff --git a/demos/asset_packs/src/main.rs b/demos/asset_packs/src/main.rs index e2fb6fce95..dedf052d7c 100644 --- a/demos/asset_packs/src/main.rs +++ b/demos/asset_packs/src/main.rs @@ -46,12 +46,13 @@ fn main() { // Create a new session for the game menu. Each session is it's own bones world with it's own // plugins, systems, and entities. - let menu_session = game.sessions.create("menu"); - menu_session + game.sessions.create_with("menu", |builder| { // Install the default bones_framework plugin for this session - .install_plugin(DefaultSessionPlugin) - // Add our menu system to the update stage - .add_system_to_stage(Update, menu_system); + builder + .install_plugin(DefaultSessionPlugin) + // Add our menu system to the update stage + .add_system_to_stage(Update, menu_system); + }); BonesBevyRenderer::new(game).app().run(); } diff --git a/demos/assets_minimal/src/main.rs b/demos/assets_minimal/src/main.rs index 092f0dd94c..b49b7c0a01 100644 --- a/demos/assets_minimal/src/main.rs +++ b/demos/assets_minimal/src/main.rs @@ -36,13 +36,17 @@ fn main() { // Create a new session for the game menu. Each session is it's own bones world with it's own // plugins, systems, and entities. - let menu_session = game.sessions.create("menu"); - menu_session - // Install the default bones_framework plugin for this session + let mut menu_session_builder = SessionBuilder::new("menu"); + + // Install the default bones_framework plugin for this session + menu_session_builder .install_plugin(DefaultSessionPlugin) // Add our menu system to the update stage .add_system_to_stage(Update, menu_system); + // Finalize session and register with game sessions. + menu_session_builder.finish_and_add(&mut game.sessions); + BonesBevyRenderer::new(game).app().run(); } diff --git a/demos/features/src/main.rs b/demos/features/src/main.rs index c97a421f01..51e4380b9d 100644 --- a/demos/features/src/main.rs +++ b/demos/features/src/main.rs @@ -125,7 +125,7 @@ pub fn create_game() -> Game { TilemapDemoMeta::register_schema(); // Create our menu session - game.sessions.create("menu").install_plugin(menu_plugin); + game.sessions.create_with("menu", menu_plugin); game } @@ -139,12 +139,11 @@ struct MenuData { } /// Menu plugin -pub fn menu_plugin(session: &mut Session) { +pub fn menu_plugin(session: &mut SessionBuilder) { // Register our menu system session // Install the bones_framework default plugins for this session .install_plugin(DefaultSessionPlugin) - .world // Initialize our menu data resource .init_resource::(); @@ -198,9 +197,7 @@ fn menu_system( session_options.delete = true; // Create a session for the match - sessions - .create("sprite_demo") - .install_plugin(sprite_demo_plugin); + sessions.create_with("sprite_demo", sprite_demo_plugin); } if BorderedButton::themed(&meta.button_style, localization.get("atlas-demo")) @@ -211,9 +208,7 @@ fn menu_system( session_options.delete = true; // Create a session for the match - sessions - .create("atlas_demo") - .install_plugin(atlas_demo_plugin); + sessions.create_with("atlas_demo", atlas_demo_plugin); } if BorderedButton::themed(&meta.button_style, localization.get("tilemap-demo")) @@ -224,9 +219,7 @@ fn menu_system( session_options.delete = true; // Create a session for the match - sessions - .create("tilemap_demo") - .install_plugin(tilemap_demo_plugin); + sessions.create_with("tilemap_demo", tilemap_demo_plugin); } if BorderedButton::themed(&meta.button_style, localization.get("audio-demo")) @@ -237,9 +230,7 @@ fn menu_system( session_options.delete = true; // Create a session for the match - sessions - .create("audio_demo") - .install_plugin(audio_demo_plugin); + sessions.create_with("audio_demo", audio_demo_plugin); } if BorderedButton::themed(&meta.button_style, localization.get("storage-demo")) @@ -250,9 +241,7 @@ fn menu_system( session_options.delete = true; // Create a session for the match - sessions - .create("storage_demo") - .install_plugin(storage_demo_plugin); + sessions.create_with("storage_demo", storage_demo_plugin); } if BorderedButton::themed(&meta.button_style, localization.get("path2d-demo")) @@ -263,9 +252,7 @@ fn menu_system( session_options.delete = true; // Create a session for the match - sessions - .create("path2d_demo") - .install_plugin(path2d_demo_plugin); + sessions.create_with("path2d_demo", path2d_demo_plugin); } if let Some(exit_bones) = &mut exit_bones { @@ -293,7 +280,7 @@ fn menu_system( } /// Plugin for running the sprite demo. -fn sprite_demo_plugin(session: &mut Session) { +fn sprite_demo_plugin(session: &mut SessionBuilder) { session .install_plugin(DefaultSessionPlugin) .add_startup_system(sprite_demo_startup) @@ -357,7 +344,7 @@ fn move_sprite( } /// Plugin for running the tilemap demo. -fn tilemap_demo_plugin(session: &mut Session) { +fn tilemap_demo_plugin(session: &mut SessionBuilder) { session .install_plugin(DefaultSessionPlugin) .add_startup_system(tilemap_startup_system) @@ -403,7 +390,7 @@ fn tilemap_startup_system( } /// Plugin for running the atlas demo. -fn atlas_demo_plugin(session: &mut Session) { +fn atlas_demo_plugin(session: &mut SessionBuilder) { session .install_plugin(DefaultSessionPlugin) .add_startup_system(atlas_demo_startup) @@ -451,7 +438,7 @@ fn atlas_demo_startup( ); } -fn audio_demo_plugin(session: &mut Session) { +fn audio_demo_plugin(session: &mut SessionBuilder) { session .install_plugin(DefaultSessionPlugin) .add_system_to_stage(Update, back_to_menu_ui) @@ -477,7 +464,7 @@ fn audio_demo_ui( }); } -fn storage_demo_plugin(session: &mut Session) { +fn storage_demo_plugin(session: &mut SessionBuilder) { session .install_plugin(DefaultSessionPlugin) .add_system_to_stage(Update, storage_demo_ui) @@ -507,7 +494,7 @@ fn storage_demo_ui( }); } -fn path2d_demo_plugin(session: &mut Session) { +fn path2d_demo_plugin(session: &mut SessionBuilder) { session .install_plugin(DefaultSessionPlugin) .add_startup_system(path2d_demo_startup) @@ -558,7 +545,7 @@ fn back_to_menu_ui( ui.add_space(20.0); if ui.button(localization.get("back-to-menu")).clicked() { session_options.delete = true; - sessions.create("menu").install_plugin(menu_plugin); + sessions.create_with("menu", menu_plugin); } }); }); diff --git a/demos/hello_world/src/main.rs b/demos/hello_world/src/main.rs index 3428194cbb..b279981e3f 100644 --- a/demos/hello_world/src/main.rs +++ b/demos/hello_world/src/main.rs @@ -10,12 +10,13 @@ fn main() { // Create a new session for the game menu. Each session is it's own bones world with it's own // plugins, systems, and entities. - let menu_session = game.sessions.create("menu"); - menu_session - // Install the default bones_framework plugin for this session - .install_plugin(DefaultSessionPlugin) - // Add our menu system to the update stage - .add_system_to_stage(Update, menu_system); + game.sessions.create_with("menu", |session| { + session + // Install the default bones_framework plugin for this session + .install_plugin(DefaultSessionPlugin) + // Add our menu system to the update stage + .add_system_to_stage(Update, menu_system); + }); BonesBevyRenderer::new(game).app().run(); } diff --git a/demos/scripting/src/main.rs b/demos/scripting/src/main.rs index 24ec4f5b42..301a0bd61e 100644 --- a/demos/scripting/src/main.rs +++ b/demos/scripting/src/main.rs @@ -22,9 +22,9 @@ fn main() { .register_default_assets(); GameMeta::register_schema(); - game.sessions - .create("launch") - .add_startup_system(launch_game_session); + game.sessions.create_with("launch", |builder| { + builder.add_startup_system(launch_game_session); + }); let mut renderer = BonesBevyRenderer::new(game); renderer.app_namespace = ( @@ -41,15 +41,17 @@ fn launch_game_session( mut session_ops: ResMut, ) { session_ops.delete = true; - let game_session = sessions.create("game"); - game_session - .install_plugin(DefaultSessionPlugin) - // Install the plugin that will load our lua plugins and run them in the game session - .install_plugin(LuaPluginLoaderSessionPlugin( - // Tell it to install the lua plugins specified in our game meta - Arc::new(meta.plugins.iter().copied().collect()), - )) - .add_startup_system(game_startup); + // Build game session and add to `Sessions` + sessions.create_with("game", |builder| { + builder + .install_plugin(DefaultSessionPlugin) + // Install the plugin that will load our lua plugins and run them in the game session + .install_plugin(LuaPluginLoaderSessionPlugin( + // Tell it to install the lua plugins specified in our game meta + Arc::new(meta.plugins.iter().copied().collect()), + )) + .add_startup_system(game_startup); + }); } fn game_startup( diff --git a/framework_crates/bones_asset/Cargo.toml b/framework_crates/bones_asset/Cargo.toml index ebd115c260..080d51df0c 100644 --- a/framework_crates/bones_asset/Cargo.toml +++ b/framework_crates/bones_asset/Cargo.toml @@ -40,7 +40,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" sha2 = "0.10" -tracing = "0.1" +tracing = { workspace = true } ulid = "1.0" ustr = { workspace = true } diff --git a/framework_crates/bones_ecs/Cargo.toml b/framework_crates/bones_ecs/Cargo.toml index d1720e8d34..8c9a0ca7ef 100644 --- a/framework_crates/bones_ecs/Cargo.toml +++ b/framework_crates/bones_ecs/Cargo.toml @@ -40,6 +40,7 @@ atomicell = "0.2" bitset-core = "0.1" once_map = "0.4.12" thiserror = "1.0" +tracing = { workspace = true } [dev-dependencies] glam = "0.24" diff --git a/framework_crates/bones_ecs/examples/pos_vel.rs b/framework_crates/bones_ecs/examples/pos_vel.rs index 3e1389bd76..6d4f52ea2d 100644 --- a/framework_crates/bones_ecs/examples/pos_vel.rs +++ b/framework_crates/bones_ecs/examples/pos_vel.rs @@ -22,15 +22,18 @@ fn main() { // Initialize an empty world let mut world = World::new(); - // Create a SystemStages to store the systems that we will run more than once. - let mut stages = SystemStages::with_core_stages(); + // Build SystemStages to store the systems that we will run more than once. + let mut stages_builder = SystemStagesBuilder::with_core_stages(); // Add our systems to the system stages - stages + stages_builder .add_startup_system(startup_system) .add_system_to_stage(CoreStage::Update, pos_vel_system) .add_system_to_stage(CoreStage::PostUpdate, print_system); + // Finish build to get `SystemStages`. + let mut stages = stages_builder.finish(); + // Run our game loop for 10 frames for _ in 0..10 { stages.run(&mut world); diff --git a/framework_crates/bones_ecs/src/resources.rs b/framework_crates/bones_ecs/src/resources.rs index 883d9fcc1d..372e2ab572 100644 --- a/framework_crates/bones_ecs/src/resources.rs +++ b/framework_crates/bones_ecs/src/resources.rs @@ -200,6 +200,16 @@ impl UntypedResources { |_, cell| cell.clone(), ) } + + /// Removes all resourcse that are not shared resources. + pub fn clear_owned_resources(&mut self) { + for (schema_id, resource_cell) in self.resources.iter_mut() { + let is_shared = self.shared_resources.contains_key(schema_id); + if !is_shared { + resource_cell.remove(); + } + } + } } /// A collection of resources. @@ -229,7 +239,7 @@ impl Resources { /// /// See [get()][Self::get] pub fn contains(&self) -> bool { - self.untyped.resources.contains_key(&T::schema().id()) + self.untyped.contains(T::schema().id()) } /// Remove a resource from the store, if it is present. @@ -285,6 +295,11 @@ impl Resources { pub fn into_untyped(self) -> UntypedResources { self.untyped } + + /// Removes all resources that are not shared. Shared resources are preserved. + pub fn clear_owned_resources(&mut self) { + self.untyped.clear_owned_resources(); + } } /// A handle to a resource from a [`Resources`] collection. @@ -434,20 +449,136 @@ impl AtomicResource { } } +/// Utility container for storing set of [`UntypedResource`]. +/// Cloning +#[derive(Default)] +pub struct UntypedResourceSet { + resources: Vec, +} + +impl Clone for UntypedResourceSet { + fn clone(&self) -> Self { + Self { + resources: self + .resources + .iter() + .map(|res| { + if let Some(clone) = res.clone_data() { + UntypedResource::new(clone) + } else { + UntypedResource::empty(res.schema()) + } + }) + .collect(), + } + } +} + +impl UntypedResourceSet { + /// Insert a startup resource. On stage / session startup (first step), will be inserted into [`World`]. + /// + /// If already exists, will be overwritten. + pub fn insert_resource(&mut self, resource: T) { + // Update an existing resource of the same type. + for r in &mut self.resources { + if r.schema() == T::schema() { + let mut borrow = r.borrow_mut(); + + if let Some(b) = borrow.as_mut() { + *b.cast_mut() = resource; + } else { + *borrow = Some(SchemaBox::new(resource)) + } + return; + } + } + + // Or insert a new resource if we couldn't find one + self.resources + .push(UntypedResource::new(SchemaBox::new(resource))) + } + + /// Init resource with default, and return mutable ref for modification. + /// If already exists, returns mutable ref to existing resource. + pub fn init_resource(&mut self) -> RefMut { + if !self.resources.iter().any(|x| x.schema() == T::schema()) { + self.insert_resource(T::default()); + } + self.resource_mut::().unwrap() + } + + /// Get mutable reference to startup resource if found. + #[track_caller] + pub fn resource_mut(&self) -> Option> { + let res = self.resources.iter().find(|x| x.schema() == T::schema())?; + let borrow = res.borrow_mut(); + + if borrow.is_some() { + // SOUND: We know the type matches T + Some(RefMut::map(borrow, |b| unsafe { + b.as_mut().unwrap().as_mut().cast_into_mut_unchecked() + })) + } else { + None + } + } + + /// Insert an [`UntypedResource`] with empty cell for [`Schema`] of `T`. + /// If resource already exists for this schema, overwrite it with empty. + pub fn insert_empty(&mut self) { + for r in &mut self.resources { + if r.schema() == T::schema() { + let mut borrow = r.borrow_mut(); + *borrow = None; + return; + } + } + + // Or insert a new empty resource if we couldn't find one + self.resources.push(UntypedResource::empty(T::schema())); + } + + /// Get immutable ref to Vec of resources + pub fn resources(&self) -> &Vec { + &self.resources + } + + /// Insert resources in world. If resource already exists, overwrites it. + /// + /// If `remove_empty` is set, if resource cell is empty, it will remove the + /// resource from cell on world. + pub fn insert_on_world(&self, world: &mut World, remove_empty: bool) { + for resource in self.resources.iter() { + let resource_cell = world.resources.untyped().get_cell(resource.schema()); + + // Deep copy resource and insert into world. + if let Some(resource_copy) = resource.clone_data() { + resource_cell + .insert(resource_copy) + .expect("Schema mismatch error"); + } else if remove_empty { + // Remove the resource on world + resource_cell.remove(); + } + } + } +} + #[cfg(test)] mod test { + use std::sync::Arc; + use crate::prelude::*; + #[derive(HasSchema, Clone, Debug, Default)] + #[repr(C)] + struct A(String); + + #[derive(HasSchema, Clone, Debug, Default)] + #[repr(C)] + struct B(u32); #[test] fn sanity_check() { - #[derive(HasSchema, Clone, Debug, Default)] - #[repr(C)] - struct A(String); - - #[derive(HasSchema, Clone, Debug, Default)] - #[repr(C)] - struct B(u32); - let r1 = Resources::new(); r1.insert(A(String::from("hi"))); @@ -470,4 +601,28 @@ mod test { assert_eq!(r1b.borrow().unwrap().0, 2); assert_eq!(r1a.borrow().unwrap().0, "world"); } + + #[test] + fn resources_clear_owned_values() { + let mut r = Resources::new(); + + // insert A as non-shared resource + r.insert(A(String::from("foo"))); + + // Insert B as shared resource + r.untyped + .insert_cell(Arc::new(UntypedResource::new(SchemaBox::new(B(1))))) + .unwrap(); + + // Should only clear non-shared resources + r.clear_owned_resources(); + + // Verify non-shared was removed + let res_a = r.get::(); + assert!(res_a.is_none()); + + // Verify shared is still present + let res_b = r.get::().unwrap(); + assert_eq!(res_b.0, 1); + } } diff --git a/framework_crates/bones_ecs/src/stage.rs b/framework_crates/bones_ecs/src/stage.rs index a71411fcfb..543fbdf14b 100644 --- a/framework_crates/bones_ecs/src/stage.rs +++ b/framework_crates/bones_ecs/src/stage.rs @@ -11,75 +11,29 @@ use crate::prelude::*; #[derive(Deref, DerefMut, Clone, Copy, HasSchema, Default)] pub struct CurrentSystemStage(pub Ulid); -/// An ordered collection of [`SystemStage`]s. -pub struct SystemStages { +/// Builder for [`SystemStages`]. It is immutable once created, +pub struct SystemStagesBuilder { /// The stages in the collection, in the order that they will be run. - pub stages: Vec>, - /// Whether or not the startup systems have been run yet. - pub has_started: bool, + stages: Vec>, /// The systems that should run at startup. - pub startup_systems: Vec>, - /// Systems that are continously run until they succeed(return Some). These run before all stages. Uses Option to allow for easy usage of `?`. - pub single_success_systems: Vec>>, -} + /// They will be executed next step based on if [`SessionStarted`] resource in world says session has not started, or if resource does not exist. + startup_systems: Vec>, -impl std::fmt::Debug for SystemStages { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SystemStages") - // TODO: Add list of stages to the debug render for `SystemStages`. - // We can at least list the names of each stage for `SystemStages` debug - // implementation. - .finish() - } + /// Resources installed during session plugin installs. Copied to world as first step on startup of stages' execution. + startup_resources: UntypedResourceSet, + + /// Systems that are continously run until they succeed(return Some). These run before all stages. Uses Option to allow for easy usage of `?`. + single_success_systems: Vec>>, } -impl Default for SystemStages { +impl Default for SystemStagesBuilder { fn default() -> Self { Self::with_core_stages() } } -impl SystemStages { - /// Execute the systems on the given `world`. - pub fn run(&mut self, world: &mut World) { - // If we haven't run our startup systems yet - if !self.has_started { - // Set the current stage resource - world.insert_resource(CurrentSystemStage(Ulid(0))); - - // For each startup system - for system in &mut self.startup_systems { - // Run the system - system.run(world, ()); - } - - // Don't run startup systems again - self.has_started = true; - } - - // Run single success systems - self.single_success_systems.retain_mut(|system| { - let result = system.run(world, ()); - result.is_none() // Keep the system if it didn't succeed (returned None) - }); - - // Run each stage - for stage in &mut self.stages { - // Set the current stage resource - world.insert_resource(CurrentSystemStage(stage.id())); - - // Run the stage - stage.run(world); - } - - // Cleanup killed entities - world.maintain(); - - // Remove the current system stage resource - world.resources.remove::(); - } - - /// Create a [`SystemStages`] collection, initialized with a stage for each [`CoreStage`]. +impl SystemStagesBuilder { + /// Create a [`SystemStagesBuilder`] for [`SystemStages`] collection, initialized with a stage for each [`CoreStage`]. pub fn with_core_stages() -> Self { Self { stages: vec![ @@ -89,13 +43,24 @@ impl SystemStages { Box::new(SimpleSystemStage::new(CoreStage::PostUpdate)), Box::new(SimpleSystemStage::new(CoreStage::Last)), ], - has_started: false, + startup_resources: default(), startup_systems: default(), single_success_systems: Vec::new(), } } + /// Finish building and convert to [`SystemStages`] + pub fn finish(self) -> SystemStages { + SystemStages { + stages: self.stages, + startup_systems: self.startup_systems, + startup_resources: self.startup_resources, + single_success_systems: self.single_success_systems, + } + } + /// Add a system that will run only once, before all of the other non-startup systems. + /// If wish to reset session and run again, can modify [`SessionStarted`] resource in world. pub fn add_startup_system(&mut self, system: S) -> &mut Self where S: IntoSystem>, @@ -171,24 +136,168 @@ impl SystemStages { self } - /// Remove all systems from all stages, including startup and single success systems. Resets has_started as well, allowing for startup systems to run once again. - pub fn reset_remove_all_systems(&mut self) { - // Reset the has_started flag - self.has_started = false; - self.remove_all_systems(); + /// Insert a startup resource. On stage / session startup (first step), will be inserted into [`World`]. + /// + /// If already exists, will be overwritten. + pub fn insert_startup_resource(&mut self, resource: T) { + self.startup_resources.insert_resource(resource); + } + + /// Init startup resource with default, and return mutable ref for modification. + /// If already exists, returns mutable ref to existing resource. + pub fn init_startup_resource(&mut self) -> RefMut { + self.startup_resources.init_resource::() + } + + /// Get mutable reference to startup resource if found. + #[track_caller] + pub fn startup_resource_mut(&self) -> Option> { + self.startup_resources.resource_mut() + } +} + +/// An ordered collection of [`SystemStage`]s. +pub struct SystemStages { + /// The stages in the collection, in the order that they will be run. + stages: Vec>, + + /// The systems that should run at startup. + /// They will be executed next step based on if [`SessionStarted`] resource in world says session has not started, or if resource does not exist. + startup_systems: Vec>, + + /// Resources installed during session plugin installs. Copied to world as first step on startup of stages' execution. + startup_resources: UntypedResourceSet, + + /// Systems that are continously run until they succeed(return Some). These run before all stages. Uses Option to allow for easy usage of `?`. + single_success_systems: Vec>>, +} + +impl std::fmt::Debug for SystemStages { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SystemStages") + // TODO: Add list of stages to the debug render for `SystemStages`. + // We can at least list the names of each stage for `SystemStages` debug + // implementation. + .finish() + } +} + +impl Default for SystemStages { + fn default() -> Self { + SystemStagesBuilder::default().finish() } +} - /// Remove all systems from all stages, including startup and single success systems. Does not reset has_started. - pub fn remove_all_systems(&mut self) { - // Clear startup systems - self.startup_systems.clear(); +impl SystemStages { + /// Create builder for construction of [`SystemStages`]. + pub fn builder() -> SystemStagesBuilder { + SystemStagesBuilder::default() + } - // Clear single success systems - self.single_success_systems.clear(); + /// Execute the systems on the given `world`. + pub fn run(&mut self, world: &mut World) { + // If we haven't run startup systems and setup resources yet, do so + self.handle_startup(world); + + // Run single success systems + for (index, system) in self.single_success_systems.iter_mut().enumerate() { + let should_run = !Self::has_single_success_system_succeeded(index, world); - // Clear systems from each stage + if should_run && system.run(world, ()).is_some() { + Self::mark_single_success_system_succeeded(index, world); + } + } + + // Run each stage for stage in &mut self.stages { - stage.remove_all_systems(); + // Set the current stage resource + world.insert_resource(CurrentSystemStage(stage.id())); + + // Run the stage + stage.run(world); + } + + // Cleanup killed entities + world.maintain(); + + // Remove the current system stage resource + world.resources.remove::(); + } + + /// If [`SessionStarted`] resource indicates have not yet started, + /// perform startup tasks (insert startup resources, run startup systems). + /// + /// While this is used internally by [`SystemStages::run`], this is also used + /// for resetting world. This allows world to immediately startup and re-initialize after reset. + pub fn handle_startup(&mut self, world: &mut World) { + if !Self::has_session_started(world) { + self.insert_startup_resources(world); + + // Set the current stage resource + world.insert_resource(CurrentSystemStage(Ulid(0))); + + // For each startup system + for system in &mut self.startup_systems { + // Run the system + system.run(world, ()); + } + + // Don't run startup systems again + Self::set_session_started(true, world); + world.resources.remove::(); + } + } + + /// Has session started and startup systems been executed? + fn has_session_started(world: &World) -> bool { + if let Some(session_started) = world.get_resource::() { + return session_started.has_started; + } + + false + } + + /// Set whether the session has been started and startup systems executed. + fn set_session_started(started: bool, world: &mut World) { + world.init_resource::().has_started = started; + } + + /// Check if single success system is marked as succeeded in [`SingleSuccessSystems`] [`Resource`]. + fn has_single_success_system_succeeded(system_index: usize, world: &World) -> bool { + if let Some(system_success) = world.get_resource::() { + return system_success.has_system_succeeded(system_index); + } + + false + } + + /// Mark a single success system as succeeded in [`SingleSuccessSystems`] [`Resource`]. + fn mark_single_success_system_succeeded(system_index: usize, world: &mut World) { + if let Some(mut system_succes) = world.get_resource_mut::() { + system_succes.set_system_completed(system_index); + return; + } + + // Resource does not exist - must initialize it + world + .init_resource::() + .set_system_completed(system_index); + } + + /// Insert the startup resources that [`SystemStages`] and session were built with into [`World`]. + fn insert_startup_resources(&self, world: &mut World) { + for resource in self.startup_resources.resources().iter() { + // Deep copy startup resource and insert into world. + let resource_copy = resource.clone_data().unwrap(); + let resource_cell = world.resources.untyped().get_cell(resource.schema()); + let prev_val = resource_cell.insert(resource_copy).unwrap(); + + // Warn on already existing resource + if prev_val.is_some() { + let schema_name = resource.schema().full_name; + tracing::warn!("SystemStages` attempted to inserted resource {schema_name} on startup that already exists in world - startup resource not inserted. + When building new session, startup resources should be initialized on `SessionBuilder`."); + } } } } @@ -358,3 +467,38 @@ impl<'a> SystemParam for Commands<'a> { Commands(state.borrow_mut().unwrap()) } } + +/// Resource tracking if Session has started and startup systems executed. +/// If reset to false, startup systems should be re-triggered. +/// If resource is not present, assumed to have not started (and will be initialized upon execution). +#[derive(Copy, Clone, HasSchema, Default)] +pub struct SessionStarted { + /// Has the session started, and startup systems executed? + pub has_started: bool, +} + +/// Resource tracking which of single success systems in `Session`'s [`SystemStages`] have completed. +/// Success is tracked to +#[derive(HasSchema, Clone, Default)] +pub struct SingleSuccessSystems { + /// Set of indices of [`SystemStages`]'s single success systems that have succeeded. + pub systems_succeeded: HashSet, +} + +impl SingleSuccessSystems { + /// Reset single success systems completion status. so they run again until success. + #[allow(dead_code)] + pub fn reset(&mut self) { + self.systems_succeeded.clear(); + } + + /// Check if system has completed + pub fn has_system_succeeded(&self, index: usize) -> bool { + self.systems_succeeded.contains(&index) + } + + /// Mark system as completed. + pub fn set_system_completed(&mut self, index: usize) { + self.systems_succeeded.insert(index); + } +} diff --git a/framework_crates/bones_ecs/src/world.rs b/framework_crates/bones_ecs/src/world.rs index 9e6ffbd655..3c75aa0c86 100644 --- a/framework_crates/bones_ecs/src/world.rs +++ b/framework_crates/bones_ecs/src/world.rs @@ -199,20 +199,10 @@ impl World { self.components.get::().borrow_mut() } - /// Provides an interface for resetting entities, and components. - pub fn reset_internals(&mut self, reset_components: bool, reset_entities: bool) { - if reset_entities { - let mut entities = self.resource_mut::(); - entities.kill_all(); - } - - if reset_components { - // Clear all component stores - self.components = ComponentStores::default(); - } - - // Always maintain to clean up any killed entities - self.maintain(); + /// Load snapshot of [`World`] into self. + pub fn load_snapshot(&mut self, snapshot: World) { + self.components = snapshot.components; + self.resources = snapshot.resources; } } diff --git a/framework_crates/bones_framework/Cargo.toml b/framework_crates/bones_framework/Cargo.toml index 1ba48f2e53..4b0ea43c74 100644 --- a/framework_crates/bones_framework/Cargo.toml +++ b/framework_crates/bones_framework/Cargo.toml @@ -106,7 +106,7 @@ send_wrapper = "0.6.0" # Tracing -tracing = "0.1" +tracing = { workspace = true } tracing-subscriber = { version = "0.3", optional = true, features = ["env-filter"] } tracing-appender = { version = "0.2", optional = true, features = ["parking_lot"] } tracing-tracy = { version = "0.11.0", optional = true, default-features = false } diff --git a/framework_crates/bones_framework/src/animation.rs b/framework_crates/bones_framework/src/animation.rs index 0d58421f41..159b244441 100644 --- a/framework_crates/bones_framework/src/animation.rs +++ b/framework_crates/bones_framework/src/animation.rs @@ -3,7 +3,7 @@ use crate::prelude::*; /// Install animation utilities into the given [`SystemStages`]. -pub fn animation_plugin(core: &mut Session) { +pub fn animation_plugin(core: &mut SessionBuilder) { core.stages .add_system_to_stage(CoreStage::Last, update_animation_banks) .add_system_to_stage(CoreStage::Last, animate_sprites); diff --git a/framework_crates/bones_framework/src/audio.rs b/framework_crates/bones_framework/src/audio.rs index c0ac4b6967..447b03fd4b 100644 --- a/framework_crates/bones_framework/src/audio.rs +++ b/framework_crates/bones_framework/src/audio.rs @@ -20,13 +20,13 @@ pub fn game_plugin(game: &mut Game) { game.insert_shared_resource(AudioManager::default()); game.init_shared_resource::(); - let session = game.sessions.create(DEFAULT_BONES_AUDIO_SESSION); + let mut session = SessionBuilder::new(DEFAULT_BONES_AUDIO_SESSION); // Audio doesn't do any rendering session.visible = false; session - .stages .add_system_to_stage(First, _process_audio_events) .add_system_to_stage(Last, _kill_finished_audios); + session.finish_and_add(&mut game.sessions); } /// Holds the handles and the volume to be played for a piece of Audio. diff --git a/framework_crates/bones_framework/src/debug.rs b/framework_crates/bones_framework/src/debug.rs index facfdfce1d..33f3c80c8b 100644 --- a/framework_crates/bones_framework/src/debug.rs +++ b/framework_crates/bones_framework/src/debug.rs @@ -51,7 +51,7 @@ pub struct FrameTimeWindowState { /// If installed, allows opening egui window with [`FrameTimeWindowState`] in [`EguiCtx`] state /// to get frame time information. -pub fn frame_time_diagnostics_plugin(core: &mut Session) { +pub fn frame_time_diagnostics_plugin(core: &mut SessionBuilder) { core.stages .add_system_to_stage(CoreStage::Last, frame_diagnostic_window); } diff --git a/framework_crates/bones_framework/src/lib.rs b/framework_crates/bones_framework/src/lib.rs index 88adc57a33..f2629410ed 100644 --- a/framework_crates/bones_framework/src/lib.rs +++ b/framework_crates/bones_framework/src/lib.rs @@ -91,7 +91,7 @@ pub mod external { /// Default plugins for bones framework sessions. pub struct DefaultSessionPlugin; impl lib::SessionPlugin for DefaultSessionPlugin { - fn install(self, session: &mut lib::Session) { + fn install(self, session: &mut lib::SessionBuilder) { session .install_plugin(animation::animation_plugin) .install_plugin(render::render_plugin); diff --git a/framework_crates/bones_framework/src/networking.rs b/framework_crates/bones_framework/src/networking.rs index 8f52b20191..9e51a0d844 100644 --- a/framework_crates/bones_framework/src/networking.rs +++ b/framework_crates/bones_framework/src/networking.rs @@ -872,20 +872,7 @@ where cell.save(frame, Some(world.clone()), None) } ggrs::GgrsRequest::LoadGameState { cell, .. } => { - // Swap out sessions to preserve them after world save. - // Sessions clone makes empty copy, so saved snapshots do not include sessions. - // Sessions are borrowed from Game for execution of this session, - // they are not like other resources and should not be preserved. - let mut sessions = Sessions::default(); - std::mem::swap( - &mut sessions, - &mut world.resource_mut::(), - ); - *world = cell.load().unwrap_or_default(); - std::mem::swap( - &mut sessions, - &mut world.resource_mut::(), - ); + world.load_snapshot(cell.load().unwrap_or_default()); } ggrs::GgrsRequest::AdvanceFrame { inputs: network_inputs, @@ -963,6 +950,17 @@ where // Run game session stages, advancing simulation stages.run(world); + + // Handle any triggered resets of world + preserve runner's managed resources like RngGenerator. + if world.reset_triggered() { + let rng = world + .get_resource::() + .map(|r| (*r).clone()); + world.handle_world_reset(stages); + if let Some(rng) = rng { + world.resources.insert(rng); + } + } } } } diff --git a/framework_crates/bones_framework/src/networking/debug.rs b/framework_crates/bones_framework/src/networking/debug.rs index 924bb7d8bc..055b60a976 100644 --- a/framework_crates/bones_framework/src/networking/debug.rs +++ b/framework_crates/bones_framework/src/networking/debug.rs @@ -25,7 +25,7 @@ pub mod prelude { /// Session plugin for network debug window. Is not installed by default. /// After installing plugin, [`NetworkDebugMenuState`] on [`EguiCtx`] state /// may be modified to open menu. -pub fn network_debug_session_plugin(session: &mut Session) { +pub fn network_debug_session_plugin(session: &mut SessionBuilder) { session.add_system_to_stage(CoreStage::First, network_debug_window); } diff --git a/framework_crates/bones_framework/src/render.rs b/framework_crates/bones_framework/src/render.rs index ba58566c45..b8cfdd0793 100644 --- a/framework_crates/bones_framework/src/render.rs +++ b/framework_crates/bones_framework/src/render.rs @@ -24,7 +24,7 @@ pub mod transform; pub mod ui; /// Bones framework rendering plugin. -pub fn render_plugin(session: &mut Session) { +pub fn render_plugin(session: &mut SessionBuilder) { session .install_plugin(sprite::sprite_plugin) .install_plugin(camera::plugin); diff --git a/framework_crates/bones_framework/src/render/camera.rs b/framework_crates/bones_framework/src/render/camera.rs index c5d87b668a..90be8e37b6 100644 --- a/framework_crates/bones_framework/src/render/camera.rs +++ b/framework_crates/bones_framework/src/render/camera.rs @@ -92,7 +92,7 @@ pub fn spawn_default_camera( } /// Install the camera utilities on the given [`SystemStages`]. -pub fn plugin(session: &mut Session) { +pub fn plugin(session: &mut SessionBuilder) { session .stages .add_system_to_stage(CoreStage::Last, apply_shake) diff --git a/framework_crates/bones_framework/src/render/sprite.rs b/framework_crates/bones_framework/src/render/sprite.rs index 8ed5194f1f..14d1807002 100644 --- a/framework_crates/bones_framework/src/render/sprite.rs +++ b/framework_crates/bones_framework/src/render/sprite.rs @@ -3,7 +3,7 @@ use crate::prelude::*; /// Sprite session plugin. -pub fn sprite_plugin(_session: &mut Session) { +pub fn sprite_plugin(_session: &mut SessionBuilder) { Sprite::register_schema(); AtlasSprite::register_schema(); } diff --git a/framework_crates/bones_framework/src/render/ui.rs b/framework_crates/bones_framework/src/render/ui.rs index a0f553099e..d47dd770e8 100644 --- a/framework_crates/bones_framework/src/render/ui.rs +++ b/framework_crates/bones_framework/src/render/ui.rs @@ -10,7 +10,7 @@ use serde::Deserialize; pub mod widgets; /// The Bones Framework UI plugin. -pub fn ui_plugin(_session: &mut Session) { +pub fn ui_plugin(_session: &mut SessionBuilder) { // TODO: remove this plugin if it remains unused. } diff --git a/framework_crates/bones_framework/tests/reset.rs b/framework_crates/bones_framework/tests/reset.rs new file mode 100644 index 0000000000..7c812021ee --- /dev/null +++ b/framework_crates/bones_framework/tests/reset.rs @@ -0,0 +1,201 @@ +use bones_framework::prelude::*; + +#[derive(HasSchema, Default, Clone)] +struct Counter(u32); + +/// Verify that startup systems run again after a world reset +#[test] +pub fn startup_system_reset() { + let mut game = Game::new(); + // Shared resource, should survive reset + game.init_shared_resource::(); + + // Session startup increments counter by 1 + game.sessions.create_with("game", |builder| { + builder.add_startup_system(|mut counter: ResMut| { + // Increment to 1 + counter.0 += 1; + }); + }); + + // Step twice, startup system should only run once + game.step(Instant::now()); + game.step(Instant::now()); + + // Verify startup system ran and incremented only once + assert_eq!(game.shared_resource::().unwrap().0, 1); + + // Add command that will trigger reset on next step + { + let game_session = game.sessions.get_mut("game").unwrap(); + game_session.world.init_resource::().add( + |mut reset: ResMutInit| { + reset.reset = true; + }, + ); + } + + // step again, world should be reset. Startup doesn't run until next step though. + game.step(Instant::now()); + + // step again to trigger startup + game.step(Instant::now()); + + // Shared resource is not included in reset, should be incremented 2nd time + assert_eq!(game.shared_resource::().unwrap().0, 2); +} + +/// Verify that single success systems run again (until success condition) +/// after a world reset +#[test] +pub fn single_success_system_reset() { + let mut game = Game::new(); + + // Session startup increments counter by 1 + game.sessions.create_with("game", |builder| { + builder.init_resource::(); + { + let res = builder.resource_mut::().unwrap(); + assert_eq!(res.0, 0); + } + // system + builder.add_single_success_system(|mut counter: ResMut| -> Option<()> { + // Increment until 2 + counter.0 += 1; + if counter.0 >= 2 { + return Some(()); + } + + None + }); + }); + + // Step three times, single success should've incremented counter to 2 and completed. + game.step(Instant::now()); + game.step(Instant::now()); + game.step(Instant::now()); + + // Verify startup system ran and incremented only once + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::().unwrap(); + assert_eq!(counter.0, 2); + } + + // Add command that will trigger reset on next step + { + let game_session = game.sessions.get_mut("game").unwrap(); + game_session.world.init_resource::().add( + |mut reset: ResMutInit| { + reset.reset = true; + }, + ); + } + + // step again, world should be reset after this step. Counter should be back at default state of 0, + // single successs system not yet run until beginning of next step. + game.step(Instant::now()); + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::().unwrap(); + assert_eq!(counter.0, 0); + } + + // Startup resource should be re-initialized, and completion status of single single success system reset. + // It will run incrementing to 1. + game.step(Instant::now()); + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::().unwrap(); + assert_eq!(counter.0, 1); + } + + // Run a few more times, single success system should stop at 2: + game.step(Instant::now()); + game.step(Instant::now()); + game.step(Instant::now()); + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::().unwrap(); + assert_eq!(counter.0, 2); + } +} + +#[test] +pub fn reset_world_resource_override() { + let mut game = Game::new(); + + // insert counter resource + game.sessions.create_with("game", |builder| { + builder.init_resource::(); + }); + + game.step(Instant::now()); + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::().unwrap(); + // asert in default state of 0 + assert_eq!(counter.0, 0); + } + + // Add command that will trigger reset on next step, + // and add reset resource of Counter with value 1 + { + let game_session = game.sessions.get_mut("game").unwrap(); + game_session.world.init_resource::().add( + |mut reset: ResMutInit| { + reset.reset = true; + reset.insert_reset_resource(Counter(1)); + }, + ); + } + + // Step to reset + game.step(Instant::now()); + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::().unwrap(); + // Verify the reset resource of value 1 was applied, instead of resetting to default state + assert_eq!(counter.0, 1); + } +} + +#[test] +pub fn reset_world_emtpy_resource() { + let mut game = Game::new(); + + // insert counter resource + game.sessions.create_with("game", |builder| { + builder.init_resource::(); + }); + + game.step(Instant::now()); + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::().unwrap(); + // asert in default state of 0 + assert_eq!(counter.0, 0); + } + + // Add command that will trigger reset on next step, + // and add reset resource of Counter with value 1 + { + let game_session = game.sessions.get_mut("game").unwrap(); + game_session.world.init_resource::().add( + |mut reset: ResMutInit| { + reset.reset = true; + reset.insert_empty_reset_resource::(); + }, + ); + } + + // Step to reset + game.step(Instant::now()); + { + let session = game.sessions.get("game").unwrap(); + let counter = session.world.get_resource::(); + + // Verify resource was removed instead of reseting to initial state of session build. + assert!(counter.is_none()) + } +} diff --git a/framework_crates/bones_lib/Cargo.toml b/framework_crates/bones_lib/Cargo.toml index e31b250b3c..af7d98b511 100644 --- a/framework_crates/bones_lib/Cargo.toml +++ b/framework_crates/bones_lib/Cargo.toml @@ -17,4 +17,5 @@ glam = ["bones_ecs/glam"] [dependencies] bones_ecs = { version = "0.4.0", path = "../bones_ecs" } instant = "0.1.12" +tracing = { workspace = true } ustr = { workspace = true } diff --git a/framework_crates/bones_lib/src/lib.rs b/framework_crates/bones_lib/src/lib.rs index 2264330c09..7bd8b09a24 100644 --- a/framework_crates/bones_lib/src/lib.rs +++ b/framework_crates/bones_lib/src/lib.rs @@ -11,20 +11,213 @@ pub use bones_ecs as ecs; /// Bones lib prelude pub mod prelude { pub use crate::{ - ecs::prelude::*, instant::Instant, time::*, Game, GamePlugin, Session, SessionCommand, - SessionOptions, SessionPlugin, SessionRunner, Sessions, + ecs::prelude::*, instant::Instant, reset::*, time::*, Game, GamePlugin, Session, + SessionBuilder, SessionCommand, SessionOptions, SessionPlugin, SessionRunner, Sessions, }; pub use ustr::{ustr, Ustr, UstrMap, UstrSet}; } pub use instant; +pub mod reset; pub mod time; use std::{collections::VecDeque, fmt::Debug, sync::Arc}; +use tracing::warn; use crate::prelude::*; +/// Builder type used to create [`Session`]. If using this directly (as opposed to [`Sessions::create_with`]), +/// it is important to rember to finish session and add to [`Sessions`] with [`SessionBuilder::finish_and_add`]. +pub struct SessionBuilder { + /// Name of session + pub name: Ustr, + /// System stage builder + pub stages: SystemStagesBuilder, + /// Whether or not this session should have it's systems run. + pub active: bool, + /// Whether or not this session should be rendered. + pub visible: bool, + /// The priority of this session relative to other sessions in the [`Game`]. + pub priority: i32, + /// The session runner to use for this session. + pub runner: Box, + + /// Tracks if builder has been finished, and warn on drop if not finished. + finish_guard: FinishGuard, +} + +impl SessionBuilder { + /// Create a new [`SessionBuilder`]. Be sure to add it to [`Sessions`] when finished, with [`SessionBuilder::finish_and_add`], or [`Sessions::create`]. + /// + /// # Panics + /// + /// Panics if the `name.try_into()` cannot convert into [`Ustr`]. + pub fn new>(name: N) -> Self + where + >::Error: Debug, + { + let name = name + .try_into() + .expect("Session name could not be converted into Ustr."); + Self { + name, + stages: default(), + active: true, + visible: true, + priority: 0, + runner: Box::new(DefaultSessionRunner), + finish_guard: FinishGuard { finished: false }, + } + } + + /// Get the [`SystemStagesBuilder`] (though the stage build functions are also on [`SessionBuilder`] for convenience). + pub fn stages(&mut self) -> &mut SystemStagesBuilder { + &mut self.stages + } + + /// Whether or not session should run systems. + pub fn set_active(&mut self, active: bool) -> &mut Self { + self.active = active; + self + } + + /// Whether or not session should be rendered. + pub fn set_visible(&mut self, visible: bool) -> &mut Self { + self.visible = visible; + self + } + + /// The priority of this session relative to other sessions in the [`Game`]. + pub fn set_priority(&mut self, priority: i32) -> &mut Self { + self.priority = priority; + self + } + + /// Insert a resource. + /// + /// Note: The resource is not actually initialized in World until first step of [`SystemStages`]. + /// To mutate or inspect a resource inserted by another [`SessionPlugin`] during session build, use [`SessionBuilder::resource_mut`]. + pub fn insert_resource(&mut self, resource: T) -> &mut Self { + self.stages.insert_startup_resource(resource); + self + } + + /// Insert a resource using default value (if not found). Returns a mutable ref for modification. + /// + /// Note: The resource is not actually initialized in World until first step of [`SystemStages`]. + /// To mutate or inspect a resource inserted by another [`SessionPlugin`] during session build, use [`SessionBuilder::resource_mut`]. + pub fn init_resource(&mut self) -> RefMut { + self.stages.init_startup_resource::() + } + + /// Get mutable reference to a resource if it exists. + pub fn resource_mut(&self) -> Option> { + self.stages.startup_resource_mut::() + } + + /// Add a system that will run only once, before all of the other non-startup systems. + /// If wish to reset startup systems during gameplay and run again, can modify [`SessionStarted`] resource in world. + pub fn add_startup_system(&mut self, system: S) -> &mut Self + where + S: IntoSystem>, + { + self.stages.add_startup_system(system.system()); + self + } + + /// 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 `?`. + pub fn add_single_success_system(&mut self, system: S) -> &mut Self + where + S: IntoSystem, Sys = StaticSystem<(), Option<()>>>, + { + self.stages.add_single_success_system(system.system()); + self + } + + /// Add a [`System`] to the stage with the given label. + pub fn add_system_to_stage(&mut self, label: impl StageLabel, system: S) -> &mut Self + where + S: IntoSystem>, + { + self.stages.add_system_to_stage(label, system); + self + } + + /// Insert a new stage, before another existing stage + pub fn insert_stage_before( + &mut self, + label: L, + stage: S, + ) -> &mut SessionBuilder { + self.stages.insert_stage_before(label, stage); + self + } + + /// Insert a new stage, after another existing stage + pub fn insert_stage_after( + &mut self, + label: L, + stage: S, + ) -> &mut SessionBuilder { + self.stages.insert_stage_after(label, stage); + + self + } + + /// Set the session runner for this session. + pub fn set_session_runner(&mut self, runner: Box) { + self.runner = runner; + } + + /// Install a plugin. + pub fn install_plugin(&mut self, plugin: impl SessionPlugin) -> &mut Self { + plugin.install(self); + self + } + + /// Finalize and add to [`Sessions`]. + /// + /// Alternatively, you may directly pass a [`SessionBuilder`] to [`Sessions::create`] to add and finalize. + pub fn finish_and_add(mut self, sessions: &mut Sessions) -> &mut Session { + let session = Session { + world: { + let mut w = World::default(); + w.init_resource::