Table of Contents

Plugins Overview

The RisingV.Core.Plugins (RisingV.Shared.Plugins) abstraction serves as the foundational entry point for building modular, self-contained feature packsβ€”pluginsβ€”in the RisingV ecosystem. Rather than working directly with raw BepInEx BasePlugin classes, RisingV plugins leverage a consistent, six-stage lifecycle, integrated configuration binding, and automatic manager wiring (engines, databases, systems) to ensure a robust, maintainable, and hot-swappable modding framework.


API Documentation

For full details on available classes, interfaces, and methods within the Plugins namespace, refer to the official API reference:

πŸ”— RisingV.Core.Plugins API Reference


1. Core Abstraction

At its core, RisingV’s plugin system wraps BepInEx’s BasePlugin into a richer abstraction:

  • Uniform Six-Stage Lifecycle
    Each plugin follows a strict sequence:

    1. Initialize
    2. Load
    3. Ready
    4. Reload
    5. Unload
    6. Terminate
  • Configuration Binding
    Built atop BaseConfig, each plugin can define a strongly-typed PluginConfig subclass. The abstraction queues up ConfigEntry<T> bindings and automatically reads or hot-reloads them at the appropriate lifecycle stage.

  • Manager Wiring
    RisingV encourages separation of concerns by dividing functionality into managers such as:

    • EngineManager (coordinates Engines)
    • DatabaseManager (coordinates Databases)
    • SystemManager (coordinates custom systems or processors)
      All managers are registered during Initialize and automatically invoked during Load and Ready.
  • Logging & EventBridge Integration
    Each plugin receives a Logger instance scoped to its GUID. Additionally, Publish and Subscribe wrappers simplify event handling through the shared EventBridge.


2. Key Contracts

IPlugin

Defines the minimal contract for any RisingV plugin:

public interface IPlugin
{
    string   Guid     { get; }
    string   Name     { get; }
    Version  Version  { get; }
    string   Author   { get; }

    bool IsInitialized { get; }
    bool IsLoaded      { get; }
    bool IsReady       { get; }

    void Initialize();
    void Load();
    void Ready();
    void Reload(ReloadReason reason);
    void Unload();
    void Terminate();
}
  • GUID, Name, Version, Author
    Read-only metadata for plugin identification.

  • Lifecycle State Flags
    IsInitialized, IsLoaded, IsReady serve as idempotent guards; any override must invoke base.*() to keep them in sync.

  • Lifecycle Methods

    • Initialize(): Register configuration, managers, default data.
    • Load(): Connect to external resources, allocate heavy objects.
    • Ready(): Safe point for inter-plugin communication (all plugins have completed Load).
    • Reload(ReloadReason): Triggered on Hot-Reload or manual reload commands.
    • Unload(): Dispose caches, detach from events, stop threads.
    • Terminate(): Final cleanup before server shutdown.

RisingBasePlugin

A concrete, opinionated base class that implements IPlugin and extends BepInEx’s BasePlugin:

  • Logger
    Protected Logger Log is instantiated using the plugin’s GUID. Use Log.Info(), Log.Warning(), etc., for structured logging.

  • PluginConfig
    A public PluginConfig Config instance is auto-instantiated. Override CreateConfig() to supply a custom PluginConfig subclass.

  • Manager Accessors
    Provides helper properties to retrieve common managers:

    • EngineManager Engines { get; }
    • DatabaseManager Databases { get; }
    • SystemManager Systems { get; }
    • Additional managers (e.g., AspectManager, ItemManager) as needed.
  • EventBridge Wrappers
    Simplifies usage of EventBridge.Publish(...) and EventBridge.Subscribe<TEvent>(handler).


3. Lifecycle Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Initializeβ”‚ ──► β”‚   Load   β”‚ ──► β”‚  Ready   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
      β–²               β”‚               β”‚
      β”‚               β–Ό               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Terminate β”‚ ◄── β”‚  Unload  β”‚ ◄── β”‚  Reload  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Initialize

    • Instantiate and register all necessary managers (Engines, Databases, Systems).
    • Create or bind configuration entries (hot-reloadable by default).
    • Seed any default data or static references.
  • Load

    • Connect to external services (e.g., network resources, file I/O).
    • Allocate heavy or long-lived objects (caches, background threads).
    • Register event handlers (through EventBridge or game hooks).
  • Ready

    • All other plugins have completed Load.
    • Safe to perform cross-plugin interactions (subscribe to events published by other plugins).
    • Begin gameplay-related logic (hook into game state, spawn objects).
  • Reload

    • Called on manual β€œreloadall” or BepInEx hot-reload.
    • Plugin should reload modified configurations or refresh resources.
    • Use ReloadReason.IsFull to discriminate between full reload and partial reloads.
  • Unload

    • Dispose of large caches, stop or signal threads to exit.
    • Detach all event listeners to prevent memory leaks.
    • Prepare for potential re-initialization on the next reload.
  • Terminate

    • Final cleanup: close file handles, save critical data, remove persistent references.
    • Called when the server or application is shutting down.

4. Quick‑Start Skeleton

Below is a minimal plugin template demonstrating how to leverage the RisingV plugin abstraction:

using BepInEx;
using RisingV.Shared.Plugins;
using RisingV.Shared.Engines;
using RisingV.Core.Engines;

public class MyFeatureConfig() 
    : PluginConfig(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_GUID)
{
    public ConfigEntry<float> AttackModifier { get; set; }

    public override void Load()
    {
        base.Load();
        
        AttackModifier = Bind("Gameplay", "AttackModifier", 1f, 
            "Multiplier applied to all player attack damage");
    }
}

public class MyFeatureContext() : PluginContext(typeof(MyPluginInfo), new MyFeatureConfig());

[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
[BepInDependency("RisingV.Core")]    // Ensures RisingV.Core binaries are loaded first
[BepInDependency("RisingV.Scripting")] // If your plugin uses scripting features
public class MyFeaturePlugin : RisingPlugin<MyFeatureContext>
{
    protected override void OnInitialize()
    {
        EngineManager.AddEngine<DamageEngine>(this);
        EngineManager.AddEngine<BuffEngine>(this);
        EngineManager.AddEngine<DeathEngine>(this);
    }
    
    protected override void OnLoad()
    {
        // Pre-Initialize logic (before managers are created)
    }
    
    protected override void OnUnload()
    {
        // Return true to allow unload, or false to veto
        return true;
    }

    public override void Ready()
    {
        // All managers and other plugins are ready: hook game logic here
        if (Context.Config.AttackModifier.Value != 1f)
        {
            // Apply a global damage multiplier or patch methods accordingly
        }
    }
}

Tips:

  • Most plugins only need to use OnInitialize or OnLoad.
  • BasePlugin automatically invokes Load() and Ready() on all registered managers, so custom logic can often be placed solely in Ready().
  • Declare [BepInDependency] attributes to enforce load order for dependent plugins or RisingV subsystems.

5. PluginConfig Pattern

Every RisingV plugin typically defines a PluginConfig subclass:

public sealed class LootTweaksConfig() 
    : PluginConfig(LootTweaksInfo.PLUGIN_GUID, LootTweaksInfo.PLUGIN_GUID)
{
    public ConfigEntry<float>? Multiplier { get; private set; }

    public override void Load()
    {
        base.Load();
        Multiplier = Bind("General", "Multiplier", 2f, 
            "Multiply boss loot counts by this factor.");
    }
}
  • Inherits BaseConfig, so you automatically get:
    • An Enabled toggle for your entire plugin.
    • Queued binding of all ConfigEntry<T> fields.
    • Hot-reload support: when the underlying configuration file changes, Reload() will rebind updated values.

Best Practice: Always perform Bind() calls inside Load()β€”not in the constructorβ€”to avoid NullReferenceException issues.


6. PluginManager

  • Located in RisingV.Shared.Plugins.
  • Discover and register all plugins (both via reflection on [BepInPlugin] attributes and manually instantiated ones used in unit tests).
  • Resolves dependencies using [PluginDependency("SomeOtherGUID")] attributes.

PluginManager ensures that plugins are loaded, unloaded, and reloaded in a deterministic order based on declared dependencies.


7. Best Practices & Patterns

  1. One Feature per Plugin
    Keep each plugin focused on a single responsibility. This makes it easier to manage dependencies and reduces coupling.

  2. Register Managers Early
    Override OnInitialize (via RisingPlugin<TContext>). Avoid delaying manager registration to Ready unless absolutely necessary.

  3. Leverage Hot-Reload

    • Plugins automatically support hot-reload if you bind config entries and register managers correctly.
    • Use ReloadReason to distinguish between a simple config change (IsConfigChange) and a full reload (IsFull).
  4. Use EventBridge

    • Prefer publishing and subscribing to events rather than writing direct Harmony patches across plugins.
    • Events decouple plugin logic and allow multiple subscribers to respond independently.
  5. Cleanup on Unload

    • Detach all event handlers in Unload().
    • Dispose of or null out large data structures to prevent memory leaks.
    • Stop any custom background tasks or threads.

8. Troubleshooting

Symptom Possible Cause Fix
Plugin stuck in β€œLoaded” state Forgot to call Ready() on a manager or in BasePlugin Ensure you call base.Ready() and/or register managers before calling Ready().
NullReferenceException on Config access Attempted to read ConfigEntry<T> in constructor Move all Bind() calls to Load() method.
Circular plugin dependency detected Two plugins depend on each other via [PluginDependency] Break one dependency or mark it optional.
Hot-reloaded plugin fails to reinitialize Managers were not designed for hot‐reload Implement idempotent Reload() logic and reset internal state properly.

9. Summary

RisingV’s plugin abstraction layer builds on BepInEx to provide:

  • A uniform six‐stage lifecycle to manage initialization, loading, readiness, reloads, unloading, and termination.
  • Automatic wiring of key subsystems (Engines, Databases, Systems) via helper managers.
  • First‐class configuration binding through PluginConfig (hot-reloadable).
  • Integrated logging and event publishing/subscribing via EventBridge.
  • A well-defined PluginManager to discover, sort, and control plugins based on declared dependencies.

By using this system, plugin authors achieve consistency, robustness, and hot‐swappability, enabling rapid development of feature-rich, modular add-ons for the RisingV framework.