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