Path: blob/main/crates/bevy_dev_tools/src/schedule_data/plugin.rs
30636 views
//! Convenience plugin for automatically performing serialization of schedules on boot.1use std::{fs::File, io::Write, path::PathBuf};23use bevy_app::{App, Main, Plugin};4use bevy_ecs::{5error::{BevyError, ResultSeverityExt, Severity},6intern::Interned,7resource::Resource,8schedule::{9common_conditions::run_once, IntoScheduleConfigs, ScheduleLabel, Schedules, SystemSet,10},11world::World,12};13use bevy_platform::collections::HashMap;14use ron::ser::PrettyConfig;1516use crate::schedule_data::serde::AppData;1718/// A plugin to automatically collect and write all schedule data on boot to a file that can later19/// be parsed.20///21/// By default, the schedule data is written to `<current working directory>/app_data.ron`. This can22/// be configured to a different path using [`SerializeSchedulesFilePath`].23pub struct SerializeSchedulesPlugin {24/// The schedule into which the systems for collecting/writing the schedule data are added.25///26/// This schedule **will not** have its schedule data collected, as well as any "parent"27/// schedules. In order to run a schedule, Bevy removes it from the world, meaning if this28/// system is added to schedule [`Update`](bevy_app::Update), that schedule and also [`Main`]29/// will not be included in the [`AppData`]. The default is the [`Main`] schedule since usually30/// there is only one system ([`Main::run_main`]), so there's very little data to collect.31///32/// Avoid changing this field. This is intended for power-users who might not use the [`Main`]33/// schedule at all. It may also be worth considering just calling [`AppData::from_schedules`]34/// manually to ensure a particular schedule is present.35///36/// Usually, this will be set using [`Self::in_schedule`].37pub schedule: Interned<dyn ScheduleLabel>,38}3940impl Default for SerializeSchedulesPlugin {41fn default() -> Self {42Self {43schedule: Main.intern(),44}45}46}4748impl SerializeSchedulesPlugin {49/// Creates an instance of [`Self`] that inserts into the specified schedule.50pub fn in_schedule(label: impl ScheduleLabel) -> Self {51Self {52schedule: label.intern(),53}54}55}5657impl Plugin for SerializeSchedulesPlugin {58fn build(&self, app: &mut App) {59app.init_resource::<SerializeSchedulesFilePath>()60.add_systems(61self.schedule,62collect_system_data63.run_if(run_once)64.in_set(SerializeSchedulesSystems)65// While we may not be in the `Main` schedule at all, the default is that, so we66// should make this work properly in the default case.67.before(Main::run_main),68);69}70}7172/// A system set for allowing users to configure scheduling properties of systems in73/// [`SerializeSchedulesPlugin`].74#[derive(SystemSet, Hash, PartialEq, Eq, Debug, Clone)]75pub struct SerializeSchedulesSystems;7677/// The file path where schedules will be written to after collected by78/// [`SerializeSchedulesPlugin`].79#[derive(Resource)]80pub struct SerializeSchedulesFilePath(pub PathBuf);8182impl Default for SerializeSchedulesFilePath {83fn default() -> Self {84Self("app_data.ron".into())85}86}8788/// The inner part of [`collect_system_data`] that returns the [`AppData`] so we can write tests89/// without needing to write to disk.90fn collect_system_data_inner(world: &mut World) -> Result<AppData, BevyError> {91let schedules = world.resource::<Schedules>();92let labels = schedules93.iter()94.map(|schedule| schedule.1.label())95.collect::<Vec<_>>();96let mut label_to_build_metadata = HashMap::new();9798for label in labels {99// Hokey pokey the schedule out of the world so we can initialize it. Note: we can't just100// remove the whole `Schedule` resource since `Schedule::initialize` accesses `Schedules`101// internally.102let result = world.schedule_scope(label, |world, schedule| schedule.initialize(world));103let Some(build_metadata) = result? else {104return Err(105"The schedule has already been built, so we can't collect its system data".into(),106);107};108109label_to_build_metadata.insert(label, build_metadata);110}111112let schedules = world.resource::<Schedules>();113Ok(AppData::from_schedules(114schedules,115world.components(),116&label_to_build_metadata,117)?)118}119120/// A system that collects all the schedule data and writes it to [`SerializeSchedulesFilePath`].121fn collect_system_data(world: &mut World) -> Result<(), BevyError> {122let app_data = collect_system_data_inner(world).with_severity(Severity::Warning)?;123let file_path = world124.get_resource::<SerializeSchedulesFilePath>()125.ok_or("Missing SerializeSchedulesFilePath resource")126.with_severity(Severity::Warning)?;127let mut file = File::create(&file_path.0).with_severity(Severity::Warning)?;128// Use \n unconditionally so that Windows formatting is predictable.129let serialized = ron::ser::to_string_pretty(&app_data, PrettyConfig::default().new_line("\n"))?;130file.write_all(serialized.as_bytes())131.with_severity(Severity::Warning)?;132Ok(())133}134135#[cfg(test)]136mod tests {137use bevy_app::{App, PostUpdate, Update};138139use crate::schedule_data::{140plugin::collect_system_data_inner,141serde::tests::{remove_module_paths, simple_system, sort_app_data},142};143144#[test]145fn collects_all_schedules() {146// Start with an empty app so only our stuff gets added.147let mut app = App::empty();148149fn a() {}150fn b() {}151fn c() {}152app.add_systems(Update, (a, b));153app.add_systems(PostUpdate, c);154155// Normally users would use the plugin, but to avoid writing to disk in a test, we just call156// the inner part of the system directly.157let mut app_data = collect_system_data_inner(app.world_mut()).unwrap();158remove_module_paths(&mut app_data);159sort_app_data(&mut app_data);160161assert_eq!(app_data.schedules.len(), 2);162let post_update = &app_data.schedules[0];163assert_eq!(post_update.name, "PostUpdate");164assert_eq!(post_update.systems, [simple_system("c")]);165let update = &app_data.schedules[1];166assert_eq!(update.name, "Update");167assert_eq!(update.systems, [simple_system("a"), simple_system("b")]);168}169170#[test]171fn uses_safe_schedule_scope() {172// This tests a niche situation where a schedule has already been built when173// `collect_system_data_inner` runs. Since this method runs before the `Main` schedule, this174// can only happen if either: a) the user is using a custom schedule, or b) the user runs a175// schedule from **inside a plugin** - which is extremely cursed. Either way, better to be176// safe than sorry!177178// Start with an empty app so only our stuff gets added.179let mut app = App::empty();180181fn a() {}182app.add_systems(Update, a);183app.world_mut().run_schedule(Update);184185// Normally users would use the plugin, but to avoid writing to disk in a test, we just call186// the inner part of the system directly.187188// We expect an error since the schedule has already been built.189collect_system_data_inner(app.world_mut()).unwrap_err();190191// If the schedule is missing, this would panic! This could happen if there was an error192// extracting the schedule data, and we didn't hokey-pokey safely.193app.world_mut().schedule_scope(Update, |_, _| {});194}195}196197198