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