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