-
Notifications
You must be signed in to change notification settings - Fork 0
Core.Storage
This code implements a modular storage and configuration system for a .NET-based engine. It is designed to handle different types of data (in-memory vs. persistent) and provides a structured way to manage application settings (configurations).
Here is a breakdown of the key components and how they work together:
The system distinguishes between two types of storage defined in StorageType.cs:
-
MemoryDatabaseService: A simpleDictionary<string, object>that stays in RAM. Data is lost when the application closes. -
PersistentDatabase: Also a dictionary, but it usesPersistentLoaderAndSaverServiceto load data from a file (session.bda) at startup and save changes back to it.
The PersistentLoaderAndSaverService is responsible for the file I/O.
-
Monitoring: When created, it starts a background task (
MonitorAsync) that checks every 5 seconds if the database is "Dirty" (has unsaved changes). - JSON Serialization: It converts the dictionary into a JSON string to store it on disk.
The DataCollectionService acts as a central hub. Instead of interacting with the memory or persistent databases directly, you use this service and specify which StorageType you want to use.
- It handles type conversion. For example, if you store an integer as a string in a file,
GetItemOrSetDefault<T>will try to convert it back to anintautomatically.
This is a more high-level system for application settings (like graphics options or game rules).
-
File-based: It specifically looks for a
Config.jsonfile. -
Reactive: It supports
IConfigChangeTracker, meaning other parts of your code can be notified immediately when a setting changes. -
Thread-safeish: It uses
Dirtyflags and explicitSave()calls to ensure file writes don't happen constantly, though it saves immediately on everySetcall in the current implementation.
If you want to save a high score that persists across game sessions:
// Assuming you have access to the DataCollectionService via your ObjectManager
var storage = manager.Get<DataCollectionService>();
// Save a persistent value
storage.SetItem(StorageType.Persistent, "HighScore", 1500);
// Retrieve it later (or get a default if it doesn't exist)
int score = storage.GetItemOrSetDefault<int>(StorageType.Persistent, "HighScore", 0);You can create a specific class for your game settings by inheriting from BaseConfig.
public class GameSettings : BaseConfig<GameSettings>
{
protected override GameSettings Instance => this;
// Define your settings as properties or methods
public float Volume
{
get => GetOrDefault("Audio", "Volume", 0.8f);
set => Set("Audio", "Volume", value);
}
public bool IsMuted
{
get => GetOrDefault("Audio", "Muted", false);
set => Set("Audio", "Muted", value);
}
protected override void DoRegisterDefaultValues()
{
// You can pre-register values here if needed
GetOrDefault("Audio", "Volume", 0.8f);
}
}
// Usage:
GameSettings.Load(); // Initializes and registers with ObjectManager
var settings = manager.Get<GameSettings>();
settings.Volume = 0.5f; // This will automatically trigger a save to Config.jsonThe StorageModule class shows how the whole system is "wired up" at startup:
// This is typically called during engine initialization
StorageModule.Load(myObjectManager);
// This registers:
// 1. The File Loader/Saver
// 2. The Persistent Database (linking it to the loader/saver)
// 3. The Memory Database
// 4. The DataCollectionService (which groups the two databases)This storage and configuration system is designed for high decoupling. A key feature is that you can always access and modify settings through the IUniversalConfig interface without ever needing to know which specific class (like BasicConfig or a custom GameSettings) is currently active.
Here is the updated breakdown focusing on the universal access pattern:
The system is built around the IUniversalConfig interface. This allows any part of the engine—whether it's the UI, the physics engine, or a game loop—to request "the current configuration" from the GlobalObjectManager.
- Decoupling: You don't need to know if the settings are coming from a JSON file, a database, or a hardcoded fallback.
-
Automatic Registration: Whenever a class inheriting from
BaseConfig<T>is instantiated, it automatically registers itself as the globalIUniversalConfig.
-
BaseConfig<T>: The primary implementation that handles reading/writing toConfig.jsonand supports change tracking. -
FallbackConfig: A "safe" implementation provided for cases where no real configuration is loaded. it returns default values and ignoresSetcalls to prevent the application from crashing. -
IConfigChangeTracker: An interface that allows other systems to "listen" for changes to any value within theIUniversalConfig.
While IUniversalConfig handles structured application settings, the lower-level storage system handles raw data:
-
MemoryDatabaseService: Volatile RAM storage. -
PersistentDatabase: File-based storage (session.bda) with an automatic background save task that runs every 5 seconds if data is "dirty."
This is the most common way to use the system. You don't care about the implementation; you just want a value.
using Meatcorps.Engine.Core.ObjectManager;
using Meatcorps.Engine.Core.Interfaces.Config;
// Get the universal config interface from the global manager
var config = GlobalObjectManager.ObjectManager.Get<IUniversalConfig>();
// Safely get a value with a fallback default
// If 'Graphics' group or 'Resolution' key doesn't exist, it returns "1920x1080"
string res = config.GetOrDefault("Graphics", "Resolution", "1920x1080");
// Update a setting
config.Set("Audio", "MasterVolume", 0.5f);Because IUniversalConfig is used globally, you can track changes across the entire app by implementing IConfigChangeTracker.
public class AudioSystem : IConfigChangeTracker
{
public void ConfigChanged(string group, string key, string value)
{
if (group == "Audio" && key == "MasterVolume")
{
float volume = float.Parse(value);
// Update the actual audio engine volume here...
Console.WriteLine($"Volume adjusted to: {volume}");
}
}
}
// In your initialization:
GlobalObjectManager.ObjectManager.RegisterList<IConfigChangeTracker>();
GlobalObjectManager.ObjectManager.AddToList<IConfigChangeTracker>(new AudioSystem());If your code runs in a environment where Config.json hasn't been loaded yet, the GlobalObjectManager can be set up to return a FallbackConfig, ensuring your code doesn't throw a NullReferenceException.
// If no BaseConfig is registered, you can provide the Fallback
if (GlobalObjectManager.ObjectManager.Get<IUniversalConfig>() == null)
{
GlobalObjectManager.ObjectManager.Register<IUniversalConfig>(new FallbackConfig());
}
// Now this line is safe even if there is no config file on disk
var theme = GlobalObjectManager.ObjectManager.Get<IUniversalConfig>().GetOrDefault("UI", "Theme", "Dark");