diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d49c6364..9c08907d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,8 +186,6 @@ jobs: hot, edit, launcher, - remote, - asset, initialize_resource, update_resource, reset_resource, @@ -198,6 +196,9 @@ jobs: clear_on_reload, setup_on_reload, setup_in_state, + replacable_state, + remote, + asset, ] runs-on: ${{matrix.os}} steps: diff --git a/README.md b/README.md index 84705bab..38377d23 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Fuller documentation is available at: ()) - .add_state::() .add_plugins(WorldInspectorPlugin::new()) .add_systems(Startup, setup) .setup_reloadable_elements::() @@ -36,21 +35,34 @@ pub fn bevy_main(initial_plugins: impl InitialPlugins) { .run(); } -#[derive(States, PartialEq, Eq, Clone, Copy, Debug, Hash, Default)] +#[derive(States, PartialEq, Eq, Clone, Copy, Debug, Hash, Default, Serialize, Deserialize)] pub enum AppState { #[default] - State, AnotherState, + State, + TwoSpheres, +} + +impl ReplacableState for AppState { + fn get_type_name() -> &'static str { + "app-state" + } + + fn get_next_type_name() -> &'static str { + "next-app-state" + } } #[dexterous_developer_setup(first_reloadable)] fn reloadable(app: &mut ReloadableAppContents) { + app.add_state::(); println!("Setting up reloadabless #1"); app.add_systems(Update, (move_cube, toggle)); println!("Reset Setup"); app.reset_setup::(setup_cube); println!("Reset Setup In State"); app.reset_setup_in_state::(AppState::AnotherState, setup_sphere); + app.reset_setup_in_state::(AppState::TwoSpheres, setup_two_spheres); println!("Done"); } @@ -147,6 +159,37 @@ fn setup_sphere( )); } +fn setup_two_spheres( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Sphere, + PbrBundle { + mesh: meshes.add(Mesh::from(shape::UVSphere { + radius: 0.2, + ..Default::default() + })), + material: materials.add(Color::PINK.into()), + transform: Transform::from_xyz(1.0, 0.5, 0.0), + ..default() + }, + )); + commands.spawn(( + Sphere, + PbrBundle { + mesh: meshes.add(Mesh::from(shape::UVSphere { + radius: 0.2, + ..Default::default() + })), + material: materials.add(Color::PINK.into()), + transform: Transform::from_xyz(-1.0, 0.5, 0.0), + ..default() + }, + )); +} + #[allow(unused)] fn setup( mut commands: Commands, @@ -201,7 +244,8 @@ fn toggle(input: Res>, mut commands: Commands, current: Res AppState::AnotherState, - AppState::AnotherState => AppState::State, + AppState::AnotherState => AppState::TwoSpheres, + AppState::TwoSpheres => AppState::State, }; commands.insert_resource(NextState(Some(next))); } diff --git a/dexterous_developer_internal/src/bevy_support/cold.rs b/dexterous_developer_internal/src/bevy_support/cold.rs index 7b940307..fb34ad35 100644 --- a/dexterous_developer_internal/src/bevy_support/cold.rs +++ b/dexterous_developer_internal/src/bevy_support/cold.rs @@ -25,7 +25,7 @@ impl<'a> ReloadableApp for ReloadableAppContents<'a> { self } - fn insert_replacable_resource(&mut self) -> &mut Self { + fn insert_replacable_resource(&mut self) -> &mut Self { self.0.init_resource::(); self } @@ -69,4 +69,9 @@ impl<'a> ReloadableApp for ReloadableAppContents<'a> { .add_systems(OnExit(state), clear_marked_system::); self } + + fn add_state(&mut self) -> &mut Self { + self.0.add_state::(); + self + } } diff --git a/dexterous_developer_internal/src/bevy_support/hot_internal/reloadable_app.rs b/dexterous_developer_internal/src/bevy_support/hot_internal/reloadable_app.rs index db24bb6a..dafb4acf 100644 --- a/dexterous_developer_internal/src/bevy_support/hot_internal/reloadable_app.rs +++ b/dexterous_developer_internal/src/bevy_support/hot_internal/reloadable_app.rs @@ -1,5 +1,6 @@ use bevy::{ - ecs::schedule::ScheduleLabel, + ecs::schedule::common_conditions::run_once, + ecs::schedule::{run_enter_schedule, ScheduleLabel}, prelude::*, utils::{HashMap, HashSet}, }; @@ -68,7 +69,7 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> { self } - fn insert_replacable_resource(&mut self) -> &mut Self { + fn insert_replacable_resource(&mut self) -> &mut Self { let name = R::get_type_name(); if !self.resources.contains(name) { self.resources.insert(name.to_string()); @@ -176,6 +177,21 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> { ), ) } + + fn add_state(&mut self) -> &mut Self { + self.insert_replacable_resource::>() + .insert_replacable_resource::>() + .add_systems( + StateTransition, + (( + run_enter_schedule::.run_if(run_once()), + apply_state_transition::, + ) + .chain(),), + ); + + self + } } fn element_selection_condition(name: &'static str) -> impl Fn(Option>) -> bool { diff --git a/dexterous_developer_internal/src/bevy_support/hot_internal/replacable_types.rs b/dexterous_developer_internal/src/bevy_support/hot_internal/replacable_types.rs index 46453f15..2dd59953 100644 --- a/dexterous_developer_internal/src/bevy_support/hot_internal/replacable_types.rs +++ b/dexterous_developer_internal/src/bevy_support/hot_internal/replacable_types.rs @@ -3,14 +3,14 @@ use bevy::{ utils::HashMap, }; -use crate::{ReplacableComponent, ReplacableResource}; +use crate::{CustomReplacableResource, ReplacableComponent}; #[derive(Resource, Default)] pub struct ReplacableResourceStore { map: HashMap>, } -pub fn serialize_replacable_resource( +pub fn serialize_replacable_resource( mut store: ResMut, resource: Option>, mut commands: Commands, @@ -18,14 +18,14 @@ pub fn serialize_replacable_resource( let Some(resource) = resource else { return; }; - if let Ok(v) = rmp_serde::to_vec(resource.as_ref()) { + if let Ok(v) = resource.to_vec() { store.map.insert(R::get_type_name().to_string(), v); } commands.remove_resource::(); } -pub fn deserialize_replacable_resource( +pub fn deserialize_replacable_resource( store: Res, mut commands: Commands, ) { @@ -34,7 +34,7 @@ pub fn deserialize_replacable_resource( let v: R = store .map .get(name) - .and_then(|v| rmp_serde::from_slice(v.as_slice()).ok()) + .and_then(|v| R::from_slice(v).ok()) .unwrap_or_default(); commands.insert_resource(v); diff --git a/dexterous_developer_internal/src/bevy_support/types.rs b/dexterous_developer_internal/src/bevy_support/types.rs index 6752bf52..71026ad7 100644 --- a/dexterous_developer_internal/src/bevy_support/types.rs +++ b/dexterous_developer_internal/src/bevy_support/types.rs @@ -5,10 +5,67 @@ pub trait ReplacableResource: Resource + Serialize + DeserializeOwned + Default fn get_type_name() -> &'static str; } +pub trait CustomReplacableResource: Resource + Default { + fn get_type_name() -> &'static str; + + fn to_vec(&self) -> anyhow::Result>; + + fn from_slice(val: &[u8]) -> anyhow::Result; +} + +impl CustomReplacableResource for T { + fn get_type_name() -> &'static str { + T::get_type_name() + } + + fn to_vec(&self) -> anyhow::Result> { + Ok(rmp_serde::to_vec(self)?) + } + + fn from_slice(val: &[u8]) -> anyhow::Result { + Ok(rmp_serde::from_slice(val)?) + } +} + pub trait ReplacableComponent: Component + Serialize + DeserializeOwned + Default { fn get_type_name() -> &'static str; } +pub trait ReplacableState: States + Serialize + DeserializeOwned { + fn get_type_name() -> &'static str; + fn get_next_type_name() -> &'static str; +} + +impl CustomReplacableResource for State { + fn get_type_name() -> &'static str { + S::get_type_name() + } + + fn to_vec(&self) -> anyhow::Result> { + Ok(rmp_serde::to_vec(self.get())?) + } + + fn from_slice(val: &[u8]) -> anyhow::Result { + let val = rmp_serde::from_slice(val)?; + Ok(Self::new(val)) + } +} + +impl CustomReplacableResource for NextState { + fn get_type_name() -> &'static str { + S::get_next_type_name() + } + + fn to_vec(&self) -> anyhow::Result> { + Ok(rmp_serde::to_vec(&self.0)?) + } + + fn from_slice(val: &[u8]) -> anyhow::Result { + let val = rmp_serde::from_slice(val)?; + Ok(Self(val)) + } +} + pub(crate) mod private { pub trait ReloadableAppSealed {} } @@ -20,7 +77,7 @@ pub trait ReloadableApp: private::ReloadableAppSealed { systems: impl IntoSystemConfigs, ) -> &mut Self; - fn insert_replacable_resource(&mut self) -> &mut Self; + fn insert_replacable_resource(&mut self) -> &mut Self; fn reset_resource(&mut self) -> &mut Self; fn reset_resource_to_value(&mut self, value: R) -> &mut Self; fn register_replacable_component(&mut self) -> &mut Self; @@ -31,6 +88,7 @@ pub trait ReloadableApp: private::ReloadableAppSealed { state: S, systems: impl IntoSystemConfigs, ) -> &mut Self; + fn add_state(&mut self) -> &mut Self; } pub trait ReloadableSetup { diff --git a/docs/src/Intro.md b/docs/src/Intro.md index d600c222..a4b546ae 100644 --- a/docs/src/Intro.md +++ b/docs/src/Intro.md @@ -4,7 +4,7 @@ This library provides an experimental hot reload system for Bevy. ## Features -- Define the reloadable areas of your game explicitly - which can include systems, components and resources (w/ some limitations) +- Define the reloadable areas of your game explicitly - which can include systems, components, state and resources (w/ some limitations) - Reset resources to a default or pre-determined value upon reload - serialize/deserialize your reloadable resources & components, allowing you to evolve their schemas so long as they are compatible with the de-serializer (using rmp_serde) - mark entities to get removed on hot reload diff --git a/docs/src/reloadables.md b/docs/src/reloadables.md index 4c4fb8ca..7bb36b6a 100644 --- a/docs/src/reloadables.md +++ b/docs/src/reloadables.md @@ -35,9 +35,9 @@ These are added using `.add_systems`, exactly like you would add a system to a n These are special systems, that are supposed to set things up - which might need to be re-done upon a reload. The classic example is a game UI, which you might want to re-build after a reload. There are a few helpers for these types of systems -First, we can clear all entites that have a marker component and then run our setup function using `.reset_setup(systems_go_here)`. -And if we want it to only happen on entering a specific state (or re-loading while within that state), we can use `.reset_setup_in_state(state, systems)`. -Alternatively, if we just want to clear stuff out on a reload, we can use a marker component and call `.clear_marked_on_reload()`. +First, we can clear all entites that have a marker component and then run our setup function using `.reset_setup::(systems_go_here)`. +And if we want it to only happen on entering a specific state (or re-loading while within that state), we can use `.reset_setup_in_state::(state, systems)`. +Alternatively, if we just want to clear stuff out on a reload, we can use a marker component and call `.clear_marked_on_reload::()`. ## Resources @@ -45,7 +45,7 @@ Reloading resources is a little more complex - so we have a few variations ### Reset on Reload -If you want to reset a resource when the library reloads, you can use either `.reset_resource()` which uses it's default value, or `.reset_resource_to_value(value)` which uses a value you provide. +If you want to reset a resource when the library reloads, you can use either `.reset_resource::()` which uses it's default value, or `.reset_resource_to_value(value)` which uses a value you provide. ### Replaceable Resources @@ -62,11 +62,11 @@ impl ReplacableResource { } ``` -Then, you can register it using `.insert_replacable_resource()`. This will cause the resource to be serialized before the library is reloaded, and replaced with a new version after reload. Since serialization is done using msgpack, it should be able to cope with adding new fields or removing old ones - but keep in mind the way serde handles that kind of stuff. +Then, you can register it using `.insert_replacable_resource::()`. This will cause the resource to be serialized before the library is reloaded, and replaced with a new version after reload. Since serialization is done using msgpack, it should be able to cope with adding new fields or removing old ones - but keep in mind the way serde handles that kind of stuff. ## Replacable Components -The last type of reloadable are replacable components. These function like replacable resources, but involve replacing components on various entities. Here you implement the `ReplacableComponent` trait: +You can also set up replacable components. These function like replacable resources, but involve replacing components on various entities. Here you implement the `ReplacableComponent` trait: ```rust #[derive(Component, Serialize, Deserialize, Default)] @@ -80,4 +80,33 @@ impl ReplacableComponent { ``` -And then register it using `.register_replacable_component()`. +And then register it using `.register_replacable_component::()`. + +## Replacable State + +States are also possible to set up as replacable. Here you implement the `ReplacableState` trait: + +```rust +#[derive(States, PartialEq, Eq, Clone, Copy, Debug, Hash, Default, Serialize, Deserialize)] +pub enum AppState { + #[default] + A, + B, +} + + +impl ReplacableState for AppState { + fn get_type_name() -> &'static str { + "app-state" + } + + fn get_next_type_name() -> &'static str { + "next-app-state" + } +} + +``` + +Note that unlike `ReplacableResource` or `ReplacableComponent`, with `ReplacableState` you need to give it a name as well as giving a name for the `NextState` resource it'll create. + +You can then add the state using `.add_state::()`. diff --git a/testing/dexterous_developer_tests/src/insert_replacable_state.txt b/testing/dexterous_developer_tests/src/insert_replacable_state.txt new file mode 100644 index 00000000..0b56e011 --- /dev/null +++ b/testing/dexterous_developer_tests/src/insert_replacable_state.txt @@ -0,0 +1,52 @@ +use bevy::prelude::*; +use dexterous_developer::*; +use serde::{Deserialize, Serialize}; +use crate::shared::{StdInput}; + +fn update_a(input: Res, mut next: ResMut>) { + let text = input.0.as_str(); + println!("Got: {text}"); + if text == "toggle" { + next.set(AppState::B); + } +} + +fn update_b(input: Res, mut next: ResMut>) { + let text = input.0.as_str(); + println!("Received: {text}"); + if text == "toggle" { + next.set(AppState::A); + } +} + + +fn startup() { + println!("Press Enter to Progress, or type 'exit' to exit"); +} + +#[derive(States, PartialEq, Eq, Clone, Copy, Debug, Hash, Default, Serialize, Deserialize)] +pub enum AppState { + #[default] + A, + B, +} + + +impl ReplacableState for AppState { + fn get_type_name() -> &'static str { + "app-state" + } + + fn get_next_type_name() -> &'static str { + "next-app-state" + } +} + +#[dexterous_developer_setup] +pub fn reloadable(app: &mut ReloadableAppContents) { + app + .add_state::() + .add_systems(Startup, startup) + .add_systems(Update, update_a.run_if(in_state(AppState::A))) + .add_systems(Update, update_b.run_if(in_state(AppState::B))); +} \ No newline at end of file diff --git a/testing/dexterous_developer_tests/src/lib.rs b/testing/dexterous_developer_tests/src/lib.rs index 025c98b8..9a0d5f19 100644 --- a/testing/dexterous_developer_tests/src/lib.rs +++ b/testing/dexterous_developer_tests/src/lib.rs @@ -595,6 +595,68 @@ async fn can_run_existing(path: &Path) { process.exit().await; } +async fn replacable_state() { + let mut project: TestProject = + TestProject::new("reloadables_test", "insert_replacable_state").unwrap(); + let mut process = project.run_hot_cli().await.unwrap(); + + process.is_ready().await; + + process.send("\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Ran Update"]).await; + + project + .write_file( + PathBuf::from("src/update.rs").as_path(), + include_str!("./insert_replacable_state.txt"), + ) + .expect("Couldn't update file"); + + process.has_updated().await; + + process.send("test\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Got: test"]).await; + + process.send("toggle\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Got: toggle"]).await; + + process.send("test\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Received: test"]).await; + + project + .write_file( + PathBuf::from("src/update.rs").as_path(), + include_str!("./update_replacable_state.txt"), + ) + .expect("Couldn't update file"); + + process.has_updated().await; + + process.send("test\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Received: test"]).await; + process.send("toggle\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Received: toggle"]).await; + + process.send("test\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Input: test"]).await; + process.send("toggle\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Input: toggle"]).await; + + process.send("test\n").expect("Failed to send empty line"); + + process.wait_for_lines(&["Got: test"]).await; + + process.exit().await; +} + pub async fn run_tests() { let mut args = env::args(); args.next(); @@ -647,6 +709,9 @@ pub async fn run_tests() { "setup_in_state" => { run_setup_in_state().await; } + "replacable_state" => { + replacable_state().await; + } "remote" => { println!("Can run remote"); can_run_remote().await; diff --git a/testing/dexterous_developer_tests/src/update_replacable_state.txt b/testing/dexterous_developer_tests/src/update_replacable_state.txt new file mode 100644 index 00000000..d70ef45a --- /dev/null +++ b/testing/dexterous_developer_tests/src/update_replacable_state.txt @@ -0,0 +1,62 @@ +use bevy::prelude::*; +use dexterous_developer::*; +use serde::{Deserialize, Serialize}; +use crate::shared::{StdInput}; + +fn update_a(input: Res, mut next: ResMut>) { + let text = input.0.as_str(); + println!("Got: {text}"); + if text == "toggle" { + next.set(AppState::B); + } +} + +fn update_b(input: Res, mut next: ResMut>) { + let text = input.0.as_str(); + println!("Received: {text}"); + if text == "toggle" { + next.set(AppState::C); + } +} +fn update_c(input: Res, mut next: ResMut>) { + let text = input.0.as_str(); + println!("Input: {text}"); + if text == "toggle" { + next.set(AppState::A); + } +} + + + +fn startup() { + println!("Press Enter to Progress, or type 'exit' to exit"); +} + +#[derive(States, PartialEq, Eq, Clone, Copy, Debug, Hash, Default, Serialize, Deserialize)] +pub enum AppState { + #[default] + A, + B, + C, +} + + +impl ReplacableState for AppState { + fn get_type_name() -> &'static str { + "app-state" + } + + fn get_next_type_name() -> &'static str { + "next-app-state" + } +} + +#[dexterous_developer_setup] +pub fn reloadable(app: &mut ReloadableAppContents) { + app + .add_state::() + .add_systems(Startup, startup) + .add_systems(Update, update_a.run_if(in_state(AppState::A))) + .add_systems(Update, update_b.run_if(in_state(AppState::B))) + .add_systems(Update, update_c.run_if(in_state(AppState::C))); +} \ No newline at end of file