Server (7), client (5), CI (4) briefings plus joint integration plan. Save/load with D-085 per-game dirs, tier eviction/scope, test infra. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
10 KiB
Sprint 19: Persist — Server Tasks
Goal: The player can save and resume a game session with per-game directories; the simulation tier system gains eviction and scope pinning; and the first test infrastructure ships with information boundary validation and IPC hardening.
Branch: server
Agents: Dudley (simulation), Tyre (architecture), Hoshe (QA)
Carry-over from Sprint 18
None. Sprint 18 closed clean.
New Tickets
| # | Title | Blocked by |
|---|---|---|
| #553 | Save/load: server ECS extraction | #256 (done) |
| #96 | State serialization system | — |
| #97 | Timestamp-based eviction | — |
| #98 | Scope tag system | — |
| #200 | Test module organization | — |
| #272 | Information boundary negative test suite | #200 |
| #555 | Protocol version handshake: server | — |
Use db/connectors/ticket show <id> for full details.
Key Decisions
decisions/architecture.md— D-010 (determinism + info boundaries), D-020 (IPC architecture, MessagePack), D-026 (simulation tiers: Active/Background/State-saved/Ungenerated, timestamp eviction, scope tags), D-030 (three-layer test architecture), D-041 (knowledge graph data model, StableId)decisions/questions.md— Q-029 (save file format design — Sprint 19 ships quick-and-dirty, Q-029 tracks the thorough design pass for later)
Open Questions to Resolve Early
- Q-029: Save file format design — Sprint 19 uses MessagePack from
SaveStateV1. Resolution of full versioning/migration strategy is deferred. Do not block #553 on Q-029; proceed with MessagePack format as specified.
Notes
#553 — Save/load: server ECS extraction
server/src/simulation/save_state.rs already defines SaveStateV1 (done in #256). The data model is complete: tick, seed, RNG, player_knowledge: KnowledgeGraph, relationship_graph: RelationshipGraph, npc_states: Vec<NpcSaveState>. Roundtrip tests pass.
What this ticket must deliver:
- A
save_to_file(path: &Path, world: &World) -> Result<()>function: queries ECS for all relevant components, builds aSaveStateV1, callsstate.to_bytes(), writes to disk. Per-game directory path is provided by the client via a newIpcCommand::SaveGame { path: String }variant. - A
load_from_file(path: &Path, world: &mut World) -> Result<()>function: reads bytes, callsSaveStateV1::from_bytes, re-spawns entities, injectsKnowledgeGraph,RelationshipGraph, andSimulationTimeas resources, reseeds the RNG. - A
SaveCommandandLoadCommandIPC message pair wired throughserver/src/bridge/— server receives save/load triggers from the client, executes, sendsSaveComplete/LoadCompleteresponse. - Format version check on load: reject files with
format_version != SAVE_FORMAT_VERSIONwith a clear error.
Integration points: server/src/simulation/save_state.rs (data model), server/src/bridge/types.rs (new IPC commands), server/src/bridge/local.rs or tcp.rs (command dispatch), server/src/knowledge/graph.rs (KG re-injection), server/src/npc/relationships.rs (RelationshipGraph re-injection).
Gotcha: ECS entity IDs are generational — do not save bevy Entity handles. SaveStateV1 already uses StableId(u64) throughout. On load, re-spawn entities and re-register StableId -> Entity in EntityRegistry.
#96 — State serialization system
Complement to #553. Where #553 handles whole-game ECS extraction, #96 implements the per-NPC serialization primitive for tier transitions.
What this ticket must deliver:
- A
serialize_npc_to_frozen(entity: Entity, world: &World) -> NpcSaveStatefunction producing the frozen struct (~1-2 KB per NPC per D-026) - A
deserialize_npc_from_frozen(state: &NpcSaveState, commands: &mut Commands)that re-spawns a full NPC entity with the correct component set - Used by the tier system when evicting to
StateSaved: instead of keeping ECS components live, serialize toNpcSaveStateand despawn. On reactivation: deserialize and re-spawn. - Unit tests: serialize + deserialize produces an entity with identical component values
Existing shape: NpcSaveState in save_state.rs captures position, SecretSeverity, Relationships, stress, tolerance, contentment, and optional KnowledgeGraph. Verify this covers all components needed for Background/Active reconstruction. Flag any missing axis (D-024) in a code comment for follow-up.
#97 — Timestamp-based eviction
server/src/simulation/tier.rs has the tier marker components (ActiveSim, BackgroundSim, StateSaved) and the distance-based update_tier_markers system. What is missing: the LRU eviction when sim-space fills up.
What this ticket must deliver:
- A
LastInteractionTick(u64)component on all NPCs, updated whenever the player interacts with or observes an NPC - A
SimSpacePressureresource tracking currentActiveSimcount vs. capacity (cap: 80 per D-026) - An
evict_excess_activesystem: whenActiveSimcount exceeds capacity, demote the N oldest-by-LastInteractionTickentities toBackgroundSim(orStateSavedif beyond background radius) - Uses a priority queue (BinaryHeap keyed by
LastInteractionTick) for O(log N) eviction selection
Gotcha: eviction must not demote entities with active scope tags (see #98). The eviction system runs after #98's ScopeTag check.
#98 — Scope tag system
Scope tags are the mechanism by which certain NPCs stay pinned to ActiveSim regardless of distance or LRU pressure (D-026: "neighborhood, active-quest, colleague, known-contact").
What this ticket must deliver:
- A
ScopeTagcomponent (or enum-tagged component) with variants:Neighborhood,ActiveQuest,Colleague,KnownContact - A
ScopePinnedmarker component: attached to any NPC carrying aScopeTag, removed when no scope tags remain - The eviction system (#97) skips entities with
ScopePinned - Scope tags are assigned by gameplay systems:
Neighborhoodfrom proximity at session start,KnownContactfromKnowledgeGraphentries with confidence >=KnowsOf,ColleaguefromRelationshipGraphedges withFriendorColleaguekind,ActiveQuestreserved for future quest system
Integration: server/src/simulation/tier.rs (eviction exclusion), server/src/knowledge/graph.rs (KnownContact assignment trigger), server/src/npc/relationships.rs (Colleague assignment trigger).
#200 — Test module organization
server/src/test_world/ already exists with constants.rs, invariants.rs, mod.rs, reset.rs, and rooms/. This is the foundation.
What this ticket must deliver:
- Establish the external test module pattern for the server crate:
#[cfg(test)] mod testsin each module, plus a top-leveltests/directory alongsidesrc/for integration tests that run against the full simulation - Document the three-layer test architecture (D-030): Layer 1 = fixture-based serialization (fast), Layer 2 = mock subprocess protocol state machine (medium), Layer 3 = real subprocess integration (slow)
- Create
tests/integration/mod.rsas the entry point for Layer 3 tests - Ensure
cargo testinserver/runs all layers correctly - No-ops are fine for Layer 2 and 3 stubs — the important deliverable is the directory structure and entry points
#272 is blocked by this ticket — the information boundary tests land in the new structure.
#272 — Information boundary negative test suite
The core asymmetric information claim of the game: entity X cannot see what entity Y knows, unless the observation system explicitly grants it.
What this ticket must deliver:
- A suite of negative tests asserting that information does NOT cross boundaries:
- Player's
KnowledgeGraphdoes not contain NPC data that was not observed (no passive leakage) ObserverSnapshotfor the player does not include entities outside LOS (fog of perception holds)- Background-tier NPC
KnowledgeGraphis not updated by Active-tier systems (tier boundary holds) SaveStateV1for one NPC does not serialize another NPC'sKnowledgeGraph
- Player's
- Uses
test_world/for scenario setup — reuse existing helpers - These tests live in Layer 1 (pure unit) and Layer 2 (mock world) of D-030
Gotcha: "negative tests" means asserting absence. Use assert!(kg.entities.get(&id).is_none()) patterns — not just "test passed because nothing happened."
#555 — Protocol version handshake: server
server/src/bridge/types.rs defines PROTOCOL_VERSION: u8 = 14. The version is already included in ObserverSnapshot as pub version: u8.
What this ticket must deliver:
- Verify the first
ObserverSnapshotemitted after subprocess startup includesversion: PROTOCOL_VERSION - Add a handshake phase: before normal tick loop begins, server emits a minimal
HandshakeMessage { protocol_version: PROTOCOL_VERSION }as the very first framed message on the IPC channel - Client reads this message and validates before sending any
PlayerInput - If the server receives a
PlayerInputbefore completing handshake, log a warning and process normally (forward-compatible) - Integration point:
server/src/bridge/local.rs(startup sequence),server/src/bridge/framing.rs(message framing)
Coordinate with ci team (#556) — the client-side validation is their ticket.
Dependency Chain
#555 (protocol handshake: server) → #556 (ci: protocol handshake: client)
#200 (test module organization) → #272 (information boundary tests)
#98 (scope tag system) → feeds into #97 (eviction respects scope pins)
#256 (done: save state data model) → #553 (server ECS extraction)
#553 (server ECS extraction) → #554 (client: save/load UI)
#96 (state serialization) → feeds into #553 (used during ECS extraction)
Parallel starts: #555, #97, #98, #96, #200 — all unblocked week 1
#553 starts after #96 is at review stage (needs serialize_npc_to_frozen)
#272 starts after #200 merges
PR Workflow
When ready to submit, create a PR with tea CLI. All flags are required to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): description" --description "body" --base main --head server