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