Skip to content

Core.Storage

Dennis Steffen edited this page Jan 4, 2026 · 1 revision

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:

1. Storage Databases (IKeyValueDatabase)

The system distinguishes between two types of storage defined in StorageType.cs:

  • MemoryDatabaseService: A simple Dictionary<string, object> that stays in RAM. Data is lost when the application closes.
  • PersistentDatabase: Also a dictionary, but it uses PersistentLoaderAndSaverService to load data from a file (session.bda) at startup and save changes back to it.

2. Auto-Saving Mechanism

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.

3. Data Collection Service

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 an int automatically.

4. Configuration System (BaseConfig<T>)

This is a more high-level system for application settings (like graphics options or game rules).

  • File-based: It specifically looks for a Config.json file.
  • Reactive: It supports IConfigChangeTracker, meaning other parts of your code can be notified immediately when a setting changes.
  • Thread-safeish: It uses Dirty flags and explicit Save() calls to ensure file writes don't happen constantly, though it saves immediately on every Set call in the current implementation.

Examples of Usage

Example 1: Using the Data Collection Service

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);

Example 2: Creating a Custom Configuration

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.json

Example 3: Initializing the Module

The 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)

IUniversalConfig

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:

1. Universal Access via IUniversalConfig

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 global IUniversalConfig.

2. The Configuration Hierarchy

  • BaseConfig<T>: The primary implementation that handles reading/writing to Config.json and supports change tracking.
  • FallbackConfig: A "safe" implementation provided for cases where no real configuration is loaded. it returns default values and ignores Set calls to prevent the application from crashing.
  • IConfigChangeTracker: An interface that allows other systems to "listen" for changes to any value within the IUniversalConfig.

3. Storage Databases (IKeyValueDatabase)

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."

Examples of Usage

Example 1: Accessing Settings Universally

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);

Example 2: Reacting to Configuration Changes

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());

Example 3: Handling Fallbacks

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");

Clone this wiki locally