1use std::any::TypeId;
2use std::borrow::Cow;
3use std::sync::Arc;
4
5use bevy::ecs::system::SystemState;
6use bevy::platform::collections::{HashMap, HashSet};
7use bevy::prelude::*;
8use bevy::state::state::FreelyMutableState;
9use bevy_egui::egui;
10
11use crate::editor_panels::YoleckPanelUi;
12use crate::entity_management::{YoleckEntryHeader, YoleckRawEntry};
13use crate::entity_uuid::YoleckEntityUuid;
14use crate::exclusive_systems::{
15 YoleckActiveExclusiveSystem, YoleckEntityCreationExclusiveSystems,
16 YoleckExclusiveSystemDirective, YoleckExclusiveSystemsQueue,
17};
18use crate::knobs::YoleckKnobsCache;
19use crate::prelude::{YoleckComponent, YoleckUi};
20#[cfg(feature = "vpeol")]
21use crate::vpeol;
22use crate::{
23 BoxedArc, YoleckBelongsToLevel, YoleckEditMarker, YoleckEditSystems,
24 YoleckEntityConstructionSpecs, YoleckInternalSchedule, YoleckManaged, YoleckState,
25};
26
27#[derive(States, Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
29pub enum YoleckEditorState {
30 #[default]
32 EditorActive,
33 GameActive,
35}
36
37pub struct YoleckSyncWithEditorState<T>
74where
75 T: 'static
76 + States
77 + FreelyMutableState
78 + Sync
79 + Send
80 + std::fmt::Debug
81 + Clone
82 + std::cmp::Eq
83 + std::hash::Hash,
84{
85 pub when_editor: T,
86 pub when_game: T,
87}
88
89impl<T> Plugin for YoleckSyncWithEditorState<T>
90where
91 T: 'static
92 + States
93 + FreelyMutableState
94 + Sync
95 + Send
96 + std::fmt::Debug
97 + Clone
98 + std::cmp::Eq
99 + std::hash::Hash,
100{
101 fn build(&self, app: &mut App) {
102 app.insert_state(self.when_editor.clone());
103 let when_editor = self.when_editor.clone();
104 let when_game = self.when_game.clone();
105 app.add_systems(
106 Update,
107 move |editor_state: Res<State<YoleckEditorState>>,
108 mut game_state: ResMut<NextState<T>>| {
109 game_state.set(match editor_state.get() {
110 YoleckEditorState::EditorActive => when_editor.clone(),
111 YoleckEditorState::GameActive => when_game.clone(),
112 });
113 },
114 );
115 }
116}
117
118#[derive(Debug, Message)]
123pub enum YoleckEditorEvent {
124 EntitySelected(Entity),
125 EntityDeselected(Entity),
126 EditedEntityPopulated(Entity),
127}
128
129enum YoleckDirectiveInner {
130 SetSelected(Option<Entity>),
131 ChangeSelectedStatus {
132 entity: Entity,
133 force_to: Option<bool>,
134 },
135 PassToEntity(Entity, TypeId, BoxedArc),
136 SpawnEntity {
137 level: Entity,
138 type_name: String,
139 data: serde_json::Map<String, serde_json::Value>,
140 select_created_entity: bool,
141 #[allow(clippy::type_complexity)]
142 modify_exclusive_systems:
143 Option<Box<dyn Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue)>>,
144 },
145}
146
147#[derive(Message)]
149pub struct YoleckDirective(YoleckDirectiveInner);
150
151impl YoleckDirective {
152 pub fn pass_to_entity<T: 'static + Send + Sync>(entity: Entity, data: T) -> Self {
158 Self(YoleckDirectiveInner::PassToEntity(
159 entity,
160 TypeId::of::<T>(),
161 Arc::new(data),
162 ))
163 }
164
165 pub fn set_selected(entity: Option<Entity>) -> Self {
167 Self(YoleckDirectiveInner::SetSelected(entity))
168 }
169
170 pub fn toggle_selected(entity: Entity) -> Self {
172 Self(YoleckDirectiveInner::ChangeSelectedStatus {
173 entity,
174 force_to: None,
175 })
176 }
177
178 pub fn spawn_entity(
210 level: Entity,
211 type_name: impl ToString,
212 select_created_entity: bool,
213 ) -> SpawnEntityBuilder {
214 SpawnEntityBuilder {
215 level,
216 type_name: type_name.to_string(),
217 select_created_entity,
218 data: Default::default(),
219 modify_exclusive_systems: None,
220 }
221 }
222}
223
224pub struct SpawnEntityBuilder {
225 level: Entity,
226 type_name: String,
227 select_created_entity: bool,
228 data: HashMap<Cow<'static, str>, serde_json::Value>,
229 #[allow(clippy::type_complexity)]
230 modify_exclusive_systems: Option<Box<dyn Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue)>>,
231}
232
233impl SpawnEntityBuilder {
234 pub fn with<T: YoleckComponent>(self, component: T) -> Self {
236 self.with_raw(
237 T::KEY,
238 serde_json::to_value(component).expect("should always work"),
239 )
240 }
241
242 pub fn with_raw(
243 mut self,
244 component_name: impl Into<Cow<'static, str>>,
245 component_data: serde_json::Value,
246 ) -> Self {
247 self.data.insert(component_name.into(), component_data);
248 self
249 }
250
251 pub fn extend(
252 mut self,
253 components: impl Iterator<Item = (impl Into<Cow<'static, str>>, serde_json::Value)>,
254 ) -> Self {
255 for (component_name, component_data) in components.into_iter() {
256 self = self.with_raw(component_name, component_data);
257 }
258 self
259 }
260
261 pub fn modify_exclusive_systems(
263 mut self,
264 dlg: impl 'static + Sync + Send + Fn(&mut YoleckExclusiveSystemsQueue),
265 ) -> Self {
266 self.modify_exclusive_systems = Some(Box::new(dlg));
267 self
268 }
269}
270
271impl From<SpawnEntityBuilder> for YoleckDirective {
272 fn from(value: SpawnEntityBuilder) -> Self {
273 YoleckDirective(YoleckDirectiveInner::SpawnEntity {
274 level: value.level,
275 type_name: value.type_name,
276 data: value
277 .data
278 .into_iter()
279 .map(|(k, v)| (k.into_owned(), v))
280 .collect(),
281 select_created_entity: value.select_created_entity,
282 modify_exclusive_systems: value.modify_exclusive_systems,
283 })
284 }
285}
286
287#[derive(Resource)]
288pub struct YoleckPassedData(pub(crate) HashMap<Entity, HashMap<TypeId, BoxedArc>>);
289
290impl YoleckPassedData {
291 pub fn get<T: 'static>(&self, entity: Entity) -> Option<&T> {
314 Some(
315 self.0
316 .get(&entity)?
317 .get(&TypeId::of::<T>())?
318 .downcast_ref()
319 .expect("Passed data TypeId must be correct"),
320 )
321 }
322}
323
324fn format_caption(entity: Entity, yoleck_managed: &YoleckManaged) -> String {
325 if yoleck_managed.name.is_empty() {
326 format!("{} {:?}", yoleck_managed.type_name, entity)
327 } else {
328 format!(
329 "{} ({} {:?})",
330 yoleck_managed.name, yoleck_managed.type_name, entity
331 )
332 }
333}
334
335pub fn new_entity_section(
337 mut ui: ResMut<YoleckPanelUi>,
338 construction_specs: Res<YoleckEntityConstructionSpecs>,
339 yoleck: Res<YoleckState>,
340 editor_state: Res<State<YoleckEditorState>>,
341 mut writer: MessageWriter<YoleckDirective>,
342 active_exclusive_system: Option<Res<YoleckActiveExclusiveSystem>>,
343) -> Result {
344 if active_exclusive_system.is_some() {
345 return Ok(());
346 }
347
348 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
349 return Ok(());
350 }
351
352 let button_response = ui.button("Add New Entity");
353
354 egui::Popup::menu(&button_response).show(|ui| {
355 for entity_type in construction_specs.entity_types.iter() {
356 if ui.button(&entity_type.name).clicked() {
357 writer.write(YoleckDirective(YoleckDirectiveInner::SpawnEntity {
358 level: yoleck.level_being_edited,
359 type_name: entity_type.name.clone(),
360 data: Default::default(),
361 select_created_entity: true,
362 modify_exclusive_systems: None,
363 }));
364 }
365 }
366 });
367 Ok(())
368}
369
370#[allow(clippy::too_many_arguments)]
372pub fn entity_selection_section(
373 mut ui: ResMut<YoleckPanelUi>,
374 mut filter_custom_name: Local<String>,
375 mut filter_types: Local<HashSet<String>>,
376 construction_specs: Res<YoleckEntityConstructionSpecs>,
377 yoleck_managed_query: Query<(
378 Entity,
379 &YoleckManaged,
380 Option<&YoleckEditMarker>,
381 Option<&YoleckEntityUuid>,
382 )>,
383 editor_state: Res<State<YoleckEditorState>>,
384 mut writer: MessageWriter<YoleckDirective>,
385 active_exclusive_system: Option<Res<YoleckActiveExclusiveSystem>>,
386 #[cfg(feature = "vpeol")] vpeol_camera_state_query: Query<&vpeol::VpeolCameraState>,
387) -> Result {
388 if active_exclusive_system.is_some() {
389 return Ok(());
390 }
391
392 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
393 return Ok(());
394 }
395
396 egui::CollapsingHeader::new("Filter").show(ui.as_mut(), |ui| {
397 ui.horizontal(|ui| {
398 ui.label("By Name:");
399 ui.text_edit_singleline(&mut *filter_custom_name);
400 });
401 for entity_type in construction_specs.entity_types.iter() {
402 let mut should_show = filter_types.contains(&entity_type.name);
403 if ui.checkbox(&mut should_show, &entity_type.name).changed() {
404 if should_show {
405 filter_types.insert(entity_type.name.clone());
406 } else {
407 filter_types.remove(&entity_type.name);
408 }
409 }
410 }
411 });
412
413 #[cfg(not(feature = "vpeol"))]
414 let entities_under_cursor: HashSet<Entity> = Default::default();
415 #[cfg(feature = "vpeol")]
416 let entities_under_cursor: HashSet<Entity> = vpeol_camera_state_query
417 .iter()
418 .filter_map(|camera_state| Some(camera_state.entity_under_cursor.as_ref()?.0))
419 .collect();
420
421 for (entity, yoleck_managed, edit_marker, entity_uuid) in yoleck_managed_query.iter() {
422 if !filter_types.is_empty() && !filter_types.contains(&yoleck_managed.type_name) {
423 continue;
424 }
425 if !yoleck_managed.name.contains(filter_custom_name.as_str()) {
426 continue;
427 }
428 let is_selected = edit_marker.is_some();
429
430 let caption = format_caption(entity, yoleck_managed);
431 let mut potentially_highlighted_caption = egui::RichText::new(&caption);
432 if entities_under_cursor.contains(&entity) {
433 potentially_highlighted_caption =
434 potentially_highlighted_caption.background_color(egui::Color32::DARK_RED);
435 }
436
437 if let Some(entity_uuid) = entity_uuid {
438 let uuid = entity_uuid.get();
439 let sense = egui::Sense::click_and_drag();
440 let response = ui
441 .selectable_label(is_selected, potentially_highlighted_caption)
442 .interact(sense);
443
444 if response.drag_started() {
445 egui::DragAndDrop::set_payload(ui.ctx(), uuid);
446 } else if response.clicked() {
447 if ui.input(|input| input.modifiers.shift) {
448 writer.write(YoleckDirective::toggle_selected(entity));
449 } else if is_selected {
450 writer.write(YoleckDirective::set_selected(None));
451 } else {
452 writer.write(YoleckDirective::set_selected(Some(entity)));
453 }
454 }
455
456 if response.dragged() {
457 ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
458
459 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
460 egui::Area::new(egui::Id::new("dragged_entity_preview"))
461 .fixed_pos(pointer_pos + egui::vec2(10.0, 10.0))
462 .order(egui::Order::Tooltip)
463 .show(ui.ctx(), |ui| {
464 egui::Frame::popup(ui.style()).show(ui, |ui| {
465 ui.label(caption);
466 });
467 });
468 }
469 }
470 } else if ui
471 .selectable_label(is_selected, potentially_highlighted_caption)
472 .clicked()
473 {
474 if ui.input(|input| input.modifiers.shift) {
475 writer.write(YoleckDirective::toggle_selected(entity));
476 } else if is_selected {
477 writer.write(YoleckDirective::set_selected(None));
478 } else {
479 writer.write(YoleckDirective::set_selected(Some(entity)));
480 }
481 }
482 }
483
484 Ok(())
485}
486
487#[allow(clippy::type_complexity)]
489pub fn entity_editing_section(
490 world: &mut World,
491 mut previously_edited_entity: Local<Option<Entity>>,
492 mut new_entity_created_this_frame: Local<bool>,
493 mut system_state: Local<
494 Option<
495 SystemState<(
496 ResMut<YoleckState>,
497 Query<(Entity, &mut YoleckManaged), With<YoleckEditMarker>>,
498 Query<Entity, With<YoleckEditMarker>>,
499 MessageReader<YoleckDirective>,
500 Commands,
501 Res<State<YoleckEditorState>>,
502 MessageWriter<YoleckEditorEvent>,
503 ResMut<YoleckKnobsCache>,
504 Option<Res<YoleckActiveExclusiveSystem>>,
505 ResMut<YoleckExclusiveSystemsQueue>,
506 Res<YoleckEntityCreationExclusiveSystems>,
507 )>,
508 >,
509 >,
510) -> Result {
511 let system_state = system_state.get_or_insert_with(|| SystemState::new(world));
512
513 world.resource_scope(|world, mut ui: Mut<YoleckPanelUi>| {
514 let ui = &mut **ui;
515 let mut passed_data = YoleckPassedData(Default::default());
516 {
517 let (
518 mut yoleck,
519 mut yoleck_managed_query,
520 yoleck_edited_query,
521 mut directives_reader,
522 mut commands,
523 editor_state,
524 mut writer,
525 mut knobs_cache,
526 active_exclusive_system,
527 mut exclusive_systems_queue,
528 entity_creation_exclusive_systems,
529 ) = system_state.get_mut(world);
530
531 if !matches!(editor_state.get(), YoleckEditorState::EditorActive) {
532 return Ok(());
533 }
534
535 let mut data_passed_to_entities: HashMap<Entity, HashMap<TypeId, BoxedArc>> =
536 Default::default();
537 for directive in directives_reader.read() {
538 match &directive.0 {
539 YoleckDirectiveInner::PassToEntity(entity, type_id, data) => {
540 if false {
541 data_passed_to_entities
542 .entry(*entity)
543 .or_default()
544 .insert(*type_id, data.clone());
545 }
546 passed_data
547 .0
548 .entry(*entity)
549 .or_default()
550 .insert(*type_id, data.clone());
551 }
552 YoleckDirectiveInner::SetSelected(entity) => {
553 if active_exclusive_system.is_some() {
554 continue;
556 }
557 if let Some(entity) = entity {
558 let mut already_selected = false;
559 for entity_to_deselect in yoleck_edited_query.iter() {
560 if entity_to_deselect == *entity {
561 already_selected = true;
562 } else {
563 commands
564 .entity(entity_to_deselect)
565 .remove::<YoleckEditMarker>();
566 writer.write(YoleckEditorEvent::EntityDeselected(
567 entity_to_deselect,
568 ));
569 }
570 }
571 if !already_selected {
572 commands.entity(*entity).insert(YoleckEditMarker);
573 writer.write(YoleckEditorEvent::EntitySelected(*entity));
574 }
575 } else {
576 for entity_to_deselect in yoleck_edited_query.iter() {
577 commands
578 .entity(entity_to_deselect)
579 .remove::<YoleckEditMarker>();
580 writer
581 .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));
582 }
583 }
584 }
585 YoleckDirectiveInner::ChangeSelectedStatus { entity, force_to } => {
586 if active_exclusive_system.is_some() {
587 continue;
589 }
590 match (force_to, yoleck_edited_query.contains(*entity)) {
591 (Some(true), true) | (Some(false), false) => {
592 }
594 (None, false) | (Some(true), false) => {
595 commands.entity(*entity).insert(YoleckEditMarker);
597 writer.write(YoleckEditorEvent::EntitySelected(*entity));
598 }
599 (None, true) | (Some(false), true) => {
600 commands.entity(*entity).remove::<YoleckEditMarker>();
602 writer.write(YoleckEditorEvent::EntityDeselected(*entity));
603 }
604 }
605 }
606 YoleckDirectiveInner::SpawnEntity {
607 level,
608 type_name,
609 data,
610 select_created_entity,
611 modify_exclusive_systems: override_exclusive_systems,
612 } => {
613 if active_exclusive_system.is_some() {
614 continue;
615 }
616 let mut cmd = commands.spawn((
617 YoleckRawEntry {
618 header: YoleckEntryHeader {
619 type_name: type_name.clone(),
620 name: "".to_owned(),
621 uuid: None,
622 },
623 data: data.clone(),
624 },
625 YoleckBelongsToLevel { level: *level },
626 ));
627 if *select_created_entity {
628 writer.write(YoleckEditorEvent::EntitySelected(cmd.id()));
629 cmd.insert(YoleckEditMarker);
630 for entity_to_deselect in yoleck_edited_query.iter() {
631 commands
632 .entity(entity_to_deselect)
633 .remove::<YoleckEditMarker>();
634 writer
635 .write(YoleckEditorEvent::EntityDeselected(entity_to_deselect));
636 }
637 *exclusive_systems_queue =
638 entity_creation_exclusive_systems.create_queue();
639 if let Some(override_exclusive_systems) = override_exclusive_systems {
640 override_exclusive_systems(exclusive_systems_queue.as_mut());
641 }
642 *new_entity_created_this_frame = true;
643 }
644 yoleck.level_needs_saving = true;
645 }
646 }
647 }
648
649 let entity_being_edited;
650 if let Ok((entity, mut yoleck_managed)) = yoleck_managed_query.single_mut() {
651 entity_being_edited = Some(entity);
652 ui.horizontal(|ui| {
653 ui.heading(format_caption(entity, &yoleck_managed));
654 if ui.button("Delete").clicked() {
655 commands.entity(entity).despawn();
656 writer.write(YoleckEditorEvent::EntityDeselected(entity));
657 yoleck.level_needs_saving = true;
658 }
659 });
660 ui.horizontal(|ui| {
661 ui.label("Custom Name:");
662 ui.text_edit_singleline(&mut yoleck_managed.name);
663 });
664 } else {
665 entity_being_edited = None;
666 }
667
668 if *previously_edited_entity != entity_being_edited {
669 *previously_edited_entity = entity_being_edited;
670 for knob_entity in knobs_cache.drain() {
671 commands.entity(knob_entity).despawn();
672 }
673 } else {
674 knobs_cache.clean_untouched(|knob_entity| {
675 commands.entity(knob_entity).despawn();
676 });
677 }
678 }
679 system_state.apply(world);
680
681 let frame = egui::Frame::new();
682 let mut prepared = frame.begin(ui);
683 let content_ui = std::mem::replace(
684 &mut prepared.content_ui,
685 ui.new_child(egui::UiBuilder {
686 max_rect: Some(ui.max_rect()),
687 layout: Some(*ui.layout()), ..Default::default()
689 }),
690 );
691 world.insert_resource(YoleckUi(content_ui));
692 world.insert_resource(passed_data);
693
694 enum ActiveExclusiveSystemStatus {
695 DidNotRun,
696 StillRunningSame,
697 JustFinishedRunning,
698 }
699
700 let behavior_for_exclusive_system = if let Some(mut active_exclusive_system) =
701 world.remove_resource::<YoleckActiveExclusiveSystem>()
702 {
703 let result = active_exclusive_system
704 .0
705 .run((), world)
706 .map_err(|e| match e {
707 bevy::ecs::system::RunSystemError::Skipped(e) => e.into(),
708 bevy::ecs::system::RunSystemError::Failed(e) => e,
709 })?;
710 match result {
711 YoleckExclusiveSystemDirective::Listening => {
712 world.insert_resource(active_exclusive_system);
713 ActiveExclusiveSystemStatus::StillRunningSame
714 }
715 YoleckExclusiveSystemDirective::Finished => {
716 ActiveExclusiveSystemStatus::JustFinishedRunning
717 }
718 }
719 } else {
720 ActiveExclusiveSystemStatus::DidNotRun
721 };
722
723 let should_run_regular_systems = match behavior_for_exclusive_system {
724 ActiveExclusiveSystemStatus::DidNotRun => loop {
725 let Some(mut new_exclusive_system) = world
726 .resource_mut::<YoleckExclusiveSystemsQueue>()
727 .pop_front()
728 else {
729 break true;
730 };
731 new_exclusive_system.initialize(world);
732 let first_run_result =
733 new_exclusive_system.run((), world).map_err(|e| match e {
734 bevy::ecs::system::RunSystemError::Skipped(e) => e.into(),
735 bevy::ecs::system::RunSystemError::Failed(e) => e,
736 })?;
737 if *new_entity_created_this_frame
738 || matches!(first_run_result, YoleckExclusiveSystemDirective::Listening)
739 {
740 world.insert_resource(YoleckActiveExclusiveSystem(new_exclusive_system));
741 break false;
742 }
743 },
744 ActiveExclusiveSystemStatus::StillRunningSame => false,
745 ActiveExclusiveSystemStatus::JustFinishedRunning => false,
746 };
747
748 if should_run_regular_systems {
749 world.resource_scope(|world, mut yoleck_edit_systems: Mut<YoleckEditSystems>| {
750 yoleck_edit_systems.run_systems(world);
751 });
752 }
753 let YoleckUi(content_ui) = world
754 .remove_resource()
755 .expect("The YoleckUi resource was put in the world by this very function");
756 world.remove_resource::<YoleckPassedData>();
757 prepared.content_ui = content_ui;
758 prepared.end(&mut *ui);
759
760 world.run_schedule(YoleckInternalSchedule::UpdateManagedDataFromComponents);
762 Ok(())
763 })
764}