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