use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;
use bevy_app::prelude::*;
use bevy_ecs::observer::Observer;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{InternedScheduleLabel, Schedules, SystemKey};
use bevy_log::error;
use bevy_reflect::prelude::*;
use rig_ecs::prelude::Notice;
use rig_ecs::restore::SavedKeys;
use rig_ecs::stage::AgentPass;
#[derive(Component, Reflect, Clone, Debug)]
#[reflect(Component, Clone, Debug)]
pub struct PluginSource {
pub package: String,
pub source: String,
}
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
#[relationship(relationship_target = Provides)]
pub struct ProvidedBy(pub Entity);
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
#[relationship_target(relationship = ProvidedBy)]
pub struct Provides(Vec<Entity>);
#[derive(Resource)]
pub struct PluginEntity<P: Plugin> {
entity: Entity,
plugin: PhantomData<fn() -> P>,
}
impl<P: Plugin> PluginEntity<P> {
pub fn entity(&self) -> Entity {
self.entity
}
}
#[derive(Resource, Default, Debug)]
pub struct PluginSystems(HashMap<(String, String), Option<Entity>>);
impl PluginSystems {
pub fn plugin(&self, system: &str, schedule: &str) -> Option<Entity> {
let key = (system.to_owned(), schedule.to_owned());
self.0.get(&key).copied().flatten()
}
fn record(
world: &mut World,
before: &HashSet<(InternedScheduleLabel, SystemKey)>,
plugin: Entity,
) {
let mut added = Vec::new();
if let Some(schedules) = world.get_resource::<Schedules>() {
for (label, schedule) in schedules.iter() {
for (key, system, _) in schedule.graph().systems.iter() {
if !before.contains(&(schedule.label(), key)) {
added.push((system.name().to_string(), format!("{label:?}")));
}
}
}
}
let mut systems = world.get_resource_or_init::<PluginSystems>();
for key in added {
let by = systems.0.entry(key).or_insert(Some(plugin));
if *by != Some(plugin) {
*by = None;
}
}
}
}
pub fn stage_conflicts(world: &mut World) -> Vec<String> {
let label = format!("{AgentPass:?}");
let conflicts = world.try_schedule_scope(AgentPass, |world, pass| {
let built = pass.systems().into_iter().flatten();
let names: HashMap<SystemKey, String> = (pass.graph().systems.iter())
.map(|(key, system, _)| (key, system))
.chain(built)
.map(|(key, system)| (key, system.name().to_string()))
.collect();
let failure = pass.initialize(world).err();
let graph = pass.graph();
let name = |key| {
names
.get(key)
.cloned()
.unwrap_or_else(|| "an unnamed system".to_owned())
};
let conflicts: Vec<(String, String, String)> = (graph.conflicting_systems().iter())
.map(|(first, second, data)| {
let data = data
.iter()
.filter_map(|id| world.components().get_name(*id));
let data = data.map(|name| name.to_string()).collect::<Vec<_>>();
(name(first), name(second), data.join(", "))
})
.collect();
match failure {
Some(failure) if conflicts.is_empty() => vec![(
String::new(),
String::new(),
failure.to_string(graph, world),
)],
_ => conflicts,
}
});
let systems = world.get_resource::<PluginSystems>();
let plugin = |system: &str| {
systems
.and_then(|systems| systems.plugin(system, &label))
.and_then(|plugin| world.get::<Name>(plugin))
.map_or_else(|| "the harness".to_owned(), |name| format!("`{name}`"))
};
let conflicts = conflicts.unwrap_or_default().into_iter();
conflicts
.map(|(first, second, data)| {
if first.is_empty() {
return format!("the agent loop does not build: {data}");
}
let on = if data.is_empty() {
"the whole world".to_owned()
} else {
data
};
let plugins = match (plugin(&first), plugin(&second)) {
(one, other) if one == other => one,
(one, other) => format!("{one} and {other}"),
};
format!("{plugins}: {first} and {second}, on {on}")
})
.collect()
}
pub fn check_stages_at_start(app: &mut App) {
app.add_systems(Startup, warn_stage_conflicts);
}
fn warn_stage_conflicts(world: &mut World) {
for conflict in stage_conflicts(world) {
world.write_message(Notice::warn(
None,
format!(
"Agent-loop systems use the same data with no order between them, so which \
runs first is chance: {conflict}"
),
));
}
}
fn systems(world: &World) -> HashSet<(InternedScheduleLabel, SystemKey)> {
let Some(schedules) = world.get_resource::<Schedules>() else {
return HashSet::new();
};
let systems = schedules.iter().flat_map(|(_, schedule)| {
(schedule.graph().systems.iter()).map(|(key, ..)| (schedule.label(), key))
});
systems.collect()
}
pub fn load<P: Plugin + Default>(app: &mut App, package: &str, source: &str) {
if app.is_plugin_added::<P>() {
return;
}
let plugin = P::default();
let source = PluginSource {
package: package.to_owned(),
source: source.to_owned(),
};
let entity = app
.world_mut()
.spawn((Name::new(plugin.name().to_owned()), source))
.id();
app.insert_resource(PluginEntity::<P> {
entity,
plugin: PhantomData,
});
let attribute = attribute(app.world_mut(), entity);
let before = systems(app.world());
let outer = app.world_mut().remove_resource::<Loading>();
app.insert_resource(Loading(entity));
app.add_plugins(Attribute::Open(entity));
app.add_plugins(plugin);
app.add_plugins(Attribute::Close);
app.world_mut().remove_resource::<Loading>();
if let Some(outer) = outer {
app.insert_resource(outer);
}
PluginSystems::record(app.world_mut(), &before, entity);
stop_attributing(app.world_mut(), attribute);
}
#[derive(Component, Reflect, Clone, Debug, Default, PartialEq, Eq)]
#[reflect(Component, Default, Clone, Debug)]
pub struct Keeps {
pub saved: Vec<String>,
pub files: Vec<String>,
}
impl Keeps {
pub fn saved(&self, key: &str) -> bool {
self.saved.iter().any(|saved| saved == key)
}
pub fn file(&self, name: &str) -> bool {
self.files.iter().any(|folder| {
name.strip_prefix(folder.as_str())
.is_some_and(|rest| rest.starts_with('/'))
})
}
}
pub trait AppKeepsExt {
fn keeps_saved<T: Component + TypePath>(&mut self) -> &mut Self;
fn keeps_files(&mut self, folder: &str) -> &mut Self;
fn saved_as<T: TypePath>(&mut self, key: &'static str) -> SavedAs<'_>;
}
#[must_use = "a saved key is recorded already; call `listed` to list its values too"]
pub struct SavedAs<'a> {
app: &'a mut App,
key: Option<&'static str>,
}
impl<'a> SavedAs<'a> {
pub fn listed(self) -> &'a mut App {
if let Some(key) = self.key {
self.app
.world_mut()
.get_resource_or_init::<SavedKeys>()
.list(key);
}
self.app
}
}
impl AppKeepsExt for App {
fn keeps_saved<T: Component + TypePath>(&mut self) -> &mut Self {
keep(self.world_mut(), |keeps| &mut keeps.saved, T::type_path());
self
}
fn keeps_files(&mut self, folder: &str) -> &mut Self {
let folder = folder.trim_end_matches('/');
keep(self.world_mut(), |keeps| &mut keeps.files, folder);
self
}
fn saved_as<T: TypePath>(&mut self, key: &'static str) -> SavedAs<'_> {
let world = self.world_mut();
let added = world
.get_resource_or_init::<SavedKeys>()
.add(T::type_path(), key);
let key = match added {
Ok(()) => {
keep(world, |keeps| &mut keeps.saved, key);
Some(key)
}
Err(failure) => {
error!("not saving `{}` under `{key}`: {failure}", T::type_path());
None
}
};
SavedAs { app: self, key }
}
}
fn keep(world: &mut World, of: fn(&mut Keeps) -> &mut Vec<String>, what: &str) {
let loading = world.get_resource::<Loading>().map(|loading| loading.0);
let plugin = loading.or_else(|| Some(world.get_resource::<Attributing>()?.plugin));
let Some(mut plugin) = plugin.and_then(|plugin| world.get_entity_mut(plugin).ok()) else {
return;
};
if !plugin.contains::<Keeps>() {
plugin.insert(Keeps::default());
}
let Some(mut keeps) = plugin.get_mut::<Keeps>() else {
return;
};
let list = of(&mut keeps);
if !list.iter().any(|kept| kept == what) {
list.push(what.to_owned());
}
}
#[derive(Resource)]
struct Loading(Entity);
fn attribute(world: &mut World, plugin: Entity) -> Entity {
world
.add_observer(
move |added: On<Add<(Name, Observer)>>, mut commands: Commands| {
commands
.entity(added.entity)
.try_insert_if_new(ProvidedBy(plugin));
},
)
.id()
}
fn stop_attributing(world: &mut World, attribute: Entity) {
world.flush();
world.despawn(attribute);
}
enum Attribute {
Open(Entity),
Close,
}
#[derive(Resource)]
struct Attributing {
plugin: Entity,
observer: Entity,
before: HashSet<(InternedScheduleLabel, SystemKey)>,
}
impl Attribute {
fn bracket(&self, app: &mut App) {
let world = app.world_mut();
match self {
Self::Open(plugin) => {
let observer = attribute(world, *plugin);
let before = systems(world);
world.insert_resource(Attributing {
plugin: *plugin,
observer,
before,
});
}
Self::Close => {
if let Some(attributing) = world.remove_resource::<Attributing>() {
PluginSystems::record(world, &attributing.before, attributing.plugin);
stop_attributing(world, attributing.observer);
}
}
}
}
}
impl Plugin for Attribute {
fn build(&self, _app: &mut App) {}
fn finish(&self, app: &mut App) {
self.bracket(app);
}
fn cleanup(&self, app: &mut App) {
self.bracket(app);
}
fn is_unique(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests;