1use std::path::PathBuf;
2use std::{fs, io};
3
4use bevy::platform::collections::HashSet;
5use bevy::prelude::*;
6use bevy_egui::egui;
7
8use crate::editor_panels::YoleckPanelUi;
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, YoleckPlaytestLevel, YoleckRawLevel,
21 YoleckState,
22};
23
24const EXTENSION: &str = ".yol";
25const EXTENSION_WITHOUT_DOT: &str = "yol";
26
27#[derive(Resource)]
41pub struct YoleckEditorLevelsDirectoryPath(pub PathBuf);
42
43#[derive(Debug)]
44enum SelectedLevelFile {
45 Unsaved(String),
46 Existing(String),
47}
48
49#[doc(hidden)]
50pub struct LevelFilesManagerTopSectionLocals {
51 should_list_files: bool,
52 loaded_files_index: io::Result<Vec<YoleckLevelIndexEntry>>,
53 file_popup_open: bool,
54 selected_level_file: SelectedLevelFile,
55}
56
57impl Default for LevelFilesManagerTopSectionLocals {
58 fn default() -> Self {
59 Self {
60 should_list_files: true,
61 loaded_files_index: Ok(vec![]),
62 file_popup_open: false,
63 selected_level_file: SelectedLevelFile::Unsaved(String::new()),
64 }
65 }
66}
67
68#[allow(clippy::too_many_arguments)]
69pub fn level_files_manager_top_section(
70 mut ui: ResMut<YoleckPanelUi>,
71 mut locals: Local<LevelFilesManagerTopSectionLocals>,
72 mut commands: Commands,
73 mut yoleck: ResMut<YoleckState>,
74 mut levels_directory: ResMut<YoleckEditorLevelsDirectoryPath>,
75 mut editable_levels: ResMut<YoleckEditableLevels>,
76 construction_specs: Res<YoleckEntityConstructionSpecs>,
77 yoleck_managed_query: Query<(&YoleckManaged, Option<&YoleckEntityUuid>)>,
78 keep_levels_query: Query<Entity, With<YoleckKeepLevel>>,
79 editor_state: Res<State<YoleckEditorState>>,
80 mut knobs_cache: ResMut<YoleckKnobsCache>,
81 mut level_assets: ResMut<Assets<YoleckRawLevel>>,
82 entity_upgrading: Option<Res<YoleckEntityUpgrading>>,
83 active_exclusive_system: Option<Res<YoleckActiveExclusiveSystem>>,
84) -> Result {
85 if active_exclusive_system.is_some() {
86 return Ok(());
87 }
88
89 let LevelFilesManagerTopSectionLocals {
90 should_list_files,
91 loaded_files_index,
92 file_popup_open,
93 selected_level_file,
94 } = &mut *locals;
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 = &construction_specs.component_handlers[component];
122 Some((
123 handler.key().to_owned(),
124 handler.serialize(component_data.as_ref()),
125 ))
126 })
127 .collect()
128 } else {
129 error!(
130 "Entity type {:?} is not registered",
131 yoleck_managed.type_name
132 );
133 Default::default()
134 }
135 },
136 })
137 })
138 };
139
140 if matches!(editor_state.get(), YoleckEditorState::EditorActive) {
141 enum LevelManagementAction {
142 DoNothing,
143 ClearLevel,
144 LoadLevel { filename: String },
145 SaveExisting { filename: String },
146 }
147
148 let mut level_management_action = LevelManagementAction::DoNothing;
149
150 let file_button_response = ui.button("File");
151 if file_button_response.clicked() {
152 *file_popup_open = !*file_popup_open;
153 }
154
155 if *file_popup_open {
156 let button_rect = file_button_response.rect;
157 let area_response = egui::Area::new(egui::Id::new("file_popup_area"))
158 .order(egui::Order::Foreground)
159 .fixed_pos(egui::pos2(button_rect.left(), button_rect.bottom() + 2.0))
160 .show(ui.ctx(), |ui| {
161 egui::Frame::popup(ui.style()).show(ui, |ui| {
162 ui.set_min_width(400.0);
163 ui.set_max_width(600.0);
164
165 let mut path_str = levels_directory.0.to_string_lossy().to_string();
166 ui.horizontal(|ui| {
167 ui.label("Levels Directory:");
168 if ui.text_edit_singleline(&mut path_str).lost_focus() {
169 *should_list_files = true;
170 }
171 });
172 levels_directory.0 = path_str.into();
173
174 let mk_files_index = || levels_directory.0.join("index.yoli");
175
176 let save_index = |loaded_files_index: &[YoleckLevelIndexEntry]| {
177 let index_file = mk_files_index();
178 match fs::File::create(&index_file) {
179 Ok(fd) => {
180 let index =
181 YoleckLevelIndex::new(loaded_files_index.iter().cloned());
182 serde_json::to_writer(fd, &index).unwrap();
183 }
184 Err(err) => {
185 warn!("Cannot open {:?} - {}", index_file, err);
186 }
187 }
188 };
189
190 if *should_list_files {
191 *should_list_files = false;
192
193 let editable_levels_update_result = fs::read_dir(&levels_directory.0)
194 .and_then(|files| {
195 editable_levels.levels = files
196 .filter_map(|file| {
197 let file = match file {
198 Ok(file) => file,
199 Err(err) => return Some(Err(err)),
200 };
201 if file.path().extension()
202 != Some(std::ffi::OsStr::new(EXTENSION_WITHOUT_DOT))
203 {
204 return None;
205 }
206 Some(Ok(file.file_name().to_string_lossy().into()))
207 })
208 .collect::<Result<_, _>>()?;
209 Ok(())
210 });
211
212 *loaded_files_index = editable_levels_update_result.and_then(|()| {
213 let index_file = mk_files_index();
214 let mut files_index: Vec<YoleckLevelIndexEntry> =
215 match fs::File::open(&index_file) {
216 Ok(fd) => {
217 let index: YoleckLevelIndex =
218 serde_json::from_reader(fd)?;
219 index.iter().cloned().collect()
220 }
221 Err(err) => {
222 warn!("Cannot open {:?} - {}", index_file, err);
223 Vec::new()
224 }
225 };
226 let mut existing_files: HashSet<String> = files_index
227 .iter()
228 .map(|file| file.filename.clone())
229 .collect();
230 for filename in editable_levels.names() {
231 if !existing_files.remove(filename) {
232 files_index.push(YoleckLevelIndexEntry {
233 filename: filename.to_owned(),
234 });
235 }
236 }
237 files_index.retain(|file| !existing_files.contains(&file.filename));
238 save_index(&files_index);
239 Ok(files_index)
240 });
241 }
242
243 match &mut *loaded_files_index {
244 Ok(files) => {
245 let mut swap_with_previous = None;
246 egui::ScrollArea::vertical()
247 .max_height(200.0)
248 .show(ui, |ui| {
249 for (index, file) in files.iter().enumerate() {
250 let is_selected =
251 if let SelectedLevelFile::Existing(selected_name) =
252 &*selected_level_file
253 {
254 *selected_name == file.filename
255 } else {
256 false
257 };
258 ui.horizontal(|ui| {
259 if ui
260 .add_enabled(0 < index, egui::Button::new("^"))
261 .clicked()
262 {
263 swap_with_previous = Some(index);
264 }
265 if ui
266 .add_enabled(
267 index < files.len() - 1,
268 egui::Button::new("v"),
269 )
270 .clicked()
271 {
272 swap_with_previous = Some(index + 1);
273 }
274 let yoleck = yoleck.as_mut();
275 if ui
276 .selectable_label(is_selected, &file.filename)
277 .clicked()
278 {
279 #[allow(clippy::collapsible_else_if)]
280 if !is_selected && !yoleck.level_needs_saving {
281 *selected_level_file =
282 SelectedLevelFile::Existing(
283 file.filename.clone(),
284 );
285 level_management_action =
286 LevelManagementAction::LoadLevel {
287 filename: file.filename.clone(),
288 };
289 }
290 }
291 });
292 }
293 });
294 if let Some(swap_with_previous) = swap_with_previous {
295 files.swap(swap_with_previous, swap_with_previous - 1);
296 save_index(files);
297 }
298 ui.horizontal(|ui| {
299 #[allow(clippy::collapsible_else_if)]
300 match &mut *selected_level_file {
301 SelectedLevelFile::Unsaved(file_name) => {
302 ui.text_edit_singleline(file_name);
303 let button = ui.add_enabled(
304 !file_name.is_empty(),
305 egui::Button::new("Create"),
306 );
307 if button.clicked() {
308 if !file_name.ends_with(EXTENSION) {
309 file_name.push_str(EXTENSION);
310 }
311 let mut file_path = levels_directory.0.clone();
312 file_path.push(&file_name);
313 match fs::OpenOptions::new()
314 .write(true)
315 .create_new(true)
316 .open(&file_path)
317 {
318 Ok(fd) => {
319 info!(
320 "Saving current new level to {:?}",
321 file_path
322 );
323 serde_json::to_writer(
324 fd,
325 &gen_raw_level_file(),
326 )
327 .unwrap();
328 *selected_level_file =
329 SelectedLevelFile::Existing(
330 file_name.to_owned(),
331 );
332 *should_list_files = true;
333 yoleck.level_needs_saving = false;
334 }
335 Err(err) => {
336 warn!(
337 "Cannot open {:?} - {}",
338 file_path, err
339 );
340 }
341 }
342 }
343 }
344 SelectedLevelFile::Existing(_) => {
345 let button = ui.add_enabled(
346 !yoleck.level_needs_saving,
347 egui::Button::new("New Level"),
348 );
349 if button.clicked() {
350 level_management_action =
351 LevelManagementAction::ClearLevel;
352 *selected_level_file =
353 SelectedLevelFile::Unsaved(String::new());
354 yoleck.level_being_edited = commands
355 .spawn((YoleckLevelInEditor, YoleckKeepLevel))
356 .id();
357 }
358 }
359 }
360 });
361 }
362 Err(err) => {
363 ui.label(format!("Cannot read: {err}"));
364 }
365 }
366 });
367 });
368
369 if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
370 *file_popup_open = false;
371 }
372
373 if ui.input(|i| i.pointer.primary_clicked())
374 && let Some(pos) = ui.input(|i| i.pointer.interact_pos())
375 && !area_response.response.rect.contains(pos)
376 && !button_rect.contains(pos)
377 {
378 *file_popup_open = false;
379 }
380 }
381
382 match selected_level_file {
383 SelectedLevelFile::Unsaved(_) => {
384 ui.add_enabled_ui(yoleck.level_needs_saving, |ui| {
385 if ui.button("Wipe Level").clicked() {
386 level_management_action = LevelManagementAction::ClearLevel;
387 }
388 });
389 }
390 SelectedLevelFile::Existing(filename) => {
391 ui.label(filename.as_str());
392 ui.add_enabled_ui(yoleck.level_needs_saving, |ui| {
393 if ui.button("SAVE").clicked() {
394 level_management_action = LevelManagementAction::SaveExisting {
395 filename: filename.clone(),
396 }
397 }
398 if ui.button("REVERT").clicked() {
399 level_management_action = LevelManagementAction::LoadLevel {
400 filename: filename.clone(),
401 };
402 }
403 });
404 }
405 }
406
407 match level_management_action {
408 LevelManagementAction::DoNothing => {}
409 LevelManagementAction::ClearLevel => {
410 for level_entity in keep_levels_query.iter() {
411 commands.entity(level_entity).despawn();
412 }
413 for knob_entity in knobs_cache.drain() {
414 commands.entity(knob_entity).despawn();
415 }
416
417 yoleck.level_needs_saving = false;
418 }
419 LevelManagementAction::LoadLevel { filename } => {
420 for level_entity in keep_levels_query.iter() {
422 commands.entity(level_entity).despawn();
423 }
424 for knob_entity in knobs_cache.drain() {
425 commands.entity(knob_entity).despawn();
426 }
427
428 yoleck.level_needs_saving = false;
429
430 let fd = fs::File::open(levels_directory.0.join(&filename)).unwrap();
431 let level: serde_json::Value = serde_json::from_reader(fd).unwrap();
432 match upgrade_level_file(level) {
433 Ok(level) => {
434 let level: YoleckRawLevel = serde_json::from_value(level).unwrap();
435 let level_asset_handle = level_assets.add(level);
436 yoleck.level_being_edited = commands
437 .spawn((YoleckLevelInEditor, YoleckLoadLevel(level_asset_handle)))
438 .id();
439 }
440 Err(err) => {
441 warn!("Cannot upgrade {:?} - {}", filename, err);
442 }
443 }
444 }
445 LevelManagementAction::SaveExisting { filename } => {
446 let file_path = levels_directory.0.join(filename);
447 info!("Saving current level to {:?}", file_path);
448 let fd = fs::OpenOptions::new()
449 .write(true)
450 .create(false)
451 .truncate(true)
452 .open(file_path)?;
453 serde_json::to_writer(fd, &gen_raw_level_file())?;
454 yoleck.level_needs_saving = false;
455 }
456 }
457 }
458
459 Ok(())
460}
461
462#[allow(clippy::too_many_arguments)]
464pub fn playtest_buttons_section(
465 mut ui: ResMut<YoleckPanelUi>,
466 mut commands: Commands,
467 mut yoleck: ResMut<YoleckState>,
468 mut playtest_level: ResMut<YoleckPlaytestLevel>,
469 construction_specs: Res<YoleckEntityConstructionSpecs>,
470 yoleck_managed_query: Query<(&YoleckManaged, Option<&YoleckEntityUuid>)>,
471 keep_levels_query: Query<Entity, With<YoleckKeepLevel>>,
472 mut next_editor_state: ResMut<NextState<YoleckEditorState>>,
473 mut knobs_cache: ResMut<YoleckKnobsCache>,
474 mut level_assets: ResMut<Assets<YoleckRawLevel>>,
475 entity_upgrading: Option<Res<YoleckEntityUpgrading>>,
476) -> Result {
477 let gen_raw_level_file = || {
478 let app_format_version = if let Some(entity_upgrading) = &entity_upgrading {
479 entity_upgrading.app_format_version
480 } else {
481 0
482 };
483 YoleckRawLevel::new(app_format_version, {
484 yoleck_managed_query
485 .iter()
486 .map(|(yoleck_managed, entity_uuid)| YoleckRawEntry {
487 header: YoleckEntryHeader {
488 type_name: yoleck_managed.type_name.clone(),
489 name: yoleck_managed.name.clone(),
490 uuid: entity_uuid.map(|entity_uuid| entity_uuid.get()),
491 },
492 data: {
493 if let Some(entity_type_info) =
494 construction_specs.get_entity_type_info(&yoleck_managed.type_name)
495 {
496 entity_type_info
497 .components
498 .iter()
499 .filter_map(|component| {
500 let component_data =
501 yoleck_managed.components_data.get(component)?;
502 let handler = &construction_specs.component_handlers[component];
503 Some((
504 handler.key().to_owned(),
505 handler.serialize(component_data.as_ref()),
506 ))
507 })
508 .collect()
509 } else {
510 error!(
511 "Entity type {:?} is not registered",
512 yoleck_managed.type_name
513 );
514 Default::default()
515 }
516 },
517 })
518 })
519 };
520
521 let mut clear_level = |commands: &mut Commands| {
522 for level_entity in keep_levels_query.iter() {
523 commands.entity(level_entity).despawn();
524 }
525 for knob_entity in knobs_cache.drain() {
526 commands.entity(knob_entity).despawn();
527 }
528 };
529
530 if let Some(level) = &playtest_level.0 {
531 let finish_playtest_response = ui.button("Finish Playtest");
532 if ui.button("Restart Playtest").clicked() {
533 clear_level(&mut commands);
534 let level_asset_handle = level_assets.add(level.clone());
535 yoleck.level_being_edited = commands
536 .spawn((YoleckLevelInPlaytest, YoleckLoadLevel(level_asset_handle)))
537 .id();
538 }
539 if finish_playtest_response.clicked() {
540 clear_level(&mut commands);
541 next_editor_state.set(YoleckEditorState::EditorActive);
542 let level_asset_handle = level_assets.add(level.clone());
543 yoleck.level_being_edited = commands
544 .spawn((YoleckLevelInEditor, YoleckLoadLevel(level_asset_handle)))
545 .id();
546 playtest_level.0 = None;
547 }
548 } else if ui.button("Playtest").clicked() {
549 let level = gen_raw_level_file();
550 clear_level(&mut commands);
551 next_editor_state.set(YoleckEditorState::GameActive);
552 let level_asset_handle = level_assets.add(level.clone());
553 yoleck.level_being_edited = commands
554 .spawn((YoleckLevelInPlaytest, YoleckLoadLevel(level_asset_handle)))
555 .id();
556 playtest_level.0 = Some(level);
557 }
558
559 ui.separator();
560 Ok(())
561}