Skip to main content

nightshade_api/
reflect.rs

1//! Component reflection: identify, read, write, add, remove, and snapshot any
2//! of the engine's optional components by [`ComponentKind`].
3//!
4//! An authoring tool is the mirror image of the spawn-and-forget API: it must
5//! read back whatever it can write, list what an entity carries, and capture a
6//! value before an edit so the edit can be undone. This module is that mirror.
7//! [`ComponentPatch`] is a whole-component value the tool sets;
8//! [`ComponentSnapshot`] is a captured before/after value for an undo stack. The
9//! patch and snapshot carry the engine's own component types directly, so there
10//! is no parallel mirror format to keep in sync.
11
12use nightshade::ecs::animation::components::AnimationPlayer;
13use nightshade::ecs::audio::components::AudioSource;
14use nightshade::ecs::camera::components::Camera;
15use nightshade::ecs::decal::components::Decal;
16use nightshade::ecs::material::components::Material;
17use nightshade::ecs::morph::components::MorphWeights;
18use nightshade::ecs::particles::components::ParticleEmitter;
19use nightshade::ecs::prefab::components::PrefabSource;
20use nightshade::ecs::primitives::{CameraCullingMask, CullingMask, RenderLayer};
21use nightshade::ecs::script::Script;
22use nightshade::ecs::text::components::{Text, TextProperties};
23use nightshade::ecs::transform::components::IgnoreParentScale;
24use nightshade::prelude::*;
25use serde::{Deserialize, Serialize};
26
27/// One optional component an entity may carry, the unit the inspector adds,
28/// removes, and tests by.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, enum2schema::Schema)]
30#[schema(string_enum)]
31pub enum ComponentKind {
32    ParticleEmitter,
33    Decal,
34    AudioSource,
35    RigidBody,
36    Collider,
37    CharacterController,
38    NavmeshAgent,
39    Camera,
40    Text,
41    AnimationPlayer,
42    Visibility,
43    CastsShadow,
44    Light,
45    RenderLayer,
46    CullingMask,
47    CameraCullingMask,
48    IgnoreParentScale,
49    AudioListener,
50    CollisionListener,
51    PhysicsInterpolation,
52    MorphWeights,
53    MaterialVariants,
54    PanOrbitCamera,
55    ThirdPersonCamera,
56    CameraEnvironment,
57    CameraPostProcess,
58    ConstrainedAspect,
59    ViewportUpdateMode,
60    Script,
61}
62
63/// Every [`ComponentKind`], for enumerating what an entity has and could gain.
64pub const ALL_COMPONENT_KINDS: [ComponentKind; 29] = [
65    ComponentKind::ParticleEmitter,
66    ComponentKind::Decal,
67    ComponentKind::AudioSource,
68    ComponentKind::RigidBody,
69    ComponentKind::Collider,
70    ComponentKind::CharacterController,
71    ComponentKind::NavmeshAgent,
72    ComponentKind::Camera,
73    ComponentKind::Text,
74    ComponentKind::AnimationPlayer,
75    ComponentKind::Visibility,
76    ComponentKind::CastsShadow,
77    ComponentKind::Light,
78    ComponentKind::RenderLayer,
79    ComponentKind::CullingMask,
80    ComponentKind::CameraCullingMask,
81    ComponentKind::IgnoreParentScale,
82    ComponentKind::AudioListener,
83    ComponentKind::CollisionListener,
84    ComponentKind::PhysicsInterpolation,
85    ComponentKind::MorphWeights,
86    ComponentKind::MaterialVariants,
87    ComponentKind::PanOrbitCamera,
88    ComponentKind::ThirdPersonCamera,
89    ComponentKind::CameraEnvironment,
90    ComponentKind::CameraPostProcess,
91    ComponentKind::ConstrainedAspect,
92    ComponentKind::ViewportUpdateMode,
93    ComponentKind::Script,
94];
95
96/// The ECS bitflag for a kind. The single source the presence test and the
97/// removal share, so they cannot disagree on which flag a kind owns.
98pub fn kind_mask(kind: ComponentKind) -> u64 {
99    use nightshade::ecs::world as masks;
100    match kind {
101        ComponentKind::ParticleEmitter => masks::PARTICLE_EMITTER,
102        ComponentKind::Decal => masks::DECAL,
103        ComponentKind::AudioSource => masks::AUDIO_SOURCE,
104        ComponentKind::RigidBody => masks::RIGID_BODY,
105        ComponentKind::Collider => masks::COLLIDER,
106        ComponentKind::CharacterController => masks::CHARACTER_CONTROLLER,
107        ComponentKind::NavmeshAgent => masks::NAVMESH_AGENT,
108        ComponentKind::Camera => masks::CAMERA,
109        ComponentKind::Text => masks::TEXT,
110        ComponentKind::AnimationPlayer => masks::ANIMATION_PLAYER,
111        ComponentKind::Visibility => masks::VISIBILITY,
112        ComponentKind::CastsShadow => masks::CASTS_SHADOW,
113        ComponentKind::Light => masks::LIGHT,
114        ComponentKind::RenderLayer => masks::RENDER_LAYER,
115        ComponentKind::CullingMask => masks::CULLING_MASK,
116        ComponentKind::CameraCullingMask => masks::CAMERA_CULLING_MASK,
117        ComponentKind::IgnoreParentScale => masks::IGNORE_PARENT_SCALE,
118        ComponentKind::AudioListener => masks::AUDIO_LISTENER,
119        ComponentKind::CollisionListener => masks::COLLISION_LISTENER,
120        ComponentKind::PhysicsInterpolation => masks::PHYSICS_INTERPOLATION,
121        ComponentKind::MorphWeights => masks::MORPH_WEIGHTS,
122        ComponentKind::MaterialVariants => masks::MATERIAL_VARIANTS,
123        ComponentKind::PanOrbitCamera => masks::PAN_ORBIT_CAMERA,
124        ComponentKind::ThirdPersonCamera => masks::THIRD_PERSON_CAMERA,
125        ComponentKind::CameraEnvironment => masks::CAMERA_ENVIRONMENT,
126        ComponentKind::CameraPostProcess => masks::CAMERA_POST_PROCESS,
127        ComponentKind::ConstrainedAspect => masks::CONSTRAINED_ASPECT,
128        ComponentKind::ViewportUpdateMode => masks::VIEWPORT_UPDATE_MODE,
129        ComponentKind::Script => masks::SCRIPT,
130    }
131}
132
133/// Whether the entity carries the component.
134pub fn has_component(world: &World, entity: Entity, kind: ComponentKind) -> bool {
135    world.core.entity_has_components(entity, kind_mask(kind))
136}
137
138/// Drops the component off the entity. A no-op if it is not present.
139pub fn remove_component(world: &mut World, entity: Entity, kind: ComponentKind) {
140    world.core.remove_components(entity, kind_mask(kind));
141}
142
143/// The kinds the entity currently carries.
144pub fn present_components(world: &World, entity: Entity) -> Vec<ComponentKind> {
145    ALL_COMPONENT_KINDS
146        .into_iter()
147        .filter(|kind| has_component(world, entity, *kind))
148        .collect()
149}
150
151/// The kinds the entity does not carry yet, the inspector's add menu.
152pub fn addable_components(world: &World, entity: Entity) -> Vec<ComponentKind> {
153    ALL_COMPONENT_KINDS
154        .into_iter()
155        .filter(|kind| !has_component(world, entity, *kind))
156        .collect()
157}
158
159/// Attaches the kind to the entity with its default value. A no-op for a kind
160/// the entity already carries would still overwrite it, so guard with
161/// [`has_component`] when that matters.
162pub fn add_component_default(world: &mut World, entity: Entity, kind: ComponentKind) {
163    use nightshade::ecs::world as masks;
164    match kind {
165        ComponentKind::ParticleEmitter => {
166            world.core.add_components(entity, masks::PARTICLE_EMITTER);
167            world
168                .core
169                .set_particle_emitter(entity, ParticleEmitter::default());
170        }
171        ComponentKind::Decal => {
172            world.core.add_components(entity, masks::DECAL);
173            world.core.set_decal(entity, Decal::default());
174        }
175        ComponentKind::AudioSource => {
176            world.core.add_components(entity, masks::AUDIO_SOURCE);
177            world.core.set_audio_source(entity, AudioSource::default());
178        }
179        ComponentKind::RigidBody => {
180            world.core.add_components(entity, masks::RIGID_BODY);
181            world
182                .core
183                .set_rigid_body(entity, RigidBodyComponent::new_dynamic());
184        }
185        ComponentKind::Collider => {
186            world.core.add_components(entity, masks::COLLIDER);
187            world
188                .core
189                .set_collider(entity, ColliderComponent::new_cuboid(0.5, 0.5, 0.5));
190        }
191        ComponentKind::CharacterController => {
192            world
193                .core
194                .add_components(entity, masks::CHARACTER_CONTROLLER);
195            world.core.set_character_controller(
196                entity,
197                CharacterControllerComponent::new_capsule(0.9, 0.3),
198            );
199        }
200        ComponentKind::NavmeshAgent => {
201            world.core.add_components(entity, masks::NAVMESH_AGENT);
202            world.core.set_navmesh_agent(entity, NavMeshAgent::new());
203        }
204        ComponentKind::Camera => {
205            world.core.add_components(entity, masks::CAMERA);
206            world.core.set_camera(entity, Camera::default());
207        }
208        ComponentKind::Text => {
209            let text_index = world.resources.text.cache.add_text("Text");
210            world.core.add_components(entity, masks::TEXT);
211            world.core.set_text(entity, Text::new(text_index));
212        }
213        ComponentKind::AnimationPlayer => {
214            world.core.add_components(entity, masks::ANIMATION_PLAYER);
215            world
216                .core
217                .set_animation_player(entity, AnimationPlayer::default());
218        }
219        ComponentKind::Visibility => {
220            world.core.add_components(entity, masks::VISIBILITY);
221            world
222                .core
223                .set_visibility(entity, Visibility { visible: true });
224        }
225        ComponentKind::CastsShadow => {
226            world.core.add_components(entity, masks::CASTS_SHADOW);
227            world.core.set_casts_shadow(entity, CastsShadow);
228        }
229        ComponentKind::Light => {
230            world.core.add_components(entity, masks::LIGHT);
231            world.core.set_light(entity, Light::default());
232        }
233        ComponentKind::RenderLayer => {
234            world.core.add_components(entity, masks::RENDER_LAYER);
235            world.core.set_render_layer(entity, RenderLayer::default());
236        }
237        ComponentKind::CullingMask => {
238            world.core.add_components(entity, masks::CULLING_MASK);
239            world.core.set_culling_mask(entity, CullingMask::default());
240        }
241        ComponentKind::CameraCullingMask => {
242            world
243                .core
244                .add_components(entity, masks::CAMERA_CULLING_MASK);
245            world
246                .core
247                .set_camera_culling_mask(entity, CameraCullingMask::default());
248        }
249        ComponentKind::IgnoreParentScale => {
250            world
251                .core
252                .add_components(entity, masks::IGNORE_PARENT_SCALE);
253            world
254                .core
255                .set_ignore_parent_scale(entity, IgnoreParentScale);
256        }
257        ComponentKind::AudioListener => {
258            world.core.add_components(entity, masks::AUDIO_LISTENER);
259            world
260                .core
261                .set_audio_listener(entity, nightshade::ecs::audio::components::AudioListener);
262        }
263        ComponentKind::CollisionListener => {
264            world.core.add_components(entity, masks::COLLISION_LISTENER);
265            world.core.set_collision_listener(
266                entity,
267                nightshade::ecs::physics::components::CollisionListener,
268            );
269        }
270        ComponentKind::PhysicsInterpolation => {
271            world
272                .core
273                .add_components(entity, masks::PHYSICS_INTERPOLATION);
274            world.core.set_physics_interpolation(
275                entity,
276                nightshade::ecs::physics::components::PhysicsInterpolation::default(),
277            );
278        }
279        ComponentKind::MorphWeights => {
280            world.core.add_components(entity, masks::MORPH_WEIGHTS);
281            world
282                .core
283                .set_morph_weights(entity, MorphWeights::default());
284        }
285        ComponentKind::MaterialVariants => {
286            world.core.add_components(entity, masks::MATERIAL_VARIANTS);
287            world.core.set_material_variants(
288                entity,
289                nightshade::ecs::material::components::MaterialVariants::default(),
290            );
291        }
292        ComponentKind::PanOrbitCamera => {
293            world.core.add_components(entity, masks::PAN_ORBIT_CAMERA);
294            world.core.set_pan_orbit_camera(
295                entity,
296                nightshade::ecs::camera::components::PanOrbitCamera::default(),
297            );
298        }
299        ComponentKind::ThirdPersonCamera => {
300            world
301                .core
302                .add_components(entity, masks::THIRD_PERSON_CAMERA);
303            world.core.set_third_person_camera(
304                entity,
305                nightshade::ecs::camera::components::ThirdPersonCamera::default(),
306            );
307        }
308        ComponentKind::CameraEnvironment => {
309            world.core.add_components(entity, masks::CAMERA_ENVIRONMENT);
310            world.core.set_camera_environment(
311                entity,
312                nightshade::ecs::camera::components::CameraEnvironment::default(),
313            );
314        }
315        ComponentKind::CameraPostProcess => {
316            world
317                .core
318                .add_components(entity, masks::CAMERA_POST_PROCESS);
319            world.core.set_camera_post_process(
320                entity,
321                nightshade::ecs::camera::components::CameraPostProcess::default(),
322            );
323        }
324        ComponentKind::ConstrainedAspect => {
325            world.core.add_components(entity, masks::CONSTRAINED_ASPECT);
326            world.core.set_constrained_aspect(
327                entity,
328                nightshade::ecs::camera::components::ConstrainedAspect::default(),
329            );
330        }
331        ComponentKind::ViewportUpdateMode => {
332            world
333                .core
334                .add_components(entity, masks::VIEWPORT_UPDATE_MODE);
335            world.core.set_viewport_update_mode(
336                entity,
337                nightshade::ecs::camera::components::ViewportUpdateMode::default(),
338            );
339        }
340        ComponentKind::Script => {
341            world.core.add_components(entity, masks::SCRIPT);
342            world.core.set_script(entity, Script::default());
343        }
344    }
345}
346
347/// A whole-component value applied to an entity. These are the engine's own
348/// component types; applying one writes it with the same setter the inspector
349/// would use by hand.
350#[derive(Clone, Serialize, Deserialize, enum2schema::Schema)]
351pub enum ComponentPatch {
352    Light(Light),
353    Visibility(bool),
354    CastsShadow(bool),
355    Camera(Camera),
356    RigidBody(RigidBodyComponent),
357    Collider(ColliderComponent),
358    CharacterController(CharacterControllerComponent),
359    NavmeshAgent(NavMeshAgent),
360    ParticleEmitter(Box<ParticleEmitter>),
361    Decal(Decal),
362    AudioSource(AudioSource),
363    RenderLayer(RenderLayer),
364    CullingMask(CullingMask),
365    CameraCullingMask(CameraCullingMask),
366    IgnoreParentScale(bool),
367    MorphWeights(Vec<f32>),
368    Text {
369        content: String,
370        properties: TextProperties,
371    },
372    Script(String),
373}
374
375impl ComponentPatch {
376    /// The kind this patch writes.
377    pub fn kind(&self) -> ComponentKind {
378        match self {
379            Self::Light(_) => ComponentKind::Light,
380            Self::Visibility(_) => ComponentKind::Visibility,
381            Self::CastsShadow(_) => ComponentKind::CastsShadow,
382            Self::Camera(_) => ComponentKind::Camera,
383            Self::RigidBody(_) => ComponentKind::RigidBody,
384            Self::Collider(_) => ComponentKind::Collider,
385            Self::CharacterController(_) => ComponentKind::CharacterController,
386            Self::NavmeshAgent(_) => ComponentKind::NavmeshAgent,
387            Self::ParticleEmitter(_) => ComponentKind::ParticleEmitter,
388            Self::Decal(_) => ComponentKind::Decal,
389            Self::AudioSource(_) => ComponentKind::AudioSource,
390            Self::RenderLayer(_) => ComponentKind::RenderLayer,
391            Self::CullingMask(_) => ComponentKind::CullingMask,
392            Self::CameraCullingMask(_) => ComponentKind::CameraCullingMask,
393            Self::IgnoreParentScale(_) => ComponentKind::IgnoreParentScale,
394            Self::MorphWeights(_) => ComponentKind::MorphWeights,
395            Self::Text { .. } => ComponentKind::Text,
396            Self::Script(_) => ComponentKind::Script,
397        }
398    }
399}
400
401/// Writes a [`ComponentPatch`] onto the entity, adding the component first for
402/// the variants whose presence is the value (visibility, casts-shadow,
403/// ignore-parent-scale, script).
404pub fn apply_patch(world: &mut World, entity: Entity, patch: ComponentPatch) {
405    use nightshade::ecs::world as masks;
406    match patch {
407        ComponentPatch::Light(light) => {
408            if let Some(slot) = world.core.get_light_mut(entity) {
409                *slot = light;
410            }
411        }
412        ComponentPatch::Visibility(visible) => {
413            world.core.add_components(entity, masks::VISIBILITY);
414            world.core.set_visibility(entity, Visibility { visible });
415        }
416        ComponentPatch::CastsShadow(value) => {
417            if value {
418                world.core.add_components(entity, masks::CASTS_SHADOW);
419                world.core.set_casts_shadow(entity, CastsShadow);
420            } else {
421                world.core.remove_components(entity, masks::CASTS_SHADOW);
422            }
423        }
424        ComponentPatch::Camera(camera) => {
425            if let Some(slot) = world.core.get_camera_mut(entity) {
426                *slot = camera;
427            }
428        }
429        ComponentPatch::RigidBody(mut body) => {
430            if let Some(slot) = world.core.get_rigid_body_mut(entity) {
431                body.handle = slot.handle;
432                *slot = body;
433            }
434        }
435        ComponentPatch::Collider(mut collider) => {
436            if let Some(slot) = world.core.get_collider_mut(entity) {
437                collider.handle = slot.handle;
438                *slot = collider;
439            }
440        }
441        ComponentPatch::CharacterController(controller) => {
442            if let Some(slot) = world.core.get_character_controller_mut(entity) {
443                *slot = controller;
444            }
445        }
446        ComponentPatch::NavmeshAgent(agent) => {
447            if let Some(slot) = world.core.get_navmesh_agent_mut(entity) {
448                *slot = agent;
449            }
450        }
451        ComponentPatch::ParticleEmitter(emitter) => {
452            if let Some(slot) = world.core.get_particle_emitter_mut(entity) {
453                *slot = *emitter;
454            }
455        }
456        ComponentPatch::Decal(decal) => {
457            if let Some(slot) = world.core.get_decal_mut(entity) {
458                *slot = decal;
459            }
460        }
461        ComponentPatch::AudioSource(source) => {
462            if let Some(slot) = world.core.get_audio_source_mut(entity) {
463                *slot = source;
464            }
465        }
466        ComponentPatch::RenderLayer(layer) => {
467            if let Some(slot) = world.core.get_render_layer_mut(entity) {
468                *slot = layer;
469            }
470        }
471        ComponentPatch::CullingMask(mask) => {
472            if let Some(slot) = world.core.get_culling_mask_mut(entity) {
473                *slot = mask;
474            }
475        }
476        ComponentPatch::CameraCullingMask(mask) => {
477            if let Some(slot) = world.core.get_camera_culling_mask_mut(entity) {
478                *slot = mask;
479            }
480        }
481        ComponentPatch::IgnoreParentScale(present) => {
482            if present {
483                world
484                    .core
485                    .add_components(entity, masks::IGNORE_PARENT_SCALE);
486                world
487                    .core
488                    .set_ignore_parent_scale(entity, IgnoreParentScale);
489            } else {
490                world
491                    .core
492                    .remove_components(entity, masks::IGNORE_PARENT_SCALE);
493            }
494        }
495        ComponentPatch::MorphWeights(weights) => {
496            if let Some(slot) = world.core.get_morph_weights_mut(entity) {
497                for (index, weight) in weights.into_iter().enumerate() {
498                    slot.set_weight(index, weight);
499                }
500            }
501        }
502        ComponentPatch::Text {
503            content,
504            properties,
505        } => {
506            let text_index = world.core.get_text(entity).map(|text| text.text_index);
507            if let Some(text_index) = text_index {
508                world.resources.text.cache.set_text(text_index, &content);
509                if let Some(text) = world.core.get_text_mut(entity) {
510                    text.properties = properties;
511                    text.dirty = true;
512                }
513            }
514        }
515        ComponentPatch::Script(source) => {
516            world.core.add_components(entity, masks::SCRIPT);
517            world.core.set_script(entity, Script::from_source(source));
518        }
519    }
520}
521
522/// What a [`ComponentSnapshot`] captures, the undo-relevant subset of
523/// [`ComponentKind`] plus name, material, and prefab source.
524#[derive(Clone, Copy, PartialEq, Eq, Hash)]
525pub enum SnapshotKind {
526    Light,
527    Visibility,
528    CastsShadow,
529    Name,
530    ParticleEmitter,
531    Decal,
532    AudioSource,
533    RigidBody,
534    Collider,
535    CharacterController,
536    NavmeshAgent,
537    Camera,
538    AnimationPlayer,
539    Text,
540    RenderLayer,
541    CullingMask,
542    CameraCullingMask,
543    IgnoreParentScale,
544    PrefabSource,
545}
546
547/// The [`SnapshotKind`] that captures a [`ComponentKind`]'s undo state, if one
548/// does. Kinds with no value to restore (camera sub-components, listeners)
549/// return `None`.
550pub fn snapshot_kind_for(kind: ComponentKind) -> Option<SnapshotKind> {
551    Some(match kind {
552        ComponentKind::ParticleEmitter => SnapshotKind::ParticleEmitter,
553        ComponentKind::Decal => SnapshotKind::Decal,
554        ComponentKind::AudioSource => SnapshotKind::AudioSource,
555        ComponentKind::RigidBody => SnapshotKind::RigidBody,
556        ComponentKind::Collider => SnapshotKind::Collider,
557        ComponentKind::CharacterController => SnapshotKind::CharacterController,
558        ComponentKind::NavmeshAgent => SnapshotKind::NavmeshAgent,
559        ComponentKind::Camera => SnapshotKind::Camera,
560        ComponentKind::Text => SnapshotKind::Text,
561        ComponentKind::AnimationPlayer => SnapshotKind::AnimationPlayer,
562        ComponentKind::Visibility => SnapshotKind::Visibility,
563        ComponentKind::CastsShadow => SnapshotKind::CastsShadow,
564        ComponentKind::Light => SnapshotKind::Light,
565        ComponentKind::RenderLayer => SnapshotKind::RenderLayer,
566        ComponentKind::CullingMask => SnapshotKind::CullingMask,
567        ComponentKind::CameraCullingMask => SnapshotKind::CameraCullingMask,
568        ComponentKind::IgnoreParentScale => SnapshotKind::IgnoreParentScale,
569        _ => return None,
570    })
571}
572
573/// A captured component value, the unit an undo stack stores and reapplies. The
574/// optional variants record whether the component was present, so reapplying
575/// restores presence as well as value.
576#[derive(Clone)]
577pub enum ComponentSnapshot {
578    Light(Light),
579    Visibility(bool),
580    CastsShadow(bool),
581    Name(String),
582    ParticleEmitter(Option<Box<ParticleEmitter>>),
583    Decal(Option<Box<Decal>>),
584    AudioSource(Option<Box<AudioSource>>),
585    RigidBody(Option<Box<RigidBodyComponent>>),
586    Collider(Option<Box<ColliderComponent>>),
587    CharacterController(Option<Box<CharacterControllerComponent>>),
588    NavmeshAgent(Option<Box<NavMeshAgent>>),
589    Camera(Option<Camera>),
590    AnimationPlayer(Option<Box<AnimationPlayer>>),
591    Material {
592        name: String,
593        material: Option<Box<Material>>,
594    },
595    Text {
596        component: Option<Box<Text>>,
597        content: Option<String>,
598    },
599    RenderLayer(Option<RenderLayer>),
600    CullingMask(Option<CullingMask>),
601    CameraCullingMask(Option<CameraCullingMask>),
602    IgnoreParentScale(bool),
603    PrefabSource(Option<Box<PrefabSource>>),
604}
605
606impl PartialEq for ComponentSnapshot {
607    fn eq(&self, other: &Self) -> bool {
608        match (self, other) {
609            (Self::Light(a), Self::Light(b)) => {
610                a.light_type == b.light_type
611                    && a.color == b.color
612                    && a.intensity == b.intensity
613                    && a.range == b.range
614                    && a.inner_cone_angle == b.inner_cone_angle
615                    && a.outer_cone_angle == b.outer_cone_angle
616                    && a.cast_shadows == b.cast_shadows
617                    && a.shadow_bias == b.shadow_bias
618            }
619            (Self::Visibility(a), Self::Visibility(b)) => a == b,
620            (Self::CastsShadow(a), Self::CastsShadow(b)) => a == b,
621            (Self::Name(a), Self::Name(b)) => a == b,
622            (Self::ParticleEmitter(a), Self::ParticleEmitter(b)) => {
623                snapshot_bytes(a) == snapshot_bytes(b)
624            }
625            (Self::Decal(a), Self::Decal(b)) => a == b,
626            (Self::AudioSource(a), Self::AudioSource(b)) => snapshot_bytes(a) == snapshot_bytes(b),
627            (Self::RigidBody(a), Self::RigidBody(b)) => snapshot_bytes(a) == snapshot_bytes(b),
628            (Self::Collider(a), Self::Collider(b)) => snapshot_bytes(a) == snapshot_bytes(b),
629            (Self::CharacterController(a), Self::CharacterController(b)) => {
630                snapshot_bytes(a) == snapshot_bytes(b)
631            }
632            (Self::NavmeshAgent(a), Self::NavmeshAgent(b)) => {
633                snapshot_bytes(a) == snapshot_bytes(b)
634            }
635            (Self::Camera(a), Self::Camera(b)) => a == b,
636            (Self::AnimationPlayer(a), Self::AnimationPlayer(b)) => a == b,
637            (
638                Self::Material {
639                    name: a_name,
640                    material: a_mat,
641                },
642                Self::Material {
643                    name: b_name,
644                    material: b_mat,
645                },
646            ) => a_name == b_name && a_mat == b_mat,
647            (
648                Self::Text {
649                    component: a_component,
650                    content: a_content,
651                },
652                Self::Text {
653                    component: b_component,
654                    content: b_content,
655                },
656            ) => {
657                snapshot_bytes(a_component) == snapshot_bytes(b_component) && a_content == b_content
658            }
659            (Self::RenderLayer(a), Self::RenderLayer(b)) => a == b,
660            (Self::CullingMask(a), Self::CullingMask(b)) => a == b,
661            (Self::CameraCullingMask(a), Self::CameraCullingMask(b)) => a == b,
662            (Self::IgnoreParentScale(a), Self::IgnoreParentScale(b)) => a == b,
663            (Self::PrefabSource(a), Self::PrefabSource(b)) => {
664                snapshot_bytes(a) == snapshot_bytes(b)
665            }
666            _ => false,
667        }
668    }
669}
670
671fn snapshot_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
672    bincode::serialize(value).unwrap_or_default()
673}
674
675/// Captures the entity's current value for `kind`, or `None` when the kind has
676/// nothing to capture on this entity.
677pub fn snapshot_component(
678    world: &World,
679    entity: Entity,
680    kind: SnapshotKind,
681) -> Option<ComponentSnapshot> {
682    match kind {
683        SnapshotKind::Light => world
684            .core
685            .get_light(entity)
686            .cloned()
687            .map(ComponentSnapshot::Light),
688        SnapshotKind::Visibility => world
689            .core
690            .get_visibility(entity)
691            .map(|visibility| ComponentSnapshot::Visibility(visibility.visible)),
692        SnapshotKind::CastsShadow => Some(ComponentSnapshot::CastsShadow(
693            world.core.entity_has_casts_shadow(entity),
694        )),
695        SnapshotKind::Name => Some(ComponentSnapshot::Name(
696            world
697                .core
698                .get_name(entity)
699                .map(|name| name.0.clone())
700                .unwrap_or_default(),
701        )),
702        SnapshotKind::ParticleEmitter => Some(ComponentSnapshot::ParticleEmitter(
703            world
704                .core
705                .get_particle_emitter(entity)
706                .cloned()
707                .map(Box::new),
708        )),
709        SnapshotKind::Decal => Some(ComponentSnapshot::Decal(
710            world.core.get_decal(entity).cloned().map(Box::new),
711        )),
712        SnapshotKind::AudioSource => Some(ComponentSnapshot::AudioSource(
713            world.core.get_audio_source(entity).cloned().map(Box::new),
714        )),
715        SnapshotKind::RigidBody => Some(ComponentSnapshot::RigidBody(
716            world.core.get_rigid_body(entity).cloned().map(Box::new),
717        )),
718        SnapshotKind::Collider => Some(ComponentSnapshot::Collider(
719            world.core.get_collider(entity).cloned().map(Box::new),
720        )),
721        SnapshotKind::CharacterController => Some(ComponentSnapshot::CharacterController(
722            world
723                .core
724                .get_character_controller(entity)
725                .cloned()
726                .map(Box::new),
727        )),
728        SnapshotKind::NavmeshAgent => Some(ComponentSnapshot::NavmeshAgent(
729            world.core.get_navmesh_agent(entity).cloned().map(Box::new),
730        )),
731        SnapshotKind::Camera => Some(ComponentSnapshot::Camera(
732            world.core.get_camera(entity).copied(),
733        )),
734        SnapshotKind::AnimationPlayer => Some(ComponentSnapshot::AnimationPlayer(
735            world
736                .core
737                .get_animation_player(entity)
738                .cloned()
739                .map(Box::new),
740        )),
741        SnapshotKind::Text => {
742            let component = world.core.get_text(entity).cloned().map(Box::new);
743            let content = component
744                .as_ref()
745                .and_then(|text| world.resources.text.cache.get_text(text.text_index))
746                .map(str::to_string);
747            Some(ComponentSnapshot::Text { component, content })
748        }
749        SnapshotKind::RenderLayer => Some(ComponentSnapshot::RenderLayer(
750            world.core.get_render_layer(entity).copied(),
751        )),
752        SnapshotKind::CullingMask => Some(ComponentSnapshot::CullingMask(
753            world.core.get_culling_mask(entity).copied(),
754        )),
755        SnapshotKind::CameraCullingMask => Some(ComponentSnapshot::CameraCullingMask(
756            world.core.get_camera_culling_mask(entity).copied(),
757        )),
758        SnapshotKind::IgnoreParentScale => Some(ComponentSnapshot::IgnoreParentScale(
759            world.core.entity_has_ignore_parent_scale(entity),
760        )),
761        SnapshotKind::PrefabSource => Some(ComponentSnapshot::PrefabSource(
762            world.core.get_prefab_source(entity).cloned().map(Box::new),
763        )),
764    }
765}
766
767/// Restores a captured snapshot onto the entity, re-adding or dropping the
768/// component to match what the snapshot recorded.
769pub fn restore_component(snapshot: &ComponentSnapshot, world: &mut World, entity: Entity) {
770    use nightshade::ecs::world as masks;
771    match snapshot {
772        ComponentSnapshot::Light(light) => {
773            if let Some(target) = world.core.get_light_mut(entity) {
774                *target = light.clone();
775            }
776        }
777        ComponentSnapshot::Visibility(visible) => {
778            world.core.add_components(entity, masks::VISIBILITY);
779            world
780                .core
781                .set_visibility(entity, Visibility { visible: *visible });
782        }
783        ComponentSnapshot::CastsShadow(value) => {
784            if *value {
785                world.core.add_components(entity, masks::CASTS_SHADOW);
786                world.core.set_casts_shadow(entity, CastsShadow);
787            } else {
788                world.core.remove_components(entity, masks::CASTS_SHADOW);
789            }
790        }
791        ComponentSnapshot::Name(text) => {
792            world.core.set_name(entity, Name(text.clone()));
793        }
794        ComponentSnapshot::ParticleEmitter(value) => {
795            restore_optional(
796                world,
797                entity,
798                value.as_deref().cloned(),
799                masks::PARTICLE_EMITTER,
800                |world, entity, value| world.core.set_particle_emitter(entity, value),
801            );
802        }
803        ComponentSnapshot::Decal(value) => {
804            restore_optional(
805                world,
806                entity,
807                value.as_deref().cloned(),
808                masks::DECAL,
809                |world, entity, value| world.core.set_decal(entity, value),
810            );
811        }
812        ComponentSnapshot::AudioSource(value) => {
813            restore_optional(
814                world,
815                entity,
816                value.as_deref().cloned(),
817                masks::AUDIO_SOURCE,
818                |world, entity, value| world.core.set_audio_source(entity, value),
819            );
820        }
821        ComponentSnapshot::RigidBody(value) => {
822            restore_optional(
823                world,
824                entity,
825                value.as_deref().cloned(),
826                masks::RIGID_BODY,
827                |world, entity, value| world.core.set_rigid_body(entity, value),
828            );
829        }
830        ComponentSnapshot::Collider(value) => {
831            restore_optional(
832                world,
833                entity,
834                value.as_deref().cloned(),
835                masks::COLLIDER,
836                |world, entity, value| world.core.set_collider(entity, value),
837            );
838        }
839        ComponentSnapshot::CharacterController(value) => {
840            restore_optional(
841                world,
842                entity,
843                value.as_deref().cloned(),
844                masks::CHARACTER_CONTROLLER,
845                |world, entity, value| world.core.set_character_controller(entity, value),
846            );
847        }
848        ComponentSnapshot::NavmeshAgent(value) => {
849            restore_optional(
850                world,
851                entity,
852                value.as_deref().cloned(),
853                masks::NAVMESH_AGENT,
854                |world, entity, value| world.core.set_navmesh_agent(entity, value),
855            );
856        }
857        ComponentSnapshot::Camera(value) => {
858            restore_optional(
859                world,
860                entity,
861                *value,
862                masks::CAMERA,
863                |world, entity, value| world.core.set_camera(entity, value),
864            );
865        }
866        ComponentSnapshot::AnimationPlayer(value) => {
867            restore_optional(
868                world,
869                entity,
870                value.as_deref().cloned(),
871                masks::ANIMATION_PLAYER,
872                |world, entity, value| world.core.set_animation_player(entity, value),
873            );
874        }
875        ComponentSnapshot::Material { name, material } => {
876            if let Some(material) = material.as_deref() {
877                queue_ecs_command(
878                    world,
879                    nightshade::ecs::world::commands::EcsCommand::ReloadMaterial {
880                        name: name.clone(),
881                        material: Box::new(material.clone()),
882                    },
883                );
884            }
885        }
886        ComponentSnapshot::Text { component, content } => {
887            restore_optional(
888                world,
889                entity,
890                component.as_deref().cloned(),
891                masks::TEXT,
892                |world, entity, value| world.core.set_text(entity, value),
893            );
894            if let (Some(component), Some(content)) = (component.as_deref(), content.as_deref()) {
895                world
896                    .resources
897                    .text
898                    .cache
899                    .set_text(component.text_index, content);
900            }
901        }
902        ComponentSnapshot::RenderLayer(value) => {
903            restore_optional(
904                world,
905                entity,
906                *value,
907                masks::RENDER_LAYER,
908                |world, entity, value| world.core.set_render_layer(entity, value),
909            );
910        }
911        ComponentSnapshot::CullingMask(value) => {
912            restore_optional(
913                world,
914                entity,
915                *value,
916                masks::CULLING_MASK,
917                |world, entity, value| world.core.set_culling_mask(entity, value),
918            );
919        }
920        ComponentSnapshot::CameraCullingMask(value) => {
921            restore_optional(
922                world,
923                entity,
924                *value,
925                masks::CAMERA_CULLING_MASK,
926                |world, entity, value| world.core.set_camera_culling_mask(entity, value),
927            );
928        }
929        ComponentSnapshot::IgnoreParentScale(present) => {
930            if *present {
931                world
932                    .core
933                    .add_components(entity, masks::IGNORE_PARENT_SCALE);
934                world
935                    .core
936                    .set_ignore_parent_scale(entity, IgnoreParentScale);
937            } else {
938                world
939                    .core
940                    .remove_components(entity, masks::IGNORE_PARENT_SCALE);
941            }
942        }
943        ComponentSnapshot::PrefabSource(value) => {
944            restore_optional(
945                world,
946                entity,
947                value.as_deref().cloned(),
948                masks::PREFAB_SOURCE,
949                |world, entity, value| world.core.set_prefab_source(entity, value),
950            );
951        }
952    }
953}
954
955/// Drops the matching component mask off `entity` and re-adds the component when
956/// `value` is present, restoring whichever state the snapshot captured.
957fn restore_optional<T, F>(world: &mut World, entity: Entity, value: Option<T>, mask: u64, set: F)
958where
959    F: FnOnce(&mut World, Entity, T),
960{
961    if let Some(value) = value {
962        world.core.add_components(entity, mask);
963        set(world, entity, value);
964    } else {
965        world.core.remove_components(entity, mask);
966    }
967}