Skip to main content

bevy_mod_scripting_bindings/
schedule.rs

1//! Dynamic scheduling from scripts
2
3use ::{
4    bevy_app::{
5        First, FixedFirst, FixedLast, FixedMain, FixedPostUpdate, FixedPreUpdate, FixedUpdate,
6        Last, PostStartup, PostUpdate, PreStartup, PreUpdate, RunFixedMainLoop, Startup, Update,
7    },
8    bevy_ecs::schedule::ScheduleLabel,
9};
10use bevy_ecs::resource::Resource;
11use bevy_platform::collections::HashMap;
12use bevy_system_reflection::ReflectSchedule;
13use parking_lot::RwLock;
14use std::{any::TypeId, sync::Arc};
15#[derive(Default, Clone, Resource)]
16/// A Send + Sync registry of bevy schedules.
17pub struct AppScheduleRegistry(Arc<RwLock<ScheduleRegistry>>);
18
19impl AppScheduleRegistry {
20    /// Reads the schedule registry.
21    pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, ScheduleRegistry> {
22        self.0.read()
23    }
24
25    /// Writes to the schedule registry.
26    pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, ScheduleRegistry> {
27        self.0.write()
28    }
29
30    /// Creates a new schedule registry pre-populated with default bevy schedules.
31    pub fn new() -> Self {
32        Self(Arc::new(RwLock::new(ScheduleRegistry::new())))
33    }
34}
35
36#[derive(Default)]
37/// A registry of bevy schedules.
38pub struct ScheduleRegistry {
39    schedules: HashMap<TypeId, ReflectSchedule>,
40}
41
42#[profiling::all_functions]
43impl ScheduleRegistry {
44    /// Creates a new schedule registry containing all default bevy schedules.
45    pub fn new() -> Self {
46        let mut self_ = Self::default();
47        self_
48            .register(Update)
49            .register(First)
50            .register(PreUpdate)
51            .register(RunFixedMainLoop)
52            .register(PostUpdate)
53            .register(Last)
54            .register(PreStartup)
55            .register(Startup)
56            .register(PostStartup)
57            .register(FixedMain)
58            .register(FixedFirst)
59            .register(FixedPreUpdate)
60            .register(FixedUpdate)
61            .register(FixedPostUpdate)
62            .register(FixedLast);
63        self_
64    }
65
66    /// Retrieves a schedule by name
67    pub fn get_schedule_by_name(&self, name: &str) -> Option<&ReflectSchedule> {
68        self.schedules.iter().find_map(|(_, schedule)| {
69            (schedule.identifier() == name || schedule.type_path() == name).then_some(schedule)
70        })
71    }
72
73    /// Registers a schedule
74    pub fn register<T: ScheduleLabel + 'static>(&mut self, label: T) -> &mut Self {
75        let schedule = ReflectSchedule::from_label(label);
76        self.schedules.insert(TypeId::of::<T>(), schedule);
77        self
78    }
79
80    /// Retrieves the given schedule
81    pub fn get(&self, type_id: TypeId) -> Option<&ReflectSchedule> {
82        self.schedules.get(&type_id)
83    }
84
85    /// Retrieves the given schedule mutably
86    pub fn get_mut(&mut self, type_id: TypeId) -> Option<&mut ReflectSchedule> {
87        self.schedules.get_mut(&type_id)
88    }
89
90    /// Checks if the given schedule is contained
91    pub fn contains(&self, type_id: TypeId) -> bool {
92        self.schedules.contains_key(&type_id)
93    }
94
95    /// Creates an iterator over all schedules
96    pub fn iter(&self) -> impl Iterator<Item = (&TypeId, &ReflectSchedule)> {
97        self.schedules.iter()
98    }
99
100    /// Creates an iterator over all schedules mutably
101    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&TypeId, &mut ReflectSchedule)> {
102        self.schedules.iter_mut()
103    }
104}
105
106#[cfg(test)]
107#[allow(
108    dead_code,
109    unused_imports,
110    reason = "tests are there but not working currently"
111)]
112mod tests {
113    // use crate::config::{GetPluginThreadConfig, ScriptingPluginConfiguration};
114    use ::{
115        bevy_app::{App, Plugin, Update},
116        bevy_ecs::{
117            entity::Entity,
118            schedule::{NodeId, Schedules, SystemKey},
119            system::IntoSystem,
120        },
121        bevy_system_reflection::ReflectSystem,
122        std::{cell::OnceCell, rc::Rc},
123    };
124
125    // use test_utils::make_test_plugin;
126
127    use super::*;
128
129    #[test]
130    fn test_schedule_registry() {
131        let mut registry = ScheduleRegistry::default();
132        registry.register(Update);
133
134        assert!(registry.contains(TypeId::of::<Update>()));
135
136        let schedule = registry.get(TypeId::of::<Update>()).unwrap();
137        assert_eq!(schedule.identifier(), "Update");
138        assert_eq!(schedule.type_path(), std::any::type_name::<Update>());
139        assert_eq!(
140            registry
141                .get_schedule_by_name("Update")
142                .unwrap()
143                .identifier(),
144            "Update"
145        );
146    }
147
148    fn test_system_generic<T>() {}
149    fn test_system() {}
150
151    #[test]
152    fn test_reflect_system_names() {
153        let system = IntoSystem::into_system(test_system_generic::<String>);
154        let system = ReflectSystem::from_system(&system, SystemKey::default());
155
156        assert_eq!(system.identifier(), "test_system_generic");
157        assert_eq!(
158            system.path(),
159            "bevy_mod_scripting_bindings::schedule::tests::test_system_generic<alloc::string::String>"
160        );
161
162        let system = IntoSystem::into_system(test_system);
163        let system = ReflectSystem::from_system(&system, SystemKey::default());
164
165        assert_eq!(system.identifier(), "test_system");
166        assert_eq!(
167            system.path(),
168            "bevy_mod_scripting_bindings::schedule::tests::test_system"
169        );
170    }
171
172    // make_test_plugin!(crate);
173
174    // #[test]
175    // fn test_into_system_set_identical_for_real_and_reflect_set() {
176    //     let root_system = || {};
177    //     let as_system = IntoSystem::into_system(root_system);
178    //     let as_reflect_system = ReflectSystem::from_system(&as_system);
179
180    //     let set1 = Box::new(IntoSystemSet::into_system_set(root_system)) as Box<dyn SystemSet>;
181    //     let set2 =
182    //         Box::new(IntoSystemSet::into_system_set(as_reflect_system)) as Box<dyn SystemSet>;
183
184    //     let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
185    //     set1.dyn_hash(&mut hasher1);
186    //     let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
187    //     set2.dyn_hash(&mut hasher2);
188    //     pretty_assertions::assert_eq!(hasher1.finish(), hasher2.finish());
189
190    //     pretty_assertions::assert_eq!(set1.system_type(), set2.system_type());
191    //     assert!(set1.dyn_eq(&set2));
192    // }
193
194    #[derive(ScheduleLabel, Hash, PartialEq, Eq, Debug, Clone)]
195    struct TestSchedule;
196
197    fn test_system_a() {}
198    fn test_system_b() {}
199
200    /// Verifies that the given schedule graph contains the expected node names and edges.
201    ///
202    /// # Arguments
203    ///
204    /// * `app` - A mutable reference to the Bevy App.
205    /// * `schedule_label` - The schedule label to locate the schedule.
206    /// * `expected_nodes` - A slice of node names expected to be present.
207    /// * `expected_edges` - A slice of tuples representing expected edges (from, to).
208    pub fn verify_schedule_graph<T>(
209        app: &mut App,
210        schedule_label: T,
211        expected_nodes: &[&str],
212        expected_edges: &[(&str, &str)],
213    ) where
214        T: ScheduleLabel + std::hash::Hash + Eq + Clone + Send + Sync + 'static,
215    {
216        // Remove schedules, then remove the schedule to verify.
217        let mut schedules = app
218            .world_mut()
219            .remove_resource::<Schedules>()
220            .expect("Schedules resource not found");
221        let mut schedule = schedules
222            .remove(schedule_label.clone())
223            .expect("Schedule not found");
224
225        schedule.initialize(app.world_mut()).unwrap();
226        let graph = schedule.graph();
227
228        // Build a mapping from system name to its node id.
229
230        let resolve_name = |node_id: NodeId| {
231            let out = {
232                let name = match node_id {
233                    NodeId::System(system_key) => graph
234                        .systems
235                        .get(system_key)
236                        .map(|system| system.system().name().clone().to_string()),
237                    NodeId::Set(system_set_key) => graph
238                        .system_sets
239                        .get(system_set_key)
240                        .map(|set| format!("{set:?}").to_string()),
241                };
242
243                if let Some(name) = name {
244                    name
245                } else {
246                    // try schedule systems
247                    let mut default = format!("{node_id:?}").to_string();
248                    for (system_node, system) in schedule.systems().unwrap() {
249                        if node_id == NodeId::System(system_node) {
250                            default = system.name().clone().to_string();
251                        }
252                    }
253                    default
254                }
255            };
256
257            // trim module path
258            let trim = "bevy_mod_scripting_bindings::schedule::tests::";
259            out.replace(trim, "")
260        };
261
262        let all_nodes = graph
263            .dependency()
264            .graph()
265            .nodes()
266            .map(&resolve_name)
267            .collect::<Vec<_>>();
268
269        // Assert expected nodes exist.
270        for &node in expected_nodes {
271            assert!(
272                all_nodes.contains(&node.to_owned()),
273                "Graph does not contain expected node '{node}' nodes: {all_nodes:?}"
274            );
275        }
276
277        // Collect all edges as (from, to) name pairs.
278        let mut found_edges = Vec::new();
279        for (from, to) in graph.dependency().graph().all_edges() {
280            let name_from = resolve_name(from);
281            let name_to = resolve_name(to);
282            found_edges.push((name_from, name_to));
283        }
284
285        // Assert each expected edge exists.
286        for &(exp_from, exp_to) in expected_edges {
287            assert!(
288                found_edges.contains(&(exp_from.to_owned(), exp_to.to_owned())),
289                "Expected edge ({exp_from} -> {exp_to}) not found. Found edges: {found_edges:?}"
290            );
291        }
292
293        // Optionally, reinsert the schedule back into the schedules resource.
294        schedules.insert(schedule);
295        app.world_mut().insert_resource(schedules);
296    }
297
298    // #[test]
299    // fn test_builder_creates_correct_system_graph_against_rust_systems() {
300    //     let mut app = App::new();
301    //     app.add_plugins((
302    //         bevy::asset::AssetPlugin::default(),
303    //         bevy::diagnostic::DiagnosticsPlugin,
304    //         TestPlugin::default(), // assuming TestPlugin is defined appropriately
305    //     ));
306
307    //     let system_a = IntoSystem::into_system(test_system_a);
308
309    //     let system_b = IntoSystem::into_system(test_system_b);
310
311    //     let mut system_builder = ScriptSystemBuilder::new("test".into(), ScriptId::from("test"));
312    //     // Set ordering: script system runs after "root1" and before "root2".
313    //     system_builder
314    //         .after(ReflectSystem::from_system(&system_a, NodeId::System(0)))
315    //         .before(ReflectSystem::from_system(&system_b, NodeId::System(1)));
316
317    //     app.init_schedule(TestSchedule);
318    //     app.add_systems(TestSchedule, system_a);
319    //     app.add_systems(TestSchedule, system_b);
320    //     let _ = system_builder.build::<TestPlugin>(
321    //         WorldGuard::new(app.world_mut()),
322    //         &ReflectSchedule::from_label(TestSchedule),
323    //     );
324
325    //     verify_schedule_graph(
326    //         &mut app,
327    //         TestSchedule,
328    //         // expected nodes
329    //         &["test_system_a", "test_system_b", "script_system_test"],
330    //         // expected edges (from, to), i.e. before, after, relationships
331    //         &[
332    //             ("SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem<fn(), test_system_a>())", "script_system_test"),
333    //             ("script_system_test", "SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem<fn(), test_system_b>())"),
334    //         ],
335    //     );
336    // }
337
338    // #[test]
339    // fn test_builder_creates_correct_system_graph_against_script_systems() {
340    //     let mut app = App::new();
341    //     app.add_plugins((
342    //         bevy::asset::AssetPlugin::default(),
343    //         bevy::diagnostic::DiagnosticsPlugin,
344    //         TestPlugin::default(), // assuming TestPlugin is defined appropriately
345    //     ));
346    //     app.init_schedule(TestSchedule);
347    //     let reflect_schedule = ReflectSchedule::from_label(TestSchedule);
348
349    //     let system_root = ScriptSystemBuilder::new("root".into(), "script_root.lua".into())
350    //         .build::<TestPlugin>(WorldGuard::new(app.world_mut()), &reflect_schedule)
351    //         .unwrap();
352
353    //     let mut system_a = ScriptSystemBuilder::new("a".into(), "script_a.lua".into());
354    //     system_a.before(system_root.clone());
355    //     system_a
356    //         .build::<TestPlugin>(WorldGuard::new(app.world_mut()), &reflect_schedule)
357    //         .unwrap();
358
359    //     let mut system_b = ScriptSystemBuilder::new("b".into(), "script_b.lua".into());
360    //     system_b.after(system_root.clone());
361    //     system_b
362    //         .build::<TestPlugin>(WorldGuard::new(app.world_mut()), &reflect_schedule)
363    //         .unwrap();
364
365    //     verify_schedule_graph(
366    //         &mut app,
367    //         TestSchedule,
368    //         // expected nodes
369    //         &["script_system_root", "script_system_a", "script_system_b"],
370    //         // expected edges (from, to), i.e. before, after, relationships
371    //         &[
372    //             // this doesn't work currently TODO: fix this, i.e. we inject the systems but not the ordering constraints
373    //             // ("SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem<fn(), test_system_a>())", "script_system_test"),
374    //             // ("script_system_test", "SystemTypeSet(fn bevy_ecs::system::function_system::FunctionSystem<fn(), test_system_b>())"),
375    //         ],
376    //     );
377    // }
378}