Skip to content

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 (zero UnityEngine/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 a WorldFlagStore, adds a save domain (WorldFlagsSaveData), and forwards store changes as a pooled WorldFlagChangedEvent on the engine's IEventManager.
  • Editor — WorldFlagsWindow: a Play Mode live inspector (Tools/LoLGameplayKit/World Flags) listing every flag with simple in-place editing.

Quickstart

  1. Create a GameplayKitConfig asset (Create → LoLGameplayKit), leave enableWorldFlags on, and place it in a Resources folder under the exact name GameplayKitConfig.
  2. That's it — registration is automatic. The module ships WorldFlagsServiceRegistrar, an IServiceRegistrar the engine's initializer discovers by assembly scan and constructs parameterlessly after all engine services are up. The registrar loads the config from Resources, checks the toggle, constructs WorldFlagService, calls Initialize() itself (the engine does not call Initialize/LateInitialize/Shutdown on externally registered services), and registers IWorldFlagService.
  3. 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
  1. React to changes made anywhere in the game via the pooled event (see Samples/Scripts/WorldFlagsSample.cs for 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 FlagKind silently overwrites both the kind and the value. There is no "wrong type" exception at write time — a flag is just "whatever the last Set* 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." Use TryGetKind/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. Unset fires with NewValue == null; the first Set on a new key fires with OldValue == null.
  • Clear() and RestoreSnapshot() are bulk operations, not N unsets: neither raises per-flag Changed events (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 enumerate GetAll() and call Unset itself.

Save behavior

WorldFlagsSaveData : GameplayKitSaveData is the module's save domain (DataId = "gameplaykit.flags", SchemaVersion = 1). WorldFlagService:

  • Gets/registers the domain via ISaveSystem.GetData<WorldFlagsSaveData>() in Initialize().
  • Subscribes to the domain's inherited BeforeSaveEvent/AfterLoadEvent (the sanctioned "subscribe to save notifications from external components" seam on PersistableData) — capturing a WorldFlagStore.CaptureSnapshot() into the domain before serialization, and calling WorldFlagStore.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).