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:
IDataSource
– where the raw bytes live (files, memory, web, etc.).IDataLoader
– how those bytes become strongly‑typed objects.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
{
}
Examples – JsonDataLoader
, 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();
}
Examples – FileDataSource
, 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
withFileShare.ReadWrite
, so you can hot‑edit JSON while the game is running and then callReload
.
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.