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 popup_id = ui.make_persistent_id("add_new_entity_popup_id");
332 let button_response = ui.button("Add New Entity");
333 if button_response.clicked() {
334 ui.memory_mut(|memory| memory.toggle_popup(popup_id));
335 }
336
337 egui::popup_below_widget(
338 ui,
339 popup_id,
340 &button_response,
341 egui::PopupCloseBehavior::CloseOnClickOutside,
342 |ui| {
343 for entity_type in construction_specs.entity_types.iter() {
344 if ui.button(&entity_type.name).clicked() {
345 writer.write(YoleckDirective(YoleckDirectiveInner::SpawnEntity {
346 level: yoleck.level_being_edited,
347 type_name: entity_type.name.clone(),
348 data: serde_json::Value::Object(Default::default()),
349 select_created_entity: true,
350 modify_exclusive_systems: None,
351 }));
352 ui.memory_mut(|memory| memory.toggle_popup(popup_id));
353 }
354 }
355 },
356 );
357
358 system_state.apply(world);
359 }
360}
361
362pub fn entity_selection_section(world: &mut World) -> impl FnMut(&mut World, &mut egui::Ui) {
364 let mut filter_custom_name = String::new();
365 let mut filter_types = HashSet::<String>::new();
366
367 let mut system_state = SystemState::<(
368 Res<YoleckEntityConstructionSpecs>,
369 Query<(Entity, &YoleckManaged, Option<&YoleckEditMarker>)>,
370 Res<State<YoleckEditorState>>,
371 EventWriter<YoleckDirective>,
372 Option<Res<YoleckActiveExclusiveSystem>>,
373 )>::new(world);
374
375 move |world, ui| {
376 let (
377 construction_specs,
378 yoleck_managed_query,
379 editor_state,
380 mut writer,
381 active_exclusive_system,
382 ) = system_state.get_mut(world);
383 if active_exclusive_system.is_some() {
384 return;
385 }
386
387 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
388 return;
389 }
390
391 egui::CollapsingHeader::new("Select").show(ui, |ui| {
392 egui::CollapsingHeader::new("Filter").show(ui, |ui| {
393 ui.horizontal(|ui| {
394 ui.label("By Name:");
395 ui.text_edit_singleline(&mut filter_custom_name);
396 });
397 for entity_type in construction_specs.entity_types.iter() {
398 let mut should_show = filter_types.contains(&entity_type.name);
399 if ui.checkbox(&mut should_show, &entity_type.name).changed() {
400 if should_show {
401 filter_types.insert(entity_type.name.clone());
402 } else {
403 filter_types.remove(&entity_type.name);
404 }
405 }
406 }
407 });
408 for (entity, yoleck_managed, edit_marker) in yoleck_managed_query.iter() {
409 if !filter_types.is_empty() && !filter_types.contains(&yoleck_managed.type_name) {
410 continue;
411 }
412 if !yoleck_managed.name.contains(filter_custom_name.as_str()) {
413 continue;
414 }
415 let is_selected = edit_marker.is_some();
416 if ui
417 .selectable_label(is_selected, format_caption(entity, yoleck_managed))
418 .clicked()
419 {
420 if ui.input(|input| input.modifiers.shift) {
421 writer.write(YoleckDirective::toggle_selected(entity));
422 } else if is_selected {
423 writer.write(YoleckDirective::set_selected(None));
424 } else {
425 writer.write(YoleckDirective::set_selected(Some(entity)));
426 }
427 }
428 }
429 });
430 }
431}
432
433pub fn entity_editing_section(world: &mut World) -> impl FnMut(&mut World, &mut egui::Ui) {
435 let mut system_state = SystemState::<(
436 ResMut<YoleckState>,
437 Query<(Entity, &mut YoleckManaged), With<YoleckEditMarker>>,
438 Query<Entity, With<YoleckEditMarker>>,
439 EventReader<YoleckDirective>,
440 Commands,
441 Res<State<YoleckEditorState>>,
442 EventWriter<YoleckEditorEvent>,
443 ResMut<YoleckKnobsCache>,
444 Option<Res<YoleckActiveExclusiveSystem>>,
445 ResMut<YoleckExclusiveSystemsQueue>,
446 Res<YoleckEntityCreationExclusiveSystems>,
447 )>::new(world);
448
449 let mut previously_edited_entity: Option<Entity> = None;
450 let mut new_entity_created_this_frame = false;
451
452 move |world, ui| {
453 let mut passed_data = YoleckPassedData(Default::default());
454 {
455 let (
456 mut yoleck,
457 mut yoleck_managed_query,
458 yoleck_edited_query,
459 mut directives_reader,
460 mut commands,
461 editor_state,
462 mut writer,
463 mut knobs_cache,
464 active_exclusive_system,
465 mut exclusive_systems_queue,
466 entity_creation_exclusive_systems,
467 ) = system_state.get_mut(world);
468
469 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
470 return;
471 }
472
473 let mut data_passed_to_entities: HashMap<Entity, HashMap<TypeId, BoxedArc>> =
474 Default::default();
475 for directive in directives_reader.read() {
476 match &directive.0 {
477 YoleckDirectiveInner::PassToEntity(entity, type_id, data) => {
478 if false {
479 data_passed_to_entities
480 .entry(*entity)
481 .or_default()
482 .insert(*type_id, data.clone());
483 }
484 passed_data
485 .0
486 .entry(*entity)
487 .or_default()
488 .insert(*type_id, data.clone());
489 }
490 YoleckDirectiveInner::SetSelected(entity) => {
491 if active_exclusive_system.is_some() {
492 continue;
494 }
495 if let Some(entity) = entity {
496 let mut already_selected = false;
497 for entity_to_deselect in yoleck_edited_query.iter() {
498 if entity_to_deselect == *entity {
499 already_selected = true;
500 } else {
501 commands
502 .entity(entity_to_deselect)
503 .remove::<YoleckEditMarker>();
504 writer.write(YoleckEditorEvent::EntityDeselected(
505 entity_to_deselect,
506 ));
507 }
508 }
509 if !already_selected {
510 commands.entity(*entity).insert(YoleckEditMarker);
511 writer.write(YoleckEditorEvent::EntitySelected(*entity));
512 }
513 } else {
514 for entity_to_deselect in yoleck_edited_query.iter() {
515 commands
516 .entity(entity_to_deselect)
517 .remove::<YoleckEditMarker>();
518 writer
519 .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));
520 }
521 }
522 }
523 YoleckDirectiveInner::ChangeSelectedStatus { entity, force_to } => {
524 if active_exclusive_system.is_some() {
525 continue;
527 }
528 match (force_to, yoleck_edited_query.contains(*entity)) {
529 (Some(true), true) | (Some(false), false) => {
530 }
532 (None, false) | (Some(true), false) => {
533 commands.entity(*entity).insert(YoleckEditMarker);
535 writer.write(YoleckEditorEvent::EntitySelected(*entity));
536 }
537 (None, true) | (Some(false), true) => {
538 commands.entity(*entity).remove::<YoleckEditMarker>();
540 writer.write(YoleckEditorEvent::EntityDeselected(*entity));
541 }
542 }
543 }
544 YoleckDirectiveInner::SpawnEntity {
545 level,
546 type_name,
547 data,
548 select_created_entity,
549 modify_exclusive_systems: override_exclusive_systems,
550 } => {
551 if active_exclusive_system.is_some() {
552 continue;
553 }
554 let mut cmd = commands.spawn((
555 YoleckRawEntry {
556 header: YoleckEntryHeader {
557 type_name: type_name.clone(),
558 name: "".to_owned(),
559 uuid: None,
560 },
561 data: data.clone(),
562 },
563 YoleckBelongsToLevel { level: *level },
564 ));
565 if *select_created_entity {
566 writer.write(YoleckEditorEvent::EntitySelected(cmd.id()));
567 cmd.insert(YoleckEditMarker);
568 for entity_to_deselect in yoleck_edited_query.iter() {
569 commands
570 .entity(entity_to_deselect)
571 .remove::<YoleckEditMarker>();
572 writer
573 .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));
574 }
575 *exclusive_systems_queue =
576 entity_creation_exclusive_systems.create_queue();
577 if let Some(override_exclusive_systems) = override_exclusive_systems {
578 override_exclusive_systems(exclusive_systems_queue.as_mut());
579 }
580 new_entity_created_this_frame = true;
581 }
582 yoleck.level_needs_saving = true;
583 }
584 }
585 }
586
587 let entity_being_edited;
588 if let Ok((entity, mut yoleck_managed)) = yoleck_managed_query.single_mut() {
589 entity_being_edited = Some(entity);
590 ui.horizontal(|ui| {
591 ui.heading(format!(
592 "Editing {}",
593 format_caption(entity, &yoleck_managed)
594 ));
595 if ui.button("Delete").clicked() {
596 commands.entity(entity).despawn();
597 writer.write(YoleckEditorEvent::EntityDeselected(entity));
598 yoleck.level_needs_saving = true;
599 }
600 });
601 ui.horizontal(|ui| {
602 ui.label("Custom Name:");
603 ui.text_edit_singleline(&mut yoleck_managed.name);
604 });
605 } else {
606 entity_being_edited = None;
607 }
608
609 if previously_edited_entity != entity_being_edited {
610 previously_edited_entity = entity_being_edited;
611 for knob_entity in knobs_cache.drain() {
612 commands.entity(knob_entity).despawn();
613 }
614 } else {
615 knobs_cache.clean_untouched(|knob_entity| {
616 commands.entity(knob_entity).despawn();
617 });
618 }
619 }
620 system_state.apply(world);
621
622 let frame = egui::Frame::new();
623 let mut prepared = frame.begin(ui);
624 let content_ui = std::mem::replace(
625 &mut prepared.content_ui,
626 ui.new_child(egui::UiBuilder {
627 max_rect: Some(ui.max_rect()),
628 layout: Some(*ui.layout()), ..Default::default()
630 }),
631 );
632 world.insert_resource(YoleckUi(content_ui));
633 world.insert_resource(passed_data);
634
635 enum ActiveExclusiveSystemStatus {
636 DidNotRun,
637 StillRunningSame,
638 JustFinishedRunning,
639 }
640
641 let behavior_for_exclusive_system = if let Some(mut active_exclusive_system) =
642 world.remove_resource::<YoleckActiveExclusiveSystem>()
643 {
644 let result = active_exclusive_system.0.run((), world);
645 match result {
646 YoleckExclusiveSystemDirective::Listening => {
647 world.insert_resource(active_exclusive_system);
648 ActiveExclusiveSystemStatus::StillRunningSame
649 }
650 YoleckExclusiveSystemDirective::Finished => {
651 ActiveExclusiveSystemStatus::JustFinishedRunning
652 }
653 }
654 } else {
655 ActiveExclusiveSystemStatus::DidNotRun
656 };
657
658 let should_run_regular_systems = match behavior_for_exclusive_system {
659 ActiveExclusiveSystemStatus::DidNotRun => loop {
660 let Some(mut new_exclusive_system) = world
661 .resource_mut::<YoleckExclusiveSystemsQueue>()
662 .pop_front()
663 else {
664 break true;
665 };
666 new_exclusive_system.initialize(world);
667 let first_run_result = new_exclusive_system.run((), world);
668 if new_entity_created_this_frame
669 || matches!(first_run_result, YoleckExclusiveSystemDirective::Listening)
670 {
671 world.insert_resource(YoleckActiveExclusiveSystem(new_exclusive_system));
672 break false;
673 }
674 },
675 ActiveExclusiveSystemStatus::StillRunningSame => false,
676 ActiveExclusiveSystemStatus::JustFinishedRunning => false,
677 };
678
679 if should_run_regular_systems {
680 world.resource_scope(|world, mut yoleck_edit_systems: Mut<YoleckEditSystems>| {
681 yoleck_edit_systems.run_systems(world);
682 });
683 }
684 let YoleckUi(content_ui) = world
685 .remove_resource()
686 .expect("The YoleckUi resource was put in the world by this very function");
687 world.remove_resource::<YoleckPassedData>();
688 prepared.content_ui = content_ui;
689 prepared.end(ui);
690
691 world.run_schedule(YoleckInternalSchedule::UpdateManagedDataFromComponents);
693 }
694}