From 96b2a3dbf06cabc3924684bbd437440b3708bb89 Mon Sep 17 00:00:00 2001 From: makspll Date: Mon, 31 Mar 2025 19:28:07 +0100 Subject: [PATCH] feat: add option to emit callback response event & `RunScriptCallback` command --- .../bevy_mod_scripting_core/src/commands.rs | 395 ++++++++++++------ crates/bevy_mod_scripting_core/src/event.rs | 39 ++ crates/bevy_mod_scripting_core/src/handler.rs | 235 +++++++---- crates/bevy_mod_scripting_core/src/lib.rs | 3 +- 4 files changed, 472 insertions(+), 200 deletions(-) diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index 02c49bb3a8..57b37195a9 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -2,13 +2,16 @@ use crate::{ asset::ScriptAsset, - bindings::WorldGuard, + bindings::{ScriptValue, WorldGuard}, context::ContextBuilder, error::{InteropError, ScriptError}, - event::{IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded}, + event::{ + CallbackLabel, IntoCallbackLabel, OnScriptLoaded, OnScriptUnloaded, + ScriptCallbackResponseEvent, + }, extractors::{with_handler_system_state, HandlerContext}, - handler::{handle_script_errors, CallbackSettings}, - script::{Script, ScriptId, StaticScripts}, + handler::{handle_script_errors, send_callback_response}, + script::{Script, ScriptId, Scripts, StaticScripts}, IntoScriptPluginParams, }; use bevy::{asset::Handle, ecs::entity::Entity, log::debug, prelude::Command}; @@ -35,51 +38,25 @@ impl DeleteScript

{ impl Command for DeleteScript

{ fn apply(self, world: &mut bevy::prelude::World) { - with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { - if let Some(script) = handler_ctxt.scripts.scripts.remove(&self.id) { - debug!("Deleting script with id: {}", self.id); - - // first let the script uninstall itself - let mut context = script.context.lock(); - - match (CallbackSettings::

::call)( - handler_ctxt.callback_settings.callback_handler, - vec![], - bevy::ecs::entity::Entity::from_raw(0), - &self.id, - &OnScriptUnloaded::into_callback_label(), - &mut context, - &handler_ctxt - .context_loading_settings - .context_pre_handling_initializers, - &handler_ctxt.runtime_container.runtime, - guard.clone(), - ) { - Ok(_) => {} - Err(e) => { - handle_script_errors( - guard, - [e.with_context(format!( - "Running unload hook for script with id: {}. Language: {}", - self.id, - P::LANGUAGE - ))] - .into_iter(), - ); - } - } - - debug!("Removing script with id: {}", self.id); - // since we removed the script and are dropping the context, - // it's going to get de-allocated if it's the last context irrespective if we're - // using a global or individual allocation strategy - } else { - bevy::log::error!( - "Attempted to delete script with id: {} but it does not exist, doing nothing!", - self.id - ); - } - }) + // first apply unload callback + RunScriptCallback::

::new( + self.id.clone(), + Entity::from_raw(0), + OnScriptUnloaded::into_callback_label(), + vec![], + false, + ) + .apply(world); + + let mut scripts = world.get_resource_or_init::>(); + if scripts.remove(self.id.clone()) { + debug!("Deleted script with id: {}", self.id); + } else { + bevy::log::error!( + "Attempted to delete script with id: {} but it does not exist, doing nothing!", + self.id + ); + } } } @@ -173,28 +150,30 @@ impl CreateOrUpdateScript

{ #[profiling::all_functions] impl Command for CreateOrUpdateScript

{ fn apply(self, world: &mut bevy::prelude::World) { - with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { - let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id); - - let assigned_shared_context = - match handler_ctxt.context_loading_settings.assignment_strategy { - crate::context::ContextAssignmentStrategy::Individual => None, - crate::context::ContextAssignmentStrategy::Global => { - let is_new_context = handler_ctxt.scripts.scripts.is_empty(); - if !is_new_context { - handler_ctxt - .scripts - .scripts - .values() - .next() - .map(|s| s.context.clone()) - } else { - None + let success = with_handler_system_state( + world, + |guard, handler_ctxt: &mut HandlerContext

| { + let is_new_script = !handler_ctxt.scripts.scripts.contains_key(&self.id); + + let assigned_shared_context = + match handler_ctxt.context_loading_settings.assignment_strategy { + crate::context::ContextAssignmentStrategy::Individual => None, + crate::context::ContextAssignmentStrategy::Global => { + let is_new_context = handler_ctxt.scripts.scripts.is_empty(); + if !is_new_context { + handler_ctxt + .scripts + .scripts + .values() + .next() + .map(|s| s.context.clone()) + } else { + None + } } - } - }; + }; - debug!( + debug!( "{}: CreateOrUpdateScript command applying (script_id: {}, new context?: {}, new script?: {})", P::LANGUAGE, self.id, @@ -202,61 +181,154 @@ impl Command for CreateOrUpdateScript

{ is_new_script ); - let result = match &assigned_shared_context { - Some(assigned_shared_context) => { - if is_new_script { - // this will happen when sharing contexts - // make a new script with the shared context - let script = Script { - id: self.id.clone(), - asset: self.asset.clone(), - context: assigned_shared_context.clone(), - }; - // it can potentially be loaded but without a successful script reload but that - // leaves us in an okay state - handler_ctxt.scripts.scripts.insert(self.id.clone(), script); + let result = match &assigned_shared_context { + Some(assigned_shared_context) => { + if is_new_script { + // this will happen when sharing contexts + // make a new script with the shared context + let script = Script { + id: self.id.clone(), + asset: self.asset.clone(), + context: assigned_shared_context.clone(), + }; + // it can potentially be loaded but without a successful script reload but that + // leaves us in an okay state + handler_ctxt.scripts.scripts.insert(self.id.clone(), script); + } + bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); + self.reload_context(guard.clone(), handler_ctxt) } - bevy::log::debug!("{}: reloading script with id: {}", P::LANGUAGE, self.id); - self.reload_context(guard.clone(), handler_ctxt) - } - None => { - bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); - self.load_context(guard.clone(), handler_ctxt) - } - }; + None => { + bevy::log::debug!("{}: loading script with id: {}", P::LANGUAGE, self.id); + self.load_context(guard.clone(), handler_ctxt) + } + }; - let result = result.and_then(|()| { - handler_ctxt.call::( - &self.id, - Entity::from_raw(0), - vec![], - guard.clone(), - )?; - Ok(()) - }); - - match result { - Ok(_) => { - bevy::log::debug!( - "{}: script with id: {} successfully created or updated", - P::LANGUAGE, - self.id - ); - } - Err(e) => { - let phrase = if assigned_shared_context.is_some() { - "reloading" - } else { - "loading" - }; + let phrase = if assigned_shared_context.is_some() { + "reloading" + } else { + "loading" + }; + + if let Err(err) = result { handle_script_errors( guard, - vec![e - .with_script(self.id) - .with_context(format!("{}: {phrase} script", P::LANGUAGE))] + vec![err + .with_script(self.id.clone()) + .with_context(P::LANGUAGE) + .with_context(phrase)] .into_iter(), ); + return false; } + + bevy::log::debug!( + "{}: script with id: {} successfully created or updated", + P::LANGUAGE, + self.id + ); + + true + }, + ); + + // immediately run command for callback, but only if loading went fine + if success { + RunScriptCallback::

::new( + self.id, + Entity::from_raw(0), + OnScriptLoaded::into_callback_label(), + vec![], + false, + ) + .apply(world) + } + } +} + +/// Runs a callback on the script with the given ID if it exists +pub struct RunScriptCallback { + /// The ID of the script to run the callback on + pub id: ScriptId, + /// The entity to use for the callback + pub entity: Entity, + /// The callback to run + pub callback: CallbackLabel, + /// optional context passed down to errors + pub context: Option<&'static str>, + /// The arguments to pass to the callback + pub args: Vec, + /// Whether the callback should emit a response event + pub trigger_response: bool, + /// Hack to make this Send, C does not need to be Send since it is not stored in the command + pub _ph: std::marker::PhantomData, +} + +impl RunScriptCallback

{ + /// Creates a new RunCallbackCommand with the given ID, callback and arguments + pub fn new( + id: ScriptId, + entity: Entity, + callback: CallbackLabel, + args: Vec, + trigger_response: bool, + ) -> Self { + Self { + id, + entity, + context: None, + callback, + args, + trigger_response, + _ph: std::marker::PhantomData, + } + } + + /// Sets the context for the command, makes produced errors more useful. + pub fn with_context(mut self, context: &'static str) -> Self { + self.context = Some(context); + self + } +} + +impl Command for RunScriptCallback

{ + fn apply(self, world: &mut bevy::prelude::World) { + with_handler_system_state(world, |guard, handler_ctxt: &mut HandlerContext

| { + if !handler_ctxt.is_script_fully_loaded(self.id.clone()) { + bevy::log::error!( + "{}: Cannot apply callback command, as script does not exist: {}. Ignoring.", + P::LANGUAGE, + self.id + ); + return; + } + + let result = handler_ctxt.call_dynamic_label( + &self.callback, + &self.id, + self.entity, + self.args, + guard.clone(), + ); + + if self.trigger_response { + send_callback_response( + guard.clone(), + ScriptCallbackResponseEvent::new( + self.callback, + self.id.clone(), + result.clone(), + ), + ); + } + + if let Err(err) = result { + let mut error_with_context = err.with_script(self.id).with_context(P::LANGUAGE); + if let Some(ctxt) = self.context { + error_with_context = error_with_context.with_context(ctxt); + } + + handle_script_errors(guard, vec![error_with_context].into_iter()); } }) } @@ -307,6 +379,7 @@ impl Command for RemoveStaticScript { mod test { use bevy::{ app::App, + ecs::event::Events, log::{Level, LogPlugin}, prelude::{Entity, World}, }; @@ -325,6 +398,8 @@ mod test { fn setup_app() -> App { // setup all the resources necessary let mut app = App::new(); + + app.add_event::(); app.add_plugins(LogPlugin { filter: "bevy_mod_scripting_core=debug,info".to_owned(), level: Level::TRACE, @@ -407,18 +482,40 @@ mod test { assert_eq!(*context, *found_context, "{}", message); } + fn assert_response_events( + app: &mut World, + expected: impl Iterator, + context: &'static str, + ) { + let mut events = app + .get_resource_mut::>() + .unwrap(); + let responses = events.drain().collect::>(); + let expected: Vec<_> = expected.collect(); + assert_eq!( + responses.len(), + expected.len(), + "Incorrect amount of events received {}", + context + ); + for (a, b) in responses.iter().zip(expected.iter()) { + assert_eq!(a.label, b.label, "{}", context); + assert_eq!(a.script, b.script, "{}", context); + assert_eq!(a.response, b.response, "{}", context); + } + } + #[test] fn test_commands_with_default_assigner() { let mut app = setup_app(); - let world = app.world_mut(); let content = "content".as_bytes().to_vec().into_boxed_slice(); let command = CreateOrUpdateScript::::new("script".into(), content, None); - command.apply(world); + command.apply(app.world_mut()); // check script assert_context_and_script( - world, + app.world_mut(), "script", "content initialized pre-handling-initialized callback-ran-on_script_loaded", "Initial script creation failed", @@ -427,11 +524,11 @@ mod test { // update the script let content = "new content".as_bytes().to_vec().into_boxed_slice(); let command = CreateOrUpdateScript::::new("script".into(), content, None); - command.apply(world); + command.apply(app.world_mut()); // check script assert_context_and_script( - world, + app.world_mut(), "script", "new content initialized pre-handling-initialized callback-ran-on_script_loaded", "Script update failed", @@ -441,26 +538,63 @@ mod test { let content = "content2".as_bytes().to_vec().into_boxed_slice(); let command = CreateOrUpdateScript::::new("script2".into(), content, None); - command.apply(world); + command.apply(app.world_mut()); // check second script - assert_context_and_script( - world, + app.world_mut(), "script2", "content2 initialized pre-handling-initialized callback-ran-on_script_loaded", "Second script creation failed", ); + // run a specific callback on the first script + RunScriptCallback::::new( + "script".into(), + Entity::from_raw(0), + OnScriptLoaded::into_callback_label(), + vec![], + true, + ) + .apply(app.world_mut()); + + // check this has applied + assert_context_and_script( + app.world_mut(), + "script", + "new content initialized pre-handling-initialized callback-ran-on_script_loaded callback-ran-on_script_loaded", + "Script callback failed", + ); + // assert events sent + assert_response_events( + app.world_mut(), + vec![ScriptCallbackResponseEvent::new( + OnScriptLoaded::into_callback_label(), + "script".into(), + Ok(ScriptValue::Unit), + )] + .into_iter(), + "script callback failed", + ); + // delete both scripts let command = DeleteScript::::new("script".into()); - command.apply(world); + command.apply(app.world_mut()); let command = DeleteScript::::new("script2".into()); - command.apply(world); + command.apply(app.world_mut()); // check that the scripts are gone - let scripts = world.get_resource::>().unwrap(); + let scripts = app + .world_mut() + .get_resource::>() + .unwrap(); assert!(scripts.scripts.is_empty()); + + assert_response_events( + app.world_mut(), + vec![].into_iter(), + "did not expect response events", + ); } #[test] @@ -557,6 +691,11 @@ mod test { let scripts = app.world().get_resource::>().unwrap(); assert_eq!(scripts.scripts.len(), 0, "scripts weren't removed"); + assert_response_events( + app.world_mut(), + vec![].into_iter(), + "did not expect any response events", + ); } #[test] diff --git a/crates/bevy_mod_scripting_core/src/event.rs b/crates/bevy_mod_scripting_core/src/event.rs index ceed3e8a92..7d9c5c3da7 100644 --- a/crates/bevy_mod_scripting_core/src/event.rs +++ b/crates/bevy_mod_scripting_core/src/event.rs @@ -128,6 +128,7 @@ pub enum Recipients { /// A callback event meant to trigger a callback in a subset/set of scripts in the world with the given arguments #[derive(Clone, Event, Debug)] +#[non_exhaustive] pub struct ScriptCallbackEvent { /// The label of the callback pub label: CallbackLabel, @@ -135,6 +136,8 @@ pub struct ScriptCallbackEvent { pub recipients: Recipients, /// The arguments to the callback pub args: Vec, + /// Whether the callback should emit a response event + pub trigger_response: bool, } impl ScriptCallbackEvent { @@ -148,15 +151,51 @@ impl ScriptCallbackEvent { label: label.into(), args, recipients, + trigger_response: false, } } + /// Marks this event as expecting a response. + /// + /// When set, an `ScriptCallbackResponse` event will be emitted when the callback is completed with the result of the callback IF the callback was executed. + pub fn with_response(mut self) -> Self { + self.trigger_response = true; + self + } + /// Creates a new callback event with the given label, arguments and all scripts as recipients pub fn new_for_all>(label: L, args: Vec) -> Self { Self::new(label, args, Recipients::All) } } +/// Event published when a script completes a callback, and a response is requested +#[derive(Clone, Event, Debug)] +#[non_exhaustive] +pub struct ScriptCallbackResponseEvent { + /// the label of the callback + pub label: CallbackLabel, + /// the script that replied + pub script: ScriptId, + /// the response received + pub response: Result, +} + +impl ScriptCallbackResponseEvent { + /// Creates a new callback response event with the given label, script and response + pub fn new>( + label: L, + script: ScriptId, + response: Result, + ) -> Self { + Self { + label: label.into(), + script, + response, + } + } +} + static FORBIDDEN_KEYWORDS: [&str; 82] = [ // Lua "and", diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 9da9d995d3..49c0ab5b85 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -6,7 +6,10 @@ use crate::{ }, context::ContextPreHandlingInitializer, error::{InteropErrorInner, ScriptError}, - event::{CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptErrorEvent}, + event::{ + CallbackLabel, IntoCallbackLabel, ScriptCallbackEvent, ScriptCallbackResponseEvent, + ScriptErrorEvent, + }, extractors::{HandlerContext, WithWorldGuard}, script::{ScriptComponent, ScriptId}, IntoScriptPluginParams, @@ -200,6 +203,17 @@ pub(crate) fn event_handler_inner( guard.clone(), ); + if event.trigger_response { + send_callback_response( + guard.clone(), + ScriptCallbackResponseEvent::new( + callback_label.clone(), + script_id.clone(), + call_result.clone(), + ), + ); + } + match call_result { Ok(_) => {} Err(e) => { @@ -233,6 +247,20 @@ pub(crate) fn event_handler_inner( handle_script_errors(guard, errors.into_iter()); } +/// Sends a callback response event to the world +pub fn send_callback_response(world: WorldGuard, response: ScriptCallbackResponseEvent) { + let err = world.with_resource_mut(|mut events: Mut>| { + events.send(response); + }); + + if let Err(err) = err { + bevy::log::error!( + "Failed to send script callback response: {}", + err.display_with_world(world.clone()) + ); + } +} + /// Handles errors caused by script execution and sends them to the error event channel pub fn handle_script_errors + Clone>(world: WorldGuard, errors: I) { let err = world.with_resource_mut(|mut error_events: Mut>| { @@ -286,6 +314,27 @@ mod test { make_test_plugin!(crate); + fn assert_response_events( + app: &mut World, + expected: impl Iterator, + ) { + let mut events = app + .get_resource_mut::>() + .unwrap(); + let responses = events.drain().collect::>(); + let expected: Vec<_> = expected.collect(); + assert_eq!( + responses.len(), + expected.len(), + "Incorrect amount of events received" + ); + for (a, b) in responses.iter().zip(expected.iter()) { + assert_eq!(a.label, b.label); + assert_eq!(a.script, b.script); + assert_eq!(a.response, b.response); + } + } + fn setup_app( runtime: TestRuntime, scripts: HashMap>, @@ -293,6 +342,7 @@ mod test { let mut app = App::new(); app.add_event::(); + app.add_event::(); app.add_event::(); app.insert_resource::>(CallbackSettings { callback_handler: |args, entity, script, _, ctxt, _, runtime| { @@ -320,6 +370,43 @@ mod test { app } + #[test] + fn test_handler_emits_response_events() { + let test_script_id = Cow::Borrowed("test_script"); + let test_script = Script { + id: test_script_id.clone(), + asset: None, + context: Arc::new(Mutex::new(TestContext::default())), + }; + let scripts = HashMap::from_iter(vec![(test_script_id.clone(), test_script.clone())]); + let runtime = TestRuntime { + invocations: vec![].into(), + }; + let mut app = setup_app::(runtime, scripts); + app.world_mut() + .spawn(ScriptComponent(vec![test_script_id.clone()])); + + app.world_mut().send_event( + ScriptCallbackEvent::new( + OnTestCallback::into_callback_label(), + vec![ScriptValue::String("test_args".into())], + crate::event::Recipients::All, + ) + .with_response(), + ); + app.update(); + + assert_response_events( + app.world_mut(), + vec![ScriptCallbackResponseEvent::new( + OnTestCallback::into_callback_label(), + test_script_id.clone(), + Ok(ScriptValue::Unit), + )] + .into_iter(), + ); + } + #[test] fn test_handler_called_with_right_args() { let test_script_id = Cow::Borrowed("test_script"); @@ -343,35 +430,37 @@ mod test { vec![ScriptValue::String("test_args".into())], )); app.update(); + { + let test_script = app + .world() + .get_resource::>() + .unwrap() + .scripts + .get(&test_script_id) + .unwrap(); + + let test_context = test_script.context.lock(); + + let test_runtime = app + .world() + .get_resource::>() + .unwrap(); + + assert_eq!( + test_context.invocations, + vec![ScriptValue::String("test_args".into())] + ); - let test_script = app - .world() - .get_resource::>() - .unwrap() - .scripts - .get(&test_script_id) - .unwrap(); - - let test_context = test_script.context.lock(); - - let test_runtime = app - .world() - .get_resource::>() - .unwrap(); - - assert_eq!( - test_context.invocations, - vec![ScriptValue::String("test_args".into())] - ); - - let runtime_invocations = test_runtime.runtime.invocations.lock(); - assert_eq!( - runtime_invocations - .iter() - .map(|(e, s)| (*e, s.clone())) - .collect::>(), - vec![(test_entity_id, test_script_id.clone())] - ); + let runtime_invocations = test_runtime.runtime.invocations.lock(); + assert_eq!( + runtime_invocations + .iter() + .map(|(e, s)| (*e, s.clone())) + .collect::>(), + vec![(test_entity_id, test_script_id.clone())] + ); + } + assert_response_events(app.world_mut(), vec![].into_iter()); } #[test] @@ -416,33 +505,35 @@ mod test { )); app.update(); + { + let test_scripts = app.world().get_resource::>().unwrap(); + let test_runtime = app + .world() + .get_resource::>() + .unwrap(); + let test_runtime = test_runtime.runtime.invocations.lock(); + let script_after = test_scripts.scripts.get(&test_script_id).unwrap(); + let context_after = script_after.context.lock(); + assert_eq!( + context_after.invocations, + vec![ + ScriptValue::String("test_args_script".into()), + ScriptValue::String("test_args_entity".into()) + ] + ); - let test_scripts = app.world().get_resource::>().unwrap(); - let test_runtime = app - .world() - .get_resource::>() - .unwrap(); - let test_runtime = test_runtime.runtime.invocations.lock(); - let script_after = test_scripts.scripts.get(&test_script_id).unwrap(); - let context_after = script_after.context.lock(); - assert_eq!( - context_after.invocations, - vec![ - ScriptValue::String("test_args_script".into()), - ScriptValue::String("test_args_entity".into()) - ] - ); - - assert_eq!( - test_runtime - .iter() - .map(|(e, s)| (*e, s.clone())) - .collect::>(), - vec![ - (test_entity_id, test_script_id.clone()), - (test_entity_id, test_script_id.clone()) - ] - ); + assert_eq!( + test_runtime + .iter() + .map(|(e, s)| (*e, s.clone())) + .collect::>(), + vec![ + (test_entity_id, test_script_id.clone()), + (test_entity_id, test_script_id.clone()) + ] + ); + } + assert_response_events(app.world_mut(), vec![].into_iter()); } #[test] @@ -479,22 +570,24 @@ mod test { )); app.update(); - - let test_scripts = app.world().get_resource::>().unwrap(); - let test_context = test_scripts - .scripts - .get(&test_script_id) - .unwrap() - .context - .lock(); - - assert_eq!( - test_context.invocations, - vec![ - ScriptValue::String("test_args_script".into()), - ScriptValue::String("test_script_id".into()) - ] - ); + { + let test_scripts = app.world().get_resource::>().unwrap(); + let test_context = test_scripts + .scripts + .get(&test_script_id) + .unwrap() + .context + .lock(); + + assert_eq!( + test_context.invocations, + vec![ + ScriptValue::String("test_args_script".into()), + ScriptValue::String("test_script_id".into()) + ] + ); + } + assert_response_events(app.world_mut(), vec![].into_iter()); } #[test] diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index dd16b844f1..634c5218d9 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -23,7 +23,7 @@ use context::{ ContextPreHandlingInitializer, }; use error::ScriptError; -use event::ScriptCallbackEvent; +use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent}; use handler::{CallbackSettings, HandlerFn}; use runtime::{initialize_runtime, Runtime, RuntimeContainer, RuntimeInitializer, RuntimeSettings}; use script::{ScriptComponent, ScriptId, Scripts, StaticScripts}; @@ -302,6 +302,7 @@ fn once_per_app_init(app: &mut App) { app.add_event::() .add_event::() + .add_event::() .init_resource::() .init_resource::() .init_asset::()