WorldFlags¶
A typed global variable store for gameplay code: bool/int/float/string flags keyed by string, with save integration and change notification. Doc 05 calls this "the Databox-vacuum module — small surface, do it properly," and that's the whole design brief: no expressions, no computed flags, no reactive queries (those are explicitly T2, gated on a second real consumer per the Kit's rule of two).
What it is¶
- Headless core —
WorldFlagStore: a pure C# class (zeroUnityEngine/engine references) that owns flag semantics. Fully unit-testable without Unity; portable if the Kit is ever spun out standalone. - Service layer —
IWorldFlagService/WorldFlagService: thin glue that wraps aWorldFlagStore, adds a save domain (WorldFlagsSaveData), and forwards store changes as a pooledWorldFlagChangedEventon the engine'sIEventManager. - Editor —
WorldFlagsWindow: a Play Mode live inspector (Tools/LoLGameplayKit/World Flags) listing every flag with simple in-place editing.
Quickstart¶
- Create a
GameplayKitConfigasset (Create → LoLGameplayKit), leaveenableWorldFlagson, and place it in aResourcesfolder under the exact nameGameplayKitConfig. - That's it — registration is automatic. The module ships
WorldFlagsServiceRegistrar, anIServiceRegistrarthe engine's initializer discovers by assembly scan and constructs parameterlessly after all engine services are up. The registrar loads the config fromResources, checks the toggle, constructsWorldFlagService, callsInitialize()itself (the engine does not callInitialize/LateInitialize/Shutdownon externally registered services), and registersIWorldFlagService. - Resolve and use it from any game system:
var flags = ServiceLocator.Instance.Get<IWorldFlagService>();
flags.SetBool("quest.met_duke", true);
flags.SetInt("player.reputation", 10);
bool metDuke = flags.GetBool("quest.met_duke", defaultValue: false); // never throws
- React to changes made anywhere in the game via the pooled event (see
Samples/Scripts/WorldFlagsSample.csfor the full listener pattern):
public class MySystem : MonoBehaviour, IEventListener<WorldFlagChangedEvent>
{
void OnEnable() => this.EventStartListening<WorldFlagChangedEvent>();
void OnDisable() => this.EventStopListening<WorldFlagChangedEvent>();
public void OnGameEvent(WorldFlagChangedEvent evt)
{
if (evt.Key == "quest.met_duke") { /* ... */ }
}
}
API overview¶
WorldFlagStore (headless core)¶
| Member | Notes |
|---|---|
SetBool/SetInt/SetFloat/SetString(key, value) |
Creates or overwrites a flag. |
GetBool/GetInt/GetFloat/GetString(key, default) |
Never throws — returns default for a missing key or a key held under a different kind. |
TryGetBool/TryGetInt/TryGetFloat/TryGetString(key, out value) |
false for missing/wrong-kind; use to distinguish "missing" from "wrong kind" when that matters. |
Has(key) |
True if the key holds any value, any kind. |
TryGetKind(key, out kind) |
The current FlagKind of a key. |
Unset(key) |
Removes a flag; returns whether one was actually removed. |
Keys, GetAll() |
Enumeration for diagnostics/editor tooling. |
Clear() |
Bulk reset — see change-event semantics below. |
CaptureSnapshot() / RestoreSnapshot(snapshot) |
The save-integration seam. |
event Action<FlagChange> Changed |
Fires only on an actual value change. |
IWorldFlagService mirrors this surface 1:1 (plus the inherited ILoLEngineService lifecycle),
so game code almost never touches WorldFlagStore directly — it's an implementation detail of
the service, exposed publicly for testability and for the rare case a system wants a private,
unsaved flag store of its own.
Key design decisions¶
- Kind-overwrite, not kind-locking: setting a key that already holds a different
FlagKindsilently overwrites both the kind and the value. There is no "wrong type" exception at write time — a flag is just "whatever the lastSet*call said it is." This keeps the store forgiving for loosely-coordinated systems (quest data staged as strings during content iteration, then promoted to bools once stable) without a migration step. - Reads never throw: every
Get*takes a default and returns it for both "never set" and "set under a different kind." UseTryGetKind/TryGet*when the distinction matters. - Change events fire only on real change: setting the same kind and value a flag already
holds is a no-op, no event.
Unsetfires withNewValue == null; the firstSeton a new key fires withOldValue == null. Clear()andRestoreSnapshot()are bulk operations, not N unsets: neither raises per-flagChangedevents (a "new game" reset or a full save-load isn't "the player's 1,000 flags all changed one at a time"). A caller that needs per-flag notification for a bulk operation should enumerateGetAll()and callUnsetitself.
Save behavior¶
WorldFlagsSaveData : GameplayKitSaveData is the module's save domain (DataId =
"gameplaykit.flags", SchemaVersion = 1). WorldFlagService:
- Gets/registers the domain via
ISaveSystem.GetData<WorldFlagsSaveData>()inInitialize(). - Subscribes to the domain's inherited
BeforeSaveEvent/AfterLoadEvent(the sanctioned "subscribe to save notifications from external components" seam onPersistableData) — capturing aWorldFlagStore.CaptureSnapshot()into the domain before serialization, and callingWorldFlagStore.RestoreSnapshot()from the domain right after deserialization. - Also restores immediately in
Initialize(), in case the domain already held data (a load that happened before this service existed, or a shared domain another system populated first).
If ISaveSystem is not available (module registered without it, or the engine's save service
disabled), the service still works — flags just don't persist. A warning is logged once at
Initialize().
Events contract¶
WorldFlagChangedEvent : GameEvent is dispatched via IEventManager.TriggerEvent — pooled,
per the Kit's Events contract. Fields: Key, Kind, OldValue, NewValue (boxed as the
underlying bool/int/float/string; null on first-set's OldValue or unset's NewValue).
WorldFlagService also exposes an equivalent plain C# event Action<FlagChange> Changed for
non-GameEvent consumers (tests, editor tooling) that don't want to pay for pool plumbing.
If IEventManager is not available, the service still works — flag changes just don't raise
WorldFlagChangedEvent (the C# Changed event on the service still fires either way, since
that path doesn't go through the engine event system). A warning is logged once at Initialize().
Seam ledger¶
Per the Kit's portability discipline (doc 07, "Portability discipline"), every module's docs
page lists exactly which engine surface it consumes. WorldFlagStore (the headless core)
consumes none — zero UnityEngine/LoLEngine.Core references. WorldFlagService (the
service layer) consumes:
| Engine surface | Namespace | Used for |
|---|---|---|
ILoLEngineService |
LoLEngine.Core.ServiceManagement.Interfaces |
Service lifecycle (Initialize/Shutdown). |
ServiceDependencyAttribute |
LoLEngine.Core.ServiceManagement.Dependencies |
Documentation-of-intent only: the engine's dependency sorter never processes externally registered services, so the attribute has no runtime effect here. |
IEventManager, GameEvent |
LoLEngine.Core.Events.Interfaces / LoLEngine.Core.Events.GameEvents |
Pooled WorldFlagChangedEvent dispatch. |
ISaveSystem, PersistableData (via GameplayKitSaveData) |
LoLEngine.Core.DataPersistence.Interfaces / .Data |
Save domain registration and the BeforeSaveEvent/AfterLoadEvent hooks. |
UnityEngine.Debug |
UnityEngine |
Degrade-gracefully warnings when an optional dependency is absent. |
WorldFlagsWindow (editor) additionally consumes UnityEditor.EditorWindow/EditorGUILayout
and LoLEngine.Core.ServiceManagement.Service.ServiceLocator — expected for an editor-only
inspector, out of scope for the portability discipline (which concerns runtime code only).