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
EngineManageras 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
- Declare Hard dependencies in
Dependencies, soft links inRecommended. - Validate
EngineManager.CheckDependenciesgathers missing items and throwsMissingDependenciesException(or logs a warning for recommended). - Auto‑load If both
EngineManager.AllowAutoLoadand the engine’sConfig.AutoLoadDependenciesaretrue, 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
- Edit
BepInEx/config/RisingV/Core/Chat.cfg. EngineManager.Reloadpropagates aReloadReasonto every engine.- Override
Reloadto 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. 🚀