bevy_mod_scripting_core/
context.rs

1//! Traits and types for managing script contexts.
2
3use crate::{
4    bindings::{ThreadWorldContainer, WorldContainer, WorldGuard},
5    error::{InteropError, ScriptError},
6    script::{Script, ScriptId},
7    IntoScriptPluginParams,
8};
9use bevy::ecs::{entity::Entity, system::Resource};
10use std::{collections::HashMap, sync::atomic::AtomicU32};
11
12/// A trait that all script contexts must implement.
13pub trait Context: 'static + Send + Sync {}
14impl<T: 'static + Send + Sync> Context for T {}
15
16/// The type of a context id
17pub type ContextId = u32;
18
19/// Stores script state for a scripting plugin. Scripts are identified by their `ScriptId`, while contexts are identified by their `ContextId`.
20#[derive(Resource)]
21pub struct ScriptContexts<P: IntoScriptPluginParams> {
22    /// The contexts of the scripts
23    pub contexts: HashMap<ContextId, P::C>,
24}
25
26impl<P: IntoScriptPluginParams> Default for ScriptContexts<P> {
27    fn default() -> Self {
28        Self {
29            contexts: Default::default(),
30        }
31    }
32}
33
34static CONTEXT_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
35impl<P: IntoScriptPluginParams> ScriptContexts<P> {
36    /// Allocates a new ContextId and inserts the context into the map
37    pub fn insert(&mut self, ctxt: P::C) -> ContextId {
38        let id = CONTEXT_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
39        self.contexts.insert(id, ctxt);
40        id
41    }
42
43    /// Inserts a context with a specific id
44    pub fn insert_with_id(&mut self, id: ContextId, ctxt: P::C) -> Option<P::C> {
45        self.contexts.insert(id, ctxt)
46    }
47
48    /// Allocate new context id without inserting a context
49    pub fn allocate_id(&self) -> ContextId {
50        CONTEXT_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
51    }
52
53    /// Removes a context from the map
54    pub fn remove(&mut self, id: ContextId) -> Option<P::C> {
55        self.contexts.remove(&id)
56    }
57
58    /// Get a reference to a context
59    pub fn get(&self, id: ContextId) -> Option<&P::C> {
60        self.contexts.get(&id)
61    }
62
63    /// Get a mutable reference to a context
64    pub fn get_mut(&mut self, id: ContextId) -> Option<&mut P::C> {
65        self.contexts.get_mut(&id)
66    }
67
68    /// Check if a context exists
69    pub fn contains(&self, id: ContextId) -> bool {
70        self.contexts.contains_key(&id)
71    }
72}
73
74/// Initializer run once after creating a context but before executing it for the first time as well as after re-loading the script
75pub type ContextInitializer<P> =
76    fn(&str, &mut <P as IntoScriptPluginParams>::C) -> Result<(), ScriptError>;
77
78/// Initializer run every time before executing or loading/re-loading a script
79pub type ContextPreHandlingInitializer<P> =
80    fn(&str, Entity, &mut <P as IntoScriptPluginParams>::C) -> Result<(), ScriptError>;
81
82/// Settings concerning the creation and assignment of script contexts as well as their initialization.
83#[derive(Resource)]
84pub struct ContextLoadingSettings<P: IntoScriptPluginParams> {
85    /// Defines the strategy used to load and reload contexts
86    pub loader: ContextBuilder<P>,
87    /// Defines the strategy used to assign contexts to scripts
88    pub assigner: ContextAssigner<P>,
89    /// Initializers run once after creating a context but before executing it for the first time
90    pub context_initializers: Vec<ContextInitializer<P>>,
91    /// Initializers run every time before executing or loading a script
92    pub context_pre_handling_initializers: Vec<ContextPreHandlingInitializer<P>>,
93}
94
95impl<P: IntoScriptPluginParams> Default for ContextLoadingSettings<P> {
96    fn default() -> Self {
97        Self {
98            loader: ContextBuilder::default(),
99            assigner: ContextAssigner::default(),
100            context_initializers: Default::default(),
101            context_pre_handling_initializers: Default::default(),
102        }
103    }
104}
105
106impl<T: IntoScriptPluginParams> Clone for ContextLoadingSettings<T> {
107    fn clone(&self) -> Self {
108        Self {
109            loader: self.loader.clone(),
110            assigner: self.assigner.clone(),
111            context_initializers: self.context_initializers.clone(),
112            context_pre_handling_initializers: self.context_pre_handling_initializers.clone(),
113        }
114    }
115}
116/// A strategy for loading contexts
117pub type ContextLoadFn<P> = fn(
118    script_id: &ScriptId,
119    content: &[u8],
120    context_initializers: &[ContextInitializer<P>],
121    pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
122    runtime: &mut <P as IntoScriptPluginParams>::R,
123) -> Result<<P as IntoScriptPluginParams>::C, ScriptError>;
124
125/// A strategy for reloading contexts
126pub type ContextReloadFn<P> = fn(
127    script_id: &ScriptId,
128    content: &[u8],
129    previous_context: &mut <P as IntoScriptPluginParams>::C,
130    context_initializers: &[ContextInitializer<P>],
131    pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
132    runtime: &mut <P as IntoScriptPluginParams>::R,
133) -> Result<(), ScriptError>;
134
135/// A strategy for loading and reloading contexts
136pub struct ContextBuilder<P: IntoScriptPluginParams> {
137    /// The function to load a context
138    pub load: ContextLoadFn<P>,
139    /// The function to reload a context
140    pub reload: ContextReloadFn<P>,
141}
142
143impl<P: IntoScriptPluginParams> Default for ContextBuilder<P> {
144    fn default() -> Self {
145        Self {
146            load: |_, _, _, _, _| Err(InteropError::invariant("no context loader set").into()),
147            reload: |_, _, _, _, _, _| {
148                Err(InteropError::invariant("no context reloader set").into())
149            },
150        }
151    }
152}
153
154impl<P: IntoScriptPluginParams> ContextBuilder<P> {
155    /// load a context
156    pub fn load(
157        loader: ContextLoadFn<P>,
158        script: &ScriptId,
159        content: &[u8],
160        context_initializers: &[ContextInitializer<P>],
161        pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
162        world: WorldGuard,
163        runtime: &mut P::R,
164    ) -> Result<P::C, ScriptError> {
165        WorldGuard::with_existing_static_guard(world.clone(), |world| {
166            ThreadWorldContainer.set_world(world)?;
167            (loader)(
168                script,
169                content,
170                context_initializers,
171                pre_handling_initializers,
172                runtime,
173            )
174        })
175    }
176
177    /// reload a context
178    pub fn reload(
179        reloader: ContextReloadFn<P>,
180        script: &ScriptId,
181        content: &[u8],
182        previous_context: &mut P::C,
183        context_initializers: &[ContextInitializer<P>],
184        pre_handling_initializers: &[ContextPreHandlingInitializer<P>],
185        world: WorldGuard,
186        runtime: &mut P::R,
187    ) -> Result<(), ScriptError> {
188        WorldGuard::with_existing_static_guard(world, |world| {
189            ThreadWorldContainer.set_world(world)?;
190            (reloader)(
191                script,
192                content,
193                previous_context,
194                context_initializers,
195                pre_handling_initializers,
196                runtime,
197            )
198        })
199    }
200}
201
202impl<P: IntoScriptPluginParams> Clone for ContextBuilder<P> {
203    fn clone(&self) -> Self {
204        Self {
205            load: self.load,
206            reload: self.reload,
207        }
208    }
209}
210
211/// A strategy for assigning contexts to new and existing but re-loaded scripts as well as for managing old contexts
212pub struct ContextAssigner<P: IntoScriptPluginParams> {
213    /// Assign a context to the script.
214    /// The assigner can either return `Some(id)` or `None`.
215    /// Returning None will request the processor to assign a new context id to assign to this script.
216    ///
217    /// Regardless, whether a script gets a new context id or not, the processor will check if the given context exists.
218    /// If it does not exist, it will create a new context and assign it to the script.
219    /// If it does exist, it will NOT create a new context, but assign the existing one to the script, and re-load the context.
220    ///
221    /// This function is only called once for each script, when it is loaded for the first time.
222    pub assign: fn(
223        script_id: &ScriptId,
224        new_content: &[u8],
225        contexts: &ScriptContexts<P>,
226    ) -> Option<ContextId>,
227
228    /// Handle the removal of the script, if any clean up in contexts is necessary perform it here.
229    ///
230    /// If you do not clean up the context here, it will stay in the context map!
231    pub remove: fn(context_id: ContextId, script: &Script, contexts: &mut ScriptContexts<P>),
232}
233
234impl<P: IntoScriptPluginParams> ContextAssigner<P> {
235    /// Create an assigner which re-uses a single global context for all scripts, only use if you know what you're doing.
236    /// Will not perform any clean up on removal.
237    pub fn new_global_context_assigner() -> Self {
238        Self {
239            assign: |_, _, _| Some(0), // always use the same id in rotation
240            remove: |_, _, _| {},      // do nothing
241        }
242    }
243
244    /// Create an assigner which assigns a new context to each script. This is the default strategy.
245    pub fn new_individual_context_assigner() -> Self {
246        Self {
247            assign: |_, _, _| None,
248            remove: |id, _, c| _ = c.remove(id),
249        }
250    }
251}
252
253impl<P: IntoScriptPluginParams> Default for ContextAssigner<P> {
254    fn default() -> Self {
255        Self::new_individual_context_assigner()
256    }
257}
258
259impl<P: IntoScriptPluginParams> Clone for ContextAssigner<P> {
260    fn clone(&self) -> Self {
261        Self {
262            assign: self.assign,
263            remove: self.remove,
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use crate::asset::Language;
271
272    use super::*;
273
274    struct DummyParams;
275    impl IntoScriptPluginParams for DummyParams {
276        type C = String;
277        type R = ();
278
279        const LANGUAGE: Language = Language::Lua;
280
281        fn build_runtime() -> Self::R {}
282    }
283
284    #[test]
285    fn test_script_contexts_insert_get() {
286        let mut contexts: ScriptContexts<DummyParams> = ScriptContexts::default();
287        let id = contexts.insert("context1".to_string());
288        assert_eq!(contexts.contexts.get(&id), Some(&"context1".to_string()));
289        assert_eq!(
290            contexts.contexts.get_mut(&id),
291            Some(&mut "context1".to_string())
292        );
293    }
294
295    #[test]
296    fn test_script_contexts_allocate_id() {
297        let contexts: ScriptContexts<DummyParams> = ScriptContexts::default();
298        let id = contexts.allocate_id();
299        let next_id = contexts.allocate_id();
300        assert_eq!(next_id, id + 1);
301    }
302
303    #[test]
304    fn test_script_contexts_remove() {
305        let mut contexts: ScriptContexts<DummyParams> = ScriptContexts::default();
306        let id = contexts.insert("context1".to_string());
307        let removed = contexts.remove(id);
308        assert_eq!(removed, Some("context1".to_string()));
309        assert!(!contexts.contexts.contains_key(&id));
310
311        // assert next id is still incremented
312        let next_id = contexts.allocate_id();
313        assert_eq!(next_id, id + 1);
314    }
315}