--- title: "Sprint 19 — Server Briefing" description: "Save/load ECS extraction, state serialization, timestamp-based eviction, scope tags" type: sprint status: archived sprint: 19 team: "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 ` 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`. 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): ```bash tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): description" --description "body" --base main --head server ```