use std::{hash::Hash, sync::Arc};
use bevy_mod_scripting_script::ScriptAttachment;
use parking_lot::{Mutex, RwLock};
use super::*;
use crate::IntoScriptPluginParams;
pub trait ContextKeySelector: Send + Sync + std::fmt::Debug + 'static {
fn select(&self, context_key: &ScriptAttachment) -> Option<ContextKey>;
}
impl<F: Fn(&ScriptAttachment) -> Option<ContextKey> + Send + Sync + std::fmt::Debug + 'static>
ContextKeySelector for F
{
fn select(&self, context_key: &ScriptAttachment) -> Option<ContextKey> {
(self)(context_key)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextRule {
EntityScript,
Entity,
Script,
Shared,
}
impl ContextKeySelector for ContextRule {
fn select(&self, context_key: &ScriptAttachment) -> Option<ContextKey> {
let context_key: ContextKey = context_key.clone().into();
match self {
ContextRule::Entity => context_key.entity.map(|e| ContextKey {
entity: Some(e),
script: None,
}),
ContextRule::Script => context_key.script.map(|h| ContextKey {
entity: None,
script: Some(h),
}),
ContextRule::EntityScript => {
context_key
.entity
.zip(context_key.script)
.map(|(entity, script)| ContextKey {
entity: Some(entity),
script: Some(script),
})
}
ContextRule::Shared => Some(ContextKey::default()),
}
}
}
#[derive(Debug)]
pub struct ContextPolicy {
pub priorities: Vec<Arc<dyn ContextKeySelector>>,
}
impl Clone for ContextPolicy {
fn clone(&self) -> Self {
Self {
priorities: self.priorities.to_vec(),
}
}
}
impl Default for ContextPolicy {
fn default() -> Self {
ContextPolicy::per_entity_and_script()
}
}
impl ContextPolicy {
pub fn which_rule(&self, context_key: &ScriptAttachment) -> Option<&dyn ContextKeySelector> {
self.priorities
.iter()
.find_map(|rule| rule.select(context_key).is_some().then_some(rule.as_ref()))
}
pub fn shared() -> Self {
ContextPolicy {
priorities: vec![Arc::new(ContextRule::Shared)],
}
}
pub fn per_entity() -> Self {
ContextPolicy {
priorities: vec![
Arc::new(ContextRule::Entity),
Arc::new(ContextRule::Script),
Arc::new(ContextRule::Shared),
],
}
}
pub fn per_script() -> Self {
ContextPolicy {
priorities: vec![Arc::new(ContextRule::Script), Arc::new(ContextRule::Shared)],
}
}
pub fn per_entity_and_script() -> Self {
ContextPolicy {
priorities: vec![
Arc::new(ContextRule::EntityScript),
Arc::new(ContextRule::Script),
Arc::new(ContextRule::Shared),
],
}
}
}
impl ContextKeySelector for ContextPolicy {
fn select(&self, context_key: &ScriptAttachment) -> Option<ContextKey> {
self.priorities
.iter()
.find_map(|priority| priority.select(context_key))
}
}
#[derive(Default)]
struct ContextEntry<P: IntoScriptPluginParams> {
residents: HashSet<ScriptAttachment>,
context: Context<P>,
}
#[derive(Default)]
pub enum Context<P: IntoScriptPluginParams> {
LoadedAndActive(Arc<Mutex<P::C>>),
#[default]
Loading,
Unloading(Arc<Mutex<P::C>>),
Reloading(Arc<Mutex<P::C>>),
}
impl<P: IntoScriptPluginParams> From<Arc<Mutex<P::C>>> for Context<P> {
fn from(val: Arc<Mutex<P::C>>) -> Self {
Context::LoadedAndActive(val)
}
}
impl<P: IntoScriptPluginParams> std::fmt::Debug for Context<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LoadedAndActive { .. } => f.debug_struct("LoadedAndActive").finish(),
Self::Loading => write!(f, "Loading"),
Self::Unloading { .. } => f.debug_struct("Unloading").finish(),
Self::Reloading { .. } => f.debug_struct("Reloading").finish(),
}
}
}
impl<P: IntoScriptPluginParams> std::fmt::Display for Context<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Context::LoadedAndActive { .. } => f.write_str("Loaded"),
Context::Loading => f.write_str("Loading"),
Context::Unloading { .. } => f.write_str("Unloading"),
Context::Reloading { .. } => f.write_str("Reloading"),
}
}
}
impl<P: IntoScriptPluginParams> Context<P> {
pub fn as_loaded(&self) -> Option<&Arc<Mutex<P::C>>> {
match self {
Context::LoadedAndActive(context) => Some(context),
_ => None,
}
}
pub fn is_loading_or_reloading(&self) -> bool {
matches!(self, Context::Loading | Context::Reloading(_))
}
pub fn as_available_context(&self) -> Option<&Arc<Mutex<P::C>>> {
match self {
Context::LoadedAndActive(mutex)
| Context::Unloading(mutex)
| Context::Reloading(mutex) => Some(mutex),
Context::Loading => None,
}
}
}
impl<P: IntoScriptPluginParams> Clone for Context<P> {
fn clone(&self) -> Self {
match self {
Self::LoadedAndActive(context) => Self::LoadedAndActive(context.clone()),
Self::Loading => Self::Loading,
Self::Unloading(context) => Self::Unloading(context.clone()),
Self::Reloading(context) => Self::Reloading(context.clone()),
}
}
}
#[derive(Resource)]
pub struct ScriptContexts<P: IntoScriptPluginParams>(Arc<RwLock<ScriptContextInner<P>>>);
impl<P: IntoScriptPluginParams> ScriptContexts<P> {
pub fn new(policy: ContextPolicy) -> Self {
Self(Arc::new(RwLock::new(ScriptContextInner::new(policy))))
}
pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, ScriptContextInner<P>> {
self.0.read()
}
pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, ScriptContextInner<P>> {
self.0.write()
}
}
impl<P: IntoScriptPluginParams> Clone for ScriptContexts<P> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<P: IntoScriptPluginParams> Default for ScriptContexts<P> {
fn default() -> Self {
Self::new(ContextPolicy::default())
}
}
pub struct ScriptContextInner<P: IntoScriptPluginParams> {
map: HashMap<ContextKey, ContextEntry<P>>,
pub policy: ContextPolicy,
}
impl<P: IntoScriptPluginParams> ScriptContextInner<P> {
pub fn new(policy: ContextPolicy) -> Self {
Self {
map: HashMap::default(),
policy,
}
}
fn get_entry(&self, context_key: &ScriptAttachment) -> Option<&ContextEntry<P>> {
self.policy
.select(context_key)
.and_then(|key| self.map.get(&key))
}
fn get_entry_mut(&mut self, context_key: &ScriptAttachment) -> Option<&mut ContextEntry<P>> {
self.policy
.select(context_key)
.and_then(|key| self.map.get_mut(&key))
}
pub fn get_context(&self, context_key: &ScriptAttachment) -> Option<Context<P>> {
self.get_entry(context_key)
.map(|entry| entry.context.clone())
}
pub fn replace_context(
&mut self,
context_key: &ScriptAttachment,
replace_with: Context<P>,
) -> Option<Context<P>> {
self.get_entry_mut(context_key)
.map(|entry| std::mem::replace(&mut entry.context, replace_with))
}
pub fn get_if_resident(&self, context_key: &ScriptAttachment) -> Option<Context<P>> {
self.get_entry(context_key).and_then(|entry| {
if entry.residents.contains(context_key) {
Some(entry.context.clone())
} else {
None
}
})
}
pub fn insert(
&mut self,
context_key: ScriptAttachment,
context: Context<P>,
) -> Result<(), (ScriptAttachment, Context<P>)> {
match self.policy.select(&context_key) {
Some(key) => {
let entry = self
.map
.entry(key.clone())
.and_modify(|c| c.context = context.clone())
.or_insert_with(|| ContextEntry {
residents: HashSet::from_iter([context_key.clone()]),
context,
});
entry.residents.insert(context_key.clone());
Ok(())
}
None => Err((context_key, context)),
}
}
pub fn mark_active_if_not_loading<'a>(
&mut self,
context_key: &'a ScriptAttachment,
) -> Result<(), &'a ScriptAttachment> {
if let Some(entry) = self.get_entry_mut(context_key)
&& let Some(ctxt) = entry.context.as_available_context()
{
entry.context = Context::LoadedAndActive(ctxt.clone());
return Ok(());
}
Err(context_key)
}
pub fn insert_resident(
&mut self,
context_key: ScriptAttachment,
) -> Result<bool, ScriptAttachment> {
if let Some(entry) = self.get_entry_mut(&context_key) {
Ok(entry.residents.insert(context_key))
} else {
Err(context_key)
}
}
pub fn remove_resident(&mut self, context_key: &ScriptAttachment) {
if let Some(entry) = self.get_entry_mut(context_key) {
entry.residents.remove(context_key);
}
}
pub fn all_residents(
&self,
) -> impl Iterator<Item = (ScriptAttachment, Context<P>)> + use<'_, P> {
self.map.values().flat_map(|entry| {
entry
.residents
.iter()
.map(move |resident| (resident.clone(), entry.context.clone()))
})
}
pub fn all_residents_len(&self) -> usize {
self.map.values().map(|entry| entry.residents.len()).sum()
}
pub fn first_resident_from_each_context(
&self,
) -> impl Iterator<Item = (ScriptAttachment, Context<P>)> + use<'_, P> {
self.map.values().filter_map(|entry| {
entry
.residents
.iter()
.next()
.map(|resident| (resident.clone(), entry.context.clone()))
})
}
pub fn residents(
&self,
context_key: &ScriptAttachment,
) -> impl Iterator<Item = (ScriptAttachment, Context<P>)> + use<'_, P> {
self.get_entry(context_key).into_iter().flat_map(|entry| {
entry
.residents
.iter()
.map(move |resident| (resident.clone(), entry.context.clone()))
})
}
pub fn residents_len(&self, context_key: &ScriptAttachment) -> usize {
self.get_entry(context_key)
.map_or(0, |entry| entry.residents.len())
}
pub fn contains(&self, context_key: &ScriptAttachment) -> bool {
self.get_entry(context_key)
.is_some_and(|entry| entry.residents.contains(context_key))
}
pub fn remove(&mut self, context_key: &ScriptAttachment) -> Option<Context<P>> {
self.policy
.select(context_key)
.and_then(|key| self.map.remove(&key).map(|entry| entry.context))
}
}
impl<P: IntoScriptPluginParams> Default for ScriptContextInner<P> {
fn default() -> Self {
Self {
map: HashMap::default(),
policy: ContextPolicy::default(),
}
}
}
#[cfg(test)]
mod tests {
use crate::config::{GetPluginThreadConfig, ScriptingPluginConfiguration};
use bevy_app::{App, Plugin};
use bevy_mod_scripting_bindings::ScriptValue;
use test_utils::make_test_plugin;
use super::*;
make_test_plugin!(crate);
#[test]
fn test_insertion_per_script_policy() {
let policy = ContextPolicy::per_script();
let script_context = ScriptContexts::<TestPlugin>::new(policy.clone());
let mut script_context = script_context.write();
let context_key =
ScriptAttachment::EntityScript(Entity::from_raw_u32(1u32).unwrap(), Handle::default());
let context_key2 =
ScriptAttachment::EntityScript(Entity::from_raw_u32(2u32).unwrap(), Handle::default());
assert_eq!(policy.select(&context_key), policy.select(&context_key2));
script_context
.insert(
context_key.clone(),
Context::LoadedAndActive(Arc::new(Mutex::new(TestContext::default()))),
)
.unwrap();
assert!(script_context.contains(&context_key));
assert_eq!(script_context.residents_len(&context_key), 1);
let resident = script_context.residents(&context_key).next().unwrap();
assert_eq!(resident.0, context_key);
assert!(script_context.get_context(&context_key).is_some());
assert!(
script_context
.insert_resident(context_key2.clone())
.unwrap()
);
assert!(script_context.contains(&context_key2));
let mut residents = script_context.residents(&context_key2).collect::<Vec<_>>();
residents.sort_by_key(|r| r.0.entity());
assert_eq!(residents[0].0, context_key2);
assert_eq!(residents[1].0, context_key);
assert_eq!(residents.len(), 2);
assert_eq!(script_context.residents_len(&context_key2), 2);
}
}