//! 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"); app.register_type::("Node"); app.register_type::("Layer"); app.register_type::("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"); // 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!["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>); 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")); } }