546 lines
17 KiB
Markdown
546 lines
17 KiB
Markdown
# Agrarian Persistence Design Document
|
|
|
|
## Purpose
|
|
|
|
This document defines Agrarian's persistence direction for the MVP foundation.
|
|
It separates what the game saves, what remains external metadata, how save data
|
|
should be versioned, and what must be deferred until later persistence systems
|
|
are needed.
|
|
|
|
The design goal is to preserve meaningful player and world progress without
|
|
locking the project into brittle prototype data.
|
|
|
|
## Persistence Principles
|
|
|
|
### Save Meaningful State
|
|
|
|
Persist things that change gameplay or player progress:
|
|
|
|
- player identity;
|
|
- survival stats;
|
|
- inventory;
|
|
- placed structures;
|
|
- world time and weather state;
|
|
- resource depletion where needed;
|
|
- active tile/package version.
|
|
|
|
Do not persist static terrain files, generated caches, editor-only assets, or
|
|
pure presentation state.
|
|
|
|
### Version Everything That Can Outlive A Build
|
|
|
|
Save data is a compatibility contract. Every persistent record should carry
|
|
enough version/context to migrate or safely reject it later.
|
|
|
|
Minimum version fields:
|
|
|
|
- save format version;
|
|
- game build/version;
|
|
- active map/tile ID;
|
|
- active tile package version;
|
|
- record type version where needed.
|
|
|
|
### External Metadata Stays External
|
|
|
|
Tile registry and terrain package metadata should remain external authoritative
|
|
metadata. Save data should reference tile IDs/package versions, not copy entire
|
|
tile registries into saves.
|
|
|
|
### Server Owns Writes
|
|
|
|
Only the server writes authoritative persistence records. Clients display
|
|
replicated state and may request actions, but they do not write authoritative
|
|
save state directly.
|
|
|
|
## MVP Persistence Scope
|
|
|
|
The MVP persistence scope is intentionally narrow:
|
|
|
|
- player identity;
|
|
- player survival snapshot;
|
|
- player inventory snapshot;
|
|
- placed campfire/shelter/basic build pieces;
|
|
- resource depletion state where needed for shared gameplay;
|
|
- world time;
|
|
- weather state/seed/source metadata;
|
|
- active Ground Zero tile ID;
|
|
- active Ground Zero tile package version.
|
|
|
|
Containers should be included once they become part of the playable loop. If
|
|
containers are not in the first playable loop yet, they remain a near-term
|
|
implementation item rather than a blocker for this design document.
|
|
|
|
## Player Identity
|
|
|
|
MVP player identity can be simple.
|
|
|
|
Acceptable early identity options:
|
|
|
|
- local platform/user identifier;
|
|
- deterministic test profile ID;
|
|
- server-assigned temporary player ID;
|
|
- future account ID once account services exist.
|
|
|
|
Persistence should avoid using player display names as primary keys. Display
|
|
names can change.
|
|
|
|
Minimum player record:
|
|
|
|
```text
|
|
PlayerId
|
|
DisplayName
|
|
LastKnownTileId
|
|
LastKnownTransform
|
|
SurvivalSnapshot
|
|
CareHistorySnapshot
|
|
InventorySnapshot
|
|
UpdatedAt
|
|
RecordVersion
|
|
```
|
|
|
|
## Survival Snapshot
|
|
|
|
Persist the survival state needed to resume a player:
|
|
|
|
- health;
|
|
- stamina;
|
|
- hunger;
|
|
- thirst;
|
|
- body temperature;
|
|
- injury severity;
|
|
- sickness severity;
|
|
- exhaustion;
|
|
- alive/dead state if death persistence is active.
|
|
- future corpse/backpack recovery records once death inventory loss is enabled.
|
|
|
|
Survival rates and tuning values should not be duplicated into the save unless
|
|
there is a specific compatibility reason. They belong in code/config/data
|
|
assets.
|
|
|
|
0.1.J decision: the MVP death/respawn loop does not spawn a physical corpse or
|
|
backpack actor yet. Respawn currently stabilizes the character at the Ground
|
|
Zero MVP spawn/home location without inventory-drop recovery. When inventory
|
|
loss is introduced, corpse/backpack records should be persistent, server-owned,
|
|
time-limited or decay-aware, tied to the death event id/location, and recoverable
|
|
through normal interaction rules.
|
|
|
|
MVP implementation note: `UAgrarianPersistenceSubsystem::SaveCurrentWorld`
|
|
captures live Agrarian player characters into `FAgrarianSavedPlayer`, including
|
|
transform, survival snapshot, care history snapshot, and inventory stacks.
|
|
`RestorePlayers` reapplies those records to matching live characters before or
|
|
alongside world actor restore.
|
|
|
|
The same save path captures authoritative world weather state from
|
|
`AAgrarianGameState`: world hour, collapsed weather enum, mapped provider inputs,
|
|
and `FAgrarianWeatherDebugSnapshot`. The debug snapshot includes weather source,
|
|
provider timestamp, tile ID, tile center coordinate, provider weather code, and
|
|
the final applied in-game weather state so weather-related save/load issues can
|
|
be inspected per tile.
|
|
|
|
`LoadCurrentWorld` is the unified MVP load entry point. It restores world
|
|
weather/time state first, then restores matching player records and persistent
|
|
world actors. Admin load commands should call this combined path so weather,
|
|
players, and structures are rehydrated from the same save slot instead of
|
|
partially restoring only actors or players.
|
|
|
|
## Care History Snapshot
|
|
|
|
Reserve a long-term care history snapshot beside the immediate survival
|
|
snapshot. The MVP records neutral normalized fields only; later lifecycle and
|
|
aging systems will decide how aggressively those histories affect health,
|
|
stamina, strength, endurance, disease resistance, injury risk, recovery, visual
|
|
aging, and old-age decline.
|
|
|
|
Reserved care history fields:
|
|
|
|
- nutrition quality;
|
|
- illness burden;
|
|
- injury burden;
|
|
- sleep quality;
|
|
- shelter quality;
|
|
- stress burden;
|
|
- workload burden;
|
|
- treatment quality.
|
|
|
|
## Inventory Snapshot
|
|
|
|
Persist inventory as item definition IDs and stack counts.
|
|
|
|
Do persist:
|
|
|
|
- item ID;
|
|
- stack count;
|
|
- durability/condition when added;
|
|
- container/location ID when containers exist.
|
|
|
|
Do not persist full item definition data. Item definitions belong in Data
|
|
Assets and are loaded by ID.
|
|
|
|
## World State
|
|
|
|
MVP world state should include:
|
|
|
|
- world/gameplay time;
|
|
- weather state;
|
|
- weather seed or source timestamp when available;
|
|
- active tile ID;
|
|
- active tile package version;
|
|
- save timestamp;
|
|
- save format version.
|
|
|
|
World time uses the MVP calendar target:
|
|
|
|
```text
|
|
4 real hours = 1 in-game day
|
|
```
|
|
|
|
Day/night presentation can later use real-region solar metadata, but the save
|
|
should persist the gameplay calendar state and enough tile context to recreate
|
|
presentation.
|
|
|
|
## Structure And Placement State
|
|
|
|
Placed gameplay structures should persist when they are part of the survival
|
|
loop.
|
|
|
|
Minimum placed object record:
|
|
|
|
```text
|
|
ObjectId
|
|
ObjectTypeId
|
|
TileId
|
|
Transform
|
|
OwnerPlayerId
|
|
HealthOrCondition
|
|
CustomState
|
|
CreatedAt
|
|
UpdatedAt
|
|
RecordVersion
|
|
```
|
|
|
|
For MVP, this includes:
|
|
|
|
- campfire;
|
|
- primitive shelter;
|
|
- basic build pieces once available.
|
|
|
|
Future structure persistence should support decay, ownership, permissions,
|
|
settlement membership, repair state, and destruction history.
|
|
|
|
## Resource Depletion State
|
|
|
|
Not every resource needs persistent depletion in the MVP.
|
|
|
|
Persist depletion when:
|
|
|
|
- multiple players can observe the changed state;
|
|
- the depleted state affects survival;
|
|
- immediate respawn would undermine the loop;
|
|
- the actor is intentionally part of a shared world memory test.
|
|
|
|
Avoid persisting every foliage or minor gatherable actor until performance and
|
|
world-density rules are clear.
|
|
|
|
Minimum resource state:
|
|
|
|
```text
|
|
ResourceId
|
|
ResourceTypeId
|
|
TileId
|
|
RemainingAmount
|
|
DepletedAt
|
|
RespawnAt
|
|
RecordVersion
|
|
```
|
|
|
|
## Tile Metadata In Save Data
|
|
|
|
Save data should reference external tile metadata, not duplicate it.
|
|
|
|
Persist in save:
|
|
|
|
- active tile ID;
|
|
- active tile package version;
|
|
- tile registry schema version;
|
|
- tile package checksum or manifest hash if needed for validation.
|
|
|
|
Keep external:
|
|
|
|
- full tile registry;
|
|
- terrain metadata;
|
|
- heightmap metadata;
|
|
- landform analysis;
|
|
- water/shoreline analysis;
|
|
- neighbor edge verification;
|
|
- tile package files.
|
|
|
|
Reason:
|
|
|
|
- terrain packages may improve over time;
|
|
- tile registry can grow independently;
|
|
- save files should stay small and migrate safely;
|
|
- player/world state should not corrupt when tile metadata updates.
|
|
|
|
## Containers
|
|
|
|
Containers are a near-term persistence item once storage gameplay exists.
|
|
|
|
Minimum container record:
|
|
|
|
```text
|
|
ContainerId
|
|
ContainerTypeId
|
|
TileId
|
|
Transform
|
|
OwnerPlayerId
|
|
InventorySnapshot
|
|
LockOrPermissionState
|
|
RecordVersion
|
|
```
|
|
|
|
Until containers are implemented, inventory can remain player-carried and
|
|
placed structure state can remain separate.
|
|
|
|
## Save Timing
|
|
|
|
MVP save timing should include:
|
|
|
|
- manual admin save command;
|
|
- load on server start;
|
|
- periodic server-side save interval once server state is changing regularly;
|
|
- explicit save before planned server shutdown.
|
|
|
|
Recommended initial interval:
|
|
|
|
```text
|
|
every 5 minutes during active play
|
|
```
|
|
|
|
The interval should be tunable. It should not block gameplay on slow disk I/O.
|
|
|
|
## Load On Server Start
|
|
|
|
Server startup should:
|
|
|
|
1. Load save metadata.
|
|
2. Validate save format version.
|
|
3. Validate active tile ID and package version.
|
|
4. Load world state.
|
|
5. Load placed structures/resource state.
|
|
6. Load known player records.
|
|
7. Spawn or restore runtime actors.
|
|
8. Log skipped or migrated records.
|
|
|
|
If save validation fails, the server should fail clearly or start a new world
|
|
only when explicitly configured to do so.
|
|
|
|
## Migration Strategy
|
|
|
|
Persistence must assume data formats will change.
|
|
|
|
Rules:
|
|
|
|
- never silently reinterpret old records as new formats;
|
|
- add migration functions when changing saved record shape;
|
|
- keep unknown fields harmless where possible;
|
|
- log migrations;
|
|
- reject unsupported future save versions;
|
|
- back up saves before destructive migrations.
|
|
|
|
MVP can use simple file saves, but the data shape should still include versions
|
|
so future database migration is possible.
|
|
|
|
## Storage Backend
|
|
|
|
MVP storage can be file-based while the system is small.
|
|
|
|
Acceptable MVP options:
|
|
|
|
- Unreal `USaveGame` records;
|
|
- JSON records for inspectability;
|
|
- hybrid approach where gameplay uses `USaveGame` and tile metadata remains
|
|
JSON/external.
|
|
|
|
Long-term candidates:
|
|
|
|
- SQLite for local/server structured state;
|
|
- PostgreSQL for persistent multiplayer services;
|
|
- object storage/CDN for tile packages;
|
|
- append-only event logs for economy/governance where auditability matters.
|
|
|
|
Do not choose the long-term database before the MVP proves the shape of the
|
|
state that needs to persist.
|
|
|
|
## Backup And Recovery
|
|
|
|
Persistence is not a backup by itself.
|
|
|
|
MVP operations should keep:
|
|
|
|
- repository backup;
|
|
- VM backup;
|
|
- save data backup;
|
|
- tile package backup;
|
|
- tile registry backup.
|
|
|
|
Before changing save formats, take a manual backup of current save data.
|
|
|
|
## Security
|
|
|
|
Persistent data should never store secrets.
|
|
|
|
Do not store:
|
|
|
|
- passwords;
|
|
- API tokens;
|
|
- private keys;
|
|
- admin reset tokens;
|
|
- mail credentials;
|
|
- DigitalOcean tokens.
|
|
|
|
Player records should store stable IDs and gameplay state, not sensitive
|
|
account credentials.
|
|
|
|
For the MVP `USaveGame` path, each saved player keeps both a backwards-compatible
|
|
`PlayerId` string and an `FAgrarianSavedPlayerIdentity` block. The stable ID
|
|
prefers the replicated `APlayerState` unique network ID when one exists, then
|
|
falls back to player name, then pawn name for local prototype sessions. The
|
|
identity block also records the display player name, raw network ID, whether
|
|
the network ID was used, and the last known pawn name. It deliberately does not
|
|
store passwords, tokens, emails, or platform credentials.
|
|
|
|
Player stats are stored through `FAgrarianSavedPlayer::Survival`, which captures
|
|
the replicated `FAgrarianSurvivalSnapshot`. The MVP stat save includes health,
|
|
stamina, exhaustion, hunger, thirst, body temperature, generic injury,
|
|
bleeding, sprain, sickness, death state, and death reason. Loading applies the
|
|
snapshot through `UAgrarianSurvivalComponent::ApplySavedState`, keeping clamping
|
|
and replicated change notifications inside the survival component.
|
|
|
|
Long-term care history placeholders are stored through
|
|
`FAgrarianSavedPlayer::CareHistory`. The saved `FAgrarianCareHistorySnapshot`
|
|
reserves nutrition, illness, injury, sleep, shelter, stress, workload, and
|
|
treatment quality data for later generational and health systems. Version 0.1.M
|
|
only persists and restores those placeholders; it does not apply aging,
|
|
lifespan, inheritance, or generational outcome gameplay.
|
|
|
|
Player inventory is stored through `FAgrarianSavedPlayer::Inventory`, copied
|
|
from `UAgrarianInventoryComponent::Items`, and restored through
|
|
`UAgrarianInventoryComponent::RestoreSavedItems`. Restore recomputes derived
|
|
values and broadcasts inventory changes so HUD/debug listeners see loaded item
|
|
state.
|
|
|
|
Container persistence reserves `FAgrarianSavedContainer` records with stable
|
|
container ID, container type ID, transform, item stacks, and owner player ID.
|
|
Version 0.1.M does not yet have a placed container actor to capture; the current
|
|
`simple_container` remains a craftable inventory item and is covered by player
|
|
inventory persistence. The saved container array is intentionally present now
|
|
so the first placed-container actor can use the same save format without a
|
|
schema break.
|
|
|
|
Ground Zero tile registry persistence is represented by
|
|
`FAgrarianSavedTileRegistryState` on `UAgrarianSaveGame`. The MVP defaults point
|
|
to active tile `gz_us_ca_pacifica_utm10n_e544_n4160`, registry source
|
|
`Data/Tiles/ground_zero_tiles.json`, registry schema version `1`, generation
|
|
version `1`, and package version `0`. This records the active tile baseline
|
|
inside the save file while the authoritative tile registry remains the external
|
|
JSON registry.
|
|
|
|
World time is stored in `UAgrarianSaveGame::WorldHours`. The persistence
|
|
subsystem captures it from `AAgrarianGameState::WorldHours` during world save
|
|
and restores it only on authority during world load.
|
|
|
|
Weather state is stored as the fallback `UAgrarianSaveGame::Weather` enum plus
|
|
`WeatherInputs` and `WeatherDebug` snapshots. When provider data exists, load
|
|
reapplies the mapped weather inputs; otherwise it restores the saved enum
|
|
weather state. This gives the MVP a deterministic fallback while preserving the
|
|
latest real/provider-mapped tile weather payload when one was available.
|
|
|
|
The server-side autosave interval lives on `AAgrarianGameGameMode` as
|
|
`ServerAutoSaveIntervalSeconds`, defaulting to five minutes. On authority,
|
|
`BeginPlay` starts a repeating timer that calls `RunServerAutoSave`, which uses
|
|
`UAgrarianPersistenceSubsystem::SaveCurrentWorld`. Setting the interval to zero
|
|
disables the MVP autosave timer.
|
|
|
|
Load-on-server-start also lives on `AAgrarianGameGameMode` through
|
|
`bLoadWorldOnServerStart`, enabled by default. On authoritative `BeginPlay`, the
|
|
GameMode registers the current MVP persistent actor classes, checks
|
|
`DoesSaveExist`, and calls `LoadCurrentWorld` without clearing existing map
|
|
actors so map-authored resources and startup content remain owned by the map.
|
|
|
|
Backup-before-save is enabled by default through
|
|
`UAgrarianPersistenceSubsystem::bBackupBeforeSave`. Before overwriting an
|
|
existing save, `WriteSave` copies the current slot file from `Saved/SaveGames`
|
|
into `Saved/SaveGames/Backups` with a UTC timestamp. Missing save files simply
|
|
skip backup creation, which keeps the first save path simple.
|
|
|
|
## Testing Gates
|
|
|
|
Minimum persistence smoke test:
|
|
|
|
1. Start server.
|
|
2. Join as a player.
|
|
3. Change survival/inventory state.
|
|
4. Place a campfire or primitive shelter.
|
|
5. Deplete one resource node if resource persistence is enabled.
|
|
6. Save manually.
|
|
7. Stop server.
|
|
8. Start server.
|
|
9. Confirm player/world state restored as expected.
|
|
|
|
Minimum migration test:
|
|
|
|
1. Load a previous save format fixture.
|
|
2. Run migration or compatibility load.
|
|
3. Confirm unsupported data is rejected or logged clearly.
|
|
4. Confirm supported data survives.
|
|
|
|
Minimum tile validation test:
|
|
|
|
1. Save active Ground Zero tile ID/package version.
|
|
2. Restart server.
|
|
3. Confirm the server validates the active package version.
|
|
4. Confirm clients can still download the required package from the tile
|
|
endpoint.
|
|
|
|
## Current Roadmap Decisions
|
|
|
|
This document defines:
|
|
|
|
- MVP persistence scope;
|
|
- save data vs external tile registry boundary;
|
|
- player identity save direction;
|
|
- player stats save direction;
|
|
- player inventory save direction;
|
|
- resource depletion persistence rule;
|
|
- world time save direction;
|
|
- weather seed/state save direction;
|
|
- container persistence direction;
|
|
- server-side save interval direction;
|
|
- load-on-server-start direction;
|
|
- initial tile registry persistence direction.
|
|
|
|
Implementation work remains tracked separately in the roadmap.
|
|
|
|
The MVP resource-node implementation stores depletion as existing map/tile
|
|
actor state. Each `AAgrarianResourceNode` may define a stable
|
|
`PersistenceNodeId`; otherwise the actor name is used as a fallback. Save files
|
|
capture `RemainingHarvests` for resource nodes present in the loaded world and
|
|
restore only matching existing nodes. This deliberately avoids spawning resource
|
|
nodes from saves, keeping tile-authored resources owned by tile content while
|
|
letting persistence remember depletion for active tiles.
|
|
|
|
Resource depletion is stored in `FAgrarianSavedResourceNode` records with stable
|
|
`ResourceNodeId`, remaining harvest count, and the MVP respawn flag. The
|
|
persistence subsystem captures active loaded resource nodes during world save
|
|
and applies depletion only to matching map-authored nodes during load.
|
|
|
|
## Open Questions
|
|
|
|
- Should the first playable MVP use `USaveGame`, JSON, or a hybrid save
|
|
backend?
|
|
- What is the first stable `SaveFormatVersion` value?
|
|
- Should disconnected players remain in world physically or be removed?
|
|
- How much inventory should death/respawn preserve?
|
|
- When do saves move from file-based records to database-backed records?
|
|
- How should family/generation records attach to player identity later?
|