bevy_yoleck/
editor.rs

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/// Whether or not the Yoleck editor is active.
28#[derive(States, Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
29pub enum YoleckEditorState {
30    /// Editor mode. The editor is active and can be used to edit entities.
31    #[default]
32    EditorActive,
33    /// Game mode. Either the actual game or playtest from the editor mode.
34    GameActive,
35}
36
37/// Sync the game's state back and forth when the level editor enters and exits playtest mode.
38///
39/// Add this as a plugin. When using it, there is no need to initialize the state with `add_state`
40/// because `YoleckSyncWithEditorState` will initialize it and set its initial value to
41/// `when_editor`. This means that the state's default value should be it's initial value for
42/// non-editor mode (which is not necessarily `when_game`, because the game may start in a menu
43/// state or a loading state)
44///
45/// ```no_run
46/// # use bevy::prelude::*;
47/// # use bevy_yoleck::prelude::*;
48/// # use bevy_yoleck::bevy_egui::EguiPlugin;
49/// #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
50/// enum GameState {
51///     #[default]
52///     Loading,
53///     Game,
54///     Editor,
55/// }
56///
57/// # let mut app = App::new();
58/// # let executable_started_in_editor_mode = true;
59/// if executable_started_in_editor_mode {
60///     // These two plugins are needed for editor mode:
61///     app.add_plugins(EguiPlugin::default());
62///     app.add_plugins(YoleckPluginForEditor);
63///     app.add_plugins(YoleckSyncWithEditorState {
64///         when_editor: GameState::Editor,
65///         when_game: GameState::Game,
66///     });
67/// } else {
68///     // This plugin is needed for game mode:
69///     app.add_plugins(YoleckPluginForGame);
70///
71///     app.init_state::<GameState>();
72/// }
73pub 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/// Events emitted by the Yoleck editor.
119///
120/// Modules that provide editing overlays over the viewport (like [vpeol](crate::vpeol)) can
121/// use these events to update their status to match with the editor.
122#[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/// Event that can be sent to control Yoleck's editor.
148#[derive(Message)]
149pub struct YoleckDirective(YoleckDirectiveInner);
150
151impl YoleckDirective {
152    /// Pass data from an external system (usually a [ViewPort Editing OverLay](crate::vpeol)) to an entity.
153    ///
154    /// This data can be received using the [`YoleckPassedData`] resource. If the data is
155    /// passed to a knob, it can also be received using the knob handle's
156    /// [`get_passed_data`](crate::knobs::YoleckKnobHandle::get_passed_data) method.
157    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    /// Set the entity selected in the Yoleck editor.
166    pub fn set_selected(entity: Option<Entity>) -> Self {
167        Self(YoleckDirectiveInner::SetSelected(entity))
168    }
169
170    /// Set the entity selected in the Yoleck editor.
171    pub fn toggle_selected(entity: Entity) -> Self {
172        Self(YoleckDirectiveInner::ChangeSelectedStatus {
173            entity,
174            force_to: None,
175        })
176    }
177
178    /// Spawn a new entity with pre-populated data.
179    ///
180    /// ```no_run
181    /// # use serde::{Deserialize, Serialize};
182    /// # use bevy::prelude::*;
183    /// # use bevy_yoleck::prelude::*;
184    /// # use bevy_yoleck::YoleckDirective;
185    /// # use bevy_yoleck::vpeol_2d::Vpeol2dPosition;
186    /// # #[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
187    /// # struct Example;
188    /// fn duplicate_example(
189    ///     mut ui: ResMut<YoleckUi>,
190    ///     mut edit: YoleckEdit<(&YoleckBelongsToLevel, &Vpeol2dPosition), With<Example>>,
191    ///     mut writer: MessageWriter<YoleckDirective>,
192    /// ) {
193    ///     let Ok((belongs_to_level, position)) = edit.single() else { return };
194    ///     if ui.button("Duplicate").clicked() {
195    ///         writer.write(
196    ///             YoleckDirective::spawn_entity(
197    ///                 belongs_to_level.level,
198    ///                 "Example",
199    ///                 // Automatically select the newly created entity:
200    ///                 true,
201    ///             )
202    ///             // Create the new example entity 100 units below the current one:
203    ///             .with(Vpeol2dPosition(position.0 - 100.0 * Vec2::Y))
204    ///             .into(),
205    ///         );
206    ///     }
207    /// }
208    /// ```
209    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    /// Override a component of the spawned entity.
235    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    /// Change the exclusive systems that will be running the entity is spawned.
262    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    /// Get data sent to an entity from external systems (usually from (usually a [ViewPort Editing
292    /// OverLay](crate::vpeol))
293    ///
294    /// The data is sent using [a directive event](crate::YoleckDirective::pass_to_entity).
295    ///
296    /// ```no_run
297    /// # use bevy::prelude::*;
298    /// # use bevy_yoleck::prelude::*;;
299    /// # #[derive(Component)]
300    /// # struct Example {
301    /// #     message: String,
302    /// # }
303    /// fn edit_example(
304    ///     mut edit: YoleckEdit<(Entity, &mut Example)>,
305    ///     passed_data: Res<YoleckPassedData>,
306    /// ) {
307    ///     let Ok((entity, mut example)) = edit.single_mut() else { return };
308    ///     if let Some(message) = passed_data.get::<String>(entity) {
309    ///         example.message = message.clone();
310    ///     }
311    /// }
312    /// ```
313    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
335/// The UI part for creating new entities. See [`YoleckEditorLeftPanelSections`](crate::YoleckEditorLeftPanelSections).
336pub 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/// The UI part for selecting entities. See [`YoleckEditorLeftPanelSections`](crate::YoleckEditorLeftPanelSections).
371#[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/// The UI part for editing entities. See [`YoleckEditorLeftPanelSections`](crate::YoleckEditorLeftPanelSections).
488#[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                            // TODO: pass the selection command to the exclusive system?
555                            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                            // TODO: pass the selection command to the exclusive system?
588                            continue;
589                        }
590                        match (force_to, yoleck_edited_query.contains(*entity)) {
591                            (Some(true), true) | (Some(false), false) => {
592                                // Nothing to do
593                            }
594                            (None, false) | (Some(true), false) => {
595                                // Add to selection
596                                commands.entity(*entity).insert(YoleckEditMarker);
597                                writer.write(YoleckEditorEvent::EntitySelected(*entity));
598                            }
599                            (None, true) | (Some(false), true) => {
600                                // Remove from selection
601                                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()), // Is this necessary?
688                ..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        // Some systems may have edited the entries, so we need to update them
761        world.run_schedule(YoleckInternalSchedule::UpdateManagedDataFromComponents);
762        Ok(())
763    })
764}