28 KiB
The Steward — Simulation State Schema
Current contract
SimulationStateRecord is the versioned JSON boundary for the current local
simulation. The current world schema is v16 and captures:
- simulation seed, stable regional world/location scope, 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;
- scene-independent animal identity, position, hunger, feeding history, reservation, availability, current routine site, active travel destination, and next routine tick;
- pantry contents, carried NPC inventory, the ordered economic event stream, and its next stable event ID;
- sparse directed relationship records for familiarity, trust, affection, respect, fear, obligation, and hostility, with the exact causal event for every non-neutral change;
- player and NPC known-event records that reference objective event history without copying it, including acquisition source, hop count, confidence, salience, pin state, acquisition tick, and historical communicator provenance;
- opportunity records with stable type/status, interested NPC, trigger event, target storage/resource/amount, exact later resolution event identity, or a deterministic invalidation reason and close tick;
- world-backed situations, player journal discovery/tracking state, social commitments, and bounded semantic conversation acts. These records retain definition IDs and exact causal event references, never rendered dialogue prose or copied quest progress;
- a player record holding the player-citizen's hunger, energy, health, downed state, and carried inventory. The player's world position remains presentation-owned and is intentionally not serialized;
- combatant records (health, weapon, faction, position, hostility, and enemy definition ID) for NPC defenders and standalone hostile archetypes;
- faction records (food, warriors, aggression, morale, stance, war plan) for the village and the hill tribe.
- the player's Standing reputation (0–100), resolved-need count, and per-NPC gratitude, plus named player quest records that reference the originating opportunity or animal, requester NPC, resource/target/amount, Standing reward, and exact resolution or invalidation tick;
- the player's embodied state: hunger, energy, starvation progress, death count, and carried inventory with its capacity (schema v13).
The top-level identity is:
{
"schema": "the_steward.simulation",
"schema_version": 16
}
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:
- running a fixed-seed scenario for 48 ticks without interruption;
- running the same scenario for 24 ticks;
- serializing and restoring into a fresh manager;
- running the remaining 24 ticks;
- 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. Active, resolved, and invalidated opportunity round-tripping, contradictory target state, invalid resource sources, and cross-record event references are covered too. The emergent slice additionally verifies that situation evidence, journal links, commitment participants/outcomes, and semantic conversation history survive a save between accepting and completing real work.
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. NPC
inventory IDs and amounts are canonicalized to strings and floats; empty or
colliding normalized IDs, non-numeric values, and negative or non-finite
amounts are rejected.
Parsed storage values are likewise canonicalized so save/restore continuation
retains byte-stable checksums. Current storage parsing rejects empty item IDs,
negative or non-finite amounts, negative or non-finite capacity, and contents
above capacity. Runtime deposit and withdrawal requests also reject empty item
IDs and non-finite amounts without mutating authoritative stock. When the
authored pantry or woodpile is present, current world parsing also requires
its authoritative food or wood amount to match the duplicated village summary;
contradictory saves are rejected instead of silently changing checksum during
restore. World-v1 migration expands a storage's inferred capacity when needed
so valid legacy stock above the current default is preserved rather than
discarded.
The compound NPC feed_animal route needs no additional saved phase. An
in-progress record already contains the exact animal target_id, matching
animal reservation, carried inventory, task state, and active travel
destination. Insufficient carried food plus a pantry destination is the pickup
leg; the definition cost in inventory plus the animal destination is the
delivery leg. Restore emits only the saved travel request and never repeats
pickup. A compatible pre-delivery v11 feed record may instead have empty
inventory and the claimed animal as its destination; arrival redirects that
same claim through the current pantry waypoint before any withdrawal. If that
older record is already working, its first resumed tick performs the same
reroute and carries its saved work progress back to the goat. Successful
pickup changes the saved destination to the exact animal before publishing
synchronous inventory, event, or village signals, so signal-time saves cannot
encode carried food with a stale pantry destination.
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 v1–v2 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 combines persisted salience/pinning with current causal use. A fact is protected while it explains any relationship dimension, an open situation, journal discovery, or commitment. Unpinned ordinary memories remain bounded and age through deterministic reviews. The objective event stream is never pruned by this rule.
SimulationStateRecord v8 adds the top-level opportunities array and
simulation.next_opportunity_id. The first nested OpportunityStateRecord
schema stores opportunity_id, opportunity_type, status, created_tick,
trigger_event_id, interested_npc_id, target_id, resource_id,
target_amount, resolution_event_id, and resolved_tick. World schemas
v1–v7 migrate explicitly to an empty opportunity list with next ID zero.
SimulationStateRecord v9 preserves world-v8 opportunity history and upgrades
the nested record to OpportunityStateRecord v2. The nested schema adds
closed_tick and invalidation_reason, accepts supply_missing_wood, and adds
the stable invalidated status. Nested opportunity v1 records normalize their
resolved tick into the close tick and remain valid pantry history. Economic
event schema v3 adds normalized action_id and required_amount fields;
nested v1/v2 events receive empty/zero defaults, while a new task_blocked
event requires its stable action, source storage, resource, and positive
requirement contract.
The v3 event envelope also carries validated extension fields for authored
activity facts without adding a parallel quest record. An
activity_completed fact stores the target/action IDs, world and location
scope, metric ID, actual bounded delta, and exact item cost. Loading derives
the expected handler, effect, event type, metric, maximum delta, and cost from
the current action definition and rejects forged actor, discriminator, scope,
or amount combinations even when no knowledge record references the event.
Player and NPC performed knowledge points to that same immutable fact.
SimulationStateRecord v10 adds the top-level animals array and the first
AnimalStateRecord schema. Each animal record stores its stable animal and
species IDs, display name, authoritative world position, hunger,
last_fed_tick, reservation owner, and enabled/player/NPC feeding flags.
World schema v9 preserves opportunity history while migrating to an empty
animal list.
SimulationStateRecord v11 preserves world-v10 animals and upgrades nested
AnimalStateRecord v1 to v2. The nested schema adds routine_site_id,
travel_target_site_id, exact travel_target_position,
has_travel_target, and next_routine_tick. Migrated animals receive an
idle, immediately due routine without inventing a destination; loaded-world
selection establishes the first real target later.
SimulationStateRecord v12 lets RelationshipStateRecord reference the player
sentinel (PLAYER_ACTOR_ID) as a subject, so a villager's directed trust can
point at the player. World v11 migration adds nothing but accepts the relaxed
validation; existing NPC-only relationship history is preserved. Player-triggered
food supply is recorded as a pantry storage_deposited from the player inventory
(or, for legacy v11 saves, as a direct harvest into the pantry).
SimulationStateRecord v13 adds the top-level player record
(PlayerStateRecord v1) with the player-citizen's hunger, energy, and carried
inventory. World schemas v12 and earlier migrate to a fresh default player
record (hunger 40, energy 100, empty inventory). Player needs advance on the
deterministic tick with exact binary fractions so continuation checksums stay
stable. Resource records also migrate to v4, adding max_amount and
regrow_rate; legacy v3 resources keep their current amount as the cap and
gain no regrowth.
SimulationStateRecord v14 adds the top-level combatants and factions
arrays. CombatantStateRecord v1 tracks health, equipped weapon, faction,
hostile flag, and position for NPC defenders, tribe raiders, and wolves.
FactionStateRecord v1 tracks food, warriors, aggression, morale, stance, and
the tribe's war plan with its confidence. World schemas v13 and earlier migrate
to empty combatant lists plus default village/tribe factions; the manager
registers living NPC combatants on restore. Combatant positions are quantized
to a binary-exact grid and faction food to a binary-exact step so that
arbitrary world floats round-trip through JSON without breaking deterministic
continuation checksums.
PlayerStateRecord v2 adds health, max_health, downed, and
downed_ticks so wolves and raiders can wound and down the player, with a
brief forced recovery that restores a fighting baseline. Nested v1 player
records migrate to full health and standing. The player combatant used by the
conflict system is derived state and is never serialized; hostile attacks
reduce the authoritative player health through the ordered event stream.
SimulationStateRecord v15 adds top-level situations, quest_journal,
commitments, and conversation_history arrays plus monotonic next-ID counters
for each family. World schemas v1-v14 migrate to empty emergent collections and
zero counters. On manager restore, a compatible legacy pantry opportunity can
be imported once into the new situation system when no situation history is
present; the two records are not independently generated substitutes for world
state.
SituationStateRecord stores the definition and dedupe IDs, trigger event,
stable primitive context, selected objective alternative, lifecycle, and exact
resolution event. Live progress remains derived from the definition, ordinary
event store, and current authoritative facts. QuestJournalEntryStateRecord
stores discovery, tracking preference, selected alternative, and archive/close
state only; it neither copies objectives nor owns progress. The player therefore
cannot discover a journal entry until knowledge reaches them through an actual
conversation or other ordinary knowledge path.
CommitmentStateRecord links one situation to typed debtor and creditor entity
references, semantic terms, a deadline, lifecycle, and exact outcome event.
Acceptance does not reserve stock or prevent other actors from resolving the
situation. A matching outcome performed by the debtor fulfills it; another
actor's outcome supersedes it; a still-achievable missed deadline breaks it at
the due tick. Release remains an explicit social action. Consequence application
is keyed by commitment/outcome identity so replay or restore cannot award it
twice.
ConversationActStateRecord stores only a bounded semantic history: stable act
ID, tick, typed speaker/listener references, supported intent ID, and at most
eight topic IDs. Conversation IDs, current turns, option revisions, rendered
templates, balloon state, and prose are transient. Reopening dialogue plans from
current facts plus the retained semantic acts, so wording can evolve without
changing authoritative history.
SimulationStateRecord v16 adds non-empty world_id and location_id fields to
the top-level simulation record. Activity completion and blocked-action facts
retain that same scope in their immutable payload, so event-store indexes,
knowledge validation, and save restoration cannot reinterpret a local position
as belonging to another loaded context. World v15 migrates explicitly to the
existing Bosnia/Jajce IDs. An event log that already contains facts refuses a
different configured scope instead of silently reindexing history. Restore
preflights this scope against the active adapter before changing clock,
economy, NPC, or event state, so both empty- and populated-history mismatches
fail atomically. Older unstructured task_blocked facts are retained with
explicit legacy provenance for save continuity, but they are not promoted into
knowable evidence or new situation triggers.
Nested KnownEventStateRecord v4 adds the player as a valid knower/source and
records hop count, confidence, salience, and explicit pinning. Communicated
facts remain exactly one hop from a performed or witnessed source. Nested
RelationshipStateRecord v2 adds affection, respect, fear, obligation, and
hostility, makes either player or NPC a valid endpoint, and stores a causal
event ID for all seven dimensions. Nested AnimalStateRecord v3 persists an
animal_definition_id; older records migrate to the goat definition. Nested
CombatantStateRecord v2 persists enemy_definition_id; legacy hostile kinds
map to their current raider or wolf definitions, while NPC defenders retain an
empty enemy definition.
Current-schema parsing requires unique animal IDs that do not collide with
resource or storage IDs. An animal reservation must belong to an existing NPC
whose active feed task targets that exact animal. Every animal_fed event
must name an existing animal as its destination, food as its item, the exact
feed action cost, and a valid NPC actor or the player sentinel. New NPC events
name the matching npc_<id>_inventory; player events name the pantry. Direct
pantry-sourced NPC facts remain accepted as legacy-compatible v11 history,
while runtime emits matching-inventory sources for new NPC deliveries. The
latest matching feed event tick must agree with the animal's last_fed_tick.
The bounded opportunity family accepts at most one open record globally.
Opportunity, trigger-event, and resolution-event IDs are unique and the next
ID must remain above restored history. Referenced NPCs, storage, resources, and
events must exist. A restock_empty_pantry trigger is a positive NPC food
withdrawal from village_pantry to that actor's matching inventory; an open
record additionally requires the pantry to remain below its one-food target
and its interested NPC to have acquired that fact no later than creation. A
supply_missing_wood trigger is the performer's known patrol or study
task_blocked fact against village_woodpile with a one-wood requirement; an
open record additionally requires a living interested performer and the
woodpile below target.
Resolution references an exact later matching NPC inventory-to-storage deposit
or player resource_extracted event from an existing player-usable matching
resource into the target storage, with matching resolved and closed ticks.
Resolved history does not require the storage still to contain the resource
because later use is valid. Invalidation has no resolution event and requires a
stable close tick and is currently valid only for supply_missing_wood.
interested_died must agree with the restored NPC's death state; the runtime
creates evidence_stale at the one-day unresolved boundary.
While a need is open, its interested villager's trigger evidence is treated as lasting during deterministic memory maintenance. Resolution or invalidation releases that fact back to the normal bounded-retention rules; neither deletes objective event or opportunity history.
OpportunityHelperResult and OpportunityPlayerResponseResult are deliberately
absent from this schema. The helper result is re-derived from the open
opportunity plus current NPC knowledge, directed relationship, inventory,
action-definition, storage-capacity, and finite-resource records. The player
result additionally requires no capable helper and derives its ordinary gather
action, real destination, and source count from current enabled player-usable
finite resources. Save/restore regressions require the same applicable route
result and an unchanged checksum before and after querying.
When an idle derived helper chooses the reported supply action, only the existing NPC task, task-state, target, travel, and reservation fields serialize. The helper result remains absent: restore retains any in-progress ordinary task and independently re-derives current capability from restored facts.
Active-need communication adds no schema fields. It persists through the
existing KnownEventStateRecord event, listener, source NPC, direct source
method, and acquisition-tick fields. Communication and player-response HUD
animations are transient and are cleared rather than replayed after restore.
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. Current-schema parsing also canonicalizes numeric, boolean, and ID
variants so a freshly registered finite source retains a byte-stable checksum
after a JSON round trip.
Animal authority
AnimalCareSystem owns AnimalStateRecord instances independently of the
scene tree and advances hunger and due routine selection in stable-ID order.
AnimalNode binds a loaded presentation and interaction point to that
authority; the simulation position and selected destination remain canonical
while the node is loaded or absent.
Animals are not ResourceNode instances and are not inserted into the
resource discovery grid. The first bounded slice uses a deterministic linear
loaded-animal query. NPC and player feeding resolve through the same exact
animal mutation but pay from their real current source. Player care withdraws
directly from village_pantry. NPC care reserves the animal, performs an exact
pantry-to-inventory pickup at the authored storage waypoint, carries that unit
to the same claim, and consumes inventory only at delivery. Failed or partial
pickup changes neither inventory nor animal history.
The first routine consumers alternate Dunja and Zora between stable-ID private
shelters and one shared pasture without drawing RNG. AnimalRoutineSite
declares optional loaded-world resident ownership: an empty resident ID is
shared species context, while a named resident makes a shelter private.
ActiveWorldAdapter filters those loaded markers for each stable animal; site
ownership is not persistent state and does not require a schema bump. The
selected site ID and exact position do enter state, and each AnimalNode
follows a real NavigationServer3D path while reporting factual position
changes. A mid-route save restores the same position and target; unloading a
scene node pauses presentation only, and reloading it resumes from those facts.
Hunger cancels travel before care targeting.
Multiple animal records remain independently authoritative. A schema-v11 save may contain concurrent feed reservations only when each reserving NPC targets that exact animal in a valid travel or work state. Restoring Dunja and Zora preserves both claim pairings and the checksum. The same existing NPC fields preserve either pantry or goat travel plus carried food, so a post-pickup restore continues without another withdrawal or RNG draw. Completing one feed releases and mutates only its destination record.
The persistent hungry/content presentation derives from authoritative hunger. The short feed response is transient and is reset rather than serialized or replayed after restore.
Local quicksave boundary
SaveSlotStore writes the existing versioned local 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 v1-v16 layouts;
- line-of-sight/hearing evidence, personalized confidence decay, reinforcement, false beliefs, or multi-hop rumours;
- the full regional situation predicate library, regional quest bounds, contracts beyond the first social commitment consumer, or generated free-form prose;
- persistence of a currently open dialogue UI/turn. Semantic conversation history and commitment authority persist, while a balloon is regenerated after restore;
- production integration of the regional chunked save manifest, lazy location loading, or aggregate population promotion/persistence;
- persistence for the player transform or presentation-only scene state.
Those features should build on this boundary rather than inventing parallel serialization paths.