Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,811 changes: 2,045 additions & 766 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion adapters/bevy_dexterous_developer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ bevy_dexterous_developer_library = { version = "0.4.0-alpha.3", path = "crates/b
bevy_dexterous_developer_dynamic = { version = "0.4.0-alpha.3", path = "crates/bevy_dexterous_developer_dynamic", optional = true }

[dev-dependencies]
bevy = { version = "0.14", default-features = true, features=["dynamic_linking"] }
bevy = { version = "0.17", default-features = true, features=["dynamic_linking"] }
dexterous_developer_test_utils = { version = "0.4.0-alpha.3", path = "../../dexterous_developer_test_utils"}
tokio = { version = "1", features = ["full"]}
test-temp-dir = { version = "0.2"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ documentation.workspace = true
crate-type = ["dylib"]

[dependencies]
bevy = { version = "0.14", default-features = false}
bevy = { version = "0.17", default-features = false}
bevy_dexterous_developer_library = { version = "0.4.0-alpha.3", path = "../bevy_dexterous_developer_library" }
dexterous_developer_types = { version = "0.4.0-alpha.3", path = "../../../../dexterous_developer_types" }
dexterous_developer_instance = { version = "0.4.0-alpha.3", path = "../../../../dexterous_developer_instance" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ hot = [
anyhow = { version = "1" }
dexterous_developer_types = { version = "0.4.0-alpha.3", path = "../../../../dexterous_developer_types" }
dexterous_developer_instance = { version = "0.4.0-alpha.3", path = "../../../../dexterous_developer_instance" }
bevy = { version = "0.14", default-features = false, features = [
bevy = { version = "0.17", default-features = false, features = [
"serialize",
"bevy_state"
"bevy_state",
"bevy_log"
]}
serde = { version = "1", features = ["derive"] }
rmp-serde = { version = "1" }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use super::types::*;
use bevy::{
ecs::{
schedule::IntoScheduleConfigs,
system::ScheduleSystem,
},
prelude::{App, OnEnter, OnExit, Startup},
state::app::AppExtStates,
};
Expand All @@ -22,7 +26,7 @@ impl ReloadableApp for ReloadableAppContents<'_> {
fn add_systems<M, L: bevy::ecs::schedule::ScheduleLabel + Eq + std::hash::Hash + Clone>(
&mut self,
schedule: L,
systems: impl bevy::prelude::IntoSystemConfigs<M>,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.0.add_systems(schedule, systems);
self
Expand Down Expand Up @@ -71,7 +75,7 @@ impl ReloadableApp for ReloadableAppContents<'_> {

fn reset_setup<C: bevy::prelude::Component, M>(
&mut self,
systems: impl bevy::prelude::IntoSystemConfigs<M>,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.0.add_systems(Startup, systems);
self
Expand All @@ -80,15 +84,16 @@ impl ReloadableApp for ReloadableAppContents<'_> {
fn reset_setup_in_state<C: bevy::prelude::Component, S: bevy::prelude::States, M>(
&mut self,
state: S,
systems: impl bevy::prelude::IntoSystemConfigs<M>,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
self.0
.add_systems(OnEnter(state.clone()), systems)
.add_systems(OnExit(state), clear_marked_system::<C>);
self
}

fn add_event<T: bevy::prelude::Event>(&mut self) -> &mut Self {
#[allow(deprecated)]
fn add_event<T: bevy::prelude::Event + bevy::ecs::message::Message>(&mut self) -> &mut Self {
self.0.add_event::<T>();
self
}
Expand All @@ -113,6 +118,7 @@ impl ReloadableApp for ReloadableAppContents<'_> {
self
}

#[allow(deprecated)]
fn enable_state_scoped_entities<S: bevy::state::state::States + ReplacableType>(
&mut self,
) -> &mut Self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub use crate::types::*;
pub use reloadable_app_setup::*;

use reload_systems::{cleanup_schedules, reload};
pub use reloadable_app::{ReloadableAppCleanupData, ReloadableAppContents, ReloadableAppElements};
pub use reloadable_app::{ActiveReloadableSchedules, ReloadableAppCleanupData, ReloadableAppContents, ReloadableAppElements, RegisteredScheduleRunners};
use replacable_types::{ReplacableComponentStore, ReplacableResourceStore};
use schedules::*;

Expand Down Expand Up @@ -50,6 +50,8 @@ impl Plugin for HotReloadPlugin {

app.init_resource::<ReloadableAppElements>()
.init_resource::<ReloadableAppCleanupData>()
.init_resource::<RegisteredScheduleRunners>()
.init_resource::<ActiveReloadableSchedules>()
.init_resource::<ReplacableResourceStore>()
.init_resource::<ReplacableComponentStore>()
.insert_resource(InternalHotReload(chrono::Local::now(), false));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use dexterous_developer_instance::internal::HOT_RELOAD_INFO;

use crate::{
hot::{
CleanupReloaded, CleanupSchedules, DeserializeReloadables, OnReloadComplete,
ReloadableAppCleanupData, ReloadableAppElements, SerializeReloadables, SetupReload,
ActiveReloadableSchedules, CleanupReloaded, CleanupSchedules, DeserializeReloadables,
OnReloadComplete, ReloadableAppCleanupData, ReloadableAppElements,
RegisteredScheduleRunners, SerializeReloadables, SetupReload,
},
ReloadSettings, ReloadableAppContents,
};
Expand Down Expand Up @@ -148,34 +149,78 @@ pub fn register_schedules(world: &mut World) {
};
debug!("Has reloadable app");

let Some(mut schedules) = world.get_resource_mut::<Schedules>() else {
return;
};
// Get the registered runners tracker - uses string keys to persist across library swaps
let mut registered_runners = world
.remove_resource::<RegisteredScheduleRunners>()
.unwrap_or_default();

debug!("Has schedules resource");
// Get the active schedules resource to store current labels
let mut active_schedules = world
.remove_resource::<ActiveReloadableSchedules>()
.unwrap_or_default();

let mut inner = ReloadableAppCleanupData::default();

for (original, schedule, reloadable_schedule_label) in reloadable.schedule_iter() {
// Collect schedule data first to avoid borrow issues
let schedule_data: Vec<_> = reloadable.schedule_iter().collect();

for (original, schedule, reloadable_schedule_label) in schedule_data {
debug!("Adding {original:?} to schedule");
inner.labels.insert(reloadable_schedule_label.clone());
let exists = schedules.insert(schedule);
if exists.is_none() {
if let Some(root) = schedules.get_mut(original.clone()) {
let label = reloadable_schedule_label.clone();

// Use a string key to track the root schedule
let root_schedule_key = format!("{:?}", original.inner());

// Get the schedule's actual interned label (this is what Bevy will use for lookup)
let schedule_label = schedule.label();

// Store the current active label for this root schedule
// The runner will look this up at runtime
active_schedules.schedules.insert(root_schedule_key.clone(), schedule_label);

let Some(mut schedules) = world.get_resource_mut::<Schedules>() else {
continue;
};

// Insert the new schedule (this replaces any existing schedule with the same label)
schedules.insert(schedule);

if !registered_runners.runners.contains(&root_schedule_key) {
// First time seeing this schedule - add a dynamic runner to the root
let inner_label = original.inner();
let key_for_runner = root_schedule_key.clone();

if let Some(root) = schedules.get_mut(inner_label) {
println!("Adding reloadable schedule runner to root schedule: {}", root_schedule_key);
// The runner looks up the current active label from the resource at runtime
root.add_systems(move |w: &mut World| {
let _ = w.try_run_schedule(label.clone());
if let Some(active) = w.get_resource::<ActiveReloadableSchedules>() {
if let Some(label) = active.schedules.get(&key_for_runner) {
let _ = w.try_run_schedule(*label);
}
}
});
registered_runners.runners.insert(root_schedule_key);
} else {
let mut root = Schedule::new(original);
println!("Root schedule not found, creating new one: {}", root_schedule_key);
let mut root = Schedule::new(inner_label);
root.add_systems(move |w: &mut World| {
let _ = w.try_run_schedule(reloadable_schedule_label.clone());
if let Some(active) = w.get_resource::<ActiveReloadableSchedules>() {
if let Some(label) = active.schedules.get(&key_for_runner) {
let _ = w.try_run_schedule(*label);
}
}
});
schedules.insert(root);
registered_runners.runners.insert(root_schedule_key);
}
} else {
println!("Runner already registered for: {}, updating active label", root_schedule_key);
}
}

world.insert_resource(active_schedules);
world.insert_resource(registered_runners);
world.insert_resource(inner);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use bevy::{
ecs::{event::EventRegistry, schedule::ScheduleLabel},
ecs::{
message::Messages,
schedule::{IntoScheduleConfigs, ScheduleLabel},
system::ScheduleSystem,
},
prelude::*,
state::{state::FreelyMutableState, state_scoped::clear_state_scoped_entities},
utils::{HashMap, HashSet},
state::state::FreelyMutableState,
};
use std::collections::{HashMap, HashSet};

use super::{super::types::*, reload_systems::dexterous_developer_occured};

Expand All @@ -14,6 +18,23 @@ pub struct ReloadableAppCleanupData {
pub labels: HashSet<ReloadableSchedule<WrappedSchedule>>,
}

/// Tracks which root schedules have had runners added.
/// Uses string-based keys to persist across library swaps (since InternedScheduleLabel
/// values are interned per-library and don't match after a hot reload).
#[derive(Default, Resource, Clone, Debug)]
pub struct RegisteredScheduleRunners {
pub runners: HashSet<String>,
}

/// Stores the currently active reloadable schedule labels.
/// This allows runners to look up the current label at runtime rather than
/// capturing a specific label that becomes stale after library swaps.
#[derive(Default, Resource)]
pub struct ActiveReloadableSchedules {
/// Maps root schedule name (e.g., "Update") to the current active reloadable schedule label
pub schedules: HashMap<String, bevy::ecs::schedule::InternedScheduleLabel>,
}

#[derive(Default, Resource)]
pub struct ReloadableAppElements {
schedules: HashMap<WrappedSchedule, (Schedule, ReloadableSchedule<WrappedSchedule>)>,
Expand Down Expand Up @@ -58,7 +79,7 @@ impl<'a> ReloadableAppContents<'a> {
fn run_only_on_first_load<M>(
&mut self,
name: &'static str,
systems: impl IntoSystemConfigs<M>,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
if self.run_once.insert(name.to_string()) {
self.add_systems(OnReloadComplete, systems);
Expand All @@ -73,7 +94,7 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
fn add_systems<M, L: ScheduleLabel + Eq + ::std::hash::Hash + Clone>(
&mut self,
schedule: L,
systems: impl IntoSystemConfigs<M>,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
trace!("Adding To Schedule {schedule:?}");
let schedules = &mut self.schedules;
Expand Down Expand Up @@ -214,7 +235,7 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
self
}

fn reset_setup<C: Component, M>(&mut self, systems: impl IntoSystemConfigs<M>) -> &mut Self {
fn reset_setup<C: Component, M>(&mut self, systems: impl IntoScheduleConfigs<ScheduleSystem, M>) -> &mut Self {
let name = self.name;
self.add_systems(
CleanupReloaded,
Expand All @@ -229,7 +250,7 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
fn reset_setup_in_state<C: Component, S: States, M>(
&mut self,
state: S,
systems: impl IntoSystemConfigs<M>,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) -> &mut Self {
let name = self.name;
self.add_systems(
Expand All @@ -240,28 +261,33 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
.add_systems(
PreUpdate,
systems.run_if(
in_state(state).and_then(
in_state(state).and(
dexterous_developer_occured
.and_then(element_selection_condition(name))
.or_else(|res: Res<State<S>>| resource_changed::<State<S>>(res)),
.and(element_selection_condition(name))
.or(resource_changed::<State<S>>),
),
),
)
}

fn add_event<T: Event>(&mut self) -> &mut Self {
#[allow(deprecated)]
fn add_event<T: Event + bevy::ecs::message::Message>(&mut self) -> &mut Self {
let name = self.name;
self.add_systems(
OnReloadComplete,
(move |world: &mut World| {
EventRegistry::register_event::<T>(world);
// Initialize the Messages resource if not present
world.init_resource::<Messages<T>>();
})
.run_if(element_selection_condition(name)),
);
self
}

fn insert_state<S: FreelyMutableState + ReplacableType>(&mut self, state: S) -> &mut Self {
fn insert_state<S: FreelyMutableState + ReplacableType>(&mut self, state: S) -> &mut Self
where
StateTransitionEvent<S>: Event + bevy::ecs::message::Message,
{
let name = S::get_type_name();
if !self.resources.contains(name) {
{
Expand All @@ -270,7 +296,7 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
.reset_resource::<NextState<S>>()
.add_event::<StateTransitionEvent<S>>()
.run_only_on_first_load(name, move |world: &mut World| {
world.send_event(StateTransitionEvent {
world.write_message(StateTransitionEvent {
exited: None,
entered: Some(state.clone()),
});
Expand Down Expand Up @@ -298,7 +324,10 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
self
}

fn add_sub_state<S: SubStates + ReplacableType>(&mut self) -> &mut Self {
fn add_sub_state<S: SubStates + ReplacableType>(&mut self) -> &mut Self
where
StateTransitionEvent<S>: Event + bevy::ecs::message::Message,
{
let name = S::get_type_name();
if !self.resources.contains(name) {
self.register_serializable_resource::<State<S>>()
Expand Down Expand Up @@ -326,7 +355,10 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
self
}

fn add_computed_state<S: ComputedStates + ReplacableType>(&mut self) -> &mut Self {
fn add_computed_state<S: ComputedStates + ReplacableType>(&mut self) -> &mut Self
where
StateTransitionEvent<S>: Event + bevy::ecs::message::Message,
{
let name = S::get_type_name();
if !self.resources.contains(name) {
self.register_serializable_resource::<State<S>>()
Expand All @@ -353,14 +385,11 @@ impl<'a> crate::ReloadableApp for ReloadableAppContents<'a> {
self
}

#[allow(deprecated)]
fn enable_state_scoped_entities<S: States + ReplacableType>(&mut self) -> &mut Self {
self.register_serializable_component::<StateScoped<S>>()
.add_systems(
StateTransition,
clear_state_scoped_entities::<S>
.after(ExitSchedules::<S>::default())
.before(TransitionSchedules::<S>::default()),
)
// In Bevy 0.17+, state scoped entities are enabled by default
// This function is kept for API compatibility but does nothing
self
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use bevy::{
ecs::schedule::IntoSystemConfigs,
ecs::{
schedule::IntoScheduleConfigs,
system::ScheduleSystem,
},
prelude::{debug, error, Commands, Component, Entity, Query, Res, ResMut, Resource},
utils::HashMap,
};
use std::collections::HashMap;

use crate::ReplacableType;

Expand Down Expand Up @@ -43,7 +46,7 @@ pub fn deserialize_replacable_resource_with_default<R: ReplacableType + Default

pub fn deserialize_replacable_resource_with_value<R: ReplacableType + Resource>(
initializer: R,
) -> impl IntoSystemConfigs<()> {
) -> impl IntoScheduleConfigs<ScheduleSystem, ()> {
let mut container = Some(initializer);
(move |store: Res<ReplacableResourceStore>, mut commands: Commands| {
let name = R::get_type_name();
Expand Down
Loading