Skip to content

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.

  1. Instantiation: You create a raw service Box<dyn ForgeService>.
  2. Registration: context.register_service(...) is called, putting it in an un-initialized queue.
  3. Provisioning: The core engine traverses the queue, invoking provision(&mut context) on each service.
  4. If ProvisionError is returned, the engine fails to boot natively.
  5. Services can grab configuration, database pools, or map references locally here.
  6. 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.

  1. 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.
  2. Panic Isolation: While the FrameManager attempts to gracefully capture Result<(), FrameError>, native Rust panics in a service's tick() will generally abort the specific service task or the whole engine depending on catch_unwind configurations (debug vs release).
  3. We highly recommend handling conditions explicitly via FrameError::Critical instead 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 check budget.is_exhausted() inside hot loops.
  • An IterationBudget(usize) is useful for capping batch processing chunks.
  • An Unbounded budget allows unrestricted execution (standard for test and initialization environments).