Files
Oxide/engine/src/app/module.rs
T
Homer Simpson 9eead719b0 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>
2026-07-05 20:41:02 +02:00

166 lines
5.7 KiB
Rust

//! 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"));
}
}