Import Oxide engine (Stages 0–10) under MIT license

Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit f56a1eea3b
128 changed files with 40493 additions and 2 deletions
+503
View File
@@ -0,0 +1,503 @@
//! The application core: an [`App`] assembled by registering [`Module`]s.
//!
//! Stage 5 ties the core framework together. An `App` owns the shared engine
//! state — the [`Scene`], the [`AssetServer`], the [`TypeRegistry`], the
//! [`LayerRegistry`], frame [`Time`], and arbitrary user resources — plus a
//! [`Schedule`] of systems. Functionality is added by **modules**: each
//! [`Module::build`] registers systems, component types, asset loaders, and
//! resources, so the engine is composed rather than hard-wired and an exported
//! game compiles in only the modules it uses.
//!
//! ```
//! use oxide_engine::app::{App, DefaultModules};
//!
//! let mut app = App::new();
//! app.add_modules(DefaultModules);
//! app.update(1.0 / 60.0); // advance one frame
//! ```
mod module;
mod schedule;
pub use module::{CoreModule, DefaultModules, Module, RenderModule};
pub use schedule::Schedule;
use std::any::{Any, TypeId};
use std::collections::{BTreeMap, HashMap};
use schedule::{run_phase, SystemEntry, Systems};
use crate::asset::{AssetLoader, AssetServer};
use crate::layer::LayerRegistry;
use crate::reflect::TypeRegistry;
use crate::scene::Scene;
/// The default fixed-timestep duration (60 Hz) for [`Schedule::FixedUpdate`].
pub const DEFAULT_FIXED_TIMESTEP: f32 = 1.0 / 60.0;
/// An upper bound on fixed steps per frame, so a long stall (e.g. a breakpoint)
/// cannot trigger an unbounded catch-up "spiral of death".
const MAX_FIXED_STEPS_PER_FRAME: u32 = 8;
/// Per-frame timing, refreshed by [`App::update`] and readable by systems.
#[derive(Debug, Clone, Copy)]
pub struct Time {
/// Seconds elapsed since the previous frame.
pub delta: f32,
/// Seconds elapsed since the app started.
pub elapsed: f32,
/// The fixed-timestep duration used by [`Schedule::FixedUpdate`].
pub fixed_delta: f32,
/// Frames advanced so far.
pub frame: u64,
}
impl Default for Time {
fn default() -> Self {
Self {
delta: 0.0,
elapsed: 0.0,
fixed_delta: DEFAULT_FIXED_TIMESTEP,
frame: 0,
}
}
}
/// The application core. See the [module docs](self).
pub struct App {
/// The active scene graph.
pub scene: Scene,
/// The shared asset server (built-in loaders registered).
pub assets: AssetServer,
/// The reflection/type registry for dual-editable components.
pub types: TypeRegistry,
/// The project's named layers.
pub layers: LayerRegistry,
/// Per-frame timing.
pub time: Time,
resources: HashMap<TypeId, Box<dyn Any>>,
systems: Systems,
/// Registered modules → enabled flag.
modules: BTreeMap<&'static str, bool>,
/// The module currently being built, so registrations can be attributed.
current_module: Option<&'static str>,
/// Per-module bookkeeping for clean removal.
module_types: HashMap<&'static str, Vec<&'static str>>,
module_loaders: HashMap<&'static str, Vec<String>>,
module_resources: HashMap<&'static str, Vec<TypeId>>,
fixed_accumulator: f32,
}
impl App {
/// A new app with empty core state and no modules. The [`AssetServer`] comes
/// with the engine's built-in loaders already registered.
pub fn new() -> Self {
Self {
scene: Scene::new(),
assets: AssetServer::new(),
types: TypeRegistry::new(),
layers: LayerRegistry::new(),
time: Time::default(),
resources: HashMap::new(),
systems: Systems::default(),
modules: BTreeMap::new(),
current_module: None,
module_types: HashMap::new(),
module_loaders: HashMap::new(),
module_resources: HashMap::new(),
fixed_accumulator: 0.0,
}
}
// --- Modules -----------------------------------------------------------
/// Adds a module, running its [`Module::build`] and attributing everything
/// it registers to it.
///
/// # Panics
/// Panics if a module with the same [`name`](Module::name) is already added.
pub fn add_module<M: Module>(&mut self, module: M) -> &mut Self {
let name = module.name();
assert!(
!self.modules.contains_key(name),
"module '{name}' is already added"
);
self.modules.insert(name, true);
let previous = self.current_module.replace(name);
module.build(self);
self.current_module = previous;
self
}
/// Adds a bundle of modules (e.g. [`DefaultModules`]).
pub fn add_modules<B: ModuleBundle>(&mut self, bundle: B) -> &mut Self {
bundle.add_to(self);
self
}
/// Whether a module is registered.
pub fn has_module(&self, name: &str) -> bool {
self.modules.contains_key(name)
}
/// The registered module names, sorted.
pub fn modules(&self) -> impl Iterator<Item = &'static str> + '_ {
self.modules.keys().copied()
}
/// Whether a registered module is enabled. Unknown modules report `false`.
pub fn is_module_enabled(&self, name: &str) -> bool {
self.modules.get(name).copied().unwrap_or(false)
}
/// Enables or disables a module's systems without removing them. Disabled
/// modules' systems are skipped each frame. Returns whether the module exists.
pub fn set_module_enabled(&mut self, name: &str, enabled: bool) -> bool {
match self.modules.get_mut(name) {
Some(flag) => {
*flag = enabled;
true
}
None => false,
}
}
/// Removes a module and everything it contributed — systems, registered
/// component types, asset loaders, and resources — leaving no dangling
/// references. Returns whether the module existed.
pub fn remove_module(&mut self, name: &str) -> bool {
if self.modules.remove(name).is_none() {
return false;
}
self.systems.remove_module(name);
for type_name in self.module_types.remove(name).unwrap_or_default() {
self.types.unregister(type_name);
}
for ext in self.module_loaders.remove(name).unwrap_or_default() {
self.assets.unregister_loader(&ext);
}
for type_id in self.module_resources.remove(name).unwrap_or_default() {
self.resources.remove(&type_id);
}
true
}
/// Whether a system contributed by `module` should run this frame: systems
/// with no owning module always run; module-owned systems run only while
/// their module is enabled.
pub(crate) fn is_system_enabled(&self, module: Option<&'static str>) -> bool {
match module {
None => true,
Some(name) => self.is_module_enabled(name),
}
}
// --- Registration (attributed to the current module) -------------------
/// Adds a system to a schedule phase. Systems run in phase order, then in
/// registration order within a phase.
pub fn add_system(
&mut self,
phase: Schedule,
system: impl FnMut(&mut App) + 'static,
) -> &mut Self {
self.systems.push(
phase,
SystemEntry {
module: self.current_module,
run: Box::new(system),
},
);
self
}
/// Registers a reflected component type under `name` (see [`TypeRegistry`]).
pub fn register_type<T>(&mut self, name: &'static str) -> &mut Self
where
T: hecs::Component + serde::Serialize + serde::de::DeserializeOwned,
{
self.types.register::<T>(name);
if let Some(module) = self.current_module {
self.module_types.entry(module).or_default().push(name);
}
self
}
/// Registers an asset loader (see [`AssetServer::register_loader`]).
pub fn add_loader<L: AssetLoader>(&mut self, loader: L) -> &mut Self {
if let Some(module) = self.current_module {
let exts = loader.extensions().iter().map(|e| e.to_lowercase());
self.module_loaders.entry(module).or_default().extend(exts);
}
self.assets.register_loader(loader);
self
}
// --- Resources ---------------------------------------------------------
/// Inserts (or replaces) a shared resource of type `T`.
pub fn insert_resource<T: 'static>(&mut self, value: T) -> &mut Self {
let id = TypeId::of::<T>();
if let Some(module) = self.current_module {
self.module_resources.entry(module).or_default().push(id);
}
self.resources.insert(id, Box::new(value));
self
}
/// Borrows a resource of type `T`, or `None` if absent.
pub fn get_resource<T: 'static>(&self) -> Option<&T> {
self.resources
.get(&TypeId::of::<T>())
.and_then(|b| b.downcast_ref::<T>())
}
/// Mutably borrows a resource of type `T`, or `None` if absent.
pub fn get_resource_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.resources
.get_mut(&TypeId::of::<T>())
.and_then(|b| b.downcast_mut::<T>())
}
/// Removes and returns the resource of type `T`, or `None` if absent.
///
/// Lets a system take exclusive ownership of a resource for the duration of
/// a call — e.g. the physics step takes the `PhysicsWorld` out so it can
/// borrow the [`Scene`] mutably at the same time — then re-inserts it.
pub fn remove_resource<T: 'static>(&mut self) -> Option<T> {
self.resources
.remove(&TypeId::of::<T>())
.and_then(|b| b.downcast::<T>().ok())
.map(|b| *b)
}
/// Whether a resource of type `T` is present.
pub fn has_resource<T: 'static>(&self) -> bool {
self.resources.contains_key(&TypeId::of::<T>())
}
// --- Running -----------------------------------------------------------
/// Sets the fixed-timestep duration used by [`Schedule::FixedUpdate`].
pub fn set_fixed_timestep(&mut self, seconds: f32) -> &mut Self {
assert!(seconds > 0.0, "fixed timestep must be positive");
self.time.fixed_delta = seconds;
self
}
/// The number of systems registered across all phases.
pub fn system_count(&self) -> usize {
self.systems.total()
}
/// Advances one frame by `delta` seconds: runs the per-frame phases once and
/// [`FixedUpdate`](Schedule::FixedUpdate) as many whole fixed steps as the
/// accumulated time allows (capped to avoid a catch-up spiral).
pub fn update(&mut self, delta: f32) {
self.time.delta = delta;
self.time.elapsed += delta;
self.time.frame += 1;
// How many fixed steps to run this frame.
self.fixed_accumulator += delta;
let mut steps = (self.fixed_accumulator / self.time.fixed_delta) as u32;
if steps > MAX_FIXED_STEPS_PER_FRAME {
steps = MAX_FIXED_STEPS_PER_FRAME;
self.fixed_accumulator = 0.0;
} else {
self.fixed_accumulator -= steps as f32 * self.time.fixed_delta;
}
self.run_frame(steps);
}
/// Advances **exactly one fixed timestep**: bumps frame time by
/// [`fixed_delta`](Time::fixed_delta) and runs the per-frame phases once with
/// a single [`FixedUpdate`](Schedule::FixedUpdate), bypassing the
/// accumulator. This is the editor play-mode **Step** primitive — single-step
/// the simulation while paused — and yields one deterministic tick.
pub fn step(&mut self) {
let dt = self.time.fixed_delta;
self.time.delta = dt;
self.time.elapsed += dt;
self.time.frame += 1;
self.run_frame(1);
}
/// Runs the per-frame phases once with `fixed_steps` runs of
/// [`FixedUpdate`](Schedule::FixedUpdate). The systems are moved out first so
/// each gets exclusive `&mut App`, then anything registered mid-frame is
/// folded back. Shared by [`update`](Self::update) and [`step`](Self::step).
fn run_frame(&mut self, fixed_steps: u32) {
let mut systems = std::mem::take(&mut self.systems);
run_phase(&mut systems, self, Schedule::First);
run_phase(&mut systems, self, Schedule::Input);
run_phase(&mut systems, self, Schedule::PreUpdate);
for _ in 0..fixed_steps {
run_phase(&mut systems, self, Schedule::FixedUpdate);
}
run_phase(&mut systems, self, Schedule::Update);
run_phase(&mut systems, self, Schedule::PostUpdate);
run_phase(&mut systems, self, Schedule::Render);
run_phase(&mut systems, self, Schedule::Last);
// Fold back anything registered during the frame, then restore.
systems.merge(std::mem::take(&mut self.systems));
self.systems = systems;
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
/// A group of modules added together. Implemented for [`DefaultModules`] and for
/// tuples, so `app.add_modules((ModuleA, ModuleB))` works.
pub trait ModuleBundle {
/// Adds every module in the bundle to `app`.
fn add_to(self, app: &mut App);
}
impl<A: Module> ModuleBundle for (A,) {
fn add_to(self, app: &mut App) {
app.add_module(self.0);
}
}
impl<A: Module, B: Module> ModuleBundle for (A, B) {
fn add_to(self, app: &mut App) {
app.add_module(self.0);
app.add_module(self.1);
}
}
impl<A: Module, B: Module, C: Module> ModuleBundle for (A, B, C) {
fn add_to(self, app: &mut App) {
app.add_module(self.0);
app.add_module(self.1);
app.add_module(self.2);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::{Transform, Vec3};
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn empty_app_updates_and_advances_time() {
let mut app = App::new();
assert_eq!(app.time.frame, 0);
app.update(0.5);
assert_eq!(app.time.frame, 1);
assert!((app.time.elapsed - 0.5).abs() < 1e-6);
assert!((app.time.delta - 0.5).abs() < 1e-6);
}
#[test]
fn systems_run_in_phase_then_registration_order() {
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut app = App::new();
let l = log.clone();
app.add_system(Schedule::Update, move |_| l.borrow_mut().push("update-1"));
let l = log.clone();
app.add_system(Schedule::Update, move |_| l.borrow_mut().push("update-2"));
let l = log.clone();
app.add_system(Schedule::First, move |_| l.borrow_mut().push("first"));
app.update(0.0);
assert_eq!(*log.borrow(), vec!["first", "update-1", "update-2"]);
}
#[test]
fn fixed_update_runs_by_accumulated_time() {
let count = Rc::new(Cell::new(0u32));
let mut app = App::new();
app.set_fixed_timestep(0.1);
let c = count.clone();
app.add_system(Schedule::FixedUpdate, move |_| c.set(c.get() + 1));
app.update(0.25); // 0.25 / 0.1 = 2 whole steps, ~0.05 left over
assert_eq!(count.get(), 2);
// 0.05 carried + 0.06 = 0.11 -> 1 more step (kept off the exact float
// boundary so the result is robust to f32 rounding).
app.update(0.06);
assert_eq!(count.get(), 3);
}
#[test]
fn fixed_update_is_capped_against_spiral() {
let count = Rc::new(Cell::new(0u32));
let mut app = App::new();
app.set_fixed_timestep(0.001);
let c = count.clone();
app.add_system(Schedule::FixedUpdate, move |_| c.set(c.get() + 1));
app.update(10.0); // would be 10000 steps; capped
assert_eq!(count.get(), MAX_FIXED_STEPS_PER_FRAME);
}
#[test]
fn step_runs_one_fixed_tick_and_the_per_frame_phases_once() {
let fixed = Rc::new(Cell::new(0u32));
let update = Rc::new(Cell::new(0u32));
let mut app = App::new();
app.set_fixed_timestep(0.1);
let f = fixed.clone();
app.add_system(Schedule::FixedUpdate, move |_| f.set(f.get() + 1));
let u = update.clone();
app.add_system(Schedule::Update, move |_| u.set(u.get() + 1));
app.step();
// Exactly one fixed step and one Update, regardless of accumulator.
assert_eq!(fixed.get(), 1);
assert_eq!(update.get(), 1);
assert_eq!(app.time.frame, 1);
assert!((app.time.elapsed - 0.1).abs() < 1e-6);
assert!((app.time.delta - 0.1).abs() < 1e-6);
// A second step advances exactly one more, deterministically.
app.step();
assert_eq!(fixed.get(), 2);
assert_eq!(update.get(), 2);
}
#[test]
fn resources_round_trip() {
let mut app = App::new();
app.insert_resource(42u32);
assert_eq!(app.get_resource::<u32>(), Some(&42));
*app.get_resource_mut::<u32>().unwrap() += 1;
assert_eq!(app.get_resource::<u32>(), Some(&43));
assert!(app.get_resource::<String>().is_none());
// remove_resource takes ownership and clears the slot.
assert_eq!(app.remove_resource::<u32>(), Some(43));
assert!(!app.has_resource::<u32>());
assert_eq!(app.remove_resource::<u32>(), None);
}
#[test]
fn a_system_can_mutate_the_scene_each_frame() {
let mut app = App::new();
app.scene.spawn("a", Transform::IDENTITY);
// Each Update, nudge every entity's transform.
app.add_system(Schedule::Update, |app| {
let entities: Vec<_> = app.scene.entities().collect();
for e in entities {
if let Some(mut t) = app.scene.get_mut::<Transform>(e) {
t.translation += Vec3::X;
}
}
});
app.update(0.0);
app.update(0.0);
let e = app.scene.entities().next().unwrap();
assert!((app.scene.local_transform(e).unwrap().translation.x - 2.0).abs() < 1e-6);
}
}
+165
View File
@@ -0,0 +1,165 @@
//! The [`Module`] trait and the engine's built-in modules.
//!
//! A module is the unit of engine extension: it bundles systems, component
//! types, asset loaders, and resources behind one documented entry point, so
//! anyone — including AI agents — can add a capability by writing a module, and
//! an exported game compiles in only the modules it registers. The editor
//! integration half of the trait arrives in Stage 6.
use super::{App, ModuleBundle, Schedule};
use crate::layer::{Layer, Tags};
use crate::math::Transform;
use crate::render::MeshRenderer;
use crate::scene::Node;
/// A self-contained unit of engine functionality.
///
/// Implement [`build`](Self::build) to register everything the module provides
/// via the [`App`] facade ([`add_system`](App::add_system),
/// [`register_type`](App::register_type), [`add_loader`](App::add_loader),
/// [`insert_resource`](App::insert_resource)). Everything registered during
/// `build` is attributed to the module, so it can be enabled, disabled, or
/// removed as a unit.
pub trait Module: 'static {
/// A stable, unique name (used to enable/disable/remove the module and, in
/// later stages, to express dependencies).
fn name(&self) -> &'static str;
/// Registers the module's systems, types, loaders, and resources on `app`.
fn build(&self, app: &mut App);
}
/// The core module: registers the always-present scene component types for
/// reflection (dual-editability), so the editor and scripts can address them.
///
/// This is the runtime "wrapper" for the math/scene/layer building blocks that
/// already exist as plain library types — it does not add behavior, it exposes
/// those types through the [`TypeRegistry`](crate::reflect::TypeRegistry).
pub struct CoreModule;
impl Module for CoreModule {
fn name(&self) -> &'static str {
"core"
}
fn build(&self, app: &mut App) {
app.register_type::<Transform>("Transform");
app.register_type::<Node>("Node");
app.register_type::<Layer>("Layer");
app.register_type::<Tags>("Tags");
}
}
/// The render module: registers the renderable scene components for reflection.
///
/// The forward renderer itself is driven by the editor/host today; this module
/// is what makes [`MeshRenderer`] a first-class, dual-editable component. As the
/// data-driven render pipeline grows it will register its render-phase systems
/// here too.
pub struct RenderModule;
impl Module for RenderModule {
fn name(&self) -> &'static str {
"render"
}
fn build(&self, app: &mut App) {
app.register_type::<MeshRenderer>("MeshRenderer");
// Placeholder render-phase system so the phase is exercised; real passes
// land with the Stage 5 render pipeline piece.
app.add_system(Schedule::Render, |_app| {});
}
}
/// The engine's standard set of built-in modules, added with
/// [`App::add_modules`](super::App::add_modules).
///
/// ```
/// use oxide_engine::app::{App, DefaultModules};
/// let mut app = App::new();
/// app.add_modules(DefaultModules);
/// assert!(app.has_module("core") && app.has_module("render"));
/// ```
pub struct DefaultModules;
impl ModuleBundle for DefaultModules {
fn add_to(self, app: &mut App) {
app.add_module(CoreModule);
app.add_module(RenderModule);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::PrimitiveShape;
#[test]
fn default_modules_register_core_types() {
let mut app = App::new();
app.add_modules(DefaultModules);
assert!(app.has_module("core"));
assert!(app.has_module("render"));
assert!(app.types.is_registered("Transform"));
assert!(app.types.is_registered("MeshRenderer"));
assert_eq!(app.modules().collect::<Vec<_>>(), vec!["core", "render"]);
}
#[test]
fn removing_a_module_removes_its_contributions() {
let mut app = App::new();
app.add_modules(DefaultModules);
assert!(app.types.is_registered("MeshRenderer"));
let systems_before = app.system_count();
assert!(app.remove_module("render"));
// Its registered type is gone, its render system is gone, core remains.
assert!(!app.has_module("render"));
assert!(!app.types.is_registered("MeshRenderer"));
assert!(app.types.is_registered("Transform"));
assert!(app.system_count() < systems_before);
}
#[test]
fn disabling_a_module_skips_its_systems_without_removing() {
use std::cell::Cell;
use std::rc::Rc;
struct Ticker(Rc<Cell<u32>>);
impl Module for Ticker {
fn name(&self) -> &'static str {
"ticker"
}
fn build(&self, app: &mut App) {
let counter = self.0.clone();
app.add_system(Schedule::Update, move |_| counter.set(counter.get() + 1));
}
}
let count = Rc::new(Cell::new(0u32));
let mut app = App::new();
app.add_module(Ticker(count.clone()));
app.update(0.0);
assert_eq!(count.get(), 1);
app.set_module_enabled("ticker", false);
app.update(0.0); // skipped
assert_eq!(count.get(), 1);
app.set_module_enabled("ticker", true);
app.update(0.0); // runs again
assert_eq!(count.get(), 2);
}
#[test]
fn a_module_can_add_a_loader_removed_with_it() {
// A module registers MeshRenderer + uses a primitive, then is removed.
let mut app = App::new();
app.add_module(RenderModule);
// Sanity: the primitive enum the render component references is usable.
assert_eq!(PrimitiveShape::ALL.len(), 3);
assert!(app.remove_module("render"));
assert!(!app.types.is_registered("MeshRenderer"));
}
}
+146
View File
@@ -0,0 +1,146 @@
//! The system schedule: the ordered phases an [`App`](super::App) runs each
//! frame, and the per-phase lists of systems modules attach to.
use super::App;
/// The ordered phases of one frame.
///
/// Systems are attached to a phase and run in phase order; within a phase they
/// run in registration order, so behavior is fully deterministic. The phases
/// mirror a conventional game loop:
///
/// - [`First`](Self::First) — start-of-frame bookkeeping.
/// - [`Input`](Self::Input) — gather input (Stage 7).
/// - [`PreUpdate`](Self::PreUpdate) — engine work before game logic.
/// - [`FixedUpdate`](Self::FixedUpdate) — fixed-timestep work; runs **zero or
/// more** times per frame so simulation is frame-rate independent. Physics
/// (Stage 9) lives here.
/// - [`Update`](Self::Update) — per-frame game logic.
/// - [`PostUpdate`](Self::PostUpdate) — engine work after game logic.
/// - [`Render`](Self::Render) — drawing (Stage 5 pipeline onward).
/// - [`Last`](Self::Last) — end-of-frame cleanup.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Schedule {
First,
Input,
PreUpdate,
FixedUpdate,
Update,
PostUpdate,
Render,
Last,
}
impl Schedule {
/// The once-per-frame phases, in order (everything except `FixedUpdate`,
/// which is driven separately by the fixed-timestep accumulator).
pub(crate) const PER_FRAME: [Schedule; 7] = [
Schedule::First,
Schedule::Input,
Schedule::PreUpdate,
Schedule::Update,
Schedule::PostUpdate,
Schedule::Render,
Schedule::Last,
];
}
/// One registered system: a closure plus the module that contributed it (so the
/// module can be disabled or removed).
pub(crate) struct SystemEntry {
pub(crate) module: Option<&'static str>,
pub(crate) run: Box<dyn FnMut(&mut App)>,
}
/// The collection of systems, grouped by phase, in registration order.
#[derive(Default)]
pub(crate) struct Systems {
first: Vec<SystemEntry>,
input: Vec<SystemEntry>,
pre_update: Vec<SystemEntry>,
fixed_update: Vec<SystemEntry>,
update: Vec<SystemEntry>,
post_update: Vec<SystemEntry>,
render: Vec<SystemEntry>,
last: Vec<SystemEntry>,
}
impl Systems {
fn phase_mut(&mut self, phase: Schedule) -> &mut Vec<SystemEntry> {
match phase {
Schedule::First => &mut self.first,
Schedule::Input => &mut self.input,
Schedule::PreUpdate => &mut self.pre_update,
Schedule::FixedUpdate => &mut self.fixed_update,
Schedule::Update => &mut self.update,
Schedule::PostUpdate => &mut self.post_update,
Schedule::Render => &mut self.render,
Schedule::Last => &mut self.last,
}
}
fn phase(&self, phase: Schedule) -> &[SystemEntry] {
match phase {
Schedule::First => &self.first,
Schedule::Input => &self.input,
Schedule::PreUpdate => &self.pre_update,
Schedule::FixedUpdate => &self.fixed_update,
Schedule::Update => &self.update,
Schedule::PostUpdate => &self.post_update,
Schedule::Render => &self.render,
Schedule::Last => &self.last,
}
}
pub(crate) fn push(&mut self, phase: Schedule, entry: SystemEntry) {
self.phase_mut(phase).push(entry);
}
pub(crate) fn total(&self) -> usize {
Schedule::PER_FRAME
.iter()
.chain(std::iter::once(&Schedule::FixedUpdate))
.map(|p| self.phase(*p).len())
.sum()
}
const ALL_PHASES: [Schedule; 8] = [
Schedule::First,
Schedule::Input,
Schedule::PreUpdate,
Schedule::FixedUpdate,
Schedule::Update,
Schedule::PostUpdate,
Schedule::Render,
Schedule::Last,
];
/// Drops every system contributed by `module`.
pub(crate) fn remove_module(&mut self, module: &str) {
for phase in Self::ALL_PHASES {
self.phase_mut(phase).retain(|e| e.module != Some(module));
}
}
/// Appends all of `other`'s systems (used to fold back systems registered
/// while the frame was running).
pub(crate) fn merge(&mut self, mut other: Systems) {
for phase in Self::ALL_PHASES {
let tail = std::mem::take(other.phase_mut(phase));
self.phase_mut(phase).extend(tail);
}
}
}
/// Runs one phase: every enabled system in registration order.
///
/// The [`Systems`] are moved out of the [`App`] before phases run (so systems
/// get exclusive `&mut App` access), so `systems` and `app` here are disjoint.
/// Systems from a disabled module are skipped without being removed.
pub(crate) fn run_phase(systems: &mut Systems, app: &mut App, phase: Schedule) {
for entry in systems.phase_mut(phase) {
if app.is_system_enabled(entry.module) {
(entry.run)(app);
}
}
}