1use std::collections::{HashMap, HashSet};
32use std::marker::PhantomData;
33
34use bevy_app::prelude::*;
35use bevy_ecs::observer::Observer;
36use bevy_ecs::prelude::*;
37use bevy_ecs::schedule::{InternedScheduleLabel, Schedules, SystemKey};
38use bevy_log::error;
39use bevy_reflect::prelude::*;
40use rig_ecs::prelude::Notice;
41use rig_ecs::restore::SavedKeys;
42use rig_ecs::stage::AgentPass;
43
44#[derive(Component, Reflect, Clone, Debug)]
46#[reflect(Component, Clone, Debug)]
47pub struct PluginSource {
48 pub package: String,
50 pub source: String,
53}
54
55#[derive(Component, Reflect, Debug)]
57#[reflect(Component)]
58#[relationship(relationship_target = Provides)]
59pub struct ProvidedBy(pub Entity);
60
61#[derive(Component, Reflect, Debug)]
63#[reflect(Component)]
64#[relationship_target(relationship = ProvidedBy)]
65pub struct Provides(Vec<Entity>);
66
67#[derive(Resource)]
70pub struct PluginEntity<P: Plugin> {
71 entity: Entity,
72 plugin: PhantomData<fn() -> P>,
73}
74
75impl<P: Plugin> PluginEntity<P> {
76 pub fn entity(&self) -> Entity {
78 self.entity
79 }
80}
81
82#[derive(Resource, Default, Debug)]
88pub struct PluginSystems(HashMap<(String, String), Option<Entity>>);
89
90impl PluginSystems {
91 pub fn plugin(&self, system: &str, schedule: &str) -> Option<Entity> {
94 let key = (system.to_owned(), schedule.to_owned());
95 self.0.get(&key).copied().flatten()
96 }
97
98 fn record(
100 world: &mut World,
101 before: &HashSet<(InternedScheduleLabel, SystemKey)>,
102 plugin: Entity,
103 ) {
104 let mut added = Vec::new();
105 if let Some(schedules) = world.get_resource::<Schedules>() {
106 for (label, schedule) in schedules.iter() {
107 for (key, system, _) in schedule.graph().systems.iter() {
108 if !before.contains(&(schedule.label(), key)) {
109 added.push((system.name().to_string(), format!("{label:?}")));
110 }
111 }
112 }
113 }
114 let mut systems = world.get_resource_or_init::<PluginSystems>();
115 for key in added {
116 let by = systems.0.entry(key).or_insert(Some(plugin));
117 if *by != Some(plugin) {
118 *by = None;
119 }
120 }
121 }
122}
123
124pub fn stage_conflicts(world: &mut World) -> Vec<String> {
133 let label = format!("{AgentPass:?}");
134 let conflicts = world.try_schedule_scope(AgentPass, |world, pass| {
135 let built = pass.systems().into_iter().flatten();
138 let names: HashMap<SystemKey, String> = (pass.graph().systems.iter())
139 .map(|(key, system, _)| (key, system))
140 .chain(built)
141 .map(|(key, system)| (key, system.name().to_string()))
142 .collect();
143 let failure = pass.initialize(world).err();
144 let graph = pass.graph();
145 let name = |key| {
146 names
147 .get(key)
148 .cloned()
149 .unwrap_or_else(|| "an unnamed system".to_owned())
150 };
151 let conflicts: Vec<(String, String, String)> = (graph.conflicting_systems().iter())
152 .map(|(first, second, data)| {
153 let data = data
154 .iter()
155 .filter_map(|id| world.components().get_name(*id));
156 let data = data.map(|name| name.to_string()).collect::<Vec<_>>();
157 (name(first), name(second), data.join(", "))
158 })
159 .collect();
160 match failure {
161 Some(failure) if conflicts.is_empty() => vec![(
162 String::new(),
163 String::new(),
164 failure.to_string(graph, world),
165 )],
166 _ => conflicts,
167 }
168 });
169 let systems = world.get_resource::<PluginSystems>();
170 let plugin = |system: &str| {
171 systems
172 .and_then(|systems| systems.plugin(system, &label))
173 .and_then(|plugin| world.get::<Name>(plugin))
174 .map_or_else(|| "the harness".to_owned(), |name| format!("`{name}`"))
175 };
176 let conflicts = conflicts.unwrap_or_default().into_iter();
177 conflicts
178 .map(|(first, second, data)| {
179 if first.is_empty() {
180 return format!("the agent loop does not build: {data}");
181 }
182 let on = if data.is_empty() {
183 "the whole world".to_owned()
184 } else {
185 data
186 };
187 let plugins = match (plugin(&first), plugin(&second)) {
188 (one, other) if one == other => one,
189 (one, other) => format!("{one} and {other}"),
190 };
191 format!("{plugins}: {first} and {second}, on {on}")
192 })
193 .collect()
194}
195
196pub fn check_stages_at_start(app: &mut App) {
199 app.add_systems(Startup, warn_stage_conflicts);
200}
201
202fn warn_stage_conflicts(world: &mut World) {
203 for conflict in stage_conflicts(world) {
204 world.write_message(Notice::warn(
205 None,
206 format!(
207 "Agent-loop systems use the same data with no order between them, so which \
208 runs first is chance: {conflict}"
209 ),
210 ));
211 }
212}
213
214fn systems(world: &World) -> HashSet<(InternedScheduleLabel, SystemKey)> {
216 let Some(schedules) = world.get_resource::<Schedules>() else {
217 return HashSet::new();
218 };
219 let systems = schedules.iter().flat_map(|(_, schedule)| {
220 (schedule.graph().systems.iter()).map(|(key, ..)| (schedule.label(), key))
221 });
222 systems.collect()
223}
224
225pub fn load<P: Plugin + Default>(app: &mut App, package: &str, source: &str) {
229 if app.is_plugin_added::<P>() {
230 return;
231 }
232 let plugin = P::default();
233 let source = PluginSource {
234 package: package.to_owned(),
235 source: source.to_owned(),
236 };
237 let entity = app
238 .world_mut()
239 .spawn((Name::new(plugin.name().to_owned()), source))
240 .id();
241 app.insert_resource(PluginEntity::<P> {
242 entity,
243 plugin: PhantomData,
244 });
245 let attribute = attribute(app.world_mut(), entity);
246 let before = systems(app.world());
247 let outer = app.world_mut().remove_resource::<Loading>();
248 app.insert_resource(Loading(entity));
249 app.add_plugins(Attribute::Open(entity));
253 app.add_plugins(plugin);
254 app.add_plugins(Attribute::Close);
255 app.world_mut().remove_resource::<Loading>();
256 if let Some(outer) = outer {
257 app.insert_resource(outer);
258 }
259 PluginSystems::record(app.world_mut(), &before, entity);
260 stop_attributing(app.world_mut(), attribute);
261}
262
263#[derive(Component, Reflect, Clone, Debug, Default, PartialEq, Eq)]
272#[reflect(Component, Default, Clone, Debug)]
273pub struct Keeps {
274 pub saved: Vec<String>,
276 pub files: Vec<String>,
278}
279
280impl Keeps {
281 pub fn saved(&self, key: &str) -> bool {
283 self.saved.iter().any(|saved| saved == key)
284 }
285
286 pub fn file(&self, name: &str) -> bool {
289 self.files.iter().any(|folder| {
290 name.strip_prefix(folder.as_str())
291 .is_some_and(|rest| rest.starts_with('/'))
292 })
293 }
294}
295
296pub trait AppKeepsExt {
300 fn keeps_saved<T: Component + TypePath>(&mut self) -> &mut Self;
302
303 fn keeps_files(&mut self, folder: &str) -> &mut Self;
306
307 fn saved_as<T: TypePath>(&mut self, key: &'static str) -> SavedAs<'_>;
314}
315
316#[must_use = "a saved key is recorded already; call `listed` to list its values too"]
318pub struct SavedAs<'a> {
319 app: &'a mut App,
320 key: Option<&'static str>,
322}
323
324impl<'a> SavedAs<'a> {
325 pub fn listed(self) -> &'a mut App {
330 if let Some(key) = self.key {
331 self.app
332 .world_mut()
333 .get_resource_or_init::<SavedKeys>()
334 .list(key);
335 }
336 self.app
337 }
338}
339
340impl AppKeepsExt for App {
341 fn keeps_saved<T: Component + TypePath>(&mut self) -> &mut Self {
342 keep(self.world_mut(), |keeps| &mut keeps.saved, T::type_path());
343 self
344 }
345
346 fn keeps_files(&mut self, folder: &str) -> &mut Self {
347 let folder = folder.trim_end_matches('/');
348 keep(self.world_mut(), |keeps| &mut keeps.files, folder);
349 self
350 }
351
352 fn saved_as<T: TypePath>(&mut self, key: &'static str) -> SavedAs<'_> {
353 let world = self.world_mut();
354 let added = world
355 .get_resource_or_init::<SavedKeys>()
356 .add(T::type_path(), key);
357 let key = match added {
358 Ok(()) => {
359 keep(world, |keeps| &mut keeps.saved, key);
360 Some(key)
361 }
362 Err(failure) => {
363 error!("not saving `{}` under `{key}`: {failure}", T::type_path());
364 None
365 }
366 };
367 SavedAs { app: self, key }
368 }
369}
370
371fn keep(world: &mut World, of: fn(&mut Keeps) -> &mut Vec<String>, what: &str) {
374 let loading = world.get_resource::<Loading>().map(|loading| loading.0);
375 let plugin = loading.or_else(|| Some(world.get_resource::<Attributing>()?.plugin));
376 let Some(mut plugin) = plugin.and_then(|plugin| world.get_entity_mut(plugin).ok()) else {
377 return;
378 };
379 if !plugin.contains::<Keeps>() {
380 plugin.insert(Keeps::default());
381 }
382 let Some(mut keeps) = plugin.get_mut::<Keeps>() else {
383 return;
384 };
385 let list = of(&mut keeps);
386 if !list.iter().any(|kept| kept == what) {
387 list.push(what.to_owned());
388 }
389}
390
391#[derive(Resource)]
393struct Loading(Entity);
394
395fn attribute(world: &mut World, plugin: Entity) -> Entity {
398 world
399 .add_observer(
400 move |added: On<Add<(Name, Observer)>>, mut commands: Commands| {
401 commands
402 .entity(added.entity)
403 .try_insert_if_new(ProvidedBy(plugin));
404 },
405 )
406 .id()
407}
408
409fn stop_attributing(world: &mut World, attribute: Entity) {
411 world.flush();
412 world.despawn(attribute);
413}
414
415enum Attribute {
418 Open(Entity),
419 Close,
420}
421
422#[derive(Resource)]
425struct Attributing {
426 plugin: Entity,
427 observer: Entity,
428 before: HashSet<(InternedScheduleLabel, SystemKey)>,
429}
430
431impl Attribute {
432 fn bracket(&self, app: &mut App) {
433 let world = app.world_mut();
434 match self {
435 Self::Open(plugin) => {
436 let observer = attribute(world, *plugin);
437 let before = systems(world);
438 world.insert_resource(Attributing {
439 plugin: *plugin,
440 observer,
441 before,
442 });
443 }
444 Self::Close => {
445 if let Some(attributing) = world.remove_resource::<Attributing>() {
446 PluginSystems::record(world, &attributing.before, attributing.plugin);
447 stop_attributing(world, attributing.observer);
448 }
449 }
450 }
451 }
452}
453
454impl Plugin for Attribute {
455 fn build(&self, _app: &mut App) {}
456
457 fn finish(&self, app: &mut App) {
458 self.bracket(app);
459 }
460
461 fn cleanup(&self, app: &mut App) {
462 self.bracket(app);
463 }
464
465 fn is_unique(&self) -> bool {
466 false
467 }
468}
469
470#[cfg(test)]
471mod tests;