bevy_yoleck/
entity_management.rs

1use std::collections::BTreeSet;
2
3use bevy::asset::io::Reader;
4use bevy::asset::{AssetLoader, LoadContext};
5use bevy::platform::collections::HashMap;
6use bevy::prelude::*;
7use bevy::reflect::TypePath;
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use crate::editor::YoleckEditorState;
12use crate::entity_upgrading::YoleckEntityUpgrading;
13use crate::errors::YoleckAssetLoaderError;
14use crate::level_files_upgrading::upgrade_level_file;
15use crate::populating::PopulateReason;
16use crate::prelude::{YoleckEntityUuid, YoleckUuidRegistry};
17use crate::{
18    YoleckBelongsToLevel, YoleckEntityConstructionSpecs, YoleckEntityLifecycleStatus,
19    YoleckInternalSchedule, YoleckLevelJustLoaded, YoleckManaged, YoleckSchedule, YoleckState,
20};
21
22/// Used by Yoleck to determine how to handle the entity.
23#[derive(Serialize, Deserialize, Debug, Clone)]
24pub struct YoleckEntryHeader {
25    #[serde(rename = "type")]
26    pub type_name: String,
27    /// A name to display near the entity in the entities list.
28    ///
29    /// This is for level editors' convenience only - it will not be used in the games.
30    #[serde(default)]
31    pub name: String,
32
33    /// A persistable way to identify the specific entity.
34    ///
35    /// Will be set automatically if the entity type was defined with
36    /// [`with_uuid`](crate::YoleckEntityType::with_uuid).
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub uuid: Option<Uuid>,
39}
40
41/// An entry for a Yoleck entity, as it appears in level files.
42#[derive(Component, Debug, Clone)]
43pub struct YoleckRawEntry {
44    pub header: YoleckEntryHeader,
45    pub data: serde_json::Map<String, serde_json::Value>,
46}
47
48impl Serialize for YoleckRawEntry {
49    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50    where
51        S: serde::Serializer,
52    {
53        (&self.header, &self.data).serialize(serializer)
54    }
55}
56
57impl<'de> Deserialize<'de> for YoleckRawEntry {
58    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59    where
60        D: serde::Deserializer<'de>,
61    {
62        let (header, data) = Deserialize::deserialize(deserializer)?;
63        Ok(Self { header, data })
64    }
65}
66
67pub(crate) fn yoleck_process_raw_entries(
68    editor_state: Res<State<YoleckEditorState>>,
69    mut commands: Commands,
70    mut raw_entries_query: Query<(Entity, &mut YoleckRawEntry), With<YoleckBelongsToLevel>>,
71    construction_specs: Res<YoleckEntityConstructionSpecs>,
72    mut uuid_registry: ResMut<YoleckUuidRegistry>,
73) {
74    let mut entities_by_type = HashMap::<String, Vec<Entity>>::new();
75    for (entity, mut raw_entry) in raw_entries_query.iter_mut() {
76        entities_by_type
77            .entry(raw_entry.header.type_name.clone())
78            .or_default()
79            .push(entity);
80        let mut cmd = commands.entity(entity);
81        cmd.remove::<YoleckRawEntry>();
82
83        let mut components_data = HashMap::new();
84
85        if let Some(entity_type_info) =
86            construction_specs.get_entity_type_info(&raw_entry.header.type_name)
87        {
88            if entity_type_info.has_uuid {
89                let uuid = raw_entry.header.uuid.unwrap_or_else(Uuid::new_v4);
90                cmd.insert(YoleckEntityUuid(uuid));
91                uuid_registry.0.insert(uuid, cmd.id());
92            }
93            for component_name in entity_type_info.components.iter() {
94                let Some(handler) = construction_specs.component_handlers.get(component_name)
95                else {
96                    error!("Component type {:?} is not registered", component_name);
97                    continue;
98                };
99                let raw_component_data = raw_entry
100                    .data
101                    .get_mut(handler.key())
102                    .map(|component_data| component_data.take());
103                handler.init_in_entity(raw_component_data, &mut cmd, &mut components_data);
104            }
105            for dlg in entity_type_info.on_init.iter() {
106                dlg(*editor_state.get(), &mut cmd);
107            }
108        } else {
109            error!("Entity type {:?} is not registered", raw_entry.header.name);
110        }
111
112        cmd.insert(YoleckManaged {
113            name: raw_entry.header.name.to_owned(),
114            type_name: raw_entry.header.type_name.to_owned(),
115            lifecycle_status: YoleckEntityLifecycleStatus::JustCreated,
116            components_data,
117        });
118    }
119}
120
121pub(crate) fn yoleck_prepare_populate_schedule(
122    mut query: Query<(Entity, &mut YoleckManaged)>,
123    mut entities_to_populate: ResMut<EntitiesToPopulate>,
124    mut yoleck_state: Option<ResMut<YoleckState>>,
125    editor_state: Res<State<YoleckEditorState>>,
126) {
127    entities_to_populate.0.clear();
128    let mut level_needs_saving = false;
129    for (entity, mut yoleck_managed) in query.iter_mut() {
130        match yoleck_managed.lifecycle_status {
131            YoleckEntityLifecycleStatus::Synchronized => {}
132            YoleckEntityLifecycleStatus::JustCreated => {
133                let populate_reason = match editor_state.get() {
134                    YoleckEditorState::EditorActive => PopulateReason::EditorInit,
135                    YoleckEditorState::GameActive => PopulateReason::RealGame,
136                };
137                entities_to_populate.0.push((entity, populate_reason));
138            }
139            YoleckEntityLifecycleStatus::JustChanged => {
140                entities_to_populate
141                    .0
142                    .push((entity, PopulateReason::EditorUpdate));
143                level_needs_saving = true;
144            }
145        }
146        yoleck_managed.lifecycle_status = YoleckEntityLifecycleStatus::Synchronized;
147    }
148    if level_needs_saving && let Some(yoleck_state) = yoleck_state.as_mut() {
149        yoleck_state.level_needs_saving = true;
150    }
151}
152
153pub(crate) fn yoleck_run_populate_schedule(world: &mut World) {
154    world.run_schedule(YoleckSchedule::Populate);
155    world.run_schedule(YoleckSchedule::OverrideCommonComponents);
156}
157
158#[derive(Resource)]
159pub(crate) struct EntitiesToPopulate(pub Vec<(Entity, PopulateReason)>);
160
161pub(crate) fn process_loading_command(
162    query: Query<(Entity, &YoleckLoadLevel)>,
163    mut raw_levels_assets: ResMut<Assets<YoleckRawLevel>>,
164    entity_upgrading: Option<Res<YoleckEntityUpgrading>>,
165    mut commands: Commands,
166) {
167    for (level_entity, load_level) in query.iter() {
168        if let Some(raw_level) = raw_levels_assets.get_mut(&load_level.0) {
169            if let Some(entity_upgrading) = &entity_upgrading {
170                entity_upgrading.upgrade_raw_level_file(raw_level);
171            }
172            commands
173                .entity(level_entity)
174                .remove::<YoleckLoadLevel>()
175                .insert((YoleckLevelJustLoaded, YoleckKeepLevel));
176            for entry in raw_level.entries() {
177                commands.spawn((
178                    entry.clone(),
179                    YoleckBelongsToLevel {
180                        level: level_entity,
181                    },
182                ));
183            }
184        }
185    }
186}
187
188pub(crate) fn yoleck_run_post_load_resolutions_schedule(world: &mut World) {
189    world.run_schedule(YoleckInternalSchedule::PostLoadResolutions);
190}
191
192pub(crate) fn yoleck_run_level_loaded_schedule(world: &mut World) {
193    world.run_schedule(YoleckSchedule::LevelLoaded);
194}
195
196pub(crate) fn yoleck_remove_just_loaded_marker_from_levels(
197    query: Query<Entity, With<YoleckLevelJustLoaded>>,
198    mut commands: Commands,
199) {
200    for level_entity in query.iter() {
201        commands
202            .entity(level_entity)
203            .remove::<YoleckLevelJustLoaded>();
204    }
205}
206
207pub(crate) fn process_unloading_command(
208    mut removed_levels: RemovedComponents<YoleckKeepLevel>,
209    level_owned_entities_query: Query<(Entity, &YoleckBelongsToLevel)>,
210    mut commands: Commands,
211) {
212    if removed_levels.is_empty() {
213        return;
214    }
215    let removed_levels: BTreeSet<Entity> = removed_levels.read().collect();
216    for (entity, belongs_to_level) in level_owned_entities_query.iter() {
217        if removed_levels.contains(&belongs_to_level.level) {
218            commands.entity(entity).despawn();
219        }
220    }
221}
222
223/// Command Yoleck to load a level.
224///
225/// ```no_run
226/// # use bevy::prelude::*;
227/// # use bevy_yoleck::prelude::*;
228/// fn level_loading_system(
229///     asset_server: Res<AssetServer>,
230///     mut commands: Commands,
231/// ) {
232///     commands.spawn(YoleckLoadLevel(asset_server.load("levels/level1.yol")));
233/// }
234/// ```
235///
236/// After the level is loaded, `YoleckLoadLevel` will be removed and [`YoleckKeepLevel`] will be
237/// added instead. To unload the level, either remove `YoleckKeepLevel` or despawn the entire level
238/// entity.
239///
240/// Immediately after the level is loaded, but before the populate systems get to run, Yoleck will
241/// run the [`YoleckSchedule::LevelLoaded`] schedule, allowing the game to register systems there
242/// and interfere with the level entities while they are still just freshly deserialized
243/// [`YoleckComponent`](crate::prelude::YoleckComponent) data. During that time, the entities of
244/// the levels that were just loaded will be marked with [`YoleckLevelJustLoaded`], allowing to
245/// these systems to distinguish them from already existing levels.
246///
247/// Note that the entities inside the level will _not_ be children of the level entity. Games that
248/// want to load multiple levels and dynamically position them should use
249/// [`VpeolRepositionLevel`](crate::vpeol::VpeolRepositionLevel).
250#[derive(Component)]
251pub struct YoleckLoadLevel(pub Handle<YoleckRawLevel>);
252
253/// Marks an entity that represents a level. Its removal will unload the level.
254///
255/// This component is created automatically on entities that use [`YoleckLoadLevel`] when the level
256/// is loaded.
257///
258/// To unload the level, either despawn the entity or remove this component from it.
259#[derive(Component)]
260pub struct YoleckKeepLevel;
261
262#[derive(TypePath)]
263pub(crate) struct YoleckLevelAssetLoader;
264
265/// Represents a level file.
266#[derive(Asset, TypePath, Debug, Serialize, Deserialize, Clone)]
267pub struct YoleckRawLevel(
268    pub(crate) YoleckRawLevelHeader,
269    serde_json::Value, // level data
270    pub(crate) Vec<YoleckRawEntry>,
271);
272
273/// Internal Yoleck metadata for a level file.
274#[derive(Debug, Serialize, Deserialize, Clone)]
275pub struct YoleckRawLevelHeader {
276    format_version: usize,
277    pub app_format_version: usize,
278}
279
280impl YoleckRawLevel {
281    pub fn new(
282        app_format_version: usize,
283        entries: impl IntoIterator<Item = YoleckRawEntry>,
284    ) -> Self {
285        Self(
286            YoleckRawLevelHeader {
287                format_version: 2,
288                app_format_version,
289            },
290            serde_json::Value::Object(Default::default()),
291            entries.into_iter().collect(),
292        )
293    }
294
295    pub fn entries(&self) -> &[YoleckRawEntry] {
296        &self.2
297    }
298
299    pub fn into_entries(self) -> impl Iterator<Item = YoleckRawEntry> {
300        self.2.into_iter()
301    }
302}
303
304impl AssetLoader for YoleckLevelAssetLoader {
305    type Asset = YoleckRawLevel;
306    type Settings = ();
307    type Error = YoleckAssetLoaderError;
308
309    fn extensions(&self) -> &[&str] {
310        &["yol"]
311    }
312
313    async fn load(
314        &self,
315        reader: &mut dyn Reader,
316        _settings: &Self::Settings,
317        _load_context: &mut LoadContext<'_>,
318    ) -> Result<Self::Asset, Self::Error> {
319        let mut bytes = Vec::new();
320        reader.read_to_end(&mut bytes).await?;
321        let json = std::str::from_utf8(&bytes)?;
322        let level: serde_json::Value = serde_json::from_str(json)?;
323        let level = upgrade_level_file(level)?;
324        let level: YoleckRawLevel = serde_json::from_value(level)?;
325        Ok(level)
326    }
327}