Skip to content

Configuration

How a server operator changes a number without recompiling — the mechanism behind 01_VISION.md §4 test 5: "Can a server operator change it without a compiler? If not, it is not finished."


1. Shape

Configuration is a GameResource, exactly like an item or job definition (ADR-0004) — inspector-editable, hot reloadable, and validated at load, because it is the same kind of thing: data an operator should be able to author without a programmer.

namespace Applejack.Needs;

/// <summary>
/// Tuning for the needs framework. One asset per server, loaded once at
/// <see cref="GameModule.Configure"/>. See Documentation/Architecture/05_CONFIGURATION.md
/// and 14_NEEDS_FRAMEWORK.md.
/// </summary>
[GameResource("Needs Configuration", "needsconfig", "Tuning for ambient need decay.", Icon = "restaurant")]
public sealed class NeedsConfiguration : GameResource
{
    [Property] public List<NeedDefinition> Definitions { get; set; } = [];
}

Corrected from an earlier draft of this sample, built against real needs in 14_NEEDS_FRAMEWORK.md: this used to show bespoke HungerFillSeconds/HungerDamagePerSecond fields directly on NeedsConfiguration, under the legacy's non-inverted polarity (0 = fine, 100 = starving). Both were wrong for what actually generalises — see 14_NEEDS_FRAMEWORK.md §2 for why: the field belongs on NeedDefinition itself (renamed DepletionSeconds, inverted to a satiation value), and the damage field was dropped entirely rather than kept as a config value nothing reads (no Health service exists yet to apply damage). Fixed here in the same commit that built the real thing, per this project's standing practice for illustrative-sample drift.

2. Loading

Each module reads its own configuration once, in Configure (02_MODULE_MODEL.md §1), through a small context that resolves the operator's configured asset — or the framework's shipped default if the operator has not overridden it:

namespace Applejack.Core.Configuration;

/// <summary>
/// Resolves a module's configuration asset. Available only during
/// <see cref="Modules.GameModule.Configure"/> -- configuration is read once at boot, not
/// polled.
/// </summary>
public interface IModuleConfigurationContext
{
    /// <summary>
    /// Loads the operator's configured instance of <typeparamref name="TConfig"/>, or the
    /// framework-shipped default asset if the operator has not provided an override. A
    /// <typeparamref name="TConfig"/> that fails to load throws, naming the asset -- a
    /// broken config file is a startup failure, not a silent fallback to defaults.
    /// </summary>
    TConfig Load<TConfig>() where TConfig : GameResource, new();
}

3. Overrides

An operator overrides tuning by placing their own .needsconfig (or equivalent) asset in their addon's Assets/ folder, following the naming convention documented in Guides/00_GETTING_STARTED.md. No code, no recompile, no editing a 25 KB Lua file by hand — the exact operation 01_VISION.md names as the replacement for sh_config.lua.

Configuration is read once at boot, deliberately, not polled or hot-swapped mid-session: a running server with tax rates that silently change under players' feet is a worse operator experience than requiring a restart. If a specific value genuinely needs to change live (an event organiser adjusting a rate mid-session), that is an explicit admin command calling into the owning module's service — a deliberate, logged, in-session change — not a side effect of the config asset hot-reloading.

4. Validation

Every configuration type validates itself at load, the same way item definitions do (09_ITEM_FRAMEWORK.md): a value out of its valid range is a named load failure, not a silently clamped or ignored one.

protected override void PostLoad()
{
    foreach (var need in Definitions)
    {
        if (need.DepletionSeconds <= 0)
            throw new ConfigurationValidationException(
                this, nameof(NeedDefinition.DepletionSeconds), $"must be positive for '{need.Id}'");
    }
}

GameResource.PostLoad() is the engine's supported hook for this — called once after the asset's properties have been populated, per Custom Assets.

5. What is configuration vs. what is code

Per 00_CONSTITUTION.md"if a designer might reasonably want to change it, it is a GameResource property, not a constant":

Configuration ([Property] on a GameResource) Code (a constant or algorithm)
A price, a capacity, a cooldown, a tax rate The shape of a rule — how tax is calculated
A need's fill time and damage rate The needs framework's tick loop
The list of doors a character may own The ownership model itself
Which items are contraband The definition of what contraband means

If you are tempted to add an if branch keyed on a magic number, that number belongs in a configuration asset — a magic number in gameplay code is forbidden outright.

6. Testing

Configuration types are plain GameResource subclasses with no engine dependency in their validation logic — PostLoad and any parsing it delegates to are unit-testable by constructing the type directly and calling validation, without loading a real asset file.