Table of Contents

Database Abstraction

Namespaces: RisingV.Shared.Databases, RisingV.Shared.Databases.Sources, RisingV.Shared.Databases.Loaders
Assembly: RisingV.Shared

The Database abstraction provides a plug‑and‑play way to persist structured game data while following the same Manager lifecycle you use elsewhere in the RisingV framework.
It splits concerns into three layers:

  1. IDataSourcewhere the raw bytes live (files, memory, web, etc.).
  2. IDataLoaderhow those bytes become strongly‑typed objects.
  3. IDatabase / Database<TKey,TData,…> – CRUD façade that glues the two together and exposes a consistent API.

All databases are orchestrated by DatabaseManager, a concrete TypeMapManager that slots neatly into your plugin’s start‑up sequence.


1. Key Interfaces & Base Classes

Layer Contract Responsibility
Loader IDataLoader<TKey,TData> Parse / serialise a single entry.
Source IDataSource<TKey,TData,TLoader> Store, enumerate and update entries using a concrete loader.
Database IDatabase<TKey,TData> High‑level CRUD + lifecycle; delegates to source/loader.
Manager DatabaseManager Registers databases, coordinates lifecycle, hot‑reload.

Tip – The generic Database<TKey,TData,TSource,TLoader> abstract base implements all boilerplate; you rarely need to derive directly from interfaces.


IDataLoader<TKey,TData>

public interface IDataLoader<TKey, TData> : IDataLoader
    where TData : IData
{
    void Load<TSource>(TSource source, object? input) where TSource : IDataSource<TKey, TData>;
}

public interface IDataLoader<TKey, TData, in TInput> : IDataLoader<TKey, TData>
    where TData : IData
{
    void Load<TSource>(TSource source, TInput input) where TSource : IDataSource<TKey, TData>;

    void IDataLoader<TKey, TData>.Load<TSource>(TSource source, object? input)
    {
        Load(source, (TInput)input! ?? throw new InvalidOperationException());
    }
}

public interface IDataStreamLoader<TKey, TData> : IDataLoader<TKey, TData, Stream>
    where TData : IData
{
}

ExamplesJsonDataLoader, MapDataLoader, BinaryDataLoader.

IDataSource<TKey,TData,TLoader>

public interface IDataSource<TKey,TData,TLoader>
    where TLoader : IDataLoader<TKey,TData>
{
    bool   IsInitialized { get; }
    bool   IsLoaded      { get; }

    void   Initialize(IDatabase db);
    void   Load(IDatabase db, TLoader loader);
    void   Ready(IDatabase db);
    void   Terminate(IDatabase db);

    TKey   Add(TData obj);
    TData? Remove(TKey key);
    bool   TryGet(TKey key, out TData value);
    IEnumerable<TData> All();
}

ExamplesFileDataSource, MapDataSource.

Database<TKey,TData,TSource,TLoader>

Wrapped façade that adds:

  • VerifySource() – sanity checks on file paths or connection strings.
  • Lifecycle pass‑through to the source.
  • Convenience CRUD delegates (Add, Remove, indexer, etc.).
  • Automatic IsInitialized / IsLoaded proxy properties.

2. DatabaseManager

public sealed class DatabaseManager
    : TypeMapManager<IDatabase, DatabaseManager>, IPluginComponent
  • Creates a per‑plugin <BepInEx>/config/{PluginGUID} folder.
  • Offers typed helpers:
public T? AddDatabase<T>(IPlugin plugin) where T : IDatabase;
public T? GetDatabase<T>(bool required = true);
public bool RemoveDatabase<T>();
  • Inherits full state machine from ManagerBase (Initialize → Load → Ready …).

3. Lifecycle Walkthrough

Stage Manager Action Database Action Source Action
Initialize Validates config path Initialize allocate handles, caches
Load Iterates databases Load read files / query server
Ready Signals ready Ready final post‑load hooks
Reload Propagates reasons Reload reload internal caches
Unload Disposes Unload flush buffers, close files
Terminate Final GC Terminate release all resources

All state transitions are idempotent and orchestrated by TypeMapManager.TryReady.


4. Quick‑Start Example

1️⃣ Define a data record

public record BossDrop(string BossId, string ItemId, int Min, int Max) : IData;

2️⃣ Implement a JSON loader

public class BossDropJsonLoader
    : JsonDataLoader<string, BossDrop>
{
    protected override string ExtractKey(BossDrop obj) => obj.BossId;
}

3️⃣ Choose a source

public class BossDropFileSource
    : FileDataSource<string, BossDrop, BossDropJsonLoader>
{
    public BossDropFileSource(string path) : base(path) { }
}

4️⃣ Compose the database

public class BossDropDatabase
    : Database<string, BossDrop, BossDropFileSource, BossDropJsonLoader>
{
    private static readonly Logger Log = Logger.Create<BossDropDatabase>();

    protected override Logger Log => BossDropDatabase.Log;

    public BossDropDatabase()
        : base("BossDrops",
               new BossDropFileSource("BossDrops.json"),
               new BossDropJsonLoader()) { }
}

5️⃣ Register with the manager

_dbMgr = SharedComponents.Get<DatabaseManager>(); // or `DatabaseManager` property in the plugin
_dbMgr.AddDatabase<BossDropDatabase>(this);

Now query anywhere:

var drops    = _dbMgr.GetDatabase<BossDropDatabase>()!;
var skeleton = drops["VampireSkeletonBoss"];

5. Extending & Customising

Need encryption? Implement a SecureFileDataSource that decrypts on‐the‑fly.
Need schema migrations? Wrap your loader so Load detects a legacy version and upgrades before returning the object.

Common extension points:

Hook Use‑case
Database.VerifySource() Validate DB directory, ping remote host, check permissions.
IDataSource.KeyResolver Key resolver for this data source.
IDataSource.Load Add caching or change load order.
DatabaseManager.CheckDependencies Ensure another DB is ready before loading.

6. Thread‑Safety Notes

  • Individual databases are not inherently thread‑safe; wrap access in locks if you mutate collections concurrently.
  • File‑based sources use standard FileStream with FileShare.ReadWrite, so you can hot‑edit JSON while the game is running and then call Reload.

7. Troubleshooting

Symptom Possible Cause Fix
DatabaseException: name is null or empty Forgot to pass name in base constructor Provide a non‑empty string.
FileNotFoundException on Load Wrong path or missing file Call VerifySource or create default file.
NullReferenceException on GetDatabase<T> Manager not Ready or DB failed to load Check logs for errors; ensure correct plugin load order.

TL;DR

The Database abstraction lets you plug different sources and loaders together like LEGO bricks, then hand everything off to DatabaseManager for a consistent lifecycle — resulting in clear, testable, and hot‑reloadable data persistence for your RisingV mods.