Table of Contents

Engine Abstraction

Namespace root: RisingV.Shared.Engines
Assemblies: RisingV.Shared

The Engine abstraction is a lightweight framework for building modular, self‑contained subsystems (“engines”) that plug into a Manager–driven lifecycle. Each engine encapsulates a single concern—AI, combat, analytics, UI overlay, etc.—and relies on an EngineManager (which in turn derives from the generic manager base) to orchestrate start‑up, hot‑reload, and shut‑down.

Think of an engine as a service and EngineManager as the service host.


1. Building Blocks

Component Role Key Properties / Methods
Dependency record Describes a hard or soft link on another engine. Type Type, string? Version
IEngineConfig BepInEx‑backed feature config that every engine owns. Enabled, AutoLoadDependencies
EngineConfig Standard concrete config with sensible defaults. Deconstruct for pattern matching
IEngine Minimal marker + lifecycle bridge to the EngineManager. IEngineConfig Config, bool Enabled, GetDependencies(), GetRecommended()
Engine<TConfig> Base class that implements 90 % of the boilerplate. Virtual Load, abstract Ready, helper filters
EngineManager Specialised TypeMapManager<IEngine> that solves dependency graphs and calls lifecycle hooks in order. AddEngine, OrderEnginesByDependencies, CheckDependencies

The abstraction inherits all advantages of the Manager layer: deterministic lifecycle, hot‑reload hooks, and pluggable storage.


2. Dependency Model

  1. Declare Hard dependencies in Dependencies, soft links in Recommended.
  2. ValidateEngineManager.CheckDependencies gathers missing items and throws MissingDependenciesException (or logs a warning for recommended).
  3. Auto‑load If both EngineManager.AllowAutoLoad and the engine’s Config.AutoLoadDependencies are true, missing engines are instantiated on the fly and injected into the manager.
public sealed class CombatEngine : Engine<EngineConfig>
{
    public CombatEngine() : base(new EngineConfig("Combat", "Core combat loop"))
    {
        Dependencies.Add(new Dependency(typeof(PhysicsEngine)));
        Recommended.Add(new Dependency(typeof(AnalyticsEngine)));
    }
}

3. Lifecycle Walkthrough

EngineManager forwards the familiar six‑stage lifecycle to each engine:

Stage Typical work in an engine
Initialize Minimal allocation, subscribe to global events.
Load Read config, build internal data tables, resolve external handles.
Ready Hook into other engines, register gameplay systems. Must be overridden.
Reload Refresh runtime state when config/files change.
Unload Dispose timers, unregister events.
Terminate Final GC‑safe clean‑up (rarely needed in practice).

Ready is kept abstract in Engine<TConfig> so you never forget to wire the core behaviour.


4. Authoring a New Engine

public sealed class ChatEngine : Engine<EngineConfig>
{
    public ChatEngine() : base(new EngineConfig(
        name:        "Chat",
        description: "Player‑to‑player text chat",
        version:     "1.2.0",
        author:      "Ben Dol",
        relativePath:"RisingV/Core"))
    {
        // No hard deps, optional analytics
        Recommended.Add(new Dependency(typeof(AnalyticsEngine)));
    }

    public override void Ready(EngineManager mgr, List<IPlugin> plugins)
    {
        // Register chat commands, message bus, etc.
        mgr.Log.Info("ChatEngine ready – commands registered.");
    }
}

Register with your plugin:

_engineManager.AddEngine<ChatEngine>(this);

// This is called by the EngineManager no need to manually call, but just to demonstrate the order
_engineManager.Initialize(this);
_engineManager.Load(this);
_engineManager.Ready(this);   // after all engines have been added

5. Runtime Hot‑Reload

  1. Edit BepInEx/config/RisingV/Core/Chat.cfg.
  2. EngineManager.Reload propagates a ReloadReason to every engine.
  3. Override Reload to pick up new values:
public override void Reload(EngineManager m, ReloadReason r)
{
    base.Reload(m, r);
    Log.Info("ChatEngine reloaded: {}", r.Reason);
    _throttle = Config.GetValue("ThrottlePerMinute", 20);
}

6. Best Practices & Tips

  • Single Responsibility – keep each engine focused; split large ones.
  • Prefer Recommended – reserve hard dependencies for must‑have links.
  • Guard With Enabled – short‑circuit early if the engine is disabled.
  • Leverage Auto‑Load – great for optional gameplay features users can toggle.
  • Test Dependency Graphs – unit‑test EngineManager.OrderEnginesByDependencies.

7. FAQ

Q: Do I need a separate manager for every engine type?
A: No. A single EngineManager can host any number of heterogeneous engines.

Q: Can engines depend on managers?
A: Indirectly – they retrieve managers via the plugin context, but circular dependencies (Engine ⇄ Manager) should be avoided.

Q: How heavy is auto‑load?
A: Instantiation is a single Activator.CreateInstance call plus registration, so negligible for typical plugin sizes.


TL;DR

Engines are modular, reload‑friendly mini‑services.
Engine<TConfig> gives you the boilerplate; EngineManager keeps them orchestrated and dependency‑aware.
Focus on domain logic—let the abstraction handle the rest. 🚀