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    pub fn with_uuid(mut self) -> Self {
105        self.has_uuid = true;
106        self
107    }
108}
109
110pub(crate) trait YoleckComponentHandler: 'static + Sync + Send {
111    fn component_type(&self) -> TypeId;
112    fn key(&self) -> &'static str;
113    fn init_in_entity(
114        &self,
115        data: Option<serde_json::Value>,
116        cmd: &mut EntityCommands,
117        components_data: &mut HashMap<TypeId, BoxedAny>,
118    );
119    fn build_in_bevy_app(&self, app: &mut App);
120    fn serialize(&self, component: &dyn Any) -> serde_json::Value;
121}
122
123#[derive(Default)]
124struct YoleckComponentHandlerImpl<T: YoleckComponent> {
125    _phantom_data: PhantomData<T>,
126}
127
128impl<T: YoleckComponent> YoleckComponentHandler for YoleckComponentHandlerImpl<T> {
129    fn component_type(&self) -> TypeId {
130        TypeId::of::<T>()
131    }
132
133    fn key(&self) -> &'static str {
134        T::KEY
135    }
136
137    fn init_in_entity(
138        &self,
139        data: Option<serde_json::Value>,
140        cmd: &mut EntityCommands,
141        components_data: &mut HashMap<TypeId, BoxedAny>,
142    ) {
143        let component: T = if let Some(data) = data {
144            match serde_json::from_value(data) {
145                Ok(component) => component,
146                Err(err) => {
147                    error!("Cannot load {:?}: {:?}", T::KEY, err);
148                    return;
149                }
150            }
151        } else {
152            Default::default()
153        };
154        components_data.insert(self.component_type(), Box::new(component.clone()));
155        cmd.insert(component);
156    }
157
158    fn build_in_bevy_app(&self, app: &mut App) {
159        if let Some(schedule) =
160            app.get_schedule_mut(YoleckInternalSchedule::UpdateManagedDataFromComponents)
161        {
162            schedule.add_systems(Self::update_data_from_components);
163        }
164    }
165
166    fn serialize(&self, component: &dyn Any) -> serde_json::Value {
167        let concrete = component
168            .downcast_ref::<T>()
169            .expect("Serialize must be called with the correct type");
170        serde_json::to_value(concrete).expect("Component must always be serializable")
171    }
172}
173
174impl<T: YoleckComponent> YoleckComponentHandlerImpl<T> {
175    fn update_data_from_components(mut query: Query<(&mut YoleckManaged, &mut T)>) {
176        for (mut yoleck_managed, component) in query.iter_mut() {
177            let yoleck_managed = yoleck_managed.as_mut();
178            match yoleck_managed.components_data.entry(TypeId::of::<T>()) {
179                bevy::platform::collections::hash_map::Entry::Vacant(entry) => {
180                    yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;
181                    entry.insert(Box::<T>::new(component.clone()));
182                }
183                bevy::platform::collections::hash_map::Entry::Occupied(mut entry) => {
184                    let existing: &mut T = entry
185                        .get_mut()
186                        .downcast_mut()
187                        .expect("Component data is of wrong type");
188                    if existing != component.as_ref() {
189                        yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;
190                        *existing = component.clone();
191                    }
192                }
193            }
194        }
195    }
196}