bevy_yoleck/
editor.rs

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/// Whether or not the Yoleck editor is active.
23#[derive(States, Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
24pub enum YoleckEditorState {
25    /// Editor mode. The editor is active and can be used to edit entities.
26    #[default]
27    EditorActive,
28    /// Game mode. Either the actual game or playtest from the editor mode.
29    GameActive,
30}
31
32/// Sync the game's state back and forth when the level editor enters and exits playtest mode.
33///
34/// Add this as a plugin. When using it, there is no need to initialize the state with `add_state`
35/// because `YoleckSyncWithEditorState` will initialize it and set its initial value to
36/// `when_editor`. This means that the state's default value should be it's initial value for
37/// non-editor mode (which is not necessarily `when_game`, because the game may start in a menu
38/// state or a loading state)
39///
40/// ```no_run
41/// # use bevy::prelude::*;
42/// # use bevy_yoleck::prelude::*;
43/// # use bevy_yoleck::bevy_egui::EguiPlugin;
44/// #[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
45/// enum GameState {
46///     #[default]
47///     Loading,
48///     Game,
49///     Editor,
50/// }
51///
52/// # let mut app = App::new();
53/// # let executable_started_in_editor_mode = true;
54/// if executable_started_in_editor_mode {
55///     // These two plugins are needed for editor mode:
56///     app.add_plugins(EguiPlugin {
57///         enable_multipass_for_primary_context: true,
58///     });
59///     app.add_plugins(YoleckPluginForEditor);
60///     app.add_plugins(YoleckSyncWithEditorState {
61///         when_editor: GameState::Editor,
62///         when_game: GameState::Game,
63///     });
64/// } else {
65///     // This plugin is needed for game mode:
66///     app.add_plugins(YoleckPluginForGame);
67///
68///     app.init_state::<GameState>();
69/// }
70pub 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/// Events emitted by the Yoleck editor.
116///
117/// Modules that provide editing overlays over the viewport (like [vpeol](crate::vpeol)) can
118/// use these events to update their status to match with the editor.
119#[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/// Event that can be sent to control Yoleck's editor.
145#[derive(Event)]
146pub struct YoleckDirective(YoleckDirectiveInner);
147
148impl YoleckDirective {
149    /// Pass data from an external system (usually a [ViewPort Editing OverLay](crate::vpeol)) to an entity.
150    ///
151    /// This data can be received using the [`YoleckPassedData`] resource. If the data is
152    /// passed to a knob, it can also be received using the knob handle's
153    /// [`get_passed_data`](crate::knobs::YoleckKnobHandle::get_passed_data) method.
154    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    /// Set the entity selected in the Yoleck editor.
163    pub fn set_selected(entity: Option<Entity>) -> Self {
164        Self(YoleckDirectiveInner::SetSelected(entity))
165    }
166
167    /// Set the entity selected in the Yoleck editor.
168    pub fn toggle_selected(entity: Entity) -> Self {
169        Self(YoleckDirectiveInner::ChangeSelectedStatus {
170            entity,
171            force_to: None,
172        })
173    }
174
175    /// Spawn a new entity with pre-populated data.
176    ///
177    /// ```no_run
178    /// # use serde::{Deserialize, Serialize};
179    /// # use bevy::prelude::*;
180    /// # use bevy_yoleck::prelude::*;
181    /// # use bevy_yoleck::YoleckDirective;
182    /// # use bevy_yoleck::vpeol_2d::Vpeol2dPosition;
183    /// # #[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
184    /// # struct Example;
185    /// fn duplicate_example(
186    ///     mut ui: ResMut<YoleckUi>,
187    ///     mut edit: YoleckEdit<(&YoleckBelongsToLevel, &Vpeol2dPosition), With<Example>>,
188    ///     mut writer: EventWriter<YoleckDirective>,
189    /// ) {
190    ///     let Ok((belongs_to_level, position)) = edit.single() else { return };
191    ///     if ui.button("Duplicate").clicked() {
192    ///         writer.write(
193    ///             YoleckDirective::spawn_entity(
194    ///                 belongs_to_level.level,
195    ///                 "Example",
196    ///                 // Automatically select the newly created entity:
197    ///                 true,
198    ///             )
199    ///             // Create the new example entity 100 units below the current one:
200    ///             .with(Vpeol2dPosition(position.0 - 100.0 * Vec2::Y))
201    ///             .into(),
202    ///         );
203    ///     }
204    /// }
205    /// ```
206    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    /// Override a component of the spawned entity.
232    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    /// Change the exclusive systems that will be running the entity is spawned.
241    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    /// Get data sent to an entity from external systems (usually from (usually a [ViewPort Editing
267    /// OverLay](crate::vpeol))
268    ///
269    /// The data is sent using [a directive event](crate::YoleckDirective::pass_to_entity).
270    ///
271    /// ```no_run
272    /// # use bevy::prelude::*;
273    /// # use bevy_yoleck::prelude::*;;
274    /// # #[derive(Component)]
275    /// # struct Example {
276    /// #     message: String,
277    /// # }
278    /// fn edit_example(
279    ///     mut edit: YoleckEdit<(Entity, &mut Example)>,
280    ///     passed_data: Res<YoleckPassedData>,
281    /// ) {
282    ///     let Ok((entity, mut example)) = edit.single_mut() else { return };
283    ///     if let Some(message) = passed_data.get::<String>(entity) {
284    ///         example.message = message.clone();
285    ///     }
286    /// }
287    /// ```
288    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
310/// The UI part for creating new entities. See [`YoleckEditorSections`](crate::YoleckEditorSections).
311pub 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
351/// The UI part for selecting entities. See [`YoleckEditorSections`](crate::YoleckEditorSections).
352pub 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
422/// The UI part for editing entities. See [`YoleckEditorSections`](crate::YoleckEditorSections).
423pub 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                            // TODO: pass the selection command to the exclusive system?
482                            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                            // TODO: pass the selection command to the exclusive system?
515                            continue;
516                        }
517                        match (force_to, yoleck_edited_query.contains(*entity)) {
518                            (Some(true), true) | (Some(false), false) => {
519                                // Nothing to do
520                            }
521                            (None, false) | (Some(true), false) => {
522                                // Add to selection
523                                commands.entity(*entity).insert(YoleckEditMarker);
524                                writer.write(YoleckEditorEvent::EntitySelected(*entity));
525                            }
526                            (None, true) | (Some(false), true) => {
527                                // Remove from selection
528                                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()), // Is this necessary?
618                ..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        // Some systems may have edited the entries, so we need to update them
681        world.run_schedule(YoleckInternalSchedule::UpdateManagedDataFromComponents);
682    }
683}