use bevy::ecs::system::{Command, EntityCommands};
use bevy::prelude::{
AssetServer, Bundle, Commands, Component, DespawnRecursiveExt, Entity, EntityRef, Handle,
IntoSystemConfigs, Plugin as BevyPlugin, Query, Reflect, Res, Scene,
SceneBundle as BevySceneBundle, SceneSpawner, World,
};
use bevy::scene::SceneInstance;
#[derive(Bundle)]
#[allow(missing_docs /* field description is trivial */)]
pub struct SceneBundle {
pub reload: Hook,
pub scene: BevySceneBundle,
}
pub struct HookFn(
pub Box<dyn Fn(&EntityRef, &mut EntityCommands, &World, Entity) + Send + Sync + 'static>,
);
impl Default for HookFn {
fn default() -> Self {
Self(Box::new(|_, _, _, _| {}))
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Reflect)]
pub enum State {
Loading,
Hooked,
MustReload,
MustDelete,
}
#[derive(Component, Reflect)]
pub struct Hook {
pub state: State,
#[reflect(ignore)]
pub hook: HookFn,
}
impl Hook {
pub fn new<F>(hook: F) -> Self
where
F: Fn(&EntityRef, &mut EntityCommands, &World, Entity) + Send + Sync + 'static,
{
Self {
state: State::Loading,
hook: HookFn(Box::new(hook)),
}
}
}
struct UpdateHook {
entity: Entity,
new_state: State,
}
impl Command for UpdateHook {
fn apply(self, world: &mut World) {
if let Some(mut hook) = world.get_mut::<Hook>(self.entity) {
hook.state = self.new_state;
}
}
}
pub fn run_reloadable_hooks(
instances: Query<(Entity, &Handle<Scene>, &SceneInstance, &Hook)>,
scene_manager: Res<SceneSpawner>,
assets: Res<AssetServer>,
world: &World,
mut cmds: Commands,
) {
for (entity, handle, instance, reload) in instances.iter() {
let instance_ready = scene_manager.instance_is_ready(**instance);
match reload.state {
State::Loading if instance_ready => {
cmds.add(UpdateHook { entity, new_state: State::Hooked });
let entities = scene_manager.iter_instance_entities(**instance);
for entity_ref in entities.filter_map(|e| world.get_entity(e)) {
let mut cmd = cmds.entity(entity_ref.id());
(reload.hook.0)(&entity_ref, &mut cmd, world, entity);
}
}
State::Hooked | State::Loading => continue,
State::MustReload => {
let Some(file_path) = assets.get_path(handle) else {
bevy::log::warn!("Tried to reload a scene without a registered path");
continue;
};
let entities = scene_manager.iter_instance_entities(**instance);
for entity in entities.filter(|e| world.get_entity(*e).is_some()) {
cmds.entity(entity).despawn_recursive();
}
cmds.add(UpdateHook { entity, new_state: State::Loading });
cmds.entity(entity)
.insert(assets.load::<Scene>(file_path))
.remove::<SceneInstance>();
}
State::MustDelete => {
let entities = scene_manager.iter_instance_entities(**instance);
for entity in entities.filter(|e| world.get_entity(*e).is_some()) {
cmds.entity(entity).despawn_recursive();
}
cmds.entity(entity).despawn_recursive();
}
}
}
}
pub struct Plugin;
impl BevyPlugin for Plugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.register_type::<Hook>()
.register_type::<State>()
.add_systems(
bevy::prelude::SpawnScene,
run_reloadable_hooks.after(bevy::scene::scene_spawner_system),
);
}
}