An experimental, status effects-as-entities system for Bevy.
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 ®istry,
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}