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>
162 lines
10 KiB
Markdown
162 lines
10 KiB
Markdown
---
|
|
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 <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):
|
|
```bash
|
|
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): description" --description "body" --base main --head server
|
|
```
|