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}