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.