World Service
The fundamental unit of encapsulation in the Plantangenet Simulation is a service implementing the ForgeService trait.
It manages per-feature initialization, configuration fetching, and execution budgets (run-time limitations) per frame. By providing dynamic initialization capabilities via provision(), services can register their dependencies on engine components natively.
Trait Definition Reference
pub trait ForgeService {
/// Returns the canonical internal name of the service (e.g., "HobService").
fn name(&self) -> &'static str;
/// Invoked immediately on `SystemBus::register`. The service should establish
/// internal state, configure caches, and link against existing services.
fn provision(&mut self, context: &mut WorldContext) -> Result<(), ProvisionError>;
/// Hook executed on standard frame advancement (`on_frame_active`).
/// Passed execution limitations for time-slicing long-running behaviors.
fn tick(&mut self, context: &mut WorldContext, budget: FrameBudget) -> Result<(), FrameError>;
/// Optional: The number of milliseconds or iterations allowed per frame.
fn frame_budget(&self) -> FrameBudget {
FrameBudget::default() // Often Unbounded in dev mode
}
}
provision() and Registration lifecycle
When you register a service via WorldContext::register_service, the WorldManager sequentially coordinates injection dependencies.
- Instantiation: You create a raw service
Box<dyn ForgeService>. - Registration:
context.register_service(...)is called, putting it in an un-initialized queue. - Provisioning: The core engine traverses the queue, invoking
provision(&mut context)on each service. - If
ProvisionErroris returned, the engine fails to boot natively. - Services can grab configuration, database pools, or map references locally here.
- Active Loop: The engine transfers the service array into the active frame loop for execution during
tick().
Example
impl ForgeService for WeatherService {
fn name(&self) -> &'static str { "WeatherService" }
fn provision(&mut self, context: &mut WorldContext) -> Result<(), ProvisionError> {
let sql_pool = context.sql_pool().expect("Missing DB pool");
self.local_cache = WeatherCache::new(sql_pool);
context.event_bus().subscribe("weather_change", self.handle_weather);
Ok(())
}
fn tick(&mut self, context: &mut WorldContext, _budget: FrameBudget) -> Result<(), FrameError> {
self.local_cache.flush_updates();
Ok(())
}
}
tick() and Panic Isolation Guarantees
During WorldContext::tick(), each registered service's tick method will be executed natively in registration order.
- Ordering matters: If Service A (
Weather) generates events that Service B (Crops) relies on, Service A must be registered first if read-latency needs to be under one frame. - Panic Isolation: While the
FrameManagerattempts to gracefully captureResult<(), FrameError>, native Rust panics in a service'stick()will generally abort the specific service task or the whole engine depending oncatch_unwindconfigurations (debug vs release). - We highly recommend handling conditions explicitly via
FrameError::Criticalinstead of panicking (unwrap,expect) inside the ticker to allow for soft engine restarts.
FrameBudget Usage
tick() passes down a FrameBudget parameter natively to prevent any single service from exhausting the processing time of a tick interval.
- A
TimeBudget(Duration)allows the service to checkbudget.is_exhausted()inside hot loops. - An
IterationBudget(usize)is useful for capping batch processing chunks. - An
Unboundedbudget allows unrestricted execution (standard for test and initialization environments).