use std::{marker::PhantomData, sync::Arc};
use ::{bevy_ecs::entity::Entity, bevy_reflect::Reflect};
use bevy_asset::AssetId;
use bevy_ecs::message::Message;
use bevy_mod_scripting_asset::{Language, ScriptAsset};
use bevy_mod_scripting_bindings::ScriptValue;
use bevy_mod_scripting_script::ScriptAttachment;
use parking_lot::Mutex;
use crate::{
IntoScriptPluginParams,
error::ScriptError,
script::{ScriptContext, ScriptId},
};
#[derive(Debug, Message)]
pub struct ScriptErrorEvent {
pub error: ScriptError,
}
impl ScriptErrorEvent {
pub fn new(error: ScriptError) -> Self {
Self { error }
}
}
#[derive(Message, Clone, Debug)]
pub struct ScriptAttachedEvent(pub ScriptAttachment);
#[derive(Message, Clone, Debug)]
pub struct ScriptDetachedEvent(pub ScriptAttachment);
#[derive(Message, Clone, Debug)]
pub struct ScriptAssetModifiedEvent(pub AssetId<ScriptAsset>);
#[derive(Message)]
pub struct ForPlugin<T, P: IntoScriptPluginParams>(T, PhantomData<fn(P)>);
impl<T: std::fmt::Debug, P: IntoScriptPluginParams> std::fmt::Debug for ForPlugin<T, P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ForPlugin").field(&self.0).finish()
}
}
impl<T, P: IntoScriptPluginParams> From<T> for ForPlugin<T, P> {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T: Clone, P: IntoScriptPluginParams> Clone for ForPlugin<T, P> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1)
}
}
impl<T, P: IntoScriptPluginParams> ForPlugin<T, P> {
pub fn new(message: T) -> Self {
Self(message, Default::default())
}
pub fn event(&self) -> &T {
&self.0
}
pub fn event_mut(&mut self) -> &mut T {
&mut self.0
}
pub fn inner(self) -> T {
self.0
}
}
#[derive(Reflect, Clone, PartialEq, Eq, Hash, Debug)]
pub struct CallbackLabel(String);
impl CallbackLabel {
fn filter_invalid(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut first = true;
for char in s.chars() {
if char == '_'
|| ((!first && char.is_ascii_alphanumeric()) || char.is_ascii_alphabetic())
{
out.push(char);
first = false;
} else {
continue;
}
}
if FORBIDDEN_KEYWORDS.contains(&s) {
String::default()
} else {
out
}
}
pub fn new_lossy(label: &str) -> Self {
Self(Self::filter_invalid(label))
}
pub fn new(label: &str) -> Option<Self> {
let new_lossy = Self::new_lossy(label);
if new_lossy.0.len() != label.len() {
None
} else {
Some(new_lossy)
}
}
}
#[macro_export]
macro_rules! callback_labels {
($($(#[doc = $doc:expr])* $name:ident => $label:expr),* $(,)?) => {
$(
$(#[doc = $doc])*
#[doc = "A callback label for the event: "]
#[doc = stringify!($label)]
pub struct $name;
impl $crate::event::IntoCallbackLabel for $name {
fn into_callback_label() -> $crate::event::CallbackLabel {
$label.into()
}
}
)*
};
}
callback_labels!(
OnScriptLoaded => "on_script_loaded",
OnScriptUnloaded => "on_script_unloaded",
OnScriptReloaded => "on_script_reloaded",
);
pub trait IntoCallbackLabel {
fn into_callback_label() -> CallbackLabel;
}
impl<T: IntoCallbackLabel> From<T> for CallbackLabel {
fn from(_: T) -> Self {
T::into_callback_label()
}
}
impl From<&str> for CallbackLabel {
fn from(s: &str) -> Self {
Self::new_lossy(s)
}
}
impl From<String> for CallbackLabel {
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
impl AsRef<str> for CallbackLabel {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for CallbackLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ref())
}
}
#[derive(Clone, Debug)]
pub enum Recipients {
AllScripts,
AllContexts,
ScriptEntity(ScriptId, Entity),
StaticScript(ScriptId),
}
impl Recipients {
pub fn get_recipients<P: IntoScriptPluginParams>(
&self,
script_context: ScriptContext<P>,
) -> Vec<(ScriptAttachment, Arc<Mutex<P::C>>)> {
let script_context = script_context.read();
match self {
Recipients::AllScripts => script_context.all_residents().collect(),
Recipients::AllContexts => script_context.first_resident_from_each_context().collect(),
Recipients::ScriptEntity(script, entity) => {
let attachment = ScriptAttachment::EntityScript(*entity, script.clone());
script_context
.get_context(&attachment)
.into_iter()
.map(|entry| (attachment.clone(), entry))
.collect()
}
Recipients::StaticScript(script) => {
let attachment = ScriptAttachment::StaticScript(script.clone());
script_context
.get_context(&attachment)
.into_iter()
.map(|entry| (attachment.clone(), entry))
.collect()
}
}
}
}
#[derive(Clone, Message, Debug)]
#[non_exhaustive]
pub struct ScriptCallbackEvent {
pub label: CallbackLabel,
pub recipients: Recipients,
pub language: Option<Language>,
pub args: Vec<ScriptValue>,
pub trigger_response: bool,
}
impl ScriptCallbackEvent {
pub fn new<L: Into<CallbackLabel>>(
label: L,
args: Vec<ScriptValue>,
recipients: Recipients,
language: Option<Language>,
) -> Self {
Self {
label: label.into(),
language,
args,
recipients,
trigger_response: false,
}
}
pub fn with_response(mut self) -> Self {
self.trigger_response = true;
self
}
pub fn new_for_all_scripts<L: Into<CallbackLabel>>(label: L, args: Vec<ScriptValue>) -> Self {
Self::new(label, args, Recipients::AllScripts, None)
}
pub fn new_for_all_contexts<L: Into<CallbackLabel>>(label: L, args: Vec<ScriptValue>) -> Self {
Self::new(label, args, Recipients::AllContexts, None)
}
}
#[derive(Clone, Message, Debug)]
#[non_exhaustive]
pub struct ScriptCallbackResponseEvent {
pub label: CallbackLabel,
pub language: Language,
pub context_key: ScriptAttachment,
pub response: Result<ScriptValue, ScriptError>,
}
impl ScriptCallbackResponseEvent {
pub fn new<L: Into<CallbackLabel>>(
label: L,
context_key: ScriptAttachment,
response: Result<ScriptValue, ScriptError>,
language: Language,
) -> Self {
Self {
label: label.into(),
context_key,
response,
language,
}
}
pub fn source_entity(&self) -> Option<Entity> {
self.context_key.entity()
}
}
static FORBIDDEN_KEYWORDS: [&str; 82] = [
"and",
"break",
"do",
"else",
"elseif",
"end",
"false",
"for",
"function",
"if",
"in",
"local",
"nil",
"not",
"or",
"repeat",
"return",
"then",
"true",
"until",
"while",
"true",
"false",
"let",
"const",
"is_shared",
"if",
"else",
"switch",
"do",
"while",
"loop",
"until",
"for",
"in",
"continue",
"break",
"fn",
"private",
"is_def_fn",
"this",
"return",
"throw",
"try",
"catch",
"import",
"export",
"as",
"global",
"Fn",
"call",
"curry",
"type_of",
"print",
"debug",
"eval",
"is_def_var",
"var",
"static",
"is",
"goto",
"match",
"case",
"public",
"protected",
"new",
"use",
"with",
"module",
"package",
"super",
"spawn",
"thread",
"go",
"sync",
"async",
"await",
"yield",
"default",
"void",
"null",
"nil",
];
#[cfg(test)]
mod test {
use super::*;
use std::sync::Arc;
use ::{
bevy_app::{App, Plugin},
bevy_asset::{AssetId, Handle},
bevy_ecs::entity::Entity,
};
use parking_lot::Mutex;
use test_utils::make_test_plugin;
use uuid::{Uuid, uuid};
use super::FORBIDDEN_KEYWORDS;
use crate::{
config::{GetPluginThreadConfig, ScriptingPluginConfiguration},
event::Recipients,
script::{ContextPolicy, ScriptContext},
};
#[test]
fn test_invalid_strings() {
FORBIDDEN_KEYWORDS.iter().for_each(|keyword| {
assert_eq!(super::CallbackLabel::new(keyword), None);
});
}
#[test]
fn test_bad_chars() {
let bad_chars = [
'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+', '=', '{', '}', '[', ']',
'|', '\\', ':', ';', '"', '\'', '<', '>', ',', '.', '?', '/', '`', '~',
];
bad_chars.iter().for_each(|char| {
assert_eq!(super::CallbackLabel::new(&format!("bad{char}")), None);
});
}
#[test]
fn bad_first_letter() {
let bad_chars = [
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '@', '#', '$', '%', '^', '&', '*',
'(', ')', '-', '+', '=', '{', '}', '[', ']', '|', '\\', ':', ';', '"', '\'', '<', '>',
',', '.', '?', '/', '`', '~',
];
bad_chars.iter().for_each(|char| {
assert_eq!(super::CallbackLabel::new(&format!("{char}bad")), None);
});
}
#[test]
fn test_valid_idents() {
let valid = ["h", "_v", "hello", "_2d", "heloo_2", "_1231412"];
valid.iter().for_each(|ident| {
assert!(super::CallbackLabel::new(ident).is_some());
assert_eq!(super::CallbackLabel::new_lossy(ident).as_ref(), *ident);
});
}
make_test_plugin!(crate);
fn make_test_contexts() -> ScriptContext<TestPlugin> {
let policy = ContextPolicy::per_entity();
let script_context = ScriptContext::<TestPlugin>::new(policy);
let mut script_context_guard = script_context.write();
let context_a = Arc::new(Mutex::new(TestContext {
invocations: vec![ScriptValue::String("a".to_string().into())],
}));
let context_b = Arc::new(Mutex::new(TestContext {
invocations: vec![ScriptValue::String("b".to_string().into())],
}));
let context_c = Arc::new(Mutex::new(TestContext {
invocations: vec![ScriptValue::String("c".to_string().into())],
}));
let context_d = Arc::new(Mutex::new(TestContext {
invocations: vec![ScriptValue::String("d".to_string().into())],
}));
let entity_script_a = Handle::Uuid(
uuid!("163f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
);
let entity_script_b = Handle::Uuid(
uuid!("263f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
);
let entity_script_c = Handle::Uuid(
uuid!("363f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
);
let entity_script_d = Handle::Uuid(
uuid!("463f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
);
let static_script_a = Handle::Uuid(
uuid!("563f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
);
let static_script_b = Handle::Uuid(
uuid!("663f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
);
script_context_guard
.insert(
ScriptAttachment::EntityScript(Entity::from_raw_u32(0).unwrap(), entity_script_a),
context_a,
)
.unwrap();
script_context_guard
.insert_resident(ScriptAttachment::EntityScript(
Entity::from_raw_u32(0).unwrap(),
entity_script_b,
))
.unwrap();
script_context_guard
.insert(
ScriptAttachment::EntityScript(Entity::from_raw_u32(1).unwrap(), entity_script_c),
context_b,
)
.unwrap();
script_context_guard
.insert_resident(ScriptAttachment::EntityScript(
Entity::from_raw_u32(1).unwrap(),
entity_script_d,
))
.unwrap();
script_context_guard
.insert(ScriptAttachment::StaticScript(static_script_a), context_c)
.unwrap();
script_context_guard
.insert(ScriptAttachment::StaticScript(static_script_b), context_d)
.unwrap();
drop(script_context_guard);
script_context
}
fn recipients_to_asset_ids(
recipients: &[(ScriptAttachment, Arc<Mutex<TestContext>>)],
) -> Vec<(Uuid, String)> {
recipients
.iter()
.map(|(attachment, context)| {
if let AssetId::Uuid { uuid } = attachment.script().id() {
let locked = context.lock();
let first_invocation_string =
if let Some(ScriptValue::String(s)) = locked.invocations.first() {
s.clone()
} else {
panic!("Expected first invocation to be a string")
};
(uuid, first_invocation_string.to_string())
} else {
panic!(
"Expected AssetId::Index, got {:?}",
attachment.script().id()
)
}
})
.collect()
}
#[test]
fn test_all_scripts_recipients() {
let script_context = make_test_contexts();
let recipients = Recipients::AllScripts.get_recipients(script_context);
assert_eq!(recipients.len(), 6);
let mut id_context_pairs = recipients_to_asset_ids(&recipients);
id_context_pairs.sort_by_key(|(id, _)| *id);
assert_eq!(
id_context_pairs,
vec![
(
uuid!("163f1128-62f9-456f-9b76-a326fbe86fa8"),
"a".to_string()
),
(
uuid!("263f1128-62f9-456f-9b76-a326fbe86fa8"),
"a".to_string()
),
(
uuid!("363f1128-62f9-456f-9b76-a326fbe86fa8"),
"b".to_string()
),
(
uuid!("463f1128-62f9-456f-9b76-a326fbe86fa8"),
"b".to_string()
),
(
uuid!("563f1128-62f9-456f-9b76-a326fbe86fa8"),
"c".to_string()
),
(
uuid!("663f1128-62f9-456f-9b76-a326fbe86fa8"),
"d".to_string()
),
]
);
}
#[test]
fn test_all_contexts_recipients() {
let script_context = make_test_contexts();
let recipients = Recipients::AllContexts.get_recipients(script_context);
assert_eq!(recipients.len(), 4);
let mut id_context_pairs = recipients_to_asset_ids(&recipients);
id_context_pairs.sort_by_key(|(id, _)| *id);
assert!(
id_context_pairs.contains(&(
uuid!("163f1128-62f9-456f-9b76-a326fbe86fa8"),
"a".to_string()
)) || id_context_pairs.contains(&(
uuid!("263f1128-62f9-456f-9b76-a326fbe86fa8"),
"a".to_string()
))
);
assert!(
id_context_pairs.contains(&(
uuid!("363f1128-62f9-456f-9b76-a326fbe86fa8"),
"b".to_string()
)) || id_context_pairs.contains(&(
uuid!("463f1128-62f9-456f-9b76-a326fbe86fa8"),
"b".to_string()
))
);
assert!(id_context_pairs.contains(&(
uuid!("563f1128-62f9-456f-9b76-a326fbe86fa8"),
"c".to_string()
)));
assert!(id_context_pairs.contains(&(
uuid!("663f1128-62f9-456f-9b76-a326fbe86fa8"),
"d".to_string()
)));
}
#[test]
fn test_script_entity_recipients() {
let script_context = make_test_contexts();
let recipients = Recipients::ScriptEntity(
Handle::Uuid(
uuid!("163f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
),
Entity::from_raw_u32(0).unwrap(),
)
.get_recipients(script_context);
assert_eq!(recipients.len(), 1);
let id_context_pairs = recipients_to_asset_ids(&recipients);
assert_eq!(
id_context_pairs,
vec![(
uuid!("163f1128-62f9-456f-9b76-a326fbe86fa8"),
"a".to_string()
)]
);
}
#[test]
fn test_static_script_recipients() {
let script_context = make_test_contexts();
let recipients = Recipients::StaticScript(Handle::Uuid(
uuid!("563f1128-62f9-456f-9b76-a326fbe86fa8"),
Default::default(),
))
.get_recipients(script_context);
assert_eq!(recipients.len(), 1);
let id_context_pairs = recipients_to_asset_ids(&recipients);
assert_eq!(
id_context_pairs,
vec![(
uuid!("563f1128-62f9-456f-9b76-a326fbe86fa8"),
"c".to_string()
)]
);
}
}