use crate::{
assets::required::RequiredResources,
ecs::{
plugin::Plugin,
resources::Resources,
system::{IntoSystem, System},
system_set::IntoSystemSet,
},
};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SystemStage {
Startup,
PreUpdate,
Update,
PostUpdate,
PreRender,
AssetSync,
AssetSyncDeps,
Render,
PostRender,
}
impl SystemStage {
pub fn is_convergent(self) -> bool {
matches!(self, Self::Startup | Self::AssetSync | Self::AssetSyncDeps)
}
}
const TICK_STAGES: [SystemStage; 6] = [
SystemStage::PreUpdate,
SystemStage::Update,
SystemStage::PostUpdate,
SystemStage::PreRender,
SystemStage::Render,
SystemStage::PostRender,
];
enum Readiness {
Ready,
WaitingOnLazy,
MissingUnprovided {
system: &'static str,
resource: &'static str,
},
}
pub type AppRunner = Box<dyn FnOnce(App)>;
pub struct App {
pub(crate) world: hecs::World,
pub(crate) resources: Resources,
plugins: Vec<Box<dyn Plugin>>,
systems: BTreeMap<SystemStage, Vec<Box<dyn System>>>,
runner: Option<AppRunner>,
pub(crate) required: RequiredResources,
startup_done: Vec<bool>,
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
impl App {
pub fn new() -> Self {
let mut world = hecs::World::default();
let mut resources = Resources::new(&mut world);
resources.insert_resource(&mut world, ());
Self {
world: world,
resources: resources,
plugins: Vec::new(),
systems: BTreeMap::new(),
runner: Some(Box::new(|mut app| {
loop {
app.update();
}
})),
required: RequiredResources::new(),
startup_done: Vec::new(),
}
}
fn check_readiness(
world: &hecs::World,
resources: &Resources,
required: &RequiredResources,
system: &dyn System,
) -> Readiness {
for req in system.requires() {
if (req.present)(world, resources) {
continue;
}
if required.is_provided(req.type_id) {
return Readiness::WaitingOnLazy;
}
return Readiness::MissingUnprovided {
system: system.name(),
resource: req.name,
};
}
Readiness::Ready
}
fn panic_missing_unprovided(stage: SystemStage, system: &'static str, resource: &'static str) -> ! {
panic!(
"{stage:?}: system `{system}` requires `{resource}`, which nothing has \
registered as provided.\n\n\
If `{resource}` genuinely arrives later (an async backend, a LazyResource, \
an Asset upload), call `app.required.provides::<{resource}>()` in whichever \
plugin inserts it, and this will wait instead of erroring. Otherwise, insert \
it via App::add_resource before this stage runs."
);
}
fn run_stage_once(&mut self, stage: SystemStage) -> bool {
let gen_before = self.resources.generation();
if let Some(systems) = self.systems.get_mut(&stage) {
for system in systems.iter_mut() {
match Self::check_readiness(&self.world, &self.resources, &self.required, system.as_ref()) {
Readiness::Ready => {}
Readiness::WaitingOnLazy => continue,
Readiness::MissingUnprovided { system, resource } => {
Self::panic_missing_unprovided(stage, system, resource)
}
}
let _guard = crate::ecs::resources::set_current_system(system.name());
system.run(&self.world, &self.resources);
}
}
self.resources.get_command_buffer().run_on(&mut self.world);
self.resources.generation() != gen_before
}
fn run_startup_ready(&mut self) -> bool {
let Some(systems) = self.systems.get_mut(&SystemStage::Startup) else {
return false;
};
if self.startup_done.len() != systems.len() {
self.startup_done.resize(systems.len(), false);
}
let mut any_ran = false;
for (idx, system) in systems.iter_mut().enumerate() {
if self.startup_done[idx] {
continue;
}
let ready = system
.requires()
.iter()
.all(|req| (req.present)(&self.world, &self.resources));
if !ready {
continue;
}
let _guard = crate::ecs::resources::set_current_system(system.name());
system.run(&self.world, &self.resources);
self.startup_done[idx] = true;
any_ran = true;
}
if any_ran {
self.resources.get_command_buffer().run_on(&mut self.world);
}
any_ran
}
fn reconverge(&mut self, max_passes: u32) {
for pass in 0..max_passes {
let gen_before = self.resources.generation();
self.run_startup_ready();
self.run_stage_once(SystemStage::AssetSync);
self.run_stage_once(SystemStage::AssetSyncDeps);
if self.resources.generation() == gen_before {
return;
}
if pass == max_passes - 1 {
tracing::warn!(
"Startup/AssetSync/AssetSyncDeps did not settle after {max_passes} passes — \
a dependency may be permanently unsatisfiable. Check for a Startup system \
whose Res/ResMut requirement is never met, a LazyResource whose construct() \
always returns None, or an Asset whose upload() always returns None."
);
}
}
}
pub fn add_plugin(&mut self, plugin: impl Plugin) -> &mut Self {
self.plugins.push(Box::new(plugin));
self
}
pub fn add_resource(&mut self, res: impl hecs::Component) -> &mut Self {
self.resources.insert_resource(&mut self.world, res);
self
}
pub fn get_resource<'a, T: hecs::Component>(&'a self) -> hecs::Ref<'a, T> {
self.resources.get_resource(&self.world)
}
pub fn get_resource_mut<'a, T: hecs::Component>(&'a self) -> hecs::RefMut<'a, T> {
self.resources.get_resource_mut(&self.world)
}
pub fn try_insert_resource<T: hecs::Component>(&mut self, res: T) -> bool {
self.resources.try_insert(&mut self.world, res)
}
pub fn provides<T: 'static>(&mut self) -> &mut Self {
self.required.provides::<T>();
self
}
pub fn add_system<Marker>(
&mut self,
stage: SystemStage,
system: impl IntoSystem<Marker> + 'static,
) -> &mut Self {
self.systems
.entry(stage)
.or_default()
.push(Box::new(system.into_system()));
self
}
pub fn add_systems<Marker>(
&mut self,
stage: SystemStage,
systems: impl IntoSystemSet<Marker>,
) -> &mut Self {
let entry = self.systems.entry(stage).or_default();
entry.extend(systems.into_system_set());
self
}
pub fn build(&mut self) -> &mut Self {
let mut iterations = 0;
const MAX_PLUGIN_BUILD_ITERATIONS: u32 = 64;
while !self.plugins.is_empty() {
iterations += 1;
if iterations > MAX_PLUGIN_BUILD_ITERATIONS {
panic!(
"App::build() exceeded {MAX_PLUGIN_BUILD_ITERATIONS} plugin-registration passes — \
likely a cycle where plugins keep registering each other. Check for a plugin whose \
build() unconditionally re-adds itself or another plugin that re-adds it."
);
}
let plugins: Vec<_> = self.plugins.drain(..).collect();
for plugin in plugins {
plugin.build(self);
}
}
self.required.validate();
let startup_len = self
.systems
.get(&SystemStage::Startup)
.map(Vec::len)
.unwrap_or(0);
self.startup_done = vec![false; startup_len];
self.reconverge(64);
self
}
pub fn update(&mut self) {
self.reconverge(64);
for stage in TICK_STAGES {
self.run_stage_once(stage);
self.reconverge(64);
}
}
pub fn set_runner<F>(&mut self, runner: F) -> &mut Self
where
F: FnOnce(App) + 'static,
{
self.runner = Some(Box::new(runner));
self
}
pub fn run(&mut self) {
let mut owned_app = std::mem::take(self);
let runner = owned_app.runner.take().expect("No runner found!");
runner(owned_app);
}
}