396 lines
22 KiB
Markdown
396 lines
22 KiB
Markdown
# The Steward — Simulation State Schema
|
||
|
||
## Current contract
|
||
|
||
`SimulationStateRecord` is the versioned JSON boundary for the current
|
||
simulation. The current world schema is v14 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, 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;
|
||
- 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;
|
||
- a player record holding the player-citizen's hunger, energy, and carried
|
||
inventory. The player's world position remains presentation-owned and is
|
||
intentionally not serialized;
|
||
- combatant records (health, weapon, faction, position, hostility) for NPC
|
||
defenders and standalone raiders/wolves;
|
||
- faction records (food, warriors, aggression, morale, stance, war plan) for
|
||
the village and the hill tribe.
|
||
|
||
The top-level identity is:
|
||
|
||
```json
|
||
{
|
||
"schema": "the_steward.simulation",
|
||
"schema_version": 14
|
||
}
|
||
```
|
||
|
||
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. 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](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.
|
||
|
||
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.
|
||
|
||
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 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–v10 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.
|