bevy_yoleck/
specs_registration.rs1use 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
13pub trait YoleckComponent:
19    Default + Clone + PartialEq + Component<Mutability = Mutable> + Serialize + for<'a> Deserialize<'a>
20{
21    const KEY: &'static str;
22}
23
24pub struct YoleckEntityType {
31    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    pub fn with<T: YoleckComponent>(mut self) -> Self {
52        self.components
53            .push(Box::<YoleckComponentHandlerImpl<T>>::default());
54        self
55    }
56
57    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    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    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    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}