Repository: TheBevyFlock/bevy_simple_subsecond_system
Pull request #12: Added reloadable, the ability to add and remove update systems
Author: login: MalekiRe
Created: 2025-05-22T21:56:20Z | Merged: 2025-05-24T00:39:30Z

You can now add Update systems inside the `reloadable` closure and you can add and remove them at will, writing new bevy systems during hot reloading. Also it doesn't even need the hot macro.


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 1152ca1..fa65a66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1059,13 +1059,16 @@ version = "0.1.10"
 dependencies = [
  "bevy",
  "bevy_app",
+ "bevy_derive",
  "bevy_ecs",
  "bevy_ecs_macros",
+ "bevy_internal",
  "bevy_log",
  "bevy_platform",
  "bevy_simple_subsecond_system_macros",
  "crossbeam-channel",
  "dioxus-devtools",
+ "send_wrapper",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 588b22b..0f2038c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,14 +18,16 @@ categories = ["game-development"]
 bevy_ecs = { version = "0.16.0" }
 bevy_log = { version = "0.16.0" }
 bevy_app = { version = "0.16.0" }
+bevy_derive = { version = "0.16.0" }
 bevy_platform = { version = "0.16.0" }
 bevy_ecs_macros = { version = "0.16.0" }
+bevy_internal = { version = "0.16.0", features = ["track_location"] }
 dioxus-devtools = { version = "0.7.0-alpha.0" }
 bevy_simple_subsecond_system_macros = { path = "macros", version = "0.1.6" }
 crossbeam-channel = "0.5.15"
-
+send_wrapper = "0.6.0"
 [dev-dependencies]
-bevy = { version = "0.16.0", features = ["bevy_ui_debug"] }
+bevy = { version = "0.16.0", features = ["bevy_ui_debug", "track_location"] }
 
 [lints.clippy]
 # Bevy supplies arguments to systems via dependency injection, so it's natural for systems to
diff --git a/examples/add_systems.rs b/examples/add_systems.rs
new file mode 100644
index 0000000..b035af7
--- /dev/null
+++ b/examples/add_systems.rs
@@ -0,0 +1,65 @@
+use bevy::{input::common_conditions::input_just_pressed, prelude::*};
+use bevy_simple_subsecond_system::prelude::*;
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        .add_plugins(SimpleSubsecondPlugin::default())
+        // try adding and removing systems from here! make whole new ones!
+        .with_hot_patch(|app: &mut App| {
+            // StartupRerunHotPatch is like Startup, but will rerun on hot-reload.
+            // You need the #[hot(hot_patch_signature = true)] macro to auto-despawn entities spawned in it!
+            app.add_systems(StartupRerunHotPatch, spawn_ui);
+            // All other systems do not require `#[hot]`.
+            // Try writing, adding, and removing new ones here at runtime!
+            app.add_systems(Update, print_hello);
+            app.add_systems(
+                Update,
+                change_text.run_if(input_just_pressed(KeyCode::Space)),
+            );
+        })
+        .run();
+}
+
+#[hot(hot_patch_signature = true)]
+fn spawn_ui(mut commands: Commands) {
+    commands.queue(|world: &mut World| {
+        // Currently bevy forgets to do `track_caller` on `commands.spawn` so to
+        // auto-despawn entities spawned inside a StartupRerunHotPatch schedule
+        // we need to call spawn on `world` instead.
+        world.spawn((
+            Node {
+                // You can change the `Node` however you want at runtime
+                position_type: PositionType::Absolute,
+                width: Val::Percent(100.0),
+                height: Val::Percent(100.0),
+                align_items: AlignItems::Center,
+                justify_content: JustifyContent::Center,
+                flex_direction: FlexDirection::Column,
+                row_gap: Val::Px(20.0),
+                ..default()
+            },
+            children![
+                Text::new("Press space to change the text below:"),
+                (
+                    Text::new("(no button pressed yet, or this system was reset)"),
+                    InfoLabel
+                ),
+            ],
+        ));
+        world.spawn(Camera2d);
+    });
+}
+
+fn print_hello() {
+    info_once!("Hello, world!");
+}
+
+#[derive(Component)]
+struct InfoLabel;
+
+fn change_text(mut query: Query<&mut Text, With<InfoLabel>>, time: Res<Time>) {
+    for mut text in &mut query {
+        text.0 = format!("You pressed the space key at t = {} s", time.elapsed_secs());
+    }
+}
diff --git a/examples/patch_on_update.rs b/examples/patch_on_update.rs
index 8a8360b..f4de266 100644
--- a/examples/patch_on_update.rs
+++ b/examples/patch_on_update.rs
@@ -1,11 +1,17 @@
 use bevy::prelude::*;
 use bevy_simple_subsecond_system::prelude::*;
+
 fn main() {
     App::new()
         .add_plugins(DefaultPlugins)
         .add_plugins(SimpleSubsecondPlugin::default())
+        .add_event::<MyEvent>()
         .add_systems(Startup, setup)
-        .add_systems(Update, configure_ui)
+        .with_hot_patch(|app: &mut App| {
+            app.add_systems(Update, some_reader);
+            app.add_systems(Update, write_event);
+            app.add_systems(Update, configure_ui);
+        })
         .run();
 }
 
@@ -18,6 +24,22 @@ fn setup(mut commands: Commands) {
     commands.spawn(Camera2d);
 }
 
+#[derive(Event)]
+pub struct MyEvent;
+
+fn write_event(mut event_writer: EventWriter<MyEvent>, key: Res<ButtonInput<KeyCode>>) {
+    if key.just_pressed(KeyCode::Space) {
+        println!("Writing");
+        event_writer.write(MyEvent);
+    }
+}
+
+fn some_reader(mut event_reader: EventReader<MyEvent>) {
+    for _ in event_reader.read() {
+        println!("read eventtttttttt");
+    }
+}
+
 #[hot]
 fn configure_ui(ui: Single<Entity, With<Ui>>, mut commands: Commands) {
     commands.entity(*ui).despawn_related::<Children>().insert((
@@ -33,8 +55,8 @@ fn configure_ui(ui: Single<Entity, With<Ui>>, mut commands: Commands) {
             ..default()
         },
         children![
-            Text::new("Hello, world!"),
-            Text::new("Try adding new texts below"),
+            Text::new("Hello, worldaa!"),
+            Text::new("Try adding new texts below awa"),
         ],
     ));
 }
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 4e8d6af..6ad1b76 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -1,5 +1,6 @@
 use proc_macro::TokenStream;
 use quote::{format_ident, quote};
+use syn::spanned::Spanned;
 use syn::{
     FnArg, Ident, ItemFn, LitBool, Pat, PatIdent, ReturnType, Token, Type, TypePath, TypeReference,
     parse::{Parse, ParseStream},
@@ -70,6 +71,12 @@ pub fn hot(attr: TokenStream, item: TokenStream) -> TokenStream {
     let hotpatched_fn = format_ident!("__{}_hotpatched", original_fn_name);
     let original_wrapper_fn = format_ident!("__{}_original", original_fn_name);
 
+    let newlines = if let Some(source_text) = block.span().unwrap().source_text() {
+        source_text.chars().filter(|ch| *ch == '\n').count() as u32
+    } else {
+        0
+    };
+
     // Capture parameter types, names, and mutability
     let mut param_types = Vec::new();
     let mut param_idents = Vec::new();
@@ -166,11 +173,17 @@ pub fn hot(attr: TokenStream, item: TokenStream) -> TokenStream {
     let hotpatched_fn_definition = match has_single_world_param(sig) {
         WorldParam::Mut | WorldParam::Ref => quote! {
             #vis fn #hotpatched_fn #impl_generics(world: &mut ::bevy_simple_subsecond_system::__macros_internal::World) #where_clause #original_output {
+                if let Some(mut reload_positions) = world.get_resource_mut::<::bevy_simple_subsecond_system::__macros_internal::__ReloadPositions>() {
+                    reload_positions.insert((file!(), line!(), line!() + #newlines));
+                }
                 #original_wrapper_fn #maybe_generics(world)
             }
         },
         WorldParam::None => quote! {
             #vis fn #hotpatched_fn #impl_generics(world: &mut ::bevy_simple_subsecond_system::__macros_internal::World) #where_clause #original_output {
+                if let Some(mut reload_positions) = world.get_resource_mut::<::bevy_simple_subsecond_system::__macros_internal::__ReloadPositions>() {
+                    reload_positions.insert((file!(), line!(), line!() + #newlines));
+                }
                 use ::bevy_simple_subsecond_system::__macros_internal::SystemState;
                 let mut __system_state: SystemState<(#(#param_types),*)> = SystemState::new(world);
                 let __unsafe_world = world.as_unsafe_world_cell_readonly();
diff --git a/src/hot_patched_app.rs b/src/hot_patched_app.rs
new file mode 100644
index 0000000..5e2af21
--- /dev/null
+++ b/src/hot_patched_app.rs
@@ -0,0 +1,269 @@
+//! API for hot-patching new systems into your running app.
+//! See [`HotPatchedAppExt::with_hot_patch`] for the main API.
+
+use crate::__macros_internal::__ReloadPositions as ReloadPositions;
+use bevy_app::{
+    App, NonSendMarker, PostStartup, PostUpdate, PreStartup, PreUpdate, Startup, Update,
+};
+use bevy_derive::{Deref, DerefMut};
+use bevy_ecs::prelude::*;
+#[cfg(all(not(target_family = "wasm"), debug_assertions))]
+use bevy_ecs::system::{Commands, Res};
+use bevy_ecs_macros::ScheduleLabel;
+use bevy_log::{debug, error};
+
+use crate::HotPatched;
+
+/// Wrapper around [`App`] used by [`HotPatchedAppExt::with_hot_patch`], which allows you to add and remove systems at runtime.
+#[derive(Deref, DerefMut)]
+struct HotPatchedApp(send_wrapper::SendWrapper<App>);
+
+impl Default for HotPatchedApp {
+    fn default() -> Self {
+        HotPatchedApp(send_wrapper::SendWrapper::new(App::default()))
+    }
+}
+
+/// The [`Startup`] schedule, but rerun on hot-reload.
+/// Only valid inside the context of [`HotPatchedAppExt::with_hot_patch`].
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct StartupRerunHotPatch;
+
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+struct HotPatchUpdate;
+
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+struct HotPatchPostUpdate;
+
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+struct HotPatchPreUpdate;
+
+/// Trait for [`App`] to add and remove systems at runtime.
+pub trait HotPatchedAppExt {
+    /// Call this with plugins and systems and it will auto-add and remove systems in the `Update` schedule to your running app.
+    ///
+    /// # Example
+    ///
+    /// ```ignore
+    /// # use bevy::prelude::*;
+    /// # use bevy_simple_subsecond_system::prelude::*;
+    ///
+    /// App::new()
+    ///     .add_plugins(DefaultPlugins)
+    ///     .add_plugins(SimpleSubsecondPlugin::default())
+    ///     .with_hot_patch(|app: &mut App| {
+    ///         // Systems in the `StartupRerunHotPatch` schedule will be rerun on hot-reload.
+    ///         // They require `#[hot(hot_patch_signature = true)]`
+    ///         app.add_systems(StartupRerunHotPatch, setup);
+    ///         // All other systems do not require `#[hot]`.
+    ///         app.add_systems(Update, my_system);
+    ///         app.add_systems(PostUpdate, second_system);
+    ///     });
+    ///
+    /// #[hot(hot_patch_signature = true)]
+    /// fn setup(mut commands: Commands) {
+    ///     commands.spawn(Camera2d::default());
+    ///     commands.spawn(Text::new("Hello, world!"));
+    /// }
+    ///
+    /// fn my_system() {
+    ///     info!("Hello, world!");
+    /// }
+    ///
+    /// fn second_system() {
+    ///     info!("Goodbye, world!");
+    /// }
+    /// ```
+    fn with_hot_patch(&mut self, func: impl FnMut(&mut App) + Send + Sync + 'static) -> &mut App;
+}
+
+impl HotPatchedAppExt for App {
+    fn with_hot_patch(
+        &mut self,
+        mut func: impl FnMut(&mut App) + Send + Sync + 'static,
+    ) -> &mut App {
+        let mut app = App::new();
+        app.init_schedule(Startup);
+        app.init_schedule(PostStartup);
+        app.init_schedule(PreStartup);
+        std::mem::swap(
+            app.get_schedule_mut(Startup).unwrap(),
+            self.get_schedule_mut(Startup).unwrap(),
+        );
+        std::mem::swap(
+            app.get_schedule_mut(PreStartup).unwrap(),
+            self.get_schedule_mut(PreStartup).unwrap(),
+        );
+        std::mem::swap(
+            app.get_schedule_mut(PostStartup).unwrap(),
+            self.get_schedule_mut(PostStartup).unwrap(),
+        );
+
+        func(&mut app);
+
+        std::mem::swap(
+            app.get_schedule_mut(Startup).unwrap(),
+            self.get_schedule_mut(Startup).unwrap(),
+        );
+        std::mem::swap(
+            app.get_schedule_mut(PreStartup).unwrap(),
+            self.get_schedule_mut(PreStartup).unwrap(),
+        );
+        std::mem::swap(
+            app.get_schedule_mut(PostStartup).unwrap(),
+            self.get_schedule_mut(PostStartup).unwrap(),
+        );
+
+        self.add_systems(PreUpdate, |world: &mut World| {
+            let _ = world.try_run_schedule(HotPatchPreUpdate);
+        });
+        self.add_systems(Update, |world: &mut World| {
+            let _ = world.try_run_schedule(HotPatchUpdate);
+        });
+        self.add_systems(PostUpdate, |world: &mut World| {
+            let _ = world.try_run_schedule(HotPatchPostUpdate);
+        });
+
+        self.add_systems(Startup, |world: &mut World| {
+            world.insert_resource(ReloadPositions::default());
+        });
+
+        let hot_patched_func = move |mut hot_patched_app: HotPatchedApp| -> HotPatchedApp {
+            func(&mut hot_patched_app.0);
+            hot_patched_app
+        };
+        let reloadable_section =
+            std::sync::Mutex::new(dioxus_devtools::subsecond::HotFn::current(hot_patched_func));
+        self.add_systems(
+            PreUpdate,
+            move |_: Option<NonSend<NonSendMarker>>,
+                  mut ran_once: Local<bool>,
+                  mut schedules: ResMut<Schedules>,
+                  mut commands: Commands,
+                  hotreload_event: EventReader<HotPatched>| {
+                if hotreload_event.is_empty() {
+                    if *ran_once {
+                        return;
+                    }
+                    *ran_once = true;
+                }
+
+                let reload_app = reloadable_section
+                    .lock()
+                    .unwrap()
+                    .try_call((HotPatchedApp::default(),));
+
+                let mut reload_app = match reload_app {
+                    Ok(reload_app) => reload_app,
+                    Err(e) => {
+                        error!("Failed to call hotpatch function: {e:?}");
+                        return;
+                    }
+                };
+
+                let mut reload_schedules = reload_app
+                    .world_mut()
+                    .get_resource_mut::<Schedules>()
+                    .unwrap();
+
+                if let Some(mut reload_update) = reload_schedules.remove(Update) {
+                    schedules.remove(HotPatchUpdate);
+                    let hot_reload_update = schedules.entry(HotPatchUpdate);
+                    *hot_reload_update.graph_mut() = std::mem::take(reload_update.graph_mut());
+                    commands.run_system_cached(|world: &mut World| {
+                        world.schedule_scope(HotPatchUpdate, |world, hot_reload_update| {
+                            let result = hot_reload_update.initialize(world);
+                            if let Err(e) = result {
+                                error!("Failed to initialize hotpatch update: {e}");
+                            }
+                        });
+                    });
+                };
+
+                if let Some(mut reload_post_update) = reload_schedules.remove(PostUpdate) {
+                    schedules.remove(HotPatchPostUpdate);
+                    let hot_reload_post_update = schedules.entry(HotPatchPostUpdate);
+                    *hot_reload_post_update.graph_mut() =
+                        std::mem::take(reload_post_update.graph_mut());
+                    commands.run_system_cached(|world: &mut World| {
+                        world.schedule_scope(
+                            HotPatchPostUpdate,
+                            |world, hot_reload_post_update| {
+                                let result = hot_reload_post_update.initialize(world);
+                                if let Err(e) = result {
+                                    error!("Failed to initialize hotpatch post-update: {e}");
+                                }
+                            },
+                        );
+                    });
+                };
+
+                if let Some(mut reload_pre_update) = reload_schedules.remove(PreUpdate) {
+                    schedules.remove(HotPatchPreUpdate);
+                    let hot_reload_pre_update = schedules.entry(HotPatchPreUpdate);
+                    *hot_reload_pre_update.graph_mut() =
+                        std::mem::take(reload_pre_update.graph_mut());
+                    commands.run_system_cached(|world: &mut World| {
+                        world.schedule_scope(HotPatchPreUpdate, |world, hot_reload_pre_update| {
+                            let result = hot_reload_pre_update.initialize(world);
+                            if let Err(e) = result {
+                                error!("Failed to initialize hotpatch pre-update: {e}");
+                            }
+                        });
+                    });
+                };
+
+                if let Some(mut auto_reload_startup) = reload_schedules.remove(StartupRerunHotPatch)
+                {
+                    schedules.remove(StartupRerunHotPatch);
+                    let schedule: &mut Schedule = schedules.entry(StartupRerunHotPatch);
+                    *schedule.graph_mut() = std::mem::take(auto_reload_startup.graph_mut());
+                    commands.run_system_cached(|world: &mut World| {
+                        world.schedule_scope(StartupRerunHotPatch, |world, auto_reload_startup| {
+                            let result = auto_reload_startup.initialize(world);
+                            if let Err(e) = result {
+                                error!("Failed to initialize hotpatch auto_reload_startup: {e}");
+                            }
+                        });
+                    });
+
+                    commands.run_system_cached(
+                        |mut commands: Commands,
+                         query: Query<Entity>,
+                         reload_positions: Res<ReloadPositions>,
+                         world: &World| {
+                            for e in query.iter() {
+                                let Some(location) = world
+                                    .entities()
+                                    .entity_get_spawned_or_despawned_by(e)
+                                    .into_option()
+                                else {
+                                    continue;
+                                };
+                                let Some(location) = location else { continue };
+                                for (file, line_start, line_end) in reload_positions.iter() {
+                                    if location.file() != *file {
+                                        continue;
+                                    }
+                                    if location.line() > *line_start && location.line() < *line_end
+                                    {
+                                        debug!("despawning an entity at: {location:?}");
+                                        commands.entity(e.entity()).despawn();
+                                    }
+                                }
+                            }
+                        },
+                    );
+                    commands.run_system_cached(|world: &mut World| {
+                        // we clear our reload positions every time so we can fill them up with new stuff.
+                        world.insert_resource(ReloadPositions::default());
+                        if let Err(e) = world.try_run_schedule(StartupRerunHotPatch) {
+                            error!("Failed to auto-reload startup: {e:?}");
+                        }
+                    })
+                }
+            },
+        );
+        self
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 994e57b..63d3bdb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,20 +5,22 @@
 #[cfg(all(not(target_family = "wasm"), debug_assertions))]
 use __macros_internal::__HotPatchedSystems as HotPatchedSystems;
 use bevy_app::{App, Plugin, PreUpdate};
+use bevy_ecs::prelude::*;
 #[cfg(all(not(target_family = "wasm"), debug_assertions))]
 use bevy_ecs::system::{Commands, Res};
-use bevy_ecs::{
-    event::{Event, EventWriter},
-    schedule::IntoScheduleConfigs,
-};
 pub use bevy_simple_subsecond_system_macros::*;
 pub use dioxus_devtools;
 #[cfg(all(not(target_family = "wasm"), debug_assertions))]
 use dioxus_devtools::{subsecond::apply_patch, *};
 
+pub mod hot_patched_app;
+
 /// Everything you need to use hotpatching
 pub mod prelude {
-    pub use super::{HotPatched, SimpleSubsecondPlugin};
+    pub use super::{
+        HotPatched, SimpleSubsecondPlugin,
+        hot_patched_app::{HotPatchedAppExt as _, StartupRerunHotPatch},
+    };
     pub use bevy_simple_subsecond_system_macros::*;
 }
 
@@ -94,13 +96,14 @@ fn update_system_ptr(hot_patched_systems: Res<HotPatchedSystems>, mut commands:
 }
 #[doc(hidden)]
 pub mod __macros_internal {
+    use bevy_derive::{Deref, DerefMut};
     pub use bevy_ecs::{
         system::{IntoSystem, SystemId, SystemState},
         world::World,
     };
     pub use bevy_ecs_macros::Resource;
     pub use bevy_log::debug;
-    use bevy_platform::collections::HashMap;
+    use bevy_platform::collections::{HashMap, HashSet};
     use std::any::TypeId;
 
     #[derive(Resource, Default)]
@@ -112,4 +115,8 @@ pub mod __macros_internal {
         pub current_ptr: u64,
         pub last_ptr: u64,
     }
+
+    #[doc(hidden)]
+    #[derive(Deref, DerefMut, Resource, Default, Debug)]
+    pub struct __ReloadPositions(pub HashSet<(&'static str, u32, u32)>);
 }


Reviews:
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:37:58Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:38:19Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:38:31Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:39:23Z
user: login: janhohenheim
body: Some questions:
- Does this work when the new system is created at runtime, i.e. was not written yet when the app started?
- Does the new system get hotpatched?
- Can the system call new functions that did not exist on app creation?
- Can *those* functions also be hotpatched?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:40:32Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:04:41Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:05:01Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:05:58Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:08:58Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:14:15Z
user: login: janhohenheim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T01:38:42Z

Review comments:
diff_hunk: @@ -1,14 +1,26 @@
 use bevy::prelude::*;
 use bevy_simple_subsecond_system::prelude::*;
+use bevy_simple_subsecond_system::{AppWrapper, ReloadableAppExt};
+
 fn main() {
     App::new()
         .add_plugins(DefaultPlugins)
         .add_plugins(SimpleSubsecondPlugin::default())
         .add_systems(Startup, setup)
         .add_systems(Update, configure_ui)
+        .reloadable(|mut app: AppWrapper| -> AppWrapper {
+            app.add_systems(Update, do_thing);
+            app
+        })
path: examples/patch_on_update.rs
user: login: janhohenheim
body: Could you move this into its own example? :)
Add a line like "try adding new functions into this closure at runtime!"
created_at: 2025-05-22T22:37:58Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
path: src/lib.rs
user: login: janhohenheim
body: Maybe `HotPatchedApp`?
created_at: 2025-05-22T22:38:19Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
+impl AppWrapper {
+    pub fn new() -> AppWrapper {
+        AppWrapper(send_wrapper::SendWrapper::new(bevy::app::App::default()))
+    }
+}
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct HotReloadUpdate;
path: src/lib.rs
user: login: janhohenheim
body: Replace `HotReload` with `HotPatch`
created_at: 2025-05-22T22:38:31Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
+impl AppWrapper {
+    pub fn new() -> AppWrapper {
+        AppWrapper(send_wrapper::SendWrapper::new(bevy::app::App::default()))
+    }
+}
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct HotReloadUpdate;
+
+pub trait ReloadableAppExt {
+    /// Call this with plugins and systems and it will auto-add and remove systems in the `Update` schedule to your running app
+    fn reloadable(
path: src/lib.rs
user: login: janhohenheim
body: I'd prefer `with_hot_patched` or something like that, similar to the old `with_children` API
created_at: 2025-05-22T22:39:23Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
+impl AppWrapper {
+    pub fn new() -> AppWrapper {
+        AppWrapper(send_wrapper::SendWrapper::new(bevy::app::App::default()))
+    }
+}
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct HotReloadUpdate;
+
+pub trait ReloadableAppExt {
+    /// Call this with plugins and systems and it will auto-add and remove systems in the `Update` schedule to your running app
+    fn reloadable(
+        &mut self,
+        func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App;
+}
+
+impl ReloadableAppExt for App {
+    fn reloadable(
+        &mut self,
+        mut func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App {
+        // we run this once during startup here so that way when we are actually restarting the app all the systems get added
+        let mut reload_app = func(AppWrapper::new());
+        if let Some(mut schedules) = reload_app.world_mut().get_resource_mut::<Schedules>() {
+            if let Some(mut update) = schedules.remove(Update) {
+                let mut hot_reload_update = schedules.entry(HotReloadUpdate);
+                *hot_reload_update.graph_mut() = std::mem::take(update.graph_mut());
+                hot_reload_update.initialize(self.world_mut()).unwrap();
+            }
+        }
+        self.add_systems(Update, |world: &mut World| {
+            let _ = world.try_run_schedule(HotReloadUpdate);
+        });
+        let mut reloadable_section =
+            std::sync::Mutex::new(dioxus_devtools::subsecond::HotFn::current(func));
+        self.add_systems(
+            PostUpdate,
+            move |_: Option<NonSend<NonSendMarker>>,
+                  mut schedules: ResMut<Schedules>,
+                  mut commands: Commands,
+                  hotreload_event: EventReader<HotPatched>| {
+                if hotreload_event.is_empty() {
+                    return;
+                }
+                let mut reload_app = reloadable_section
+                    .lock()
+                    .unwrap()
+                    .try_call((AppWrapper::new(),))
+                    .unwrap();
+                let Some(mut reload_schedules) = reload_app.world_mut().get_resource_mut::<Schedules>()
+                else {
+                    return;
+                };
+
+                let Some(mut reload_update) = reload_schedules.remove(Update) else {
+                    return;
+                };
+                schedules.remove(HotReloadUpdate);
+                let mut hot_reload_update = schedules.entry(HotReloadUpdate);
+                *hot_reload_update.graph_mut() = std::mem::take(reload_update.graph_mut());
+                commands.run_system_cached(|world: &mut World| {
+                    world.schedule_scope(HotReloadUpdate, |world, hot_reload_update| {
+                        hot_reload_update.initialize(world).unwrap();
path: src/lib.rs
user: login: janhohenheim
body: Does this `unwrap` need to be here? Or can we just log?
created_at: 2025-05-22T23:04:41Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
+impl AppWrapper {
+    pub fn new() -> AppWrapper {
+        AppWrapper(send_wrapper::SendWrapper::new(bevy::app::App::default()))
+    }
+}
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct HotReloadUpdate;
+
+pub trait ReloadableAppExt {
+    /// Call this with plugins and systems and it will auto-add and remove systems in the `Update` schedule to your running app
+    fn reloadable(
+        &mut self,
+        func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App;
+}
+
+impl ReloadableAppExt for App {
+    fn reloadable(
+        &mut self,
+        mut func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App {
+        // we run this once during startup here so that way when we are actually restarting the app all the systems get added
+        let mut reload_app = func(AppWrapper::new());
+        if let Some(mut schedules) = reload_app.world_mut().get_resource_mut::<Schedules>() {
+            if let Some(mut update) = schedules.remove(Update) {
+                let mut hot_reload_update = schedules.entry(HotReloadUpdate);
+                *hot_reload_update.graph_mut() = std::mem::take(update.graph_mut());
+                hot_reload_update.initialize(self.world_mut()).unwrap();
+            }
+        }
+        self.add_systems(Update, |world: &mut World| {
+            let _ = world.try_run_schedule(HotReloadUpdate);
+        });
+        let mut reloadable_section =
+            std::sync::Mutex::new(dioxus_devtools::subsecond::HotFn::current(func));
+        self.add_systems(
+            PostUpdate,
+            move |_: Option<NonSend<NonSendMarker>>,
+                  mut schedules: ResMut<Schedules>,
+                  mut commands: Commands,
+                  hotreload_event: EventReader<HotPatched>| {
+                if hotreload_event.is_empty() {
+                    return;
+                }
+                let mut reload_app = reloadable_section
+                    .lock()
+                    .unwrap()
+                    .try_call((AppWrapper::new(),))
+                    .unwrap();
path: src/lib.rs
user: login: janhohenheim
body: Can we log instead of unwrapping?

created_at: 2025-05-22T23:05:01Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
+impl AppWrapper {
+    pub fn new() -> AppWrapper {
+        AppWrapper(send_wrapper::SendWrapper::new(bevy::app::App::default()))
+    }
+}
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct HotReloadUpdate;
+
+pub trait ReloadableAppExt {
+    /// Call this with plugins and systems and it will auto-add and remove systems in the `Update` schedule to your running app
+    fn reloadable(
+        &mut self,
+        func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App;
+}
+
+impl ReloadableAppExt for App {
+    fn reloadable(
+        &mut self,
+        mut func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App {
+        // we run this once during startup here so that way when we are actually restarting the app all the systems get added
+        let mut reload_app = func(AppWrapper::new());
+        if let Some(mut schedules) = reload_app.world_mut().get_resource_mut::<Schedules>() {
+            if let Some(mut update) = schedules.remove(Update) {
+                let mut hot_reload_update = schedules.entry(HotReloadUpdate);
+                *hot_reload_update.graph_mut() = std::mem::take(update.graph_mut());
+                hot_reload_update.initialize(self.world_mut()).unwrap();
path: src/lib.rs
user: login: janhohenheim
body: Can we use `expect` instead of `unwrap` here?
created_at: 2025-05-22T23:05:58Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
+impl AppWrapper {
+    pub fn new() -> AppWrapper {
+        AppWrapper(send_wrapper::SendWrapper::new(bevy::app::App::default()))
+    }
+}
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct HotReloadUpdate;
+
+pub trait ReloadableAppExt {
+    /// Call this with plugins and systems and it will auto-add and remove systems in the `Update` schedule to your running app
+    fn reloadable(
+        &mut self,
+        func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App;
+}
+
+impl ReloadableAppExt for App {
+    fn reloadable(
+        &mut self,
+        mut func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
path: src/lib.rs
user: login: janhohenheim
body: If we pass `&mut AppWrapper` instead, the API would look a bit nicer, as nothing has to be returned :)
created_at: 2025-05-22T23:08:58Z
author_association: MEMBER
diff_hunk: @@ -1,14 +1,26 @@
 use bevy::prelude::*;
 use bevy_simple_subsecond_system::prelude::*;
+use bevy_simple_subsecond_system::{AppWrapper, ReloadableAppExt};
+
 fn main() {
     App::new()
         .add_plugins(DefaultPlugins)
         .add_plugins(SimpleSubsecondPlugin::default())
         .add_systems(Startup, setup)
         .add_systems(Update, configure_ui)
+        .reloadable(|mut app: AppWrapper| -> AppWrapper {
+            app.add_systems(Update, do_thing);
path: examples/patch_on_update.rs
user: login: janhohenheim
body: I wonder how this would behave if we add a fixed update system in here. Judging from the implementation, it should work, right?
created_at: 2025-05-22T23:14:15Z
author_association: MEMBER
diff_hunk: @@ -86,3 +87,76 @@ pub mod __macros_internal {
         pub last_ptr: u64,
     }
 }
+
+#[derive(Deref, DerefMut)]
+pub struct AppWrapper(send_wrapper::SendWrapper<bevy::app::App>);
+impl AppWrapper {
+    pub fn new() -> AppWrapper {
+        AppWrapper(send_wrapper::SendWrapper::new(bevy::app::App::default()))
+    }
+}
+#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct HotReloadUpdate;
+
+pub trait ReloadableAppExt {
+    /// Call this with plugins and systems and it will auto-add and remove systems in the `Update` schedule to your running app
+    fn reloadable(
+        &mut self,
+        func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
+    ) -> &mut App;
+}
+
+impl ReloadableAppExt for App {
+    fn reloadable(
+        &mut self,
+        mut func: impl FnMut(AppWrapper) -> AppWrapper + Send + Sync + 'static,
path: src/lib.rs
user: login: janhohenheim
body: Edit: didn't manage to do this on my own because of lifetimes, grrr
created_at: 2025-05-23T01:38:42Z
author_association: MEMBER

PR comments:
user: login: janhohenheim
created_at: 2025-05-23T01:11:45Z
body: > Some questions:
> 
>     * Does this work when the new system is created at runtime, i.e. was not written yet when the app started?
> 
>     * Does the new system get hotpatched?
> 
>     * Can the system call new functions that did not exist on app creation?
> 
>     * Can _those_ functions also be hotpatched?

- yes
- yes
- yes
- yes 
:D
author_association: MEMBER
user: login: janhohenheim
created_at: 2025-05-23T01:56:59Z
body: Open issues:
- The comments I didn't resolve above
- moving configure_ui into the closure breaks the UI
- Implement this for `Startup` systems too or people will be confused.
  - Ensure reruns still work for that case
- Make sure that this desugars to regular registrations for Wasm and release builds
author_association: MEMBER