bevy_yoleck/
level_files_manager.rs

1use std::path::PathBuf;
2use std::{fs, io};
3
4use bevy::ecs::system::SystemState;
5use bevy::platform::collections::HashSet;
6use bevy::prelude::*;
7use bevy_egui::egui;
8
9use crate::entity_management::{
10    YoleckEntryHeader, YoleckKeepLevel, YoleckLoadLevel, YoleckRawEntry,
11};
12use crate::entity_upgrading::YoleckEntityUpgrading;
13use crate::exclusive_systems::YoleckActiveExclusiveSystem;
14use crate::knobs::YoleckKnobsCache;
15use crate::level_files_upgrading::upgrade_level_file;
16use crate::level_index::YoleckLevelIndexEntry;
17use crate::prelude::{YoleckEditorState, YoleckEntityUuid};
18use crate::{
19    YoleckEditableLevels, YoleckEntityConstructionSpecs, YoleckLevelInEditor,
20    YoleckLevelInPlaytest, YoleckLevelIndex, YoleckManaged, YoleckRawLevel, YoleckState,
21};
22
23const EXTENSION: &str = ".yol";
24const EXTENSION_WITHOUT_DOT: &str = "yol";
25
26/// The path for the levels directory.
27///
28/// [The plugin](crate::YoleckPluginForEditor) sets it to `./assets/levels/`, but it can be set to
29/// other values:
30/// ```no_run
31/// # use std::path::Path;
32/// # use bevy::prelude::*;
33/// # use bevy_yoleck::YoleckEditorLevelsDirectoryPath;
34/// # let mut app = App::new();
35/// app.insert_resource(YoleckEditorLevelsDirectoryPath(
36///     Path::new(".").join("some").join("other").join("path"),
37/// ));
38/// ```
39#[derive(Resource)]
40pub struct YoleckEditorLevelsDirectoryPath(pub PathBuf);
41
42/// The UI part for managing level files. See [`YoleckEditorSections`](crate::YoleckEditorSections).
43pub fn level_files_manager_section(world: &mut World) -> impl FnMut(&mut World, &mut egui::Ui) {
44    let mut system_state = SystemState::<(
45        Commands,
46        ResMut<YoleckState>,
47        ResMut<YoleckEditorLevelsDirectoryPath>,
48        ResMut<YoleckEditableLevels>,
49        Res<YoleckEntityConstructionSpecs>,
50        Query<(&YoleckManaged, Option<&YoleckEntityUuid>)>,
51        Query<Entity, With<YoleckKeepLevel>>,
52        Res<State<YoleckEditorState>>,
53        ResMut<NextState<YoleckEditorState>>,
54        ResMut<YoleckKnobsCache>,
55        ResMut<Assets<YoleckRawLevel>>,
56        Option<Res<YoleckEntityUpgrading>>,
57        Option<Res<YoleckActiveExclusiveSystem>>,
58    )>::new(world);
59
60    let mut should_list_files = true;
61    let mut loaded_files_index: io::Result<Vec<YoleckLevelIndexEntry>> = Ok(vec![]);
62
63    #[derive(Debug)]
64    enum SelectedLevelFile {
65        Unsaved(String),
66        Existing(String),
67    }
68
69    let mut selected_level_file = SelectedLevelFile::Unsaved(String::new());
70
71    let mut level_being_playtested: Option<YoleckRawLevel> = None;
72
73    move |world, ui: &mut egui::Ui| {
74        let (
75            mut commands,
76            mut yoleck,
77            mut levels_directory,
78            mut editable_levels,
79            construction_specs,
80            yoleck_managed_query,
81            keep_levels_query,
82            editor_state,
83            mut next_editor_state,
84            mut knobs_cache,
85            mut level_assets,
86            entity_upgrading,
87            active_exclusive_system,
88        ) = system_state.get_mut(world);
89
90        if active_exclusive_system.is_some() {
91            return;
92        }
93
94        let gen_raw_level_file = || {
95            let app_format_version = if let Some(entity_upgrading) = &entity_upgrading {
96                entity_upgrading.app_format_version
97            } else {
98                0
99            };
100            YoleckRawLevel::new(app_format_version, {
101                yoleck_managed_query
102                    .iter()
103                    .map(|(yoleck_managed, entity_uuid)| YoleckRawEntry {
104                        header: YoleckEntryHeader {
105                            type_name: yoleck_managed.type_name.clone(),
106                            name: yoleck_managed.name.clone(),
107                            uuid: entity_uuid.map(|entity_uuid| entity_uuid.get()),
108                        },
109                        data: {
110                            if let Some(entity_type_info) =
111                                construction_specs.get_entity_type_info(&yoleck_managed.type_name)
112                            {
113                                entity_type_info
114                                    .components
115                                    .iter()
116                                    .filter_map(|component| {
117                                        let component_data =
118                                            yoleck_managed.components_data.get(component)?;
119                                        let handler =
120                                            &construction_specs.component_handlers[component];
121                                        Some((
122                                            handler.key(),
123                                            handler.serialize(component_data.as_ref()),
124                                        ))
125                                    })
126                                    .collect()
127                            } else {
128                                error!(
129                                    "Entity type {:?} is not registered",
130                                    yoleck_managed.type_name
131                                );
132                                Default::default()
133                            }
134                        },
135                    })
136            })
137        };
138
139        let mut clear_level = |commands: &mut Commands| {
140            for level_entity in keep_levels_query.iter() {
141                commands.entity(level_entity).despawn();
142            }
143            for knob_entity in knobs_cache.drain() {
144                commands.entity(knob_entity).despawn();
145            }
146        };
147
148        ui.horizontal(|ui| {
149            if let Some(level) = &level_being_playtested {
150                let finish_playtest_response = ui.button("Finish Playtest");
151                if ui.button("Restart Playtest").clicked() {
152                    clear_level(&mut commands);
153                    let level_asset_handle = level_assets.add(level.clone());
154                    yoleck.level_being_edited = commands
155                        .spawn((YoleckLevelInPlaytest, YoleckLoadLevel(level_asset_handle)))
156                        .id();
157                }
158                if finish_playtest_response.clicked() {
159                    clear_level(&mut commands);
160                    next_editor_state.set(YoleckEditorState::EditorActive);
161                    let level_asset_handle = level_assets.add(level.clone());
162                    yoleck.level_being_edited = commands
163                        .spawn((YoleckLevelInEditor, YoleckLoadLevel(level_asset_handle)))
164                        .id();
165                    level_being_playtested = None;
166                }
167            } else {
168                #[allow(clippy::collapsible_else_if)]
169                if ui.button("Playtest").clicked() {
170                    let level = gen_raw_level_file();
171                    clear_level(&mut commands);
172                    next_editor_state.set(YoleckEditorState::GameActive);
173                    let level_asset_handle = level_assets.add(level.clone());
174                    yoleck.level_being_edited = commands
175                        .spawn((YoleckLevelInPlaytest, YoleckLoadLevel(level_asset_handle)))
176                        .id();
177                    level_being_playtested = Some(level);
178                }
179            }
180        });
181
182        if matches!(editor_state.get(), YoleckEditorState::EditorActive) {
183            egui::CollapsingHeader::new("Files")
184                .default_open(true)
185                .show(ui, |ui| {
186                    let mut path_str = levels_directory.0.to_string_lossy().to_string();
187                    ui.horizontal(|ui| {
188                        ui.label("Levels Directory:");
189                        if ui.text_edit_singleline(&mut path_str).lost_focus() {
190                            should_list_files = true;
191                        }
192                    });
193                    levels_directory.0 = path_str.into();
194
195                    let mk_files_index = || levels_directory.0.join("index.yoli");
196
197                    let save_index = |loaded_files_index: &[YoleckLevelIndexEntry]| {
198                        let index_file = mk_files_index();
199                        match fs::File::create(&index_file) {
200                            Ok(fd) => {
201                                let index =
202                                    YoleckLevelIndex::new(loaded_files_index.iter().cloned());
203                                serde_json::to_writer(fd, &index).unwrap();
204                            }
205                            Err(err) => {
206                                warn!("Cannot open {:?} - {}", index_file, err);
207                            }
208                        }
209                    };
210
211                    let save_existing = |filename: &str| -> io::Result<()> {
212                        let file_path = levels_directory.0.join(filename);
213                        info!("Saving current level to {:?}", file_path);
214                        let fd = fs::OpenOptions::new()
215                            .write(true)
216                            .create(false)
217                            .truncate(true)
218                            .open(file_path)?;
219                        serde_json::to_writer(fd, &gen_raw_level_file())?;
220                        Ok(())
221                    };
222
223                    if should_list_files {
224                        should_list_files = false;
225
226                        let editable_levels_update_result = fs::read_dir(&levels_directory.0)
227                            .and_then(|files| {
228                                editable_levels.levels = files
229                                    .filter_map(|file| {
230                                        let file = match file {
231                                            Ok(file) => file,
232                                            Err(err) => return Some(Err(err)),
233                                        };
234                                        if file.path().extension()
235                                            != Some(std::ffi::OsStr::new(EXTENSION_WITHOUT_DOT))
236                                        {
237                                            return None;
238                                        }
239                                        Some(Ok(file.file_name().to_string_lossy().into()))
240                                    })
241                                    .collect::<Result<_, _>>()?;
242                                Ok(())
243                            });
244
245                        loaded_files_index = editable_levels_update_result.and_then(|()| {
246                            let index_file = mk_files_index();
247                            let mut files_index: Vec<YoleckLevelIndexEntry> =
248                                match fs::File::open(&index_file) {
249                                    Ok(fd) => {
250                                        let index: YoleckLevelIndex = serde_json::from_reader(fd)?;
251                                        index.iter().cloned().collect()
252                                    }
253                                    Err(err) => {
254                                        warn!("Cannot open {:?} - {}", index_file, err);
255                                        Vec::new()
256                                    }
257                                };
258                            let mut existing_files: HashSet<String> = files_index
259                                .iter()
260                                .map(|file| file.filename.clone())
261                                .collect();
262                            for filename in editable_levels.names() {
263                                if !existing_files.remove(filename) {
264                                    files_index.push(YoleckLevelIndexEntry {
265                                        filename: filename.to_owned(),
266                                    });
267                                }
268                            }
269                            files_index.retain(|file| !existing_files.contains(&file.filename));
270                            save_index(&files_index);
271                            Ok(files_index)
272                        });
273                    }
274                    match &mut loaded_files_index {
275                        Ok(files) => {
276                            let mut swap_with_previous = None;
277                            egui::ScrollArea::vertical()
278                                .max_height(30.0)
279                                .show(ui, |ui| {
280                                    for (index, file) in files.iter().enumerate() {
281                                        let is_selected =
282                                            if let SelectedLevelFile::Existing(selected_name) =
283                                                &selected_level_file
284                                            {
285                                                *selected_name == file.filename
286                                            } else {
287                                                false
288                                            };
289                                        ui.horizontal(|ui| {
290                                            if ui
291                                                .add_enabled(0 < index, egui::Button::new("^"))
292                                                .clicked()
293                                            {
294                                                swap_with_previous = Some(index);
295                                            }
296                                            if ui
297                                                .add_enabled(
298                                                    index < files.len() - 1,
299                                                    egui::Button::new("v"),
300                                                )
301                                                .clicked()
302                                            {
303                                                swap_with_previous = Some(index + 1);
304                                            }
305                                            let yoleck = yoleck.as_mut();
306                                            let mut load_level = || {
307                                                clear_level(&mut commands);
308                                                let fd = fs::File::open(
309                                                    levels_directory.0.join(&file.filename),
310                                                )
311                                                .unwrap();
312                                                let level: serde_json::Value =
313                                                    serde_json::from_reader(fd).unwrap();
314                                                match upgrade_level_file(level) {
315                                                    Ok(level) => {
316                                                        let level: YoleckRawLevel =
317                                                            serde_json::from_value(level).unwrap();
318                                                        let level_asset_handle =
319                                                            level_assets.add(level);
320                                                        yoleck.level_being_edited = commands
321                                                            .spawn((
322                                                                YoleckLevelInEditor,
323                                                                YoleckLoadLevel(level_asset_handle),
324                                                            ))
325                                                            .id();
326                                                    }
327                                                    Err(err) => {
328                                                        warn!(
329                                                            "Cannot upgrade {:?} - {}",
330                                                            file.filename, err
331                                                        );
332                                                    }
333                                                }
334                                            };
335                                            if ui
336                                                .selectable_label(is_selected, &file.filename)
337                                                .clicked()
338                                            {
339                                                #[allow(clippy::collapsible_else_if)]
340                                                if !is_selected && !yoleck.level_needs_saving {
341                                                    selected_level_file =
342                                                        SelectedLevelFile::Existing(
343                                                            file.filename.clone(),
344                                                        );
345                                                    load_level();
346                                                }
347                                            }
348                                            if is_selected && yoleck.level_needs_saving {
349                                                if ui.button("SAVE").clicked() {
350                                                    save_existing(&file.filename).unwrap();
351                                                    yoleck.level_needs_saving = false;
352                                                }
353                                                if ui.button("REVERT").clicked() {
354                                                    load_level();
355                                                    yoleck.level_needs_saving = false;
356                                                }
357                                            }
358                                        });
359                                    }
360                                });
361                            if let Some(swap_with_previous) = swap_with_previous {
362                                files.swap(swap_with_previous, swap_with_previous - 1);
363                                save_index(files);
364                            }
365                            ui.horizontal(|ui| {
366                                #[allow(clippy::collapsible_else_if)]
367                                match &mut selected_level_file {
368                                    SelectedLevelFile::Unsaved(file_name) => {
369                                        ui.text_edit_singleline(file_name);
370                                        let button = ui.add_enabled(
371                                            !file_name.is_empty(),
372                                            egui::Button::new("Create"),
373                                        );
374                                        if button.clicked() {
375                                            if !file_name.ends_with(EXTENSION) {
376                                                file_name.push_str(EXTENSION);
377                                            }
378                                            let mut file_path = levels_directory.0.clone();
379                                            file_path.push(&file_name);
380                                            match fs::OpenOptions::new()
381                                                .write(true)
382                                                .create_new(true)
383                                                .open(&file_path)
384                                            {
385                                                Ok(fd) => {
386                                                    info!(
387                                                        "Saving current new level to {:?}",
388                                                        file_path
389                                                    );
390                                                    serde_json::to_writer(
391                                                        fd,
392                                                        &gen_raw_level_file(),
393                                                    )
394                                                    .unwrap();
395                                                    selected_level_file =
396                                                        SelectedLevelFile::Existing(
397                                                            file_name.to_owned(),
398                                                        );
399                                                    should_list_files = true;
400                                                    yoleck.level_needs_saving = false;
401                                                }
402                                                Err(err) => {
403                                                    warn!("Cannot open {:?} - {}", file_path, err);
404                                                }
405                                            }
406                                        }
407                                        if yoleck.level_needs_saving
408                                            && ui.button("Wipe Level").clicked()
409                                        {
410                                            clear_level(&mut commands);
411                                            yoleck.level_needs_saving = false;
412                                        }
413                                    }
414                                    SelectedLevelFile::Existing(_) => {
415                                        let button = ui.add_enabled(
416                                            !yoleck.level_needs_saving,
417                                            egui::Button::new("New Level"),
418                                        );
419                                        if button.clicked() {
420                                            clear_level(&mut commands);
421                                            selected_level_file =
422                                                SelectedLevelFile::Unsaved(String::new());
423                                            yoleck.level_being_edited = commands
424                                                .spawn((YoleckLevelInEditor, YoleckKeepLevel))
425                                                .id();
426                                        }
427                                    }
428                                }
429                            });
430                        }
431                        Err(err) => {
432                            ui.label(format!("Cannot read: {}", err));
433                        }
434                    }
435                });
436        }
437        system_state.apply(world);
438    }
439}