diff --git a/Cargo.toml b/Cargo.toml index 8912ebd0b0..d217aa9d16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ bevy_mod_scripting_asset = { path = "crates/bevy_mod_scripting_asset", version = bevy_mod_scripting_bindings = { path = "crates/bevy_mod_scripting_bindings", version = "0.16.0", default-features = false } bevy_mod_scripting_display = { path = "crates/bevy_mod_scripting_display", version = "0.16.0", default-features = false } bevy_mod_scripting_script = { path = "crates/bevy_mod_scripting_script", version = "0.16.0", default-features = false } + # bevy bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.16.0" } @@ -230,7 +231,10 @@ bevy = { workspace = true, features = [ "bevy_asset", "bevy_core_pipeline", "bevy_sprite", + "bevy_state", "x11", + "bevy_ui", + "default_font", ] } bevy_platform = { workspace = true } clap = { workspace = true, features = ["derive"] } @@ -308,7 +312,11 @@ required-features = [] [[example]] name = "runscript" -path = "examples/run-script.rs" +path = "examples/run_script.rs" + +[[example]] +name = "script_loading" +path = "examples/script_loading.rs" [workspace.lints.clippy] panic = "deny" diff --git a/assets/scripts/dummy.lua b/assets/scripts/dummy.lua new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/bevy_mod_scripting_bindings/src/globals/core.rs b/crates/bevy_mod_scripting_bindings/src/globals/core.rs index 828dbe4b90..d9880432b0 100644 --- a/crates/bevy_mod_scripting_bindings/src/globals/core.rs +++ b/crates/bevy_mod_scripting_bindings/src/globals/core.rs @@ -7,7 +7,7 @@ use ::{ bevy_reflect::TypeRegistration, }; use bevy_app::App; -use bevy_log::warn; +use bevy_log::{warn, warn_once}; use bevy_mod_scripting_asset::ScriptAsset; use bevy_mod_scripting_derive::script_globals; use bevy_platform::collections::HashMap; @@ -155,7 +155,7 @@ impl CoreGlobals { .insert(type_path.to_owned(), registration) .is_some() { - warn!( + warn_once!( "duplicate entry inside `types` global for type: {}. {MSG_DUPLICATE_GLOBAL}", type_path ) diff --git a/crates/bevy_mod_scripting_core/src/pipeline/machines.rs b/crates/bevy_mod_scripting_core/src/pipeline/machines.rs index 2c777a2844..32dfe922ab 100644 --- a/crates/bevy_mod_scripting_core/src/pipeline/machines.rs +++ b/crates/bevy_mod_scripting_core/src/pipeline/machines.rs @@ -135,6 +135,11 @@ pub trait TransitionListener: 'static + Send + Sync { } impl ActiveMachines

{ + /// Returns the currently processing machine + pub fn current_machine(&self) -> Option<&ScriptMachine

> { + self.machines.front() + } + /// Adds a listener to the back of the listener list for the state pub fn push_listener(&mut self, listener: impl TransitionListener + 'static) { let erased = listener.erased::

(); @@ -229,10 +234,9 @@ impl ScriptMachine

{ match &mut self.internal_state { MachineExecutionState::Initialized(machine_state) => { debug!( - "State '{}' entered. For script: {}, {:?}", + "State '{}' entered. For script: {}", machine_state.state_name(), self.context.attachment, - self.context.attachment.script(), ); if let Some(listeners) = listeners.get(&machine_state.as_ref().type_id()) { diff --git a/crates/bevy_mod_scripting_core/src/pipeline/mod.rs b/crates/bevy_mod_scripting_core/src/pipeline/mod.rs index 1c8d4c4b6c..f759e874c8 100644 --- a/crates/bevy_mod_scripting_core/src/pipeline/mod.rs +++ b/crates/bevy_mod_scripting_core/src/pipeline/mod.rs @@ -11,8 +11,10 @@ use bevy_ecs::{ system::{Command, Local, Res, ResMut, SystemParam}, world::World, }; +use bevy_log::debug; use bevy_mod_scripting_asset::ScriptAsset; use bevy_mod_scripting_bindings::WorldGuard; +use bevy_mod_scripting_display::DisplayProxy; use bevy_platform::collections::HashSet; use parking_lot::Mutex; use smallvec::SmallVec; @@ -22,7 +24,7 @@ use crate::{ context::ScriptingLoader, error::ScriptError, event::{ - ForPlugin, ScriptAssetModifiedEvent, ScriptAttachedEvent, ScriptDetachedEvent, + ForPlugin, Recipients, ScriptAssetModifiedEvent, ScriptAttachedEvent, ScriptDetachedEvent, ScriptErrorEvent, }, pipeline::hooks::{ @@ -175,7 +177,7 @@ impl LoadedWithHandles<'_, '_, T> { self.loading.retain(|e| { let handle = e.get_script_handle(); match self.asset_server.get_load_state(&handle) { - Some(LoadState::Loaded) => { + Some(LoadState::Loaded) | None => { // none in case this is added in memory and not through asset server let strong = StrongScriptHandle::from_assets(handle, &mut self.assets); if let Some(strong) = strong { self.loaded_with_handles.push_front((e.clone(), strong)); @@ -183,7 +185,14 @@ impl LoadedWithHandles<'_, '_, T> { false } Some(LoadState::Loading) => true, - _ => false, + state => { + + debug!( + "discarding script lifecycle triggers with handle: {} due to asset load state: {state:?}", + handle.display() + ); + false + } } }); @@ -345,6 +354,55 @@ impl PipelineRun for App { } } +#[derive(SystemParam)] +/// System parameter composing resources related to script loading, exposing utility methods for checking on your script pipeline status +pub struct ScriptPipelineState<'w, P: IntoScriptPluginParams> { + contexts: Res<'w, ScriptContext

>, + machines: Res<'w, ActiveMachines

>, +} + +impl<'w, P: IntoScriptPluginParams> ScriptPipelineState<'w, P> { + /// Returns the handle to the currently processing script, if the handle came from an asset server and a path, + /// it can be used to display the currently loading script + pub fn currently_loading_script(&self) -> Option> { + self.machines + .current_machine() + .map(|machine| machine.context.attachment.script()) + } + + /// Returns the number of scripts currently being processed, + /// this includes loads, reloads and removals, when this is zero, no processing is happening at the moment + pub fn num_processing_scripts(&self) -> usize { + self.machines.active_machines() + } + + /// returns true if the current processing batch is completed, + /// a batch is completed when the last active processing machine is finished. + /// If new machines are added during the processing of a batch, that batch is "extended". + pub fn processing_batch_completed(&self) -> bool { + self.num_processing_scripts() == 0 + } + + /// Returns the number of scripts currently existing in contexts. + /// This corresponds to [`Recipients::AllScripts`], i.e. it counts 'residents' within contexts as a script + pub fn num_loaded_scripts(&self) -> usize { + Recipients::AllScripts + .get_recipients(self.contexts.clone()) + .len() + } + + /// returns a number between 0 and 100.0 to represent the current script pipeline progress, + /// 0 representing no progress made, and 100 all processing completed, together with the numbers used for the fraction loaded and total. + pub fn progress(&self) -> (f32, usize, usize) { + let fraction = self.num_loaded_scripts(); + let total = self.num_processing_scripts() + fraction; + if total == 0 { + return (0.0, 0, 0); + } + ((fraction as f32 / total as f32) * 100.0, fraction, total) + } +} + #[cfg(test)] mod test { use bevy_asset::{AssetApp, AssetId, AssetPlugin}; diff --git a/crates/bevy_mod_scripting_core/src/pipeline/start.rs b/crates/bevy_mod_scripting_core/src/pipeline/start.rs index b5ecb2cb8a..f02b15a111 100644 --- a/crates/bevy_mod_scripting_core/src/pipeline/start.rs +++ b/crates/bevy_mod_scripting_core/src/pipeline/start.rs @@ -1,5 +1,6 @@ use super::*; use bevy_asset::AssetEvent; +use bevy_log::{debug, trace}; /// A handle to a script asset which can only be made from a strong handle #[derive(Clone, Debug)] @@ -83,6 +84,7 @@ pub fn filter_script_attachments( mut filtered: EventWriter>, ) { let mut batch = events.get_loaded().map(|(mut a, b)| { + trace!("dispatching script attachment event for: {a:?}"); *a.0.script_mut() = b.0; ForPlugin::new(a) }); @@ -106,6 +108,7 @@ pub fn filter_script_detachments( .map(ForPlugin::new); if let Some(next) = batch.next() { + trace!("dispatching script dettachments for plugin"); filtered.write_batch(std::iter::once(next).chain(batch)); } } @@ -120,6 +123,7 @@ pub fn process_attachments( let contexts = contexts.read(); events.read().for_each(|wrapper| { let attachment_event = wrapper.event(); + debug!("received attachment event: {attachment_event:?}"); let id = attachment_event.0.script(); let mut context = Context { attachment: attachment_event.0.clone(), diff --git a/crates/bevy_mod_scripting_core/src/script/mod.rs b/crates/bevy_mod_scripting_core/src/script/mod.rs index f8d9732da8..296d4cb175 100644 --- a/crates/bevy_mod_scripting_core/src/script/mod.rs +++ b/crates/bevy_mod_scripting_core/src/script/mod.rs @@ -19,6 +19,7 @@ use ::{ mod context_key; mod script_context; use bevy_ecs::component::Component; +use bevy_log::trace; use bevy_mod_scripting_asset::ScriptAsset; use bevy_mod_scripting_script::ScriptAttachment; pub use context_key::*; @@ -65,6 +66,7 @@ impl ScriptComponent { /// the removal of the script. pub fn on_remove(mut world: DeferredWorld, context: HookContext) { let context_keys = Self::get_context_keys_present(&world, context.entity); + trace!("on remove hook for script components: {context_keys:?}"); world.send_event_batch(context_keys.into_iter().map(ScriptDetachedEvent)); } @@ -72,6 +74,7 @@ impl ScriptComponent { /// the addition of the script. pub fn on_add(mut world: DeferredWorld, context: HookContext) { let context_keys = Self::get_context_keys_present(&world, context.entity); + trace!("on add hook for script components: {context_keys:?}"); world.send_event_batch(context_keys.into_iter().map(ScriptAttachedEvent)); } } diff --git a/crates/bevy_mod_scripting_core/src/script/script_context.rs b/crates/bevy_mod_scripting_core/src/script/script_context.rs index c5c160451e..952514b0fd 100644 --- a/crates/bevy_mod_scripting_core/src/script/script_context.rs +++ b/crates/bevy_mod_scripting_core/src/script/script_context.rs @@ -391,6 +391,11 @@ impl ScriptContextInner

{ }) } + /// Returns the count of residents as would be returned by [`Self::all_residents`] + pub fn all_residents_len(&self) -> usize { + self.map.values().map(|entry| entry.residents.len()).sum() + } + /// Retrieves the first resident from each context. /// /// For example if using a single global context, and with 2 scripts: diff --git a/docs/src/ScriptPipeline/pipeline.md b/docs/src/ScriptPipeline/pipeline.md index 4b895a0c35..2788a5449f 100644 --- a/docs/src/ScriptPipeline/pipeline.md +++ b/docs/src/ScriptPipeline/pipeline.md @@ -80,4 +80,8 @@ The script loading/unloading order will look as follows: - the order in which components are attached/detached, will determing what order scripts will be processed - scripts are processed one-by-one, i.e. each machine is ticked to completion before the next one is started - meaning for example if two scripts are loaded, their `on_script_loaded` hooks will not run at the same "lockstep". -- loading/unloading might happen over multiple frames, depending on the pipeline's settings. \ No newline at end of file +- loading/unloading might happen over multiple frames, depending on the pipeline's settings. + +## Waiting for scripts to load + +In order to check on the pipeline and figure out when everything is ready, you can use the `ScriptPipelineState

` system parameter in a system as shown in the `script_loading` [example](https://github.com/makspll/bevy_mod_scripting/blob/main/examples/script_loading.rs). \ No newline at end of file diff --git a/docs/src/Summary/managing-scripts.md b/docs/src/Summary/managing-scripts.md index 1bd8a88d70..938a7fb546 100644 --- a/docs/src/Summary/managing-scripts.md +++ b/docs/src/Summary/managing-scripts.md @@ -49,6 +49,13 @@ fn load_script(asset_server: Res, mut commands: Commands) { commands.spawn(ScriptComponent(vec![handle])); } ``` + +

+ +Prefer using strong asset handles, internal references will only persist weak versions of the handle, leaving you in control of the asset handle via the container component. + +
+ ### Create `ScriptAsset` and Add It ```rust # extern crate bevy; @@ -64,6 +71,11 @@ fn add_script(mut script_assets: ResMut>, mut commands: Comm commands.spawn(ScriptComponent(vec![handle])); } ``` +
+ +Scripts added directly through assets, or via asset server without an asset path, will not contain path information, and logs will be slightly less useful. + +
## Static Scripts You can use attach and detach commands, which will run the [script pipeline](../ScriptPipeline/pipeline.md), repeatedly until the requested script attachments are processed. If you don't want to incur big frametime slowdowns, you can instead send `ScriptAttachedEvent` and `ScriptDetachedEvent` manually, and let the pipeline pick these up as normal. diff --git a/examples/run-script.rs b/examples/run_script.rs similarity index 100% rename from examples/run-script.rs rename to examples/run_script.rs diff --git a/examples/script_loading.rs b/examples/script_loading.rs new file mode 100644 index 0000000000..d1d5a23743 --- /dev/null +++ b/examples/script_loading.rs @@ -0,0 +1,190 @@ +use bevy::prelude::*; +use bevy_mod_scripting::lua::*; +use bevy_mod_scripting::prelude::*; +use bevy_mod_scripting_core::pipeline::ScriptPipelineState; + +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] +enum GameState { + #[default] + ScriptLoading, + Running, +} + +// create a large number of scripts which will take some time to process +pub fn initialize_script_loading( + // mut script_assets: ResMut>, + asset_server: ResMut, + mut commands: Commands, +) { + for _ in 0..1000 { + // you can create assets in memory instead of loading + // the result is the same, apart from the fact your strong handles won't contain path information + let script = ScriptAsset::new( + " + function on_script_loaded() + end + " + .to_string(), + ); + let handle = asset_server.add(script); + // in this case though, reusing the same asset means you only get one logical "script" and one asset handle + // we want to simulate many unique scripts + // let handle = asset_server + // .load(AssetPath::from_static("scripts/dummy.lua")); + commands.spawn(ScriptComponent(vec![handle])); + } +} + +/// Marker component for the UI root +#[derive(Component)] +struct LoadingUiRoot; + +/// Marker components for UI updates +#[derive(Component)] +struct LoadingBarFill; + +#[derive(Component)] +struct ScriptNameText; + +#[derive(Component)] +struct PercentageText; + +/// Creates a centered loading UI +fn setup_loading_ui(mut commands: Commands) { + commands.spawn(Camera2d); + commands + .spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(Color::NONE), + )) + .with_children(|parent| { + parent + .spawn(( + Node { + width: Val::Px(400.0), + height: Val::Px(120.0), + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + row_gap: Val::Px(10.0), + ..default() + }, + BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.3)), + )) + .with_children(|column| { + // Script name text + column.spawn(( + Text::new("Loading: (none)"), + TextFont { + font_size: 24.0, + ..default() + }, + TextColor(Color::WHITE), + ScriptNameText, + )); + + // Bar background + column + .spawn(( + Node { + width: Val::Px(300.0), + height: Val::Px(24.0), + justify_content: JustifyContent::FlexStart, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(Color::darker(&Color::WHITE, 0.5)), + )) + .with_children(|bar_bg| { + // Filled portion of bar + bar_bg.spawn(( + Node { + width: Val::Percent(0.0), // updated dynamically + height: Val::Percent(100.0), + ..default() + }, + BackgroundColor(Color::linear_rgb(0.0, 1.0, 0.0)), + LoadingBarFill, + )); + }); + + // Percentage text + column.spawn(( + Text::new("0%"), + TextFont { + font_size: 20.0, + ..default() + }, + TextColor(Color::WHITE), + PercentageText, + )); + }); + }); +} + +/// Updates the loading UI elements based on progress +fn update_loading_ui( + pipeline_state: ScriptPipelineState, + mut fill_query: Query<&mut Node, With>, + mut name_query: Query<&mut Text, (With, Without)>, + mut percent_query: Query<&mut Text, (With, Without)>, + mut next_state: ResMut>, +) { + // this is the progress of the currently loading batch of scripts + // if some scripts are reloading these will be included in the count + let (progress, loaded, total) = pipeline_state.progress(); + + let current_script = pipeline_state + .currently_loading_script() + .and_then(|handle| handle.path().cloned()) + .map(|p| p.to_string()) + .unwrap_or(String::from("script")); + + // Update bar fill width + if let Ok(mut node) = fill_query.single_mut() { + node.width = Val::Percent(progress); + } + + // Update script name + if let Ok(mut text) = name_query.single_mut() { + *text = Text::new(format!("Loading: {current_script}")); + } + + // Update percentage text + if let Ok(mut text) = percent_query.single_mut() { + *text = Text::new(format!("{progress:.0}% ({loaded}/{total})")); + } + + if pipeline_state.processing_batch_completed() && loaded > 0 { + next_state.set(GameState::Running) + } +} + +fn set_loading_ui_completed( + mut name_query: Query<&mut Text, (With, Without)>, +) { + // Update script name + if let Ok(mut text) = name_query.single_mut() { + *text = Text::new("Loaded all scripts!".to_string()); + } +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(BMSPlugin) + .init_state::() + .add_systems(Startup, (initialize_script_loading, setup_loading_ui)) + .add_systems( + Update, + update_loading_ui.run_if(in_state(GameState::ScriptLoading)), + ) + .add_systems(OnEnter(GameState::Running), set_loading_ui_completed) + .run(); +}