Files
settled-reach/docs/sprints/sprint-19/server.md
T
jpmschweitzerandClaude Opus 4.6 bccdcfcdcb docs(docs): add frontmatter to all sprint briefings
Standardized YAML frontmatter on all 115 sprint briefing files across
sprints 1-26 with title, description, type, status, sprint number, and
team fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:15:45 +01:00

10 KiB

title, description, type, status, sprint, team
title description type status sprint team
Sprint 19 — Server Briefing Save/load ECS extraction, state serialization, timestamp-based eviction, scope tags sprint archived 19 server

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 a SaveStateV1, calls state.to_bytes(), writes to disk. Per-game directory path is provided by the client via a new IpcCommand::SaveGame { path: String } variant.
  • A load_from_file(path: &Path, world: &mut World) -> Result<()> function: reads bytes, calls SaveStateV1::from_bytes, re-spawns entities, injects KnowledgeGraph, RelationshipGraph, and SimulationTime as resources, reseeds the RNG.
  • A SaveCommand and LoadCommand IPC message pair wired through server/src/bridge/ — server receives save/load triggers from the client, executes, sends SaveComplete/LoadComplete response.
  • Format version check on load: reject files with format_version != SAVE_FORMAT_VERSION with 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) -> NpcSaveState function 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 to NpcSaveState and 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 SimSpacePressure resource tracking current ActiveSim count vs. capacity (cap: 80 per D-026)
  • An evict_excess_active system: when ActiveSim count exceeds capacity, demote the N oldest-by-LastInteractionTick entities to BackgroundSim (or StateSaved if 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 ScopeTag component (or enum-tagged component) with variants: Neighborhood, ActiveQuest, Colleague, KnownContact
  • A ScopePinned marker component: attached to any NPC carrying a ScopeTag, removed when no scope tags remain
  • The eviction system (#97) skips entities with ScopePinned
  • Scope tags are assigned by gameplay systems: Neighborhood from proximity at session start, KnownContact from KnowledgeGraph entries with confidence >= KnowsOf, Colleague from RelationshipGraph edges with Friend or Colleague kind, ActiveQuest reserved 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 tests in each module, plus a top-level tests/ directory alongside src/ 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.rs as the entry point for Layer 3 tests
  • Ensure cargo test in server/ 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:
    1. Player's KnowledgeGraph does not contain NPC data that was not observed (no passive leakage)
    2. ObserverSnapshot for the player does not include entities outside LOS (fog of perception holds)
    3. Background-tier NPC KnowledgeGraph is not updated by Active-tier systems (tier boundary holds)
    4. SaveStateV1 for one NPC does not serialize another NPC's KnowledgeGraph
  • 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 ObserverSnapshot emitted after subprocess startup includes version: 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 PlayerInput before 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