use std::collections::HashSet;
use astrelis_core::profiling::{profile_function, profile_scope};
use crate::plugin::{Plugin, PluginDyn, PluginGroup, PluginGroupAdapter};
use crate::resource::Resources;
#[derive(Debug, Clone)]
pub enum EngineError {
CircularDependency {
plugin: &'static str,
chain: Vec<&'static str>,
},
MissingDependency {
plugin: &'static str,
dependency: &'static str,
},
DuplicatePlugin {
name: &'static str,
},
}
impl std::fmt::Display for EngineError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EngineError::CircularDependency { plugin, chain } => {
write!(
f,
"Circular dependency detected involving plugin '{}'. Chain: {:?}",
plugin, chain
)
}
EngineError::MissingDependency { plugin, dependency } => {
write!(
f,
"Plugin '{}' requires dependency '{}' which was not added",
plugin, dependency
)
}
EngineError::DuplicatePlugin { name } => {
write!(f, "Plugin '{}' was added more than once", name)
}
}
}
}
impl std::error::Error for EngineError {}
pub struct Engine {
resources: Resources,
plugin_names: HashSet<&'static str>,
plugins: Vec<Box<dyn PluginDyn>>,
}
impl Engine {
pub fn builder() -> EngineBuilder {
EngineBuilder::new()
}
pub fn resources(&self) -> &Resources {
&self.resources
}
pub fn resources_mut(&mut self) -> &mut Resources {
&mut self.resources
}
pub fn get<R: crate::resource::Resource>(&self) -> Option<&R> {
self.resources.get::<R>()
}
pub fn get_mut<R: crate::resource::Resource>(&mut self) -> Option<&mut R> {
self.resources.get_mut::<R>()
}
pub fn has_plugin(&self, name: &str) -> bool {
self.plugin_names.contains(name)
}
pub fn plugin_names(&self) -> impl Iterator<Item = &'static str> + '_ {
self.plugin_names.iter().copied()
}
pub fn shutdown(&mut self) {
profile_function!();
tracing::info!("Shutting down engine with {} plugins", self.plugins.len());
for plugin in self.plugins.iter().rev() {
tracing::debug!("Cleaning up plugin: {}", plugin.name());
plugin.cleanup(&mut self.resources);
}
tracing::info!("Engine shutdown complete");
}
}
impl Default for Engine {
fn default() -> Self {
EngineBuilder::new().build()
}
}
pub struct EngineBuilder {
plugins: Vec<Box<dyn PluginDyn>>,
resources: Resources,
}
impl EngineBuilder {
pub fn new() -> Self {
Self {
plugins: Vec::new(),
resources: Resources::new(),
}
}
pub fn add_plugin(mut self, plugin: impl Plugin + 'static) -> Self {
self.plugins.push(Box::new(plugin));
self
}
pub fn add_plugins(mut self, group: impl PluginGroup) -> Self {
let adapter = PluginGroupAdapter::new(group);
for plugin in adapter.into_plugins() {
self.plugins.push(plugin);
}
self
}
pub fn insert_resource<R: crate::resource::Resource>(mut self, resource: R) -> Self {
self.resources.insert(resource);
self
}
pub fn try_build(mut self) -> Result<Engine, EngineError> {
profile_function!();
let sorted_indices = self.try_sort_plugins_by_dependency_indices()?;
let mut plugin_names = HashSet::new();
profile_scope!("plugin_build_phase");
for &idx in &sorted_indices {
let plugin = &self.plugins[idx];
tracing::debug!("Building plugin: {}", plugin.name());
plugin.build(&mut self.resources);
plugin_names.insert(plugin.name());
}
profile_scope!("plugin_finish_phase");
for &idx in &sorted_indices {
self.plugins[idx].finish(&mut self.resources);
}
tracing::info!(
"Engine built with {} plugins: {:?}",
plugin_names.len(),
plugin_names
);
let sorted_plugins = Self::reorder_plugins(self.plugins, &sorted_indices);
Ok(Engine {
resources: self.resources,
plugin_names,
plugins: sorted_plugins,
})
}
pub fn build(self) -> Engine {
profile_function!();
self.try_build().expect("Failed to build engine")
}
fn reorder_plugins(
plugins: Vec<Box<dyn PluginDyn>>,
sorted_indices: &[usize],
) -> Vec<Box<dyn PluginDyn>> {
let mut plugins_opt: Vec<Option<Box<dyn PluginDyn>>> =
plugins.into_iter().map(Some).collect();
sorted_indices
.iter()
.map(|&idx| plugins_opt[idx].take().expect("Plugin already taken"))
.collect()
}
fn try_sort_plugins_by_dependency_indices(&self) -> Result<Vec<usize>, EngineError> {
profile_function!();
let mut seen_names = HashSet::new();
for plugin in &self.plugins {
if !seen_names.insert(plugin.name()) {
return Err(EngineError::DuplicatePlugin {
name: plugin.name(),
});
}
}
let plugin_map: std::collections::HashMap<_, _> = self
.plugins
.iter()
.enumerate()
.map(|(i, p)| (p.name(), i))
.collect();
for plugin in &self.plugins {
for dep in plugin.dependencies() {
if !plugin_map.contains_key(dep) {
return Err(EngineError::MissingDependency {
plugin: plugin.name(),
dependency: dep,
});
}
}
}
let mut sorted = Vec::new();
let mut visited = HashSet::new();
let mut visiting = HashSet::new();
let mut visit_stack = Vec::new();
fn visit(
name: &'static str,
plugins: &[Box<dyn PluginDyn>],
plugin_map: &std::collections::HashMap<&'static str, usize>,
visited: &mut HashSet<&'static str>,
visiting: &mut HashSet<&'static str>,
visit_stack: &mut Vec<&'static str>,
sorted: &mut Vec<usize>,
) -> Result<(), EngineError> {
if visited.contains(name) {
return Ok(());
}
if visiting.contains(name) {
let cycle_start = visit_stack.iter().position(|&n| n == name).unwrap_or(0);
let mut chain: Vec<&'static str> = visit_stack[cycle_start..].to_vec();
chain.push(name);
return Err(EngineError::CircularDependency {
plugin: name,
chain,
});
}
if let Some(&idx) = plugin_map.get(name) {
visiting.insert(name);
visit_stack.push(name);
for dep in plugins[idx].dependencies() {
visit(
dep,
plugins,
plugin_map,
visited,
visiting,
visit_stack,
sorted,
)?;
}
visit_stack.pop();
visiting.remove(name);
visited.insert(name);
sorted.push(idx);
}
Ok(())
}
for plugin in &self.plugins {
visit(
plugin.name(),
&self.plugins,
&plugin_map,
&mut visited,
&mut visiting,
&mut visit_stack,
&mut sorted,
)?;
}
Ok(sorted)
}
}
impl Default for EngineBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::FnPlugin;
#[test]
fn test_engine_builder() {
let engine = EngineBuilder::new()
.add_plugin(FnPlugin::new("test", |resources| {
resources.insert(42i32);
}))
.build();
assert_eq!(*engine.get::<i32>().unwrap(), 42);
assert!(engine.has_plugin("test"));
}
#[test]
fn test_insert_resource() {
let engine = EngineBuilder::new()
.insert_resource("pre-inserted".to_string())
.build();
assert_eq!(engine.get::<String>().unwrap(), "pre-inserted");
}
#[test]
fn test_plugin_order() {
struct FirstPlugin;
impl Plugin for FirstPlugin {
type Dependencies = ();
fn build(&self, resources: &mut Resources) {
resources.insert(vec!["first"]);
}
}
struct SecondPlugin;
impl Plugin for SecondPlugin {
type Dependencies = FirstPlugin;
fn build(&self, resources: &mut Resources) {
if let Some(v) = resources.get_mut::<Vec<&'static str>>() {
v.push("second");
}
}
}
let engine = EngineBuilder::new()
.add_plugin(SecondPlugin)
.add_plugin(FirstPlugin)
.build();
let order = engine.get::<Vec<&'static str>>().unwrap();
assert_eq!(order, &vec!["first", "second"]);
}
#[test]
fn test_default_engine() {
let engine = Engine::default();
assert!(engine.resources().is_empty());
}
#[test]
fn test_engine_shutdown() {
use std::sync::{Arc, Mutex};
let cleanup_log = Arc::new(Mutex::new(Vec::new()));
struct TestPlugin {
name: &'static str,
log: Arc<Mutex<Vec<&'static str>>>,
}
impl Plugin for TestPlugin {
type Dependencies = ();
fn name(&self) -> &'static str {
self.name
}
fn build(&self, resources: &mut Resources) {
resources.insert(format!("{}_built", self.name));
}
fn cleanup(&self, _resources: &mut Resources) {
self.log.lock().unwrap().push(self.name);
}
}
let log1 = cleanup_log.clone();
let log2 = cleanup_log.clone();
let log3 = cleanup_log.clone();
let mut engine = EngineBuilder::new()
.add_plugin(TestPlugin {
name: "First",
log: log1,
})
.add_plugin(TestPlugin {
name: "Second",
log: log2,
})
.add_plugin(TestPlugin {
name: "Third",
log: log3,
})
.build();
assert!(engine.get::<String>().is_some());
engine.shutdown();
let log = cleanup_log.lock().unwrap();
assert_eq!(*log, vec!["Third", "Second", "First"]);
}
#[test]
fn test_shutdown_with_dependencies() {
use std::sync::{Arc, Mutex};
let cleanup_log = Arc::new(Mutex::new(Vec::new()));
struct BasePlugin {
log: Arc<Mutex<Vec<&'static str>>>,
}
impl Plugin for BasePlugin {
type Dependencies = ();
fn build(&self, resources: &mut Resources) {
resources.insert(vec!["base"]);
}
fn cleanup(&self, _resources: &mut Resources) {
self.log.lock().unwrap().push("BasePlugin");
}
}
struct DependentPlugin {
log: Arc<Mutex<Vec<&'static str>>>,
}
impl Plugin for DependentPlugin {
type Dependencies = BasePlugin;
fn build(&self, resources: &mut Resources) {
if let Some(v) = resources.get_mut::<Vec<&'static str>>() {
v.push("dependent");
}
}
fn cleanup(&self, _resources: &mut Resources) {
self.log.lock().unwrap().push("DependentPlugin");
}
}
let log1 = cleanup_log.clone();
let log2 = cleanup_log.clone();
let mut engine = EngineBuilder::new()
.add_plugin(DependentPlugin { log: log2 })
.add_plugin(BasePlugin { log: log1 })
.build();
engine.shutdown();
let log = cleanup_log.lock().unwrap();
assert_eq!(*log, vec!["DependentPlugin", "BasePlugin"]);
}
}