diff --git a/Cargo.toml b/Cargo.toml index e9c93dba57..ef7dfe3a24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,7 @@ bevy_mod_scripting_display = { path = "crates/bevy_mod_scripting_display", versi bevy_mod_scripting_script = { path = "crates/bevy_mod_scripting_script", version = "0.19.0", default-features = false } lua_language_server_lad_backend = { path = "crates/lad_backends/lua_language_server_lad_backend", version = "0.19.0", default-features = false } bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.19.0" } - +bevy_mod_scripting_world = { path = "crates/bevy_mod_scripting_world", version = "0.19.0", default-features = true} # bevy @@ -203,6 +203,7 @@ smol_str = { version = "0.2.0", default-features = false } nonmax = { version = "0.5", default-features = false, features = ["std"] } # other +fixedbitset = { version = "0.5" } serde_json = { version = "1.0", default-features = false } indexmap = { version = "2.7", default-features = false, features = ["std"] } profiling = { version = "1.0", default-features = false, features = [ @@ -224,7 +225,6 @@ syn = { version = "2.0", default-features = false } proc-macro2 = { version = "1.0", default-features = false } smallvec = { version = "1.11", default-features = false } itertools = { version = "0.14", default-features = false } -fixedbitset = { version = "0.5", default-features = false } variadics_please = { version = "1.1.0", default-features = false } anyhow = { version = "1.0", default-features = false } indent_write = { version = "2", default-features = false, features = ["std"] } @@ -294,7 +294,7 @@ members = [ "crates/bevy_mod_scripting_script", "crates/bevy_mod_scripting_bindings_domain", "crates/bindings/*", - "crates/testing_crates/bevy_mod_scripting_test_scenario_syntax", + "crates/testing_crates/bevy_mod_scripting_test_scenario_syntax", "crates/bevy_mod_scripting_world", ] resolver = "2" exclude = ["codegen", "crates/macro_tests", "xtask"] diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 9a5720e434..37a6499bbf 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -10,7 +10,9 @@ use bevy::{ }, reflect::Reflect, }; -use bevy_mod_scripting_bindings::{FromScript, IntoScript, M, R, ReflectReference, ScriptValue, V}; +use bevy_mod_scripting_bindings::{ + FromScript, IntoScript, M, R, ReflectReference, ScriptValue, V, WorldExtensions, +}; use criterion::{ BatchSize, BenchmarkFilter, BenchmarkGroup, Criterion, criterion_main, measurement::Measurement, }; diff --git a/crates/bevy_mod_scripting_bindings/Cargo.toml b/crates/bevy_mod_scripting_bindings/Cargo.toml index 7bdfb0eb6b..a8a5c5a6e0 100644 --- a/crates/bevy_mod_scripting_bindings/Cargo.toml +++ b/crates/bevy_mod_scripting_bindings/Cargo.toml @@ -18,6 +18,7 @@ bevy_mod_scripting_derive = { workspace = true } bevy_mod_scripting_display = { workspace = true } bevy_mod_scripting_script = { workspace = true } bevy_mod_scripting_bindings_domain = { workspace = true } +bevy_mod_scripting_world = { workspace = true } bevy_system_reflection = { workspace = true } bevy_diagnostic = { workspace = true } bevy_ecs = { workspace = true } diff --git a/crates/bevy_mod_scripting_bindings/src/access_map.rs b/crates/bevy_mod_scripting_bindings/src/access_map.rs deleted file mode 100644 index 5ff6961e40..0000000000 --- a/crates/bevy_mod_scripting_bindings/src/access_map.rs +++ /dev/null @@ -1,1268 +0,0 @@ -//! A map of access claims used to safely and dynamically access the world. - -use bevy_mod_scripting_derive::DebugWithTypeInfo; -use bevy_mod_scripting_display::{DisplayWithTypeInfo, GetTypeInfo, WithTypeInfo}; -use bevy_platform::collections::{HashMap, HashSet}; - -use ::bevy_ecs::{component::ComponentId, world::unsafe_world_cell::UnsafeWorldCell}; -use bevy_ecs::{component::Component, resource::Resource}; -use bevy_log::error; -use parking_lot::Mutex; -use smallvec::SmallVec; -use std::hash::{BuildHasherDefault, Hasher}; - -use crate::error::InteropError; - -use super::{ReflectAllocationId, ReflectBase}; - -#[derive(Debug, Clone, PartialEq, Eq)] -/// An owner of an access claim and the code location of the claim. -pub struct ClaimOwner { - location: std::panic::Location<'static>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -/// A count of the number of readers and writers of an access claim. -pub struct AccessCount { - /// The number of readers including thread information - read_by: SmallVec<[ClaimOwner; 1]>, - /// If the current read is a write access, this will be set - written: bool, -} - -impl Default for AccessCount { - fn default() -> Self { - Self::new() - } -} - -#[profiling::all_functions] -impl AccessCount { - fn new() -> Self { - Self { - read_by: Default::default(), - written: false, - } - } - - fn can_read(&self) -> bool { - !self.written - } - - fn can_write(&self) -> bool { - self.read_by.is_empty() && !self.written - } - - fn as_location(&self) -> Option> { - self.read_by.first().map(|o| o.location) - } - - fn readers(&self) -> usize { - self.read_by.len() - } -} - -/// For structs which can be mapped to a u64 index -pub trait AccessMapKey { - /// Convert the key to an index - /// - /// The key 0 must not be be used as it's reserved for global access - fn as_index(&self) -> u64; - - /// Convert an index back to the original struct - fn from_index(value: u64) -> Self; -} - -#[profiling::all_functions] -impl AccessMapKey for u64 { - fn as_index(&self) -> u64 { - *self - } - - fn from_index(value: u64) -> Self { - value - } -} - -/// Describes kinds of base value we are accessing via reflection -#[derive(PartialEq, Eq, Copy, Clone, Hash, DebugWithTypeInfo)] -#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")] -pub enum ReflectAccessKind { - /// Accessing a component or resource - ComponentOrResource, - /// Accessing an owned value - Allocation, - /// Accessing the world - Global, -} - -/// Describes the id pointing to the base value we are accessing via reflection, for components and resources this is the ComponentId -/// for script owned values this is an allocationId, this is used to ensure we have permission to access the value. -#[derive(PartialEq, Eq, Copy, Clone, Hash, DebugWithTypeInfo)] -#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")] -pub struct ReflectAccessId { - pub(crate) kind: ReflectAccessKind, - pub(crate) id: u64, -} - -impl DisplayWithTypeInfo for ReflectAccessId { - fn display_with_type_info( - &self, - f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, - ) -> std::fmt::Result { - match self.kind { - ReflectAccessKind::ComponentOrResource => { - write!( - f, - "Component or resource: {}", - WithTypeInfo::new_with_opt_info( - &ComponentId::new(self.id as usize), - type_info_provider - ) - ) - } - ReflectAccessKind::Allocation => write!( - f, - "Allocation to: {}", - WithTypeInfo::new_with_opt_info( - &ReflectAllocationId::new(self.id), - type_info_provider - ) - ), - ReflectAccessKind::Global => write!(f, "World(Global)"), - } - } -} - -#[profiling::all_functions] -impl AccessMapKey for ReflectAccessId { - fn as_index(&self) -> u64 { - // project two linear non-negative ranges [0,inf] to a single linear non-negative range, offset by 1 to avoid 0 - // y1 = 2x - 0 + 2 = 2x + 2 - // y2 = 2x - 1 + 2 = 2x + 1 - match self.kind { - ReflectAccessKind::ComponentOrResource => (self.id * 2) + 2, - ReflectAccessKind::Allocation => (self.id * 2) + 1, - ReflectAccessKind::Global => 0, - } - } - - fn from_index(value: u64) -> Self { - // reverse the projection - // x1 = (y1 - 2) / 2 - // x2 = (y2 - 1) / 2 - - match value { - 0 => ReflectAccessId { - kind: ReflectAccessKind::Global, - id: 0, - }, - v if v % 2 == 0 => ReflectAccessId { - kind: ReflectAccessKind::ComponentOrResource, - id: (v - 2) / 2, - }, - v => ReflectAccessId { - kind: ReflectAccessKind::Allocation, - id: (v - 1) / 2, - }, - } - } -} - -#[profiling::all_functions] -impl ReflectAccessId { - /// Creates a new access id for the global world - pub fn for_global() -> Self { - Self { - kind: ReflectAccessKind::Global, - id: 0, - } - } - - /// Creates a new access id for a resource - pub fn for_resource(cell: &UnsafeWorldCell) -> Result { - let resource_id = cell.components().resource_id::().ok_or_else(|| { - InteropError::unregistered_component_or_resource_type(std::any::type_name::()) - })?; - - Ok(Self { - kind: ReflectAccessKind::ComponentOrResource, - id: resource_id.index() as u64, - }) - } - - /// Creates a new access id for a component - pub fn for_component(cell: &UnsafeWorldCell) -> Result { - let component_id = cell.components().component_id::().ok_or_else(|| { - InteropError::unregistered_component_or_resource_type(std::any::type_name::()) - })?; - - Ok(Self::for_component_id(component_id)) - } - - /// Creates a new access id for a component id - pub fn for_allocation(id: ReflectAllocationId) -> Self { - Self { - kind: ReflectAccessKind::Allocation, - id: id.id(), - } - } - - /// Creates a new access id for a component id - pub fn for_component_id(id: ComponentId) -> Self { - Self { - kind: ReflectAccessKind::ComponentOrResource, - id: id.index() as u64, - } - } - - /// Creates a new access id for a reference - pub fn for_reference(base: ReflectBase) -> Self { - match base { - ReflectBase::Resource(id) => Self::for_component_id(id), - ReflectBase::Component(_, id) => Self::for_component_id(id), - ReflectBase::Owned(id) => Self::for_allocation(id), - ReflectBase::Asset(_, assets_resource_id) => Self::for_component_id(assets_resource_id), - } - } -} - -impl From for ReflectAccessId { - fn from(id: ComponentId) -> Self { - ReflectAccessId::for_component_id(id) - } -} - -impl From for ReflectAccessId { - fn from(id: ReflectAllocationId) -> Self { - ReflectAccessId::for_allocation(id) - } -} - -#[profiling::all_functions] -impl From for ComponentId { - fn from(val: ReflectAccessId) -> Self { - ComponentId::new(val.id as usize) - } -} - -#[profiling::all_functions] -impl From for ReflectAllocationId { - fn from(val: ReflectAccessId) -> Self { - ReflectAllocationId::new(val.id) - } -} - -#[derive(Debug, Default)] -/// A map of access claims -pub struct AccessMap(Mutex); - -/// A trait for controlling system world access at runtime. -/// -/// This trait provides methods to claim and release read, write, and global access -/// to various parts of the world. Implementations of this trait manage internal state -/// to ensure safe and concurrent access to resources. Methods include scope-based locking, -/// as well as introspection of access state via code location information. -pub trait DynamicSystemMeta { - /// Executes the provided closure within a temporary access scope. - /// - /// Any accesses claimed within the scope are rolled back once the closure returns. - fn with_scope O>(&self, f: F) -> O; - - /// Returns `true` if the world is exclusively locked. - /// - /// When exclusively locked, no additional individual or global accesses may be claimed. - fn is_locked_exclusively(&self) -> bool; - - /// Retrieves the code location where the global lock was claimed (if any). - /// - /// This is useful for debugging conflicts involving the global access lock. - fn global_access_location(&self) -> Option>; - - /// Attempts to claim read access for the given key. - /// - /// Returns `true` if the read access is successfully claimed. The claim will fail if - /// the key is currently locked for write or if a global lock is active. - #[track_caller] - fn claim_read_access(&self, key: K) -> bool; - - /// Attempts to claim write access for the given key. - /// - /// Returns `true` if the write access is successfully claimed. Write access fails if any - /// read or write access is active for the key or if a global lock is held. - #[track_caller] - fn claim_write_access(&self, key: K) -> bool; - - /// Attempts to claim a global access lock. - /// - /// Returns `true` if the global access is successfully claimed. Global access precludes any - /// individual accesses until it is released. - #[track_caller] - fn claim_global_access(&self) -> bool; - - /// Releases an access claimed for the provided key. - /// - /// # Panics - /// - /// Panics if the access is released by a thread different from the one that claimed it. - fn release_access(&self, key: K); - - /// Releases an active global access lock. - /// - /// # Panics - /// - /// Panics if the global access is released from a thread other than the one that claimed it. - fn release_global_access(&self); - - /// Returns a list of active accesses. - /// - /// The list is provided as key and corresponding access count pairs. - fn list_accesses(&self) -> Vec<(K, AccessCount)>; - - /// Returns the number of active individual accesses. - /// - /// In the case of a global lock, this method considers that as a single active access. - fn count_accesses(&self) -> usize; - - /// Releases all active accesses. - /// - /// Both individual and global accesses will be removed. - fn release_all_accesses(&self); - - /// Returns the location where the specified key was first accessed. - /// - /// This is useful for debugging and tracing access failures. - fn access_location(&self, key: K) -> Option>; - - /// Returns the location of the first access among all keys. - /// - /// This can assist in identifying the origin of access conflicts. - fn access_first_location(&self) -> Option>; -} - -#[derive(Default)] -/// A hash function which doesn't do much. for maps which expect very small hashes. -/// Assumes only needs to hash u64 values, unsafe otherwise -struct SmallIdentityHash(u64); -impl Hasher for SmallIdentityHash { - fn finish(&self) -> u64 { - self.0 - } - - fn write(&mut self, bytes: &[u8]) { - // concat all bytes via && - // this is a bit of a hack, but it works for our use case - // and is faster than using a hash function - #[allow(clippy::expect_used, reason = "cannot handle this panic otherwise")] - let arr: &[u8; 8] = bytes.try_into().expect("this hasher only supports u64"); - // depending on endianess - - #[cfg(target_endian = "big")] - let word = u64::from_be_bytes(*arr); - #[cfg(target_endian = "little")] - let word = u64::from_le_bytes(*arr); - self.0 = word - } -} - -#[derive(Default, Debug, Clone)] -struct AccessMapInner { - individual_accesses: HashMap>, - global_lock: AccessCount, -} - -#[profiling::all_functions] -impl AccessMapInner { - #[inline] - fn entry(&self, key: u64) -> Option<&AccessCount> { - self.individual_accesses.get(&key) - } - - #[inline] - fn entry_mut(&mut self, key: u64) -> Option<&mut AccessCount> { - self.individual_accesses.get_mut(&key) - } - - #[inline] - fn entry_or_default(&mut self, key: u64) -> &mut AccessCount { - self.individual_accesses.entry(key).or_default() - } - - #[inline] - fn remove(&mut self, key: u64) { - self.individual_accesses.remove(&key); - } -} - -const GLOBAL_KEY: u64 = 0; - -#[profiling::all_functions] -impl DynamicSystemMeta for AccessMap { - fn with_scope O>(&self, f: F) -> O { - // Snapshot the current inner state. - let backup = { - let inner = self.0.lock(); - inner.clone() - }; - - let result = f(); - - // Roll back the inner state. - { - let mut inner = self.0.lock(); - *inner = backup; - } - - result - } - - fn is_locked_exclusively(&self) -> bool { - let inner = self.0.lock(); - // If global_lock cannot be written, then it is locked exclusively. - !inner.global_lock.can_write() - } - - fn global_access_location(&self) -> Option> { - let inner = self.0.lock(); - inner.global_lock.as_location() - } - - #[track_caller] - fn claim_read_access(&self, key: K) -> bool { - let mut inner = self.0.lock(); - - if !inner.global_lock.can_write() { - return false; - } - - let key = key.as_index(); - if key == GLOBAL_KEY { - error!("Trying to claim read access to global key, this is not allowed"); - return false; - } - - let entry = inner.entry_or_default(key); - - if entry.can_read() { - entry.read_by.push(ClaimOwner { - location: *std::panic::Location::caller(), - }); - true - } else { - false - } - } - - #[track_caller] - fn claim_write_access(&self, key: K) -> bool { - let mut inner = self.0.lock(); - - if !inner.global_lock.can_write() { - return false; - } - - let key = key.as_index(); - if key == GLOBAL_KEY { - error!("Trying to claim write access to global key, this is not allowed"); - return false; - } - - let entry = inner.entry_or_default(key); - - if entry.can_write() { - entry.read_by.push(ClaimOwner { - location: *std::panic::Location::caller(), - }); - entry.written = true; - true - } else { - false - } - } - - #[track_caller] - fn claim_global_access(&self) -> bool { - let mut inner = self.0.lock(); - - if !inner.individual_accesses.is_empty() || !inner.global_lock.can_write() { - return false; - } - inner.global_lock.read_by.push(ClaimOwner { - location: *std::panic::Location::caller(), - }); - inner.global_lock.written = true; - true - } - - fn release_access(&self, key: K) { - let mut inner = self.0.lock(); - let key = key.as_index(); - - if let Some(entry) = inner.entry_mut(key) { - entry.written = false; - entry.read_by.pop(); - if entry.readers() == 0 { - inner.remove(key); - } - } - } - - fn release_global_access(&self) { - let mut inner = self.0.lock(); - inner.global_lock.read_by.pop(); - inner.global_lock.written = false; - } - - fn list_accesses(&self) -> Vec<(K, AccessCount)> { - let inner = self.0.lock(); - inner - .individual_accesses - .iter() - .map(|(key, a)| (K::from_index(*key), a.clone())) - .collect() - } - - fn count_accesses(&self) -> usize { - if self.is_locked_exclusively() { - 1 - } else { - let inner = self.0.lock(); - inner.individual_accesses.len() - } - } - - fn release_all_accesses(&self) { - let mut inner = self.0.lock(); - inner.individual_accesses.clear(); - // Release global access if held. - inner.global_lock.written = false; - inner.global_lock.read_by.clear(); - } - - fn access_location(&self, key: K) -> Option> { - let inner = self.0.lock(); - if key.as_index() == 0 { - // it blocked by individual access - inner.global_lock.as_location().or_else(|| { - inner - .individual_accesses - .iter() - .next() - .and_then(|(_, access_count)| access_count.as_location()) - }) - } else { - inner - .entry(key.as_index()) - .and_then(|access| access.as_location()) - } - } - - fn access_first_location(&self) -> Option> { - let inner = self.0.lock(); - inner - .individual_accesses - .iter() - .next() - .and_then(|(_, access)| access.as_location()) - } -} - -/// An inverse of [`AccessMap`], It limits the accesses allowed to be claimed to those in a pre-specified subset. -pub struct SubsetAccessMap { - inner: AccessMap, - subset: Box bool + Send + Sync + 'static>, -} - -#[profiling::all_functions] -impl SubsetAccessMap { - /// Creates a new subset access map with the provided subset of ID's as well as a exception function. - pub fn new( - subset: impl IntoIterator, - exception: impl Fn(u64) -> bool + Send + Sync + 'static, - ) -> Self { - let set = subset - .into_iter() - .map(|k| k.as_index()) - .collect::>(); - Self { - inner: Default::default(), - subset: Box::new(move |id| set.contains(&id) || exception(id)), - } - } - - fn in_subset(&self, key: u64) -> bool { - (self.subset)(key) - } -} - -#[profiling::all_functions] -impl DynamicSystemMeta for SubsetAccessMap { - fn with_scope O>(&self, f: F) -> O { - self.inner.with_scope(f) - } - - fn is_locked_exclusively(&self) -> bool { - self.inner.is_locked_exclusively() - } - - fn global_access_location(&self) -> Option> { - self.inner.global_access_location() - } - - fn claim_read_access(&self, key: K) -> bool { - if !self.in_subset(key.as_index()) { - return false; - } - self.inner.claim_read_access(key) - } - - fn claim_write_access(&self, key: K) -> bool { - if !self.in_subset(key.as_index()) { - return false; - } - self.inner.claim_write_access(key) - } - - fn claim_global_access(&self) -> bool { - if !self.in_subset(0) { - return false; - } - self.inner.claim_global_access() - } - - fn release_access(&self, key: K) { - self.inner.release_access(key); - } - - fn release_global_access(&self) { - self.inner.release_global_access(); - } - - fn list_accesses(&self) -> Vec<(K, AccessCount)> { - self.inner.list_accesses() - } - - fn count_accesses(&self) -> usize { - self.inner.count_accesses() - } - - fn release_all_accesses(&self) { - self.inner.release_all_accesses(); - } - - fn access_location(&self, key: K) -> Option> { - self.inner.access_location(key) - } - - fn access_first_location(&self) -> Option> { - self.inner.access_first_location() - } -} - -/// A polymorphic enum for access map types. -/// -/// Equivalent to `dyn DynamicSystemMeta` for most purposes -pub enum AnyAccessMap { - /// A map which allows any and all accesses to be claimed - UnlimitedAccessMap(AccessMap), - /// A map which only allows accesses to keys in a pre-specified subset - SubsetAccessMap(SubsetAccessMap), -} - -#[profiling::all_functions] -impl DynamicSystemMeta for AnyAccessMap { - fn with_scope O>(&self, f: F) -> O { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.with_scope(f), - AnyAccessMap::SubsetAccessMap(map) => map.with_scope(f), - } - } - - fn is_locked_exclusively(&self) -> bool { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.is_locked_exclusively(), - AnyAccessMap::SubsetAccessMap(map) => map.is_locked_exclusively(), - } - } - - fn global_access_location(&self) -> Option> { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.global_access_location(), - AnyAccessMap::SubsetAccessMap(map) => map.global_access_location(), - } - } - - fn claim_read_access(&self, key: K) -> bool { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.claim_read_access(key), - AnyAccessMap::SubsetAccessMap(map) => map.claim_read_access(key), - } - } - - fn claim_write_access(&self, key: K) -> bool { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.claim_write_access(key), - AnyAccessMap::SubsetAccessMap(map) => map.claim_write_access(key), - } - } - - fn claim_global_access(&self) -> bool { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.claim_global_access(), - AnyAccessMap::SubsetAccessMap(map) => map.claim_global_access(), - } - } - - fn release_access(&self, key: K) { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.release_access(key), - AnyAccessMap::SubsetAccessMap(map) => map.release_access(key), - } - } - - fn release_global_access(&self) { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.release_global_access(), - AnyAccessMap::SubsetAccessMap(map) => map.release_global_access(), - } - } - - fn list_accesses(&self) -> Vec<(K, AccessCount)> { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.list_accesses(), - AnyAccessMap::SubsetAccessMap(map) => map.list_accesses(), - } - } - - fn count_accesses(&self) -> usize { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.count_accesses(), - AnyAccessMap::SubsetAccessMap(map) => map.count_accesses(), - } - } - - fn release_all_accesses(&self) { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.release_all_accesses(), - AnyAccessMap::SubsetAccessMap(map) => map.release_all_accesses(), - } - } - - fn access_location(&self, key: K) -> Option> { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.access_location(key), - AnyAccessMap::SubsetAccessMap(map) => map.access_location(key), - } - } - - fn access_first_location(&self) -> Option> { - match self { - AnyAccessMap::UnlimitedAccessMap(map) => map.access_first_location(), - AnyAccessMap::SubsetAccessMap(map) => map.access_first_location(), - } - } -} - -/// A trait for displaying a code location nicely -pub trait DisplayCodeLocation { - /// Displays the location - fn display_location(self) -> String; -} - -#[profiling::all_functions] -impl DisplayCodeLocation for std::panic::Location<'_> { - fn display_location(self) -> String { - format!("\"{}:{}\"", self.file(), self.line()) - } -} - -#[profiling::all_functions] -impl DisplayCodeLocation for Option> { - fn display_location(self) -> String { - self.map(|l| l.display_location()) - .unwrap_or_else(|| "\"unknown location\"".to_owned()) - } -} - -/// A macro for claiming access to a value for reading -macro_rules! with_access_read { - ($access_map:expr, $id:expr, $msg:expr, $body:block) => {{ - if !$crate::access_map::DynamicSystemMeta::claim_read_access($access_map, $id) { - Err($crate::error::InteropError::cannot_claim_access( - $id, - $crate::access_map::DynamicSystemMeta::access_location($access_map, $id), - $msg, - )) - } else { - let result = $body; - $crate::access_map::DynamicSystemMeta::release_access($access_map, $id); - Ok(result) - } - }}; -} - -pub(crate) use with_access_read; -/// A macro for claiming access to a value for writing -macro_rules! with_access_write { - ($access_map:expr, $id:expr, $msg:expr, $body:block) => { - if !$crate::access_map::DynamicSystemMeta::claim_write_access($access_map, $id) { - Err($crate::error::InteropError::cannot_claim_access( - $id, - $crate::access_map::DynamicSystemMeta::access_location($access_map, $id), - $msg, - )) - } else { - let result = $body; - $crate::access_map::DynamicSystemMeta::release_access($access_map, $id); - Ok(result) - } - }; -} -pub(crate) use with_access_write; - -/// A macro for claiming global access -macro_rules! with_global_access { - ($access_map:expr, $msg:expr, $body:block) => { - if !$crate::access_map::DynamicSystemMeta::claim_global_access($access_map) { - Err($crate::error::InteropError::cannot_claim_access( - $crate::access_map::ReflectAccessId::for_global(), - $crate::access_map::DynamicSystemMeta::access_location( - $access_map, - $crate::access_map::ReflectAccessId::for_global(), - ), - $msg, - )) - } else { - #[allow(clippy::redundant_closure_call)] - let result = (|| $body)(); - $crate::access_map::DynamicSystemMeta::release_global_access($access_map); - Ok(result) - } - }; -} - -pub(crate) use with_global_access; - -#[cfg(test)] -mod test { - use std::hash::Hash; - - use super::*; - - #[test] - fn access_map_list_accesses() { - let access_map = AccessMap::default(); - - access_map.claim_read_access(1); - access_map.claim_write_access(2); - - let accesses = access_map.list_accesses::(); - - assert_eq!(accesses.len(), 2); - let access_0 = accesses.iter().find(|(k, _)| *k == 1).unwrap(); - let access_1 = accesses.iter().find(|(k, _)| *k == 2).unwrap(); - - assert_eq!(access_0.1.readers(), 1); - assert_eq!(access_1.1.readers(), 1); - - assert!(!access_0.1.written); - assert!(access_1.1.written); - } - - #[test] - fn subset_access_map_list_accesses() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 1 || id == 2), - }; - - subset_access_map.claim_read_access(1); - subset_access_map.claim_write_access(2); - - let accesses = subset_access_map.list_accesses::(); - - assert_eq!(accesses.len(), 2); - let access_0 = accesses.iter().find(|(k, _)| *k == 1).unwrap(); - let access_1 = accesses.iter().find(|(k, _)| *k == 2).unwrap(); - - assert_eq!(access_0.1.readers(), 1); - assert_eq!(access_1.1.readers(), 1); - - assert!(!access_0.1.written); - assert!(access_1.1.written); - } - - #[test] - fn access_map_read_access_blocks_write() { - let access_map = AccessMap::default(); - - assert!(access_map.claim_read_access(1)); - assert!(!access_map.claim_write_access(1)); - access_map.release_access(1); - assert!(access_map.claim_write_access(1)); - } - - #[test] - fn subset_access_map_read_access_blocks_write() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 1), - }; - - assert!(subset_access_map.claim_read_access(1)); - assert!(!subset_access_map.claim_write_access(1)); - subset_access_map.release_access(1); - assert!(subset_access_map.claim_write_access(1)); - } - - #[test] - fn access_map_write_access_blocks_read() { - let access_map = AccessMap::default(); - - assert!(access_map.claim_write_access(1)); - assert!(!access_map.claim_read_access(1)); - access_map.release_access(1); - assert!(access_map.claim_read_access(1)); - } - - #[test] - fn subset_access_map_write_access_blocks_read() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 1), - }; - - assert!(subset_access_map.claim_write_access(1)); - assert!(!subset_access_map.claim_read_access(1)); - subset_access_map.release_access(1); - assert!(subset_access_map.claim_read_access(1)); - } - - #[test] - fn access_map_global_access_blocks_all() { - let access_map = AccessMap::default(); - - assert!(access_map.claim_global_access()); - assert!(!access_map.claim_read_access(1)); - assert!(!access_map.claim_write_access(1)); - access_map.release_global_access(); - assert!(access_map.claim_write_access(1)); - access_map.release_access(1); - assert!(access_map.claim_read_access(1)); - } - - #[test] - fn subset_access_map_global_access_blocks_all() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 1 || id == 0), - }; - - assert!(subset_access_map.claim_global_access()); - assert!(!subset_access_map.claim_read_access(1)); - assert!(!subset_access_map.claim_write_access(1)); - subset_access_map.release_global_access(); - assert!(subset_access_map.claim_write_access(1)); - subset_access_map.release_access(1); - assert!(subset_access_map.claim_read_access(1)); - } - - #[test] - fn access_map_any_access_blocks_global() { - let access_map = AccessMap::default(); - - assert!(access_map.claim_read_access(1)); - assert!(!access_map.claim_global_access()); - access_map.release_access(1); - - assert!(access_map.claim_write_access(1)); - assert!(!access_map.claim_global_access()); - } - - #[test] - fn subset_map_any_access_blocks_global() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 0 || id == 1), - }; - - assert!(subset_access_map.claim_read_access(1)); - assert!(!subset_access_map.claim_global_access()); - subset_access_map.release_access(1); - - assert!(subset_access_map.claim_write_access(1)); - assert!(!subset_access_map.claim_global_access()); - } - - #[test] - fn as_and_from_index_for_access_id_non_overlapping() { - let global = ReflectAccessId::for_global(); - - let first_component = ReflectAccessId { - kind: ReflectAccessKind::ComponentOrResource, - id: 0, - }; - - let first_allocation = ReflectAccessId { - kind: ReflectAccessKind::Allocation, - id: 0, - }; - - let second_component = ReflectAccessId { - kind: ReflectAccessKind::ComponentOrResource, - id: 1, - }; - - let second_allocation = ReflectAccessId { - kind: ReflectAccessKind::Allocation, - id: 1, - }; - - assert_eq!(global.as_index(), 0); - assert_eq!(first_allocation.as_index(), 1); - assert_eq!(first_component.as_index(), 2); - assert_eq!(second_allocation.as_index(), 3); - assert_eq!(second_component.as_index(), 4); - - assert_eq!(ReflectAccessId::from_index(0), global); - assert_eq!(ReflectAccessId::from_index(1), first_allocation); - assert_eq!(ReflectAccessId::from_index(2), first_component); - assert_eq!(ReflectAccessId::from_index(3), second_allocation); - assert_eq!(ReflectAccessId::from_index(4), second_component); - } - - #[test] - fn access_map_with_scope_unrolls_individual_accesses() { - let access_map = AccessMap::default(); - // Claim a read access outside the scope - assert!(access_map.claim_read_access(3)); - - // Inside with_scope, claim additional accesses - access_map.with_scope(|| { - assert!(access_map.claim_read_access(1)); - assert!(access_map.claim_write_access(2)); - // At this point, individual_accesses contains keys 0, 1 and 2. - let accesses = access_map.list_accesses::(); - assert_eq!(accesses.len(), 3); - }); - - // After with_scope returns, accesses claimed inside (keys 1 and 2) are unrolled. - let accesses = access_map.list_accesses::(); - // Only the access claimed outside (key 3) remains. - assert_eq!(accesses.len(), 1); - let (k, count) = &accesses[0]; - assert_eq!(*k, 3); - // The outside access remains valid. - assert!(count.readers() > 0); - } - - #[test] - fn subset_map_with_scope_unrolls_individual_accesses() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 1 || id == 2 || id == 3), - }; - - // Claim a read access outside the scope - assert!(subset_access_map.claim_read_access(3)); - - // Inside with_scope, claim additional accesses - subset_access_map.with_scope(|| { - assert!(subset_access_map.claim_read_access(1)); - assert!(subset_access_map.claim_write_access(2)); - // At this point, individual_accesses contains keys 0, 1 and 2. - let accesses = subset_access_map.list_accesses::(); - assert_eq!(accesses.len(), 3); - }); - - // After with_scope returns, accesses claimed inside (keys 1 and 2) are unrolled. - let accesses = subset_access_map.list_accesses::(); - // Only the access claimed outside (key 3) remains. - assert_eq!(accesses.len(), 1); - let (k, count) = &accesses[0]; - assert_eq!(*k, 3); - // The outside access remains valid. - assert!(count.readers() > 0); - } - - #[test] - fn access_map_with_scope_unrolls_global_accesses() { - let access_map = AccessMap::default(); - - access_map.with_scope(|| { - assert!(access_map.claim_global_access()); - // At this point, global_access is claimed. - assert!(!access_map.claim_read_access(1)); - }); - - let accesses = access_map.list_accesses::(); - assert_eq!(accesses.len(), 0); - } - - #[test] - fn subset_map_with_scope_unrolls_global_accesses() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 0 || id == 1), - }; - - subset_access_map.with_scope(|| { - assert!(subset_access_map.claim_global_access()); - // At this point, global_access is claimed. - assert!(!subset_access_map.claim_read_access(1)); - }); - - let accesses = subset_access_map.list_accesses::(); - assert_eq!(accesses.len(), 0); - } - - #[test] - fn access_map_count_accesses_counts_globals() { - let access_map = AccessMap::default(); - - // Initially, no accesses are active. - assert_eq!(access_map.count_accesses(), 0); - - // Claim global access. When global access is active, - // count_accesses should return 1. - assert!(access_map.claim_global_access()); - assert_eq!(access_map.count_accesses(), 1); - access_map.release_global_access(); - - // Now claim individual accesses. - assert!(access_map.claim_read_access(1)); - assert!(access_map.claim_write_access(2)); - // Since two separate keys were claimed, count_accesses should return 2. - assert_eq!(access_map.count_accesses(), 2); - - // Cleanup individual accesses. - access_map.release_access(1); - access_map.release_access(2); - } - - #[test] - fn subset_map_count_accesses_counts_globals() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 0 || id == 1 || id == 2), - }; - - // Initially, no accesses are active. - assert_eq!(subset_access_map.count_accesses(), 0); - - // Claim global access. When global access is active, - // count_accesses should return 1. - assert!(subset_access_map.claim_global_access()); - assert_eq!(subset_access_map.count_accesses(), 1); - subset_access_map.release_global_access(); - - // Now claim individual accesses. - assert!(subset_access_map.claim_read_access(1)); - assert!(subset_access_map.claim_write_access(2)); - // Since two separate keys were claimed, count_accesses should return 2. - assert_eq!(subset_access_map.count_accesses(), 2); - - // Cleanup individual accesses. - subset_access_map.release_access(1); - subset_access_map.release_access(2); - } - - #[test] - fn access_map_location_is_tracked_for_all_types_of_accesses() { - let access_map = AccessMap::default(); - - assert!(access_map.claim_global_access()); - assert!( - access_map - .access_location(ReflectAccessId::for_global()) - .is_some() - ); - access_map.release_global_access(); - - // Claim a read access - assert!(access_map.claim_read_access(1)); - assert!(access_map.access_location(1).is_some()); - access_map.release_access(1); - - // Claim a write access - assert!(access_map.claim_write_access(2)); - assert!(access_map.access_location(2).is_some()); - access_map.release_access(2); - } - - #[test] - fn subset_map_location_is_tracked_for_all_types_of_accesses() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 0 || id == 1 || id == 2), - }; - - assert!(subset_access_map.claim_global_access()); - assert!( - subset_access_map - .access_location(ReflectAccessId::for_global()) - .is_some() - ); - subset_access_map.release_global_access(); - - // Claim a read access - assert!(subset_access_map.claim_read_access(1)); - assert!(subset_access_map.access_location(1).is_some()); - subset_access_map.release_access(1); - - // Claim a write access - assert!(subset_access_map.claim_write_access(2)); - assert!(subset_access_map.access_location(2).is_some()); - subset_access_map.release_access(2); - } - - #[test] - fn subset_map_prevents_access_to_out_of_subset_access() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 1), - }; - - assert!(!subset_access_map.claim_read_access(2)); - assert!(!subset_access_map.claim_write_access(2)); - assert!(!subset_access_map.claim_global_access()); - } - - #[test] - fn subset_map_retains_subset_in_scope() { - let access_map = AccessMap::default(); - let subset_access_map = SubsetAccessMap { - inner: access_map, - subset: Box::new(|id| id == 1), - }; - - subset_access_map.with_scope(|| { - assert!(subset_access_map.claim_read_access(1)); - assert!(!subset_access_map.claim_read_access(2)); - assert!(!subset_access_map.claim_write_access(2)); - }); - - assert!(subset_access_map.claim_read_access(1)); - assert!(!subset_access_map.claim_read_access(2)); - assert!(!subset_access_map.claim_write_access(2)); - } - - #[test] - fn test_hasher_on_u64() { - let mut hasher = SmallIdentityHash::default(); - let value = 42u64; - value.hash(&mut hasher); - assert_eq!(hasher.finish(), 42); - } -} diff --git a/crates/bevy_mod_scripting_bindings/src/allocator.rs b/crates/bevy_mod_scripting_bindings/src/allocator.rs index 2e22e5ba10..e4f1e6b100 100644 --- a/crates/bevy_mod_scripting_bindings/src/allocator.rs +++ b/crates/bevy_mod_scripting_bindings/src/allocator.rs @@ -11,6 +11,7 @@ use bevy_mod_scripting_derive::DebugWithTypeInfo; use bevy_mod_scripting_display::{ DebugWithTypeInfo, DebugWithTypeInfoBuilder, DisplayWithTypeInfo, }; +use bevy_mod_scripting_world::{WorldAccessRange, WorldGuard}; use bevy_platform::collections::HashMap; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::{ @@ -18,6 +19,7 @@ use std::{ cmp::Ordering, fmt::{Display, Formatter}, hash::Hasher, + num::NonZero, sync::{Arc, atomic::AtomicU64}, }; @@ -34,11 +36,21 @@ pub const ALLOCATOR_TOTAL_COLLECTED_DIAG_PATH: DiagnosticPath = #[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")] pub struct ReflectAllocationId(pub(crate) Arc); +impl From<&ReflectAllocationId> for WorldAccessRange { + fn from(val: &ReflectAllocationId) -> Self { + WorldAccessRange::External(unsafe { + // Safety: trivially 1 or more + // if we run out of u64's we have much bigger problems + NonZero::new_unchecked(val.0.checked_add(1).unwrap_or(1)) + }) + } +} + impl DisplayWithTypeInfo for ReflectAllocationId { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + _type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { write!(f, "{}", self.id()) } @@ -119,7 +131,7 @@ impl DebugWithTypeInfo for ReflectAllocation { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { f.debug_tuple_with_type_info("ReflectAllocation", type_info_provider) .field(&((self.0.get() as *mut ()) as usize)) diff --git a/crates/bevy_mod_scripting_bindings/src/docgen/info.rs b/crates/bevy_mod_scripting_bindings/src/docgen/info.rs index bb29b19ebb..e42b8891c7 100644 --- a/crates/bevy_mod_scripting_bindings/src/docgen/info.rs +++ b/crates/bevy_mod_scripting_bindings/src/docgen/info.rs @@ -3,7 +3,8 @@ use crate::function::arg_meta::ArgMeta; use crate::function::namespace::Namespace; use bevy_mod_scripting_derive::DebugWithTypeInfo; -use bevy_mod_scripting_display::{DisplayWithTypeInfo, GetTypeInfo, WithTypeInfo}; +use bevy_mod_scripting_display::{DisplayWithTypeInfo, WithTypeInfo}; +use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::Reflect; use std::{any::TypeId, borrow::Cow}; @@ -133,7 +134,7 @@ impl DisplayWithTypeInfo for FunctionArgInfo { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { if let Some(name) = &self.name { write!(f, "{name}: ")?; @@ -188,7 +189,7 @@ impl DisplayWithTypeInfo for FunctionReturnInfo { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { let type_id = self.type_id; WithTypeInfo::new_with_opt_info(&type_id, type_info_provider) diff --git a/crates/bevy_mod_scripting_bindings/src/error.rs b/crates/bevy_mod_scripting_bindings/src/error.rs index 0a2e73bd39..19a900aa54 100644 --- a/crates/bevy_mod_scripting_bindings/src/error.rs +++ b/crates/bevy_mod_scripting_bindings/src/error.rs @@ -1,14 +1,16 @@ //! Error types for the bindings use crate::{ - FunctionCallContext, Namespace, ReflectBaseType, ReflectReference, access_map::ReflectAccessId, + FunctionCallContext, Namespace, ReflectAllocationId, ReflectBaseType, ReflectReference, script_value::ScriptValue, }; -use bevy_ecs::entity::Entity; +use bevy_ecs::{component::ComponentId, entity::Entity}; use bevy_mod_scripting_asset::Language; use bevy_mod_scripting_derive::DebugWithTypeInfo; use bevy_mod_scripting_display::{ - DebugWithTypeInfo, DisplayWithTypeInfo, GetTypeInfo, OrFakeId, PrintReflectAsDebug, - WithTypeInfo, + DebugWithTypeInfo, DisplayWithTypeInfo, OrFakeId, PrintReflectAsDebug, WithTypeInfo, +}; +use bevy_mod_scripting_world::{ + DynWorldAccessError, WorldAccessGuard, WorldAccessRange, WorldGuard, }; use bevy_reflect::{ApplyError, PartialReflect, Reflect}; use std::{any::TypeId, borrow::Cow, error::Error, fmt::Display, panic::Location, sync::Arc}; @@ -21,7 +23,7 @@ impl bevy_mod_scripting_display::DebugWithTypeInfo for ReflectWrapper { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { PrintReflectAsDebug::new_with_opt_info(&*self.0, type_info_provider) .to_string_with_type_info(f, type_info_provider) @@ -32,7 +34,7 @@ impl DisplayWithTypeInfo for ReflectWrapper { fn display_with_type_info( &self, f: &mut std::fmt::Formatter, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { // TODO: different display? PrintReflectAsDebug::new_with_opt_info(&*self.0, type_info_provider) @@ -48,7 +50,7 @@ impl bevy_mod_scripting_display::DebugWithTypeInfo for ExternalError { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { write!(f, "External error: {}", self.0) } @@ -58,12 +60,93 @@ impl DisplayWithTypeInfo for ExternalError { fn display_with_type_info( &self, f: &mut std::fmt::Formatter, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { write!(f, "External error: {}", self.0) } } +impl From for InteropError { + fn from(value: DynWorldAccessError) -> Self { + match value { + DynWorldAccessError::MissingWorld => Self::MissingWorld, + DynWorldAccessError::CannotClaimAccess(key, location, msg) => { + Self::cannot_claim_access(key, location, msg) + } + DynWorldAccessError::UnregisteredResource(type_id) + | DynWorldAccessError::UnregisteredComponent(type_id) => { + Self::unregistered_component_or_resource_type(type_id) + } + } + } +} + +/// A wrapper around [`WorldAccessRange`] implementing [`DisplayWithTypeInfo`] and family +#[derive(Clone)] +pub struct WorldAccessRangeWithDisplay(WorldAccessRange); + +impl DisplayWithTypeInfo for WorldAccessRangeWithDisplay { + fn display_with_type_info( + &self, + f: &mut std::fmt::Formatter<'_>, + type_info_provider: Option<&WorldAccessGuard>, + ) -> std::fmt::Result { + if let Some(provider) = type_info_provider { + match self.0 { + WorldAccessRange::ComponentOrResource(component_range) => { + f.write_str("Component or Resource: ")?; + let component_id: ComponentId = component_range.into(); + write!( + f, + "{}", + WithTypeInfo::new_with_info(&component_id, provider) + ) + } + WorldAccessRange::External(non_zero) => { + f.write_str("Allocation to: ")?; + + write!( + f, + "{}", + WithTypeInfo::new_with_info( + &ReflectAllocationId::new(non_zero.get()), + provider + ) + ) + } + WorldAccessRange::Global => f.write_str("World Access"), + } + } else { + match self.0 { + WorldAccessRange::ComponentOrResource(component_range) => { + f.write_str("Component or Resource: ")?; + let component_id: ComponentId = component_range.into(); + f.write_str(&component_id.index().to_string()) + } + WorldAccessRange::External(non_zero) => { + f.write_str("Allocation to: ")?; + f.write_str(&non_zero.to_string()) + } + WorldAccessRange::Global => f.write_str("World Access"), + } + } + } +} + +impl DebugWithTypeInfo for WorldAccessRangeWithDisplay { + fn to_string_with_type_info( + &self, + f: &mut std::fmt::Formatter<'_>, + type_info_provider: Option<&WorldAccessGuard>, + ) -> std::fmt::Result { + write!( + f, + "{}", + WithTypeInfo::new_with_opt_info(self, type_info_provider) + ) + } +} + /// An error occurring when converting between rust and a script context. #[derive(Clone, Reflect, DebugWithTypeInfo)] #[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")] @@ -109,7 +192,7 @@ pub enum InteropError { /// Could not claim access to a value CannotClaimAccess { /// The id of the access - base: Box, + base: Box, /// The location of the access location: Box>>, /// The context of the access @@ -119,8 +202,13 @@ pub enum InteropError { Invariant(Box), /// An unregistered component or resource type was used UnregisteredComponentOrResourceType { - /// The name of the type - type_name: Box>, + /// The typeId + type_: Box, + }, + /// A resource is registered, but not inserted + MissingResource { + /// The resource type missing. + type_: Box, }, /// An unsupported operation was performed UnsupportedOperation { @@ -295,12 +383,12 @@ impl InteropError { /// Creates a new cannot claim access error. pub fn cannot_claim_access( - base: ReflectAccessId, + base: WorldAccessRange, location: Option>, context: impl Into>, ) -> Self { Self::CannotClaimAccess { - base: Box::new(base), + base: Box::new(WorldAccessRangeWithDisplay(base)), location: Box::new(location), context: Box::new(context.into()), } @@ -312,11 +400,16 @@ impl InteropError { } /// Creates a new unregistered component or resource type error. - pub fn unregistered_component_or_resource_type( - type_name: impl Into>, - ) -> Self { + pub fn unregistered_component_or_resource_type(type_id: TypeId) -> Self { Self::UnregisteredComponentOrResourceType { - type_name: Box::new(type_name.into()), + type_: Box::new(type_id), + } + } + + /// Creates a new missing resource type error. + pub fn missing_resource(type_id: TypeId) -> Self { + Self::MissingResource { + type_: Box::new(type_id), } } @@ -454,7 +547,7 @@ impl DisplayWithTypeInfo for InteropError { fn display_with_type_info( &self, f: &mut std::fmt::Formatter, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { match self { InteropError::NotImplemented => { @@ -525,8 +618,12 @@ impl DisplayWithTypeInfo for InteropError { InteropError::Invariant(i) => { write!(f, "Invariant broken: {i}") } - InteropError::UnregisteredComponentOrResourceType { type_name } => { - write!(f, "Unregistered component or resource type: {type_name}") + InteropError::UnregisteredComponentOrResourceType { type_ } => { + write!( + f, + "Unregistered component or resource type: {}", + WithTypeInfo::new_with_opt_info(type_, type_info_provider) + ) } InteropError::UnsupportedOperation { base, @@ -663,26 +760,13 @@ impl DisplayWithTypeInfo for InteropError { WithTypeInfo::new_with_opt_info(interop_error, type_info_provider) ) } + InteropError::MissingResource { type_ } => { + write!( + f, + "Resource was not initialized in the world before accessing: {}", + WithTypeInfo::new_with_opt_info(type_, type_info_provider) + ) + } } } } - -#[cfg(test)] -mod test { - use bevy_reflect::TypeRegistry; - - use super::*; - #[test] - fn test_script_value_prints_using_type_data() { - // check script values print fine - let mut registry = TypeRegistry::empty(); - registry.register::(); - pretty_assertions::assert_str_eq!( - format!( - "{:?}", - PrintReflectAsDebug::new_with_opt_info(&ScriptValue::Integer(1), Some(®istry)) - ), - "1", - ); - } -} diff --git a/crates/bevy_mod_scripting_bindings/src/function/from.rs b/crates/bevy_mod_scripting_bindings/src/function/from.rs index 2af619419b..8498e9fafb 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/from.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/from.rs @@ -1,9 +1,9 @@ //! This module contains the [`FromScript`] trait and its implemenations. -use crate::{ - ReflectReference, ScriptValue, WorldGuard, access_map::ReflectAccessId, error::InteropError, - script_value::VariadicTuple, -}; +use super::script_function::{DynamicScriptFunction, DynamicScriptFunctionMut}; +use crate::{ReflectReference, ScriptValue, error::InteropError, script_value::VariadicTuple}; +use bevy_mod_scripting_world::WorldAccessRange; +use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{FromReflect, Reflect}; use nonmax::NonMaxU32; @@ -15,8 +15,6 @@ use std::{ path::PathBuf, }; -use super::script_function::{DynamicScriptFunction, DynamicScriptFunctionMut}; - /// Describes the procedure for constructing a value of type `T` from a [`ScriptValue`]. /// /// The [`FromScript::This`] associated type is used to allow for the implementation of this trait to return @@ -275,24 +273,24 @@ impl FromScript for R<'_, T> { ) -> Result, InteropError> { match value { ScriptValue::Reference(reflect_reference) => { - let raid = ReflectAccessId::for_reference(reflect_reference.base.base_id.clone()); - - if world.claim_read_access(raid) { - // Safety: we just claimed access - let ref_ = unsafe { reflect_reference.reflect_unsafe_non_empty(world) }?; - let cast = ref_.try_downcast_ref::().ok_or_else(|| { - InteropError::type_mismatch( - std::any::TypeId::of::(), - ref_.get_represented_type_info().map(|i| i.type_id()), - ) - })?; - Ok(R(cast)) - } else { - Err(InteropError::cannot_claim_access( + let raid: WorldAccessRange = (&reflect_reference.base.base_id).into(); + match world.claim_read_access(raid) { + Ok(()) => { + // Safety: we just claimed access + let ref_ = unsafe { reflect_reference.reflect_unsafe_non_empty(world) }?; + let cast = ref_.try_downcast_ref::().ok_or_else(|| { + InteropError::type_mismatch( + std::any::TypeId::of::(), + ref_.get_represented_type_info().map(|i| i.type_id()), + ) + })?; + Ok(R(cast)) + } + Err(access) => Err(InteropError::cannot_claim_access( raid, - world.get_access_location(raid), + Some(access.owner.location), format!("In conversion to type: R<{}>", std::any::type_name::()), - )) + )), } } _ => Err(InteropError::value_mismatch( @@ -348,22 +346,24 @@ impl FromScript for M<'_, T> { ) -> Result, InteropError> { match value { ScriptValue::Reference(reflect_reference) => { - let raid = ReflectAccessId::for_reference(reflect_reference.base.base_id.clone()); - - if world.claim_write_access(raid) { - // Safety: we just claimed write access - let ref_ = unsafe { reflect_reference.reflect_mut_unsafe_non_empty(world) }?; - let type_id = ref_.get_represented_type_info().map(|i| i.type_id()); - let cast = ref_.try_downcast_mut::().ok_or_else(|| { - InteropError::type_mismatch(std::any::TypeId::of::(), type_id) - })?; - Ok(M(cast)) - } else { - Err(InteropError::cannot_claim_access( + let raid: WorldAccessRange = (&reflect_reference.base.base_id).into(); + + match world.claim_write_access(raid) { + Ok(()) => { + // Safety: we just claimed write access + let ref_ = + unsafe { reflect_reference.reflect_mut_unsafe_non_empty(world) }?; + let type_id = ref_.get_represented_type_info().map(|i| i.type_id()); + let cast = ref_.try_downcast_mut::().ok_or_else(|| { + InteropError::type_mismatch(std::any::TypeId::of::(), type_id) + })?; + Ok(M(cast)) + } + Err(access) => Err(InteropError::cannot_claim_access( raid, - world.get_access_location(raid), + Some(access.owner.location), format!("In conversion to type: Mut<{}>", std::any::type_name::()), - )) + )), } } _ => Err(InteropError::value_mismatch( @@ -588,6 +588,16 @@ impl Union { Union(Ok(value)) } + /// Returns true if this union represents the left kind of value + pub fn is_left(&self) -> bool { + matches!(self, Union(Ok(_))) + } + + /// Returns true if this union represents the right kind of value + pub fn is_right(&self) -> bool { + matches!(self, Union(Err(_))) + } + /// Create a new union with the right value. pub fn new_right(value: T2) -> Self { Union(Err(value)) diff --git a/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs b/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs index fee1a137fb..94be0ede7a 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs @@ -1,9 +1,10 @@ //! Contains the [`FromScriptRef`] trait and its implementations. use crate::{ - FromScript, ScriptValue, WorldGuard, error::InteropError, match_by_type, + FromScript, ScriptValue, error::InteropError, match_by_type, reflection_extensions::TypeInfoExtensions, }; +use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::{ DynamicEnum, DynamicList, DynamicMap, DynamicSet, DynamicTuple, DynamicVariant, Map, PartialReflect, ReflectKind, Set, @@ -74,7 +75,7 @@ impl FromScriptRef for Box { let mut dynamic_enum = match value { ScriptValue::Unit => DynamicEnum::new("None", DynamicVariant::Unit), _ => { - let inner = Self::from_script_ref(inner_option_type, value, world)?; + let inner = Self::from_script_ref(inner_option_type, value, world.clone())?; DynamicEnum::new( "Some", DynamicVariant::Tuple(DynamicTuple::from_iter(vec![inner])), @@ -135,7 +136,9 @@ impl FromScriptRef for Box { } match value { - ScriptValue::Reference(reflect_reference) => reflect_reference.to_owned_value(world), + ScriptValue::Reference(reflect_reference) => { + reflect_reference.to_owned_value(world.clone()) + } value => Err(InteropError::value_mismatch(target, value)), } } diff --git a/crates/bevy_mod_scripting_bindings/src/function/into.rs b/crates/bevy_mod_scripting_bindings/src/function/into.rs index 582bcd5edc..76168ef005 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/into.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/into.rs @@ -1,7 +1,8 @@ //! Implementations of the [`IntoScript`] trait for various types. use super::{DynamicScriptFunction, DynamicScriptFunctionMut, Union, V}; -use crate::{ReflectReference, ScriptValue, VariadicTuple, WorldGuard, error::InteropError}; +use crate::{ReflectReference, ScriptValue, VariadicTuple, WorldExtensions, error::InteropError}; +use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; use std::{borrow::Cow, collections::VecDeque, ffi::OsString, path::PathBuf}; diff --git a/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs b/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs index 411de29482..c3c8cb6dd6 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs @@ -3,11 +3,12 @@ use std::{borrow::Cow, ffi::OsString, path::PathBuf}; use bevy_mod_scripting_display::OrFakeId; +use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::PartialReflect; use crate::{ - ReferencePart, ReflectReference, ScriptValue, WorldGuard, error::InteropError, - function::into::IntoScript, reflection_extensions::PartialReflectExt, + ReferencePart, ReflectReference, ScriptValue, error::InteropError, function::into::IntoScript, + reflection_extensions::PartialReflectExt, }; /// Converts a value represented by a reference into a [`crate::ScriptValue`]. diff --git a/crates/bevy_mod_scripting_bindings/src/function/namespace.rs b/crates/bevy_mod_scripting_bindings/src/function/namespace.rs index f61aa44fd0..cbe0519e8b 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/namespace.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/namespace.rs @@ -8,7 +8,8 @@ use crate::{ use ::bevy_reflect::{GetTypeRegistration, Reflect}; use bevy_ecs::{reflect::AppTypeRegistry, world::World}; use bevy_mod_scripting_derive::DebugWithTypeInfo; -use bevy_mod_scripting_display::{DisplayWithTypeInfo, GetTypeInfo, WithTypeInfo}; +use bevy_mod_scripting_display::{DisplayWithTypeInfo, WithTypeInfo}; +use bevy_mod_scripting_world::WorldGuard; use std::{any::TypeId, borrow::Cow, marker::PhantomData}; use super::type_dependencies::GetFunctionTypeDependencies; @@ -188,7 +189,7 @@ impl DisplayWithTypeInfo for Namespace { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { match self { Namespace::Global => f.write_str("Global Namespace"), diff --git a/crates/bevy_mod_scripting_bindings/src/function/script_function.rs b/crates/bevy_mod_scripting_bindings/src/function/script_function.rs index b9ccc2a8f4..152f7ebb9d 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/script_function.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/script_function.rs @@ -5,11 +5,12 @@ use super::{from::FromScript, into::IntoScript, namespace::Namespace}; use crate::VariadicTuple; use crate::docgen::info::{FunctionInfo, GetFunctionInfo}; use crate::function::arg_meta::ArgMeta; -use crate::{ScriptValue, ThreadWorldContainer, WorldGuard, error::InteropError}; +use crate::{ScriptValue, error::InteropError}; use bevy_ecs::prelude::Resource; use bevy_mod_scripting_asset::Language; use bevy_mod_scripting_derive::DebugWithTypeInfo; -use bevy_mod_scripting_display::{DisplayWithTypeInfo, GetTypeInfo}; +use bevy_mod_scripting_display::DisplayWithTypeInfo; +use bevy_mod_scripting_world::{ThreadWorldContainer, WorldGuard}; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -18,6 +19,7 @@ use std::collections::VecDeque; use std::hash::Hash; use std::ops::{Deref, DerefMut}; use std::sync::Arc; + #[diagnostic::on_unimplemented( message = "This function does not fulfil the requirements to be a script callable function. All arguments must implement the ScriptArgument trait and all return values must implement the ScriptReturn trait" )] @@ -104,7 +106,7 @@ impl FunctionCallContext { /// Tries to access the world, returning an error if the world is not available #[profiling::function] pub fn world<'l>(&self) -> Result, InteropError> { - ThreadWorldContainer.try_get_context().map(|c| c.world) + Ok(ThreadWorldContainer.try_get_context().map(|c| c.world)?) } /// Whether the caller uses 1-indexing on all indexes and expects 0-indexing conversions to be performed. #[profiling::function] @@ -161,7 +163,7 @@ impl DisplayWithTypeInfo for DynamicScriptFunction { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { f.write_str("fn ")?; let name = &self.info.name; @@ -182,7 +184,7 @@ impl DisplayWithTypeInfo for DynamicScriptFunctionMut { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { f.write_str("fn mut ")?; let name = &self.info.name; @@ -730,20 +732,17 @@ variadics_please::all_tuples!(impl_script_function, 0, 13, T); #[cfg(test)] mod test { + use crate::{CurrentScriptAttachment, WorldExtensions}; + use super::*; - use bevy_asset::Handle; use bevy_ecs::{prelude::Component, world::World}; - use bevy_mod_scripting_script::ScriptAttachment; + use bevy_mod_scripting_world::{ThreadScriptContext, WorldAccessGuard}; fn with_local_world(f: F) { let mut world = World::default(); - WorldGuard::with_static_guard(&mut world, |world| { - ThreadWorldContainer - .set_context(crate::ThreadScriptContext { - world, - attachment: ScriptAttachment::StaticScript(Handle::default()), - }) - .unwrap(); + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); + WorldGuard::with_static_guard(&mut world, cache, |world| { + ThreadWorldContainer.set_context(ThreadScriptContext { world }); f() }); } diff --git a/crates/bevy_mod_scripting_bindings/src/globals/core.rs b/crates/bevy_mod_scripting_bindings/src/globals/core.rs index 48f5b5c506..5cf7ba52ff 100644 --- a/crates/bevy_mod_scripting_bindings/src/globals/core.rs +++ b/crates/bevy_mod_scripting_bindings/src/globals/core.rs @@ -10,11 +10,13 @@ use bevy_app::App; use bevy_log::{warn, warn_once}; use bevy_mod_scripting_asset::ScriptAsset; use bevy_mod_scripting_derive::script_globals; +use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::HashMap; use std::{cell::RefCell, sync::Arc}; use crate::{ - ScriptComponentRegistration, ScriptResourceRegistration, ScriptTypeRegistration, WorldGuard, + ScriptComponentRegistration, ScriptResourceRegistration, ScriptTypeRegistration, + WorldExtensions, function::from::{Union, V}, }; use crate::{docgen::into_through_type_info, error::InteropError}; diff --git a/crates/bevy_mod_scripting_bindings/src/globals/mod.rs b/crates/bevy_mod_scripting_bindings/src/globals/mod.rs index bdabd4dcec..b3f6109e21 100644 --- a/crates/bevy_mod_scripting_bindings/src/globals/mod.rs +++ b/crates/bevy_mod_scripting_bindings/src/globals/mod.rs @@ -1,7 +1,6 @@ //! Contains abstractions for exposing "globals" to scripts, in a language-agnostic way. use super::{ - WorldGuard, function::arg_meta::{ScriptReturn, TypedScriptReturn}, script_value::ScriptValue, }; @@ -9,6 +8,7 @@ use crate::{ docgen::{TypedThrough, into_through_type_info, typed_through::ThroughTypeInfo}, error::InteropError, }; +use bevy_mod_scripting_world::WorldGuard; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::{any::TypeId, borrow::Cow, sync::Arc}; use {bevy_ecs::resource::Resource, bevy_platform::collections::HashMap, bevy_reflect::Typed}; @@ -260,6 +260,8 @@ impl ScriptGlobalsRegistry { #[cfg(test)] mod test { + use std::{any::Any, cell::RefCell, rc::Rc}; + use bevy_ecs::world::World; use super::*; @@ -279,7 +281,8 @@ mod test { assert_eq!( (registry.get("foo").unwrap().maker.clone().unwrap())(WorldGuard::new_exclusive( - &mut World::new() + &mut World::new(), + std::array::from_fn(|_| Rc::new(RefCell::new(())) as Rc>) )) .unwrap(), ScriptValue::from(42) @@ -290,7 +293,8 @@ mod test { assert_eq!( (registry.get("foo").unwrap().maker.clone().unwrap())(WorldGuard::new_exclusive( - &mut World::new() + &mut World::new(), + std::array::from_fn(|_| Rc::new(RefCell::new(())) as Rc>) )) .unwrap(), ScriptValue::from(43) diff --git a/crates/bevy_mod_scripting_bindings/src/lib.rs b/crates/bevy_mod_scripting_bindings/src/lib.rs index 8182b8b5f0..477795c7a6 100644 --- a/crates/bevy_mod_scripting_bindings/src/lib.rs +++ b/crates/bevy_mod_scripting_bindings/src/lib.rs @@ -1,6 +1,5 @@ //! Abstractions to help with creating bindings between bevy and scripting languages. -pub mod access_map; pub mod allocator; pub mod conversions; pub mod docgen; @@ -15,9 +14,8 @@ pub mod schedule; pub mod script_component; pub mod script_value; pub mod type_data; -pub mod world; +pub mod world_extensions; -pub use access_map::*; pub use allocator::*; pub use docgen::*; pub use error::*; @@ -33,4 +31,4 @@ pub use schedule::*; pub use script_component::*; pub use script_value::*; pub use type_data::*; -pub use world::*; +pub use world_extensions::*; diff --git a/crates/bevy_mod_scripting_bindings/src/path/mod.rs b/crates/bevy_mod_scripting_bindings/src/path/mod.rs index e8bbed1c94..6ae6ddeeca 100644 --- a/crates/bevy_mod_scripting_bindings/src/path/mod.rs +++ b/crates/bevy_mod_scripting_bindings/src/path/mod.rs @@ -4,10 +4,11 @@ use std::{borrow::Cow, fmt::Display}; use bevy_mod_scripting_asset::Language; use bevy_mod_scripting_derive::DebugWithTypeInfo; -use bevy_mod_scripting_display::{DisplayWithTypeInfo, GetTypeInfo}; +use bevy_mod_scripting_display::DisplayWithTypeInfo; +use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::{PartialReflect, ReflectMut, ReflectRef, TypeInfo, TypeRegistry}; -use crate::{ScriptValue, WorldGuard, convert}; +use crate::{ScriptValue, convert}; /// A key referencing into a `Reflect` supporting trait object. #[derive(DebugWithTypeInfo)] @@ -296,7 +297,7 @@ impl DisplayWithTypeInfo for ReferencePath { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { std::fmt::Display::fmt(self, f) } diff --git a/crates/bevy_mod_scripting_bindings/src/query.rs b/crates/bevy_mod_scripting_bindings/src/query.rs index 16623456e0..fb17db5a95 100644 --- a/crates/bevy_mod_scripting_bindings/src/query.rs +++ b/crates/bevy_mod_scripting_bindings/src/query.rs @@ -1,9 +1,10 @@ //! Utilities for querying the world. -use bevy_ecs::{ptr::OwningPtr, query::QueryBuilder, world::EntityRef}; +use bevy_ecs::{ptr::OwningPtr, query::QueryBuilder}; +use bevy_mod_scripting_world::WorldGuard; -use super::{DynamicComponent, ReflectReference, WorldAccessGuard, WorldGuard, with_global_access}; -use crate::error::InteropError; +use super::{DynamicComponent, ReflectReference}; +use crate::{WorldExtensions, error::InteropError}; use ::{ bevy_ecs::{ component::ComponentId, @@ -14,7 +15,7 @@ use ::{ }, bevy_reflect::{Reflect, TypeRegistration}, }; -use std::{any::TypeId, collections::VecDeque, ptr::NonNull, sync::Arc}; +use std::{any::TypeId, ptr::NonNull, sync::Arc}; /// A reference to a type which is not a `Resource` or `Component`. /// @@ -143,7 +144,7 @@ impl ScriptComponentRegistration { world: WorldGuard, entity: Entity, ) -> Result<(), InteropError> { - world.with_global_access(|world| { + world.with_world_mut(|world| { let mut entity = world .get_entity_mut(entity) .map_err(|_| InteropError::missing_entity(entity))?; @@ -165,7 +166,7 @@ impl ScriptComponentRegistration { // if dynamic we already know the type i.e. `ScriptComponent` // so we can just insert it - world.with_global_access(|world| { + world.with_world_mut(|world| { let mut entity = world .get_entity_mut(entity) .map_err(|_| InteropError::missing_entity(entity))?; @@ -203,7 +204,7 @@ impl ScriptComponentRegistration { // TODO: this shouldn't need entire world access it feels let type_registry = world.type_registry(); - world.with_global_access(|world| { + world.with_world_mut(|world| { let mut entity = world .get_entity_mut(entity) .map_err(|_| InteropError::missing_entity(entity))?; @@ -263,6 +264,11 @@ pub struct ScriptQueryBuilder { #[profiling::all_functions] impl ScriptQueryBuilder { + /// Creates an empty query builder + pub fn new() -> Self { + Default::default() + } + /// Adds components to the query. pub fn components(&mut self, components: Vec) -> &mut Self { self.components.extend(components); @@ -329,38 +335,3 @@ pub struct ScriptQueryResult { /// The components that matched the query. pub components: Vec, } - -#[profiling::all_functions] -impl WorldAccessGuard<'_> { - /// Queries the world for entities that match the given query. - pub fn query( - &self, - query: ScriptQueryBuilder, - ) -> Result, InteropError> { - with_global_access!(&self.inner.accesses, "Could not query", { - let world = unsafe { self.as_unsafe_world_cell()?.world_mut() }; - let mut built_query = query.as_query_state::(world); - let query_result = built_query.iter(world); - - Ok(query_result - .map(|r| { - let references: Vec<_> = query - .components - .iter() - .map(|c| ReflectReference { - base: super::ReflectBaseType { - type_id: c.type_registration().type_id(), - base_id: super::ReflectBase::Component(r.id(), c.component_id()), - }, - reflect_path: Default::default(), - }) - .collect(); - ScriptQueryResult { - entity: r.id(), - components: references, - } - }) - .collect()) - })? - } -} diff --git a/crates/bevy_mod_scripting_bindings/src/reference.rs b/crates/bevy_mod_scripting_bindings/src/reference.rs index 6478d310d5..403d3ab6d4 100644 --- a/crates/bevy_mod_scripting_bindings/src/reference.rs +++ b/crates/bevy_mod_scripting_bindings/src/reference.rs @@ -4,11 +4,9 @@ //! reflection gives us access to `dyn PartialReflect` objects via their type name, //! Scripting languages only really support `Clone` objects so if we want to support references, //! we need wrapper types which have owned and ref variants. -use super::{WorldGuard, access_map::ReflectAccessId}; use crate::{ - ReferencePart, ReferencePath, ReflectAllocationId, ReflectAllocator, ThreadWorldContainer, - error::InteropError, reflection_extensions::PartialReflectExt, with_access_read, - with_access_write, + ReferencePart, ReferencePath, ReflectAllocationId, ReflectAllocator, WorldExtensions, + error::InteropError, reflection_extensions::PartialReflectExt, }; use bevy_asset::{ReflectAsset, UntypedHandle}; use bevy_ecs::{component::Component, ptr::Ptr, resource::Resource}; @@ -16,11 +14,9 @@ use bevy_mod_scripting_derive::DebugWithTypeInfo; use bevy_mod_scripting_display::{ DebugWithTypeInfo, DisplayWithTypeInfo, OrFakeId, PrintReflectAsDebug, WithTypeInfo, }; +use bevy_mod_scripting_world::{WorldAccessGuard, WorldAccessRange, WorldGuard}; use bevy_reflect::{Access, OffsetAccess, ReflectRef, TypeRegistry}; -use std::{ - any::{Any, TypeId}, - fmt::Debug, -}; +use std::{any::TypeId, fmt::Debug}; use { bevy_ecs::{ change_detection::MutUntyped, component::ComponentId, entity::Entity, @@ -55,25 +51,17 @@ impl DisplayWithTypeInfo for ReflectReference { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { // try to display the most information we can, the type info provider happens to be the world guard, we can // actually display the reference - if let Some(type_info_provider) = type_info_provider { + if let Some(guard) = type_info_provider { // Safety: should be safe as the guard is invalidated when world is released per iteration - let any: &dyn Any = unsafe { type_info_provider.as_any_static() }; - - let guard = any.downcast_ref::().cloned().or_else(|| { - any.downcast_ref::() - .and_then(|t| t.try_get_context().ok().map(|c| c.world)) - }); - if let Some(guard) = guard - && let Ok(r) = self.with_reflect(guard.clone(), |s| { - PrintReflectAsDebug::new_with_opt_info(s, Some(type_info_provider)) - .to_string_with_type_info(f, Some(type_info_provider)) - }) - { + if let Ok(r) = self.with_reflect(guard.clone(), |s| { + PrintReflectAsDebug::new_with_opt_info(s, Some(guard)) + .to_string_with_type_info(f, Some(guard)) + }) { return r; } } @@ -378,15 +366,14 @@ impl ReflectReference { .remove(id) .ok_or_else(|| InteropError::garbage_collected_allocation(self.clone()))?; - let access_id = ReflectAccessId::for_allocation(id.clone()); - if world.claim_write_access(access_id) { + if let Ok(()) = world.claim_write_access(id) { // Safety: we claim write access, nobody else is accessing this if unsafe { &*arc.get_ptr() }.try_as_reflect().is_some() { // Safety: the only accesses exist in this function - unsafe { world.release_access(access_id) }; + unsafe { world.release_access(id) }; return Ok(unsafe { arc.take() }); } else { - unsafe { world.release_access(access_id) }; + unsafe { world.release_access(id) }; } } allocator.insert(id.clone(), arc); @@ -405,22 +392,15 @@ impl ReflectReference { world: WorldGuard, f: F, ) -> Result { - let access_id = ReflectAccessId::for_reference(self.base.base_id.clone()); - with_access_read!( - &world.inner.accesses, - access_id, - "could not access reflect reference", - { - f( - unsafe { self.reflect_unsafe(world.clone()) }?.ok_or_else(|| { - InteropError::reflection_path_error( - "Reference was out of bounds or value is missing".into(), - Some(self.clone()), - ) - })?, - ) - } - ) + world.with_read_access_and_then(&self.base.base_id, || { + Ok(f(unsafe { self.reflect_unsafe(world.clone()) }? + .ok_or_else(|| { + InteropError::reflection_path_error( + "Reference was out of bounds or value is missing".into(), + Some(self.clone()), + ) + })?)) + }) } /// The way to access the value of the reference, that is the pointed-to value. @@ -431,22 +411,15 @@ impl ReflectReference { world: WorldGuard, f: F, ) -> Result { - let access_id = ReflectAccessId::for_reference(self.base.base_id.clone()); - with_access_write!( - &world.inner.accesses, - access_id, - "Could not access reflect reference mutably", - { - f( - unsafe { self.reflect_mut_unsafe(world.clone()) }?.ok_or_else(|| { - InteropError::reflection_path_error( - "Reference was out of bounds or value is missing".into(), - Some(self.clone()), - ) - })?, - ) - } - ) + world.with_write_access_and_then(&self.base.base_id, || { + Ok(f(unsafe { self.reflect_mut_unsafe(world.clone()) }? + .ok_or_else(|| { + InteropError::reflection_path_error( + "Reference was out of bounds or value is missing".into(), + Some(self.clone()), + ) + })?)) + }) } /// Retrieves the type id of the value the reference points to. @@ -675,7 +648,7 @@ impl DisplayWithTypeInfo for ReflectBaseType { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { f.write_str("base type: ")?; WithTypeInfo::new_with_opt_info(&self.type_id, type_info_provider) @@ -698,19 +671,29 @@ impl ReflectBaseType { entity: Entity, world: WorldGuard, ) -> Result { - let reflect_id = ReflectAccessId::for_component::(&world.as_unsafe_world_cell()?)?; + let type_id = TypeId::of::(); + Ok(Self { - type_id: TypeId::of::(), - base_id: ReflectBase::Component(entity, reflect_id.into()), + type_id, + base_id: ReflectBase::Component( + entity, + world.get_component_id(type_id)?.ok_or_else(|| { + InteropError::unregistered_component_or_resource_type(type_id) + })?, + ), }) } /// Create a new reflection base pointing to a resource pub fn new_resource_base(world: WorldGuard) -> Result { - let reflect_id = ReflectAccessId::for_resource::(&world.as_unsafe_world_cell()?)?; + let type_id = TypeId::of::(); Ok(Self { - type_id: TypeId::of::(), - base_id: ReflectBase::Resource(reflect_id.into()), + type_id, + base_id: ReflectBase::Resource( + world.get_resource_id(type_id)?.ok_or_else(|| { + InteropError::unregistered_component_or_resource_type(type_id) + })?, + ), }) } @@ -801,11 +784,24 @@ pub enum ReflectBase { Asset(UntypedHandle, ComponentId), } +impl From<&ReflectBase> for WorldAccessRange { + fn from(val: &ReflectBase) -> Self { + match val { + ReflectBase::Component(_, component_id) + | ReflectBase::Resource(component_id) + | ReflectBase::Asset(_, component_id) => { + WorldAccessRange::ComponentOrResource((*component_id).into()) + } + ReflectBase::Owned(reflect_allocation_id) => reflect_allocation_id.into(), + } + } +} + impl DisplayWithTypeInfo for ReflectBase { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { match self { ReflectBase::Component(entity, component_id) => { @@ -821,30 +817,17 @@ impl DisplayWithTypeInfo for ReflectBase { .display_with_type_info(f, type_info_provider) } ReflectBase::Owned(id) => { - if let Some(type_info_provider) = type_info_provider { - // Safety: should generally be safe, as the world guard is invalidated once the world is out of scope for the iteration - let any: &dyn Any = unsafe { type_info_provider.as_any_static() }; - - let guard = any.downcast_ref::().cloned().or_else(|| { - any.downcast_ref::() - .and_then(|t| t.try_get_context().ok().map(|c| c.world)) - }); - - if let Some(guard) = guard { - let allocator = guard.allocator(); - let allocator = allocator.read(); - if let Some(allocation) = allocator.get(id) { - let ptr = allocation.get_ptr(); - if let Ok(v) = guard.with_read_access(id.clone(), |_| { - // Safety:: have access to this id - PrintReflectAsDebug::new_with_opt_info( - unsafe { &*ptr }, - Some(type_info_provider), - ) - .to_string_with_type_info(f, Some(type_info_provider)) - }) { - return v; - } + if let Some(guard) = type_info_provider { + let allocator = guard.allocator(); + let allocator = allocator.read(); + if let Some(allocation) = allocator.get(id) { + let ptr = allocation.get_ptr(); + if let Ok(v) = guard.with_read_access(id, || { + // Safety:: have access to this id + PrintReflectAsDebug::new_with_opt_info(unsafe { &*ptr }, Some(guard)) + .to_string_with_type_info(f, Some(guard)) + }) { + return v; } } } @@ -1013,7 +996,10 @@ mod test { component::Component, reflect::AppTypeRegistry, resource::Resource, world::World, }; - use crate::{AppReflectAllocator, function::script_function::AppScriptFunctionRegistry}; + use crate::{ + AppReflectAllocator, CurrentScriptAttachment, WorldExtensions, + function::script_function::AppScriptFunctionRegistry, + }; use super::*; @@ -1047,12 +1033,13 @@ mod test { #[test] fn test_component_ref() { let mut world = setup_world(); + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); let entity = world .spawn(TestComponent(vec!["hello".to_owned(), "world".to_owned()])) .id(); - let world_guard = WorldGuard::new_exclusive(&mut world); + let world_guard = WorldGuard::new_exclusive(&mut world, cache); let mut component_ref = ReflectReference::new_component_ref::(entity, world_guard.clone()) @@ -1132,10 +1119,10 @@ mod test { #[test] fn test_resource_ref() { let mut world = setup_world(); - + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); world.insert_resource(TestResource(vec!["hello".to_owned(), "world".to_owned()])); - let world_guard = WorldGuard::new_exclusive(&mut world); + let world_guard = WorldGuard::new_exclusive(&mut world, cache); let mut resource_ref = ReflectReference::new_resource_ref::(world_guard.clone()) @@ -1216,10 +1203,11 @@ mod test { #[test] fn test_allocation_ref() { let mut world = setup_world(); + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); let value: TestComponent = TestComponent(vec!["hello".to_owned(), "world".to_owned()]); - let world_guard = WorldGuard::new_exclusive(&mut world); + let world_guard = WorldGuard::new_exclusive(&mut world, cache); let allocator = world_guard.allocator(); let mut allocator_write = allocator.write(); let mut allocation_ref = ReflectReference::new_allocated(value, &mut allocator_write); diff --git a/crates/bevy_mod_scripting_bindings/src/reflection_extensions.rs b/crates/bevy_mod_scripting_bindings/src/reflection_extensions.rs index 72506cc495..ddad55a998 100644 --- a/crates/bevy_mod_scripting_bindings/src/reflection_extensions.rs +++ b/crates/bevy_mod_scripting_bindings/src/reflection_extensions.rs @@ -5,9 +5,10 @@ use std::{ cmp::max, }; +use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::{PartialReflect, Reflect, ReflectFromReflect, ReflectMut, ReflectRef, TypeInfo}; -use crate::{ReflectReference, WorldGuard, error::InteropError}; +use crate::{ReflectReference, WorldExtensions, error::InteropError}; /// Extension trait for [`PartialReflect`] providing additional functionality for working with specific types. pub trait PartialReflectExt { diff --git a/crates/bevy_mod_scripting_bindings/src/schedule.rs b/crates/bevy_mod_scripting_bindings/src/schedule.rs index dff25ee5ba..27a17ce89a 100644 --- a/crates/bevy_mod_scripting_bindings/src/schedule.rs +++ b/crates/bevy_mod_scripting_bindings/src/schedule.rs @@ -1,20 +1,15 @@ //! Dynamic scheduling from scripts -use super::WorldAccessGuard; -use crate::error::InteropError; use ::{ bevy_app::{ First, FixedFirst, FixedLast, FixedMain, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, PostStartup, PostUpdate, PreStartup, PreUpdate, RunFixedMainLoop, Startup, Update, }, - bevy_ecs::{ - schedule::{Schedule, ScheduleLabel, Schedules}, - world::World, - }, + bevy_ecs::schedule::ScheduleLabel, }; use bevy_ecs::resource::Resource; use bevy_platform::collections::HashMap; -use bevy_system_reflection::{ReflectSchedule, ReflectSystem}; +use bevy_system_reflection::ReflectSchedule; use parking_lot::RwLock; use std::{any::TypeId, sync::Arc}; #[derive(Default, Clone, Resource)] @@ -108,87 +103,6 @@ impl ScheduleRegistry { } } -#[profiling::all_functions] -/// Impls to do with dynamically querying systems and schedules -impl WorldAccessGuard<'_> { - /// Temporarilly removes the given schedule from the world, and calls the given function on it, then re-inserts it. - /// - /// Useful for initializing schedules, or modifying systems - pub fn scope_schedule O>( - &self, - label: &ReflectSchedule, - f: F, - ) -> Result { - self.with_global_access(|world| { - let mut schedules = world.get_resource_mut::().ok_or_else(|| { - InteropError::unsupported_operation( - None, - None, - "accessing schedules in a world with no schedules", - ) - })?; - - let mut removed_schedule = schedules - .remove(*label.label()) - .ok_or_else(|| InteropError::missing_schedule(label.identifier()))?; - - let result = f(world, &mut removed_schedule); - - let mut schedules = world.get_resource_mut::().ok_or_else(|| { - InteropError::unsupported_operation( - None, - None, - "removing `Schedules` resource within a schedule scope", - ) - })?; - - assert!( - removed_schedule.label() == *label.label(), - "removed schedule label doesn't match the original" - ); - schedules.insert(removed_schedule); - - Ok(result) - })? - } - - /// Retrieves all the systems in a schedule - pub fn systems(&self, schedule: &ReflectSchedule) -> Result, InteropError> { - self.with_resource(|schedules: &Schedules| { - let schedule = schedules - .get(*schedule.label()) - .ok_or_else(|| InteropError::missing_schedule(schedule.identifier()))?; - - let systems = schedule.systems().map_err(|_| { - InteropError::string(format!( - "failed to get systems from schedule '{:?}', schedule is not initialized.", - schedule.label() - )) - })?; - - Ok(systems - .map(|(node_id, system)| ReflectSystem::from_system(system.as_ref(), node_id)) - .collect()) - })? - } - - // /// Creates a system from a system builder and inserts it into the given schedule - // pub fn add_system( - // &self, - // schedule: &ReflectSchedule, - // builder: ScriptSystemBuilder, - // ) -> Result { - // debug!( - // "Adding script system '{}' for script '{}' to schedule '{}'", - // builder.name, - // builder.attachment, - // schedule.identifier() - // ); - - // builder.build::

(self.clone(), schedule) - // } -} - #[cfg(test)] #[allow( dead_code, @@ -204,6 +118,7 @@ mod tests { schedule::{NodeId, Schedules, SystemKey}, system::IntoSystem, }, + bevy_system_reflection::ReflectSystem, std::{cell::OnceCell, rc::Rc}, }; diff --git a/crates/bevy_mod_scripting_bindings/src/script_component.rs b/crates/bevy_mod_scripting_bindings/src/script_component.rs index 3e3e9a85e6..b1b7a4e31e 100644 --- a/crates/bevy_mod_scripting_bindings/src/script_component.rs +++ b/crates/bevy_mod_scripting_bindings/src/script_component.rs @@ -1,18 +1,16 @@ //! Everything necessary to support scripts registering their own components -use super::{ScriptComponentRegistration, ScriptTypeRegistration, ScriptValue, WorldAccessGuard}; -use crate::error::InteropError; +use super::{ScriptComponentRegistration, ScriptValue}; use ::{ bevy_app::{App, Plugin}, - bevy_ecs::component::{ - Component, ComponentCloneBehavior, ComponentDescriptor, Mutable, StorageType, - }, - bevy_reflect::{GetTypeRegistration, Reflect, prelude::ReflectDefault}, + bevy_ecs::component::{Component, Mutable, StorageType}, + bevy_reflect::Reflect, }; use bevy_ecs::resource::Resource; use bevy_platform::collections::HashMap; +use bevy_reflect::std_traits::ReflectDefault; use parking_lot::RwLock; -use std::{alloc::Layout, mem::needs_drop, sync::Arc}; +use std::sync::Arc; /// A dynamic script component #[derive(Reflect, Clone, Default)] #[reflect(Default)] @@ -69,63 +67,6 @@ impl ScriptComponentRegistry { } } -#[profiling::all_functions] -impl WorldAccessGuard<'_> { - /// Registers a dynamic script component, and returns a reference to its registration - pub fn register_script_component( - &self, - component_name: String, - ) -> Result { - let component_registry = self.component_registry(); - let component_registry_read = component_registry.read(); - if component_registry_read.get(&component_name).is_some() { - return Err(InteropError::unsupported_operation( - None, - None, - "script registered component already exists", - )); - } - - let component_id = self.with_global_access(|w| { - let descriptor = unsafe { - // Safety: same safety guarantees as ComponentDescriptor::new - // we know the type in advance - // we only use this method to name the component - ComponentDescriptor::new_with_layout( - component_name.clone(), - DynamicComponent::STORAGE_TYPE, - Layout::new::(), - needs_drop::().then_some(|x| x.drop_as::()), - true, - ComponentCloneBehavior::Default, - None, - ) - }; - w.register_component_with_descriptor(descriptor) - })?; - drop(component_registry_read); - let mut component_registry = component_registry.write(); - - let registration = ScriptComponentRegistration::new( - ScriptTypeRegistration::new(Arc::new( - ::get_type_registration(), - )), - component_id, - ); - - let component_info = DynamicComponentInfo { - name: component_name.clone(), - registration: registration.clone(), - }; - - component_registry.register(component_info); - - // TODO: we should probably retrieve this from the registry, but I don't see what people would want to register on this type - // in addition to the existing registrations. - Ok(registration) - } -} - /// A plugin to support dynamic script components pub struct DynamicScriptComponentPlugin; @@ -139,14 +80,19 @@ impl Plugin for DynamicScriptComponentPlugin { #[cfg(test)] mod test { use bevy_ecs::world::World; + use bevy_mod_scripting_world::{WorldAccessGuard, WorldGuard}; + + use crate::{CurrentScriptAttachment, WorldExtensions}; use super::*; #[test] fn test_script_component() { let mut world = World::new(); + world.init_resource::(); + let cache = WorldGuard::setup_cache(&world, CurrentScriptAttachment::default()); let registration = { - let guard = WorldAccessGuard::new_exclusive(&mut world); + let guard = WorldAccessGuard::new_exclusive(&mut world, cache); guard .register_script_component("ScriptTest".to_string()) diff --git a/crates/bevy_mod_scripting_bindings/src/script_value.rs b/crates/bevy_mod_scripting_bindings/src/script_value.rs index b146de89c3..1069295812 100644 --- a/crates/bevy_mod_scripting_bindings/src/script_value.rs +++ b/crates/bevy_mod_scripting_bindings/src/script_value.rs @@ -2,9 +2,8 @@ use crate::error::InteropError; use bevy_mod_scripting_derive::DebugWithTypeInfo; -use bevy_mod_scripting_display::{ - DisplayWithTypeInfo, GetTypeInfo, ReflectDisplayWithTypeInfo, WithTypeInfo, -}; +use bevy_mod_scripting_display::{DisplayWithTypeInfo, ReflectDisplayWithTypeInfo, WithTypeInfo}; +use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; use std::{borrow::Cow, collections::VecDeque}; @@ -58,7 +57,7 @@ impl DisplayWithTypeInfo for ScriptValue { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { match self { ScriptValue::Unit => f.write_str("()"), diff --git a/crates/bevy_mod_scripting_bindings/src/world.rs b/crates/bevy_mod_scripting_bindings/src/world.rs deleted file mode 100644 index b4edbd22f9..0000000000 --- a/crates/bevy_mod_scripting_bindings/src/world.rs +++ /dev/null @@ -1,1649 +0,0 @@ -//! # Motivation -//! -//! Traits and structs needed to support the creation of bindings for scripting languages. -//! reflection gives us access to `dyn PartialReflect` objects via their type name, -//! Scripting languages only really support `Clone` objects so if we want to support references, -//! we need wrapper types which have owned and ref variants. - -use super::{ - AppReflectAllocator, AppScriptComponentRegistry, ReflectBase, ReflectBaseType, - ReflectReference, ScriptComponentRegistration, ScriptResourceRegistration, - ScriptTypeRegistration, Union, - access_map::{ - AccessCount, AccessMapKey, AnyAccessMap, DynamicSystemMeta, ReflectAccessId, - ReflectAccessKind, SubsetAccessMap, - }, - function::{ - namespace::Namespace, - script_function::{AppScriptFunctionRegistry, DynamicScriptFunction, FunctionCallContext}, - }, - schedule::AppScheduleRegistry, - script_value::ScriptValue, - with_global_access, -}; -use crate::{ - error::InteropError, - function::{from::FromScript, from_ref::FromScriptRef}, - reflection_extensions::PartialReflectExt, - with_access_read, with_access_write, -}; -use ::{ - bevy_app::AppExit, - bevy_asset::{AssetServer, Handle, LoadState}, - bevy_ecs::{ - component::{Component, ComponentId}, - entity::Entity, - prelude::Resource, - reflect::{AppTypeRegistry, ReflectFromWorld, ReflectResource}, - system::Commands, - world::{CommandQueue, Mut, World, unsafe_world_cell::UnsafeWorldCell}, - }, - bevy_reflect::{ - DynamicEnum, DynamicStruct, DynamicTuple, DynamicTupleStruct, DynamicVariant, - PartialReflect, TypeRegistryArc, std_traits::ReflectDefault, - }, -}; -use bevy_asset::AssetPath; -use bevy_ecs::{ - component::Mutable, - hierarchy::{ChildOf, Children}, - system::Command, - world::WorldId, -}; -use bevy_mod_scripting_asset::ScriptAsset; -use bevy_mod_scripting_display::GetTypeInfo; -use bevy_mod_scripting_script::ScriptAttachment; -use bevy_platform::collections::HashMap; -use bevy_reflect::{TypeInfo, VariantInfo}; -use bevy_system_reflection::ReflectSchedule; -use std::{ - any::{Any, TypeId}, - borrow::Cow, - cell::RefCell, - fmt::Debug, - rc::Rc, - sync::{Arc, atomic::AtomicBool}, -}; - -/// Prefer to directly using [`WorldAccessGuard`]. If the underlying type changes, this alias will be updated. -pub type WorldGuard<'w> = WorldAccessGuard<'w>; -/// Similar to [`WorldGuard`], but without the arc, use for when you don't need the outer Arc. -pub type WorldGuardRef<'w> = &'w WorldAccessGuard<'w>; - -/// Provides safe access to the world via [`AnyAccessMap`] permissions, which enforce aliasing rules at runtime in multi-thread environments -#[derive(Clone, Debug)] -pub struct WorldAccessGuard<'w> { - /// The guard this guard pointer represents - pub(crate) inner: Rc>, - /// if true the guard is invalid and cannot be used, stored as a second pointer so that this validity can be - /// stored separate from the contents of the guard - invalid: Rc, -} -impl WorldAccessGuard<'_> { - /// Returns the id of the world this guard provides access to - pub fn id(&self) -> WorldId { - self.inner.cell.id() - } -} - -/// Used to decrease the stack size of [`WorldAccessGuard`] -pub(crate) struct WorldAccessGuardInner<'w> { - /// Safety: cannot be used unless the scope depth is less than the max valid scope - cell: UnsafeWorldCell<'w>, - // TODO: this is fairly hefty, explore sparse sets, bit fields etc - pub(crate) accesses: AnyAccessMap, - /// Cached for convenience, since we need it for most operations, means we don't need to lock the type registry every time - type_registry: TypeRegistryArc, - /// The script allocator for the world - allocator: AppReflectAllocator, - /// The function registry for the world - function_registry: AppScriptFunctionRegistry, - /// The schedule registry for the world - schedule_registry: AppScheduleRegistry, - /// The registry of script registered components - script_component_registry: AppScriptComponentRegistry, -} - -impl std::fmt::Debug for WorldAccessGuardInner<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WorldAccessGuardInner").finish() - } -} - -#[profiling::all_functions] -impl WorldAccessGuard<'static> { - /// Shortens the lifetime of the guard to the given lifetime. - pub(crate) fn shorten_lifetime<'w>(self) -> WorldGuard<'w> { - // Safety: todo - unsafe { std::mem::transmute(self) } - } -} -#[profiling::all_functions] -impl<'w> WorldAccessGuard<'w> { - /// creates a new guard derived from this one, which if invalidated, will not invalidate the original - fn scope(&self) -> Self { - let mut new_guard = self.clone(); - new_guard.invalid = Rc::new( - new_guard - .invalid - .load(std::sync::atomic::Ordering::Relaxed) - .into(), - ); - new_guard - } - - /// Returns true if the guard is valid, false if it is invalid - fn is_valid(&self) -> bool { - !self.invalid.load(std::sync::atomic::Ordering::Relaxed) - } - - /// Invalidates the world access guard, making it and any guards derived from this one unusable. - pub fn invalidate(&self) { - self.invalid - .store(true, std::sync::atomic::Ordering::Relaxed); - } - - /// Safely allows access to the world for the duration of the closure via a static [`WorldAccessGuard`]. - /// - /// The guard is invalidated at the end of the closure, meaning the world cannot be accessed at all after the closure ends. - pub fn with_static_guard( - world: &'w mut World, - f: impl FnOnce(WorldGuard<'static>) -> O, - ) -> O { - let guard = WorldAccessGuard::new_exclusive(world); - // safety: we invalidate the guard after the closure is called, meaning the world cannot be accessed at all after the 'w lifetime ends - let static_guard: WorldAccessGuard<'static> = unsafe { std::mem::transmute(guard) }; - let o = f(static_guard.clone()); - - static_guard.invalidate(); - o - } - - /// Safely allows access to the world for the duration of the closure via a static [`WorldAccessGuard`] using a previously lifetimed world guard. - /// Will invalidate the static guard at the end but not the original. - pub fn with_existing_static_guard( - guard: WorldAccessGuard<'w>, - f: impl FnOnce(WorldGuard<'static>) -> O, - ) -> O { - // safety: we invalidate the guard after the closure is called, meaning the world cannot be accessed at all after the 'w lifetime ends, from the static guard - // i.e. even if somebody squirells it away, it will be useless. - let static_guard: WorldAccessGuard<'static> = unsafe { std::mem::transmute(guard.scope()) }; - let o = f(static_guard.clone()); - static_guard.invalidate(); - o - } - - /// Creates a new [`WorldAccessGuard`] from a possibly non-exclusive access to the world. - /// - /// It requires specyfing the exact accesses that are allowed to be given out by the guard. - /// Those accesses need to be safe to be given out to the script, as the guard will assume that it is safe to give them out in any way. - /// - /// # Safety - /// - The caller must ensure that the accesses in subset are not aliased by any other access - /// - If an access is allowed in this subset, but alised by someone else, - /// either by being converted to mutable or non mutable reference, this guard will be unsafe. - pub unsafe fn new_non_exclusive( - world: UnsafeWorldCell<'w>, - subset: impl IntoIterator, - type_registry: AppTypeRegistry, - allocator: AppReflectAllocator, - function_registry: AppScriptFunctionRegistry, - schedule_registry: AppScheduleRegistry, - script_component_registry: AppScriptComponentRegistry, - ) -> Self { - Self { - inner: Rc::new(WorldAccessGuardInner { - cell: world, - accesses: AnyAccessMap::SubsetAccessMap(SubsetAccessMap::new( - subset, - // allocations live beyond the world, and can be safely accessed - |id| ReflectAccessId::from_index(id).kind == ReflectAccessKind::Allocation, - )), - type_registry: type_registry.0, - allocator, - function_registry, - schedule_registry, - script_component_registry, - }), - invalid: Rc::new(false.into()), - } - } - - /// Creates a new [`WorldAccessGuard`] for the given mutable borrow of the world. - /// - /// Creating a guard requires that some resources exist in the world, namely: - /// - [`AppTypeRegistry`] - /// - [`AppReflectAllocator`] - /// - [`AppScriptFunctionRegistry`] - /// - /// If these resources do not exist, they will be initialized. - pub fn new_exclusive(world: &'w mut World) -> Self { - let type_registry = world.get_resource_or_init::().0.clone(); - - let allocator = world.get_resource_or_init::().clone(); - - let function_registry = world - .get_resource_or_init::() - .clone(); - - let script_component_registry = world - .get_resource_or_init::() - .clone(); - - let schedule_registry = world.get_resource_or_init::().clone(); - Self { - inner: Rc::new(WorldAccessGuardInner { - cell: world.as_unsafe_world_cell(), - accesses: AnyAccessMap::UnlimitedAccessMap(Default::default()), - allocator, - type_registry, - function_registry, - schedule_registry, - script_component_registry, - }), - invalid: Rc::new(false.into()), - } - } - - /// Queues a command to the world, which will be executed later. - pub(crate) fn queue(&self, command: impl Command) -> Result<(), InteropError> { - self.with_global_access(|w| { - w.commands().queue(command); - }) - } - - /// Runs a closure within an isolated access scope, releasing leftover accesses, should only be used in a single-threaded context. - /// - /// Safety: - /// - The caller must ensure it's safe to release any potentially locked accesses. - pub(crate) unsafe fn with_access_scope O>( - &self, - f: F, - ) -> Result { - Ok(self.inner.accesses.with_scope(f)) - } - - /// Purely debugging utility to list all accesses currently held. - pub fn list_accesses(&self) -> Vec<(ReflectAccessId, AccessCount)> { - self.inner.accesses.list_accesses() - } - - /// Should only really be used for testing purposes - pub unsafe fn release_all_accesses(&self) { - self.inner.accesses.release_all_accesses(); - } - - /// Returns the number of accesses currently held. - pub fn access_len(&self) -> usize { - self.inner.accesses.count_accesses() - } - - /// Retrieves the underlying unsafe world cell, with no additional guarantees of safety - /// proceed with caution and only use this if you understand what you're doing - pub fn as_unsafe_world_cell(&self) -> Result, InteropError> { - if !self.is_valid() { - return Err(InteropError::missing_world()); - } - - Ok(self.inner.cell) - } - - /// Retrieves the underlying read only unsafe world cell, with no additional guarantees of safety - /// proceed with caution and only use this if you understand what you're doing - pub fn as_unsafe_world_cell_readonly(&self) -> Result, InteropError> { - if !self.is_valid() { - return Err(InteropError::missing_world()); - } - - Ok(self.inner.cell) - } - - /// Gets the component id of the given component or resource - pub fn get_component_id(&self, id: TypeId) -> Result, InteropError> { - Ok(self - .as_unsafe_world_cell_readonly()? - .components() - .get_id(id)) - } - - /// Gets the resource id of the given component or resource - pub fn get_resource_id(&self, id: TypeId) -> Result, InteropError> { - Ok(self - .as_unsafe_world_cell_readonly()? - .components() - .get_resource_id(id)) - } - - /// A utility for running a closure with scoped read access to the given id - pub fn with_read_access, O, F: FnOnce(&Self) -> O>( - &self, - id: T, - closure: F, - ) -> Result { - let id = id.into(); - if self.claim_read_access(id) { - let out = Ok(closure(self)); - // Safety: just claimed this access - unsafe { self.release_access(id) }; - out - } else { - Err(()) - } - } - - /// A utility for running a closure with scoped write access to the given id - pub fn with_write_access, O, F: FnOnce(&Self) -> O>( - &self, - id: T, - closure: F, - ) -> Result { - let id = id.into(); - if self.claim_write_access(id) { - let out = Ok(closure(self)); - // Safety: just claimed this access - unsafe { self.release_access(id) }; - out - } else { - Err(()) - } - } - - /// Get the location of the given access - pub fn get_access_location( - &self, - raid: ReflectAccessId, - ) -> Option> { - self.inner.accesses.access_location(raid) - } - - #[track_caller] - /// Claims read access to the given type. - pub fn claim_read_access(&self, raid: ReflectAccessId) -> bool { - self.inner.accesses.claim_read_access(raid) - } - - #[track_caller] - /// Claims write access to the given type. - pub fn claim_write_access(&self, raid: ReflectAccessId) -> bool { - self.inner.accesses.claim_write_access(raid) - } - - /// Releases read or write access to the given type. - /// - /// # Safety - /// - This can only be called safely after all references to the type created using the access have been dropped - /// - You can only call this if you previously called one of: [`WorldAccessGuard::claim_read_access`] or [`WorldAccessGuard::claim_write_access`] - /// - The number of claim and release calls for the same id must always match - pub unsafe fn release_access(&self, raid: ReflectAccessId) { - self.inner.accesses.release_access(raid) - } - - /// Claims global access to the world - pub fn claim_global_access(&self) -> bool { - self.inner.accesses.claim_global_access() - } - - /// Releases global access to the world - /// - /// # Safety - /// - This can only be called safely after all references created using the access have been dropped - pub unsafe fn release_global_access(&self) { - self.inner.accesses.release_global_access() - } - - /// Returns the type registry for the world - pub fn type_registry(&self) -> TypeRegistryArc { - self.inner.type_registry.clone() - } - - /// Returns the schedule registry for the world - pub fn schedule_registry(&self) -> AppScheduleRegistry { - self.inner.schedule_registry.clone() - } - - /// Returns the component registry for the world - pub fn component_registry(&self) -> AppScriptComponentRegistry { - self.inner.script_component_registry.clone() - } - - /// Returns the script allocator for the world - pub fn allocator(&self) -> AppReflectAllocator { - self.inner.allocator.clone() - } - - /// Returns the function registry for the world - pub fn script_function_registry(&self) -> AppScriptFunctionRegistry { - self.inner.function_registry.clone() - } - - /// Claims access to the world for the duration of the closure, allowing for global access to the world. - #[track_caller] - pub fn with_global_access O, O>( - &self, - f: F, - ) -> Result { - with_global_access!( - &self.inner.accesses, - "Could not claim exclusive world access", - { - // safety: we have global access for the duration of the closure - let world = unsafe { self.as_unsafe_world_cell()?.world_mut() }; - Ok(f(world)) - } - )? - } - - /// Safely accesses the resource by claiming and releasing access to it. - /// - /// # Panics - /// - if the resource does not exist - pub fn with_resource(&self, f: F) -> Result - where - R: Resource, - F: FnOnce(&R) -> O, - { - let cell = self.as_unsafe_world_cell()?; - let access_id = ReflectAccessId::for_resource::(&cell)?; - - with_access_read!( - &self.inner.accesses, - access_id, - format!("Could not access resource: {}", std::any::type_name::()), - { - // Safety: we have acquired access for the duration of the closure - f(unsafe { - cell.get_resource::().ok_or_else(|| { - InteropError::unregistered_component_or_resource_type( - std::any::type_name::(), - ) - })? - }) - } - ) - } - - /// Safely accesses the resource by claiming and releasing access to it. - /// - /// # Panics - /// - if the resource does not exist - pub fn with_resource_mut(&self, f: F) -> Result - where - R: Resource, - F: FnOnce(Mut) -> O, - { - let cell = self.as_unsafe_world_cell()?; - let access_id = ReflectAccessId::for_resource::(&cell)?; - with_access_write!( - &self.inner.accesses, - access_id, - format!("Could not access resource: {}", std::any::type_name::()), - { - // Safety: we have acquired access for the duration of the closure - f(unsafe { - cell.get_resource_mut::().ok_or_else(|| { - InteropError::unregistered_component_or_resource_type( - std::any::type_name::(), - ) - })? - }) - } - ) - } - - /// Safely accesses the component by claiming and releasing access to it. - pub fn with_component(&self, entity: Entity, f: F) -> Result - where - T: Component, - F: FnOnce(Option<&T>) -> O, - { - let cell = self.as_unsafe_world_cell()?; - let access_id = ReflectAccessId::for_component::(&cell)?; - with_access_read!( - &self.inner.accesses, - access_id, - format!("Could not access component: {}", std::any::type_name::()), - { - // Safety: we have acquired access for the duration of the closure - f(unsafe { cell.get_entity(entity).map(|e| e.get::()) } - .ok() - .unwrap_or(None)) - } - ) - } - - /// Safely accesses the component by claiming and releasing access to it. - pub fn with_component_mut(&self, entity: Entity, f: F) -> Result - where - T: Component, - F: FnOnce(Option>) -> O, - { - let cell = self.as_unsafe_world_cell()?; - let access_id = ReflectAccessId::for_component::(&cell)?; - - with_access_write!( - &self.inner.accesses, - access_id, - format!("Could not access component: {}", std::any::type_name::()), - { - // Safety: we have acquired access for the duration of the closure - f(unsafe { cell.get_entity(entity).map(|e| e.get_mut::()) } - .ok() - .unwrap_or(None)) - } - ) - } - - /// Safey modify or insert a component by claiming and releasing global access. - pub fn with_or_insert_component_mut( - &self, - entity: Entity, - f: F, - ) -> Result - where - T: Component + Default, - F: FnOnce(&mut T) -> O, - { - self.with_global_access(|world| match world.get_mut::(entity) { - Some(mut component) => f(&mut component), - None => { - let mut component = T::default(); - let mut commands = world.commands(); - let result = f(&mut component); - commands.entity(entity).insert(component); - result - } - }) - } - - /// Try to lookup a function with the given name on the given type id's namespaces. - /// - /// Returns the function if found, otherwise returns the name of the function that was not found. - pub fn lookup_function( - &self, - type_ids: impl IntoIterator, - name: impl Into>, - ) -> Result> { - let registry = self.script_function_registry(); - let registry = registry.read(); - - let mut name = name.into(); - for type_id in type_ids { - name = match registry.get_function(Namespace::OnType(type_id), name) { - Ok(func) => return Ok(func.clone()), - Err(name) => name, - }; - } - - Err(name) - } - - /// Iterates over all available functions on the type id's namespace + those available on any reference if any exist. - pub fn get_functions_on_type( - &self, - type_id: TypeId, - ) -> Vec<(Cow<'static, str>, DynamicScriptFunction)> { - let registry = self.script_function_registry(); - let registry = registry.read(); - - registry - .iter_namespace(Namespace::OnType(type_id)) - .chain( - registry - .iter_namespace(Namespace::OnType(std::any::TypeId::of::())), - ) - .map(|(key, func)| (key.name.clone(), func.clone())) - .collect() - } - - /// checks if a given entity exists and is valid - pub fn is_valid_entity(&self, entity: Entity) -> Result { - let cell = self.as_unsafe_world_cell()?; - Ok(cell.get_entity(entity).is_ok() && entity.index().index() != 0) - } - - /// Tries to call a fitting overload of the function with the given name and in the type id's namespace based on the arguments provided. - /// Currently does this by repeatedly trying each overload until one succeeds or all fail. - pub fn try_call_overloads( - &self, - type_id: TypeId, - name: impl Into>, - args: Vec, - context: FunctionCallContext, - ) -> Result { - let registry = self.script_function_registry(); - let registry = registry.read(); - - let name = name.into(); - let overload_iter = match registry.iter_overloads(Namespace::OnType(type_id), name) { - Ok(iter) => iter, - Err(name) => { - return Err(InteropError::missing_function( - name.to_string(), - Namespace::OnType(type_id), - Some(context.clone()), - )); - } - }; - - let mut last_error = None; - for overload in overload_iter { - match overload.call(args.clone(), context.clone()) { - Ok(out) => return Ok(out), - Err(e) => last_error = Some(e), - } - } - - Err(last_error.ok_or_else(|| InteropError::invariant("invariant, iterator should always return at least one item, and if the call fails it should return an error"))?) - } -} - -/// Impl block for higher level world methods -#[profiling::all_functions] -impl WorldAccessGuard<'_> { - fn construct_from_script_value( - &self, - descriptor: impl Into>, - type_id: TypeId, - value: Option, - ) -> Result, InteropError> { - // if the value is missing, try to construct a default and return it - let value = match value { - Some(value) => value, - None => { - let type_registry = self.type_registry(); - let type_registry = type_registry.read(); - let default_data = type_registry - .get_type_data::(type_id) - .ok_or_else(|| { - InteropError::function_interop_error( - "construct", - Namespace::OnType(TypeId::of::()), - InteropError::string(format!( - "field missing and no default provided: '{}'", - descriptor.into() - )), - None, - ) - })?; - return Ok(default_data.default().into_partial_reflect()); - } - }; - - // otherwise we need to use from_script_ref - >::from_script_ref(type_id, value, self.clone()) - } - - fn construct_dynamic_struct( - &self, - payload: &mut HashMap, - fields: Vec<(&'static str, TypeId)>, - ) -> Result { - let mut dynamic = DynamicStruct::default(); - for (field_name, field_type_id) in fields { - let constructed = self.construct_from_script_value( - field_name, - field_type_id, - payload.remove(field_name), - )?; - - dynamic.insert_boxed(field_name, constructed); - } - Ok(dynamic) - } - - fn construct_dynamic_tuple_struct( - &self, - payload: &mut HashMap, - fields: Vec, - one_indexed: bool, - ) -> Result { - let mut dynamic = DynamicTupleStruct::default(); - for (field_idx, field_type_id) in fields.into_iter().enumerate() { - // correct for indexing - let script_idx = if one_indexed { - field_idx + 1 - } else { - field_idx - }; - let field_string = script_idx.to_string(); - dynamic.insert_boxed(self.construct_from_script_value( - field_string.clone(), - field_type_id, - payload.remove(&field_string), - )?); - } - Ok(dynamic) - } - - fn construct_dynamic_tuple( - &self, - payload: &mut HashMap, - fields: Vec, - one_indexed: bool, - ) -> Result { - let mut dynamic = DynamicTuple::default(); - for (field_idx, field_type_id) in fields.into_iter().enumerate() { - // correct for indexing - let script_idx = if one_indexed { - field_idx + 1 - } else { - field_idx - }; - - let field_string = script_idx.to_string(); - - dynamic.insert_boxed(self.construct_from_script_value( - field_string.clone(), - field_type_id, - payload.remove(&field_string), - )?); - } - Ok(dynamic) - } - - /// An arbitrary type constructor utility. - /// - /// Allows the construction of arbitrary types (within limits dictated by the API) from the script directly - pub fn construct( - &self, - type_: ScriptTypeRegistration, - mut payload: HashMap, - one_indexed: bool, - ) -> Result, InteropError> { - // figure out the kind of type we're building - let type_info = type_.registration.type_info(); - // we just need to a) extract fields, if enum we need a "variant" field specifying the variant - // then build the corresponding dynamic structure, whatever it may be - - let dynamic: Box = match type_info { - TypeInfo::Struct(struct_info) => { - let fields_iter = struct_info - .field_names() - .iter() - .map(|f| { - Ok(( - *f, - struct_info - .field(f) - .ok_or_else(|| { - InteropError::invariant( - "field in field_names should have reflection information", - ) - })? - .type_id(), - )) - }) - .collect::, InteropError>>()?; - let mut dynamic = self.construct_dynamic_struct(&mut payload, fields_iter)?; - dynamic.set_represented_type(Some(type_info)); - Box::new(dynamic) - } - TypeInfo::TupleStruct(tuple_struct_info) => { - let fields_iter = (0..tuple_struct_info.field_len()) - .map(|f| { - Ok(tuple_struct_info - .field_at(f) - .ok_or_else(|| { - InteropError::invariant( - "field in field_names should have reflection information", - ) - })? - .type_id()) - }) - .collect::, InteropError>>()?; - - let mut dynamic = - self.construct_dynamic_tuple_struct(&mut payload, fields_iter, one_indexed)?; - dynamic.set_represented_type(Some(type_info)); - Box::new(dynamic) - } - TypeInfo::Tuple(tuple_info) => { - let fields_iter = (0..tuple_info.field_len()) - .map(|f| { - Ok(tuple_info - .field_at(f) - .ok_or_else(|| { - InteropError::invariant( - "field in field_names should have reflection information", - ) - })? - .type_id()) - }) - .collect::, InteropError>>()?; - - let mut dynamic = - self.construct_dynamic_tuple(&mut payload, fields_iter, one_indexed)?; - dynamic.set_represented_type(Some(type_info)); - Box::new(dynamic) - } - TypeInfo::Enum(enum_info) => { - // extract variant from "variant" - let variant = payload.remove("variant").ok_or_else(|| { - InteropError::function_interop_error( - "construct", - Namespace::OnType(TypeId::of::()), - InteropError::str("missing 'variant' field in enum constructor payload"), - None, - ) - })?; - - let variant_name = String::from_script(variant, self.clone())?; - - let variant = enum_info.variant(&variant_name).ok_or_else(|| { - InteropError::function_interop_error( - "construct", - Namespace::OnType(TypeId::of::()), - InteropError::string(format!( - "invalid variant name '{}' for enum '{}'", - variant_name, - enum_info.type_path() - )), - None, - ) - })?; - - let variant = match variant { - VariantInfo::Struct(struct_variant_info) => { - // same as above struct variant - let fields_iter = struct_variant_info - .field_names() - .iter() - .map(|f| { - Ok(( - *f, - struct_variant_info - .field(f) - .ok_or_else(|| { - InteropError::invariant( - "field in field_names should have reflection information", - ) - })? - .type_id(), - )) - }) - .collect::, InteropError>>()?; - - let dynamic = self.construct_dynamic_struct(&mut payload, fields_iter)?; - DynamicVariant::Struct(dynamic) - } - VariantInfo::Tuple(tuple_variant_info) => { - // same as tuple variant - let fields_iter = (0..tuple_variant_info.field_len()) - .map(|f| { - Ok(tuple_variant_info - .field_at(f) - .ok_or_else(|| { - InteropError::invariant( - "field in field_names should have reflection information", - ) - })? - .type_id()) - }) - .collect::, InteropError>>()?; - - let dynamic = - self.construct_dynamic_tuple(&mut payload, fields_iter, one_indexed)?; - DynamicVariant::Tuple(dynamic) - } - VariantInfo::Unit(_) => DynamicVariant::Unit, - }; - let mut dynamic = DynamicEnum::new(variant_name, variant); - dynamic.set_represented_type(Some(type_info)); - Box::new(dynamic) - } - _ => { - return Err(InteropError::unsupported_operation( - Some(type_info.type_id()), - Some(Box::new(payload)), - "Type constructor not supported", - )); - } - }; - - // try to construct type from reflect - // TODO: it would be nice to have a ::from_reflect_with_fallback equivalent, that does exactly that - // only using this as it's already there and convenient, the clone variant hitting will be confusing to end users - ::from_reflect_or_clone(dynamic.as_ref(), self.clone()) - } - - /// Loads a script from the given asset path with default settings. - pub fn load_script_asset<'a>( - &self, - asset_path: impl Into>, - ) -> Result, InteropError> { - self.with_resource(|r: &AssetServer| r.load(asset_path)) - } - - /// Checks the load state of a script asset. - pub fn get_script_asset_load_state( - &self, - script: Handle, - ) -> Result { - self.with_resource(|r: &AssetServer| r.load_state(script.id())) - } - - // /// Attaches a script - // pub fn attach_script(&self, attachment: ScriptAttachment) -> Result<(), InteropError> { - // match attachment { - // ScriptAttachment::EntityScript(entity, handle) => { - // // find existing script components on the entity - // self.with_or_insert_component_mut(entity, |c: &mut ScriptComponent| { - // c.0.push(handle.clone()) - // })?; - // } - // ScriptAttachment::StaticScript(handle) => { - // self.queue(AddStaticScript::new(handle))?; - // } - // }; - - // Ok(()) - // } - - /// Spawns a new entity in the world - pub fn spawn(&self) -> Result { - self.with_global_access(|world| { - let mut command_queue = CommandQueue::default(); - let mut commands = Commands::new(&mut command_queue, world); - let id = commands.spawn_empty().id(); - command_queue.apply(world); - id - }) - } - - /// get a type registration for the type, without checking if it's a component or resource - pub fn get_type_by_name(&self, type_name: &str) -> Option { - let type_registry = self.type_registry(); - let type_registry = type_registry.read(); - type_registry - .get_with_short_type_path(type_name) - .or_else(|| type_registry.get_with_type_path(type_name)) - .map(|registration| ScriptTypeRegistration::new(Arc::new(registration.clone()))) - } - - /// get a type erased type registration for the type including information about whether it's a component or resource - pub(crate) fn get_type_registration( - &self, - registration: ScriptTypeRegistration, - ) -> Result< - Union< - ScriptTypeRegistration, - Union, - >, - InteropError, - > { - let registration = match self.get_resource_type(registration)? { - Ok(res) => { - return Ok(Union::new_right(Union::new_right(res))); - } - Err(registration) => registration, - }; - - let registration = match self.get_component_type(registration)? { - Ok(comp) => { - return Ok(Union::new_right(Union::new_left(comp))); - } - Err(registration) => registration, - }; - - Ok(Union::new_left(registration)) - } - - /// Similar to [`Self::get_type_by_name`] but returns a type erased [`ScriptTypeRegistration`], [`ScriptComponentRegistration`] or [`ScriptResourceRegistration`] - /// depending on the underlying type and state of the world. - pub fn get_type_registration_by_name( - &self, - type_name: String, - ) -> Result< - Option< - Union< - ScriptTypeRegistration, - Union, - >, - >, - InteropError, - > { - let val = self.get_type_by_name(&type_name); - Ok(match val { - Some(registration) => Some(self.get_type_registration(registration)?), - None => { - // try the component registry - let components = self.component_registry(); - let components = components.read(); - components - .get(&type_name) - .map(|c| Union::new_right(Union::new_left(c.registration.clone()))) - } - }) - } - - /// get a schedule by name - pub fn get_schedule_by_name(&self, schedule_name: String) -> Option { - let schedule_registry = self.schedule_registry(); - let schedule_registry = schedule_registry.read(); - - schedule_registry - .get_schedule_by_name(&schedule_name) - .cloned() - } - - /// get a component type registration for the type - pub fn get_component_type( - &self, - registration: ScriptTypeRegistration, - ) -> Result, InteropError> { - Ok(match self.get_component_id(registration.type_id())? { - Some(comp_id) => Ok(ScriptComponentRegistration::new(registration, comp_id)), - None => Err(registration), - }) - } - - /// get a resource type registration for the type - pub fn get_resource_type( - &self, - registration: ScriptTypeRegistration, - ) -> Result, InteropError> { - Ok(match self.get_resource_id(registration.type_id())? { - Some(resource_id) => Ok(ScriptResourceRegistration::new(registration, resource_id)), - None => Err(registration), - }) - } - - /// add a default component to an entity - pub fn add_default_component( - &self, - entity: Entity, - registration: ScriptComponentRegistration, - ) -> Result<(), InteropError> { - // we look for ReflectDefault or ReflectFromWorld data then a ReflectComponent data - let instance = if let Some(default_td) = registration - .type_registration() - .type_registration() - .data::() - { - default_td.default() - } else if let Some(from_world_td) = registration - .type_registration() - .type_registration() - .data::() - { - self.with_global_access(|world| from_world_td.from_world(world))? - } else { - return Err(InteropError::missing_type_data( - registration.registration.type_id(), - "ReflectDefault or ReflectFromWorld".to_owned(), - )); - }; - - registration.insert_into_entity(self.clone(), entity, instance) - } - - /// insert the component into the entity - pub fn insert_component( - &self, - entity: Entity, - registration: ScriptComponentRegistration, - value: ReflectReference, - ) -> Result<(), InteropError> { - let instance = >::from_script_ref( - registration.type_registration().type_id(), - ScriptValue::Reference(value), - self.clone(), - )?; - - let reflect = instance.try_into_reflect().map_err(|v| { - InteropError::failed_from_reflect( - Some(registration.type_registration().type_id()), - format!("instance produced by conversion to target type when inserting component is not a full reflect type: {v:?}"), - ) - })?; - - registration.insert_into_entity(self.clone(), entity, reflect) - } - - /// get the component from the entity - pub fn get_component( - &self, - entity: Entity, - component_registration: ScriptComponentRegistration, - ) -> Result, InteropError> { - let cell = self.as_unsafe_world_cell()?; - let entity = cell - .get_entity(entity) - .map_err(|_| InteropError::missing_entity(entity))?; - - if entity.contains_id(component_registration.component_id) { - Ok(Some(ReflectReference { - base: ReflectBaseType { - type_id: component_registration.type_registration().type_id(), - base_id: ReflectBase::Component( - entity.id(), - component_registration.component_id, - ), - }, - reflect_path: Default::default(), - })) - } else { - Ok(None) - } - } - - /// check if the entity has the component - pub fn has_component( - &self, - entity: Entity, - component_id: ComponentId, - ) -> Result { - let cell = self.as_unsafe_world_cell()?; - let entity = cell - .get_entity(entity) - .map_err(|_| InteropError::missing_entity(entity))?; - - Ok(entity.contains_id(component_id)) - } - - /// remove the component from the entity - pub fn remove_component( - &self, - entity: Entity, - registration: ScriptComponentRegistration, - ) -> Result<(), InteropError> { - registration.remove_from_entity(self.clone(), entity) - } - - /// get the given resource - pub fn get_resource( - &self, - resource_id: ComponentId, - ) -> Result, InteropError> { - let cell = self.as_unsafe_world_cell()?; - let component_info = match cell.components().get_info(resource_id) { - Some(info) => info, - None => return Ok(None), - }; - - Ok(Some(ReflectReference { - base: ReflectBaseType { - type_id: component_info - .type_id() - .ok_or_else(|| { - InteropError::unsupported_operation( - None, - None, - format!( - "Resource {} does not have a type id. Such resources are not supported by BMS.", - component_info.name() - ), - ) - })?, - base_id: ReflectBase::Resource(resource_id), - }, - reflect_path: Default::default(), - })) - } - - /// remove the given resource - pub fn remove_resource( - &self, - registration: ScriptResourceRegistration, - ) -> Result<(), InteropError> { - let component_data = registration - .type_registration() - .type_registration() - .data::() - .ok_or_else(|| { - InteropError::missing_type_data( - registration.registration.type_id(), - "ReflectResource".to_owned(), - ) - })?; - - // TODO: this shouldn't need entire world access it feels - self.with_global_access(|world| component_data.remove(world)) - } - - /// check if the entity has the resource - pub fn has_resource(&self, resource_id: ComponentId) -> Result { - let cell = self.as_unsafe_world_cell()?; - // Safety: we are not reading the value at all - let res_ptr = unsafe { cell.get_resource_by_id(resource_id) }; - Ok(res_ptr.is_some()) - } - - /// check the given entity exists - pub fn has_entity(&self, entity: Entity) -> Result { - self.is_valid_entity(entity) - } - - /// get the children of the given entity - pub fn get_children(&self, entity: Entity) -> Result, InteropError> { - if !self.is_valid_entity(entity)? { - return Err(InteropError::missing_entity(entity)); - } - - self.with_component(entity, |c: Option<&Children>| { - c.map(|c| c.to_vec()).unwrap_or_default() - }) - } - - /// get the parent of the given entity - pub fn get_parent(&self, entity: Entity) -> Result, InteropError> { - if !self.is_valid_entity(entity)? { - return Err(InteropError::missing_entity(entity)); - } - - self.with_component(entity, |c: Option<&ChildOf>| c.map(|c| c.parent())) - } - - /// insert children into the given entity - pub fn push_children(&self, parent: Entity, children: &[Entity]) -> Result<(), InteropError> { - // verify entities exist - if !self.is_valid_entity(parent)? { - return Err(InteropError::missing_entity(parent)); - } - for c in children { - if !self.is_valid_entity(*c)? { - return Err(InteropError::missing_entity(*c)); - } - } - self.with_global_access(|world| { - let mut queue = CommandQueue::default(); - let mut commands = Commands::new(&mut queue, world); - commands.entity(parent).add_children(children); - queue.apply(world); - }) - } - - /// remove children from the given entity - pub fn remove_children(&self, parent: Entity, children: &[Entity]) -> Result<(), InteropError> { - if !self.is_valid_entity(parent)? { - return Err(InteropError::missing_entity(parent)); - } - - for c in children { - if !self.is_valid_entity(*c)? { - return Err(InteropError::missing_entity(*c)); - } - } - self.with_global_access(|world| { - let mut queue = CommandQueue::default(); - let mut commands = Commands::new(&mut queue, world); - commands.entity(parent).detach_children(children); - queue.apply(world); - }) - } - - /// insert children into the given entity at the given index - pub fn insert_children( - &self, - parent: Entity, - index: usize, - children: &[Entity], - ) -> Result<(), InteropError> { - if !self.is_valid_entity(parent)? { - return Err(InteropError::missing_entity(parent)); - } - - for c in children { - if !self.is_valid_entity(*c)? { - return Err(InteropError::missing_entity(*c)); - } - } - - self.with_global_access(|world| { - let mut queue = CommandQueue::default(); - let mut commands = Commands::new(&mut queue, world); - commands.entity(parent).insert_children(index, children); - queue.apply(world); - }) - } - - /// despawn this and all children of the given entity recursively - pub fn despawn_recursive(&self, parent: Entity) -> Result<(), InteropError> { - if !self.is_valid_entity(parent)? { - return Err(InteropError::missing_entity(parent)); - } - self.with_global_access(|world| { - let mut queue = CommandQueue::default(); - let mut commands = Commands::new(&mut queue, world); - commands.entity(parent).despawn(); - queue.apply(world); - }) - } - - /// despawn the given entity - pub fn despawn(&self, entity: Entity) -> Result<(), InteropError> { - if !self.is_valid_entity(entity)? { - return Err(InteropError::missing_entity(entity)); - } - - self.with_global_access(|world| { - let mut queue = CommandQueue::default(); - let mut commands = Commands::new(&mut queue, world); - commands.entity(entity).remove::().despawn(); - queue.apply(world); - }) - } - - /// despawn all children of the given entity recursively - pub fn despawn_descendants(&self, parent: Entity) -> Result<(), InteropError> { - if !self.is_valid_entity(parent)? { - return Err(InteropError::missing_entity(parent)); - } - - self.with_global_access(|world| { - let mut queue = CommandQueue::default(); - let mut commands = Commands::new(&mut queue, world); - commands.entity(parent).despawn_related::(); - queue.apply(world); - }) - } - - /// Sends AppExit event to the world with success status - pub fn exit(&self) -> Result<(), InteropError> { - self.with_global_access(|world| { - world.write_message(AppExit::Success); - }) - } -} - -/// A world container that stores the world in a thread local -pub struct ThreadWorldContainer; - -#[derive(Clone)] -/// Context passed down indirectly to script related functions, used to avoid prop drilling problems. -pub struct ThreadScriptContext<'l> { - /// The world pointer - pub world: WorldGuard<'l>, - /// The currently active script attachment - pub attachment: ScriptAttachment, -} - -thread_local! { - static WORLD_CALLBACK_ACCESS: RefCell>> = const { RefCell::new(None) }; -} -#[profiling::all_functions] -impl ThreadWorldContainer { - /// Tries to set the thread context to the given value - pub fn set_context(&mut self, world: ThreadScriptContext<'static>) -> Result<(), InteropError> { - WORLD_CALLBACK_ACCESS.with(|w| { - w.replace(Some(world)); - }); - Ok(()) - } - - /// Tries to get the world from the container - pub fn try_get_context<'l>(&self) -> Result, InteropError> { - WORLD_CALLBACK_ACCESS - .with(|w| w.borrow().clone().ok_or_else(InteropError::missing_world)) - .map(|v| ThreadScriptContext { - world: v.world.shorten_lifetime(), - attachment: v.attachment, - }) - } -} - -impl GetTypeInfo for ThreadWorldContainer { - fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo> { - let world = self.try_get_context().ok()?.world; - let registry = world.type_registry(); - let registry = registry.read(); - registry.get(type_id).map(|r| r.type_info()) - } - - fn query_type_registration( - &self, - type_id: TypeId, - type_data_id: TypeId, - ) -> Option> { - let world = self.try_get_context().ok()?.world; - let registry = world.type_registry(); - let registry = registry.read(); - registry - .get(type_id) - .and_then(|r| r.data_by_id(type_data_id).map(|t| t.clone_type_data())) - } - - fn get_component_info( - &self, - component_id: ComponentId, - ) -> Option<&bevy_ecs::component::ComponentInfo> { - let world = self.try_get_context().ok()?.world; - let cell = world.as_unsafe_world_cell().ok()?; - cell.components().get_info(component_id) - } - - unsafe fn as_any_static(&self) -> &dyn Any { - self - } -} - -impl GetTypeInfo for WorldGuard<'_> { - fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo> { - let registry = self.type_registry(); - let registry = registry.read(); - registry.get(type_id).map(|r| r.type_info()) - } - - fn query_type_registration( - &self, - type_id: TypeId, - type_data_id: TypeId, - ) -> Option> { - let registry = self.type_registry(); - let registry = registry.read(); - registry - .get(type_id) - .and_then(|r| r.data_by_id(type_data_id).map(|t| t.clone_type_data())) - } - - fn get_component_info( - &self, - component_id: ComponentId, - ) -> Option<&bevy_ecs::component::ComponentInfo> { - let cell = self.as_unsafe_world_cell().ok()?; - cell.components().get_info(component_id) - } - - /// # Safety - /// - TODO: should generaly be safe as the guard is invalidated once the world is out of scope - unsafe fn as_any_static(&self) -> &dyn Any { - let static_self: &WorldGuard<'static> = unsafe { std::mem::transmute(self) }; - static_self as &dyn Any - } -} - -#[cfg(test)] -mod test { - use super::*; - use bevy_reflect::{GetTypeRegistration, ReflectFromReflect}; - use test_utils::test_data::{SimpleEnum, SimpleStruct, SimpleTupleStruct, setup_world}; - - #[test] - fn test_construct_struct() { - let mut world = setup_world(|_, _| {}); - let world = WorldAccessGuard::new_exclusive(&mut world); - - let registry = world.type_registry(); - let registry = registry.read(); - - let registration = registry.get(TypeId::of::()).unwrap().clone(); - let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); - - let payload = HashMap::from_iter(vec![("foo".to_owned(), ScriptValue::Integer(1))]); - - let result = world.construct(type_registration, payload, false); - let expected = - Ok::<_, InteropError>(Box::new(SimpleStruct { foo: 1 }) as Box); - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - } - - #[test] - fn test_construct_tuple_struct() { - let mut world = setup_world(|_, _| {}); - let world = WorldAccessGuard::new_exclusive(&mut world); - - let registry = world.type_registry(); - let registry = registry.read(); - - let registration = registry - .get(TypeId::of::()) - .unwrap() - .clone(); - let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); - - // zero indexed - let payload = HashMap::from_iter(vec![("0".to_owned(), ScriptValue::Integer(1))]); - - let result = world.construct(type_registration.clone(), payload, false); - let expected = - Ok::<_, InteropError>(Box::new(SimpleTupleStruct(1)) as Box); - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - - // one indexed - let payload = HashMap::from_iter(vec![("1".to_owned(), ScriptValue::Integer(1))]); - - let result = world.construct(type_registration, payload, true); - let expected = - Ok::<_, InteropError>(Box::new(SimpleTupleStruct(1)) as Box); - - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - } - - #[test] - fn test_construct_tuple() { - let mut world = setup_world(|_, registry| { - registry.register::<(usize, usize)>(); - // TODO: does this ever get registered on normal types? I don't think so: https://github.com/bevyengine/bevy/issues/17981 - registry.register_type_data::<(usize, usize), ReflectFromReflect>(); - }); - - ::get_type_registration(); - let world = WorldAccessGuard::new_exclusive(&mut world); - - let registry = world.type_registry(); - let registry = registry.read(); - - let registration = registry - .get(TypeId::of::<(usize, usize)>()) - .unwrap() - .clone(); - let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); - - // zero indexed - let payload = HashMap::from_iter(vec![ - ("0".to_owned(), ScriptValue::Integer(1)), - ("1".to_owned(), ScriptValue::Integer(2)), - ]); - - let result = world.construct(type_registration.clone(), payload, false); - let expected = Ok::<_, InteropError>(Box::new((1, 2)) as Box); - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - - // one indexed - let payload = HashMap::from_iter(vec![ - ("1".to_owned(), ScriptValue::Integer(1)), - ("2".to_owned(), ScriptValue::Integer(2)), - ]); - - let result = world.construct(type_registration.clone(), payload, true); - let expected = Ok::<_, InteropError>(Box::new((1, 2)) as Box); - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - } - - #[test] - fn test_construct_enum() { - let mut world = setup_world(|_, _| {}); - let world = WorldAccessGuard::new_exclusive(&mut world); - - let registry = world.type_registry(); - let registry = registry.read(); - - let registration = registry.get(TypeId::of::()).unwrap().clone(); - let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); - - // struct version - let payload = HashMap::from_iter(vec![ - ("foo".to_owned(), ScriptValue::Integer(1)), - ("variant".to_owned(), ScriptValue::String("Struct".into())), - ]); - - let result = world.construct(type_registration.clone(), payload, false); - let expected = Ok::<_, InteropError>( - Box::new(SimpleEnum::Struct { foo: 1 }) as Box - ); - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - - // tuple struct version - let payload = HashMap::from_iter(vec![ - ("0".to_owned(), ScriptValue::Integer(1)), - ( - "variant".to_owned(), - ScriptValue::String("TupleStruct".into()), - ), - ]); - - let result = world.construct(type_registration.clone(), payload, false); - let expected = - Ok::<_, InteropError>(Box::new(SimpleEnum::TupleStruct(1)) as Box); - - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - - // unit version - let payload = HashMap::from_iter(vec![( - "variant".to_owned(), - ScriptValue::String("Unit".into()), - )]); - - let result = world.construct(type_registration, payload, false); - let expected = Ok::<_, InteropError>(Box::new(SimpleEnum::Unit) as Box); - pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); - } - - #[test] - fn test_scoped_handle_invalidate_doesnt_invalidate_parent() { - let mut world = setup_world(|_, _| {}); - let world = WorldAccessGuard::new_exclusive(&mut world); - let scoped_world = world.scope(); - - // can use scoped & normal worlds - scoped_world.spawn().unwrap(); - world.spawn().unwrap(); - pretty_assertions::assert_eq!(scoped_world.is_valid(), true); - pretty_assertions::assert_eq!(world.is_valid(), true); - - scoped_world.invalidate(); - - // can only use normal world - pretty_assertions::assert_eq!(scoped_world.is_valid(), false); - pretty_assertions::assert_eq!(world.is_valid(), true); - world.spawn().unwrap(); - } - - #[test] - fn with_existing_static_guard_does_not_invalidate_original() { - let mut world = setup_world(|_, _| {}); - let world = WorldAccessGuard::new_exclusive(&mut world); - - let mut sneaky_clone = None; - WorldAccessGuard::with_existing_static_guard(world.clone(), |g| { - pretty_assertions::assert_eq!(g.is_valid(), true); - sneaky_clone = Some(g.clone()); - }); - pretty_assertions::assert_eq!(world.is_valid(), true, "original world was invalidated"); - pretty_assertions::assert_eq!( - sneaky_clone.map(|c| c.is_valid()), - Some(false), - "scoped world was not invalidated" - ); - } - - #[test] - fn test_with_access_scope_success() { - let mut world = setup_world(|_, _| {}); - let guard = WorldAccessGuard::new_exclusive(&mut world); - - // within the access scope, no extra accesses are claimed - let result = unsafe { guard.with_access_scope(|| 100) }; - assert_eq!(result.unwrap(), 100); - } -} diff --git a/crates/bevy_mod_scripting_bindings/src/world_extensions.rs b/crates/bevy_mod_scripting_bindings/src/world_extensions.rs new file mode 100644 index 0000000000..3cdaad8b95 --- /dev/null +++ b/crates/bevy_mod_scripting_bindings/src/world_extensions.rs @@ -0,0 +1,2004 @@ +//! +//! Implementations of [`WorldExtensions`] trait on the world guard. + +use super::{ + AppReflectAllocator, AppScriptComponentRegistry, ReflectBase, ReflectBaseType, + ReflectReference, ScriptComponentRegistration, ScriptResourceRegistration, + ScriptTypeRegistration, Union, + function::{ + namespace::Namespace, + script_function::{AppScriptFunctionRegistry, DynamicScriptFunction, FunctionCallContext}, + }, + schedule::AppScheduleRegistry, + script_value::ScriptValue, +}; +use crate::{ + DynamicComponent, DynamicComponentInfo, ScriptQueryBuilder, ScriptQueryResult, + error::InteropError, + function::{from::FromScript, from_ref::FromScriptRef}, + reflection_extensions::PartialReflectExt, +}; +use ::{ + bevy_asset::{AssetServer, Handle, LoadState}, + bevy_ecs::{ + component::ComponentId, + entity::Entity, + reflect::{ReflectFromWorld, ReflectResource}, + world::World, + }, + bevy_reflect::{ + DynamicEnum, DynamicStruct, DynamicTuple, DynamicTupleStruct, DynamicVariant, + PartialReflect, std_traits::ReflectDefault, + }, +}; +use bevy_app::AppExit; +use bevy_asset::AssetPath; +use bevy_ecs::{ + component::{Component, ComponentCloneBehavior, ComponentDescriptor, Mutable}, + hierarchy::{ChildOf, Children}, + resource::Resource, + system::Commands, + world::{CommandQueue, EntityRef, Mut}, +}; +use bevy_mod_scripting_asset::ScriptAsset; +use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::{CachedRegistry, RegistryCache, WorldAccessGuard, WorldGuard}; +use bevy_platform::collections::HashMap; +use bevy_reflect::{GetTypeRegistration, TypeInfo, VariantInfo}; +use bevy_system_reflection::ReflectSchedule; +use std::{ + alloc::Layout, + any::TypeId, + borrow::Cow, + cell::{Ref, RefCell}, + collections::VecDeque, + mem::needs_drop, + rc::Rc, + sync::Arc, +}; + +/// Functional extensions to the [`WorldGuard`] +pub trait WorldExtensions { + /// Spawns a new empty entity and returns its ID. + fn spawn(&self) -> Result; + + /// Despawns the given entity (also removes its Children component). + fn despawn(&self, entity: Entity) -> Result<(), InteropError>; + + /// Despawns the entity and all of its descendants. + fn despawn_recursive(&self, parent: Entity) -> Result<(), InteropError>; + + /// Despawns only the descendants of the given entity. + fn despawn_descendants(&self, parent: Entity) -> Result<(), InteropError>; + + /// Checks whether the entity exists and is valid. + fn is_valid_entity(&self, entity: Entity) -> Result; + + /// Alias for `is_valid_entity`. + fn has_entity(&self, entity: Entity) -> Result; + + /// Runs a query and returns matching entities and component references. + fn query(&self, query: ScriptQueryBuilder) + -> Result, InteropError>; + + /// Inserts a component value into an entity. + fn insert_component( + &self, + entity: Entity, + registration: ScriptComponentRegistration, + value: ReflectReference, + ) -> Result<(), InteropError>; + + /// Adds a default-constructed component to an entity. + fn add_default_component( + &self, + entity: Entity, + registration: ScriptComponentRegistration, + ) -> Result<(), InteropError>; + + /// Removes a component from an entity. + fn remove_component( + &self, + entity: Entity, + registration: ScriptComponentRegistration, + ) -> Result<(), InteropError>; + + /// Retrieves a component reference from an entity if present. + fn get_component( + &self, + entity: Entity, + component_registration: ScriptComponentRegistration, + ) -> Result, InteropError>; + + /// Checks if an entity contains a specific component. + fn has_component( + &self, + entity: Entity, + component_id: ComponentId, + ) -> Result; + + /// Executes a closure with access to a component (if present). + fn with_component) -> O>( + &self, + entity: Entity, + f: F, + ) -> Result; + + /// Executes a closure with access to a component (if present). + fn with_component_mut, O, F: FnOnce(Option>) -> O>( + &self, + entity: Entity, + f: F, + ) -> Result; + + /// Executes a closure with access to a component after inserting its default value if it doesn't exist (if present). + fn with_or_insert_component_mut< + C: Component + Default, + O, + F: FnOnce(&mut C) -> O, + >( + &self, + entity: Entity, + f: F, + ) -> Result; + + /// Retrieves a resource reference if it exists. + fn get_resource( + &self, + resource_id: ComponentId, + ) -> Result, InteropError>; + + /// Removes a resource from the world. + fn remove_resource(&self, registration: ScriptResourceRegistration) + -> Result<(), InteropError>; + + /// Checks if a resource exists. + fn has_resource(&self, resource_id: ComponentId) -> Result; + + /// Executes a closure with shared access to a resource. + fn with_resource O>(&self, f: F) -> Result; + + /// Executes a closure with mutable access to a resource. + fn with_resource_mut) -> O>( + &self, + f: F, + ) -> Result; + + /// Executes a closure with immutable access to the world. + fn with_world O>(&self, f: F) -> Result; + + /// Executes a closure with mutable access to the world. + fn with_world_mut O>(&self, f: F) -> Result; + + /// Returns the parent of an entity if it has one. + fn get_parent(&self, entity: Entity) -> Result, InteropError>; + + /// Returns all children of an entity. + fn get_children(&self, entity: Entity) -> Result, InteropError>; + + /// Appends children to the given parent entity. + fn push_children(&self, parent: Entity, children: &[Entity]) -> Result<(), InteropError>; + + /// Inserts children at a specific index in the parent's child list. + fn insert_children( + &self, + parent: Entity, + index: usize, + children: &[Entity], + ) -> Result<(), InteropError>; + + /// Removes specific children from a parent entity. + fn remove_children(&self, parent: Entity, children: &[Entity]) -> Result<(), InteropError>; + + /// Sends an AppExit::Success event to the world. + fn exit(&self) -> Result<(), InteropError>; + + // /// Retrieves all systems in the given schedule. + // fn systems(&self, schedule: &ReflectSchedule) -> Result, InteropError>; + + // /// Temporarily removes a schedule, mutates it, then reinserts it. + // fn scope_schedule(&self, label: &ReflectSchedule, f: F) -> Result + // where + // F: FnOnce(&mut World, &mut Schedule) -> O; + + /// Retrieves a schedule by its name. + fn get_schedule_by_name(&self, schedule_name: String) -> Option; + + /// Loads a script asset from the given path. + fn load_script_asset<'a>( + &self, + asset_path: impl Into>, + ) -> Result, InteropError>; + + /// Returns the load state of a script asset. + fn get_script_asset_load_state( + &self, + script: Handle, + ) -> Result; + + /// Constructs a reflected value from a type and field payload. + fn construct( + &self, + type_: ScriptTypeRegistration, + payload: HashMap, + one_indexed: bool, + ) -> Result, InteropError>; + + /// Attempts to call a function overload matching the provided arguments. + fn try_call_overloads( + &self, + type_id: TypeId, + name: impl Into>, + args: Vec, + context: FunctionCallContext, + ) -> Result; + + /// Lists all functions available on a type (including reference methods). + fn get_functions_on_type( + &self, + type_id: TypeId, + ) -> Vec<(Cow<'static, str>, DynamicScriptFunction)>; + + /// Looks up a function by name across multiple type namespaces. + fn lookup_function( + &self, + type_ids: impl IntoIterator, + name: impl Into>, + ) -> Result>; + + /// Resolves a type registration by name. + fn get_type_by_name(&self, type_name: &str) -> Option; + + /// Resolves a type registration and determines if it's a component or resource. + fn get_type_registration( + &self, + registration: ScriptTypeRegistration, + ) -> Result< + Union< + ScriptTypeRegistration, + Union, + >, + InteropError, + >; + + /// Resolves a type registration by name with component/resource detection. + fn get_type_registration_by_name( + &self, + type_name: String, + ) -> Result< + Option< + Union< + ScriptTypeRegistration, + Union, + >, + >, + InteropError, + >; + + /// Attempts to interpret a type as a resource type. + fn get_resource_type( + &self, + registration: ScriptTypeRegistration, + ) -> Result, InteropError>; + + /// Attempts to interpret a type as a component type. + fn get_component_type( + &self, + registration: ScriptTypeRegistration, + ) -> Result, InteropError>; + + /// Returns the script function registry. + fn script_function_registry(&'_ self) -> Ref<'_, AppScriptFunctionRegistry>; + + /// Returns the allocator used for reflection. + fn allocator(&'_ self) -> Ref<'_, AppReflectAllocator>; + + /// Returns the component registry. + fn component_registry(&'_ self) -> Ref<'_, AppScriptComponentRegistry>; + + /// Returns the schedule registry. + fn schedule_registry(&'_ self) -> Ref<'_, AppScheduleRegistry>; + + /// Returns the current attachment if the guard is being used in the context of one. + fn current_attachment(&self) -> CurrentScriptAttachment; + + /// Sets the current attachment for the world guard context. + fn set_current_attachment(&self, attachment: ScriptAttachment); + + /// Registers a dynamic script component, and returns a reference to its registration + fn register_script_component( + &self, + component_name: String, + ) -> Result; + + /// Initializes cached registries from the world. + fn setup_cache(world: &World, attachment: CurrentScriptAttachment) -> RegistryCache; + + /// Initializes cached registries from the world, from raw components. + fn setup_cache_raw( + attachment: CurrentScriptAttachment, + allocator: AppReflectAllocator, + function_registry: AppScriptFunctionRegistry, + schedule_registry: AppScheduleRegistry, + component_registry: AppScriptComponentRegistry, + ) -> RegistryCache; +} + +impl<'w> WorldExtensions for WorldAccessGuard<'w> { + fn spawn(&self) -> Result { + self.with_world_mut(|world| { + let mut command_queue = CommandQueue::default(); + let mut commands = Commands::new(&mut command_queue, world); + let id = commands.spawn_empty().id(); + command_queue.apply(world); + id + }) + } + + fn despawn(&self, entity: Entity) -> Result<(), InteropError> { + if !self.is_valid_entity(entity)? { + return Err(InteropError::missing_entity(entity)); + } + + self.with_world_mut(|world| { + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, world); + commands.entity(entity).remove::().despawn(); + queue.apply(world); + }) + } + + fn despawn_recursive(&self, parent: Entity) -> Result<(), InteropError> { + if !self.is_valid_entity(parent)? { + return Err(InteropError::missing_entity(parent)); + } + self.with_world_mut(|world| { + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, world); + commands.entity(parent).despawn(); + queue.apply(world); + }) + } + + fn despawn_descendants(&self, parent: Entity) -> Result<(), InteropError> { + if !self.is_valid_entity(parent)? { + return Err(InteropError::missing_entity(parent)); + } + + self.with_world_mut(|world| { + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, world); + commands.entity(parent).despawn_related::(); + queue.apply(world); + }) + } + + fn is_valid_entity(&self, entity: Entity) -> Result { + let cell = self.as_unsafe_world_cell()?; + Ok(cell.get_entity(entity).is_ok() && entity.index().index() != 0) + } + + fn has_entity(&self, entity: Entity) -> Result { + self.is_valid_entity(entity) + } + + fn query( + &self, + query: crate::ScriptQueryBuilder, + ) -> Result, InteropError> { + self.with_world_mut(|world| { + let mut built_query = query.as_query_state::(world); + let query_result = built_query.iter(world); + + Ok(query_result + .map(|r| { + let references: Vec<_> = query + .components + .iter() + .map(|c| ReflectReference { + base: super::ReflectBaseType { + type_id: c.type_registration().type_id(), + base_id: super::ReflectBase::Component(r.id(), c.component_id()), + }, + reflect_path: Default::default(), + }) + .collect(); + ScriptQueryResult { + entity: r.id(), + components: references, + } + }) + .collect()) + })? + } + + /// insert the component into the entity + fn insert_component( + &self, + entity: Entity, + registration: ScriptComponentRegistration, + value: ReflectReference, + ) -> Result<(), InteropError> { + let instance = >::from_script_ref( + registration.type_registration().type_id(), + ScriptValue::Reference(value), + self.clone(), + )?; + + let reflect = instance.try_into_reflect().map_err(|v| { + InteropError::failed_from_reflect( + Some(registration.type_registration().type_id()), + format!("instance produced by conversion to target type when inserting component is not a full reflect type: {v:?}"), + ) + })?; + + registration.insert_into_entity(self.clone(), entity, reflect) + } + + /// add a default component to an entity + fn add_default_component( + &self, + entity: Entity, + registration: ScriptComponentRegistration, + ) -> Result<(), InteropError> { + // we look for ReflectDefault or ReflectFromWorld data then a ReflectComponent data + let instance = if let Some(default_td) = registration + .type_registration() + .type_registration() + .data::() + { + default_td.default() + } else if let Some(from_world_td) = registration + .type_registration() + .type_registration() + .data::() + { + self.with_world_mut(|world| from_world_td.from_world(world))? + } else { + return Err(InteropError::missing_type_data( + registration.registration.type_id(), + "ReflectDefault or ReflectFromWorld".to_owned(), + )); + }; + + registration.insert_into_entity(self.clone(), entity, instance) + } + + /// remove the component from the entity + fn remove_component( + &self, + entity: Entity, + registration: ScriptComponentRegistration, + ) -> Result<(), InteropError> { + registration.remove_from_entity(self.clone(), entity) + } + + /// get the component from the entity + fn get_component( + &self, + entity: Entity, + component_registration: ScriptComponentRegistration, + ) -> Result, InteropError> { + let cell = self.as_unsafe_world_cell()?; + let entity = cell + .get_entity(entity) + .map_err(|_| InteropError::missing_entity(entity))?; + + if entity.contains_id(component_registration.component_id) { + Ok(Some(ReflectReference { + base: ReflectBaseType { + type_id: component_registration.type_registration().type_id(), + base_id: ReflectBase::Component( + entity.id(), + component_registration.component_id, + ), + }, + reflect_path: Default::default(), + })) + } else { + Ok(None) + } + } + + /// check if the entity has the component + fn has_component( + &self, + entity: Entity, + component_id: ComponentId, + ) -> Result { + let cell = self.as_unsafe_world_cell()?; + let entity = cell + .get_entity(entity) + .map_err(|_| InteropError::missing_entity(entity))?; + + Ok(entity.contains_id(component_id)) + } + + fn with_component) -> O>( + &self, + entity: Entity, + f: F, + ) -> Result { + let type_id = std::any::TypeId::of::(); + let component_id = self.get_component_id(type_id)?.ok_or( + InteropError::unregistered_component_or_resource_type(type_id), + )?; + let cell = self.as_unsafe_world_cell()?; + // Safety: we claimed access to this component + self.with_read_access(component_id, || { + f(unsafe { cell.get_entity(entity).ok().and_then(|e| e.get::()) }) + }) + .map_err(Into::into) + } + + fn with_component_mut, O, F: FnOnce(Option>) -> O>( + &self, + entity: Entity, + f: F, + ) -> Result { + let type_id = std::any::TypeId::of::(); + let component_id = self.get_component_id(type_id)?.ok_or( + InteropError::unregistered_component_or_resource_type(type_id), + )?; + let cell = self.as_unsafe_world_cell()?; + // Safety: we claimed access to this component + self.with_write_access(component_id, || { + f(unsafe { cell.get_entity(entity).ok().and_then(|e| e.get_mut::()) }) + }) + .map_err(Into::into) + } + + fn with_or_insert_component_mut< + C: Component + Default, + O, + F: FnOnce(&mut C) -> O, + >( + &self, + entity: Entity, + f: F, + ) -> Result { + self.with_world_mut(|world| match world.get_mut::(entity) { + Some(mut component) => f(&mut component), + None => { + let mut component = C::default(); + let mut commands = world.commands(); + let result = f(&mut component); + commands.entity(entity).insert(component); + world.flush(); + result + } + }) + } + + /// get the given resource + fn get_resource( + &self, + resource_id: ComponentId, + ) -> Result, InteropError> { + let cell = self.as_unsafe_world_cell()?; + let component_info = match cell.components().get_info(resource_id) { + Some(info) => info, + None => return Ok(None), + }; + + Ok(Some(ReflectReference { + base: ReflectBaseType { + type_id: component_info + .type_id() + .ok_or_else(|| { + InteropError::unsupported_operation( + None, + None, + format!( + "Resource {} does not have a type id. Such resources are not supported by BMS.", + component_info.name() + ), + ) + })?, + base_id: ReflectBase::Resource(resource_id), + }, + reflect_path: Default::default(), + })) + } + + fn remove_resource( + &self, + registration: ScriptResourceRegistration, + ) -> Result<(), InteropError> { + let component_data = registration + .type_registration() + .type_registration() + .data::() + .ok_or_else(|| { + InteropError::missing_type_data( + registration.registration.type_id(), + "ReflectResource".to_owned(), + ) + })?; + + // TODO: this shouldn't need entire world access it feels + self.with_world_mut(|world| component_data.remove(world)) + } + + fn has_resource(&self, resource_id: ComponentId) -> Result { + let cell = self.as_unsafe_world_cell()?; + // Safety: we are not reading the value at all + let res_ptr = unsafe { cell.get_resource_by_id(resource_id) }; + Ok(res_ptr.is_some()) + } + + fn with_resource O>(&self, f: F) -> Result { + let type_id = std::any::TypeId::of::(); + let resource_id = self.get_resource_id(type_id)?.ok_or( + InteropError::unregistered_component_or_resource_type(type_id), + )?; + let cell = self.as_unsafe_world_cell()?; + // Safety: we claimed access to this resource + self.with_read_access_and_then(resource_id, || { + Ok(f(unsafe { + cell.get_resource::() + .ok_or_else(|| InteropError::missing_resource(type_id))? + })) + }) + } + + fn with_resource_mut) -> O>( + &self, + f: F, + ) -> Result { + let type_id = std::any::TypeId::of::(); + let resource_id = self.get_resource_id(type_id)?.ok_or( + InteropError::unregistered_component_or_resource_type(type_id), + )?; + let cell = self.as_unsafe_world_cell()?; + // Safety: we claimed access to this resource + self.with_write_access_and_then(resource_id, || { + Ok(f(unsafe { + cell.get_resource_mut::() + .ok_or_else(|| InteropError::missing_resource(type_id))? + })) + }) + } + + fn with_world O>(&self, f: F) -> Result { + self.with_world_access(f).map_err(Into::into) + } + + fn with_world_mut O>(&self, f: F) -> Result { + self.with_world_mut_access(f).map_err(Into::into) + } + + fn get_parent(&self, entity: Entity) -> Result, InteropError> { + if !self.is_valid_entity(entity)? { + return Err(InteropError::missing_entity(entity)); + } + + self.with_component(entity, |c: Option<&ChildOf>| c.map(|c| c.parent())) + } + + fn get_children(&self, entity: Entity) -> Result, InteropError> { + if !self.is_valid_entity(entity)? { + return Err(InteropError::missing_entity(entity)); + } + + self.with_component(entity, |c: Option<&Children>| { + c.map(|c| c.to_vec()).unwrap_or_default() + }) + } + + fn push_children(&self, parent: Entity, children: &[Entity]) -> Result<(), InteropError> { + // verify entities exist + if !self.is_valid_entity(parent)? { + return Err(InteropError::missing_entity(parent)); + } + for c in children { + if !self.is_valid_entity(*c)? { + return Err(InteropError::missing_entity(*c)); + } + } + self.with_world_mut(|world| { + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, world); + commands.entity(parent).add_children(children); + queue.apply(world); + }) + } + + fn insert_children( + &self, + parent: Entity, + index: usize, + children: &[Entity], + ) -> Result<(), InteropError> { + if !self.is_valid_entity(parent)? { + return Err(InteropError::missing_entity(parent)); + } + + for c in children { + if !self.is_valid_entity(*c)? { + return Err(InteropError::missing_entity(*c)); + } + } + + self.with_world_mut(|world| { + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, world); + commands.entity(parent).insert_children(index, children); + queue.apply(world); + }) + } + + fn remove_children(&self, parent: Entity, children: &[Entity]) -> Result<(), InteropError> { + if !self.is_valid_entity(parent)? { + return Err(InteropError::missing_entity(parent)); + } + + for c in children { + if !self.is_valid_entity(*c)? { + return Err(InteropError::missing_entity(*c)); + } + } + self.with_world_mut(|world| { + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, world); + commands.entity(parent).detach_children(children); + queue.apply(world); + }) + } + + fn exit(&self) -> Result<(), InteropError> { + self.with_world_mut(|world| { + world.write_message(AppExit::Success); + }) + } + + fn get_schedule_by_name(&self, schedule_name: String) -> Option { + let schedule_registry = self.schedule_registry(); + let schedule_registry = schedule_registry.read(); + + schedule_registry + .get_schedule_by_name(&schedule_name) + .cloned() + } + + fn load_script_asset<'a>( + &self, + asset_path: impl Into>, + ) -> Result, InteropError> { + self.with_resource(|r: &AssetServer| r.load(asset_path)) + } + + fn get_script_asset_load_state( + &self, + script: Handle, + ) -> Result { + self.with_resource(|r: &AssetServer| r.load_state(script.id())) + } + + fn construct( + &self, + type_: ScriptTypeRegistration, + mut payload: HashMap, + one_indexed: bool, + ) -> Result, InteropError> { + // figure out the kind of type we're building + let type_info = type_.registration.type_info(); + // we just need to a) extract fields, if enum we need a "variant" field specifying the variant + // then build the corresponding dynamic structure, whatever it may be + + let dynamic: Box = match type_info { + TypeInfo::Struct(struct_info) => { + let fields_iter = struct_info + .field_names() + .iter() + .map(|f| { + Ok(( + *f, + struct_info + .field(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id(), + )) + }) + .collect::, InteropError>>()?; + let mut dynamic = construct_dynamic_struct(self, &mut payload, fields_iter)?; + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + TypeInfo::TupleStruct(tuple_struct_info) => { + let fields_iter = (0..tuple_struct_info.field_len()) + .map(|f| { + Ok(tuple_struct_info + .field_at(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id()) + }) + .collect::, InteropError>>()?; + + let mut dynamic = + construct_dynamic_tuple_struct(self, &mut payload, fields_iter, one_indexed)?; + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + TypeInfo::Tuple(tuple_info) => { + let fields_iter = (0..tuple_info.field_len()) + .map(|f| { + Ok(tuple_info + .field_at(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id()) + }) + .collect::, InteropError>>()?; + + let mut dynamic = + construct_dynamic_tuple(self, &mut payload, fields_iter, one_indexed)?; + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + TypeInfo::Enum(enum_info) => { + // extract variant from "variant" + let variant = payload.remove("variant").ok_or_else(|| { + InteropError::function_interop_error( + "construct", + Namespace::OnType(TypeId::of::()), + InteropError::str("missing 'variant' field in enum constructor payload"), + None, + ) + })?; + + let variant_name = String::from_script(variant, self.clone())?; + + let variant = enum_info.variant(&variant_name).ok_or_else(|| { + InteropError::function_interop_error( + "construct", + Namespace::OnType(TypeId::of::()), + InteropError::string(format!( + "invalid variant name '{}' for enum '{}'", + variant_name, + enum_info.type_path() + )), + None, + ) + })?; + + let variant = match variant { + VariantInfo::Struct(struct_variant_info) => { + // same as above struct variant + let fields_iter = struct_variant_info + .field_names() + .iter() + .map(|f| { + Ok(( + *f, + struct_variant_info + .field(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id(), + )) + }) + .collect::, InteropError>>()?; + + let dynamic = construct_dynamic_struct(self, &mut payload, fields_iter)?; + DynamicVariant::Struct(dynamic) + } + VariantInfo::Tuple(tuple_variant_info) => { + // same as tuple variant + let fields_iter = (0..tuple_variant_info.field_len()) + .map(|f| { + Ok(tuple_variant_info + .field_at(f) + .ok_or_else(|| { + InteropError::invariant( + "field in field_names should have reflection information", + ) + })? + .type_id()) + }) + .collect::, InteropError>>()?; + + let dynamic = + construct_dynamic_tuple(self, &mut payload, fields_iter, one_indexed)?; + DynamicVariant::Tuple(dynamic) + } + VariantInfo::Unit(_) => DynamicVariant::Unit, + }; + let mut dynamic = DynamicEnum::new(variant_name, variant); + dynamic.set_represented_type(Some(type_info)); + Box::new(dynamic) + } + _ => { + return Err(InteropError::unsupported_operation( + Some(type_info.type_id()), + Some(Box::new(payload)), + "Type constructor not supported", + )); + } + }; + + // try to construct type from reflect + // TODO: it would be nice to have a ::from_reflect_with_fallback equivalent, that does exactly that + // only using this as it's already there and convenient, the clone variant hitting will be confusing to end users + ::from_reflect_or_clone(dynamic.as_ref(), self.clone()) + } + + fn try_call_overloads( + &self, + type_id: TypeId, + name: impl Into>, + args: Vec, + context: FunctionCallContext, + ) -> Result { + let registry = self.script_function_registry(); + let registry = registry.read(); + + let name = name.into(); + let overload_iter = match registry.iter_overloads(Namespace::OnType(type_id), name) { + Ok(iter) => iter, + Err(name) => { + return Err(InteropError::missing_function( + name.to_string(), + Namespace::OnType(type_id), + Some(context.clone()), + )); + } + }; + + let mut last_error = None; + for overload in overload_iter { + match overload.call(args.clone(), context.clone()) { + Ok(out) => return Ok(out), + Err(e) => last_error = Some(e), + } + } + + Err(last_error.ok_or_else(|| InteropError::invariant("invariant, iterator should always return at least one item, and if the call fails it should return an error"))?) + } + + fn get_functions_on_type( + &self, + type_id: TypeId, + ) -> Vec<(Cow<'static, str>, DynamicScriptFunction)> { + let registry = self.script_function_registry(); + let registry = registry.read(); + + registry + .iter_namespace(Namespace::OnType(type_id)) + .chain( + registry + .iter_namespace(Namespace::OnType(std::any::TypeId::of::())), + ) + .map(|(key, func)| (key.name.clone(), func.clone())) + .collect() + } + + fn lookup_function( + &self, + type_ids: impl IntoIterator, + name: impl Into>, + ) -> Result> { + let registry = self.script_function_registry(); + let registry = registry.read(); + + let mut name = name.into(); + for type_id in type_ids { + name = match registry.get_function(Namespace::OnType(type_id), name) { + Ok(func) => return Ok(func.clone()), + Err(name) => name, + }; + } + + Err(name) + } + + fn get_type_by_name(&self, type_name: &str) -> Option { + let type_registry = self.type_registry(); + let type_registry = type_registry.read(); + type_registry + .get_with_short_type_path(type_name) + .or_else(|| type_registry.get_with_type_path(type_name)) + .map(|registration| ScriptTypeRegistration::new(Arc::new(registration.clone()))) + } + + fn get_type_registration( + &self, + registration: ScriptTypeRegistration, + ) -> Result< + Union< + ScriptTypeRegistration, + Union, + >, + InteropError, + > { + let registration = match self.get_resource_type(registration)? { + Ok(res) => { + return Ok(Union::new_right(Union::new_right(res))); + } + Err(registration) => registration, + }; + + let registration = match self.get_component_type(registration)? { + Ok(comp) => { + return Ok(Union::new_right(Union::new_left(comp))); + } + Err(registration) => registration, + }; + + Ok(Union::new_left(registration)) + } + + fn get_type_registration_by_name( + &self, + type_name: String, + ) -> Result< + Option< + Union< + ScriptTypeRegistration, + Union, + >, + >, + InteropError, + > { + let val = self.get_type_by_name(&type_name); + Ok(match val { + Some(registration) => Some(self.get_type_registration(registration)?), + None => { + // try the component registry + let components = self.component_registry(); + let components = components.read(); + components + .get(&type_name) + .map(|c| Union::new_right(Union::new_left(c.registration.clone()))) + } + }) + } + + fn get_resource_type( + &self, + registration: ScriptTypeRegistration, + ) -> Result, InteropError> { + Ok(match self.get_resource_id(registration.type_id())? { + Some(resource_id) => Ok(ScriptResourceRegistration::new(registration, resource_id)), + None => Err(registration), + }) + } + + fn get_component_type( + &self, + registration: ScriptTypeRegistration, + ) -> Result, InteropError> { + Ok(match self.get_component_id(registration.type_id())? { + Some(comp_id) => Ok(ScriptComponentRegistration::new(registration, comp_id)), + None => Err(registration), + }) + } + + fn script_function_registry(&'_ self) -> Ref<'_, AppScriptFunctionRegistry> { + #[allow( + clippy::unwrap_used, + reason = "internal domain boundary, enforced at creation of the guard" + )] + self.get_cached_registry().unwrap() + } + + fn allocator(&'_ self) -> Ref<'_, AppReflectAllocator> { + #[allow( + clippy::unwrap_used, + reason = "internal domain boundary, enforced at creation of the guard" + )] + self.get_cached_registry().unwrap() + } + + fn component_registry(&'_ self) -> Ref<'_, AppScriptComponentRegistry> { + #[allow( + clippy::unwrap_used, + reason = "internal domain boundary, enforced at creation of the guard" + )] + self.get_cached_registry().unwrap() + } + + fn schedule_registry(&'_ self) -> Ref<'_, AppScheduleRegistry> { + #[allow( + clippy::unwrap_used, + reason = "internal domain boundary, enforced at creation of the guard" + )] + self.get_cached_registry().unwrap() + } + + fn current_attachment(&self) -> CurrentScriptAttachment { + self.get_cached_registry::() + .map(|r| r.clone()) + .unwrap_or(CurrentScriptAttachment(None)) + } + + fn set_current_attachment(&self, attachment: ScriptAttachment) { + self.set_cached_registry::(CurrentScriptAttachment(Some( + attachment, + ))); + } + + fn register_script_component( + &self, + component_name: String, + ) -> Result { + let component_registry = self.component_registry(); + let component_registry_read = component_registry.read(); + if component_registry_read.get(&component_name).is_some() { + return Err(InteropError::unsupported_operation( + None, + None, + "script registered component already exists", + )); + } + + let component_id = self.with_world_mut_access(|w| { + let descriptor = unsafe { + // Safety: same safety guarantees as ComponentDescriptor::new + // we know the type in advance + // we only use this method to name the component + ComponentDescriptor::new_with_layout( + component_name.clone(), + DynamicComponent::STORAGE_TYPE, + Layout::new::(), + needs_drop::().then_some(|x| x.drop_as::()), + true, + ComponentCloneBehavior::Default, + None, + ) + }; + w.register_component_with_descriptor(descriptor) + })?; + drop(component_registry_read); + let mut component_registry = component_registry.write(); + + let registration = ScriptComponentRegistration::new( + ScriptTypeRegistration::new(Arc::new( + ::get_type_registration(), + )), + component_id, + ); + + let component_info = DynamicComponentInfo { + name: component_name.clone(), + registration: registration.clone(), + }; + + component_registry.register(component_info); + + // TODO: we should probably retrieve this from the registry, but I don't see what people would want to register on this type + // in addition to the existing registrations. + Ok(registration) + } + + fn setup_cache_raw( + attachment: CurrentScriptAttachment, + allocator: AppReflectAllocator, + function_registry: AppScriptFunctionRegistry, + schedule_registry: AppScheduleRegistry, + component_registry: AppScriptComponentRegistry, + ) -> RegistryCache { + debug_assert_eq!(AppReflectAllocator::SLOT, 0); + debug_assert_eq!(AppScriptFunctionRegistry::SLOT, 1); + debug_assert_eq!(AppScheduleRegistry::SLOT, 2); + debug_assert_eq!(AppScriptComponentRegistry::SLOT, 3); + debug_assert_eq!(CurrentScriptAttachment::SLOT, 4); + + [ + Rc::new(RefCell::new(allocator)), + Rc::new(RefCell::new(function_registry)), + Rc::new(RefCell::new(schedule_registry)), + Rc::new(RefCell::new(component_registry)), + Rc::new(RefCell::new(attachment)), + ] + } + + fn setup_cache(world: &World, attachment: CurrentScriptAttachment) -> RegistryCache { + debug_assert_eq!(AppReflectAllocator::SLOT, 0); + debug_assert_eq!(AppScriptFunctionRegistry::SLOT, 1); + debug_assert_eq!(AppScheduleRegistry::SLOT, 2); + debug_assert_eq!(AppScriptComponentRegistry::SLOT, 3); + debug_assert_eq!(CurrentScriptAttachment::SLOT, 4); + + [ + Rc::new(RefCell::new( + world + .get_resource::() + .cloned() + .unwrap_or_default(), + )), + Rc::new(RefCell::new( + world + .get_resource::() + .cloned() + .unwrap_or_default(), + )), + Rc::new(RefCell::new( + world + .get_resource::() + .cloned() + .unwrap_or_default(), + )), + Rc::new(RefCell::new( + world + .get_resource::() + .cloned() + .unwrap_or_default(), + )), + Rc::new(RefCell::new(attachment)), + ] + } + + // /// Creates a system from a system builder and inserts it into the given schedule + // pub fn add_system( + // &self, + // schedule: &ReflectSchedule, + // builder: ScriptSystemBuilder, + // ) -> Result { + // debug!( + // "Adding script system '{}' for script '{}' to schedule '{}'", + // builder.name, + // builder.attachment, + // schedule.identifier() + // ); + + // builder.build::

(self.clone(), schedule) + // } +} + +fn construct_from_script_value( + guard: &WorldGuard, + descriptor: impl Into>, + type_id: TypeId, + value: Option, +) -> Result, InteropError> { + // if the value is missing, try to construct a default and return it + let value = match value { + Some(value) => value, + None => { + let type_registry = guard.type_registry(); + let type_registry = type_registry.read(); + let default_data = type_registry + .get_type_data::(type_id) + .ok_or_else(|| { + InteropError::function_interop_error( + "construct", + Namespace::OnType(TypeId::of::()), + InteropError::string(format!( + "field missing and no default provided: '{}'", + descriptor.into() + )), + None, + ) + })?; + return Ok(default_data.default().into_partial_reflect()); + } + }; + + // otherwise we need to use from_script_ref + >::from_script_ref(type_id, value, guard.clone()) +} + +fn construct_dynamic_struct( + guard: &WorldGuard, + payload: &mut HashMap, + fields: Vec<(&'static str, TypeId)>, +) -> Result { + let mut dynamic = DynamicStruct::default(); + for (field_name, field_type_id) in fields { + let constructed = construct_from_script_value( + guard, + field_name, + field_type_id, + payload.remove(field_name), + )?; + + dynamic.insert_boxed(field_name, constructed); + } + Ok(dynamic) +} + +fn construct_dynamic_tuple_struct( + guard: &WorldGuard, + payload: &mut HashMap, + fields: Vec, + one_indexed: bool, +) -> Result { + let mut dynamic = DynamicTupleStruct::default(); + for (field_idx, field_type_id) in fields.into_iter().enumerate() { + // correct for indexing + let script_idx = if one_indexed { + field_idx + 1 + } else { + field_idx + }; + let field_string = script_idx.to_string(); + dynamic.insert_boxed(construct_from_script_value( + guard, + field_string.clone(), + field_type_id, + payload.remove(&field_string), + )?); + } + Ok(dynamic) +} + +fn construct_dynamic_tuple( + guard: &WorldGuard, + payload: &mut HashMap, + fields: Vec, + one_indexed: bool, +) -> Result { + let mut dynamic = DynamicTuple::default(); + for (field_idx, field_type_id) in fields.into_iter().enumerate() { + // correct for indexing + let script_idx = if one_indexed { + field_idx + 1 + } else { + field_idx + }; + + let field_string = script_idx.to_string(); + + dynamic.insert_boxed(construct_from_script_value( + guard, + field_string.clone(), + field_type_id, + payload.remove(&field_string), + )?); + } + Ok(dynamic) +} + +impl CachedRegistry for AppReflectAllocator { + const SLOT: usize = 0; +} +impl CachedRegistry for AppScriptFunctionRegistry { + const SLOT: usize = 1; +} +impl CachedRegistry for AppScheduleRegistry { + const SLOT: usize = 2; +} +impl CachedRegistry for AppScriptComponentRegistry { + const SLOT: usize = 3; +} + +/// A wrapper around [`ScriptAttachment`] implementing [`CachedRegistry`] +#[derive(Clone, Default)] +pub struct CurrentScriptAttachment(pub Option); +impl CachedRegistry for CurrentScriptAttachment { + const SLOT: usize = 4; +} + +#[cfg(test)] +mod test { + use super::*; + use bevy_reflect::{GetTypeRegistration, Reflect, ReflectFromReflect}; + use std::sync::Arc; + use test_utils::test_data::{ + CompWithDefaultAndComponentData, GetTestEntityId, SimpleEnum, SimpleStruct, + SimpleTupleStruct, TestResource, UnitStruct, setup_world, + }; + + #[test] + fn test_construct_struct() { + let mut world = setup_world(|_, _| {}); + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); + let world = WorldAccessGuard::new_exclusive(&mut world, cache); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry.get(TypeId::of::()).unwrap().clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + let payload = HashMap::from_iter(vec![("foo".to_owned(), ScriptValue::Integer(1))]); + + let result = world.construct(type_registration, payload, false); + let expected = + Ok::<_, InteropError>(Box::new(SimpleStruct { foo: 1 }) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } + + #[test] + fn test_construct_tuple_struct() { + let mut world = setup_world(|_, _| {}); + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); + let world = WorldAccessGuard::new_exclusive(&mut world, cache); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry + .get(TypeId::of::()) + .unwrap() + .clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + // zero indexed + let payload = HashMap::from_iter(vec![("0".to_owned(), ScriptValue::Integer(1))]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = + Ok::<_, InteropError>(Box::new(SimpleTupleStruct(1)) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // one indexed + let payload = HashMap::from_iter(vec![("1".to_owned(), ScriptValue::Integer(1))]); + + let result = world.construct(type_registration, payload, true); + let expected = + Ok::<_, InteropError>(Box::new(SimpleTupleStruct(1)) as Box); + + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } + + #[test] + fn test_construct_tuple() { + let mut world = setup_world(|_, registry| { + registry.register::<(usize, usize)>(); + // TODO: does this ever get registered on normal types? I don't think so: https://github.com/bevyengine/bevy/issues/17981 + registry.register_type_data::<(usize, usize), ReflectFromReflect>(); + }); + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); + + ::get_type_registration(); + let world = WorldAccessGuard::new_exclusive(&mut world, cache); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry + .get(TypeId::of::<(usize, usize)>()) + .unwrap() + .clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + // zero indexed + let payload = HashMap::from_iter(vec![ + ("0".to_owned(), ScriptValue::Integer(1)), + ("1".to_owned(), ScriptValue::Integer(2)), + ]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = Ok::<_, InteropError>(Box::new((1, 2)) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // one indexed + let payload = HashMap::from_iter(vec![ + ("1".to_owned(), ScriptValue::Integer(1)), + ("2".to_owned(), ScriptValue::Integer(2)), + ]); + + let result = world.construct(type_registration.clone(), payload, true); + let expected = Ok::<_, InteropError>(Box::new((1, 2)) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } + + #[test] + fn test_construct_enum() { + let mut world = setup_world(|_, _| {}); + let cache = WorldAccessGuard::setup_cache(&world, CurrentScriptAttachment::default()); + let world = WorldAccessGuard::new_exclusive(&mut world, cache); + + let registry = world.type_registry(); + let registry = registry.read(); + + let registration = registry.get(TypeId::of::()).unwrap().clone(); + let type_registration = ScriptTypeRegistration::new(Arc::new(registration)); + + // struct version + let payload = HashMap::from_iter(vec![ + ("foo".to_owned(), ScriptValue::Integer(1)), + ("variant".to_owned(), ScriptValue::String("Struct".into())), + ]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = Ok::<_, InteropError>( + Box::new(SimpleEnum::Struct { foo: 1 }) as Box + ); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // tuple struct version + let payload = HashMap::from_iter(vec![ + ("0".to_owned(), ScriptValue::Integer(1)), + ( + "variant".to_owned(), + ScriptValue::String("TupleStruct".into()), + ), + ]); + + let result = world.construct(type_registration.clone(), payload, false); + let expected = + Ok::<_, InteropError>(Box::new(SimpleEnum::TupleStruct(1)) as Box); + + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + + // unit version + let payload = HashMap::from_iter(vec![( + "variant".to_owned(), + ScriptValue::String("Unit".into()), + )]); + + let result = world.construct(type_registration, payload, false); + let expected = Ok::<_, InteropError>(Box::new(SimpleEnum::Unit) as Box); + pretty_assertions::assert_str_eq!(format!("{result:#?}"), format!("{expected:#?}")); + } + + fn make_guard(world: &mut World) -> WorldAccessGuard<'_> { + let cache = WorldAccessGuard::setup_cache(world, CurrentScriptAttachment::default()); + WorldAccessGuard::new_exclusive(world, cache) + } + + fn comp_reg( + guard: &WorldAccessGuard<'_>, + ) -> ScriptComponentRegistration { + let short = std::any::type_name::().split("::").last().unwrap(); + guard + .get_component_type(guard.get_type_by_name(short).unwrap()) + .unwrap() + .unwrap() + } + + fn res_reg(guard: &WorldAccessGuard<'_>) -> ScriptResourceRegistration { + let short = std::any::type_name::().split("::").last().unwrap(); + guard + .get_resource_type(guard.get_type_by_name(short).unwrap()) + .unwrap() + .unwrap() + } + + // ── spawn / is_valid_entity / has_entity ────────────────────────────────── + + #[test] + fn spawn_produces_valid_entity() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + assert!(guard.is_valid_entity(e).unwrap()); + assert_eq!( + guard.is_valid_entity(e).unwrap(), + guard.has_entity(e).unwrap() + ); + } + + // ── despawn ─────────────────────────────────────────────────────────────── + + #[test] + fn despawn_invalidates_entity() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + guard.despawn(e).unwrap(); + assert!(!guard.is_valid_entity(e).unwrap()); + } + + #[test] + fn despawn_missing_entity_errors() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + guard.despawn(e).unwrap(); + assert!(guard.despawn(e).is_err()); + } + + // ── despawn_recursive / despawn_descendants ─────────────────────────────── + + #[test] + fn despawn_recursive_removes_parent_and_child() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let parent = guard.spawn().unwrap(); + let child = guard.spawn().unwrap(); + guard.push_children(parent, &[child]).unwrap(); + guard.despawn_recursive(parent).unwrap(); + assert!(!guard.is_valid_entity(parent).unwrap()); + assert!(!guard.is_valid_entity(child).unwrap()); + } + + #[test] + fn despawn_descendants_keeps_parent_removes_child() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let parent = guard.spawn().unwrap(); + let child = guard.spawn().unwrap(); + guard.push_children(parent, &[child]).unwrap(); + guard.despawn_descendants(parent).unwrap(); + assert!(guard.is_valid_entity(parent).unwrap()); + assert!(!guard.is_valid_entity(child).unwrap()); + } + + // ── component CRUD ──────────────────────────────────────────────────────── + + #[test] + fn add_default_has_get_remove_component_roundtrip() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + let reg = comp_reg::(&guard); + + assert!(!guard.has_component(e, reg.component_id).unwrap()); + assert!(guard.get_component(e, reg.clone()).unwrap().is_none()); + + guard.add_default_component(e, reg.clone()).unwrap(); + + assert!(guard.has_component(e, reg.component_id).unwrap()); + assert!(guard.get_component(e, reg.clone()).unwrap().is_some()); + + guard.remove_component(e, reg.clone()).unwrap(); + + assert!(!guard.has_component(e, reg.component_id).unwrap()); + } + + // ── with_component access-map: all four conflict cases ──────────────────── + + #[test] + fn with_component_read_read_succeeds() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + guard + .add_default_component(e, comp_reg::(&guard)) + .unwrap(); + + // two shared borrows of the same component must not conflict + let result = guard.with_component(e, |_: Option<&CompWithDefaultAndComponentData>| { + guard.with_component(e, |_: Option<&CompWithDefaultAndComponentData>| ()) + }); + assert!(result.unwrap().is_ok()); + } + + #[test] + fn with_component_write_read_conflicts() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + guard + .add_default_component(e, comp_reg::(&guard)) + .unwrap(); + + let result = + guard.with_component_mut(e, |_: Option>| { + guard.with_component(e, |_: Option<&CompWithDefaultAndComponentData>| ()) + }); + assert!(result.unwrap().is_err()); + } + + #[test] + fn with_component_read_write_conflicts() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + guard + .add_default_component(e, comp_reg::(&guard)) + .unwrap(); + + let result = guard.with_component(e, |_: Option<&CompWithDefaultAndComponentData>| { + guard.with_component_mut(e, |_: Option>| ()) + }); + assert!(result.unwrap().is_err()); + } + + #[test] + fn with_component_write_write_conflicts() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + guard + .add_default_component(e, comp_reg::(&guard)) + .unwrap(); + + let result = + guard.with_component_mut(e, |_: Option>| { + guard.with_component_mut(e, |_: Option>| ()) + }); + assert!(result.unwrap().is_err()); + } + + // ── with_or_insert_component_mut ───────────────────────────────────────── + + #[test] + fn with_or_insert_inserts_when_absent_and_mutates_when_present() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let e = guard.spawn().unwrap(); + + // absent: inserts default then applies mutation + guard + .with_or_insert_component_mut(e, |c: &mut UnitStruct| { + let _ = c; + }) + .unwrap(); + assert!( + guard + .has_component(e, comp_reg::(&guard).component_id) + .unwrap() + ); + + // present: mutates existing value + guard + .with_or_insert_component_mut(e, |c: &mut UnitStruct| { + let _ = c; + }) + .unwrap(); + } + + // ── resource operations ─────────────────────────────────────────────────── + + #[test] + fn has_resource_and_get_resource_and_remove_resource() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let reg = res_reg::(&guard); + + assert!(guard.has_resource(reg.resource_id).unwrap()); + assert!(guard.get_resource(reg.resource_id).unwrap().is_some()); + + guard.remove_resource(reg.clone()).unwrap(); + + assert!(!guard.has_resource(reg.resource_id).unwrap()); + // this might be unexpected but the resource component ID persists for some reason + // I guess it gets re-used if the resource is re-inserted, but + // the reference will fail + let ref_ = guard.get_resource(reg.resource_id).unwrap().unwrap(); + assert!(ref_.downcast::(guard).is_err()); + } + + // ── with_resource access-map: all four conflict cases ──────────────────── + + #[test] + fn with_resource_read_read_succeeds() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + + let result = + guard.with_resource(|_: &TestResource| guard.with_resource(|_: &TestResource| ())); + assert!(result.unwrap().is_ok()); + } + + #[test] + fn with_resource_write_read_conflicts() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + + let result = guard + .with_resource_mut(|_: Mut| guard.with_resource(|_: &TestResource| ())); + assert!(result.unwrap().is_err()); + } + + #[test] + fn with_resource_read_write_conflicts() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + + let result = guard + .with_resource(|_: &TestResource| guard.with_resource_mut(|_: Mut| ())); + assert!(result.unwrap().is_err()); + } + + #[test] + fn with_resource_write_write_conflicts() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + + let result = guard.with_resource_mut(|_: Mut| { + guard.with_resource_mut(|_: Mut| ()) + }); + assert!(result.unwrap().is_err()); + } + + // ── hierarchy ───────────────────────────────────────────────────────────── + + #[test] + fn push_get_remove_children_and_get_parent() { + let mut world = setup_world(|_, _| {}); + world.register_component::(); + world.register_component::(); + let guard = make_guard(&mut world); + let parent = guard.spawn().unwrap(); + let c1 = guard.spawn().unwrap(); + let c2 = guard.spawn().unwrap(); + + assert_eq!(guard.get_parent(c1).unwrap(), None); + + guard.push_children(parent, &[c1, c2]).unwrap(); + + assert_eq!(guard.get_children(parent).unwrap().len(), 2); + assert_eq!(guard.get_parent(c1).unwrap(), Some(parent)); + + guard.remove_children(parent, &[c1]).unwrap(); + assert_eq!(guard.get_children(parent).unwrap(), vec![c2]); + } + + #[test] + fn insert_children_at_index() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let parent = guard.spawn().unwrap(); + let c1 = guard.spawn().unwrap(); + let c2 = guard.spawn().unwrap(); + let c3 = guard.spawn().unwrap(); + guard.push_children(parent, &[c1, c3]).unwrap(); + guard.insert_children(parent, 1, &[c2]).unwrap(); + assert_eq!(guard.get_children(parent).unwrap()[1], c2); + } + + #[test] + fn hierarchy_ops_error_on_missing_entity() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let ghost = guard.spawn().unwrap(); + guard.despawn(ghost).unwrap(); + + assert!(guard.get_parent(ghost).is_err()); + assert!(guard.get_children(ghost).is_err()); + assert!(guard.push_children(ghost, &[]).is_err()); + assert!(guard.insert_children(ghost, 0, &[]).is_err()); + assert!(guard.remove_children(ghost, &[]).is_err()); + assert!(guard.despawn_recursive(ghost).is_err()); + assert!(guard.despawn_descendants(ghost).is_err()); + } + + // ── type registry helpers ───────────────────────────────────────────────── + + #[test] + fn get_type_by_name_known_and_unknown() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + assert!(guard.get_type_by_name("SimpleStruct").is_some()); + assert!(guard.get_type_by_name("NoSuchType_XYZ").is_none()); + } + + #[test] + fn get_type_registration_classifies_correctly() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + + let plain = guard + .get_type_registration(guard.get_type_by_name("usize").unwrap()) + .unwrap(); + assert!(plain.is_left()); + + // component → Right(Left(_)) + let comp = guard + .get_type_registration( + guard + .get_type_by_name("CompWithDefaultAndComponentData") + .unwrap(), + ) + .unwrap(); + assert!(comp.into_right().unwrap().is_left()); + + // resource → Right(Right(_)) + let res = guard + .get_type_registration(guard.get_type_by_name("TestResource").unwrap()) + .unwrap(); + assert!(res.into_right().unwrap().is_right()); + } + + #[test] + fn get_type_registration_by_name_unknown_returns_none() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + assert!( + guard + .get_type_registration_by_name("NoSuchType_XYZ".to_owned()) + .unwrap() + .is_none() + ); + } + + // ── dynamic component registration ──────────────────────────────────────── + + #[test] + fn register_script_component_and_duplicate_errors() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + guard + .register_script_component("MyDynComp".to_owned()) + .unwrap(); + assert!( + guard + .register_script_component("MyDynComp".to_owned()) + .is_err() + ); + assert!(guard.component_registry().read().get("MyDynComp").is_some()); + } + + // ── query ───────────────────────────────────────────────────────────────── + + #[test] + fn query_returns_only_matching_entities() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let _without = guard.spawn().unwrap(); + let reg = comp_reg::(&guard); + let mut builder = ScriptQueryBuilder::new(); + builder.with_components(vec![reg]); + + let results = guard.query(builder).unwrap(); + assert_eq!(results.len(), 1); + assert_eq!( + results[0].entity, + CompWithDefaultAndComponentData::test_entity_id() + ); + } + + // ── construct error paths ───────────────────────────────────────────────── + + #[test] + fn construct_enum_missing_variant_errors() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let reg = ScriptTypeRegistration::new(Arc::new( + guard + .type_registry() + .read() + .get(TypeId::of::()) + .unwrap() + .clone(), + )); + assert!(guard.construct(reg, HashMap::new(), false).is_err()); + } + + #[test] + fn construct_enum_invalid_variant_errors() { + let mut world = setup_world(|_, _| {}); + let guard = make_guard(&mut world); + let reg = ScriptTypeRegistration::new(Arc::new( + guard + .type_registry() + .read() + .get(TypeId::of::()) + .unwrap() + .clone(), + )); + let payload = HashMap::from_iter(vec![( + "variant".to_owned(), + ScriptValue::String("Nonexistent".into()), + )]); + assert!(guard.construct(reg, payload, false).is_err()); + } + + // ── setup_cache_raw ─────────────────────────────────────────────────────── + + #[test] + fn setup_cache_raw_produces_functional_guard() { + let mut world = setup_world(|_, _| {}); + let cache = WorldAccessGuard::setup_cache_raw( + CurrentScriptAttachment::default(), + world + .get_resource::() + .cloned() + .unwrap_or_default(), + world + .get_resource::() + .cloned() + .unwrap_or_default(), + world + .get_resource::() + .cloned() + .unwrap_or_default(), + world + .get_resource::() + .cloned() + .unwrap_or_default(), + ); + let guard = WorldAccessGuard::new_exclusive(&mut world, cache); + assert!(guard.spawn().is_ok()); + } +} diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index f3189f40a7..932009fa2b 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -26,6 +26,7 @@ bevy_mod_scripting_bindings = { workspace = true } bevy_system_reflection = { workspace = true } bevy_mod_scripting_display = { workspace = true } bevy_mod_scripting_script = { workspace = true } +bevy_mod_scripting_world = { workspace = true } bevy_reflect = { workspace = true, default-features = false, features = [] } bevy_ecs = { workspace = true, default-features = false, features = [] } bevy_app = { workspace = true, default-features = false, features = [] } diff --git a/crates/bevy_mod_scripting_core/src/commands.rs b/crates/bevy_mod_scripting_core/src/commands.rs index c448f4a0f6..29523978db 100644 --- a/crates/bevy_mod_scripting_core/src/commands.rs +++ b/crates/bevy_mod_scripting_core/src/commands.rs @@ -16,9 +16,10 @@ use crate::{ }; use bevy_ecs::{system::Command, world::World}; use bevy_log::trace; -use bevy_mod_scripting_bindings::{ScriptValue, WorldGuard}; +use bevy_mod_scripting_bindings::{CurrentScriptAttachment, ScriptValue, WorldExtensions}; use bevy_mod_scripting_display::DisplayProxy; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::{WorldAccessGuard, WorldGuard}; use parking_lot::Mutex; /// Runs a callback on the script with the given ID if it exists @@ -199,7 +200,11 @@ impl RunScriptCallback

{ pub fn run(mut self, world: &mut World) -> Result { let script_contexts = world.get_resource_or_init::>().clone(); let script_callbacks = world.get_resource_or_init::>().clone(); - let guard = WorldGuard::new_exclusive(world); + let cache = WorldAccessGuard::setup_cache( + world, + CurrentScriptAttachment(Some(self.attachment.clone())), + ); + let guard = WorldGuard::new_exclusive(world, cache); let res = if let Some(context_override) = &self.context_override { self.run_with_context(guard.clone(), context_override.clone(), script_callbacks) diff --git a/crates/bevy_mod_scripting_core/src/context.rs b/crates/bevy_mod_scripting_core/src/context.rs index 18bc6ccee2..9b72800e2c 100644 --- a/crates/bevy_mod_scripting_core/src/context.rs +++ b/crates/bevy_mod_scripting_core/src/context.rs @@ -3,10 +3,9 @@ use std::any::Any; use bevy_ecs::world::WorldId; -use bevy_mod_scripting_bindings::{ - InteropError, ThreadScriptContext, ThreadWorldContainer, WorldGuard, -}; +use bevy_mod_scripting_bindings::{InteropError, WorldExtensions}; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::WorldGuard; use crate::IntoScriptPluginParams; @@ -67,11 +66,8 @@ impl ScriptingLoader

for P { world: WorldGuard, ) -> Result { WorldGuard::with_existing_static_guard(world.clone(), |world| { + world.set_current_attachment(attachment.clone()); let world_id = world.id(); - ThreadWorldContainer.set_context(ThreadScriptContext { - world, - attachment: attachment.clone(), - })?; Self::context_loader()(attachment, content, world_id) }) } @@ -83,11 +79,8 @@ impl ScriptingLoader

for P { world: WorldGuard, ) -> Result<(), InteropError> { WorldGuard::with_existing_static_guard(world, |world| { + world.set_current_attachment(attachment.clone()); let world_id = world.id(); - ThreadWorldContainer.set_context(ThreadScriptContext { - world, - attachment: attachment.clone(), - })?; Self::context_reloader()(attachment, content, previous_context, world_id) }) } diff --git a/crates/bevy_mod_scripting_core/src/error.rs b/crates/bevy_mod_scripting_core/src/error.rs index 391d81b87d..4800705593 100644 --- a/crates/bevy_mod_scripting_core/src/error.rs +++ b/crates/bevy_mod_scripting_core/src/error.rs @@ -13,6 +13,7 @@ use bevy_mod_scripting_bindings::InteropError; use ::bevy_reflect::Reflect; use bevy_mod_scripting_display::{DebugWithTypeInfo, DisplayWithTypeInfo, WithTypeInfo}; +use bevy_mod_scripting_world::WorldGuard; /// An error with an optional script Context #[derive(Debug, Clone, Reflect)] @@ -23,7 +24,7 @@ impl DisplayWithTypeInfo for ScriptError { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { self.0.display_with_type_info(f, type_info_provider) } @@ -58,7 +59,7 @@ impl DebugWithTypeInfo for Reason { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { match self { Reason::WithoutTypeInfo(err) => write!(f, "{err}"), @@ -77,7 +78,7 @@ impl DisplayWithTypeInfo for Reason { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { match self { Reason::WithoutTypeInfo(err) => write!(f, "{err}"), @@ -102,7 +103,7 @@ impl DebugWithTypeInfo for Context { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { self.display_with_type_info(f, type_info_provider) } @@ -118,7 +119,7 @@ impl DisplayWithTypeInfo for Context { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { match self { Context::String(cow) => f.write_str(cow), @@ -149,7 +150,7 @@ impl DisplayWithTypeInfo for ScriptErrorInner { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn bevy_mod_scripting_display::GetTypeInfo>, + type_info_provider: Option<&WorldGuard>, ) -> std::fmt::Result { f.write_str("Error ")?; if let Some(script) = &self.script { diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 9a5cc6574e..cd449ce522 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -7,8 +7,8 @@ use bevy_ecs::{ query::{Access, AccessConflicts}, storage::SparseSetIndex, }; -use bevy_mod_scripting_bindings::access_map::ReflectAccessId; +use bevy_mod_scripting_world::WorldAccessRange; use fixedbitset::FixedBitSet; // /// A wrapper around a world which pre-populates access, to safely co-exist with other system params, @@ -140,7 +140,7 @@ fn individual_conflicts(conflicts: AccessConflicts) -> FixedBitSet { } } -pub(crate) fn get_all_access_ids(access: &Access) -> Vec<(ReflectAccessId, bool)> { +pub(crate) fn get_all_access_ids(access: &Access) -> Vec<(WorldAccessRange, bool)> { let mut access_all_read = Access::default(); access_all_read.read_all(); @@ -157,16 +157,10 @@ pub(crate) fn get_all_access_ids(access: &Access) -> Vec<(ReflectAccessId, bool) let mut result = Vec::new(); for c in read.ones() { - result.push(( - ReflectAccessId::for_component_id(ComponentId::get_sparse_set_index(c)), - false, - )); + result.push((ComponentId::get_sparse_set_index(c).into(), false)); } for c in written.ones() { - result.push(( - ReflectAccessId::for_component_id(ComponentId::get_sparse_set_index(c)), - true, - )); + result.push((ComponentId::get_sparse_set_index(c).into(), true)); } result diff --git a/crates/bevy_mod_scripting_core/src/handler.rs b/crates/bevy_mod_scripting_core/src/handler.rs index 353a0c898c..0c708f328b 100644 --- a/crates/bevy_mod_scripting_core/src/handler.rs +++ b/crates/bevy_mod_scripting_core/src/handler.rs @@ -4,11 +4,11 @@ use bevy_ecs::{ world::WorldId, }; use bevy_mod_scripting_bindings::{ - InteropError, ScriptValue, ThreadScriptContext, ThreadWorldContainer, WorldAccessGuard, - WorldGuard, + CurrentScriptAttachment, InteropError, ScriptValue, WorldExtensions, }; use bevy_mod_scripting_display::{DisplayProxy, WithTypeInfo}; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::{WorldAccessGuard, WorldGuard}; use crate::{ IntoScriptPluginParams, @@ -64,11 +64,8 @@ impl ScriptingHandler

for P { world: WorldGuard, ) -> Result { WorldGuard::with_existing_static_guard(world.clone(), |world| { + world.set_current_attachment(attachment.clone()); let world_id = world.id(); - ThreadWorldContainer.set_context(ThreadScriptContext { - world, - attachment: attachment.clone(), - })?; let callbacks = script_callbacks.callbacks.read(); if let Some(callback) = callbacks .get(&(attachment.clone(), callback.to_string())) @@ -97,7 +94,8 @@ pub fn event_handler( let script_context = world.get_resource_or_init::>().clone(); let script_callbacks = world.get_resource_or_init::>().clone(); let event_cursor = state.get_mut(world); - let guard = WorldAccessGuard::new_exclusive(world); + let cache = WorldAccessGuard::setup_cache(world, CurrentScriptAttachment::default()); + let guard = WorldAccessGuard::new_exclusive(world, cache); event_handler_inner::

( L::into_callback_label(), event_cursor, @@ -253,7 +251,8 @@ pub fn script_error_logger( world: &mut World, mut errors_cursor: Local>, ) { - let guard = WorldGuard::new_exclusive(world); + let cache = WorldGuard::setup_cache(world, CurrentScriptAttachment::default()); + let guard = WorldGuard::new_exclusive(world, cache); let errors = guard.with_resource(|events: &Messages| { errors_cursor .read(events) diff --git a/crates/bevy_mod_scripting_core/src/lib.rs b/crates/bevy_mod_scripting_core/src/lib.rs index 3a9f8178b0..d9be349f2b 100644 --- a/crates/bevy_mod_scripting_core/src/lib.rs +++ b/crates/bevy_mod_scripting_core/src/lib.rs @@ -23,7 +23,7 @@ use bevy_mod_scripting_asset::{Language, LanguageExtensions, ScriptAsset, Script use bevy_mod_scripting_bindings::{ AppReflectAllocator, AppScheduleRegistry, AppScriptFunctionRegistry, DummyScriptFunctionRegistry, DynamicScriptComponentPlugin, MarkAsCore, ReflectReference, - ScriptTypeRegistration, ScriptValue, ThreadWorldContainer, garbage_collector, + ScriptTypeRegistration, ScriptValue, garbage_collector, }; use context::{Context, ContextInitializer, ContextPreHandlingInitializer}; use event::{ScriptCallbackEvent, ScriptCallbackResponseEvent}; @@ -359,9 +359,6 @@ impl Plugin for BMSScriptingInfrastructurePlugin { app.add_systems(PostUpdate, script_error_logger); } - let _ = bevy_mod_scripting_display::GLOBAL_TYPE_INFO_PROVIDER - .set(|| Some(&ThreadWorldContainer)); - DynamicScriptComponentPlugin.build(app); } diff --git a/crates/bevy_mod_scripting_core/src/pipeline/machines.rs b/crates/bevy_mod_scripting_core/src/pipeline/machines.rs index a49af80c11..3ad31604ec 100644 --- a/crates/bevy_mod_scripting_core/src/pipeline/machines.rs +++ b/crates/bevy_mod_scripting_core/src/pipeline/machines.rs @@ -7,8 +7,11 @@ use std::{ use bevy_ecs::event::Event; use bevy_log::trace; -use bevy_mod_scripting_bindings::{InteropError, ScriptValue}; +use bevy_mod_scripting_bindings::{ + CurrentScriptAttachment, InteropError, ScriptValue, WorldExtensions, +}; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::{WorldAccessGuard, WorldGuard}; use bevy_platform::collections::HashMap; use super::*; @@ -444,7 +447,9 @@ impl MachineState

for LoadingInitialized { world: &mut World, ) -> Box>, ScriptError>> + Send + Sync> { let attachment = &ctxt.attachment; - let guard = WorldGuard::new_exclusive(world); + let cache = + WorldAccessGuard::setup_cache(world, CurrentScriptAttachment(Some(attachment.clone()))); + let guard = WorldGuard::new_exclusive(world, cache); let ctxt = P::load(attachment, &self.content, guard.clone()); Box::new(ready(ctxt.map_err(ScriptError::from).map(|context| { Box::new(ContextAssigned::

{ @@ -467,7 +472,9 @@ impl MachineState

for ReloadingInitialized

{ world: &mut World, ) -> Box>, ScriptError>> + Send + Sync> { let attachment = &ctxt.attachment; - let guard = WorldGuard::new_exclusive(world); + let cache = + WorldAccessGuard::setup_cache(world, CurrentScriptAttachment(Some(attachment.clone()))); + let guard = WorldGuard::new_exclusive(world, cache); let mut previous_context_guard = self.existing_context.lock(); let ctxt = P::reload( attachment, diff --git a/crates/bevy_mod_scripting_core/src/pipeline/mod.rs b/crates/bevy_mod_scripting_core/src/pipeline/mod.rs index a554db1557..cea4cd38b9 100644 --- a/crates/bevy_mod_scripting_core/src/pipeline/mod.rs +++ b/crates/bevy_mod_scripting_core/src/pipeline/mod.rs @@ -14,7 +14,6 @@ use bevy_ecs::{ }; 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; diff --git a/crates/bevy_mod_scripting_core/src/script_system.rs b/crates/bevy_mod_scripting_core/src/script_system.rs index 994259ebad..07ff7b6a89 100644 --- a/crates/bevy_mod_scripting_core/src/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/script_system.rs @@ -26,11 +26,12 @@ use bevy_ecs::{ use bevy_log::{debug, error, warn_once}; use bevy_mod_scripting_bindings::{ AppReflectAllocator, AppScheduleRegistry, AppScriptComponentRegistry, - AppScriptFunctionRegistry, InteropError, IntoScript, ReflectAccessId, ReflectReference, - ScriptQueryBuilder, ScriptQueryResult, ScriptResourceRegistration, V, WorldAccessGuard, - WorldGuard, + AppScriptFunctionRegistry, CurrentScriptAttachment, InteropError, IntoScript, ReflectReference, + ScriptQueryBuilder, ScriptQueryResult, ScriptResourceRegistration, V, WorldExtensions, }; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::{WorldAccessGuard, WorldAccessRange, WorldGuard}; +use bevy_reflect::TypeRegistryArc; use bevy_system_reflection::{ReflectSchedule, ReflectSystem}; use bevy_utils::prelude::DebugName; use std::{ @@ -182,12 +183,12 @@ impl ScriptSystemBuilder { /// TODO: inline world guard into the system state, we should be able to re-use it struct ScriptSystemState { - type_registry: AppTypeRegistry, + type_registry: TypeRegistryArc, function_registry: AppScriptFunctionRegistry, schedule_registry: AppScheduleRegistry, component_registry: AppScriptComponentRegistry, allocator: AppReflectAllocator, - subset: HashSet, + subset: HashSet, callback_label: CallbackLabel, system_params: Vec, script_contexts: ScriptContexts

, @@ -286,21 +287,24 @@ impl System for DynamicScriptSystem

{ }; let mut payload = Vec::with_capacity(state.system_params.len()); - + let cache = WorldAccessGuard::setup_cache_raw( + CurrentScriptAttachment(Some(self.target_attachment.clone())), + state.allocator.clone(), + state.function_registry.clone(), + state.schedule_registry.clone(), + state.component_registry.clone(), + ); let guard = if self.exclusive { // safety: we are an exclusive system, therefore the cell allows us to do this let world = unsafe { world.world_mut() }; - WorldAccessGuard::new_exclusive(world) + WorldAccessGuard::new_exclusive(world, cache) } else { unsafe { WorldAccessGuard::new_non_exclusive( world, state.subset.clone(), state.type_registry.clone(), - state.allocator.clone(), - state.function_registry.clone(), - state.schedule_registry.clone(), - state.component_registry.clone(), + cache, ) } }; @@ -409,7 +413,7 @@ impl System for DynamicScriptSystem

{ access.add_resource_write(component_id); component_access_set.add(access); - let raid = ReflectAccessId::for_component_id(component_id); + let raid: WorldAccessRange = component_id.into(); #[allow( clippy::panic, reason = "WIP, to be dealt with in validate params better, but panic will still remain" @@ -453,7 +457,7 @@ impl System for DynamicScriptSystem

{ } self.state = Some(ScriptSystemState { - type_registry: world.get_resource_or_init::().clone(), + type_registry: world.get_resource_or_init::().clone().0, function_registry: world .get_resource_or_init::() .clone(), @@ -543,7 +547,7 @@ impl ManageScriptSystems for WorldGuard<'_> { label: &ReflectSchedule, f: F, ) -> Result { - self.with_global_access(|world| { + self.with_world_mut(|world| { let mut schedules = world.get_resource_mut::().ok_or_else(|| { InteropError::unsupported_operation( None, @@ -676,10 +680,11 @@ mod test { ScriptAttachment::StaticScript(Handle::default()), ); builder.before_system(test_system); - + let world_mut = app.world_mut(); + let cache = WorldAccessGuard::setup_cache(world_mut, CurrentScriptAttachment::default()); let _ = builder .build::( - WorldAccessGuard::new_exclusive(app.world_mut()), + WorldAccessGuard::new_exclusive(world_mut, cache), &ReflectSchedule::from_label(TestSchedule), ) .unwrap(); diff --git a/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs b/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs index 153d86153b..fffe5c1db2 100644 --- a/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs +++ b/crates/bevy_mod_scripting_derive/src/derive/debug_with_type_info.rs @@ -138,7 +138,7 @@ pub fn debug_with_type_info(input: proc_macro::TokenStream) -> proc_macro::Token let (impl_generics, ty_generics, where_clause) = derive_input.generics.split_for_impl(); quote::quote! { impl #impl_generics #bms_display_path::DebugWithTypeInfo for #name #ty_generics #where_clause { - fn to_string_with_type_info(&self, f: &mut std::fmt::Formatter<'_>, type_info_provider: Option<&dyn #bms_display_path::GetTypeInfo>) -> std::fmt::Result { + fn to_string_with_type_info(&self, f: &mut std::fmt::Formatter<'_>, type_info_provider: Option<&#bms_display_path::WorldAccessGuard>) -> std::fmt::Result { #builder } } diff --git a/crates/bevy_mod_scripting_display/Cargo.toml b/crates/bevy_mod_scripting_display/Cargo.toml index 5971727727..54ba6b54ba 100644 --- a/crates/bevy_mod_scripting_display/Cargo.toml +++ b/crates/bevy_mod_scripting_display/Cargo.toml @@ -14,9 +14,11 @@ readme.workspace = true [dependencies] bevy_reflect = { workspace = true } bevy_asset = { workspace = true } +bevy_utils = { workspace = true } bevy_ecs = { workspace = true, features = ["bevy_reflect"] } bevy_platform = { workspace = true } parking_lot = { workspace = true } +bevy_mod_scripting_world = { workspace = true } [lints] workspace = true diff --git a/crates/bevy_mod_scripting_display/src/impls/bevy_asset.rs b/crates/bevy_mod_scripting_display/src/impls/bevy_asset.rs index 8f5c2bfe2c..9448c0dafa 100644 --- a/crates/bevy_mod_scripting_display/src/impls/bevy_asset.rs +++ b/crates/bevy_mod_scripting_display/src/impls/bevy_asset.rs @@ -1,8 +1,10 @@ +use bevy_mod_scripting_world::WorldAccessGuard; + impl crate::DebugWithTypeInfo for bevy_asset::UntypedHandle { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn crate::GetTypeInfo>, + _type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { write!(f, "{self:?}") } diff --git a/crates/bevy_mod_scripting_display/src/impls/bevy_ecs.rs b/crates/bevy_mod_scripting_display/src/impls/bevy_ecs.rs index 511d52641f..10948360f5 100644 --- a/crates/bevy_mod_scripting_display/src/impls/bevy_ecs.rs +++ b/crates/bevy_mod_scripting_display/src/impls/bevy_ecs.rs @@ -1,4 +1,5 @@ -use bevy_ecs::entity::Entity; +use bevy_ecs::{component::ComponentId, entity::Entity}; +use bevy_utils::prelude::DebugName; use crate::*; @@ -9,13 +10,14 @@ impl DebugWithTypeInfo for ComponentId { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { let mut builder = f.debug_tuple_with_type_info("ComponentId", type_info_provider); match type_info_provider { Some(type_info_provider) => match type_info_provider - .get_component_info(*self) - .map(|info| info.name().to_string()) + .as_unsafe_world_cell() + .ok() + .and_then(|i| i.components().get_name(*self)) { Some(type_info) => builder.field(&type_info), None => builder.field(&format!("Unregistered ComponentId - {self:?}")), @@ -30,12 +32,13 @@ impl DisplayWithTypeInfo for ComponentId { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { match type_info_provider { Some(type_info_provider) => match type_info_provider - .get_component_info(*self) - .map(|info| info.name().to_string()) + .as_unsafe_world_cell() + .ok() + .and_then(|i| i.components().get_name(*self)) { Some(type_info) => f.write_str(&type_info), None => { @@ -50,3 +53,13 @@ impl DisplayWithTypeInfo for ComponentId { } } } + +impl DebugWithTypeInfo for DebugName { + fn to_string_with_type_info( + &self, + f: &mut std::fmt::Formatter<'_>, + _type_info_provider: Option<&WorldAccessGuard>, + ) -> std::fmt::Result { + f.write_str(&self.to_string()) + } +} diff --git a/crates/bevy_mod_scripting_display/src/impls/bevy_platform.rs b/crates/bevy_mod_scripting_display/src/impls/bevy_platform.rs index 39c555384f..278ed142c6 100644 --- a/crates/bevy_mod_scripting_display/src/impls/bevy_platform.rs +++ b/crates/bevy_mod_scripting_display/src/impls/bevy_platform.rs @@ -6,7 +6,7 @@ impl DebugWithTypeInfo fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn crate::GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_map_with_type_info(type_info_provider) .entries( @@ -21,7 +21,7 @@ impl DebugWithTypeInfo for bevy_platform::collections:: fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn crate::GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_set_with_type_info(type_info_provider) .entries(self.iter().map(|v| v as &dyn DebugWithTypeInfo)) @@ -33,7 +33,7 @@ impl DebugWithTypeInfo for bevy_platform::collections::Has fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn crate::GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_set_with_type_info(type_info_provider) .entries(self.iter().map(|v| v as &dyn DebugWithTypeInfo)) diff --git a/crates/bevy_mod_scripting_display/src/impls/bevy_reflect.rs b/crates/bevy_mod_scripting_display/src/impls/bevy_reflect.rs index f9653e90fa..62841aec73 100644 --- a/crates/bevy_mod_scripting_display/src/impls/bevy_reflect.rs +++ b/crates/bevy_mod_scripting_display/src/impls/bevy_reflect.rs @@ -1,4 +1,4 @@ -use bevy_reflect::{ParsedPath, PartialReflect}; +use bevy_reflect::{ParsedPath, PartialReflect, TypeInfo}; use crate::*; @@ -12,7 +12,7 @@ impl DebugWithTypeInfo for Box { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { ReflectPrinter::new(f, type_info_provider).debug(self.as_ref()) } diff --git a/crates/bevy_mod_scripting_display/src/impls/parking_lock.rs b/crates/bevy_mod_scripting_display/src/impls/parking_lock.rs index 1189ad31ad..6223d38846 100644 --- a/crates/bevy_mod_scripting_display/src/impls/parking_lock.rs +++ b/crates/bevy_mod_scripting_display/src/impls/parking_lock.rs @@ -6,7 +6,7 @@ impl DebugWithTypeInfo for RwLock { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn crate::GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { if let Some(read) = self.try_read() { f.debug_tuple_with_type_info("RwLock", type_info_provider) @@ -24,7 +24,7 @@ impl DebugWithTypeInfo for parking_lot::Mutex { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn crate::GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { if let Some(guard) = self.try_lock() { f.debug_tuple_with_type_info("Mutex", type_info_provider) diff --git a/crates/bevy_mod_scripting_display/src/impls/std.rs b/crates/bevy_mod_scripting_display/src/impls/std.rs index 3b99f73f9a..d53cb8f678 100644 --- a/crates/bevy_mod_scripting_display/src/impls/std.rs +++ b/crates/bevy_mod_scripting_display/src/impls/std.rs @@ -12,7 +12,7 @@ impl DebugWithTypeInfo for TypeId { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { if *self == TypeId::of::() { return f.write_str("Unknown Type"); @@ -22,7 +22,11 @@ impl DebugWithTypeInfo for TypeId { } let name = if let Some(type_info_provider) = type_info_provider { - if let Some(type_info) = type_info_provider.get_type_info(*self) { + if let Some(type_info) = type_info_provider + .type_registry() + .read() + .get_type_info(*self) + { type_info.type_path_table().path().to_string() } else { format!("Unregistered Type - {self:?}") @@ -39,7 +43,7 @@ impl DebugWithTypeInfo for Arc { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { (**self).to_string_with_type_info(f, type_info_provider) } @@ -49,7 +53,7 @@ impl DebugWithTypeInfo for Box { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { (**self).to_string_with_type_info(f, type_info_provider) } @@ -59,7 +63,7 @@ impl DebugWithTypeInfo for Option { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { match self { Some(value) => f @@ -77,7 +81,7 @@ impl DebugWithTypeInfo for Result, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { match self { Ok(v) => f @@ -96,7 +100,7 @@ impl DebugWithTypeInfo for Vec { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_list_with_type_info(type_info_provider) .entries(self.iter().map(|v| v as &dyn DebugWithTypeInfo)) @@ -108,7 +112,7 @@ impl DebugWithTypeInfo for VecDeque { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_list_with_type_info(type_info_provider) .entries(self.iter().map(|v| v as &dyn DebugWithTypeInfo)) @@ -122,7 +126,7 @@ impl DebugWithTypeInfo fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_map_with_type_info(type_info_provider) .entries( @@ -137,7 +141,7 @@ impl DebugWithTypeInfo for std::collections::HashSet, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_set_with_type_info(type_info_provider) .entries(self.iter().map(|v| v as &dyn DebugWithTypeInfo)) @@ -149,7 +153,7 @@ impl DebugWithTypeInfo for std::collections::BTreeSet { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_set_with_type_info(type_info_provider) .entries(self.iter().map(|v| v as &dyn DebugWithTypeInfo)) @@ -163,7 +167,7 @@ impl DebugWithTypeInfo fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_map_with_type_info(type_info_provider) .entries( @@ -178,7 +182,7 @@ impl DebugWithTypeInfo for Location<'_> { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.debug_struct("Location") .field("file", &self.file()) @@ -197,7 +201,7 @@ macro_rules! impl_display_with_type_info_via_display { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { ::fmt(self, f) } @@ -216,7 +220,7 @@ impl DisplayWithTypeInfo for TypeId { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { if *self == TypeId::of::() { return f.write_str("Unknown Type"); @@ -225,7 +229,11 @@ impl DisplayWithTypeInfo for TypeId { } let name = if let Some(type_info_provider) = type_info_provider { - if let Some(type_info) = type_info_provider.get_type_info(*self) { + if let Some(type_info) = type_info_provider + .type_registry() + .read() + .get_type_info(*self) + { type_info.type_path_table().path().to_string() } else { format!("Unregistered Type - {self:?}") @@ -243,7 +251,7 @@ impl DisplayWithTypeInfo for Arc { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { (**self).display_with_type_info(f, type_info_provider) } @@ -253,7 +261,7 @@ impl DisplayWithTypeInfo for Arc { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { (**self).display_with_type_info(f, type_info_provider) } @@ -263,7 +271,7 @@ impl DisplayWithTypeInfo for Box { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { (**self).display_with_type_info(f, type_info_provider) } @@ -273,7 +281,7 @@ impl DisplayWithTypeInfo for Box { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { (**self).display_with_type_info(f, type_info_provider) } @@ -283,7 +291,7 @@ impl DisplayWithTypeInfo for Location<'_> { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { // prettier display: file:line:column write!(f, "{}:{}:{}", self.file(), self.line(), self.column()) @@ -294,7 +302,7 @@ impl DisplayWithTypeInfo for Vec { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.write_str("[")?; let mut first = true; @@ -313,7 +321,7 @@ impl DisplayWithTypeInfo for VecDeque { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { f.write_str("[")?; let mut first = true; diff --git a/crates/bevy_mod_scripting_display/src/lib.rs b/crates/bevy_mod_scripting_display/src/lib.rs index 7a873dfa59..4f6b17b7ca 100644 --- a/crates/bevy_mod_scripting_display/src/lib.rs +++ b/crates/bevy_mod_scripting_display/src/lib.rs @@ -1,108 +1,103 @@ //! Abstractions for displaying reflect values, potentially with access to the type registry -use std::{ - any::{Any, TypeId}, - ops::Deref, -}; +use std::{any::TypeId, ops::Deref}; mod handle; mod impls; mod printer; pub use {handle::*, printer::*}; -use bevy_ecs::{ - component::{ComponentId, ComponentInfo}, - reflect::AppTypeRegistry, - world::World, -}; -use bevy_reflect::{TypeData, TypeInfo, TypeRegistry, reflect_trait}; - -/// An abstraction for getting type information, potentially using the type registry. -pub trait GetTypeInfo { - /// Get a string representation of the type, potentially using the type registry. - fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo>; - - /// Queries against arbitrary type data - fn query_type_registration( - &self, - type_id: TypeId, - type_data_id: TypeId, - ) -> Option>; - - /// Get component info for a given component id, if available - fn get_component_info(&self, component_id: ComponentId) -> Option<&ComponentInfo>; - - /// A potentially unsafe function depending on the implementation which allows you to downcast to a concrete type without - /// requiring 'static on the type. - /// - /// # Safety - /// - Ensure the safety invariants for the concrete type you are expecting are respected - unsafe fn as_any_static(&self) -> &dyn Any; -} - -/// Extension trait for GetTypeInfo which provides non-type safe extensions -pub trait GetTypeInfoExtensions<'s> { - /// Typed equivalent to [`GetTypeInfo::query_type_registration`] - fn get_type_data(&'s self, type_id: TypeId) -> Option; -} - -impl<'s> GetTypeInfoExtensions<'s> for &'s dyn GetTypeInfo { - fn get_type_data(&'s self, type_id: TypeId) -> Option { - self.query_type_registration(type_id, std::any::TypeId::of::()) - .and_then(|t| t.downcast().ok()) - .map(|b| *b) - } -} - -impl GetTypeInfo for TypeRegistry { - fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo> { - self.get(type_id) - .map(|registration| registration.type_info()) - } - - fn query_type_registration( - &self, - type_id: TypeId, - type_data_id: TypeId, - ) -> Option> { - self.get(type_id) - .and_then(|r| r.data_by_id(type_data_id).map(|t| t.clone_type_data())) - } - - fn get_component_info(&self, _component_id: ComponentId) -> Option<&ComponentInfo> { - None - } - - unsafe fn as_any_static(&self) -> &dyn Any { - self - } -} - -impl GetTypeInfo for World { - fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo> { - self.get_resource::() - .and_then(|r| r.read().get_type_info(type_id)) - } - - fn query_type_registration( - &self, - type_id: TypeId, - type_data_id: TypeId, - ) -> Option> { - self.get_resource::().and_then(|r| { - r.read() - .get(type_id) - .and_then(|r| r.data_by_id(type_data_id).map(|t| t.clone_type_data())) - }) - } - - fn get_component_info(&self, component_id: ComponentId) -> Option<&ComponentInfo> { - self.components().get_info(component_id) - } - - unsafe fn as_any_static(&self) -> &dyn Any { - self - } -} +use bevy_mod_scripting_world::ThreadWorldContainer; +pub use bevy_mod_scripting_world::WorldAccessGuard; + +use bevy_reflect::reflect_trait; + +// /// An abstraction for getting type information, potentially using the type registry. +// pub trait GetTypeInfo { +// /// Get a string representation of the type, potentially using the type registry. +// fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo>; + +// /// Queries against arbitrary type data +// fn query_type_registration( +// &self, +// type_id: TypeId, +// type_data_id: TypeId, +// ) -> Option>; + +// /// Get component info for a given component id, if available +// fn get_component_info(&self, component_id: ComponentId) -> Option<&ComponentInfo>; + +// /// A potentially unsafe function depending on the implementation which allows you to downcast to a concrete type without +// /// requiring 'static on the type. +// /// +// /// # Safety +// /// - Ensure the safety invariants for the concrete type you are expecting are respected +// unsafe fn as_any_static(&self) -> &dyn Any; +// } + +// /// Extension trait for GetTypeInfo which provides non-type safe extensions +// pub trait GetTypeInfoExtensions<'s> { +// /// Typed equivalent to [`GetTypeInfo::query_type_registration`] +// fn get_type_data(&'s self, type_id: TypeId) -> Option; +// } + +// impl<'s> GetTypeInfoExtensions<'s> for &'s WorldAccessGuard<'t> { +// fn get_type_data(&'s self, type_id: TypeId) -> Option { +// self.query_type_registration(type_id, std::any::TypeId::of::()) +// .and_then(|t| t.downcast().ok()) +// .map(|b| *b) +// } +// } + +// impl GetTypeInfo for TypeRegistry { +// fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo> { +// self.get(type_id) +// .map(|registration| registration.type_info()) +// } + +// fn query_type_registration( +// &self, +// type_id: TypeId, +// type_data_id: TypeId, +// ) -> Option> { +// self.get(type_id) +// .and_then(|r| r.data_by_id(type_data_id).map(|t| t.clone_type_data())) +// } + +// fn get_component_info(&self, _component_id: ComponentId) -> Option<&ComponentInfo> { +// None +// } + +// unsafe fn as_any_static(&self) -> &dyn Any { +// self +// } +// } + +// impl GetTypeInfo for World { +// fn get_type_info(&self, type_id: TypeId) -> Option<&TypeInfo> { +// self.get_resource::() +// .and_then(|r| r.read().get_type_info(type_id)) +// } + +// fn query_type_registration( +// &self, +// type_id: TypeId, +// type_data_id: TypeId, +// ) -> Option> { +// self.get_resource::().and_then(|r| { +// r.read() +// .get(type_id) +// .and_then(|r| r.data_by_id(type_data_id).map(|t| t.clone_type_data())) +// }) +// } + +// fn get_component_info(&self, component_id: ComponentId) -> Option<&ComponentInfo> { +// self.components().get_info(component_id) +// } + +// unsafe fn as_any_static(&self) -> &dyn Any { +// self +// } +// } /// An trait for displaying values with access to type information #[reflect_trait] @@ -111,7 +106,7 @@ pub trait DisplayWithTypeInfo { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result; } @@ -119,7 +114,7 @@ impl DisplayWithTypeInfo for WithTypeInfo<'_, T> { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { self.0.display_with_type_info(f, type_info_provider) } @@ -127,12 +122,11 @@ impl DisplayWithTypeInfo for WithTypeInfo<'_, T> { impl std::fmt::Display for WithTypeInfo<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let provider = self.1.or_else(|| { - GLOBAL_TYPE_INFO_PROVIDER - .get() - .and_then(|get_provider| get_provider()) - }); - self.0.display_with_type_info(f, provider) + if let Some(provider) = self.1 { + return self.0.display_with_type_info(f, Some(provider)); + } + let provider = ThreadWorldContainer.try_get_context().ok().map(|c| c.world); + self.0.display_with_type_info(f, provider.as_ref()) } } @@ -144,16 +138,10 @@ pub trait DebugWithTypeInfo { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result; } -/// A global type info provider that can be set once and used throughout the application -/// It does not do the retrieval itself, but provides a function that points to the retrieval mechanism. -pub static GLOBAL_TYPE_INFO_PROVIDER: std::sync::OnceLock< - fn() -> Option<&'static dyn GetTypeInfo>, -> = std::sync::OnceLock::new(); - /// newtype adapter for opting into [`DisplayWithTypeInfo`] for any T: [`DisplayWithTypeInfo`] /// Use as follows /// ```rust,no_run @@ -163,7 +151,7 @@ pub static GLOBAL_TYPE_INFO_PROVIDER: std::sync::OnceLock< /// format!("{:?}", WithTypeInfo::new(&my_value)); // non-pretty print /// format!("{:#?}", WithTypeInfo::new(&my_value)); // pretty print /// ``` -pub struct WithTypeInfo<'a, T: ?Sized>(&'a T, Option<&'a dyn GetTypeInfo>); +pub struct WithTypeInfo<'a, T: ?Sized>(&'a T, Option<&'a WorldAccessGuard<'a>>); impl<'a, T: ?Sized> WithTypeInfo<'a, T> { /// Create a new WithTypeInfo wrapper. @@ -176,13 +164,13 @@ impl<'a, T: ?Sized> WithTypeInfo<'a, T> { } /// Create a new WithTypeInfo wrapper with a specific type info provider - pub fn new_with_info(value: &'a T, provider: &'a dyn GetTypeInfo) -> Self { + pub fn new_with_info(value: &'a T, provider: &'a WorldAccessGuard<'a>) -> Self { Self(value, Some(provider)) } /// Create a new WithTypeInfo wrapper passing down an optional type info provider. /// Useful for nested implementations which want to avoid multiple retrievals - pub fn new_with_opt_info(value: &'a T, provider: Option<&'a dyn GetTypeInfo>) -> Self { + pub fn new_with_opt_info(value: &'a T, provider: Option<&'a WorldAccessGuard<'a>>) -> Self { Self(value, provider) } } @@ -197,12 +185,11 @@ impl Deref for WithTypeInfo<'_, T> { impl<'a, T: DebugWithTypeInfo + ?Sized> std::fmt::Debug for WithTypeInfo<'a, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let provider = self.1.or_else(|| { - GLOBAL_TYPE_INFO_PROVIDER - .get() - .and_then(|get_provider| get_provider()) - }); - self.0.to_string_with_type_info(f, provider) + if let Some(provider) = self.1 { + return self.0.to_string_with_type_info(f, Some(provider)); + } + let provider = ThreadWorldContainer.try_get_context().ok().map(|c| c.world); + self.0.to_string_with_type_info(f, provider.as_ref()) } } @@ -210,7 +197,7 @@ impl DebugWithTypeInfo for WithTypeInfo<'_, T> { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { self.0.to_string_with_type_info(f, type_info_provider) } @@ -251,7 +238,7 @@ impl SelfBuilder for T { /// each field. pub struct DebugStruct<'a, 'b: 'a, 't> { builder: std::fmt::DebugStruct<'a, 'b>, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, } impl<'a, 'b: 'a, 't> DebugStruct<'a, 'b, 't> { @@ -263,7 +250,7 @@ impl<'a, 'b: 'a, 't> DebugStruct<'a, 'b, 't> { pub fn new( f: &'a mut std::fmt::Formatter<'b>, name: &str, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> Self { Self { builder: f.debug_struct(name), @@ -295,7 +282,7 @@ impl<'a, 'b: 'a, 't> DebugStruct<'a, 'b, 't> { /// information. pub struct DebugTuple<'a, 'b: 'a, 't> { builder: std::fmt::DebugTuple<'a, 'b>, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, } impl<'a, 'b: 'a, 't> DebugTuple<'a, 'b, 't> { @@ -304,7 +291,7 @@ impl<'a, 'b: 'a, 't> DebugTuple<'a, 'b, 't> { pub fn new( f: &'a mut std::fmt::Formatter<'b>, name: &str, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> Self { Self { builder: f.debug_tuple(name), @@ -332,12 +319,15 @@ impl<'a, 'b: 'a, 't> DebugTuple<'a, 'b, 't> { /// information during formatting. pub struct DebugList<'a, 'b: 'a, 't> { builder: std::fmt::DebugList<'a, 'b>, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, } impl<'a, 'b: 'a, 't> DebugList<'a, 'b, 't> { /// Create a new `DebugList` builder. - pub fn new(f: &'a mut std::fmt::Formatter<'b>, type_info: Option<&'t dyn GetTypeInfo>) -> Self { + pub fn new( + f: &'a mut std::fmt::Formatter<'b>, + type_info: Option<&'t WorldAccessGuard<'t>>, + ) -> Self { Self { builder: f.debug_list(), type_info, @@ -376,12 +366,15 @@ impl<'a, 'b: 'a, 't> DebugList<'a, 'b, 't> { /// information. pub struct DebugSet<'a, 'b: 'a, 't> { builder: std::fmt::DebugSet<'a, 'b>, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, } impl<'a, 'b: 'a, 't> DebugSet<'a, 'b, 't> { /// Create a new `DebugSet` builder. - pub fn new(f: &'a mut std::fmt::Formatter<'b>, type_info: Option<&'t dyn GetTypeInfo>) -> Self { + pub fn new( + f: &'a mut std::fmt::Formatter<'b>, + type_info: Option<&'t WorldAccessGuard<'t>>, + ) -> Self { Self { builder: f.debug_set(), type_info, @@ -420,12 +413,15 @@ impl<'a, 'b: 'a, 't> DebugSet<'a, 'b, 't> { /// with optional type information. pub struct DebugMap<'a, 'b: 'a, 't> { builder: std::fmt::DebugMap<'a, 'b>, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, } impl<'a, 'b: 'a, 't> DebugMap<'a, 'b, 't> { /// Create a new `DebugMap` builder. - pub fn new(f: &'a mut std::fmt::Formatter<'b>, type_info: Option<&'t dyn GetTypeInfo>) -> Self { + pub fn new( + f: &'a mut std::fmt::Formatter<'b>, + type_info: Option<&'t WorldAccessGuard<'t>>, + ) -> Self { Self { builder: f.debug_map(), type_info, @@ -496,7 +492,7 @@ pub trait DebugWithTypeInfoBuilder<'a, 'b: 'a, 't> { fn debug_struct_with_type_info( &'a mut self, name: &str, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugStruct<'a, 'b, 't>; /// Start formatting a tuple-like value with the given name using @@ -504,25 +500,25 @@ pub trait DebugWithTypeInfoBuilder<'a, 'b: 'a, 't> { fn debug_tuple_with_type_info( &'a mut self, name: &str, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugTuple<'a, 'b, 't>; /// Start formatting a list using type-aware entry formatting. fn debug_list_with_type_info( &'a mut self, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugList<'a, 'b, 't>; /// Start formatting a set using type-aware entry formatting. fn debug_set_with_type_info( &'a mut self, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugSet<'a, 'b, 't>; /// Start formatting a map using type-aware key/value formatting. fn debug_map_with_type_info( &'a mut self, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugMap<'a, 'b, 't>; } @@ -530,32 +526,32 @@ impl<'a, 'b: 'a, 't> DebugWithTypeInfoBuilder<'a, 'b, 't> for std::fmt::Formatte fn debug_struct_with_type_info( &'a mut self, name: &str, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugStruct<'a, 'b, 't> { DebugStruct::new(self, name, type_info) } fn debug_tuple_with_type_info( &'a mut self, name: &str, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugTuple<'a, 'b, 't> { DebugTuple::new(self, name, type_info) } fn debug_list_with_type_info( &'a mut self, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugList<'a, 'b, 't> { DebugList::new(self, type_info) } fn debug_set_with_type_info( &'a mut self, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugSet<'a, 'b, 't> { DebugSet::new(self, type_info) } fn debug_map_with_type_info( &'a mut self, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> DebugMap<'a, 'b, 't> { DebugMap::new(self, type_info) } @@ -567,7 +563,7 @@ macro_rules! impl_debug_with_type_info_via_debug { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn $crate::GetTypeInfo>, + _type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } @@ -582,7 +578,7 @@ macro_rules! impl_debug_with_type_info_via_display { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { ::fmt(self, f) } @@ -601,7 +597,7 @@ macro_rules! impl_display_with_type_info_via_display { fn display_with_type_info( &self, f: &mut std::fmt::Formatter<'_>, - _type_info_provider: Option<&dyn GetTypeInfo>, + _type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { ::fmt(self, f) } diff --git a/crates/bevy_mod_scripting_display/src/printer/mod.rs b/crates/bevy_mod_scripting_display/src/printer/mod.rs index 0252043e9d..1413ee5d7a 100644 --- a/crates/bevy_mod_scripting_display/src/printer/mod.rs +++ b/crates/bevy_mod_scripting_display/src/printer/mod.rs @@ -6,14 +6,14 @@ use crate::*; pub struct ReflectPrinter<'f, 'b: 'f, 't> { pub(crate) formatter: &'f mut std::fmt::Formatter<'b>, pub(crate) result: std::fmt::Result, - pub(crate) type_info: Option<&'t dyn GetTypeInfo>, + pub(crate) type_info: Option<&'t WorldAccessGuard<'t>>, } impl<'f, 'b: 'f, 't> ReflectPrinter<'f, 'b, 't> { /// Creates a new `ReflectPrinter` with the given formatter. pub fn new( formatter: &'f mut std::fmt::Formatter<'b>, - type_info: Option<&'t dyn GetTypeInfo>, + type_info: Option<&'t WorldAccessGuard<'t>>, ) -> ReflectPrinter<'f, 'b, 't> { ReflectPrinter { formatter, @@ -26,8 +26,11 @@ impl<'f, 'b: 'f, 't> ReflectPrinter<'f, 'b, 't> { pub fn debug(&mut self, value: &dyn PartialReflect) -> std::fmt::Result { if let Some(type_info_provider) = &self.type_info && let Some(reflect_type) = value.try_as_reflect() - && let Some(display_type_data) = type_info_provider - .get_type_data::(reflect_type.type_id()) + && let Some(display_type_data) = + type_info_provider + .type_registry() + .read() + .get_type_data::(reflect_type.type_id()) && let Some(as_dyn_trait) = display_type_data.get(reflect_type) { return as_dyn_trait.display_with_type_info(self.formatter, self.type_info); @@ -188,7 +191,7 @@ impl GetIdentOrPath for T { /// A wrapper type that implements `Debug` for any `PartialReflect` by using `ReflectPrinter`. /// /// For opaque types will optionally seek [`ReflectDisplayWithTypeInfo`] type data in the registry -pub struct PrintReflectAsDebug<'a, 'g>(&'a dyn PartialReflect, Option<&'g dyn GetTypeInfo>); +pub struct PrintReflectAsDebug<'a, 'g>(&'a dyn PartialReflect, Option<&'g WorldAccessGuard<'g>>); impl<'a, 'g> PrintReflectAsDebug<'a, 'g> { /// Constructs a new [`PrintReflectAsDebug`] which will use the global type info provider @@ -199,7 +202,7 @@ impl<'a, 'g> PrintReflectAsDebug<'a, 'g> { /// Constructs a new [`PrintReflectAsDebug`] which will use the provided type info provider and fallback to the global pub fn new_with_opt_info( val: &'a dyn PartialReflect, - info: Option<&'g dyn GetTypeInfo>, + info: Option<&'g WorldAccessGuard<'g>>, ) -> Self { Self(val, info) } @@ -209,7 +212,7 @@ impl DebugWithTypeInfo for PrintReflectAsDebug<'_, '_> { fn to_string_with_type_info( &self, f: &mut std::fmt::Formatter, - type_info_provider: Option<&dyn GetTypeInfo>, + type_info_provider: Option<&WorldAccessGuard>, ) -> std::fmt::Result { ReflectPrinter::new(f, self.1.or(type_info_provider)).debug(self.0) } diff --git a/crates/bevy_mod_scripting_functions/Cargo.toml b/crates/bevy_mod_scripting_functions/Cargo.toml index bcc8a787c6..4285229689 100644 --- a/crates/bevy_mod_scripting_functions/Cargo.toml +++ b/crates/bevy_mod_scripting_functions/Cargo.toml @@ -56,6 +56,7 @@ bevy_mod_scripting_script = { workspace = true } bevy_mod_scripting_derive = { workspace = true } bevy_mod_scripting_lua = { path = "../languages/bevy_mod_scripting_lua", optional = true, version = "0.19.0" } bevy_mod_scripting_rhai = { path = "../languages/bevy_mod_scripting_rhai", optional = true, version = "0.19.0" } +bevy_mod_scripting_world = { workspace = true } bevy_system_reflection = { path = "../bevy_system_reflection", version = "0.19.0" } bevy_ecs = { workspace = true, features = ["std", "bevy_reflect"] } diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 349152b227..f81973adfb 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -2,6 +2,7 @@ use bevy_mod_scripting_asset::ScriptAsset; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::ThreadWorldContainer; use bevy_platform::collections::HashMap; use std::{collections::VecDeque, ops::Deref}; @@ -11,8 +12,8 @@ use bevy_ecs::{entity::Entity, prelude::AppTypeRegistry, schedule::Schedules, wo use bevy_mod_scripting_bindings::{ DynamicScriptFunction, DynamicScriptFunctionMut, FunctionInfo, GlobalNamespace, InteropError, PartialReflectExt, ReflectReference, ScriptComponentRegistration, ScriptQueryBuilder, - ScriptQueryResult, ScriptResourceRegistration, ScriptTypeRegistration, ThreadWorldContainer, - Union, VariadicTuple, + ScriptQueryResult, ScriptResourceRegistration, ScriptTypeRegistration, Union, VariadicTuple, + WorldExtensions, function::{ from::{R, V}, from_ref::FromScriptRef, diff --git a/crates/bevy_mod_scripting_world/Cargo.toml b/crates/bevy_mod_scripting_world/Cargo.toml new file mode 100644 index 0000000000..08ed4b849d --- /dev/null +++ b/crates/bevy_mod_scripting_world/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "bevy_mod_scripting_world" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +readme.workspace = true + +[dependencies] +bevy_ecs = { workspace = true } +bevy_log = { workspace = true } +bevy_platform = { workspace = true } +bevy_reflect = { workspace = true } +# bevy_mod_scripting_script = { workspace = true } +# bevy_mod_scripting_display = { workspace = true } +profiling = { workspace = true, features = [ + "procmacros", +] } +parking_lot = { workspace = true } +smallvec = { workspace = true } +fixedbitset = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } +[lints] +workspace = true diff --git a/crates/bevy_mod_scripting_world/src/access_map.rs b/crates/bevy_mod_scripting_world/src/access_map.rs new file mode 100644 index 0000000000..7ea31d097c --- /dev/null +++ b/crates/bevy_mod_scripting_world/src/access_map.rs @@ -0,0 +1,805 @@ +//! A map of access claims used to safely and dynamically access the world. + +use bevy_ecs::component::ComponentId; + +use fixedbitset::FixedBitSet; +use parking_lot::Mutex; +use smallvec::SmallVec; + +use std::num::NonZero; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// An owner of an access claim and the code location of the claim. +pub struct ClaimOwner { + /// The code location of the claim + pub location: std::panic::Location<'static>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// A count of the number of readers and writers of an access claim. +pub struct AccessInstance { + /// The number of readers including thread information + pub owner: ClaimOwner, + /// If the current read is a write access, this will be set + written: bool, +} + +#[profiling::all_functions] +impl AccessInstance { + fn new(owner: ClaimOwner, write: bool) -> Self { + Self { + owner, + written: write, + } + } +} + +/// A wrapper for conversion between ComponentId's and nonzero indices +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ComponentRange(NonZero); + +impl From for ComponentRange { + fn from(value: ComponentId) -> Self { + // Safety: trivially holds that n + 1 cannot be zero + Self(unsafe { + NonZero::new_unchecked( + (value.index() as u16) + .checked_add(1) + .unwrap_or_else(|| unreachable!("Too many components being used")), + ) + }) + } +} + +impl From for ComponentId { + fn from(val: ComponentRange) -> Self { + ComponentId::new((val.0.get() - 1) as usize) + } +} +/// Describes access ranges in and outside a bevy world +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum WorldAccessRange { + /// An access into a resource or component in the world + ComponentOrResource(ComponentRange), + /// An access outside the world, for example into an external allocator + External(NonZero), + /// A whole world read or write + Global, +} + +impl WorldAccessRange { + /// Returns true if this access conflicts with another access in 'space', two conflicting accesses may still be live, if they are read only for example + pub fn overlaps_with_access_to(&self, other: WorldAccessRange) -> bool { + match (self, other) { + ( + WorldAccessRange::ComponentOrResource(a), + WorldAccessRange::ComponentOrResource(b), + ) => *a == b, + (WorldAccessRange::External(a), WorldAccessRange::External(b)) => *a == b, + (WorldAccessRange::Global, _) => true, + (_, WorldAccessRange::Global) => true, + _ => false, + } + } +} + +impl From for WorldAccessRange { + fn from(value: ComponentId) -> Self { + Self::ComponentOrResource(value.into()) + } +} + +// impl AccessMapKey for T { +// /// Convert the key to an index +// fn from_world(&self, world: &UnsafeWorldCell) -> WorldAccessRange { +// world +// .components() +// .component_id::() +// .map(|c| { +// WorldAccessRange::Component(unsafe { +// NonZero::new_unchecked((c.index() as u16) + 1) +// }) +// }) +// .unwrap_or(WorldAccessRange::Unregistered) +// } + +// /// Describes the type of access this key represents +// fn describe(&self) -> String { +// format!("Component: {}", std::any::type_name::()) +// } +// } + +#[derive(Debug, Default)] +/// A map of access claims +pub struct AccessMap(Mutex); + +/// A trait for controlling system world access at runtime. +/// +/// This trait provides methods to claim and release read, write, and global access +/// to various parts of the world. Implementations of this trait manage internal state +/// to ensure safe and concurrent access to resources. Methods include scope-based locking, +/// as well as introspection of access state via code location information. +pub trait DynamicSystemMeta { + /// Executes the provided closure within a temporary access scope. + /// + /// Any accesses claimed within the scope are rolled back once the closure returns. + fn with_scope O>(&self, f: F) -> O; + + /// Attempts to claim read access for the given key. + /// + /// Returns `true` if the read access is successfully claimed. The claim will fail if + /// the key is currently locked for write or if a global lock is active. + #[track_caller] + fn claim_read_access>(&self, key: K) -> Result<(), AccessInstance>; + + /// Attempts to claim write access for the given key. + /// + /// Returns `true` if the write access is successfully claimed. Write access fails if any + /// read or write access is active for the key or if a global lock is held. + #[track_caller] + fn claim_write_access>(&self, key: K) -> Result<(), AccessInstance>; + + /// Releases an access claimed for the provided key. + /// + /// # Panics + /// + /// Panics if the access is released by a thread different from the one that claimed it. + fn release_access>(&self, key: K); + + /// Returns a list of active accesses. + /// + /// The list is provided as key and corresponding access count pairs. + fn list_accesses(&self) -> Vec<(WorldAccessRange, AccessInstance)>; + + /// Returns the number of active individual accesses. + /// + /// In the case of a global lock, this method considers that as a single active access. + fn count_accesses(&self) -> usize; + + /// Releases all active accesses. + /// + /// Both individual and global accesses will be removed. + fn release_all_accesses(&self); +} + +#[derive(Default, Debug, Clone)] +struct AccessMapInner { + individual_accesses: SmallVec<[(WorldAccessRange, AccessInstance); 4]>, +} + +#[profiling::all_functions] +impl AccessMapInner { + #[inline] + fn overlapping_access( + &self, + key: WorldAccessRange, + instance: &AccessInstance, + ) -> Option<&AccessInstance> { + self.individual_accesses + .iter() + .find_map(|(entry_key, entry_instance)| { + let overlaps = key.overlaps_with_access_to(*entry_key); + let one_is_exclusive = instance.written || entry_instance.written; + (overlaps && one_is_exclusive).then_some(entry_instance) + }) + } + + #[inline] + fn overlapping_access_mut( + &mut self, + key: WorldAccessRange, + instance: &AccessInstance, + ) -> Option<&mut AccessInstance> { + self.individual_accesses + .iter_mut() + .find_map(|(entry_key, entry_instance)| { + (key.overlaps_with_access_to(*entry_key) + && !(instance.written || entry_instance.written)) + .then_some(entry_instance) + }) + } + + #[inline] + fn insert(&mut self, key: WorldAccessRange, count: AccessInstance) { + self.individual_accesses.push((key, count)); + } + + #[inline] + fn clear_access(&mut self, key: WorldAccessRange) { + let idx = self + .individual_accesses + .iter() + .position(|(entry_key, _)| *entry_key == key); + if let Some(idx) = idx { + self.individual_accesses.remove(idx); + } + } + + // #[inline] + // fn entry(&self, key: WorldAccessRange) -> Option<&AccessCount> { + // self.individual_accesses + // .iter() + // .find_map(|(entry_key, access_count)| (key == *entry_key).then_some(access_count)) + // } + + // fn entry_index(&self, key: WorldAccessRange) -> Option { + // self.individual_accesses.iter().position(|(k, _)| *k == key) + // } + + // #[inline] + // fn entry_mut(&mut self, key: WorldAccessRange) -> Option<&mut AccessCount> { + // self.individual_accesses + // .iter_mut() + // .find_map(|(entry_key, access_count)| (key == *entry_key).then_some(access_count)) + // } + + // #[inline] + // fn entry_or_insert_default(&mut self, key: WorldAccessRange) -> &mut AccessCount { + // if let Some(i) = self.entry_index(key) { + // return &mut self.individual_accesses[i].1; + // } + + // self.individual_accesses.push((key, AccessCount::default())); + // // Safety: we just added one element, option is never None + // unsafe { &mut self.individual_accesses.last_mut().unwrap_unchecked().1 } + // } + + // #[inline] + // fn remove(&mut self, key: WorldAccessRange) { + // self.individual_accesses + // .retain(|(entry_key, _)| *entry_key != key); + // } +} + +#[profiling::all_functions] +impl DynamicSystemMeta for AccessMap { + fn release_access>(&self, key: K) { + let mut inner = self.0.lock(); + let range: WorldAccessRange = key.into(); + inner.clear_access(range); + } + + fn with_scope O>(&self, f: F) -> O { + // Snapshot the current inner state. + let backup = { + let inner = self.0.lock(); + inner.clone() + }; + + let result = f(); + + // Roll back the inner state. + { + let mut inner = self.0.lock(); + *inner = backup; + } + + result + } + + #[track_caller] + fn claim_read_access>(&self, key: K) -> Result<(), AccessInstance> { + let mut inner = self.0.lock(); + + let key = key.into(); + + let instance = AccessInstance { + owner: ClaimOwner { + location: *std::panic::Location::caller(), + }, + written: false, + }; + + if let Some(access) = inner.overlapping_access(key, &instance) { + Err(access.clone()) + } else { + inner.insert(key, instance); + Ok(()) + } + } + + #[track_caller] + fn claim_write_access>(&self, key: K) -> Result<(), AccessInstance> { + let mut inner = self.0.lock(); + + let key = key.into(); + + let instance = AccessInstance { + owner: ClaimOwner { + location: *std::panic::Location::caller(), + }, + written: true, + }; + + if let Some(access) = inner.overlapping_access(key, &instance) { + Err(access.clone()) + } else { + inner.insert(key, instance); + Ok(()) + } + } + + fn list_accesses(&self) -> Vec<(WorldAccessRange, AccessInstance)> { + let inner = self.0.lock(); + inner + .individual_accesses + .iter() + .map(|(key, a)| (*key, a.clone())) + .collect() + } + + fn count_accesses(&self) -> usize { + let inner = self.0.lock(); + inner.individual_accesses.len() + } + + fn release_all_accesses(&self) { + let mut inner = self.0.lock(); + inner.individual_accesses.clear(); + } +} + +/// An inverse of [`AccessMap`], It limits the resource/component accesses allowed to be claimed to those in a pre-specified subset. +pub struct SubsetAccessMap { + inner: AccessMap, + component_subset: FixedBitSet, +} + +#[profiling::all_functions] +impl SubsetAccessMap { + /// Creates a new subset access map with the provided subset of ID's as well as a exception function. + pub fn new(subset: impl IntoIterator>) -> Self { + let components = subset.into_iter().filter_map(|a| match a.into() { + WorldAccessRange::ComponentOrResource(range) => Some(range.0.get() as usize), + _ => None, + }); + Self { + inner: Default::default(), + component_subset: FixedBitSet::from_iter(components), + } + } + + fn allowed_access(&self, range: WorldAccessRange) -> bool { + match range { + WorldAccessRange::ComponentOrResource(s) => { + self.component_subset.contains(s.0.get() as usize) + } + WorldAccessRange::External(_) => true, + WorldAccessRange::Global => false, + } + } +} + +#[profiling::all_functions] +impl DynamicSystemMeta for SubsetAccessMap { + fn with_scope O>(&self, f: F) -> O { + self.inner.with_scope(f) + } + + fn release_access>(&self, key: K) { + let key = key.into(); + if !self.allowed_access(key) { + return; + } + self.inner.release_access(key); + } + + #[track_caller] + fn claim_read_access>(&self, key: K) -> Result<(), AccessInstance> { + let key = key.into(); + if !self.allowed_access(key) { + return Err(AccessInstance { + owner: ClaimOwner { + location: *std::panic::Location::caller(), + }, + written: true, + }); + } + self.inner.claim_read_access(key) + } + + #[track_caller] + fn claim_write_access>(&self, key: K) -> Result<(), AccessInstance> { + let key = key.into(); + if !self.allowed_access(key) { + return Err(AccessInstance { + owner: ClaimOwner { + location: *std::panic::Location::caller(), + }, + written: true, + }); + } + self.inner.claim_write_access(key) + } + + fn list_accesses(&self) -> Vec<(WorldAccessRange, AccessInstance)> { + self.inner.list_accesses() + } + + fn count_accesses(&self) -> usize { + self.inner.count_accesses() + } + + fn release_all_accesses(&self) { + self.inner.release_all_accesses(); + } +} + +/// A polymorphic enum for access map types. +/// +/// Equivalent to `dyn DynamicSystemMeta` for most purposes +pub enum AnyAccessMap { + /// A map which allows any and all accesses to be claimed + UnlimitedAccessMap(AccessMap), + /// A map which only allows accesses to keys in a pre-specified subset + SubsetAccessMap(SubsetAccessMap), +} + +#[profiling::all_functions] +impl DynamicSystemMeta for AnyAccessMap { + fn with_scope O>(&self, f: F) -> O { + match self { + AnyAccessMap::UnlimitedAccessMap(map) => map.with_scope(f), + AnyAccessMap::SubsetAccessMap(map) => map.with_scope(f), + } + } + + fn release_access>(&self, key: K) { + match self { + AnyAccessMap::UnlimitedAccessMap(map) => map.release_access(key), + AnyAccessMap::SubsetAccessMap(map) => map.release_access(key), + } + } + + #[track_caller] + fn claim_read_access>(&self, key: K) -> Result<(), AccessInstance> { + match self { + AnyAccessMap::UnlimitedAccessMap(map) => map.claim_read_access(key), + AnyAccessMap::SubsetAccessMap(map) => map.claim_read_access(key), + } + } + + #[track_caller] + fn claim_write_access>(&self, key: K) -> Result<(), AccessInstance> { + match self { + AnyAccessMap::UnlimitedAccessMap(map) => map.claim_write_access(key), + AnyAccessMap::SubsetAccessMap(map) => map.claim_write_access(key), + } + } + + fn list_accesses(&self) -> Vec<(WorldAccessRange, AccessInstance)> { + match self { + AnyAccessMap::UnlimitedAccessMap(map) => map.list_accesses(), + AnyAccessMap::SubsetAccessMap(map) => map.list_accesses(), + } + } + + fn count_accesses(&self) -> usize { + match self { + AnyAccessMap::UnlimitedAccessMap(map) => map.count_accesses(), + AnyAccessMap::SubsetAccessMap(map) => map.count_accesses(), + } + } + + fn release_all_accesses(&self) { + match self { + AnyAccessMap::UnlimitedAccessMap(map) => map.release_all_accesses(), + AnyAccessMap::SubsetAccessMap(map) => map.release_all_accesses(), + } + } +} + +/// A trait for displaying a code location nicely +pub trait DisplayCodeLocation { + /// Displays the location + fn display_location(self) -> String; +} + +#[profiling::all_functions] +impl DisplayCodeLocation for std::panic::Location<'_> { + fn display_location(self) -> String { + format!("\"{}:{}\"", self.file(), self.line()) + } +} + +#[profiling::all_functions] +impl DisplayCodeLocation for Option> { + fn display_location(self) -> String { + self.map(|l| l.display_location()) + .unwrap_or_else(|| "\"unknown location\"".to_owned()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + + struct TestAccess(pub usize); + impl From for WorldAccessRange { + fn from(val: TestAccess) -> Self { + WorldAccessRange::ComponentOrResource(ComponentRange(unsafe { + NonZero::new_unchecked((val.0 + 1) as u16) + })) + } + } + + #[test] + fn access_map_list_accesses() { + let access_map = AccessMap::default(); + + let _ = access_map.claim_read_access(TestAccess(1)); + let _ = access_map.claim_write_access(TestAccess(2)); + + let accesses = access_map.list_accesses(); + + assert_eq!(accesses.len(), 2); + let access_0 = accesses + .iter() + .find(|(k, _)| *k == TestAccess(1).into()) + .unwrap(); + let access_1 = accesses + .iter() + .find(|(k, _)| *k == TestAccess(2).into()) + .unwrap(); + + assert!(!access_0.1.written); + assert!(access_1.1.written); + } + + #[test] + fn subset_access_map_list_accesses() { + let subset_access_map = SubsetAccessMap::new([TestAccess(1), TestAccess(2)]); + + assert!(subset_access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(subset_access_map.claim_write_access(TestAccess(2)).is_ok()); + + let accesses = subset_access_map.list_accesses(); + + assert_eq!(accesses.len(), 2); + let access_0 = accesses + .iter() + .find(|(k, _)| *k == TestAccess(1).into()) + .unwrap(); + let access_1 = accesses + .iter() + .find(|(k, _)| *k == TestAccess(2).into()) + .unwrap(); + + assert!(!access_0.1.written); + assert!(access_1.1.written); + } + + #[test] + fn access_map_read_access_blocks_write() { + let access_map = AccessMap::default(); + + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(access_map.claim_write_access(TestAccess(1)).is_err()); + access_map.release_access(TestAccess(1)); + assert!(access_map.claim_write_access(TestAccess(1)).is_ok()); + } + + #[test] + fn subset_access_map_read_access_blocks_write() { + let subset_access_map = SubsetAccessMap::new([TestAccess(1)]); + + assert!(subset_access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(subset_access_map.claim_write_access(TestAccess(1)).is_err()); + subset_access_map.release_access(TestAccess(1)); + assert!(subset_access_map.claim_write_access(TestAccess(1)).is_ok()); + } + + #[test] + fn access_map_write_access_blocks_read() { + let access_map = AccessMap::default(); + + assert!(access_map.claim_write_access(TestAccess(1)).is_ok()); + assert!(access_map.claim_read_access(TestAccess(1)).is_err()); + access_map.release_access(TestAccess(1)); + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + } + + #[test] + fn subset_access_map_write_access_blocks_read() { + let subset_access_map = SubsetAccessMap::new([TestAccess(1)]); + + assert!(subset_access_map.claim_write_access(TestAccess(1)).is_ok()); + assert!(subset_access_map.claim_read_access(TestAccess(1)).is_err()); + subset_access_map.release_access(TestAccess(1)); + assert!(subset_access_map.claim_read_access(TestAccess(1)).is_ok()); + } + + #[test] + fn access_map_read_global_access_blocks_all_writes() { + let access_map = AccessMap::default(); + + assert!( + access_map + .claim_read_access(WorldAccessRange::Global) + .is_ok() + ); + assert!(access_map.claim_write_access(TestAccess(1)).is_err()); + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + access_map.release_access(WorldAccessRange::Global); + access_map.release_access(TestAccess(1)); + + // can re-claim after releasing global + assert!(access_map.claim_write_access(TestAccess(1)).is_ok()); + access_map.release_access(TestAccess(1)); + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + } + + #[test] + fn access_map_write_global_access_blocks_all_access() { + let access_map = AccessMap::default(); + + assert!( + access_map + .claim_write_access(WorldAccessRange::Global) + .is_ok() + ); + assert!(access_map.claim_write_access(TestAccess(1)).is_err()); + assert!(access_map.claim_read_access(TestAccess(1)).is_err()); + access_map.release_access(WorldAccessRange::Global); + + // can re-claim after releasing global + assert!(access_map.claim_write_access(TestAccess(1)).is_ok()); + access_map.release_access(TestAccess(1)); + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + } + + #[test] + fn subset_access_map_cannot_read_global_access() { + let subset_access_map = SubsetAccessMap::new([TestAccess(1), TestAccess(2)]); + + assert!( + subset_access_map + .claim_read_access(WorldAccessRange::Global) + .is_err() + ); + } + + #[test] + fn access_map_any_access_blocks_write_global() { + let access_map = AccessMap::default(); + + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!( + access_map + .claim_write_access(WorldAccessRange::Global) + .is_err() + ); + access_map.release_access(TestAccess(1)); + + assert!(access_map.claim_write_access(TestAccess(1)).is_ok()); + assert!( + access_map + .claim_write_access(WorldAccessRange::Global) + .is_err() + ); + } + + #[test] + fn access_map_with_scope_unrolls_individual_accesses() { + let access_map = AccessMap::default(); + // Claim a read access outside the scope + assert!(access_map.claim_read_access(TestAccess(3)).is_ok()); + + // Inside with_scope, claim additional accesses + access_map.with_scope(|| { + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(access_map.claim_write_access(TestAccess(2)).is_ok()); + // At this point, individual_accesses contains keys 0, 1 and 2. + let accesses = access_map.list_accesses(); + assert_eq!(accesses.len(), 3); + }); + + // After with_scope returns, accesses claimed inside (keys 1 and 2) are unrolled. + let accesses = access_map.list_accesses(); + // Only the access claimed outside (key 3) remains. + assert_eq!(accesses.len(), 1); + let (k, count) = &accesses[0]; + assert_eq!(*k, TestAccess(3).into()); + assert!(!count.written); + } + + #[test] + fn subset_map_with_scope_unrolls_individual_accesses() { + let subset_access_map = SubsetAccessMap::new([TestAccess(1), TestAccess(2), TestAccess(3)]); + + // Claim a read access outside the scope + assert!(subset_access_map.claim_read_access(TestAccess(3)).is_ok()); + + // Inside with_scope, claim additional accesses + subset_access_map.with_scope(|| { + assert!(subset_access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(subset_access_map.claim_write_access(TestAccess(2)).is_ok()); + // At this point, individual_accesses contains keys 0, 1 and 2. + let accesses = subset_access_map.list_accesses(); + assert_eq!(accesses.len(), 3); + }); + + // After with_scope returns, accesses claimed inside (keys 1 and 2) are unrolled. + let accesses = subset_access_map.list_accesses(); + // Only the access claimed outside (key 3) remains. + assert_eq!(accesses.len(), 1); + let (k, count) = &accesses[0]; + assert_eq!(*k, TestAccess(3).into()); + assert!(!count.written); + } + + #[test] + fn access_map_with_scope_unrolls_global_accesses() { + let access_map = AccessMap::default(); + + access_map.with_scope(|| { + assert!( + access_map + .claim_write_access(WorldAccessRange::Global) + .is_ok() + ); + // At this point, global_access is claimed. + assert!(access_map.claim_read_access(TestAccess(1)).is_err()); + }); + + let accesses = access_map.list_accesses(); + assert_eq!(accesses.len(), 0); + } + + #[test] + fn access_map_count_accesses_counts_globals() { + let access_map = AccessMap::default(); + + // Initially, no accesses are active. + assert_eq!(access_map.count_accesses(), 0); + + // Claim global access. When global access is active, + // count_accesses should return 1. + assert!( + access_map + .claim_write_access(WorldAccessRange::Global) + .is_ok() + ); + assert_eq!(access_map.count_accesses(), 1); + access_map.release_access(WorldAccessRange::Global); + + // Now claim individual accesses. + assert!(access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(access_map.claim_write_access(TestAccess(2)).is_ok()); + // Since two separate keys were claimed, count_accesses should return 2. + assert_eq!(access_map.count_accesses(), 2); + + // Cleanup individual accesses. + access_map.release_access(TestAccess(1)); + access_map.release_access(TestAccess(2)); + } + + #[test] + fn subset_map_prevents_access_to_out_of_subset_access() { + let subset_access_map = SubsetAccessMap::new([TestAccess(1)]); + + assert!(subset_access_map.claim_read_access(TestAccess(2)).is_err()); + assert!(subset_access_map.claim_write_access(TestAccess(2)).is_err()); + assert!( + subset_access_map + .claim_read_access(WorldAccessRange::Global) + .is_err() + ); + } + + #[test] + fn subset_map_retains_subset_in_scope() { + let subset_access_map = SubsetAccessMap::new([TestAccess(1)]); + + subset_access_map.with_scope(|| { + assert!(subset_access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(subset_access_map.claim_read_access(TestAccess(2)).is_err()); + assert!(subset_access_map.claim_write_access(TestAccess(2)).is_err()); + }); + + assert!(subset_access_map.claim_read_access(TestAccess(1)).is_ok()); + assert!(subset_access_map.claim_read_access(TestAccess(2)).is_err()); + assert!(subset_access_map.claim_write_access(TestAccess(2)).is_err()); + } +} diff --git a/crates/bevy_mod_scripting_world/src/lib.rs b/crates/bevy_mod_scripting_world/src/lib.rs new file mode 100644 index 0000000000..23d6bb6294 --- /dev/null +++ b/crates/bevy_mod_scripting_world/src/lib.rs @@ -0,0 +1,7 @@ +//! Abstractions for interacting with the bevy world without knowing compile time type information safely. + +mod access_map; +mod world; + +pub use access_map::*; +pub use world::*; diff --git a/crates/bevy_mod_scripting_world/src/world.rs b/crates/bevy_mod_scripting_world/src/world.rs new file mode 100644 index 0000000000..2f2d659f67 --- /dev/null +++ b/crates/bevy_mod_scripting_world/src/world.rs @@ -0,0 +1,654 @@ +//! # Motivation +//! +//! Traits and structs needed to support the creation of bindings for scripting languages. +//! reflection gives us access to `dyn PartialReflect` objects via their type name, +//! Scripting languages only really support `Clone` objects so if we want to support references, +//! we need wrapper types which have owned and ref variants. + +use crate::WorldAccessRange; + +use super::access_map::{AccessInstance, AnyAccessMap, DynamicSystemMeta, SubsetAccessMap}; +use ::bevy_ecs::{ + component::ComponentId, + world::{World, unsafe_world_cell::UnsafeWorldCell}, +}; +use bevy_ecs::{ + component::Component, reflect::AppTypeRegistry, resource::Resource, system::Command, + world::WorldId, +}; +use bevy_reflect::TypeRegistryArc; +use std::{ + any::{Any, TypeId}, + cell::{Ref, RefCell}, + fmt::Debug, + panic::Location, + rc::Rc, + sync::atomic::AtomicBool, +}; + +/// Prefer to directly using [`WorldAccessGuard`]. If the underlying type changes, this alias will be updated. +pub type WorldGuard<'w> = WorldAccessGuard<'w>; +/// Similar to [`WorldGuard`], but without the arc, use for when you don't need the outer Arc. +pub type WorldGuardRef<'w> = &'w WorldAccessGuard<'w>; + +/// A class of errors related to accessing the world with untyped acccess information +#[derive(Debug)] +pub enum DynWorldAccessError { + /// World thread local was not set + MissingWorld, + /// Could not claim necessary access + CannotClaimAccess(WorldAccessRange, Option>, String), + /// Resource was not registered + UnregisteredResource(TypeId), + /// Component was not registered + UnregisteredComponent(TypeId), +} + +impl DynWorldAccessError { + /// Creates [`DynWorldAccessError::MissingWorld`] + pub fn missing_world() -> Self { + Self::MissingWorld + } + + /// Creates [`DynWorldAccessError::CannotClaimAccess`] + pub fn cannot_claim_access( + key: WorldAccessRange, + location: Option>, + msg: impl ToString, + ) -> Self { + Self::CannotClaimAccess(key, location, msg.to_string()) + } +} + +/// Provides safe access to the world via [`AnyAccessMap`] permissions, which enforce aliasing rules at runtime in multi-thread environments +#[derive(Clone, Debug)] +pub struct WorldAccessGuard<'w> { + /// The guard this guard pointer represents + pub(crate) inner: Rc>, + /// if true the guard is invalid and cannot be used, stored as a second pointer so that this validity can be + /// stored separate from the contents of the guard + invalid: Rc, +} +impl WorldAccessGuard<'_> { + /// Returns the id of the world this guard provides access to + pub fn id(&self) -> WorldId { + self.inner.cell.id() + } +} + +/// A registry which is cached withing the [`WorldAccessGuard`] to avoid many access lookups. +/// +/// Slots are reserved by each registry type, and the same slot must not ever be used by two registries of different types. +/// +/// Allows us to decouple dependencies while retaining some caching benefits. +pub trait CachedRegistry: Any { + /// The cache slot used by this registry. + /// Must not be used by another registry to work correctly. + const SLOT: usize; +} + +/// Aliases the type used as the registry cache for the world guard. +pub type RegistryCache = [Rc>; 5]; + +/// Used to decrease the stack size of [`WorldAccessGuard`] +pub(crate) struct WorldAccessGuardInner<'w> { + /// Safety: cannot be used unless the scope depth is less than the max valid scope + cell: UnsafeWorldCell<'w>, + // TODO: this is fairly hefty, explore sparse sets, bit fields etc + pub(crate) accesses: AnyAccessMap, + /// Cached for convenience, since we need it for most operations, means we don't need to lock the type registry every time + type_registry: TypeRegistryArc, + + cached_slots: RegistryCache, + // /// The script allocator for the world + // allocator: AppReflectAllocator, + // /// The function registry for the world + // function_registry: AppScriptFunctionRegistry, + // /// The schedule registry for the world + // schedule_registry: AppScheduleRegistry, + // /// The registry of script registered components + // script_component_registry: AppScriptComponentRegistry, +} + +impl std::fmt::Debug for WorldAccessGuardInner<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WorldAccessGuardInner").finish() + } +} + +#[profiling::all_functions] +impl WorldAccessGuard<'static> { + /// Shortens the lifetime of the guard to the given lifetime. + pub(crate) fn shorten_lifetime<'w>(self) -> WorldGuard<'w> { + // Safety: todo + unsafe { std::mem::transmute(self) } + } +} + +#[profiling::all_functions] +impl<'w> WorldAccessGuard<'w> { + /// Retrieves the cached type registry from this instantiation of the guard + pub fn type_registry(&self) -> &TypeRegistryArc { + &self.inner.type_registry + } + + #[track_caller] + /// Claims read access to the given type. + pub fn claim_read_access( + &self, + raid: impl Into, + ) -> Result<(), AccessInstance> { + self.inner.accesses.claim_read_access(raid) + } + + #[track_caller] + /// Claims write access to the given type. + pub fn claim_write_access( + &self, + raid: impl Into, + ) -> Result<(), AccessInstance> { + self.inner.accesses.claim_write_access(raid) + } + + /// Releases read or write access to the given type. + /// + /// # Safety + /// - This can only be called safely after all references to the type created using the access have been dropped + /// - You can only call this if you previously called one of: [`WorldAccessGuard::claim_read_access`] or [`WorldAccessGuard::claim_write_access`] + /// - The number of claim and release calls for the same id must always match + pub unsafe fn release_access(&self, raid: impl Into) { + self.inner.accesses.release_access(raid) + } + + /// Procures and releases the given access key allowing safe read access to world resources + /// requiring that key within the given closure + #[track_caller] + pub fn with_read_access O, O>( + &self, + key: impl Into, + f: F, + ) -> Result { + let key = key.into(); + if let Err(conflicting_access) = self.inner.accesses.claim_read_access(key) { + Err(DynWorldAccessError::cannot_claim_access( + key, + Some(conflicting_access.owner.location), + "Could not claim read access", + )) + } else { + let res = f(); + // Safety: we have claimed read access to this key + unsafe { self.release_access(key) }; + Ok(res) + } + } + + /// Procures and releases the given access key allowing safe read access to world resources + /// requiring that key within the given closure. + /// + /// This is a version of [`WorldAccessGuard::with_read_access`] which flattens errors using into implementations + #[track_caller] + pub fn with_read_access_and_then Result, O>( + &self, + key: impl Into, + f: F, + ) -> Result + where + DynWorldAccessError: Into, + { + let key = key.into(); + if let Err(conflicting_access) = self.inner.accesses.claim_read_access(key) { + Err(DynWorldAccessError::cannot_claim_access( + key, + Some(conflicting_access.owner.location), + "Could not claim read access", + ) + .into()) + } else { + let res = f()?; + // Safety: we have claimed read access to this key + unsafe { self.release_access(key) }; + Ok(res) + } + } + + /// Procures and releases the given access key allowing safe write access to world resources + /// requiring that key within the given closure + #[track_caller] + pub fn with_write_access O, O>( + &self, + key: impl Into, + f: F, + ) -> Result { + let key = key.into(); + if let Err(conflicting_access) = self.inner.accesses.claim_write_access(key) { + Err(DynWorldAccessError::cannot_claim_access( + key, + Some(conflicting_access.owner.location), + "Could not claim write access", + )) + } else { + let res = f(); + // Safety: we have claimed read access to this key + unsafe { self.release_access(key) }; + Ok(res) + } + } + + /// Procures and releases the given access key allowing safe write access to world resources + /// requiring that key within the given closure + /// + /// This is a version of [`WorldAccessGuard::with_write_access`] which flattens errors using into implementations. + #[track_caller] + pub fn with_write_access_and_then Result, O>( + &self, + key: impl Into, + f: F, + ) -> Result + where + DynWorldAccessError: Into, + { + let key = key.into(); + if let Err(conflicting_access) = self.inner.accesses.claim_write_access(key) { + Err(DynWorldAccessError::cannot_claim_access( + key, + Some(conflicting_access.owner.location), + "Could not claim write access", + ) + .into()) + } else { + let res = f()?; + // Safety: we have claimed read access to this key + unsafe { self.release_access(key) }; + Ok(res) + } + } + + /// Procures and releases the given access key allowing safe read access to world resources + /// requiring that key within the given closure + #[track_caller] + pub fn with_world_access O, O>( + &self, + f: F, + ) -> Result { + self.with_read_access(WorldAccessRange::Global, || { + let cell = self.as_unsafe_world_cell()?; + // Safety: we have exclusive access + Ok(f(unsafe { cell.world() })) + })? + } + + /// Procures and releases the given access key allowing safe write access to world resources + /// requiring that key within the given closure + #[track_caller] + pub fn with_world_mut_access O, O>( + &self, + f: F, + ) -> Result { + self.with_write_access(WorldAccessRange::Global, || { + let cell = self.as_unsafe_world_cell()?; + // Safety: we have exclusive access + Ok(f(unsafe { cell.world_mut() })) + })? + } + + /// Procures and releases the given access key allowing safe write access to world resources + /// requiring that key within the given closure + /// + /// This is a version of [`WorldAccessGuard::with_world_mut_access`] which flattens errors using into implementations. + #[track_caller] + pub fn with_world_mut_access_and_then Result, O>( + &self, + f: F, + ) -> Result + where + DynWorldAccessError: Into, + { + let cell = self.as_unsafe_world_cell().map_err(Into::into)?; + self.with_write_access_and_then(WorldAccessRange::Global, || { + // Safety: we have exclusive access + f(unsafe { cell.world_mut() }) + }) + } + + /// Procures and releases the given access key allowing safe read access to world resources + /// requiring that key within the given closure + /// + /// This is a version of [`WorldAccessGuard::with_world_access`] which flattens errors using into implementations. + #[track_caller] + pub fn with_world_access_and_then Result, O>( + &self, + f: F, + ) -> Result + where + DynWorldAccessError: Into, + { + let cell = self.as_unsafe_world_cell().map_err(Into::into)?; + self.with_write_access_and_then(WorldAccessRange::Global, || { + // Safety: we have exclusive access + f(unsafe { cell.world_mut() }) + }) + } + + /// Returns true if the guard is valid, false if it is invalid + fn is_valid(&self) -> bool { + !self.invalid.load(std::sync::atomic::Ordering::Relaxed) + } + + /// Invalidates the world access guard, making it and any guards derived from this one unusable. + pub fn invalidate(&self) { + self.invalid + .store(true, std::sync::atomic::Ordering::Relaxed); + } + + /// Safely allows access to the world for the duration of the closure via a static [`WorldAccessGuard`]. + /// + /// The guard is invalidated at the end of the closure, meaning the world cannot be accessed at all after the closure ends. + pub fn with_static_guard( + world: &'w mut World, + cached_slots: RegistryCache, + f: impl FnOnce(WorldGuard<'static>) -> O, + ) -> O { + let guard = WorldAccessGuard::new_exclusive(world, cached_slots); + // safety: we invalidate the guard after the closure is called, meaning the world cannot be accessed at all after the 'w lifetime ends + let static_guard: WorldAccessGuard<'static> = unsafe { std::mem::transmute(guard) }; + ThreadWorldContainer.set_context(ThreadScriptContext { + world: static_guard.clone(), + }); + let o = f(static_guard.clone()); + + static_guard.invalidate(); + o + } + + /// Safely allows access to the world for the duration of the closure via a static [`WorldAccessGuard`] using a previously lifetimed world guard. + /// Will invalidate the static guard at the end but not the original. + pub fn with_existing_static_guard( + guard: WorldAccessGuard<'w>, + f: impl FnOnce(WorldGuard<'static>) -> O, + ) -> O { + // safety: we invalidate the guard after the closure is called, meaning the world cannot be accessed at all after the 'w lifetime ends, from the static guard + // i.e. even if somebody squirells it away, it will be useless. + let static_guard: WorldAccessGuard<'static> = unsafe { std::mem::transmute(guard.scope()) }; + ThreadWorldContainer.set_context(ThreadScriptContext { + world: static_guard.clone(), + }); + let o = f(static_guard.clone()); + static_guard.invalidate(); + o + } + + /// Creates a new [`WorldAccessGuard`] from a possibly non-exclusive access to the world. + /// + /// It requires specyfing the exact accesses that are allowed to be given out by the guard. + /// Those accesses need to be safe to be given out to the script, as the guard will assume that it is safe to give them out in any way. + /// + /// # Safety + /// - The caller must ensure that the accesses in subset are not aliased by any other access + /// - If an access is allowed in this subset, but alised by someone else, + pub unsafe fn new_non_exclusive( + world: UnsafeWorldCell<'w>, + subset: impl IntoIterator>, + type_registry: TypeRegistryArc, + registry_cache: RegistryCache, + ) -> Self { + Self { + inner: Rc::new(WorldAccessGuardInner { + cell: world, + accesses: AnyAccessMap::SubsetAccessMap(SubsetAccessMap::new(subset)), + type_registry, + cached_slots: registry_cache, + }), + invalid: Rc::new(false.into()), + } + } + + /// Creates a new [`WorldAccessGuard`] for the given mutable borrow of the world. + /// + /// If these resources do not exist, they will be initialized. + pub fn new_exclusive(world: &'w mut World, registry_cache: RegistryCache) -> Self { + let type_registry = world.get_resource_or_init::().0.clone(); + Self { + inner: Rc::new(WorldAccessGuardInner { + cell: world.as_unsafe_world_cell(), + accesses: AnyAccessMap::UnlimitedAccessMap(Default::default()), + cached_slots: registry_cache, + type_registry, + }), + invalid: Rc::new(false.into()), + } + } + + /// Queues a command to the world, which will be executed later. + /// + /// Requires exclusive world access. + pub(crate) fn queue(&self, command: impl Command) -> Result<(), DynWorldAccessError> { + self.with_world_mut_access(|w| { + w.commands().queue(command); + }) + } + + /// Runs a closure within an isolated access scope, releasing leftover accesses, should only be used in a single-threaded context. + /// + /// Safety: + /// - The caller must ensure it's safe to release any potentially locked accesses. + pub unsafe fn with_access_scope O>( + &self, + f: F, + ) -> Result { + Ok(self.inner.accesses.with_scope(f)) + } + + /// Gets the component id of the given component or resource + pub fn get_component_id(&self, id: TypeId) -> Result, DynWorldAccessError> { + Ok(self + .as_unsafe_world_cell_readonly()? + .components() + .get_id(id)) + } + + /// Gets the resource id of the given component or resource + pub fn get_resource_id(&self, id: TypeId) -> Result, DynWorldAccessError> { + Ok(self + .as_unsafe_world_cell_readonly()? + .components() + .get_resource_id(id)) + } + + fn resource_component_id(&self) -> Result { + self.as_unsafe_world_cell()? + .components() + .resource_id::() + .ok_or_else(|| DynWorldAccessError::UnregisteredResource(TypeId::of::())) + } + + fn component_component_id(&self) -> Result { + self.as_unsafe_world_cell()? + .components() + .component_id::() + .ok_or_else(|| DynWorldAccessError::UnregisteredComponent(TypeId::of::())) + } + + /// creates a new guard derived from this one, which if invalidated, will not invalidate the original + fn scope(&self) -> Self { + let mut new_guard = self.clone(); + new_guard.invalid = Rc::new( + new_guard + .invalid + .load(std::sync::atomic::Ordering::Relaxed) + .into(), + ); + new_guard + } + + /// Retrieves the underlying unsafe world cell, with no additional guarantees of safety + /// proceed with caution and only use this if you understand what you're doing + pub fn as_unsafe_world_cell(&self) -> Result, DynWorldAccessError> { + if !self.is_valid() { + return Err(DynWorldAccessError::missing_world()); + } + + Ok(self.inner.cell) + } + + /// Retrieves the underlying read only unsafe world cell, with no additional guarantees of safety + /// proceed with caution and only use this if you understand what you're doing + pub fn as_unsafe_world_cell_readonly( + &self, + ) -> Result, DynWorldAccessError> { + if !self.is_valid() { + return Err(DynWorldAccessError::missing_world()); + } + + Ok(self.inner.cell) + } + + /// Purely debugging utility to list all accesses currently held. + pub fn list_accesses(&self) -> Vec<(WorldAccessRange, AccessInstance)> { + self.inner.accesses.list_accesses() + } + + /// Should only really be used for testing purposes + pub unsafe fn release_all_accesses(&self) { + self.inner.accesses.release_all_accesses(); + } + + /// Returns the number of accesses currently held. + pub fn access_len(&self) -> usize { + self.inner.accesses.count_accesses() + } +} + +/// Impl block for higher level world methods +#[profiling::all_functions] +impl WorldAccessGuard<'_> { + /// If a registry has been initialized in this world guard, downcasts it to its original type and returns + /// a reference to it + pub fn get_cached_registry<'a, T: CachedRegistry>(&'a self) -> Option> { + let idx = T::SLOT; + Ref::filter_map(self.inner.cached_slots[idx].borrow(), |r| r.downcast_ref()).ok() + } + + /// If a registry has been initialized in this world guard, downcasts it to its original type and returns + /// a reference to it + pub fn set_cached_registry(&self, registry: T) { + let idx = T::SLOT; + let mut mutt = RefCell::borrow_mut(&self.inner.cached_slots[idx]); + + #[allow( + clippy::unwrap_used, + reason = "internal domain boundary, enforced at creation of the guard" + )] + let mutt = mutt.downcast_mut().unwrap(); + *mutt = registry; + } +} + +/// A world container that stores the world in a thread local +pub struct ThreadWorldContainer; + +#[derive(Clone)] +/// Context passed down indirectly to script related functions, used to avoid prop drilling problems. +pub struct ThreadScriptContext<'l> { + /// The world pointer + pub world: WorldGuard<'l>, + // /// The currently active script attachment + // pub attachment: ScriptAttachment, +} + +thread_local! { + static WORLD_CALLBACK_ACCESS: RefCell>> = const { RefCell::new(None) }; +} +#[profiling::all_functions] +impl ThreadWorldContainer { + /// Sets the thread context to the given value + pub fn set_context(&mut self, world: ThreadScriptContext<'static>) { + WORLD_CALLBACK_ACCESS.with(|w| { + w.replace(Some(world)); + }); + } + + /// Tries to get the world from the container + pub fn try_get_context<'l>(&self) -> Result, DynWorldAccessError> { + WORLD_CALLBACK_ACCESS + .with(|w| { + w.borrow() + .clone() + .ok_or_else(DynWorldAccessError::missing_world) + }) + .map(|v| ThreadScriptContext { + world: v.world.shorten_lifetime(), + // attachment: v.attachment, + }) + } +} + +#[cfg(test)] +mod test { + use std::array; + + use super::*; + + #[derive(Default)] + struct TestRegistry; + impl CachedRegistry for TestRegistry { + const SLOT: usize = 0; + } + + #[test] + fn test_scoped_handle_invalidate_doesnt_invalidate_parent() { + let mut world = World::new(); + let world = WorldAccessGuard::new_exclusive( + &mut world, + array::from_fn(|_| Rc::new(RefCell::new(TestRegistry)) as Rc>), + ); + let scoped_world = world.scope(); + + // can use scoped & normal worlds + assert!(scoped_world.is_valid()); + assert!(world.is_valid()); + pretty_assertions::assert_eq!(scoped_world.is_valid(), true); + pretty_assertions::assert_eq!(world.is_valid(), true); + + scoped_world.invalidate(); + + // can only use normal world + pretty_assertions::assert_eq!(scoped_world.is_valid(), false); + pretty_assertions::assert_eq!(world.is_valid(), true); + assert!(world.is_valid()); + } + + #[test] + fn with_existing_static_guard_does_not_invalidate_original() { + let mut world = World::new(); + let world = WorldAccessGuard::new_exclusive( + &mut world, + array::from_fn(|_| Rc::new(RefCell::new(TestRegistry)) as Rc>), + ); + + let mut sneaky_clone = None; + WorldAccessGuard::with_existing_static_guard(world.clone(), |g| { + pretty_assertions::assert_eq!(g.is_valid(), true); + sneaky_clone = Some(g.clone()); + }); + pretty_assertions::assert_eq!(world.is_valid(), true, "original world was invalidated"); + pretty_assertions::assert_eq!( + sneaky_clone.map(|c| c.is_valid()), + Some(false), + "scoped world was not invalidated" + ); + } + + #[test] + fn test_with_access_scope_success() { + let mut world = World::new(); + let guard = WorldAccessGuard::new_exclusive( + &mut world, + array::from_fn(|_| Rc::new(RefCell::new(TestRegistry)) as Rc>), + ); + + // within the access scope, no extra accesses are claimed + let result = unsafe { guard.with_access_scope(|| 100) }; + assert_eq!(result.unwrap(), 100); + } +} diff --git a/crates/bindings/bevy_a11y_bms_bindings/Cargo.toml b/crates/bindings/bevy_a11y_bms_bindings/Cargo.toml index 4626261f94..dfd95040a5 100644 --- a/crates/bindings/bevy_a11y_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_a11y_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_animation_bms_bindings/Cargo.toml b/crates/bindings/bevy_animation_bms_bindings/Cargo.toml index 81ca7e1714..c419bed796 100644 --- a/crates/bindings/bevy_animation_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_animation_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_asset_bms_bindings/Cargo.toml b/crates/bindings/bevy_asset_bms_bindings/Cargo.toml index ab6a87f74c..f38d24bed0 100644 --- a/crates/bindings/bevy_asset_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_asset_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] bevy_ecs = { workspace = true, features = ["std", "bevy_reflect"] } bevy_app = { workspace = true, features = ["std"] } diff --git a/crates/bindings/bevy_camera_bms_bindings/Cargo.toml b/crates/bindings/bevy_camera_bms_bindings/Cargo.toml index 078991dd0d..874178377f 100644 --- a/crates/bindings/bevy_camera_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_camera_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_color_bms_bindings/Cargo.toml b/crates/bindings/bevy_color_bms_bindings/Cargo.toml index d72230bb8e..e9ffde888a 100644 --- a/crates/bindings/bevy_color_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_color_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] bevy_ecs = { workspace = true, features = ["std", "bevy_reflect"] } bevy_app = { workspace = true, features = ["std"] } diff --git a/crates/bindings/bevy_core_pipeline_bms_bindings/Cargo.toml b/crates/bindings/bevy_core_pipeline_bms_bindings/Cargo.toml index 467fcf74e6..05598cea24 100644 --- a/crates/bindings/bevy_core_pipeline_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_core_pipeline_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_ecs_bms_bindings/Cargo.toml b/crates/bindings/bevy_ecs_bms_bindings/Cargo.toml index f1e73f3e40..caf0cfeb97 100644 --- a/crates/bindings/bevy_ecs_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_ecs_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] bevy_app = { workspace = true, features = ["std"] } diff --git a/crates/bindings/bevy_gizmos_bms_bindings/Cargo.toml b/crates/bindings/bevy_gizmos_bms_bindings/Cargo.toml index 472cea69b0..32d9d0d9c7 100644 --- a/crates/bindings/bevy_gizmos_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_gizmos_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_gltf_bms_bindings/Cargo.toml b/crates/bindings/bevy_gltf_bms_bindings/Cargo.toml index d8cceeaf1a..960575be8f 100644 --- a/crates/bindings/bevy_gltf_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_gltf_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_image_bms_bindings/Cargo.toml b/crates/bindings/bevy_image_bms_bindings/Cargo.toml index b2338adb40..a2c64c62a2 100644 --- a/crates/bindings/bevy_image_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_image_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] bevy_ecs = { workspace = true, features = ["std", "bevy_reflect"] } diff --git a/crates/bindings/bevy_input_bms_bindings/Cargo.toml b/crates/bindings/bevy_input_bms_bindings/Cargo.toml index 3c68319984..4a81811b4b 100644 --- a/crates/bindings/bevy_input_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_input_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_input_focus_bms_bindings/Cargo.toml b/crates/bindings/bevy_input_focus_bms_bindings/Cargo.toml index 7232029412..97977c5805 100644 --- a/crates/bindings/bevy_input_focus_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_input_focus_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_light_bms_bindings/Cargo.toml b/crates/bindings/bevy_light_bms_bindings/Cargo.toml index 33993b351e..1eeec824c7 100644 --- a/crates/bindings/bevy_light_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_light_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_math_bms_bindings/Cargo.toml b/crates/bindings/bevy_math_bms_bindings/Cargo.toml index cf50d4eb32..a8259a46b7 100644 --- a/crates/bindings/bevy_math_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_math_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] bevy_ecs = { workspace = true, features = ["std", "bevy_reflect"] } bevy_app = { workspace = true, features = ["std"] } diff --git a/crates/bindings/bevy_mesh_bms_bindings/Cargo.toml b/crates/bindings/bevy_mesh_bms_bindings/Cargo.toml index 6e85f04912..e56a5ad391 100644 --- a/crates/bindings/bevy_mesh_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_mesh_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_pbr_bms_bindings/Cargo.toml b/crates/bindings/bevy_pbr_bms_bindings/Cargo.toml index 067353a39f..3dc0272b36 100644 --- a/crates/bindings/bevy_pbr_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_pbr_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_picking_bms_bindings/Cargo.toml b/crates/bindings/bevy_picking_bms_bindings/Cargo.toml index 8318f546d2..78880941e5 100644 --- a/crates/bindings/bevy_picking_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_picking_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_post_process_bms_bindings/Cargo.toml b/crates/bindings/bevy_post_process_bms_bindings/Cargo.toml index cdae26aafb..7e36a25114 100644 --- a/crates/bindings/bevy_post_process_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_post_process_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_reflect_bms_bindings/Cargo.toml b/crates/bindings/bevy_reflect_bms_bindings/Cargo.toml index 8a82793189..06529320c4 100644 --- a/crates/bindings/bevy_reflect_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_reflect_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] bevy_ecs = { workspace = true, features = ["std", "bevy_reflect"] } bevy_app = { workspace = true, features = ["std"] } diff --git a/crates/bindings/bevy_render_bms_bindings/Cargo.toml b/crates/bindings/bevy_render_bms_bindings/Cargo.toml index 672ae1fd23..69abdef6de 100644 --- a/crates/bindings/bevy_render_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_render_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_scene_bms_bindings/Cargo.toml b/crates/bindings/bevy_scene_bms_bindings/Cargo.toml index 190f49f030..4556a64b1d 100644 --- a/crates/bindings/bevy_scene_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_scene_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_sprite_bms_bindings/Cargo.toml b/crates/bindings/bevy_sprite_bms_bindings/Cargo.toml index a9125a3763..ae0bf6e3a8 100644 --- a/crates/bindings/bevy_sprite_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_sprite_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_sprite_render_bms_bindings/Cargo.toml b/crates/bindings/bevy_sprite_render_bms_bindings/Cargo.toml index b86d6ec9af..b00e53fed5 100644 --- a/crates/bindings/bevy_sprite_render_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_sprite_render_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_text_bms_bindings/Cargo.toml b/crates/bindings/bevy_text_bms_bindings/Cargo.toml index f18fef7fc2..ae327b3f4e 100644 --- a/crates/bindings/bevy_text_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_text_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_time_bms_bindings/Cargo.toml b/crates/bindings/bevy_time_bms_bindings/Cargo.toml index 4933283545..1cda7364a7 100644 --- a/crates/bindings/bevy_time_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_time_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_transform_bms_bindings/Cargo.toml b/crates/bindings/bevy_transform_bms_bindings/Cargo.toml index aa24ef4f34..c5697ae442 100644 --- a/crates/bindings/bevy_transform_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_transform_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_ui_bms_bindings/Cargo.toml b/crates/bindings/bevy_ui_bms_bindings/Cargo.toml index fb60ef4b68..d514053088 100644 --- a/crates/bindings/bevy_ui_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_ui_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/bindings/bevy_ui_render_bms_bindings/Cargo.toml b/crates/bindings/bevy_ui_render_bms_bindings/Cargo.toml index ce992ded7f..22bb7ee88b 100644 --- a/crates/bindings/bevy_ui_render_bms_bindings/Cargo.toml +++ b/crates/bindings/bevy_ui_render_bms_bindings/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] diff --git a/crates/languages/bevy_mod_scripting_lua/Cargo.toml b/crates/languages/bevy_mod_scripting_lua/Cargo.toml index 97641ce03e..ec47ea7541 100644 --- a/crates/languages/bevy_mod_scripting_lua/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_lua/Cargo.toml @@ -48,6 +48,7 @@ bevy_mod_scripting_bindings = { workspace = true } bevy_mod_scripting_bindings_domain = { workspace = true } bevy_mod_scripting_asset = { workspace = true } bevy_mod_scripting_script = { workspace = true } +bevy_mod_scripting_world = { workspace = true } mlua = { workspace = true, features = ["vendored", "send", "macros"] } profiling = { workspace = true } diff --git a/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs b/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs index d2cc9737f6..aa55130bf4 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs @@ -1,10 +1,11 @@ use std::any::TypeId; use bevy_mod_scripting_bindings::{ - ReflectReference, ThreadWorldContainer, error::InteropError, script_value::ScriptValue, + ReflectReference, WorldExtensions, error::InteropError, script_value::ScriptValue, }; use bevy_mod_scripting_bindings_domain::ScriptOperatorNames; use bevy_mod_scripting_display::OrFakeId; +use bevy_mod_scripting_world::ThreadWorldContainer; use mlua::{ExternalError, MetaMethod, UserData, UserDataMethods}; use crate::IntoMluaError; diff --git a/crates/languages/bevy_mod_scripting_lua/src/lib.rs b/crates/languages/bevy_mod_scripting_lua/src/lib.rs index 161eeb99c4..e9ce726152 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/lib.rs @@ -15,7 +15,7 @@ use bevy_ecs::world::{Mut, WorldId}; use bevy_log::trace; use bevy_mod_scripting_asset::{Language, ScriptAsset}; use bevy_mod_scripting_bindings::{ - InteropError, PartialReflectExt, ThreadWorldContainer, function::namespace::Namespace, + InteropError, PartialReflectExt, WorldExtensions, function::namespace::Namespace, globals::AppScriptGlobalsRegistry, script_value::ScriptValue, }; use bevy_mod_scripting_core::{ @@ -27,6 +27,7 @@ use bevy_mod_scripting_core::{ script::ContextPolicy, }; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::ThreadWorldContainer; use bindings::{ reference::{LuaReflectReference, LuaStaticReflectReference}, script_value::LuaScriptValue, @@ -96,9 +97,14 @@ fn register_plugin_globals(lua: &mut Lua) -> Result<(), mlua::Error> { lua.create_function(|_lua: &Lua, (callback, func): (String, Function)| { let thread_ctxt = ThreadWorldContainer .try_get_context() - .map_err(mlua::Error::external)?; + .map_err(IntoMluaError::to_lua_error)?; let world = thread_ctxt.world; - let attachment = thread_ctxt.attachment; + let attachment = world.current_attachment().0.ok_or_else(|| { + mlua::Error::external( + "Cannot register callback, missing script attachment context.", + ) + })?; + world .with_resource_mut(|res: Mut>| { let mut callbacks = res.callbacks.write(); @@ -391,11 +397,13 @@ pub trait IntoMluaError { fn to_lua_error(self) -> mlua::Error; } -impl IntoMluaError for InteropError { +impl> IntoMluaError for T { fn to_lua_error(self) -> mlua::Error { - mlua::Error::external(self) + let error: InteropError = self.into(); + mlua::Error::external(error) } } + #[cfg(test)] mod test { use ::bevy_asset::Handle; diff --git a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml index d791c59c95..6bf9824294 100644 --- a/crates/languages/bevy_mod_scripting_rhai/Cargo.toml +++ b/crates/languages/bevy_mod_scripting_rhai/Cargo.toml @@ -27,6 +27,7 @@ bevy_mod_scripting_display = { workspace = true } bevy_mod_scripting_bindings = { workspace = true } bevy_mod_scripting_asset = { workspace = true } bevy_mod_scripting_script = { workspace = true } +bevy_mod_scripting_world = { workspace = true } strum = { workspace = true, features = ["derive"] } parking_lot = { workspace = true } diff --git a/crates/languages/bevy_mod_scripting_rhai/src/bindings/reference.rs b/crates/languages/bevy_mod_scripting_rhai/src/bindings/reference.rs index 7337ef7da1..d187055751 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/bindings/reference.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/bindings/reference.rs @@ -5,10 +5,11 @@ use std::{ use crate::IntoRhaiError; use bevy_mod_scripting_bindings::{ - ReflectReference, ScriptValue, ThreadWorldContainer, error::InteropError, + ReflectReference, ScriptValue, WorldExtensions, error::InteropError, function::script_function::DynamicScriptFunctionMut, }; use bevy_mod_scripting_display::OrFakeId; +use bevy_mod_scripting_world::ThreadWorldContainer; use rhai::{CustomType, Dynamic, EvalAltResult}; use strum::VariantNames; diff --git a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs index 35dfe98fc1..671cdeaf2b 100644 --- a/crates/languages/bevy_mod_scripting_rhai/src/lib.rs +++ b/crates/languages/bevy_mod_scripting_rhai/src/lib.rs @@ -15,7 +15,7 @@ use bevy_log::trace; use bevy_mod_scripting_asset::{Language, ScriptAsset}; use bevy_mod_scripting_bindings::{ AppScriptGlobalsRegistry, InteropError, Namespace, PartialReflectExt, ScriptValue, - ThreadWorldContainer, + WorldExtensions, }; use bevy_mod_scripting_core::{ IntoScriptPluginParams, ScriptingPlugin, @@ -27,6 +27,7 @@ use bevy_mod_scripting_core::{ }; use bevy_mod_scripting_display::DisplayProxy; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::ThreadWorldContainer; use bindings::reference::{ReservedKeyword, RhaiReflectReference, RhaiStaticReflectReference}; use parking_lot::RwLock; pub use rhai; @@ -77,11 +78,12 @@ pub trait IntoRhaiError { fn into_rhai_error(self) -> Box; } -impl IntoRhaiError for InteropError { +impl> IntoRhaiError for T { fn into_rhai_error(self) -> Box { + let error = self.into(); Box::new(rhai::EvalAltResult::ErrorSystem( "ScriptError".to_owned(), - Box::new(self), + Box::new(error), )) } } @@ -134,9 +136,14 @@ fn register_plugin_globals(ctxt: &mut Engine) { let register_callback_fn = |callback: String, func: FnPtr| { let thread_ctxt = ThreadWorldContainer .try_get_context() - .map_err(|e| Box::new(EvalAltResult::ErrorSystem("".to_string(), Box::new(e))))?; + .map_err(IntoRhaiError::into_rhai_error)?; let world = thread_ctxt.world; - let attachment = thread_ctxt.attachment; + let attachment = world.current_attachment().0.ok_or_else(|| { + IntoRhaiError::into_rhai_error(InteropError::str( + "Cannot register callback, missing script attachment context.", + )) + })?; + world .with_resource_mut(|res: Mut>| { let mut callbacks = res.callbacks.write(); @@ -216,7 +223,7 @@ impl Default for RhaiScriptingPlugin { } } - let mut script_function_registry = world.script_function_registry(); + let mut script_function_registry = world.script_function_registry().clone(); let mut script_function_registry = script_function_registry.write(); // iterate all functions, and remap names with reserved keywords diff --git a/crates/testing_crates/script_integration_test_harness/Cargo.toml b/crates/testing_crates/script_integration_test_harness/Cargo.toml index 7b24f6e820..0fdaa7e111 100644 --- a/crates/testing_crates/script_integration_test_harness/Cargo.toml +++ b/crates/testing_crates/script_integration_test_harness/Cargo.toml @@ -39,3 +39,4 @@ bevy_mod_scripting_asset = { workspace = true } bevy_mod_scripting_script = { workspace = true } bevy_mod_scripting_bindings = { workspace = true } bevy_mod_scripting_test_scenario_syntax = { workspace = true } +bevy_mod_scripting_world = { workspace = true } \ No newline at end of file diff --git a/crates/testing_crates/script_integration_test_harness/src/lib.rs b/crates/testing_crates/script_integration_test_harness/src/lib.rs index baf43efe24..f2c7a59cd9 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -21,10 +21,7 @@ use ::{ }; use bevy_asset::Assets; use bevy_mod_scripting_asset::ScriptAsset; -use bevy_mod_scripting_bindings::{ - CoreScriptGlobalsPlugin, ReflectAccessId, ThreadScriptContext, ThreadWorldContainer, - WorldAccessGuard, WorldGuard, -}; +use bevy_mod_scripting_bindings::{CoreScriptGlobalsPlugin, WorldExtensions}; use bevy_mod_scripting_core::{ BMSScriptingInfrastructurePlugin, IntoScriptPluginParams, commands::AttachScript, @@ -35,6 +32,7 @@ use bevy_mod_scripting_core::{ use bevy_mod_scripting_display::DisplayProxy; use bevy_mod_scripting_functions::ScriptFunctionsPlugin; use bevy_mod_scripting_script::ScriptAttachment; +use bevy_mod_scripting_world::{WorldAccessGuard, WorldGuard}; use criterion::{BatchSize, measurement::Measurement}; use rand::{Rng, SeedableRng}; use test_functions::{RNG, register_test_functions}; @@ -315,9 +313,13 @@ where .world_mut() .get_resource_or_init::>() .clone(); - let guard = WorldGuard::new_exclusive(app.world_mut()); let context_key = ScriptAttachment::EntityScript(entity, script_handle.clone()); + let cache = WorldAccessGuard::setup_cache( + app.world(), + bevy_mod_scripting_bindings::CurrentScriptAttachment(Some(context_key.clone())), + ); + let guard = WorldGuard::new_exclusive(app.world_mut(), cache); let script_contexts = script_contexts.read(); let ctxt_arc = script_contexts.get_context(&context_key).unwrap(); @@ -328,13 +330,7 @@ where let runtime = P::readonly_configuration(guard.id()).runtime; let _ = WorldAccessGuard::with_existing_static_guard(guard, |guard| { - // Ensure the world is available via ThreadWorldContainer - ThreadWorldContainer - .set_context(ThreadScriptContext { - world: guard.clone(), - attachment: ScriptAttachment::StaticScript(script_handle), - }) - .map_err(|e| format!("{e:#?}"))?; + guard.set_current_attachment(context_key.clone()); // Pass the locked context to the closure for benchmarking its Lua (or generic) part bench_fn(&mut ctxt_locked, runtime, label, criterion) }); @@ -413,8 +409,8 @@ pub fn perform_benchmark_with_generator< let f3 = world.register_resource::(); let f4 = world.register_resource::(); let f5 = world.register_resource::(); - - let world_guard = WorldAccessGuard::new_exclusive(&mut world); + let cache = WorldAccessGuard::setup_cache(&world, Default::default()); + let world_guard = WorldAccessGuard::new_exclusive(&mut world, cache); let mut rng_guard = RNG.lock().unwrap(); *rng_guard = rand_chacha::ChaCha12Rng::from_seed([42u8; 32]); drop(rng_guard); @@ -433,11 +429,11 @@ pub fn perform_benchmark_with_generator< for _ in 0..rng_guard.random_range(0..=5) { // pick random component match rng_guard.random_range(0..=4) { - 0 => world_guard.claim_write_access(ReflectAccessId::for_component_id(f1)), - 1 => world_guard.claim_write_access(ReflectAccessId::for_component_id(f2)), - 2 => world_guard.claim_write_access(ReflectAccessId::for_component_id(f3)), - 3 => world_guard.claim_write_access(ReflectAccessId::for_component_id(f4)), - 4 => world_guard.claim_write_access(ReflectAccessId::for_component_id(f5)), + 0 => world_guard.claim_write_access(f1).is_ok(), + 1 => world_guard.claim_write_access(f2).is_ok(), + 2 => world_guard.claim_write_access(f3).is_ok(), + 3 => world_guard.claim_write_access(f4).is_ok(), + 4 => world_guard.claim_write_access(f5).is_ok(), _ => false, }; } diff --git a/crates/testing_crates/script_integration_test_harness/src/test_functions.rs b/crates/testing_crates/script_integration_test_harness/src/test_functions.rs index 372e839a3e..85b5c4e6cf 100644 --- a/crates/testing_crates/script_integration_test_harness/src/test_functions.rs +++ b/crates/testing_crates/script_integration_test_harness/src/test_functions.rs @@ -12,7 +12,7 @@ use ::{ use bevy_mod_scripting_asset::Language; use bevy_mod_scripting_bindings::{ DynamicScriptFunction, ReflectReference, ScriptComponentRegistration, - ScriptResourceRegistration, ScriptTypeRegistration, ScriptValue, + ScriptResourceRegistration, ScriptTypeRegistration, ScriptValue, WorldExtensions, error::InteropError, function::{ namespace::{GlobalNamespace, NamespaceBuilder}, diff --git a/crates/testing_crates/test_utils/src/test_data.rs b/crates/testing_crates/test_utils/src/test_data.rs index f3e7360c40..e4f5d5adb3 100644 --- a/crates/testing_crates/test_utils/src/test_data.rs +++ b/crates/testing_crates/test_utils/src/test_data.rs @@ -72,7 +72,7 @@ impl GenericComponent { } /// Test Resource with Reflect and ReflectResource registered -#[derive(Resource, Reflect, Default, PartialEq, Eq, Debug)] +#[derive(Resource, Reflect, Default, PartialEq, Eq, Debug, Clone)] #[reflect(Resource)] pub struct TestResource { pub bytes: Vec, diff --git a/docs/src/ReleaseNotes/0.19-to-0.20.md b/docs/src/ReleaseNotes/0.19-to-0.20.md index 3310d8617a..90dea3f3c7 100644 --- a/docs/src/ReleaseNotes/0.19-to-0.20.md +++ b/docs/src/ReleaseNotes/0.19-to-0.20.md @@ -41,4 +41,13 @@ You should use: `V`, `R` and `M` instead ## `ScriptValue::List` changes -This variant now uses `VecDeque` instead of `Vec` \ No newline at end of file +This variant now uses `VecDeque` instead of `Vec` + + +# Refactor of `WorldAccessGuard` and related types + +Many functions previously living directly in `WorldAccessGuard` impls, now need to be accessed via the `WorldExtensions` trait. + +Functions previously known as `with_exclusive_access` etc, have been split into: `with_world` and `with_world_mut`, allowing read access to the world too in some circumstances. + +`ReflectAccessId` was removed in favour of `WorldAccessRange` which can be created via `Into` from `ComponentId`'s. \ No newline at end of file diff --git a/docs/src/ReleaseNotes/0.20.0.md b/docs/src/ReleaseNotes/0.20.0.md index 4f51891827..34432bcf43 100644 --- a/docs/src/ReleaseNotes/0.20.0.md +++ b/docs/src/ReleaseNotes/0.20.0.md @@ -44,4 +44,24 @@ Two bindings have been added: `pack_args` and `unpack_args` to allow script code # New `VecDeque` impls -`VecDeque` now implements various BMS traits and can be used in bindings as a normal argument. \ No newline at end of file +`VecDeque` now implements various BMS traits and can be used in bindings as a normal argument. + +# Improved domain boundaries - Extracted `bevy_mod_scripting_world` crate + +The following types were extracted into their own crate: +- `ReflectAccessId` now known as `WorldAccessRange` +- `ClaimOwner` +- `AccessInstance` +- `AccessMap` +- `DynamicSystemMeta` +- `WorldGuard` & `WorldAccessGuard` + +the `CachedRegistry` trait was introduced to store registries such as: +- `AppScriptComponentRegistry` +- `AppReflectAllocator` +- `AppScheduleRegistry` +- `AppScriptFunctionRegistry` + +etc. in a type-erased way. + +This allowed `bevy_mod_scripting_display` to directly rely on world types, and removed a lot of awkward bridging code. \ No newline at end of file diff --git a/xtask/src/command.rs b/xtask/src/command.rs index 6f2259a9ea..1fb9b9fdeb 100644 --- a/xtask/src/command.rs +++ b/xtask/src/command.rs @@ -8,14 +8,13 @@ use std::{ use crate::GlobalArgs; -pub fn run_system_command>>( +pub fn make_system_command>>( app_settings: &GlobalArgs, command: &str, - context: &str, add_args: I, dir: Option<&Path>, capture_streams_in_output: bool, -) -> Result { +) -> Result { info!("Running system command: {command}"); let working_dir = match dir { @@ -32,6 +31,24 @@ pub fn run_system_command>>( } info!("Using command: {cmd:?}"); + Ok(cmd) +} + +pub fn run_system_command>>( + app_settings: &GlobalArgs, + command: &str, + context: &str, + add_args: I, + dir: Option<&Path>, + capture_streams_in_output: bool, +) -> Result { + let mut cmd = make_system_command( + app_settings, + command, + add_args, + dir, + capture_streams_in_output, + )?; let output = cmd.output(); if capture_streams_in_output { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 5c4502a803..f5db8d96e4 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,7 +3,7 @@ use std::{ ffi::OsString, io::{Write, stdout}, path::{Path, PathBuf}, - process::{Command, Output}, + process::{Child, Command, Output}, str::FromStr, }; @@ -16,8 +16,8 @@ use serde::{Deserialize, Serialize}; use strum::{IntoEnumIterator, VariantNames}; use xtask::{ BindingCrate, Feature, FeatureGroup, Features, GlobalArgs, Meta, codegen_crate_dir, - main_workspace_cargo_metadata, prepare_codegen, read_rust_toolchain, relative_workspace_dir, - run_system_command, run_workspace_command, workspace_dir, + main_workspace_cargo_metadata, make_system_command, prepare_codegen, read_rust_toolchain, + relative_workspace_dir, run_system_command, run_workspace_command, workspace_dir, }; /// Enumerates the binaries available in the project and their paths @@ -186,6 +186,7 @@ impl App { Xtasks::Bench { name, enable_profiling: profile, + tracy_capture_name, } => { cmd.arg("bench"); @@ -196,6 +197,10 @@ impl App { if profile { cmd.arg("--profile"); } + + if let Some(capture_tracy) = tracy_capture_name { + cmd.arg("--capture-tracy").arg(capture_tracy); + } } } @@ -432,6 +437,11 @@ enum Xtasks { /// Whether or not to enable tracy profiling #[clap(long, default_value = "false", help = "Enable tracy profiling")] enable_profiling: bool, + #[clap( + long, + help = "The name to give to the captured trace, if not passed won't capture trace" + )] + tracy_capture_name: Option, /// The name argument passed to `cargo bench`, can be used in combination with profile to selectively profile benchmarks #[clap(long, help = "The name argument passed to `cargo bench`")] name: Option, @@ -524,8 +534,15 @@ impl Xtasks { Xtasks::Bench { name, enable_profiling, + tracy_capture_name, } => { - let _ = Self::bench(app_settings, enable_profiling, name, false)?; + let _ = Self::bench( + app_settings, + enable_profiling, + name, + tracy_capture_name, + false, + )?; Ok(()) } }?; @@ -919,16 +936,33 @@ impl Xtasks { app_settings: GlobalArgs, profile: bool, name: Option, + tracy_capture_name: Option, capture_streams_in_output: bool, ) -> Result { log::info!("Profiling enabled: {profile}"); - let mut features = Features::default(); + struct DroppingChild(Child); + impl Drop for DroppingChild { + fn drop(&mut self) { + let _ = self.0.kill(); + } + } + let mut features = Features::default(); + let mut tracy_child_capture: Option = None; if profile { unsafe { std::env::set_var("ENABLE_PROFILING", "1") }; - // features.push(Feature::BevyTracy); features.0.insert(Feature::ProfileWithTracy); + if let Some(trace_name) = tracy_capture_name { + let mut cmd = make_system_command( + &app_settings, + "tracy-capture", + ["-o", &trace_name, "-a", "localhost"], + None, + capture_streams_in_output, + )?; + tracy_child_capture = Some(DroppingChild(cmd.spawn()?)); + } } else { unsafe { std::env::set_var("RUST_LOG", "bevy_mod_scripting=error") }; } @@ -967,7 +1001,7 @@ impl Xtasks { } else { output.stdout.write(additional_benchmark_lines.as_bytes())?; } - + drop(tracy_child_capture); Ok(output) } @@ -1010,7 +1044,7 @@ impl Xtasks { // first of all run bench, and save output to a file - let result = Self::bench(app_settings, false, None, true)?; + let result = Self::bench(app_settings, false, None, None, true)?; let bench_file_path = PathBuf::from("./bencher_output.txt"); let mut file = std::fs::File::create(&bench_file_path)?; file.write_all(&result.stdout)?; diff --git a/xtask/templates/bindings_crate.toml.tera b/xtask/templates/bindings_crate.toml.tera index d0445a9040..1ad433408c 100644 --- a/xtask/templates/bindings_crate.toml.tera +++ b/xtask/templates/bindings_crate.toml.tera @@ -11,6 +11,9 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[lib] +test = false + [dependencies] {% if include_bevy_ecs_dep -%} bevy_ecs = { workspace = true, features = ["std", "bevy_reflect"] }