Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_dev_tools/src/schedule_data/plugin.rs
30636 views
1
//! Convenience plugin for automatically performing serialization of schedules on boot.
2
use std::{fs::File, io::Write, path::PathBuf};
3
4
use bevy_app::{App, Main, Plugin};
5
use bevy_ecs::{
6
error::{BevyError, ResultSeverityExt, Severity},
7
intern::Interned,
8
resource::Resource,
9
schedule::{
10
common_conditions::run_once, IntoScheduleConfigs, ScheduleLabel, Schedules, SystemSet,
11
},
12
world::World,
13
};
14
use bevy_platform::collections::HashMap;
15
use ron::ser::PrettyConfig;
16
17
use crate::schedule_data::serde::AppData;
18
19
/// A plugin to automatically collect and write all schedule data on boot to a file that can later
20
/// be parsed.
21
///
22
/// By default, the schedule data is written to `<current working directory>/app_data.ron`. This can
23
/// be configured to a different path using [`SerializeSchedulesFilePath`].
24
pub struct SerializeSchedulesPlugin {
25
/// The schedule into which the systems for collecting/writing the schedule data are added.
26
///
27
/// This schedule **will not** have its schedule data collected, as well as any "parent"
28
/// schedules. In order to run a schedule, Bevy removes it from the world, meaning if this
29
/// system is added to schedule [`Update`](bevy_app::Update), that schedule and also [`Main`]
30
/// will not be included in the [`AppData`]. The default is the [`Main`] schedule since usually
31
/// there is only one system ([`Main::run_main`]), so there's very little data to collect.
32
///
33
/// Avoid changing this field. This is intended for power-users who might not use the [`Main`]
34
/// schedule at all. It may also be worth considering just calling [`AppData::from_schedules`]
35
/// manually to ensure a particular schedule is present.
36
///
37
/// Usually, this will be set using [`Self::in_schedule`].
38
pub schedule: Interned<dyn ScheduleLabel>,
39
}
40
41
impl Default for SerializeSchedulesPlugin {
42
fn default() -> Self {
43
Self {
44
schedule: Main.intern(),
45
}
46
}
47
}
48
49
impl SerializeSchedulesPlugin {
50
/// Creates an instance of [`Self`] that inserts into the specified schedule.
51
pub fn in_schedule(label: impl ScheduleLabel) -> Self {
52
Self {
53
schedule: label.intern(),
54
}
55
}
56
}
57
58
impl Plugin for SerializeSchedulesPlugin {
59
fn build(&self, app: &mut App) {
60
app.init_resource::<SerializeSchedulesFilePath>()
61
.add_systems(
62
self.schedule,
63
collect_system_data
64
.run_if(run_once)
65
.in_set(SerializeSchedulesSystems)
66
// While we may not be in the `Main` schedule at all, the default is that, so we
67
// should make this work properly in the default case.
68
.before(Main::run_main),
69
);
70
}
71
}
72
73
/// A system set for allowing users to configure scheduling properties of systems in
74
/// [`SerializeSchedulesPlugin`].
75
#[derive(SystemSet, Hash, PartialEq, Eq, Debug, Clone)]
76
pub struct SerializeSchedulesSystems;
77
78
/// The file path where schedules will be written to after collected by
79
/// [`SerializeSchedulesPlugin`].
80
#[derive(Resource)]
81
pub struct SerializeSchedulesFilePath(pub PathBuf);
82
83
impl Default for SerializeSchedulesFilePath {
84
fn default() -> Self {
85
Self("app_data.ron".into())
86
}
87
}
88
89
/// The inner part of [`collect_system_data`] that returns the [`AppData`] so we can write tests
90
/// without needing to write to disk.
91
fn collect_system_data_inner(world: &mut World) -> Result<AppData, BevyError> {
92
let schedules = world.resource::<Schedules>();
93
let labels = schedules
94
.iter()
95
.map(|schedule| schedule.1.label())
96
.collect::<Vec<_>>();
97
let mut label_to_build_metadata = HashMap::new();
98
99
for label in labels {
100
// Hokey pokey the schedule out of the world so we can initialize it. Note: we can't just
101
// remove the whole `Schedule` resource since `Schedule::initialize` accesses `Schedules`
102
// internally.
103
let result = world.schedule_scope(label, |world, schedule| schedule.initialize(world));
104
let Some(build_metadata) = result? else {
105
return Err(
106
"The schedule has already been built, so we can't collect its system data".into(),
107
);
108
};
109
110
label_to_build_metadata.insert(label, build_metadata);
111
}
112
113
let schedules = world.resource::<Schedules>();
114
Ok(AppData::from_schedules(
115
schedules,
116
world.components(),
117
&label_to_build_metadata,
118
)?)
119
}
120
121
/// A system that collects all the schedule data and writes it to [`SerializeSchedulesFilePath`].
122
fn collect_system_data(world: &mut World) -> Result<(), BevyError> {
123
let app_data = collect_system_data_inner(world).with_severity(Severity::Warning)?;
124
let file_path = world
125
.get_resource::<SerializeSchedulesFilePath>()
126
.ok_or("Missing SerializeSchedulesFilePath resource")
127
.with_severity(Severity::Warning)?;
128
let mut file = File::create(&file_path.0).with_severity(Severity::Warning)?;
129
// Use \n unconditionally so that Windows formatting is predictable.
130
let serialized = ron::ser::to_string_pretty(&app_data, PrettyConfig::default().new_line("\n"))?;
131
file.write_all(serialized.as_bytes())
132
.with_severity(Severity::Warning)?;
133
Ok(())
134
}
135
136
#[cfg(test)]
137
mod tests {
138
use bevy_app::{App, PostUpdate, Update};
139
140
use crate::schedule_data::{
141
plugin::collect_system_data_inner,
142
serde::tests::{remove_module_paths, simple_system, sort_app_data},
143
};
144
145
#[test]
146
fn collects_all_schedules() {
147
// Start with an empty app so only our stuff gets added.
148
let mut app = App::empty();
149
150
fn a() {}
151
fn b() {}
152
fn c() {}
153
app.add_systems(Update, (a, b));
154
app.add_systems(PostUpdate, c);
155
156
// Normally users would use the plugin, but to avoid writing to disk in a test, we just call
157
// the inner part of the system directly.
158
let mut app_data = collect_system_data_inner(app.world_mut()).unwrap();
159
remove_module_paths(&mut app_data);
160
sort_app_data(&mut app_data);
161
162
assert_eq!(app_data.schedules.len(), 2);
163
let post_update = &app_data.schedules[0];
164
assert_eq!(post_update.name, "PostUpdate");
165
assert_eq!(post_update.systems, [simple_system("c")]);
166
let update = &app_data.schedules[1];
167
assert_eq!(update.name, "Update");
168
assert_eq!(update.systems, [simple_system("a"), simple_system("b")]);
169
}
170
171
#[test]
172
fn uses_safe_schedule_scope() {
173
// This tests a niche situation where a schedule has already been built when
174
// `collect_system_data_inner` runs. Since this method runs before the `Main` schedule, this
175
// can only happen if either: a) the user is using a custom schedule, or b) the user runs a
176
// schedule from **inside a plugin** - which is extremely cursed. Either way, better to be
177
// safe than sorry!
178
179
// Start with an empty app so only our stuff gets added.
180
let mut app = App::empty();
181
182
fn a() {}
183
app.add_systems(Update, a);
184
app.world_mut().run_schedule(Update);
185
186
// Normally users would use the plugin, but to avoid writing to disk in a test, we just call
187
// the inner part of the system directly.
188
189
// We expect an error since the schedule has already been built.
190
collect_system_data_inner(app.world_mut()).unwrap_err();
191
192
// If the schedule is missing, this would panic! This could happen if there was an error
193
// extracting the schedule data, and we didn't hokey-pokey safely.
194
app.world_mut().schedule_scope(Update, |_, _| {});
195
}
196
}
197
198