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