An experimental, status effects-as-entities system for Bevy.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at dev 274 lines 9.2 kB view raw
1use crate::EffectMode; 2use bevy_ecs::component::{ComponentCloneBehavior, ComponentId}; 3use bevy_ecs::prelude::{ 4 AppTypeRegistry, Bundle, Entity, EntityRef, Name, ReflectComponent, Resource, World, 5}; 6use bevy_ecs::ptr::OwningPtr; 7use bevy_ecs::relationship::RelationshipHookMode; 8use bevy_utils::prelude::DebugName; 9use std::alloc::alloc; 10use std::any::TypeId; 11use std::error::Error; 12use std::fmt::Formatter; 13use std::ptr::{NonNull, copy_nonoverlapping}; 14 15#[derive(Resource)] 16pub(crate) struct BundleInspector { 17 world: World, 18 scratch_entity: Entity, 19} 20 21impl Default for BundleInspector { 22 fn default() -> Self { 23 let mut world = World::new(); 24 let scratch_entity = world.spawn_empty().id(); 25 Self { 26 world, 27 scratch_entity, 28 } 29 } 30} 31 32impl BundleInspector { 33 /// Stashes a bundle so it can be inspected. 34 /// 35 /// Calls [`clear`](Self::clear) first to avoid leakage. 36 pub fn stash_bundle<B: Bundle>(&mut self, bundle: B) -> &mut Self { 37 self.clear(); 38 39 self.world 40 .entity_mut(self.scratch_entity) 41 .insert_with_relationship_hook_mode(bundle, RelationshipHookMode::Skip); 42 43 self 44 } 45 46 /// Clears the [stashed bundle](Self::stash_bundle). 47 pub fn clear(&mut self) -> &mut Self { 48 self.world.entity_mut(self.scratch_entity).clear(); 49 50 self 51 } 52 53 /// Returns the [stashed bundle's](Self::stash_bundle) name and effect mode. 54 pub fn get_effect_meta(&self) -> (Option<Name>, EffectMode) { 55 let name = self 56 .world 57 .entity(self.scratch_entity) 58 .get::<Name>() 59 .cloned(); 60 61 let mode = self 62 .world 63 .entity(self.scratch_entity) 64 .get::<EffectMode>() 65 .copied() 66 .unwrap_or_default(); 67 68 (name, mode) 69 } 70 71 /// Returns a reference to the [stashed bundle](Self::stash_bundle). 72 pub fn get_ref(&'_ self) -> EntityRef<'_> { 73 self.world.entity(self.scratch_entity) 74 } 75 76 /// Converts a component ID to a type ID, if registered. 77 /// The component ID must be from the inspector's world, using [`get_type_id`](Self::get_type_id). 78 pub fn get_type_id(&self, component_id: ComponentId) -> Option<TypeId> { 79 self.world 80 .components() 81 .get_info(component_id) 82 .and_then(|info| info.type_id()) 83 } 84 85 /// Copies a component from the [stashed bundle](Self::stash_bundle) into an entity in a different world. 86 /// The component ID must be from the inspector's world, using [`get_type_id`](Self::get_type_id). 87 /// 88 /// # Errors 89 /// Will return an error if: 90 /// - The component is not registered in `dst_world`. 91 /// - The component cannot be cloned ([`ComponentCloneBehavior::Ignore`]). 92 /// - The stashed bundle doesn't contain the component. 93 /// - The destination entity doesn't exist in `dst_world`. 94 /// - The resource AppTypeRegistry doesn't exist in `dst_world`. 95 /// 96 /// # Safety 97 /// `src_component_id` must be for the same component as `type_id`. 98 pub unsafe fn copy_to_world( 99 &self, 100 dst_world: &mut World, 101 dst_entity: Entity, 102 type_id: TypeId, 103 src_component_id: ComponentId, 104 ) -> Result<&Self, MultiWorldCopyError> { 105 let Some(dst_component_id) = dst_world.components().get_id(type_id) else { 106 return Err(MultiWorldCopyError::Unregistered(type_id)); 107 }; 108 let component_info = dst_world.components().get_info(dst_component_id).unwrap(); 109 110 match component_info.clone_behavior() { 111 ComponentCloneBehavior::Default | ComponentCloneBehavior::Custom(_) => {} 112 ComponentCloneBehavior::Ignore => { 113 return Err(MultiWorldCopyError::Uncloneable(component_info.name())); 114 } 115 } 116 117 let Some(src) = self.world.get_by_id(self.scratch_entity, src_component_id) else { 118 return Err(MultiWorldCopyError::MissingSrcComponent( 119 component_info.name(), 120 self.scratch_entity, 121 )); 122 }; 123 124 if component_info.drop().is_none() { 125 unsafe { 126 // SAFETY: Contract is required to be upheld by the world. 127 let dst = alloc(component_info.layout()); 128 129 // SAFETY: `dst` is allocated from the component's layout. 130 // Both IDs provided by the caller must match, and `src` and `dst` are obtained using those IDs. 131 // `src` and `dst` are from different worlds, so cannot overlap. 132 copy_nonoverlapping(src.as_ptr(), dst, component_info.layout().size()); 133 134 // SAFETY: Both IDs provided by the caller must match, and `dst` was created from `src`. 135 let owning = OwningPtr::new(NonNull::new(dst).unwrap()); 136 137 // SAFETY: `existing_component_id` is extracted from `dst_world`. 138 // Both IDs provided by the caller must match, and `owning` was obtained using `src_component_id`. 139 dst_world 140 .get_entity_mut(dst_entity) 141 .map_err(|_| MultiWorldCopyError::MissingDstEntity(dst_entity))? 142 .insert_by_id(dst_component_id, owning); 143 } 144 } else { 145 let Some(registry) = dst_world.get_resource::<AppTypeRegistry>().cloned() else { 146 return Err(MultiWorldCopyError::MissingTypeRegistry); 147 }; 148 let registry = registry.read(); 149 150 let reflect_component = registry 151 .get_type_data::<ReflectComponent>(type_id) 152 .ok_or(MultiWorldCopyError::Uncloneable(component_info.name()))?; 153 154 reflect_component.copy( 155 &self.world, 156 dst_world, 157 self.scratch_entity, 158 dst_entity, 159 &registry, 160 ); 161 } 162 163 Ok(self) 164 } 165} 166 167#[derive(Debug, Eq, PartialEq, Clone)] 168pub enum MultiWorldCopyError { 169 Unregistered(TypeId), 170 Uncloneable(DebugName), 171 MissingDstEntity(Entity), 172 MissingSrcComponent(DebugName, Entity), 173 MissingTypeRegistry, 174} 175 176impl std::fmt::Display for MultiWorldCopyError { 177 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 178 match self { 179 MultiWorldCopyError::Unregistered(type_id) => write!( 180 f, 181 "Component with {type_id:?} is not registered in the destination world, and therefor cannot be inserted using merge mode.", 182 ), 183 MultiWorldCopyError::Uncloneable(name) => write!( 184 f, 185 "Component {name} cannot be cloned, and therefor cannot be inserted using merge mode.", 186 ), 187 MultiWorldCopyError::MissingDstEntity(entity) => write!( 188 f, 189 "Entity {entity} does not exist in the destination world." 190 ), 191 MultiWorldCopyError::MissingSrcComponent(name, entity) => write!( 192 f, 193 "Component {name} does not exist on the scratch entity {entity}, and therefor cannot be cloned.", 194 ), 195 MultiWorldCopyError::MissingTypeRegistry => write!( 196 f, 197 "Resource AppTypeRegistry does not exist in the destination world, and therefor no components can be cloned.", 198 ), 199 } 200 } 201} 202 203impl Error for MultiWorldCopyError {} 204 205#[cfg(test)] 206mod tests { 207 use super::*; 208 use crate::Effecting; 209 210 #[test] 211 fn get_effect_meta() { 212 let mut inspector = BundleInspector::default(); 213 214 let name = Name::new("Effect"); 215 let mode = EffectMode::Insert; 216 217 assert_eq!( 218 inspector 219 .stash_bundle((name.clone(), mode)) 220 .get_effect_meta(), 221 (Some(name), mode) 222 ); 223 } 224 225 #[test] 226 fn get_effect_meta_no_name() { 227 let mut inspector = BundleInspector::default(); 228 229 let mode = EffectMode::Insert; 230 231 assert_eq!(inspector.stash_bundle(mode).get_effect_meta(), (None, mode)); 232 } 233 234 #[test] 235 fn get_effect_meta_no_mode() { 236 let mut inspector = BundleInspector::default(); 237 238 let name = Name::new("Effect"); 239 240 assert_eq!( 241 inspector.stash_bundle(name.clone()).get_effect_meta(), 242 (Some(name), EffectMode::default()) 243 ); 244 } 245 246 #[test] 247 fn get_effect_meta_nothing() { 248 let mut inspector = BundleInspector::default(); 249 250 assert_eq!( 251 inspector.stash_bundle(()).get_effect_meta(), 252 (None, EffectMode::default()) 253 ); 254 } 255 256 #[test] 257 fn get_effect_mode_with_relation() { 258 let mut inspector = BundleInspector::default(); 259 260 let name = Name::new("Effect"); 261 let mode = EffectMode::Insert; 262 263 assert_eq!( 264 inspector 265 .stash_bundle(( 266 name.clone(), 267 mode, 268 Effecting(Entity::from_raw_u32(32).unwrap()) 269 )) 270 .get_effect_meta(), 271 (Some(name), mode) 272 ); 273 } 274}