diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 2a3f604e7b05f..9ca679a8498af 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -19,6 +19,7 @@ bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] bevy_derive = { path = "../bevy_derive", version = "0.11.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev", default-features = false } bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev", optional = true } +bevy_tasks = { path = "../bevy_tasks", version = "0.11.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } # other @@ -26,8 +27,8 @@ serde = { version = "1.0", features = ["derive"], optional = true } ron = { version = "0.8.0", optional = true } downcast-rs = "1.2.0" - [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = [ "Window" ] } +gloo-timers = "0.2" diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 613c5063df803..d1d0a45ce70e9 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -78,12 +78,17 @@ pub struct App { /// This is initially set to [`Main`]. pub main_schedule_label: BoxedScheduleLabel, sub_apps: HashMap, - plugin_registry: Vec>, + plugin_registry: Vec<(Box, PluginState)>, plugin_name_added: HashSet, /// A private counter to prevent incorrect calls to `App::run()` from `Plugin::build()` building_plugin_depth: usize, } +enum PluginState { + Built, + Finished, +} + impl Debug for App { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "App {{ sub_apps: ")?; @@ -302,7 +307,7 @@ impl App { /// event loop, but can be useful for situations where you want to use [`App::update`] pub fn ready(&self) -> bool { for plugin in &self.plugin_registry { - if !plugin.ready(self) { + if matches!(plugin.1, PluginState::Built) && !plugin.0.ready(self) { return false; } } @@ -314,9 +319,12 @@ impl App { /// [`App::update`]. pub fn finish(&mut self) { // temporarily remove the plugin registry to run each plugin's setup function on app. - let plugin_registry = std::mem::take(&mut self.plugin_registry); - for plugin in &plugin_registry { - plugin.finish(self); + let mut plugin_registry = std::mem::take(&mut self.plugin_registry); + for plugin in &mut plugin_registry { + if matches!(plugin.1, PluginState::Built) { + plugin.0.finish(self); + plugin.1 = PluginState::Finished; + } } self.plugin_registry = plugin_registry; } @@ -327,7 +335,9 @@ impl App { // temporarily remove the plugin registry to run each plugin's setup function on app. let plugin_registry = std::mem::take(&mut self.plugin_registry); for plugin in &plugin_registry { - plugin.cleanup(self); + if matches!(plugin.1, PluginState::Finished) { + plugin.0.cleanup(self); + } } self.plugin_registry = plugin_registry; } @@ -695,12 +705,91 @@ impl App { { match self.add_boxed_plugin(Box::new(plugin)) { Ok(app) => app, - Err(AppError::DuplicatePlugin { plugin_name }) => panic!( - "Error adding plugin {plugin_name}: : plugin was already added in application" - ), + Err(AppError::DuplicatePlugin { plugin_name }) => { + panic!("Error adding plugin {plugin_name}: plugin was already added in application") + } + } + } + + /// Adds a single [`Plugin`]. + /// + /// Unlike the [`add_plugin`](Self::add_plugin) method, this method will return a future that + /// will be complete once the [lifecycle of the plugin](Plugin) is finished, leaving only the + /// `cleanup` stage to do. + /// + /// # Panics + /// + /// Panics if the plugin was already added to the application. + pub async fn add_plugin_async(&mut self, plugin: T) -> &mut Self { + match self.add_boxed_plugin_async(Box::new(plugin)).await { + Ok(app) => app, + Err(AppError::DuplicatePlugin { plugin_name }) => { + panic!("Error adding plugin {plugin_name}: plugin was already added in application") + } } } + pub(crate) async fn add_boxed_plugin_async( + &mut self, + plugin: Box, + ) -> Result<&mut Self, AppError> { + struct FuturePlugin<'a>(&'a mut App, usize); + impl<'a> std::future::Future for FuturePlugin<'a> { + type Output = (); + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + // wait for the given plugin and all subplugins to be initialized completely + for plugin in self.0.plugin_registry.iter().skip(self.1) { + if !plugin.0.ready(self.0) { + #[cfg(not(target_arch = "wasm32"))] + { + // tick task pools, then wake this future + bevy_tasks::tick_global_task_pools_on_main_thread(); + cx.waker().wake_by_ref(); + } + #[cfg(target_arch = "wasm32")] + { + // wake this future in 10ms, to let other futures a chance to run + let waker = cx.waker().clone(); + use gloo_timers::callback::Timeout; + let timeout = Timeout::new(10, move || { + waker.wake(); + }); + timeout.forget(); + } + return std::task::Poll::Pending; + } + } + // temporarily remove the plugin registry to run each plugin's setup function on app. + let mut plugin_registry = std::mem::take(&mut self.0.plugin_registry); + for plugin in plugin_registry.iter_mut().skip(self.1) { + if matches!(plugin.1, PluginState::Built) { + plugin.0.finish(self.0); + plugin.1 = PluginState::Finished; + } + } + self.0.plugin_registry = plugin_registry; + + std::task::Poll::Ready(()) + } + } + impl<'b> FuturePlugin<'b> { + fn build(app: &'b mut App, plugin: Box) -> Result { + let current_registry_len = app.plugin_registry.len(); + app.add_boxed_plugin(plugin)?; + Ok(Self(app, current_registry_len)) + } + } + + let future = FuturePlugin::build(self, plugin)?; + + future.await; + Ok(self) + } + /// Boxed variant of [`add_plugin`](App::add_plugin) that can be used from a [`PluginGroup`] pub(crate) fn add_boxed_plugin( &mut self, @@ -715,7 +804,8 @@ impl App { // Reserve that position in the plugin registry. if a plugin adds plugins, they will be correctly ordered let plugin_position_in_registry = self.plugin_registry.len(); - self.plugin_registry.push(Box::new(PlaceholderPlugin)); + self.plugin_registry + .push((Box::new(PlaceholderPlugin), PluginState::Built)); self.building_plugin_depth += 1; let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self))); @@ -723,7 +813,7 @@ impl App { if let Err(payload) = result { resume_unwind(payload); } - self.plugin_registry[plugin_position_in_registry] = plugin; + self.plugin_registry[plugin_position_in_registry] = (plugin, PluginState::Built); Ok(self) } @@ -737,7 +827,7 @@ impl App { { self.plugin_registry .iter() - .any(|p| p.downcast_ref::().is_some()) + .any(|p| p.0.downcast_ref::().is_some()) } /// Returns a vector of references to any plugins of type `T` that have been added. @@ -765,7 +855,7 @@ impl App { { self.plugin_registry .iter() - .filter_map(|p| p.downcast_ref()) + .filter_map(|p| p.0.downcast_ref()) .collect() } @@ -797,6 +887,23 @@ impl App { self } + /// Adds a group of [`Plugin`]s. + /// + /// [`Plugin`]s can be grouped into a set by using a [`PluginGroup`]. + /// + /// Unlike the [`add_plugins`](Self::add_plugins) method, this method will return a future that + /// will be complete once the [lifecycle of the plugins](Plugin) is finished, leaving only the + /// `cleanup` stage to do. + /// + /// # Panics + /// + /// Panics if one of the plugin in the group was already added to the application. + pub async fn add_plugins_async(&mut self, group: T) -> &mut Self { + let builder = group.build(); + builder.finish_async(self).await; + self + } + /// Registers the type `T` in the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource, /// adding reflect data as specified in the [`Reflect`](bevy_reflect::Reflect) derive: /// ```rust,ignore diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index 725ebbe19539c..b039df6cd07e8 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -18,7 +18,7 @@ use std::any::Any; /// /// When adding a plugin to an [`App`]: /// * the app calls [`Plugin::build`] immediately, and register the plugin -/// * once the app started, it will wait for all registered [`Plugin::ready`] to return `true` +/// * once the app starts, it will wait for all registered [`Plugin::ready`] to return `true` /// * it will then call all registered [`Plugin::finish`] /// * and call all registered [`Plugin::cleanup`] pub trait Plugin: Downcast + Any + Send + Sync { diff --git a/crates/bevy_app/src/plugin_group.rs b/crates/bevy_app/src/plugin_group.rs index d5c7be8dcb602..dd0d3c580509b 100644 --- a/crates/bevy_app/src/plugin_group.rs +++ b/crates/bevy_app/src/plugin_group.rs @@ -190,6 +190,36 @@ impl PluginGroupBuilder { } } } + + /// Consumes the [`PluginGroupBuilder`] and [builds](Plugin::build) the contained [`Plugin`]s + /// in the order specified, then wait for them to be [ready](Plugin::ready) and + /// [finishes](Plugin::finish) their setup. + /// + /// Unlike the [`finish`](Self::finish) method, this method will return a future that + /// will be complete once the [lifecycle of the plugins](Plugin) is finished, leaving only the + /// `cleanup` stage to do. + /// + /// # Panics + /// + /// Panics if one of the plugin in the group was already added to the application. + pub async fn finish_async(mut self, app: &mut App) { + for ty in &self.order { + if let Some(entry) = self.plugins.remove(ty) { + if entry.enabled { + debug!("added plugin: {}", entry.plugin.name()); + if let Err(AppError::DuplicatePlugin { plugin_name }) = + app.add_boxed_plugin_async(entry.plugin).await + { + panic!( + "Error adding plugin {} in group {}: plugin was already added in application", + plugin_name, + self.group_name + ); + } + } + } + } + } } /// A plugin group which doesn't do anything. Useful for examples: diff --git a/crates/bevy_derive/src/bevy_main.rs b/crates/bevy_derive/src/bevy_main.rs index 36773c214b89a..f0a50e5c9e540 100644 --- a/crates/bevy_derive/src/bevy_main.rs +++ b/crates/bevy_derive/src/bevy_main.rs @@ -9,21 +9,53 @@ pub fn bevy_main(_attr: TokenStream, item: TokenStream) -> TokenStream { "`bevy_main` can only be used on a function called 'main'.", ); - TokenStream::from(quote! { - #[no_mangle] - #[cfg(target_os = "android")] - fn android_main(android_app: bevy::winit::AndroidApp) { - let _ = bevy::winit::ANDROID_APP.set(android_app); - main(); - } - - #[no_mangle] - #[cfg(target_os = "ios")] - extern "C" fn main_rs() { - main(); - } - - #[allow(unused)] - #input - }) + if input.sig.asyncness.is_some() { + TokenStream::from(quote! { + #[no_mangle] + #[cfg(target_os = "android")] + fn android_main(android_app: bevy::winit::AndroidApp) { + let _ = bevy::winit::ANDROID_APP.set(android_app); + futures_lite::future::block_on(main()); + } + + #[no_mangle] + #[cfg(target_os = "ios")] + extern "C" fn main_rs() { + futures_lite::future::block_on(main()); + } + + #[cfg(any(target_os = "ios", target_os = "android"))] + #[allow(unused)] + #input + + #[cfg(all(not(target_os = "ios"), not(target_os = "android")))] + fn main() { + #input + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(async move {main().await;}); + #[cfg(not(target_arch = "wasm32"))] + futures_lite::future::block_on(main()); + + } + }) + } else { + TokenStream::from(quote! { + #[no_mangle] + #[cfg(target_os = "android")] + fn android_main(android_app: bevy::winit::AndroidApp) { + let _ = bevy::winit::ANDROID_APP.set(android_app); + main(); + } + + #[no_mangle] + #[cfg(target_os = "ios")] + extern "C" fn main_rs() { + main(); + } + + #[allow(unused)] + #input + }) + } }