use std::fmt::Write;
use bevy_app::{App, Startup};
use bevy_ecs::{
schedule::{IntoScheduleConfigs, ScheduleConfigs},
system::ScheduleSystem,
};
use rand::distr::{Alphanumeric, SampleString};
mod rng;
mod schedule;
use self::rng::get_rng;
use self::schedule::StartupTreeLayer;
pub use bevy_startup_tree_macros::startup_tree;
const NAMESPACE_LEN: usize = 6;
pub trait AddStartupTree {
fn add_startup_tree<I2, I>(&mut self, startup_tree: I2) -> &mut Self
where
I2: IntoIterator<Item = I>,
I: IntoIterator<Item = ScheduleConfigs<ScheduleSystem>>;
}
impl AddStartupTree for App {
fn add_startup_tree<I2, I>(&mut self, startup_tree: I2) -> &mut Self
where
I2: IntoIterator<Item = I>,
I: IntoIterator<Item = ScheduleConfigs<ScheduleSystem>>,
{
let mut rng = get_rng();
let namespace = Alphanumeric.sample_string(&mut rng, NAMESPACE_LEN);
let label_base = format!("__startup_tree_{namespace}");
startup_tree.into_iter().enumerate().fold(None, |last_layer_set, (i, level)| {
let mut label = label_base.clone();
write!(label, "_layer_{i}").unwrap();
let label: &str = label.leak();
let layer_set = StartupTreeLayer(label);
let layer_config = if let Some(last_layer_set) = last_layer_set {
layer_set.after(last_layer_set)
} else {
layer_set.into_configs()
};
self.configure_sets(Startup, layer_config);
for system in level {
self.add_systems(Startup, system.in_set(layer_set));
}
Some(layer_set)
});
self
}
}
pub use bevy_startup_tree_macros::system_tree;
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use bevy::prelude::{App, Schedules, Startup};
use crate::{rng::reset_rng, startup_tree, AddStartupTree};
fn get_app_startup_tree_labels(app: &App) -> impl Iterator<Item = String> + '_ {
let schedules = app.world().resource::<Schedules>();
let startup_schedule = schedules.get(Startup).expect("get startup schedule");
let startup_graph = startup_schedule.graph();
startup_graph
.hierarchy()
.graph()
.nodes()
.filter_map(|id| startup_graph.get_set_at(id))
.map(|set| format!("{set:#?}"))
.filter(|label| label.starts_with("__startup_tree"))
}
fn system() {}
#[test]
fn adds_sequential_labels() {
reset_rng();
let mut app = App::new();
app.add_startup_tree(startup_tree! {
system => {
system => system
}
});
let expected_labels = HashSet::from([
"__startup_tree_zujxzB_layer_0".into(),
"__startup_tree_zujxzB_layer_1".into(),
"__startup_tree_zujxzB_layer_2".into(),
]);
let actual_labels = HashSet::from_iter(get_app_startup_tree_labels(&app));
assert_eq!(actual_labels, expected_labels);
}
#[test]
fn adds_correct_labels_for_complex_tree() {
reset_rng();
let mut app = App::new();
app.add_startup_tree(startup_tree! {
system,
system => {
system => system,
system => {
system,
system => system,
}
},
system,
});
let expected_labels = HashSet::from([
"__startup_tree_zujxzB_layer_0".into(),
"__startup_tree_zujxzB_layer_1".into(),
"__startup_tree_zujxzB_layer_2".into(),
"__startup_tree_zujxzB_layer_3".into(),
]);
let actual_labels = HashSet::from_iter(get_app_startup_tree_labels(&app));
assert_eq!(actual_labels, expected_labels);
}
#[test]
fn multiple_trees_dont_reuse_labels() {
reset_rng();
let mut app = App::new();
app.add_startup_tree(startup_tree! { system });
app.add_startup_tree(startup_tree! { system });
let expected_labels = HashSet::from([
"__startup_tree_zujxzB_layer_0".into(),
"__startup_tree_ql3QHx_layer_0".into(),
]);
let actual_labels = HashSet::from_iter(get_app_startup_tree_labels(&app));
assert_eq!(actual_labels, expected_labels);
}
mod e2e {
use bevy::prelude::*;
use crate::{rng::reseed_rng, startup_tree, AddStartupTree};
#[derive(Resource, Debug)]
struct TestEventData(Vec<TestEvent>);
#[derive(Debug, PartialEq, Eq)]
enum TestEvent {
Begin,
One,
Two,
Three,
End,
}
macro_rules! test_systems {
($($name:ident => $event:path);+ $(;)?) => {
$( fn $name(mut data: NonSendMut<TestEventData>) { data.0.push($event); } )+
};
}
test_systems! {
begin => TestEvent::Begin;
sys_1_a => TestEvent::One;
sys_1_b => TestEvent::One;
sys_1_c => TestEvent::One;
sys_1_d => TestEvent::One;
sys_2_a => TestEvent::Two;
sys_2_b => TestEvent::Two;
sys_2_c => TestEvent::Two;
sys_2_d => TestEvent::Two;
sys_3_a => TestEvent::Three;
end => TestEvent::End;
}
#[test]
fn end_to_end_test() {
reseed_rng();
let mut app = App::new();
app.add_plugins(TaskPoolPlugin::default());
app.insert_non_send_resource(TestEventData(Vec::with_capacity(11)));
app.add_systems(PreStartup, begin);
app.add_startup_tree(startup_tree! {
sys_1_a => {
sys_2_a,
sys_2_b,
},
sys_1_b => {
sys_2_c,
sys_2_d => sys_3_a,
},
sys_1_c,
sys_1_d,
});
app.add_systems(PostStartup, end);
app.update();
assert_eq!(
app.world().non_send_resource::<TestEventData>().0,
&[
TestEvent::Begin,
TestEvent::One,
TestEvent::One,
TestEvent::One,
TestEvent::One,
TestEvent::Two,
TestEvent::Two,
TestEvent::Two,
TestEvent::Two,
TestEvent::Three,
TestEvent::End
]
);
}
}
}