302 lines
16 KiB
Markdown
302 lines
16 KiB
Markdown
# The Steward — Simulation State Schema
|
||
|
||
## Current contract
|
||
|
||
`SimulationStateRecord` is the versioned JSON boundary for the current
|
||
simulation. The current world schema is v10 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;
|
||
- scene-independent animal identity, position, hunger, feeding history,
|
||
reservation, and availability state;
|
||
- 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;
|
||
- 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.
|
||
|
||
The top-level identity is:
|
||
|
||
```json
|
||
{
|
||
"schema": "the_steward.simulation",
|
||
"schema_version": 10
|
||
}
|
||
```
|
||
|
||
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](SIMULATION_DEFINITIONS.md).
|
||
|
||
## 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.
|
||
Active, resolved, and invalidated opportunity round-tripping, contradictory
|
||
target state, invalid resource sources, and cross-record event references are
|
||
covered too.
|
||
|
||
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](ECONOMIC_EVENTS.md).
|
||
|
||
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 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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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 the pantry as its source, an existing animal as its destination,
|
||
food as its item, the exact feed action cost, and a valid NPC actor or the
|
||
player sentinel. 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 their hunger in stable-ID order. `AnimalNode` binds a
|
||
loaded presentation and interaction point to that authority; the simulation
|
||
position remains 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 atomic
|
||
care operation: reserve or select the exact animal, withdraw one real food
|
||
from `village_pantry`, reduce that animal's hunger, and append one exact
|
||
`animal_fed` fact. Failed withdrawal changes neither animal nor history.
|
||
|
||
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 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–v9 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;
|
||
- generalized opportunity/quest definitions, acceptance, rewards, assignment,
|
||
dialogue, or player-facing quest-log state;
|
||
- persistence for the player transform or presentation-only scene state.
|
||
|
||
Those features should build on this boundary rather than inventing parallel
|
||
serialization paths.
|