bevy_yoleck/
specs_registration.rs

1use std::any::{Any, TypeId};
2use std::marker::PhantomData;
3
4use bevy::ecs::component::Mutable;
5use bevy::ecs::system::EntityCommands;
6use bevy::platform::collections::HashMap;
7use bevy::prelude::*;
8use serde::{Deserialize, Serialize};
9
10use crate::prelude::YoleckEditorState;
11use crate::{BoxedAny, YoleckEntityLifecycleStatus, YoleckInternalSchedule, YoleckManaged};
12
13/// A component that Yoleck will write to and read from `.yol` files.
14///
15/// Rather than being used for general ECS behavior definition, `YoleckComponent`s should be used
16/// for spawning the actual components using [populate
17/// systems](crate::prelude::YoleckSchedule::Populate).
18pub trait YoleckComponent:
19    Default + Clone + PartialEq + Component<Mutability = Mutable> + Serialize + for<'a> Deserialize<'a>
20{
21    const KEY: &'static str;
22}
23
24/// A type of entity that can be created and edited with the Yoleck level editor.
25///
26/// Yoleck will only read and write the components registered on the entity type with the
27/// [`with`](YoleckEntityType::with) method, even if the file has data of other components or if
28/// the Bevy entity has other [`YoleckComponent`]s inserted to it. These components will still take
29/// effect in edit and populate systems though, even if they are not registered on the entity.
30pub struct YoleckEntityType {
31    /// The `type_name` used to identify the entity type.
32    pub name: String,
33    pub(crate) components: Vec<Box<dyn YoleckComponentHandler>>,
34    #[allow(clippy::type_complexity)]
35    pub(crate) on_init:
36        Vec<Box<dyn 'static + Sync + Send + Fn(YoleckEditorState, &mut EntityCommands)>>,
37    pub has_uuid: bool,
38}
39
40impl YoleckEntityType {
41    pub fn new(name: impl ToString) -> Self {
42        Self {
43            name: name.to_string(),
44            components: Default::default(),
45            on_init: Default::default(),
46            has_uuid: false,
47        }
48    }
49
50    /// Register a [`YoleckComponent`] for entities of this type.
51    pub fn with<T: YoleckComponent>(mut self) -> Self {
52        self.components
53            .push(Box::<YoleckComponentHandlerImpl<T>>::default());
54        self
55    }
56
57    /// Automatically spawn regular Bevy components when creating entities of this type.
58    ///
59    /// This is useful for marker components that don't carry data that needs to be saved to files.
60    pub fn insert_on_init<T: Bundle>(
61        mut self,
62        bundle_maker: impl 'static + Sync + Send + Fn() -> T,
63    ) -> Self {
64        self.on_init.push(Box::new(move |_, cmd| {
65            cmd.insert(bundle_maker());
66        }));
67        self
68    }
69
70    /// Similar to [`insert_on_init`](Self::insert_on_init), but only applies for entities in the
71    /// editor. Will not be added during playtests or actual game.
72    pub fn insert_on_init_during_editor<T: Bundle>(
73        mut self,
74        bundle_maker: impl 'static + Sync + Send + Fn() -> T,
75    ) -> Self {
76        self.on_init.push(Box::new(move |editor_state, cmd| {
77            if matches!(editor_state, YoleckEditorState::EditorActive) {
78                cmd.insert(bundle_maker());
79            }
80        }));
81        self
82    }
83
84    /// Similar to [`insert_on_init`](Self::insert_on_init), but only applies for entities in
85    /// playtests or the actual game. Will not be added in the editor.
86    pub fn insert_on_init_during_game<T: Bundle>(
87        mut self,
88        bundle_maker: impl 'static + Sync + Send + Fn() -> T,
89    ) -> Self {
90        self.on_init.push(Box::new(move |editor_state, cmd| {
91            if matches!(editor_state, YoleckEditorState::GameActive) {
92                cmd.insert(bundle_maker());
93            }
94        }));
95        self
96    }
97
98    /// Give the entity a UUID, so that it can be persistently referred in `.yol` files.
99    ///
100    /// These entities will have a `uuid` field in their record header in the `.yol` file, and a
101    /// [`YoleckEntityUuid`](crate::prelude::YoleckEntityUuid) component that stores the same UUID
102    /// when loaded. The [`YoleckUuidRegistry`](crate::prelude::YoleckUuidRegistry) resource can be
103    /// used to resolve the entity from the UUID.
104    ///
105    /// # Required for Entity References
106    ///
107    /// **This method must be called for entity types that need to be referenced by other entities.**
108    /// Only entities with UUID can be:
109    /// - Referenced using [`YoleckEntityRef`](crate::prelude::YoleckEntityRef)
110    /// - Dragged and dropped onto entity reference fields in the editor
111    /// - Selected via the viewport click tool for entity references
112    ///
113    /// # Example
114    ///
115    /// ```no_run
116    /// # use bevy::prelude::*;
117    /// # use bevy_yoleck::prelude::*;
118    /// # let mut app = App::new();
119    /// // Planet can be referenced by other entities
120    /// app.add_yoleck_entity_type({
121    ///     YoleckEntityType::new("Planet")
122    ///         .with_uuid()  // Required for references!
123    /// #       ;YoleckEntityType::new("Planet")
124    /// });
125    /// ```
126    pub fn with_uuid(mut self) -> Self {
127        self.has_uuid = true;
128        self
129    }
130}
131
132pub(crate) trait YoleckComponentHandler: 'static + Sync + Send {
133    fn component_type(&self) -> TypeId;
134    fn key(&self) -> &'static str;
135    fn init_in_entity(
136        &self,
137        data: Option<serde_json::Value>,
138        cmd: &mut EntityCommands,
139        components_data: &mut HashMap<TypeId, BoxedAny>,
140    );
141    fn build_in_bevy_app(&self, app: &mut App);
142    fn serialize(&self, component: &dyn Any) -> serde_json::Value;
143}
144
145#[derive(Default)]
146struct YoleckComponentHandlerImpl<T: YoleckComponent> {
147    _phantom_data: PhantomData<T>,
148}
149
150impl<T: YoleckComponent> YoleckComponentHandler for YoleckComponentHandlerImpl<T> {
151    fn component_type(&self) -> TypeId {
152        TypeId::of::<T>()
153    }
154
155    fn key(&self) -> &'static str {
156        T::KEY
157    }
158
159    fn init_in_entity(
160        &self,
161        data: Option<serde_json::Value>,
162        cmd: &mut EntityCommands,
163        components_data: &mut HashMap<TypeId, BoxedAny>,
164    ) {
165        let component: T = if let Some(data) = data {
166            match serde_json::from_value(data) {
167                Ok(component) => component,
168                Err(err) => {
169                    error!("Cannot load {:?}: {:?}", T::KEY, err);
170                    return;
171                }
172            }
173        } else {
174            Default::default()
175        };
176        components_data.insert(self.component_type(), Box::new(component.clone()));
177        cmd.insert(component);
178    }
179
180    fn build_in_bevy_app(&self, app: &mut App) {
181        if let Some(schedule) =
182            app.get_schedule_mut(YoleckInternalSchedule::UpdateManagedDataFromComponents)
183        {
184            schedule.add_systems(Self::update_data_from_components);
185        }
186    }
187
188    fn serialize(&self, component: &dyn Any) -> serde_json::Value {
189        let concrete = component
190            .downcast_ref::<T>()
191            .expect("Serialize must be called with the correct type");
192        serde_json::to_value(concrete).expect("Component must always be serializable")
193    }
194}
195
196impl<T: YoleckComponent> YoleckComponentHandlerImpl<T> {
197    fn update_data_from_components(mut query: Query<(&mut YoleckManaged, &mut T)>) {
198        for (mut yoleck_managed, component) in query.iter_mut() {
199            let yoleck_managed = yoleck_managed.as_mut();
200            match yoleck_managed.components_data.entry(TypeId::of::<T>()) {
201                bevy::platform::collections::hash_map::Entry::Vacant(entry) => {
202                    yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;
203                    entry.insert(Box::<T>::new(component.clone()));
204                }
205                bevy::platform::collections::hash_map::Entry::Occupied(mut entry) => {
206                    let existing: &mut T = entry
207                        .get_mut()
208                        .downcast_mut()
209                        .expect("Component data is of wrong type");
210                    if existing != component.as_ref() {
211                        yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;
212                        *existing = component.clone();
213                    }
214                }
215            }
216        }
217    }
218}