use std::{any::Any, collections::VecDeque, marker::PhantomData, sync::Arc, time::Duration};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_asset::{AssetServer, Assets, Handle, LoadState};
use bevy_ecs::{
message::{Message, MessageReader},
resource::Resource,
schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, SystemSet},
system::{Command, Local, Res, ResMut, SystemParam},
world::World,
};
use bevy_log::debug;
use bevy_mod_scripting_asset::{Language, 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;
use crate::{
IntoScriptPluginParams,
context::ScriptingLoader,
error::ScriptError,
event::{
ForPlugin, Recipients, ScriptAssetModifiedEvent, ScriptAttachedEvent, ScriptDetachedEvent,
ScriptErrorEvent,
},
pipeline::hooks::{
OnLoadedListener, OnReloadedListener, OnUnloadedForReloadListener,
OnUnloadedForUnloadListener,
},
script::ScriptContext,
};
mod hooks;
mod machines;
mod start;
pub use {machines::*, start::*};
#[derive(SystemSet, Hash, Debug, Clone, Copy, PartialEq, Eq)]
pub enum PipelineSet {
ListeningPhase,
MachineStartPhase,
}
pub struct ScriptLoadingPipeline<P: IntoScriptPluginParams> {
pub script_component_triggers: bool,
pub hot_loading_asset_triggers: bool,
pub on_script_loaded_callback: bool,
pub on_script_reloaded_callback: bool,
pub on_script_unloaded_callback: bool,
_ph: PhantomData<fn(P)>,
pub time_budget: Option<Duration>,
}
impl<P: IntoScriptPluginParams> Clone for ScriptLoadingPipeline<P> {
fn clone(&self) -> Self {
Self {
script_component_triggers: self.script_component_triggers,
hot_loading_asset_triggers: self.hot_loading_asset_triggers,
on_script_loaded_callback: self.on_script_loaded_callback,
on_script_reloaded_callback: self.on_script_reloaded_callback,
on_script_unloaded_callback: self.on_script_unloaded_callback,
_ph: self._ph,
time_budget: self.time_budget,
}
}
}
impl<P: IntoScriptPluginParams> Default for ScriptLoadingPipeline<P> {
fn default() -> Self {
Self {
_ph: PhantomData,
script_component_triggers: true,
hot_loading_asset_triggers: true,
on_script_loaded_callback: true,
on_script_reloaded_callback: true,
on_script_unloaded_callback: true,
time_budget: Some(Duration::from_millis(16)),
}
}
}
#[derive(ScheduleLabel, Copy)]
pub struct ScriptProcessingSchedule<P: IntoScriptPluginParams>(PhantomData<fn(P)>);
impl<P: IntoScriptPluginParams> Default for ScriptProcessingSchedule<P> {
fn default() -> Self {
Self(Default::default())
}
}
impl<P: IntoScriptPluginParams> PartialEq for ScriptProcessingSchedule<P> {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl<P: IntoScriptPluginParams> Eq for ScriptProcessingSchedule<P> {}
impl<P: IntoScriptPluginParams> std::hash::Hash for ScriptProcessingSchedule<P> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
0.hash(state)
}
}
impl<P: IntoScriptPluginParams> Clone for ScriptProcessingSchedule<P> {
fn clone(&self) -> Self {
ScriptProcessingSchedule::default()
}
}
impl<P: IntoScriptPluginParams> std::fmt::Debug for ScriptProcessingSchedule<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(&format!(
"ScriptProcessingSchedule<{}>",
std::any::type_name::<P>()
))
.finish()
}
}
impl<P: IntoScriptPluginParams> ScriptLoadingPipeline<P> {
fn add_plugin_message<E: Message>(&self, app: &mut App) -> &Self {
app.add_message::<E>().add_message::<ForPlugin<E, P>>();
self
}
}
pub trait GetScriptHandle {
fn get_script_handle(&self) -> Handle<ScriptAsset>;
}
#[derive(SystemParam)]
pub struct LoadedWithHandles<'w, 's, T: GetScriptHandle + Message + Clone> {
assets: ResMut<'w, Assets<ScriptAsset>>,
asset_server: Res<'w, AssetServer>,
fresh_events: MessageReader<'w, 's, T>,
loaded_with_handles: Local<'s, VecDeque<(T, StrongScriptHandle, Language)>>,
loading: Local<'s, VecDeque<T>>,
}
impl<T: GetScriptHandle + Message + Clone> LoadedWithHandles<'_, '_, T> {
pub fn get_loaded(&mut self) -> impl Iterator<Item = (T, StrongScriptHandle, Language)> {
self.loading.extend(self.fresh_events.read().cloned());
self.loading.retain(|e| {
let handle = e.get_script_handle();
match self.asset_server.get_load_state(&handle) {
Some(LoadState::Loaded) | None => { let strong = StrongScriptHandle::from_assets(handle, &mut self.assets);
if let Some(strong) = strong {
let lang = strong.get(&self.assets).language.clone();
self.loaded_with_handles.push_front((e.clone(), strong, lang));
}
false
}
Some(LoadState::Loading) => true,
state => {
debug!(
"discarding script lifecycle triggers with handle: {} due to asset load state: {state:?}",
handle.display()
);
false
}
}
});
self.loaded_with_handles.drain(..)
}
}
impl<P: IntoScriptPluginParams> Plugin for ScriptLoadingPipeline<P> {
fn build(&self, app: &mut App) {
self.add_plugin_message::<ScriptAttachedEvent>(app)
.add_plugin_message::<ScriptDetachedEvent>(app)
.add_plugin_message::<ScriptAssetModifiedEvent>(app);
let mut active_machines = app.world_mut().get_resource_or_init::<ActiveMachines<P>>();
if self.on_script_loaded_callback {
active_machines.push_listener::<ContextAssigned<P>>(OnLoadedListener);
}
if self.on_script_reloaded_callback {
active_machines.push_listener::<ContextAssigned<P>>(OnReloadedListener);
}
if self.on_script_unloaded_callback {
active_machines.push_listener::<ReloadingInitialized<P>>(OnUnloadedForReloadListener);
active_machines.push_listener::<UnloadingInitialized<P>>(OnUnloadedForUnloadListener);
}
active_machines.budget = self.time_budget;
app.configure_sets(
PostUpdate,
PipelineSet::ListeningPhase.before(PipelineSet::MachineStartPhase),
);
if self.script_component_triggers {
app.add_systems(
PostUpdate,
filter_script_attachments::<P>
.in_set(PipelineSet::ListeningPhase)
.before(filter_script_modifications::<P>)
.before(filter_script_detachments::<P>),
);
app.add_systems(
PostUpdate,
filter_script_detachments::<P>
.in_set(PipelineSet::ListeningPhase)
.after(filter_script_attachments::<P>)
.before(filter_script_modifications::<P>),
);
}
if self.hot_loading_asset_triggers {
app.add_systems(
PostUpdate,
filter_script_modifications::<P>.in_set(PipelineSet::ListeningPhase),
);
}
app.add_systems(
PostUpdate,
(
process_attachments::<P>,
process_detachments::<P>,
process_asset_modifications::<P>,
)
.chain()
.in_set(PipelineSet::MachineStartPhase),
);
app.add_systems(
PostUpdate,
automatic_pipeline_runner::<P>.after(PipelineSet::MachineStartPhase),
);
let mut schedule = Schedule::new(ScriptProcessingSchedule::<P>(Default::default()));
schedule.add_systems(machine_ticker::<P>);
app.add_schedule(schedule);
}
}
pub fn machine_ticker<P: IntoScriptPluginParams>(world: &mut World) {
if let Some(mut machines) = world.remove_resource::<ActiveMachines<P>>() {
machines.tick_machines(world);
world.insert_resource(machines)
}
}
pub struct RunProcessingPipelineOnce<P> {
override_budget: Option<Duration>,
_ph: PhantomData<fn(P)>,
}
impl<P> RunProcessingPipelineOnce<P> {
pub fn new(override_budget: Option<Duration>) -> Self {
Self {
override_budget,
_ph: PhantomData,
}
}
}
impl<P> Default for RunProcessingPipelineOnce<P> {
fn default() -> Self {
Self::new(None)
}
}
impl<P: IntoScriptPluginParams> Command for RunProcessingPipelineOnce<P> {
fn apply(self, world: &mut World) {
let mut last_setting = None;
if let Some(override_budget) = self.override_budget {
let mut machines = world.get_resource_or_init::<ActiveMachines<P>>();
last_setting = Some(machines.budget);
machines.budget = Some(override_budget)
}
world.run_schedule(ScriptProcessingSchedule::<P>::default());
if let Some(last_setting) = last_setting {
let mut machines = world.get_resource_or_init::<ActiveMachines<P>>();
machines.budget = last_setting;
}
}
}
pub fn automatic_pipeline_runner<P: IntoScriptPluginParams>(world: &mut World) {
if world
.get_resource::<ActiveMachines<P>>()
.is_some_and(|machines| machines.active_machines() > 0)
{
RunProcessingPipelineOnce::<P>::new(None).apply(world);
}
}
pub trait PipelineRun {
fn update_until_all_scripts_processed<P: IntoScriptPluginParams>(&mut self);
}
impl PipelineRun for App {
fn update_until_all_scripts_processed<P: IntoScriptPluginParams>(&mut self) {
loop {
let world = self.world_mut();
let machines = world.get_resource::<ActiveMachines<P>>();
let has_active = machines.is_some_and(|machines| machines.active_machines() > 0);
if !has_active {
break;
}
self.update();
}
}
}
#[derive(SystemParam)]
pub struct ScriptPipelineState<'w, P: IntoScriptPluginParams> {
contexts: Res<'w, ScriptContext<P>>,
machines: Res<'w, ActiveMachines<P>>,
}
impl<'w, P: IntoScriptPluginParams> ScriptPipelineState<'w, P> {
pub fn currently_loading_script(&self) -> Option<Handle<ScriptAsset>> {
self.machines
.current_machine()
.map(|machine| machine.context.attachment.script())
}
pub fn num_processing_scripts(&self) -> usize {
self.machines.active_machines()
}
pub fn processing_batch_completed(&self) -> bool {
self.num_processing_scripts() == 0
}
pub fn num_loaded_scripts(&self) -> usize {
Recipients::AllScripts
.get_recipients(self.contexts.clone())
.len()
}
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, AssetPlugin};
use bevy_ecs::{entity::Entity, system::SystemState, world::FromWorld};
use bevy_mod_scripting_asset::Language;
use bevy_mod_scripting_bindings::ScriptValue;
use bevy_mod_scripting_script::ScriptAttachment;
use test_utils::make_test_plugin;
use crate::config::{GetPluginThreadConfig, ScriptingPluginConfiguration};
use super::*;
#[test]
fn test_system_params() {
let mut app = App::default();
app.add_message::<ScriptAttachedEvent>();
app.add_plugins(AssetPlugin::default());
app.init_asset::<ScriptAsset>();
app.finish();
let world = app.world_mut();
let mut system_state =
SystemState::<LoadedWithHandles<ScriptAttachedEvent>>::from_world(world);
{
let mut state = system_state.get_mut(world);
let loaded = state.get_loaded().collect::<Vec<_>>();
assert!(loaded.is_empty())
}
let asset_server = world.get_resource_mut::<AssetServer>().unwrap();
let asset = ScriptAsset {
content: "asd".to_string().into_boxed_str().into_boxed_bytes(),
language: Language::Lua,
};
let handle = asset_server.add(asset);
let handle_invalid = Handle::default();
world.write_message(ScriptAttachedEvent(ScriptAttachment::StaticScript(handle)));
world.write_message(ScriptAttachedEvent(ScriptAttachment::StaticScript(
handle_invalid,
)));
{
let mut state = system_state.get_mut(world);
let loaded = state.get_loaded().collect::<Vec<_>>();
assert!(loaded.is_empty());
assert_eq!(state.loading.len(), 1);
}
{
let mut state = system_state.get_mut(world);
let loaded = state.get_loaded().collect::<Vec<_>>();
assert!(loaded.is_empty())
}
}
make_test_plugin!(crate);
#[test]
fn test_run_override_is_undid() {
let mut app = App::default();
app.add_message::<ScriptAttachedEvent>();
app.add_plugins((AssetPlugin::default(), TestPlugin::default()));
app.init_asset::<ScriptAsset>();
let mut machines = ActiveMachines::<TestPlugin>::default();
machines.budget = None;
app.insert_resource(machines);
app.finish();
let world = app.world_mut();
RunProcessingPipelineOnce::<TestPlugin>::new(Some(Duration::from_secs(1))).apply(world);
let machines = world.get_resource::<ActiveMachines<TestPlugin>>().unwrap();
assert_eq!(machines.budget, None);
}
}