1use std::any::TypeId;
2use std::sync::Arc;
3
4use bevy::ecs::system::SystemState;
5use bevy::platform::collections::{HashMap, HashSet};
6use bevy::prelude::*;
7use bevy::state::state::FreelyMutableState;
8use bevy_egui::egui;
9
10use crate::entity_management::{YoleckEntryHeader, YoleckRawEntry};
11use crate::exclusive_systems::{
12 YoleckActiveExclusiveSystem, YoleckEntityCreationExclusiveSystems,
13 YoleckExclusiveSystemDirective, YoleckExclusiveSystemsQueue,
14};
15use crate::knobs::YoleckKnobsCache;
16use crate::prelude::{YoleckComponent, YoleckUi};
17use crate::{
18 BoxedArc, YoleckBelongsToLevel, YoleckEditMarker, YoleckEditSystems,
19 YoleckEntityConstructionSpecs, YoleckInternalSchedule, YoleckManaged, YoleckState,
20};
21
22#[derive(States, Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
24pub enum YoleckEditorState {
25 #[default]
27 EditorActive,
28 GameActive,
30}
31
32pub struct YoleckSyncWithEditorState<T>
71where
72 T: 'static
73 + States
74 + FreelyMutableState
75 + Sync
76 + Send
77 + std::fmt::Debug
78 + Clone
79 + std::cmp::Eq
80 + std::hash::Hash,
81{
82 pub when_editor: T,
83 pub when_game: T,
84}
85
86impl<T> Plugin for YoleckSyncWithEditorState<T>
87where
88 T: 'static
89 + States
90 + FreelyMutableState
91 + Sync
92 + Send
93 + std::fmt::Debug
94 + Clone
95 + std::cmp::Eq
96 + std::hash::Hash,
97{
98 fn build(&self, app: &mut App) {
99 app.insert_state(self.when_editor.clone());
100 let when_editor = self.when_editor.clone();
101 let when_game = self.when_game.clone();
102 app.add_systems(
103 Update,
104 move |editor_state: Res<State<YoleckEditorState>>,
105 mut game_state: ResMut<NextState<T>>| {
106 game_state.set(match editor_state.get() {
107 YoleckEditorState::EditorActive => when_editor.clone(),
108 YoleckEditorState::GameActive => when_game.clone(),
109 });
110 },
111 );
112 }
113}
114
115#[derive(Debug, Event)]
120pub enum YoleckEditorEvent {
121 EntitySelected(Entity),
122 EntityDeselected(Entity),
123 EditedEntityPopulated(Entity),
124}
125
126enum YoleckDirectiveInner {
127 SetSelected(Option<Entity>),
128 ChangeSelectedStatus {
129 entity: Entity,
130 force_to: Option<bool>,
131 },
132 PassToEntity(Entity, TypeId, BoxedArc),
133 SpawnEntity {
134 level: Entity,
135 type_name: String,
136 data: serde_json::Value,
137 select_created_entity: bool,
138 #[allow(clippy::type_complexity)]
139 modify_exclusive_systems:
140 Option<Box<dyn Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue)>>,
141 },
142}
143
144#[derive(Event)]
146pub struct YoleckDirective(YoleckDirectiveInner);
147
148impl YoleckDirective {
149 pub fn pass_to_entity<T: 'static + Send + Sync>(entity: Entity, data: T) -> Self {
155 Self(YoleckDirectiveInner::PassToEntity(
156 entity,
157 TypeId::of::<T>(),
158 Arc::new(data),
159 ))
160 }
161
162 pub fn set_selected(entity: Option<Entity>) -> Self {
164 Self(YoleckDirectiveInner::SetSelected(entity))
165 }
166
167 pub fn toggle_selected(entity: Entity) -> Self {
169 Self(YoleckDirectiveInner::ChangeSelectedStatus {
170 entity,
171 force_to: None,
172 })
173 }
174
175 pub fn spawn_entity(
207 level: Entity,
208 type_name: impl ToString,
209 select_created_entity: bool,
210 ) -> SpawnEntityBuilder {
211 SpawnEntityBuilder {
212 level,
213 type_name: type_name.to_string(),
214 select_created_entity,
215 data: Default::default(),
216 modify_exclusive_systems: None,
217 }
218 }
219}
220
221pub struct SpawnEntityBuilder {
222 level: Entity,
223 type_name: String,
224 select_created_entity: bool,
225 data: HashMap<&'static str, serde_json::Value>,
226 #[allow(clippy::type_complexity)]
227 modify_exclusive_systems: Option<Box<dyn Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue)>>,
228}
229
230impl SpawnEntityBuilder {
231 pub fn with<T: YoleckComponent>(mut self, component: T) -> Self {
233 self.data.insert(
234 T::KEY,
235 serde_json::to_value(component).expect("should always work"),
236 );
237 self
238 }
239
240 pub fn modify_exclusive_systems(
242 mut self,
243 dlg: impl 'static + Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue),
244 ) -> Self {
245 self.modify_exclusive_systems = Some(Box::new(dlg));
246 self
247 }
248}
249
250impl From<SpawnEntityBuilder> for YoleckDirective {
251 fn from(value: SpawnEntityBuilder) -> Self {
252 YoleckDirective(YoleckDirectiveInner::SpawnEntity {
253 level: value.level,
254 type_name: value.type_name,
255 data: serde_json::to_value(value.data).expect("should always work"),
256 select_created_entity: value.select_created_entity,
257 modify_exclusive_systems: value.modify_exclusive_systems,
258 })
259 }
260}
261
262#[derive(Resource)]
263pub struct YoleckPassedData(pub(crate) HashMap<Entity, HashMap<TypeId, BoxedArc>>);
264
265impl YoleckPassedData {
266 pub fn get<T: 'static>(&self, entity: Entity) -> Option<&T> {
289 Some(
290 self.0
291 .get(&entity)?
292 .get(&TypeId::of::<T>())?
293 .downcast_ref()
294 .expect("Passed data TypeId must be correct"),
295 )
296 }
297}
298
299fn format_caption(entity: Entity, yoleck_managed: &YoleckManaged) -> String {
300 if yoleck_managed.name.is_empty() {
301 format!("{} {:?}", yoleck_managed.type_name, entity)
302 } else {
303 format!(
304 "{} ({} {:?})",
305 yoleck_managed.name, yoleck_managed.type_name, entity
306 )
307 }
308}
309
310pub fn new_entity_section(world: &mut World) -> impl FnMut(&mut World, &mut egui::Ui) {
312 let mut system_state = SystemState::<(
313 Res<YoleckEntityConstructionSpecs>,
314 Res<YoleckState>,
315 Res<State<YoleckEditorState>>,
316 EventWriter<YoleckDirective>,
317 Option<Res<YoleckActiveExclusiveSystem>>,
318 )>::new(world);
319
320 move |world, ui| {
321 let (construction_specs, yoleck, editor_state, mut writer, active_exclusive_system) =
322 system_state.get_mut(world);
323 if active_exclusive_system.is_some() {
324 return;
325 }
326
327 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
328 return;
329 }
330
331 let button_response = ui.button("Add New Entity");
332
333 egui::Popup::menu(&button_response).show(|ui| {
334 for entity_type in construction_specs.entity_types.iter() {
335 if ui.button(&entity_type.name).clicked() {
336 writer.write(YoleckDirective(YoleckDirectiveInner::SpawnEntity {
337 level: yoleck.level_being_edited,
338 type_name: entity_type.name.clone(),
339 data: serde_json::Value::Object(Default::default()),
340 select_created_entity: true,
341 modify_exclusive_systems: None,
342 }));
343 }
344 }
345 });
346
347 system_state.apply(world);
348 }
349}
350
351pub fn entity_selection_section(world: &mut World) -> impl FnMut(&mut World, &mut egui::Ui) {
353 let mut filter_custom_name = String::new();
354 let mut filter_types = HashSet::<String>::new();
355
356 let mut system_state = SystemState::<(
357 Res<YoleckEntityConstructionSpecs>,
358 Query<(Entity, &YoleckManaged, Option<&YoleckEditMarker>)>,
359 Res<State<YoleckEditorState>>,
360 EventWriter<YoleckDirective>,
361 Option<Res<YoleckActiveExclusiveSystem>>,
362 )>::new(world);
363
364 move |world, ui| {
365 let (
366 construction_specs,
367 yoleck_managed_query,
368 editor_state,
369 mut writer,
370 active_exclusive_system,
371 ) = system_state.get_mut(world);
372 if active_exclusive_system.is_some() {
373 return;
374 }
375
376 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
377 return;
378 }
379
380 egui::CollapsingHeader::new("Select").show(ui, |ui| {
381 egui::CollapsingHeader::new("Filter").show(ui, |ui| {
382 ui.horizontal(|ui| {
383 ui.label("By Name:");
384 ui.text_edit_singleline(&mut filter_custom_name);
385 });
386 for entity_type in construction_specs.entity_types.iter() {
387 let mut should_show = filter_types.contains(&entity_type.name);
388 if ui.checkbox(&mut should_show, &entity_type.name).changed() {
389 if should_show {
390 filter_types.insert(entity_type.name.clone());
391 } else {
392 filter_types.remove(&entity_type.name);
393 }
394 }
395 }
396 });
397 for (entity, yoleck_managed, edit_marker) in yoleck_managed_query.iter() {
398 if !filter_types.is_empty() && !filter_types.contains(&yoleck_managed.type_name) {
399 continue;
400 }
401 if !yoleck_managed.name.contains(filter_custom_name.as_str()) {
402 continue;
403 }
404 let is_selected = edit_marker.is_some();
405 if ui
406 .selectable_label(is_selected, format_caption(entity, yoleck_managed))
407 .clicked()
408 {
409 if ui.input(|input| input.modifiers.shift) {
410 writer.write(YoleckDirective::toggle_selected(entity));
411 } else if is_selected {
412 writer.write(YoleckDirective::set_selected(None));
413 } else {
414 writer.write(YoleckDirective::set_selected(Some(entity)));
415 }
416 }
417 }
418 });
419 }
420}
421
422pub fn entity_editing_section(world: &mut World) -> impl FnMut(&mut World, &mut egui::Ui) {
424 let mut system_state = SystemState::<(
425 ResMut<YoleckState>,
426 Query<(Entity, &mut YoleckManaged), With<YoleckEditMarker>>,
427 Query<Entity, With<YoleckEditMarker>>,
428 EventReader<YoleckDirective>,
429 Commands,
430 Res<State<YoleckEditorState>>,
431 EventWriter<YoleckEditorEvent>,
432 ResMut<YoleckKnobsCache>,
433 Option<Res<YoleckActiveExclusiveSystem>>,
434 ResMut<YoleckExclusiveSystemsQueue>,
435 Res<YoleckEntityCreationExclusiveSystems>,
436 )>::new(world);
437
438 let mut previously_edited_entity: Option<Entity> = None;
439 let mut new_entity_created_this_frame = false;
440
441 move |world, ui| {
442 let mut passed_data = YoleckPassedData(Default::default());
443 {
444 let (
445 mut yoleck,
446 mut yoleck_managed_query,
447 yoleck_edited_query,
448 mut directives_reader,
449 mut commands,
450 editor_state,
451 mut writer,
452 mut knobs_cache,
453 active_exclusive_system,
454 mut exclusive_systems_queue,
455 entity_creation_exclusive_systems,
456 ) = system_state.get_mut(world);
457
458 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
459 return;
460 }
461
462 let mut data_passed_to_entities: HashMap<Entity, HashMap<TypeId, BoxedArc>> =
463 Default::default();
464 for directive in directives_reader.read() {
465 match &directive.0 {
466 YoleckDirectiveInner::PassToEntity(entity, type_id, data) => {
467 if false {
468 data_passed_to_entities
469 .entry(*entity)
470 .or_default()
471 .insert(*type_id, data.clone());
472 }
473 passed_data
474 .0
475 .entry(*entity)
476 .or_default()
477 .insert(*type_id, data.clone());
478 }
479 YoleckDirectiveInner::SetSelected(entity) => {
480 if active_exclusive_system.is_some() {
481 continue;
483 }
484 if let Some(entity) = entity {
485 let mut already_selected = false;
486 for entity_to_deselect in yoleck_edited_query.iter() {
487 if entity_to_deselect == *entity {
488 already_selected = true;
489 } else {
490 commands
491 .entity(entity_to_deselect)
492 .remove::<YoleckEditMarker>();
493 writer.write(YoleckEditorEvent::EntityDeselected(
494 entity_to_deselect,
495 ));
496 }
497 }
498 if !already_selected {
499 commands.entity(*entity).insert(YoleckEditMarker);
500 writer.write(YoleckEditorEvent::EntitySelected(*entity));
501 }
502 } else {
503 for entity_to_deselect in yoleck_edited_query.iter() {
504 commands
505 .entity(entity_to_deselect)
506 .remove::<YoleckEditMarker>();
507 writer
508 .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));
509 }
510 }
511 }
512 YoleckDirectiveInner::ChangeSelectedStatus { entity, force_to } => {
513 if active_exclusive_system.is_some() {
514 continue;
516 }
517 match (force_to, yoleck_edited_query.contains(*entity)) {
518 (Some(true), true) | (Some(false), false) => {
519 }
521 (None, false) | (Some(true), false) => {
522 commands.entity(*entity).insert(YoleckEditMarker);
524 writer.write(YoleckEditorEvent::EntitySelected(*entity));
525 }
526 (None, true) | (Some(false), true) => {
527 commands.entity(*entity).remove::<YoleckEditMarker>();
529 writer.write(YoleckEditorEvent::EntityDeselected(*entity));
530 }
531 }
532 }
533 YoleckDirectiveInner::SpawnEntity {
534 level,
535 type_name,
536 data,
537 select_created_entity,
538 modify_exclusive_systems: override_exclusive_systems,
539 } => {
540 if active_exclusive_system.is_some() {
541 continue;
542 }
543 let mut cmd = commands.spawn((
544 YoleckRawEntry {
545 header: YoleckEntryHeader {
546 type_name: type_name.clone(),
547 name: "".to_owned(),
548 uuid: None,
549 },
550 data: data.clone(),
551 },
552 YoleckBelongsToLevel { level: *level },
553 ));
554 if *select_created_entity {
555 writer.write(YoleckEditorEvent::EntitySelected(cmd.id()));
556 cmd.insert(YoleckEditMarker);
557 for entity_to_deselect in yoleck_edited_query.iter() {
558 commands
559 .entity(entity_to_deselect)
560 .remove::<YoleckEditMarker>();
561 writer
562 .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));
563 }
564 *exclusive_systems_queue =
565 entity_creation_exclusive_systems.create_queue();
566 if let Some(override_exclusive_systems) = override_exclusive_systems {
567 override_exclusive_systems(exclusive_systems_queue.as_mut());
568 }
569 new_entity_created_this_frame = true;
570 }
571 yoleck.level_needs_saving = true;
572 }
573 }
574 }
575
576 let entity_being_edited;
577 if let Ok((entity, mut yoleck_managed)) = yoleck_managed_query.single_mut() {
578 entity_being_edited = Some(entity);
579 ui.horizontal(|ui| {
580 ui.heading(format!(
581 "Editing {}",
582 format_caption(entity, &yoleck_managed)
583 ));
584 if ui.button("Delete").clicked() {
585 commands.entity(entity).despawn();
586 writer.write(YoleckEditorEvent::EntityDeselected(entity));
587 yoleck.level_needs_saving = true;
588 }
589 });
590 ui.horizontal(|ui| {
591 ui.label("Custom Name:");
592 ui.text_edit_singleline(&mut yoleck_managed.name);
593 });
594 } else {
595 entity_being_edited = None;
596 }
597
598 if previously_edited_entity != entity_being_edited {
599 previously_edited_entity = entity_being_edited;
600 for knob_entity in knobs_cache.drain() {
601 commands.entity(knob_entity).despawn();
602 }
603 } else {
604 knobs_cache.clean_untouched(|knob_entity| {
605 commands.entity(knob_entity).despawn();
606 });
607 }
608 }
609 system_state.apply(world);
610
611 let frame = egui::Frame::new();
612 let mut prepared = frame.begin(ui);
613 let content_ui = std::mem::replace(
614 &mut prepared.content_ui,
615 ui.new_child(egui::UiBuilder {
616 max_rect: Some(ui.max_rect()),
617 layout: Some(*ui.layout()), ..Default::default()
619 }),
620 );
621 world.insert_resource(YoleckUi(content_ui));
622 world.insert_resource(passed_data);
623
624 enum ActiveExclusiveSystemStatus {
625 DidNotRun,
626 StillRunningSame,
627 JustFinishedRunning,
628 }
629
630 let behavior_for_exclusive_system = if let Some(mut active_exclusive_system) =
631 world.remove_resource::<YoleckActiveExclusiveSystem>()
632 {
633 let result = active_exclusive_system.0.run((), world);
634 match result {
635 YoleckExclusiveSystemDirective::Listening => {
636 world.insert_resource(active_exclusive_system);
637 ActiveExclusiveSystemStatus::StillRunningSame
638 }
639 YoleckExclusiveSystemDirective::Finished => {
640 ActiveExclusiveSystemStatus::JustFinishedRunning
641 }
642 }
643 } else {
644 ActiveExclusiveSystemStatus::DidNotRun
645 };
646
647 let should_run_regular_systems = match behavior_for_exclusive_system {
648 ActiveExclusiveSystemStatus::DidNotRun => loop {
649 let Some(mut new_exclusive_system) = world
650 .resource_mut::<YoleckExclusiveSystemsQueue>()
651 .pop_front()
652 else {
653 break true;
654 };
655 new_exclusive_system.initialize(world);
656 let first_run_result = new_exclusive_system.run((), world);
657 if new_entity_created_this_frame
658 || matches!(first_run_result, YoleckExclusiveSystemDirective::Listening)
659 {
660 world.insert_resource(YoleckActiveExclusiveSystem(new_exclusive_system));
661 break false;
662 }
663 },
664 ActiveExclusiveSystemStatus::StillRunningSame => false,
665 ActiveExclusiveSystemStatus::JustFinishedRunning => false,
666 };
667
668 if should_run_regular_systems {
669 world.resource_scope(|world, mut yoleck_edit_systems: Mut<YoleckEditSystems>| {
670 yoleck_edit_systems.run_systems(world);
671 });
672 }
673 let YoleckUi(content_ui) = world
674 .remove_resource()
675 .expect("The YoleckUi resource was put in the world by this very function");
676 world.remove_resource::<YoleckPassedData>();
677 prepared.content_ui = content_ui;
678 prepared.end(ui);
679
680 world.run_schedule(YoleckInternalSchedule::UpdateManagedDataFromComponents);
682 }
683}