use std::{fs::File, io::Write, path::PathBuf};
use bevy_app::{App, Main, Plugin};
use bevy_ecs::{
error::{BevyError, ResultSeverityExt, Severity},
intern::Interned,
resource::Resource,
schedule::{
common_conditions::run_once, IntoScheduleConfigs, ScheduleLabel, Schedules, SystemSet,
},
world::World,
};
use bevy_platform::collections::HashMap;
use ron::ser::PrettyConfig;
use crate::schedule_data::serde::AppData;
pub struct SerializeSchedulesPlugin {
pub schedule: Interned<dyn ScheduleLabel>,
}
impl Default for SerializeSchedulesPlugin {
fn default() -> Self {
Self {
schedule: Main.intern(),
}
}
}
impl SerializeSchedulesPlugin {
pub fn in_schedule(label: impl ScheduleLabel) -> Self {
Self {
schedule: label.intern(),
}
}
}
impl Plugin for SerializeSchedulesPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SerializeSchedulesFilePath>()
.add_systems(
self.schedule,
collect_system_data
.run_if(run_once)
.in_set(SerializeSchedulesSystems)
.before(Main::run_main),
);
}
}
#[derive(SystemSet, Hash, PartialEq, Eq, Debug, Clone)]
pub struct SerializeSchedulesSystems;
#[derive(Resource)]
pub struct SerializeSchedulesFilePath(pub PathBuf);
impl Default for SerializeSchedulesFilePath {
fn default() -> Self {
Self("app_data.ron".into())
}
}
fn collect_system_data_inner(world: &mut World) -> Result<AppData, BevyError> {
let schedules = world.resource::<Schedules>();
let labels = schedules
.iter()
.map(|schedule| schedule.1.label())
.collect::<Vec<_>>();
let mut label_to_build_metadata = HashMap::new();
for label in labels {
let result = world.schedule_scope(label, |world, schedule| schedule.initialize(world));
let Some(build_metadata) = result? else {
return Err(
"The schedule has already been built, so we can't collect its system data".into(),
);
};
label_to_build_metadata.insert(label, build_metadata);
}
let schedules = world.resource::<Schedules>();
Ok(AppData::from_schedules(
schedules,
world.components(),
&label_to_build_metadata,
)?)
}
fn collect_system_data(world: &mut World) -> Result<(), BevyError> {
let app_data = collect_system_data_inner(world).with_severity(Severity::Warning)?;
let file_path = world
.get_resource::<SerializeSchedulesFilePath>()
.ok_or("Missing SerializeSchedulesFilePath resource")
.with_severity(Severity::Warning)?;
let mut file = File::create(&file_path.0).with_severity(Severity::Warning)?;
let serialized = ron::ser::to_string_pretty(&app_data, PrettyConfig::default().new_line("\n"))?;
file.write_all(serialized.as_bytes())
.with_severity(Severity::Warning)?;
Ok(())
}
#[cfg(test)]
mod tests {
use bevy_app::{App, PostUpdate, Update};
use crate::schedule_data::{
plugin::collect_system_data_inner,
serde::tests::{remove_module_paths, simple_system, sort_app_data},
};
#[test]
fn collects_all_schedules() {
let mut app = App::empty();
fn a() {}
fn b() {}
fn c() {}
app.add_systems(Update, (a, b));
app.add_systems(PostUpdate, c);
let mut app_data = collect_system_data_inner(app.world_mut()).unwrap();
remove_module_paths(&mut app_data);
sort_app_data(&mut app_data);
assert_eq!(app_data.schedules.len(), 2);
let post_update = &app_data.schedules[0];
assert_eq!(post_update.name, "PostUpdate");
assert_eq!(post_update.systems, [simple_system("c")]);
let update = &app_data.schedules[1];
assert_eq!(update.name, "Update");
assert_eq!(update.systems, [simple_system("a"), simple_system("b")]);
}
#[test]
fn uses_safe_schedule_scope() {
let mut app = App::empty();
fn a() {}
app.add_systems(Update, a);
app.world_mut().run_schedule(Update);
collect_system_data_inner(app.world_mut()).unwrap_err();
app.world_mut().schedule_scope(Update, |_, _| {});
}
}