Skip to content

Commit 2b34279

Browse files
authored
feat: add ScriptPipelineState system parameter and rust script loading example (#494)
1 parent be48926 commit 2b34279

File tree

12 files changed

+297
-9
lines changed

12 files changed

+297
-9
lines changed

Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ bevy_mod_scripting_asset = { path = "crates/bevy_mod_scripting_asset", version =
129129
bevy_mod_scripting_bindings = { path = "crates/bevy_mod_scripting_bindings", version = "0.16.0", default-features = false }
130130
bevy_mod_scripting_display = { path = "crates/bevy_mod_scripting_display", version = "0.16.0", default-features = false }
131131
bevy_mod_scripting_script = { path = "crates/bevy_mod_scripting_script", version = "0.16.0", default-features = false }
132+
132133
# bevy
133134

134135
bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.16.0" }
@@ -230,7 +231,10 @@ bevy = { workspace = true, features = [
230231
"bevy_asset",
231232
"bevy_core_pipeline",
232233
"bevy_sprite",
234+
"bevy_state",
233235
"x11",
236+
"bevy_ui",
237+
"default_font",
234238
] }
235239
bevy_platform = { workspace = true }
236240
clap = { workspace = true, features = ["derive"] }
@@ -308,7 +312,11 @@ required-features = []
308312

309313
[[example]]
310314
name = "runscript"
311-
path = "examples/run-script.rs"
315+
path = "examples/run_script.rs"
316+
317+
[[example]]
318+
name = "script_loading"
319+
path = "examples/script_loading.rs"
312320

313321
[workspace.lints.clippy]
314322
panic = "deny"

assets/scripts/dummy.lua

Whitespace-only changes.

crates/bevy_mod_scripting_bindings/src/globals/core.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use ::{
77
bevy_reflect::TypeRegistration,
88
};
99
use bevy_app::App;
10-
use bevy_log::warn;
10+
use bevy_log::{warn, warn_once};
1111
use bevy_mod_scripting_asset::ScriptAsset;
1212
use bevy_mod_scripting_derive::script_globals;
1313
use bevy_platform::collections::HashMap;
@@ -155,7 +155,7 @@ impl CoreGlobals {
155155
.insert(type_path.to_owned(), registration)
156156
.is_some()
157157
{
158-
warn!(
158+
warn_once!(
159159
"duplicate entry inside `types` global for type: {}. {MSG_DUPLICATE_GLOBAL}",
160160
type_path
161161
)

crates/bevy_mod_scripting_core/src/pipeline/machines.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ pub trait TransitionListener<State>: 'static + Send + Sync {
135135
}
136136

137137
impl<P: IntoScriptPluginParams> ActiveMachines<P> {
138+
/// Returns the currently processing machine
139+
pub fn current_machine(&self) -> Option<&ScriptMachine<P>> {
140+
self.machines.front()
141+
}
142+
138143
/// Adds a listener to the back of the listener list for the state
139144
pub fn push_listener<S: 'static>(&mut self, listener: impl TransitionListener<S> + 'static) {
140145
let erased = listener.erased::<P>();
@@ -229,10 +234,9 @@ impl<P: IntoScriptPluginParams> ScriptMachine<P> {
229234
match &mut self.internal_state {
230235
MachineExecutionState::Initialized(machine_state) => {
231236
debug!(
232-
"State '{}' entered. For script: {}, {:?}",
237+
"State '{}' entered. For script: {}",
233238
machine_state.state_name(),
234239
self.context.attachment,
235-
self.context.attachment.script(),
236240
);
237241

238242
if let Some(listeners) = listeners.get(&machine_state.as_ref().type_id()) {

crates/bevy_mod_scripting_core/src/pipeline/mod.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ use bevy_ecs::{
1111
system::{Command, Local, Res, ResMut, SystemParam},
1212
world::World,
1313
};
14+
use bevy_log::debug;
1415
use bevy_mod_scripting_asset::ScriptAsset;
1516
use bevy_mod_scripting_bindings::WorldGuard;
17+
use bevy_mod_scripting_display::DisplayProxy;
1618
use bevy_platform::collections::HashSet;
1719
use parking_lot::Mutex;
1820
use smallvec::SmallVec;
@@ -22,7 +24,7 @@ use crate::{
2224
context::ScriptingLoader,
2325
error::ScriptError,
2426
event::{
25-
ForPlugin, ScriptAssetModifiedEvent, ScriptAttachedEvent, ScriptDetachedEvent,
27+
ForPlugin, Recipients, ScriptAssetModifiedEvent, ScriptAttachedEvent, ScriptDetachedEvent,
2628
ScriptErrorEvent,
2729
},
2830
pipeline::hooks::{
@@ -175,15 +177,22 @@ impl<T: GetScriptHandle + Event + Clone> LoadedWithHandles<'_, '_, T> {
175177
self.loading.retain(|e| {
176178
let handle = e.get_script_handle();
177179
match self.asset_server.get_load_state(&handle) {
178-
Some(LoadState::Loaded) => {
180+
Some(LoadState::Loaded) | None => { // none in case this is added in memory and not through asset server
179181
let strong = StrongScriptHandle::from_assets(handle, &mut self.assets);
180182
if let Some(strong) = strong {
181183
self.loaded_with_handles.push_front((e.clone(), strong));
182184
}
183185
false
184186
}
185187
Some(LoadState::Loading) => true,
186-
_ => false,
188+
state => {
189+
190+
debug!(
191+
"discarding script lifecycle triggers with handle: {} due to asset load state: {state:?}",
192+
handle.display()
193+
);
194+
false
195+
}
187196
}
188197
});
189198

@@ -345,6 +354,55 @@ impl PipelineRun for App {
345354
}
346355
}
347356

357+
#[derive(SystemParam)]
358+
/// System parameter composing resources related to script loading, exposing utility methods for checking on your script pipeline status
359+
pub struct ScriptPipelineState<'w, P: IntoScriptPluginParams> {
360+
contexts: Res<'w, ScriptContext<P>>,
361+
machines: Res<'w, ActiveMachines<P>>,
362+
}
363+
364+
impl<'w, P: IntoScriptPluginParams> ScriptPipelineState<'w, P> {
365+
/// Returns the handle to the currently processing script, if the handle came from an asset server and a path,
366+
/// it can be used to display the currently loading script
367+
pub fn currently_loading_script(&self) -> Option<Handle<ScriptAsset>> {
368+
self.machines
369+
.current_machine()
370+
.map(|machine| machine.context.attachment.script())
371+
}
372+
373+
/// Returns the number of scripts currently being processed,
374+
/// this includes loads, reloads and removals, when this is zero, no processing is happening at the moment
375+
pub fn num_processing_scripts(&self) -> usize {
376+
self.machines.active_machines()
377+
}
378+
379+
/// returns true if the current processing batch is completed,
380+
/// a batch is completed when the last active processing machine is finished.
381+
/// If new machines are added during the processing of a batch, that batch is "extended".
382+
pub fn processing_batch_completed(&self) -> bool {
383+
self.num_processing_scripts() == 0
384+
}
385+
386+
/// Returns the number of scripts currently existing in contexts.
387+
/// This corresponds to [`Recipients::AllScripts`], i.e. it counts 'residents' within contexts as a script
388+
pub fn num_loaded_scripts(&self) -> usize {
389+
Recipients::AllScripts
390+
.get_recipients(self.contexts.clone())
391+
.len()
392+
}
393+
394+
/// returns a number between 0 and 100.0 to represent the current script pipeline progress,
395+
/// 0 representing no progress made, and 100 all processing completed, together with the numbers used for the fraction loaded and total.
396+
pub fn progress(&self) -> (f32, usize, usize) {
397+
let fraction = self.num_loaded_scripts();
398+
let total = self.num_processing_scripts() + fraction;
399+
if total == 0 {
400+
return (0.0, 0, 0);
401+
}
402+
((fraction as f32 / total as f32) * 100.0, fraction, total)
403+
}
404+
}
405+
348406
#[cfg(test)]
349407
mod test {
350408
use bevy_asset::{AssetApp, AssetId, AssetPlugin};

crates/bevy_mod_scripting_core/src/pipeline/start.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::*;
22
use bevy_asset::AssetEvent;
3+
use bevy_log::{debug, trace};
34

45
/// A handle to a script asset which can only be made from a strong handle
56
#[derive(Clone, Debug)]
@@ -83,6 +84,7 @@ pub fn filter_script_attachments<P: IntoScriptPluginParams>(
8384
mut filtered: EventWriter<ForPlugin<ScriptAttachedEvent, P>>,
8485
) {
8586
let mut batch = events.get_loaded().map(|(mut a, b)| {
87+
trace!("dispatching script attachment event for: {a:?}");
8688
*a.0.script_mut() = b.0;
8789
ForPlugin::new(a)
8890
});
@@ -106,6 +108,7 @@ pub fn filter_script_detachments<P: IntoScriptPluginParams>(
106108
.map(ForPlugin::new);
107109

108110
if let Some(next) = batch.next() {
111+
trace!("dispatching script dettachments for plugin");
109112
filtered.write_batch(std::iter::once(next).chain(batch));
110113
}
111114
}
@@ -120,6 +123,7 @@ pub fn process_attachments<P: IntoScriptPluginParams>(
120123
let contexts = contexts.read();
121124
events.read().for_each(|wrapper| {
122125
let attachment_event = wrapper.event();
126+
debug!("received attachment event: {attachment_event:?}");
123127
let id = attachment_event.0.script();
124128
let mut context = Context {
125129
attachment: attachment_event.0.clone(),

crates/bevy_mod_scripting_core/src/script/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use ::{
1919
mod context_key;
2020
mod script_context;
2121
use bevy_ecs::component::Component;
22+
use bevy_log::trace;
2223
use bevy_mod_scripting_asset::ScriptAsset;
2324
use bevy_mod_scripting_script::ScriptAttachment;
2425
pub use context_key::*;
@@ -65,13 +66,15 @@ impl ScriptComponent {
6566
/// the removal of the script.
6667
pub fn on_remove(mut world: DeferredWorld, context: HookContext) {
6768
let context_keys = Self::get_context_keys_present(&world, context.entity);
69+
trace!("on remove hook for script components: {context_keys:?}");
6870
world.send_event_batch(context_keys.into_iter().map(ScriptDetachedEvent));
6971
}
7072

7173
/// the lifecycle hook called when a script component is added to an entity, emits an appropriate event so we can handle
7274
/// the addition of the script.
7375
pub fn on_add(mut world: DeferredWorld, context: HookContext) {
7476
let context_keys = Self::get_context_keys_present(&world, context.entity);
77+
trace!("on add hook for script components: {context_keys:?}");
7578
world.send_event_batch(context_keys.into_iter().map(ScriptAttachedEvent));
7679
}
7780
}

crates/bevy_mod_scripting_core/src/script/script_context.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,11 @@ impl<P: IntoScriptPluginParams> ScriptContextInner<P> {
391391
})
392392
}
393393

394+
/// Returns the count of residents as would be returned by [`Self::all_residents`]
395+
pub fn all_residents_len(&self) -> usize {
396+
self.map.values().map(|entry| entry.residents.len()).sum()
397+
}
398+
394399
/// Retrieves the first resident from each context.
395400
///
396401
/// For example if using a single global context, and with 2 scripts:

docs/src/ScriptPipeline/pipeline.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,8 @@ The script loading/unloading order will look as follows:
8080
- the order in which components are attached/detached, will determing what order scripts will be processed
8181
- scripts are processed one-by-one, i.e. each machine is ticked to completion before the next one is started
8282
- meaning for example if two scripts are loaded, their `on_script_loaded` hooks will not run at the same "lockstep".
83-
- loading/unloading might happen over multiple frames, depending on the pipeline's settings.
83+
- loading/unloading might happen over multiple frames, depending on the pipeline's settings.
84+
85+
## Waiting for scripts to load
86+
87+
In order to check on the pipeline and figure out when everything is ready, you can use the `ScriptPipelineState<P>` 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).

docs/src/Summary/managing-scripts.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ fn load_script(asset_server: Res<AssetServer>, mut commands: Commands) {
4949
commands.spawn(ScriptComponent(vec![handle]));
5050
}
5151
```
52+
53+
<div class="warning">
54+
55+
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.
56+
57+
</div>
58+
5259
### Create `ScriptAsset` and Add It
5360
```rust
5461
# extern crate bevy;
@@ -64,6 +71,11 @@ fn add_script(mut script_assets: ResMut<Assets<ScriptAsset>>, mut commands: Comm
6471
commands.spawn(ScriptComponent(vec![handle]));
6572
}
6673
```
74+
<div class="warning">
75+
76+
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.
77+
78+
</div>
6779

6880
## Static Scripts
6981
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.

0 commit comments

Comments
 (0)