1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
use std::any::{Any, TypeId};
use std::marker::PhantomData;

use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;
use bevy::utils::HashMap;
use serde::{Deserialize, Serialize};

use crate::prelude::YoleckEditorState;
use crate::{BoxedAny, YoleckEntityLifecycleStatus, YoleckInternalSchedule, YoleckManaged};

/// A component that Yoleck will write to and read from `.yol` files.
///
/// Rather than being used for general ECS behavior definition, `YoleckComponent`s should be used
/// for spawning the actual components using [populate
/// systems](crate::prelude::YoleckSchedule::Populate).
pub trait YoleckComponent:
    Default + Clone + PartialEq + Component + Serialize + for<'a> Deserialize<'a>
{
    const KEY: &'static str;
}

/// A type of entity that can be created and edited with the Yoleck level editor.
///
/// Yoleck will only read and write the components registered on the entity type with the
/// [`with`](YoleckEntityType::with) method, even if the file has data of other components or if
/// the Bevy entity has other [`YoleckComponent`]s inserted to it. These components will still take
/// effect in edit and populate systems though, even if they are not registered on the entity.
pub struct YoleckEntityType {
    /// The `type_name` used to identify the entity type.
    pub name: String,
    pub(crate) components: Vec<Box<dyn YoleckComponentHandler>>,
    #[allow(clippy::type_complexity)]
    pub(crate) on_init:
        Vec<Box<dyn 'static + Sync + Send + Fn(YoleckEditorState, &mut EntityCommands)>>,
    pub has_uuid: bool,
}

impl YoleckEntityType {
    pub fn new(name: impl ToString) -> Self {
        Self {
            name: name.to_string(),
            components: Default::default(),
            on_init: Default::default(),
            has_uuid: false,
        }
    }

    /// Register a [`YoleckComponent`] for entities of this type.
    pub fn with<T: YoleckComponent>(mut self) -> Self {
        self.components
            .push(Box::<YoleckComponentHandlerImpl<T>>::default());
        self
    }

    /// Automatically spawn regular Bevy components when creating entities of this type.
    ///
    /// This is useful for marker components that don't carry data that needs to be saved to files.
    pub fn insert_on_init<T: Bundle>(
        mut self,
        bundle_maker: impl 'static + Sync + Send + Fn() -> T,
    ) -> Self {
        self.on_init.push(Box::new(move |_, cmd| {
            cmd.insert(bundle_maker());
        }));
        self
    }

    /// Similar to [`insert_on_init`](Self::insert_on_init), but only applies for entities in the
    /// editor. Will not be added during playtests or actual game.
    pub fn insert_on_init_during_editor<T: Bundle>(
        mut self,
        bundle_maker: impl 'static + Sync + Send + Fn() -> T,
    ) -> Self {
        self.on_init.push(Box::new(move |editor_state, cmd| {
            if matches!(editor_state, YoleckEditorState::EditorActive) {
                cmd.insert(bundle_maker());
            }
        }));
        self
    }

    /// Similar to [`insert_on_init`](Self::insert_on_init), but only applies for entities in
    /// playtests or the actual game. Will not be added in the editor.
    pub fn insert_on_init_during_game<T: Bundle>(
        mut self,
        bundle_maker: impl 'static + Sync + Send + Fn() -> T,
    ) -> Self {
        self.on_init.push(Box::new(move |editor_state, cmd| {
            if matches!(editor_state, YoleckEditorState::GameActive) {
                cmd.insert(bundle_maker());
            }
        }));
        self
    }

    /// Give the entity a UUID, so that it can be persistently referred in `.yol` files.
    ///
    /// These entities will have a `uuid` field in their record header in the `.yol` file, and a
    /// [`YoleckEntityUuid`](crate::prelude::YoleckEntityUuid) component that stores the same UUID
    /// when loaded. The [`YoleckUuidRegistry`](crate::prelude::YoleckUuidRegistry) resource can be
    /// used to resolve the entity from the UUID.
    pub fn with_uuid(mut self) -> Self {
        self.has_uuid = true;
        self
    }
}

pub(crate) trait YoleckComponentHandler: 'static + Sync + Send {
    fn component_type(&self) -> TypeId;
    fn key(&self) -> &'static str;
    fn init_in_entity(
        &self,
        data: Option<serde_json::Value>,
        cmd: &mut EntityCommands,
        components_data: &mut HashMap<TypeId, BoxedAny>,
    );
    fn build_in_bevy_app(&self, app: &mut App);
    fn serialize(&self, component: &dyn Any) -> serde_json::Value;
}

#[derive(Default)]
struct YoleckComponentHandlerImpl<T: YoleckComponent> {
    _phantom_data: PhantomData<T>,
}

impl<T: YoleckComponent> YoleckComponentHandler for YoleckComponentHandlerImpl<T> {
    fn component_type(&self) -> TypeId {
        TypeId::of::<T>()
    }

    fn key(&self) -> &'static str {
        T::KEY
    }

    fn init_in_entity(
        &self,
        data: Option<serde_json::Value>,
        cmd: &mut EntityCommands,
        components_data: &mut HashMap<TypeId, BoxedAny>,
    ) {
        let component: T = if let Some(data) = data {
            match serde_json::from_value(data) {
                Ok(component) => component,
                Err(err) => {
                    error!("Cannot load {:?}: {:?}", T::KEY, err);
                    return;
                }
            }
        } else {
            Default::default()
        };
        components_data.insert(self.component_type(), Box::new(component.clone()));
        cmd.insert(component);
    }

    fn build_in_bevy_app(&self, app: &mut App) {
        if let Some(schedule) =
            app.get_schedule_mut(YoleckInternalSchedule::UpdateManagedDataFromComponents)
        {
            schedule.add_systems(Self::update_data_from_components);
        }
    }

    fn serialize(&self, component: &dyn Any) -> serde_json::Value {
        let concrete = component
            .downcast_ref::<T>()
            .expect("Serialize must be called with the correct type");
        serde_json::to_value(concrete).expect("Component must always be serializable")
    }
}

impl<T: YoleckComponent> YoleckComponentHandlerImpl<T> {
    fn update_data_from_components(mut query: Query<(&mut YoleckManaged, &mut T)>) {
        for (mut yoleck_managed, component) in query.iter_mut() {
            let yoleck_managed = yoleck_managed.as_mut();
            match yoleck_managed.components_data.entry(TypeId::of::<T>()) {
                bevy::utils::hashbrown::hash_map::Entry::Vacant(entry) => {
                    yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;
                    entry.insert(Box::<T>::new(component.clone()));
                }
                bevy::utils::hashbrown::hash_map::Entry::Occupied(mut entry) => {
                    let existing: &mut T = entry
                        .get_mut()
                        .downcast_mut()
                        .expect("Component data is of wrong type");
                    if existing != component.as_ref() {
                        yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::JustChanged;
                        *existing = component.clone();
                    }
                }
            }
        }
    }
}