Files
gamedev-the-steward/docs/SIMULATION_STATE_SCHEMA.md
T
2026-07-12 01:10:10 +02:00

9.4 KiB
Raw Blame History

The Steward — Simulation State Schema

Current contract

SimulationStateRecord is the versioned JSON boundary for the current simulation. The current world schema is v7 and captures:

  • simulation seed, tick interval, tick count, clock remainder, and elapsed clock ticks;
  • every NPC's identity, needs, attributes, task lifecycle, target, position, starvation state, and decision RNG stream;
  • village resource counters;
  • controlled per-NPC wander RNG streams;
  • scene-independent resource state plus the definition facts needed while its ResourceNode is unloaded;
  • pantry contents, carried NPC inventory, the ordered economic event stream, and its next stable event ID;
  • directed relationship records with familiarity, trust, and the stable event ID that last changed trust;
  • per-NPC known-event records that reference objective event history without copying it, including first-acquisition method, acquisition tick, and historical communicator provenance.

The top-level identity is:

{
  "schema": "the_steward.simulation",
  "schema_version": 7
}

Unknown schema versions and structurally incomplete records are rejected. Add an explicit migration function before accepting a future version; never silently reinterpret old data as the newest layout.

NPC action and profession references are serialized as stable strings and restored as StringName. Records referencing unknown executable actions or professions are rejected against the validated definition registry. See the simulation definitions.

Deterministic continuation

RNG seeds and internal states are encoded as decimal strings. They are signed 64-bit values and cannot safely pass through every JSON number implementation without precision loss. Converting them to ordinary JSON numbers caused a restored simulation to retain the same visible state while silently changing its future random sequence.

tests/simulation_state_serialization_test.gd protects this contract by:

  1. running a fixed-seed scenario for 48 ticks without interruption;
  2. running the same scenario for 24 ticks;
  3. serializing and restoring into a fresh manager;
  4. running the remaining 24 ticks;
  5. requiring the complete final-state checksums to match.

It also verifies clock remainder, resource amount/reservation/enabled round-tripping, presentation unload/rebind, directed relationship/cause round-tripping, divergent known-event state, communicated provenance, and deterministic retention boundaries, and rejection of unsupported schemas.

NPCStateRecord v2 adds the resolved travel destination and whether it is active. Nested v1 NPC records migrate explicitly with no invented active destination. This allows a reloaded visual to resume travel without changing target selection or deterministic RNG state.

NPCStateRecord v3 adds carried inventory. SimulationStateRecord v2 adds StorageStateRecord entries; world-schema v1 migrates legacy village food and wood into the stable village_pantry and village_woodpile records. Parsed storage values are canonicalized so save/restore continuation retains byte-stable checksums.

SimulationStateRecord v3 adds ordered EconomicEventRecord entries and next_event_id. World schemas v1 and v2 migrate explicitly to an empty event stream. Event records use stable source/destination/item IDs, reject invalid or duplicate IDs, and preserve deterministic ordering across save/restore. See the economic event stream.

EconomicEventRecord v2 adds the authoritative world position captured when the fact is recorded. Nested v1 events migrate with a zero-vector fallback; old saves already encode their stable location IDs, and migrated knowledge is not recomputed spatially.

SimulationStateRecord v4 moves social authority into top-level directed RelationshipStateRecord entries. Each record contains observer_id, subject_id, familiarity, trust, and last_trust_cause_event_id (-1 when no event has changed trust). Parsing requires unique ordered pairs, existing NPC IDs, bounded relationship values, and a real event for every non-empty cause ID. The relationship system serializes records in stable observer/subject order so they participate in deterministic checksums.

World schema v3 migrates its NPC-local familiarity pairs into directed relationships with neutral trust. Nested NPC schema v4 removes that obsolete duplicate field; v3 NPC records remain accepted only through the explicit migration. World schemas v1v2 also derive the graph from any legacy familiarity data (or initialize it empty) after their storage/event migrations.

SimulationStateRecord v5 adds top-level KnownEventStateRecord entries keyed by knower_id and event_id. The objective EconomicEventRecord remains the single completed fact; knowledge records only state which NPC knows it. Parsing requires unique NPC/event pairs and valid references to both an existing NPC and an event in the same record. A relationship's causal event must also be known by that relationship's observer and, for the current trust contract, be a food deposit performed by the relationship subject.

World schema v4 migrates relationship causes into known-event records. The old schema used village-wide awareness, so this preserves the implied known fact without falsely inventing spatial witness provenance.

SimulationStateRecord v6 / KnownEventStateRecord v2 adds immutable first-acquisition provenance: performed, witnessed, communicated, or legacy. Communicated records also retain the stable NPC ID of the direct speaker. New direct facts are created only at event-record time from the actor and nearby NPC positions; historical events are never re-evaluated against current positions. A co-located shared-activity encounter can add one communicated reference to the original fact without appending or replaying the event.

World schema v5 migrates actor-owned knowledge to performed; non-actor knowledge becomes legacy because v5 may contain either a proximity witness or an older implied relationship fact. Parsing never invents that missing provenance. At runtime, communication requires a distinct source who currently knows the event through performed or witnessed acquisition. Persisted records validate that direct-method snapshot even after the source forgets. This enforces the current one-hop boundary and makes validation independent of array ordering.

SimulationStateRecord v7 / KnownEventStateRecord v3 adds acquired_tick and a snapshot of the direct speaker's acquisition method. Direct facts use the objective event tick; newly communicated facts use the conversation tick. The speaker snapshot keeps historical provenance valid even if the speaker later forgets their own copy, while runtime communication still requires a currently known performed/witnessed fact and remains one-hop.

World schema v6 migrates direct acquisition to the objective event tick. Communicated and ambiguous legacy facts receive a fresh retention lease at the saved simulation tick because their exact acquisition time was not previously stored. The v6 source record supplies the persisted direct-method snapshot.

Importance is intentionally derived rather than duplicated: a fact is lasting while it is the current trust cause for one of that knower's directed relationships; every other fact is recent. Current causal facts are protected so relationship state remains explainable. Each NPC keeps at most three recent facts, and recent facts at least one simulated day old are removed at the next deterministic daily-sized review. The objective event stream is never pruned by this rule.

Resource authority

SimulationManager owns ResourceStateRecord instances independently of the scene tree. Amount, reservation, enabled state, action/resource identity, yield, usage permissions, and discovery metadata remain available and serializable while no ResourceNode is loaded.

ResourceNode binds to a record by stable ID and provides presentation, interaction transforms, and active-world discoverability. Unloading a node does not delete its record. Loading another node with the same stable ID rebinds to the existing record and cannot overwrite its amount or reservation with scene defaults.

ResourceStateRecord v3 adds safety_risk, comfort_distance, and discovery_priority so unloaded resources keep the same target-selection meaning after save/restore. Nested v1 and v2 resource records are migrated explicitly.

Local quicksave boundary

SaveSlotStore writes the existing versioned JSON record to user://saves/quicksave.json. It validates the serialized record before replacement, limits file size, restricts slot names, preserves the previous file during replacement, and can recover that backup if replacement is interrupted. Loading parses and validates the complete record before mutating the simulation.

In main.tscn, F5 saves and F9 loads this slot. Restoration rebuilds active NPC visuals from authoritative state.

Deliberately out of scope

This phase does not yet provide:

  • a save-slot menu, metadata, thumbnails, autosaves, or multiple profiles;
  • migrations from any historical world schema other than the explicitly supported v1v6 layouts;
  • player inventory or player relationship records;
  • broader relationship dimensions, line-of-sight/hearing evidence, continuous or personalized memory decay, reinforcement, false beliefs, or multi-hop rumours;
  • persistence for the player transform or presentation-only scene state.

Those features should build on this boundary rather than inventing parallel serialization paths.