bevy/crates/bevy_asset/src/debug_asset_server.rs
Carter Anderson 1bb751cb8d Plugins own their settings. Rework PluginGroup trait. (#6336)
# Objective

Fixes #5884 #2879
Alternative to #2988 #5885 #2886

"Immutable" Plugin settings are currently represented as normal ECS resources, which are read as part of plugin init. This presents a number of problems:

1. If a user inserts the plugin settings resource after the plugin is initialized, it will be silently ignored (and use the defaults instead)
2. Users can modify the plugin settings resource after the plugin has been initialized. This creates a false sense of control over settings that can no longer be changed.

(1) and (2) are especially problematic and confusing for the `WindowDescriptor` resource, but this is a general problem.

## Solution

Immutable Plugin settings now live on each Plugin struct (ex: `WindowPlugin`). PluginGroups have been reworked to support overriding plugin values. This also removes the need for the `add_plugins_with` api, as the `add_plugins` api can use the builder pattern directly. Settings that can be used at runtime continue to be represented as ECS resources.

Plugins are now configured like this:

```rust
app.add_plugin(AssetPlugin {
  watch_for_changes: true,
  ..default()
})
```

PluginGroups are now configured like this:

```rust
app.add_plugins(DefaultPlugins
  .set(AssetPlugin {
    watch_for_changes: true,
    ..default()
  })
)
```

This is an alternative to #2988, which is similar. But I personally prefer this solution for a couple of reasons:
* ~~#2988 doesn't solve (1)~~ #2988 does solve (1) and will panic in that case. I was wrong!
* This PR directly ties plugin settings to Plugin types in a 1:1 relationship, rather than a loose "setup resource" <-> plugin coupling (where the setup resource is consumed by the first plugin that uses it).
* I'm not a huge fan of overloading the ECS resource concept and implementation for something that has very different use cases and constraints.

## Changelog

- PluginGroups can now be configured directly using the builder pattern. Individual plugin values can be overridden by using `plugin_group.set(SomePlugin {})`, which enables overriding default plugin values.  
- `WindowDescriptor` plugin settings have been moved to `WindowPlugin` and `AssetServerSettings` have been moved to `AssetPlugin`
- `app.add_plugins_with` has been replaced by using `add_plugins` with the builder pattern.

## Migration Guide

The `WindowDescriptor` settings have been moved from a resource to `WindowPlugin::window`:

```rust
// Old (Bevy 0.8)
app
  .insert_resource(WindowDescriptor {
    width: 400.0,
    ..default()
  })
  .add_plugins(DefaultPlugins)

// New (Bevy 0.9)
app.add_plugins(DefaultPlugins.set(WindowPlugin {
  window: WindowDescriptor {
    width: 400.0,
    ..default()
  },
  ..default()
}))
```


The `AssetServerSettings` resource has been removed in favor of direct `AssetPlugin` configuration:

```rust
// Old (Bevy 0.8)
app
  .insert_resource(AssetServerSettings {
    watch_for_changes: true,
    ..default()
  })
  .add_plugins(DefaultPlugins)

// New (Bevy 0.9)
app.add_plugins(DefaultPlugins.set(AssetPlugin {
  watch_for_changes: true,
  ..default()
}))
```

`add_plugins_with` has been replaced by `add_plugins` in combination with the builder pattern:

```rust
// Old (Bevy 0.8)
app.add_plugins_with(DefaultPlugins, |group| group.disable::<AssetPlugin>());

// New (Bevy 0.9)
app.add_plugins(DefaultPlugins.build().disable::<AssetPlugin>());
```
2022-10-24 21:20:33 +00:00

147 lines
5.0 KiB
Rust

//! Support for hot reloading internal assets.
//!
//! Internal assets (e.g. shaders) are bundled directly into an application and can't be hot
//! reloaded using the conventional API.
use bevy_app::{App, Plugin};
use bevy_ecs::{
event::Events,
schedule::SystemLabel,
system::{NonSendMut, Res, ResMut, Resource, SystemState},
};
use bevy_tasks::{IoTaskPool, TaskPoolBuilder};
use bevy_utils::HashMap;
use std::{
ops::{Deref, DerefMut},
path::Path,
};
use crate::{
Asset, AssetEvent, AssetPlugin, AssetServer, Assets, FileAssetIo, Handle, HandleUntyped,
};
/// A helper [`App`] used for hot reloading internal assets, which are compiled-in to Bevy plugins.
pub struct DebugAssetApp(App);
impl Deref for DebugAssetApp {
type Target = App;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DebugAssetApp {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// A label describing the system that runs [`DebugAssetApp`].
#[derive(SystemLabel, Debug, Clone, PartialEq, Eq, Hash)]
pub struct DebugAssetAppRun;
/// Facilitates the creation of a "debug asset app", whose sole responsibility is hot reloading
/// assets that are "internal" / compiled-in to Bevy Plugins.
///
/// Pair with the [`load_internal_asset`](crate::load_internal_asset) macro to load hot-reloadable
/// assets. The `debug_asset_server` feature flag must also be enabled for hot reloading to work.
/// Currently only hot reloads assets stored in the `crates` folder.
#[derive(Default)]
pub struct DebugAssetServerPlugin;
/// A collection that maps internal assets in a [`DebugAssetApp`]'s asset server to their mirrors in
/// the main [`App`].
#[derive(Resource)]
pub struct HandleMap<T: Asset> {
/// The collection of asset handles.
pub handles: HashMap<Handle<T>, Handle<T>>,
}
impl<T: Asset> Default for HandleMap<T> {
fn default() -> Self {
Self {
handles: Default::default(),
}
}
}
impl Plugin for DebugAssetServerPlugin {
fn build(&self, app: &mut bevy_app::App) {
IoTaskPool::init(|| {
TaskPoolBuilder::default()
.num_threads(2)
.thread_name("Debug Asset Server IO Task Pool".to_string())
.build()
});
let mut debug_asset_app = App::new();
debug_asset_app.add_plugin(AssetPlugin {
asset_folder: "crates".to_string(),
watch_for_changes: true,
});
app.insert_non_send_resource(DebugAssetApp(debug_asset_app));
app.add_system(run_debug_asset_app);
}
}
fn run_debug_asset_app(mut debug_asset_app: NonSendMut<DebugAssetApp>) {
debug_asset_app.0.update();
}
pub(crate) fn sync_debug_assets<T: Asset + Clone>(
mut debug_asset_app: NonSendMut<DebugAssetApp>,
mut assets: ResMut<Assets<T>>,
) {
let world = &mut debug_asset_app.0.world;
let mut state = SystemState::<(
Res<Events<AssetEvent<T>>>,
Res<HandleMap<T>>,
Res<Assets<T>>,
)>::new(world);
let (changed_shaders, handle_map, debug_assets) = state.get_mut(world);
for changed in changed_shaders.iter_current_update_events() {
let debug_handle = match changed {
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => handle,
AssetEvent::Removed { .. } => continue,
};
if let Some(handle) = handle_map.handles.get(debug_handle) {
if let Some(debug_asset) = debug_assets.get(debug_handle) {
assets.set_untracked(handle, debug_asset.clone());
}
}
}
}
/// Uses the return type of the given loader to register the given handle with the appropriate type
/// and load the asset with the given `path` and parent `file_path`.
///
/// If this feels a bit odd ... that's because it is. This was built to improve the UX of the
/// `load_internal_asset` macro.
pub fn register_handle_with_loader<A: Asset>(
_loader: fn(&'static str) -> A,
app: &mut DebugAssetApp,
handle: HandleUntyped,
file_path: &str,
path: &'static str,
) {
let mut state = SystemState::<(ResMut<HandleMap<A>>, Res<AssetServer>)>::new(&mut app.world);
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_dir_path = Path::new(&manifest_dir);
let (mut handle_map, asset_server) = state.get_mut(&mut app.world);
let asset_io = asset_server
.asset_io()
.downcast_ref::<FileAssetIo>()
.expect("The debug AssetServer only works with FileAssetIo-backed AssetServers");
let absolute_file_path = manifest_dir_path.join(
Path::new(file_path)
.parent()
.expect("file path must have a parent"),
);
let asset_folder_relative_path = absolute_file_path
.strip_prefix(asset_io.root_path())
.expect("The AssetIo root path should be a prefix of the absolute file path");
handle_map.handles.insert(
asset_server.load(asset_folder_relative_path.join(path)),
handle.clone_weak().typed::<A>(),
);
}