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}