feat(simulation): bookmark definition system with bridge protocol (#614)
BookmarkPlugin, BookmarkRegistry, SelectedBookmark resources. Tycoon bookmark defined; PROTOCOL_VERSION bumped to 22. RequestBookmarkCatalog + ConfirmBookmark actions wired into process_player_input via BookmarkInputParams SystemParam bundle (resolves Bevy's 16-system-param limit). build_catalog() accepts optional CultureResolver for D-128 location-culture mapping. Snapshot delivery at tick-0 via SnapshotBuffer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
---
|
||||
title: "Bookmark Definition — Contract Spec (Sprint 36)"
|
||||
description: "Struct shape, module placement, and bridge protocol for the CK3-style bookmark system. Contract between server #614 and client #618."
|
||||
type: architecture
|
||||
status: draft
|
||||
ticket: "#614"
|
||||
decision_refs: [D-115, D-117, D-118, D-128, D-146]
|
||||
author: "Tyre"
|
||||
created: 2026-04-19
|
||||
updated: 2026-04-19
|
||||
---
|
||||
|
||||
# Bookmark Definition — Contract Spec
|
||||
|
||||
**Tickets:** server #614 (implementation), client #618 (consumer)
|
||||
**Decisions:** D-115 (creation = skills + bookmark), D-117 (tycoon is the v0.2 bookmark), D-118 (start = small business owner), D-128 (culture implicit in location), D-146 (tile-scale preview — not in contract)
|
||||
**Scope:** Minimum viable bookmark enumeration + selection. Skills live in a sibling system (#618 territory). Culture is derived from `starting_location_id` via the #679 API — NOT a field on `BookmarkDefinition`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
A bookmark is a **named starting scenario** the player chooses at character creation. It bundles:
|
||||
- A display identity (title, subtitle, flavor blurb) — what the player reads.
|
||||
- A starting-state seed (location, career, a small set of seed parameters) — what the simulation consumes.
|
||||
|
||||
The client enumerates available bookmarks on the character-creation screen and emits a selected `bookmark_id` + `starting_location_id` when the player confirms.
|
||||
|
||||
For v0.2 there is exactly one bookmark: `tycoon`. The system is built for one but must not hard-code one — future bookmarks (explorer, homesteader, etc.) plug in as additional static entries.
|
||||
|
||||
## 2. Module placement
|
||||
|
||||
Per server-team convention (see `server/src/settings/`, `server/src/knowledge/`), bookmarks get their own top-level module:
|
||||
|
||||
```
|
||||
server/src/bookmark/
|
||||
├── mod.rs # Plugin, registry resource, public API
|
||||
└── types.rs # BookmarkDefinition, BookmarkId, wire types
|
||||
```
|
||||
|
||||
Registered as a `BookmarkPlugin` and added to the `App` alongside `SettingsPlugin` and `KnowledgePlugin`. Exports flow through `server/src/lib.rs`:
|
||||
|
||||
```rust
|
||||
pub mod bookmark; // new
|
||||
```
|
||||
|
||||
Rationale: parallel to settings/knowledge — bookmarks are a first-class domain, not simulation state. A sub-module under `settings/` would be wrong (settings are player prefs; bookmarks are content).
|
||||
|
||||
## 3. Rust types
|
||||
|
||||
### 3.1 BookmarkId (stable string key)
|
||||
|
||||
```rust
|
||||
/// Stable identifier for a bookmark definition.
|
||||
///
|
||||
/// String-backed (not an enum) so new bookmarks can be added without bumping
|
||||
/// the protocol version. v0.2 ships exactly one: `"tycoon"`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct BookmarkId(pub String);
|
||||
|
||||
impl BookmarkId {
|
||||
pub const TYCOON: &'static str = "tycoon";
|
||||
pub fn as_str(&self) -> &str { &self.0 }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 BookmarkDefinition (server-internal)
|
||||
|
||||
```rust
|
||||
/// Full static definition of a bookmark. Loaded once at startup, immutable
|
||||
/// at runtime. Lives server-side; a projection (`BookmarkWire`) crosses the
|
||||
/// bridge.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookmarkDefinition {
|
||||
/// Stable ID (e.g. `"tycoon"`).
|
||||
pub id: BookmarkId,
|
||||
|
||||
/// Short display title for the bookmark card (≤32 chars).
|
||||
/// e.g. "Tycoon" — shown as the tab/tile header.
|
||||
pub title: String,
|
||||
|
||||
/// One-line subtitle under the title (≤64 chars).
|
||||
/// e.g. "Small business owner on the make."
|
||||
pub subtitle: String,
|
||||
|
||||
/// Flavor blurb shown on selection. 2–4 sentences, Mellanie-authored.
|
||||
/// Markdown NOT supported — plain text only.
|
||||
pub flavor: String,
|
||||
|
||||
/// Default starting location the bookmark places the character in.
|
||||
/// Format: `system_id` from `server/data/systems.db` (e.g. "GJ 35").
|
||||
/// The client location picker (#680) MAY let the player choose another
|
||||
/// location within the bookmark's allowed set; this is the default.
|
||||
pub default_location: String,
|
||||
|
||||
/// Candidate starting locations the player can pick from for this
|
||||
/// bookmark (#680). Includes `default_location`. Empty = default only.
|
||||
/// For v0.2 tycoon, this is the Van Maanen's Star system entry.
|
||||
pub allowed_locations: Vec<String>,
|
||||
|
||||
/// Career seed — determines initial skills weighting, inventory, and
|
||||
/// starting business. Enum so downstream systems (skill seeder,
|
||||
/// apartment generator, monologue pool selector) can pattern-match.
|
||||
pub career: CareerKind,
|
||||
|
||||
/// Starting capital in Tractus (D-118 small-business scale — not mogul).
|
||||
pub starting_capital_tractus: i64,
|
||||
|
||||
/// Visible in the character-creation screen. Use `false` to author
|
||||
/// work-in-progress bookmarks without exposing them to the client.
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
/// Career seed. v0.2: `Tycoon` only; extensible.
|
||||
/// Used server-side to route into career-specific initialization
|
||||
/// (monologue pool, apartment generator seed, starting inventory).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CareerKind {
|
||||
/// Tycoon career — small business owner, D-117/D-118.
|
||||
Tycoon,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 BookmarkRegistry (Bevy resource)
|
||||
|
||||
```rust
|
||||
/// Immutable at runtime: built once during `BookmarkPlugin::build`.
|
||||
/// BTreeMap for deterministic iteration (D-010 principle 4, D-041).
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub struct BookmarkRegistry {
|
||||
entries: BTreeMap<String, BookmarkDefinition>,
|
||||
}
|
||||
|
||||
impl BookmarkRegistry {
|
||||
pub fn get(&self, id: &str) -> Option<&BookmarkDefinition> { ... }
|
||||
pub fn available(&self) -> impl Iterator<Item = &BookmarkDefinition> { ... }
|
||||
pub fn contains(&self, id: &str) -> bool { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Wire protocol (client-facing)
|
||||
|
||||
### 4.1 `BookmarkWire` — the projection that crosses the bridge
|
||||
|
||||
Drop server-only fields (none today, but keep the two types separate so future additions — e.g. a `validation` closure — don't leak through serde). Locates in `server/src/bridge/types.rs` next to the other `*Wire` structs.
|
||||
|
||||
```rust
|
||||
/// Bookmark projection for the client. Sent as a catalog in
|
||||
/// `ObserverSnapshot.bookmark_catalog` immediately after handshake
|
||||
/// (and re-sent once if the client re-requests via PlayerAction).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct BookmarkWire {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub flavor: String,
|
||||
pub default_location: String,
|
||||
pub allowed_locations: Vec<String>,
|
||||
pub career: CareerKindWire, // mirror of CareerKind, #[serde(rename_all = "snake_case")]
|
||||
pub starting_capital_tractus: i64,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Delivery — where bookmarks show up on the wire
|
||||
|
||||
**Option A (chosen): piggyback on `ObserverSnapshot`.** Add a new field:
|
||||
|
||||
```rust
|
||||
// In ObserverSnapshot (server/src/bridge/types.rs)
|
||||
//
|
||||
// v22 adds: bookmark_catalog (#614, D-115/D-117).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bookmark_catalog: Option<BookmarkCatalog>,
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BookmarkCatalog {
|
||||
pub bookmarks: Vec<BookmarkWire>,
|
||||
}
|
||||
```
|
||||
|
||||
**Semantics:**
|
||||
- Populated for **exactly one tick** after the handshake completes, and for **exactly one tick** after a `PlayerAction::RequestBookmarkCatalog` arrives. `None` otherwise.
|
||||
- Same pattern as `settings_response` (#627) and `economy_snapshot` (#822) — one-shot catalog responses live in their own `Option<...>` field, not in the main snapshot every tick.
|
||||
- Bumps `PROTOCOL_VERSION`. **Coordination note:** #848 (conversation-system retirement) also lands a wire-format change this sprint. Whichever PR lands first takes the next version number (22); the second rebases and takes the one after. Update the protocol comment in `bridge/types.rs` with the correct ticket ref when you land.
|
||||
|
||||
**Rejected:** a separate out-of-band message type. The bridge already framed MessagePack as `ObserverSnapshot`-shaped (`HandshakeMessage` + `StartupMessage` are the only exceptions and both exist for lifecycle reasons). A third top-level message type would need handling in `local.rs` + `tcp.rs` + test harness without buying anything over a snapshot field.
|
||||
|
||||
### 4.3 New PlayerAction variants
|
||||
|
||||
```rust
|
||||
// In PlayerAction
|
||||
/// Client requests the full bookmark catalog (#614).
|
||||
/// Server responds with `ObserverSnapshot.bookmark_catalog` in the next tick.
|
||||
RequestBookmarkCatalog,
|
||||
|
||||
/// Player confirms character creation with a chosen bookmark (#614, #618).
|
||||
/// `bookmark_id` must match a `BookmarkId` the server emitted in
|
||||
/// BookmarkCatalog. `starting_location_id` must be in
|
||||
/// `BookmarkDefinition.allowed_locations` for that bookmark.
|
||||
///
|
||||
/// On invalid `bookmark_id` or `starting_location_id`: server pushes a
|
||||
/// `SimError { kind: ProtocolError, ... }` — client should treat as a
|
||||
/// fatal character-creation error (can't start the game).
|
||||
ConfirmBookmark {
|
||||
bookmark_id: String,
|
||||
starting_location_id: String,
|
||||
},
|
||||
```
|
||||
|
||||
Note: `ConfirmBookmark` is the **trigger** for transitioning from the character-creation screen into the live world. The downstream chain (apartment generation, starting knowledge seed, culture resolution via #679) fires on this action. Details of that chain are out of scope for this spec — #614 just delivers the action into the input queue and records the selection on a new resource.
|
||||
|
||||
### 4.4 Server-side selection state
|
||||
|
||||
```rust
|
||||
/// The confirmed bookmark selection for the current session.
|
||||
/// Populated when `ConfirmBookmark` is processed. `None` during the
|
||||
/// character-creation phase (before confirm) and always `None` in a
|
||||
/// fresh session.
|
||||
#[derive(Resource, Debug, Clone, Default)]
|
||||
pub struct SelectedBookmark {
|
||||
pub bookmark_id: Option<String>,
|
||||
pub starting_location_id: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
Downstream systems (apartment generator, skill seeder) read from this
|
||||
resource. Preserved across save/load (#553) as part of the SaveState.
|
||||
|
||||
## 5. Content source — how bookmarks get into the registry
|
||||
|
||||
v0.2 scope: **hard-coded in `server/src/bookmark/mod.rs`**. A single entry:
|
||||
|
||||
```rust
|
||||
fn register_default_bookmarks(registry: &mut BookmarkRegistry) {
|
||||
registry.insert(BookmarkDefinition {
|
||||
id: BookmarkId("tycoon".to_string()),
|
||||
title: "Tycoon".into(),
|
||||
subtitle: "Small business owner on the make.".into(),
|
||||
flavor: "<Mellanie to author — 2–4 sentences>".into(),
|
||||
default_location: "GJ 35".into(), // TBD — confirm with Miri
|
||||
allowed_locations: vec!["GJ 35".into()],
|
||||
career: CareerKind::Tycoon,
|
||||
starting_capital_tractus: 5_000,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Why not TOML/YAML from disk?**
|
||||
- One bookmark. File IO adds failure modes (missing file, parse errors) with no upside.
|
||||
- When the second bookmark lands (Sprint 38+?), promote to `content/bookmarks/*.toml` — 1 day of work, pattern already established by `content/brands/`.
|
||||
|
||||
**Flavor text:** `flavor` is `<Mellanie to author>` on first pass. Ping her when #614 lands so she can fill it in before #618 renders it.
|
||||
|
||||
**Default location:** Van Maanen's Star's `system_id`. Worth double-checking with Miri that it's `GJ 35` vs another entry in `server/data/systems.db`. Tagged as TBD in the code until confirmed.
|
||||
|
||||
## 6. Handshake-time flow
|
||||
|
||||
```
|
||||
client server
|
||||
| |
|
||||
|-- TCP/stdio connect ---------------->|
|
||||
| |
|
||||
|<----------- HandshakeMessage --------|
|
||||
| |
|
||||
|-- StartupMessage (seed, archetype)-->|
|
||||
| |
|
||||
| [server initializes BookmarkRegistry]
|
||||
| |
|
||||
|<-- ObserverSnapshot(tick=0) ---------| <-- bookmark_catalog: Some(...)
|
||||
| | (and nothing else interesting;
|
||||
| | no entities, no player)
|
||||
| |
|
||||
| [client renders character creation screen]
|
||||
| |
|
||||
|-- PlayerAction::ConfirmBookmark ---->|
|
||||
| |
|
||||
| [server reads SelectedBookmark,
|
||||
| spawns player entity, runs
|
||||
| apartment generator, etc.]
|
||||
| |
|
||||
|<-- ObserverSnapshot(tick=1...) ------| <-- normal gameplay begins
|
||||
```
|
||||
|
||||
The client MAY send `PlayerAction::RequestBookmarkCatalog` explicitly (e.g. if it missed tick 0) — server re-sends the same catalog.
|
||||
|
||||
## 7. What this spec does NOT cover
|
||||
|
||||
- **Skills.** D-115 includes skills in character creation, but skill selection is a parallel system; the bookmark just seeds the initial weighting via `career`. See #618 and a separate spec (not yet written).
|
||||
- **Culture resolution.** Culture is NOT on `BookmarkDefinition`. The client calls the `resolve_culture(location_id)` function from the #679 contract. See `sprint-36-culture-api-spec.md`.
|
||||
- **Apartment/starting-state generation.** Downstream of `ConfirmBookmark`. Fires when `SelectedBookmark` is populated. Out of scope for this ticket.
|
||||
- **Bookmark preview asset.** D-146 (tile-scale preview) is rendered client-side from the player's own character descriptor, not a server asset on `BookmarkDefinition`.
|
||||
- **Save-format integration.** `SelectedBookmark` is a resource; save/load (#553, D-085) already serializes resources — adding two fields is a line-item for the save-state author.
|
||||
|
||||
## 8. Implementation plan (for Dudley)
|
||||
|
||||
**Size:** ~1 day. Straightforward Bevy plugin + bridge types + one hard-coded entry.
|
||||
|
||||
| Step | File | Work |
|
||||
|------|------|------|
|
||||
| 1 | `server/src/bookmark/mod.rs` (new) | `BookmarkPlugin`, `BookmarkRegistry` resource, `SelectedBookmark` resource, `register_default_bookmarks` fn |
|
||||
| 2 | `server/src/bookmark/types.rs` (new) | `BookmarkId`, `BookmarkDefinition`, `CareerKind` |
|
||||
| 3 | `server/src/lib.rs` | `pub mod bookmark;` |
|
||||
| 4 | `server/src/bridge/types.rs` | `BookmarkWire`, `CareerKindWire`, `BookmarkCatalog`, `PROTOCOL_VERSION` bump to the next available number (coordinate with #848 — see §4.2), `bookmark_catalog` field on `ObserverSnapshot`, `RequestBookmarkCatalog` + `ConfirmBookmark` `PlayerAction` variants |
|
||||
| 5 | `server/src/main.rs` (or wherever `App` is built) | `App.add_plugins(BookmarkPlugin)` |
|
||||
| 6 | `server/src/perception/observer.rs` (`compute_observer_snapshot`) | Drain a `PendingBookmarkCatalog` flag — populate `bookmark_catalog` for one tick after handshake OR after `RequestBookmarkCatalog` |
|
||||
| 7 | `server/src/simulation/input.rs` (or wherever PlayerAction is dispatched) | Handle `RequestBookmarkCatalog` (set the flag) and `ConfirmBookmark` (validate against registry, write `SelectedBookmark`, push `SimError` on invalid) |
|
||||
| 8 | Unit tests in `server/src/bookmark/mod.rs` | Registry construction, round-trip serialization of `BookmarkWire`, `ConfirmBookmark` validation |
|
||||
| 9 | Update `server/src/bridge/types.rs` module doc comment | "v22 adds: bookmark_catalog (#614, D-115/D-117)" |
|
||||
|
||||
## 9. Open questions
|
||||
|
||||
1. **Default `starting_location_id` for tycoon** — is it `"GJ 35"`, `"GJ 144"`, or a station-level ID? Need Miri's call. Flag as TODO in code; doesn't block implementation.
|
||||
2. **Flavor text** — Mellanie to write once #614 lands. Temporarily use a placeholder; client handles empty strings gracefully.
|
||||
3. **Later bookmarks** — when a second bookmark is planned, promote to TOML. Not a v0.2 concern.
|
||||
|
||||
---
|
||||
|
||||
**Contract status:** Ready for #614 implementation. Client #618 can start against this spec once the server ticket opens a PR with the bridge types landed (any Sprint 36 mid-point is fine).
|
||||
@@ -0,0 +1,221 @@
|
||||
//! Bookmark definition system (#614, D-115/D-117).
|
||||
//!
|
||||
//! A bookmark is a named starting scenario: skills weighting + starting state +
|
||||
//! flavor text. The player chooses a bookmark on the character-creation screen.
|
||||
//! v0.2 ships exactly one bookmark: `"tycoon"`.
|
||||
|
||||
pub mod types;
|
||||
|
||||
pub use types::{BookmarkDefinition, BookmarkId, CareerKind};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::bridge::types::{BookmarkCatalog, BookmarkWire, CareerKindWire, SnapshotBuffer};
|
||||
use crate::knowledge::{CultureResolver, resolve_culture};
|
||||
|
||||
/// Immutable registry of bookmark definitions.
|
||||
///
|
||||
/// Built once during `BookmarkPlugin::build`, read-only at runtime.
|
||||
/// `BTreeMap` for deterministic iteration (D-010 principle 4, D-041).
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub struct BookmarkRegistry {
|
||||
entries: BTreeMap<String, BookmarkDefinition>,
|
||||
}
|
||||
|
||||
impl BookmarkRegistry {
|
||||
pub fn insert(&mut self, defn: BookmarkDefinition) {
|
||||
self.entries.insert(defn.id.0.clone(), defn);
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> Option<&BookmarkDefinition> {
|
||||
self.entries.get(id)
|
||||
}
|
||||
|
||||
pub fn available(&self) -> impl Iterator<Item = &BookmarkDefinition> {
|
||||
self.entries.values().filter(|d| d.available)
|
||||
}
|
||||
|
||||
pub fn contains(&self, id: &str) -> bool {
|
||||
self.entries.contains_key(id)
|
||||
}
|
||||
|
||||
/// Build the wire catalog for the client.
|
||||
///
|
||||
/// `allowed_locations_cultures` is populated via the culture resolver (D-128, #679)
|
||||
/// when one is provided. Entries for locations that fail resolution are empty strings
|
||||
/// — callers should log a warning; see the error handling spec in #679.
|
||||
pub fn build_catalog(&self, culture: Option<&CultureResolver>) -> BookmarkCatalog {
|
||||
let bookmarks: Vec<BookmarkWire> = self
|
||||
.available()
|
||||
.map(|defn| {
|
||||
let allowed_locations_cultures = defn
|
||||
.allowed_locations
|
||||
.iter()
|
||||
.map(|loc| {
|
||||
culture
|
||||
.and_then(|r| match resolve_culture(r, loc) {
|
||||
Ok(tag) => Some(tag.0),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
location = %loc,
|
||||
error = %e,
|
||||
"culture resolution failed for bookmark location"
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect();
|
||||
BookmarkWire {
|
||||
id: defn.id.0.clone(),
|
||||
title: defn.title.clone(),
|
||||
subtitle: defn.subtitle.clone(),
|
||||
flavor: defn.flavor.clone(),
|
||||
default_location: defn.default_location.clone(),
|
||||
allowed_locations: defn.allowed_locations.clone(),
|
||||
allowed_locations_cultures,
|
||||
career: CareerKindWire::from(defn.career),
|
||||
starting_capital_tractus: defn.starting_capital_tractus,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
BookmarkCatalog { bookmarks }
|
||||
}
|
||||
}
|
||||
|
||||
/// The confirmed bookmark selection for the current session.
|
||||
///
|
||||
/// `None` during the character-creation phase (before `ConfirmBookmark` is received).
|
||||
/// Downstream systems (apartment generator, skill seeder) read from this resource.
|
||||
/// Preserved across save/load (#553) as part of the SaveState.
|
||||
#[derive(Resource, Debug, Clone, Default)]
|
||||
pub struct SelectedBookmark {
|
||||
pub bookmark_id: Option<String>,
|
||||
pub starting_location_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Bookmark system plugin.
|
||||
///
|
||||
/// Registers `BookmarkRegistry`, `SelectedBookmark`, and the startup system that
|
||||
/// stages the initial catalog in `SnapshotBuffer` for tick-0 delivery.
|
||||
pub struct BookmarkPlugin;
|
||||
|
||||
impl Plugin for BookmarkPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let mut registry = BookmarkRegistry::default();
|
||||
register_default_bookmarks(&mut registry);
|
||||
|
||||
app.insert_resource(registry)
|
||||
.init_resource::<SelectedBookmark>()
|
||||
.add_systems(Startup, prime_initial_catalog);
|
||||
|
||||
tracing::debug!("BookmarkPlugin initialized");
|
||||
}
|
||||
}
|
||||
|
||||
/// Startup system: stage the initial bookmark catalog in `SnapshotBuffer` so it
|
||||
/// is delivered in the first `ObserverSnapshot` (tick 0).
|
||||
///
|
||||
/// `SnapshotBuffer` is registered by `BridgePlugin` (added before `BookmarkPlugin`).
|
||||
/// `CultureResolverResource` is optional — missing in test worlds and when
|
||||
/// `systems.db` is unavailable (server logs a warning in that case).
|
||||
fn prime_initial_catalog(
|
||||
registry: Res<BookmarkRegistry>,
|
||||
mut buffer: ResMut<SnapshotBuffer>,
|
||||
culture: Option<Res<crate::knowledge::CultureResolverResource>>,
|
||||
) {
|
||||
let resolver = culture.as_deref().map(|c| &c.0);
|
||||
buffer.pending_bookmark_catalog = Some(registry.build_catalog(resolver));
|
||||
tracing::debug!("BookmarkPlugin: initial catalog staged for tick-0 snapshot");
|
||||
}
|
||||
|
||||
/// Register all v0.2 bookmarks.
|
||||
///
|
||||
/// Hard-coded for now (single entry). Promote to TOML loading in Sprint 38+
|
||||
/// when a second bookmark is planned; the pattern is established by `content/brands/`.
|
||||
fn register_default_bookmarks(registry: &mut BookmarkRegistry) {
|
||||
registry.insert(BookmarkDefinition {
|
||||
id: BookmarkId("tycoon".to_string()),
|
||||
title: "Tycoon".into(),
|
||||
subtitle: "Small business owner on the make.".into(),
|
||||
// TODO(mellanie): author flavor text (2–4 sentences, plain text only).
|
||||
flavor: String::new(),
|
||||
// TODO(miri): confirm system_id for Van Maanen's Star.
|
||||
default_location: "GJ 35".into(),
|
||||
allowed_locations: vec!["GJ 35".into()],
|
||||
career: CareerKind::Tycoon,
|
||||
starting_capital_tractus: 5_000,
|
||||
available: true,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::types::CareerKindWire;
|
||||
|
||||
fn make_registry() -> BookmarkRegistry {
|
||||
let mut r = BookmarkRegistry::default();
|
||||
register_default_bookmarks(&mut r);
|
||||
r
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_contains_tycoon() {
|
||||
let r = make_registry();
|
||||
assert!(r.contains("tycoon"));
|
||||
assert_eq!(r.available().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_get_returns_definition() {
|
||||
let r = make_registry();
|
||||
let defn = r.get("tycoon").expect("tycoon missing");
|
||||
assert_eq!(defn.id.as_str(), "tycoon");
|
||||
assert!(!defn.title.is_empty());
|
||||
assert_eq!(defn.career, CareerKind::Tycoon);
|
||||
assert!(defn.starting_capital_tractus > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_builds_with_one_entry() {
|
||||
let r = make_registry();
|
||||
let catalog = r.build_catalog(None);
|
||||
assert_eq!(catalog.bookmarks.len(), 1);
|
||||
let wire = &catalog.bookmarks[0];
|
||||
assert_eq!(wire.id, "tycoon");
|
||||
assert_eq!(wire.career, CareerKindWire::Tycoon);
|
||||
assert!(!wire.default_location.is_empty());
|
||||
assert_eq!(wire.allowed_locations.len(), wire.allowed_locations_cultures.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_roundtrip_msgpack() {
|
||||
let r = make_registry();
|
||||
let catalog = r.build_catalog(None);
|
||||
let bytes = rmp_serde::to_vec_named(&catalog).expect("serialize");
|
||||
let decoded: BookmarkCatalog = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded.bookmarks.len(), 1);
|
||||
assert_eq!(decoded.bookmarks[0].id, "tycoon");
|
||||
assert_eq!(decoded.bookmarks[0].career, CareerKindWire::Tycoon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_bookmark_validates_id() {
|
||||
let r = make_registry();
|
||||
assert!(r.get("tycoon").is_some());
|
||||
assert!(r.get("explorer").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_bookmark_validates_location() {
|
||||
let r = make_registry();
|
||||
let defn = r.get("tycoon").unwrap();
|
||||
assert!(defn.allowed_locations.contains(&"GJ 35".to_string()));
|
||||
assert!(!defn.allowed_locations.contains(&"Unknown System".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Stable identifier for a bookmark definition.
|
||||
///
|
||||
/// String-backed so new bookmarks can be added without bumping the protocol version.
|
||||
/// v0.2 ships exactly one: `"tycoon"`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct BookmarkId(pub String);
|
||||
|
||||
impl BookmarkId {
|
||||
pub const TYCOON: &'static str = "tycoon";
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Career seed used to route into career-specific initialization.
|
||||
///
|
||||
/// Pattern-matched by downstream systems (skill seeder, apartment generator,
|
||||
/// monologue pool selector) to derive starting state.
|
||||
/// v0.2: `Tycoon` only; extensible.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CareerKind {
|
||||
/// Tycoon career — small business owner, D-117/D-118.
|
||||
Tycoon,
|
||||
}
|
||||
|
||||
/// Full static definition of a bookmark. Loaded once at startup, immutable at runtime.
|
||||
///
|
||||
/// Lives server-side; a projection (`BookmarkWire`) crosses the bridge.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookmarkDefinition {
|
||||
/// Stable ID (e.g. `"tycoon"`).
|
||||
pub id: BookmarkId,
|
||||
/// Short display title for the bookmark card (≤32 chars).
|
||||
pub title: String,
|
||||
/// One-line subtitle under the title (≤64 chars).
|
||||
pub subtitle: String,
|
||||
/// Flavor blurb shown on selection. 2–4 sentences, plain text only.
|
||||
pub flavor: String,
|
||||
/// Default starting location (`system_id` from systems.db, e.g. `"GJ 35"`).
|
||||
pub default_location: String,
|
||||
/// Candidate starting locations the player can pick from.
|
||||
/// Includes `default_location`. Empty vec = default only.
|
||||
pub allowed_locations: Vec<String>,
|
||||
/// Career seed — determines initial skill weighting, inventory, and starting business.
|
||||
pub career: CareerKind,
|
||||
/// Starting capital in Tractus (D-118 small-business scale).
|
||||
pub starting_capital_tractus: i64,
|
||||
/// Whether this bookmark is visible in the character-creation screen.
|
||||
pub available: bool,
|
||||
}
|
||||
@@ -321,6 +321,7 @@ mod tests {
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +464,7 @@ mod tests {
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
let text = format_snapshot_text(&snap);
|
||||
assert!(text.contains("Tick 0"));
|
||||
|
||||
@@ -17,7 +17,7 @@ pub use crate::simulation::time::{DayPhase, TickRate};
|
||||
/// negotiation is unnecessary. Client should reject snapshots with version !=
|
||||
/// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration
|
||||
/// period, then the default is removed once both sides are updated.
|
||||
pub const PROTOCOL_VERSION: u8 = 21;
|
||||
pub const PROTOCOL_VERSION: u8 = 22;
|
||||
|
||||
/// Handshake message sent as the very first framed message after connection (#555).
|
||||
/// Client reads this before entering the normal tick loop and validates
|
||||
@@ -83,6 +83,8 @@ pub struct StartupMessage {
|
||||
/// v20 adds: settings_response (#627, SQLite settings IPC).
|
||||
/// v21 adds: economy_snapshot (#822, D-181 7-signal snapshot per queried system),
|
||||
/// EconStateQuery PlayerAction variant (#822).
|
||||
/// v22 adds: bookmark_catalog (#614, D-115/D-117 CK3-style bookmark system),
|
||||
/// RequestBookmarkCatalog + ConfirmBookmark PlayerAction variants (#614).
|
||||
/// Future fields: ambient sound events, HUD state (D-020 expansion).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObserverSnapshot {
|
||||
@@ -224,6 +226,56 @@ pub struct ObserverSnapshot {
|
||||
/// None during normal gameplay; client queries explicitly via `EconStateQuery`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub economy_snapshot: Option<EconomySnapshot>,
|
||||
/// Bookmark catalog (#614, D-115/D-117).
|
||||
/// Present for exactly one tick after handshake (tick 0) and after
|
||||
/// `PlayerAction::RequestBookmarkCatalog`. None otherwise.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bookmark_catalog: Option<BookmarkCatalog>,
|
||||
}
|
||||
|
||||
/// Career kind wire-safe mirror for `CareerKind` (#614, D-117).
|
||||
///
|
||||
/// Kept separate from the server-internal `CareerKind` so future server-only
|
||||
/// variants (e.g. debug/test archetypes) don't leak through serde.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CareerKindWire {
|
||||
Tycoon,
|
||||
}
|
||||
|
||||
impl From<crate::bookmark::types::CareerKind> for CareerKindWire {
|
||||
fn from(k: crate::bookmark::types::CareerKind) -> Self {
|
||||
match k {
|
||||
crate::bookmark::types::CareerKind::Tycoon => CareerKindWire::Tycoon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bookmark projection for the client (#614, D-115/D-117).
|
||||
///
|
||||
/// Sent inside `BookmarkCatalog` in `ObserverSnapshot.bookmark_catalog`.
|
||||
/// Drop server-only fields; keep the struct stable for the bridge.
|
||||
/// `allowed_locations_cultures` is pre-resolved server-side (§9 of culture spec, #679).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct BookmarkWire {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub flavor: String,
|
||||
pub default_location: String,
|
||||
pub allowed_locations: Vec<String>,
|
||||
/// Parallel to `allowed_locations`: resolved culture tag per location (#679).
|
||||
/// Populated by `BookmarkRegistry::build_catalog` once CultureResolverResource
|
||||
/// is available. Empty string placeholder until #679 lands.
|
||||
pub allowed_locations_cultures: Vec<String>,
|
||||
pub career: CareerKindWire,
|
||||
pub starting_capital_tractus: i64,
|
||||
}
|
||||
|
||||
/// Catalog of all available bookmarks sent to the client (#614).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BookmarkCatalog {
|
||||
pub bookmarks: Vec<BookmarkWire>,
|
||||
}
|
||||
|
||||
/// A single news ticker headline crossing the wire boundary (#591).
|
||||
@@ -564,6 +616,19 @@ pub enum PlayerAction {
|
||||
EconStateQuery {
|
||||
system_id: String,
|
||||
},
|
||||
/// Client requests the full bookmark catalog (#614).
|
||||
/// Server responds with `ObserverSnapshot.bookmark_catalog` in the next tick.
|
||||
RequestBookmarkCatalog,
|
||||
/// Player confirms character creation with a chosen bookmark (#614, #618).
|
||||
///
|
||||
/// `bookmark_id` must match a known `BookmarkId`. `starting_location_id` must
|
||||
/// be in `BookmarkDefinition.allowed_locations` for that bookmark.
|
||||
/// On invalid inputs: server pushes a `SimError { kind: ProtocolError }` and
|
||||
/// ignores the action — client should block the confirm button and re-prompt.
|
||||
ConfirmBookmark {
|
||||
bookmark_id: String,
|
||||
starting_location_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl PlayerAction {
|
||||
@@ -1038,6 +1103,8 @@ pub struct SnapshotBuffer {
|
||||
pub pending_settings_response: Option<crate::settings::types::SettingsResponseWire>,
|
||||
/// Pending economy snapshot, consumed once by `compute_observer_snapshot` (#822).
|
||||
pub pending_economy_response: Option<EconomySnapshot>,
|
||||
/// Pending bookmark catalog, consumed once by `compute_observer_snapshot` (#614).
|
||||
pub pending_bookmark_catalog: Option<BookmarkCatalog>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// The Settled Reach - Simulation Server
|
||||
// Rust/bevy_ecs simulation server for D-010 client-server architecture
|
||||
|
||||
pub mod bookmark;
|
||||
pub mod bridge;
|
||||
pub mod cause_chain;
|
||||
pub mod knowledge;
|
||||
|
||||
@@ -153,6 +153,23 @@ fn main() {
|
||||
app.add_plugins(settled_reach_server::npc::NpcPlugin);
|
||||
app.add_plugins(settled_reach_server::storyteller::StorytellerPlugin);
|
||||
app.add_plugins(settled_reach_server::settings::SettingsPlugin);
|
||||
app.add_plugins(settled_reach_server::bookmark::BookmarkPlugin);
|
||||
|
||||
// Initialize culture resolver (#679, D-128).
|
||||
// systems.db is shipped read-only alongside the binary.
|
||||
let systems_db_path = std::path::PathBuf::from("data/systems.db");
|
||||
match settled_reach_server::knowledge::CultureResolver::open(&systems_db_path) {
|
||||
Ok(resolver) => {
|
||||
tracing::info!("Culture resolver opened: {:?}", systems_db_path);
|
||||
app.insert_resource(settled_reach_server::knowledge::CultureResolverResource(resolver));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Culture resolver unavailable ({}). Culture lookups will not work.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize SQLite settings store (#627).
|
||||
// Path: alongside save files in the server's working directory.
|
||||
@@ -341,6 +358,7 @@ fn send_panic_error(app: &App, panic_msg: &str) {
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
sim_errors: vec![SimError {
|
||||
kind: SimErrorKind::Panic,
|
||||
message: format!("Simulation panic: {}", panic_msg),
|
||||
@@ -376,6 +394,7 @@ fn dump_schedule_graph() {
|
||||
app.add_plugins(settled_reach_server::npc::NpcPlugin);
|
||||
app.add_plugins(settled_reach_server::storyteller::StorytellerPlugin);
|
||||
app.add_plugins(settled_reach_server::settings::SettingsPlugin);
|
||||
app.add_plugins(settled_reach_server::bookmark::BookmarkPlugin);
|
||||
app.insert_resource(settled_reach_server::simulation::rng::SimRng::new(0));
|
||||
|
||||
// Access Schedules resource directly — schedules are populated by plugins
|
||||
|
||||
@@ -397,6 +397,9 @@ pub fn compute_observer_snapshot(
|
||||
// Consume pending economy snapshot for this tick (#822).
|
||||
let economy_snapshot = buffer.pending_economy_response.take();
|
||||
|
||||
// Consume pending bookmark catalog for this tick (#614).
|
||||
let bookmark_catalog = buffer.pending_bookmark_catalog.take();
|
||||
|
||||
// Consume pending save/load result for this tick (#553).
|
||||
let save_result = buffer.pending_save_result.take();
|
||||
|
||||
@@ -493,6 +496,7 @@ pub fn compute_observer_snapshot(
|
||||
current_ticker,
|
||||
settings_response,
|
||||
economy_snapshot,
|
||||
bookmark_catalog,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
// Timestamped player input events for deterministic simulation (D-010 principle 4)
|
||||
// PlayerInput: semantic actions (MoveNorth, Interact, UsePerceptionMode, ToggleStance)
|
||||
|
||||
use crate::bookmark::{BookmarkRegistry, SelectedBookmark};
|
||||
use crate::knowledge::CultureResolverResource;
|
||||
use crate::bridge::debug::DebugCommandBuffer;
|
||||
use crate::bridge::types::{FacingDirection, ObjectType, PlayerAction, PlayerInput};
|
||||
use crate::bridge::types::{
|
||||
FacingDirection, ObjectType, PlayerAction, PlayerInput, SimError, SimErrorKind, SnapshotBuffer,
|
||||
};
|
||||
use crate::knowledge::{EntityRegistry, StableId};
|
||||
use crate::perception::vision_cone::{facing_from_delta, Facing};
|
||||
use crate::settings::{SettingsCommand, SettingsCommandBuffer};
|
||||
@@ -18,6 +22,7 @@ use crate::simulation::stance::{PlayerMoveCooldown, Stance};
|
||||
use crate::simulation::time::{SimulationTime, TickRate};
|
||||
use crate::test_world::reset::{RoomResetTrigger, RoomSnapshots};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::system::SystemParam;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Maximum number of inputs the queue will hold before dropping oldest.
|
||||
@@ -78,6 +83,19 @@ impl InputQueue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundled SystemParam for bookmark-related input handling.
|
||||
///
|
||||
/// Bevy's blanket `IntoSystem` impl covers functions up to 16 parameters.
|
||||
/// Bundling the 4 bookmark params keeps `process_player_input` at exactly 16.
|
||||
#[derive(SystemParam)]
|
||||
pub struct BookmarkInputParams<'w> {
|
||||
pub registry: Option<Res<'w, BookmarkRegistry>>,
|
||||
pub selected: Option<ResMut<'w, SelectedBookmark>>,
|
||||
pub snapshot_buf: Option<ResMut<'w, SnapshotBuffer>>,
|
||||
pub sim_error_buf: Option<ResMut<'w, crate::bridge::types::SimErrorBuffer>>,
|
||||
pub culture: Option<Res<'w, CultureResolverResource>>,
|
||||
}
|
||||
|
||||
/// Drains InputQueue for the current tick, converts PlayerActions to ECS components.
|
||||
/// Handles stance toggling (D-053), movement cooldown, Take/Place verbs (#424),
|
||||
/// and save/load commands (#553).
|
||||
@@ -106,6 +124,7 @@ pub fn process_player_input(
|
||||
mut econ_query_buf: Option<ResMut<EconQueryBuffer>>,
|
||||
door_states: Query<&DoorState>,
|
||||
object_types: Query<&ObjectType>,
|
||||
mut bookmark: BookmarkInputParams<'_>,
|
||||
) {
|
||||
let current_tick = time.tick;
|
||||
let paused = time.paused();
|
||||
@@ -130,6 +149,8 @@ pub fn process_player_input(
|
||||
| PlayerAction::RequestAllSettings
|
||||
| PlayerAction::DeleteSetting { .. }
|
||||
| PlayerAction::EconStateQuery { .. }
|
||||
| PlayerAction::RequestBookmarkCatalog
|
||||
| PlayerAction::ConfirmBookmark { .. }
|
||||
)
|
||||
{
|
||||
continue;
|
||||
@@ -420,6 +441,30 @@ pub fn process_player_input(
|
||||
tracing::debug!("EconStateQuery received but EconQueryBuffer not registered — economy not loaded");
|
||||
}
|
||||
}
|
||||
PlayerAction::RequestBookmarkCatalog => {
|
||||
if let (Some(ref registry), Some(ref mut buf)) =
|
||||
(&bookmark.registry, &mut bookmark.snapshot_buf)
|
||||
{
|
||||
let resolver = bookmark.culture.as_deref().map(|c| &c.0);
|
||||
buf.pending_bookmark_catalog = Some(registry.build_catalog(resolver));
|
||||
tracing::debug!("RequestBookmarkCatalog: catalog staged");
|
||||
} else {
|
||||
tracing::warn!("RequestBookmarkCatalog: BookmarkRegistry or SnapshotBuffer not available");
|
||||
}
|
||||
}
|
||||
PlayerAction::ConfirmBookmark {
|
||||
bookmark_id,
|
||||
starting_location_id,
|
||||
} => {
|
||||
handle_confirm_bookmark(
|
||||
bookmark_id,
|
||||
starting_location_id,
|
||||
&bookmark.registry,
|
||||
&mut bookmark.selected,
|
||||
&mut bookmark.sim_error_buf,
|
||||
current_tick,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1121,6 +1166,52 @@ fn handle_teleport_to_hub(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_confirm_bookmark(
|
||||
bookmark_id: String,
|
||||
starting_location_id: String,
|
||||
registry: &Option<Res<BookmarkRegistry>>,
|
||||
selected_bookmark: &mut Option<ResMut<SelectedBookmark>>,
|
||||
sim_error_buf: &mut Option<ResMut<crate::bridge::types::SimErrorBuffer>>,
|
||||
tick: u64,
|
||||
) {
|
||||
let Some(ref registry) = registry else {
|
||||
tracing::warn!("ConfirmBookmark: BookmarkRegistry not available");
|
||||
return;
|
||||
};
|
||||
let Some(defn) = registry.get(&bookmark_id) else {
|
||||
let msg = format!("ConfirmBookmark: unknown bookmark_id {:?}", bookmark_id);
|
||||
tracing::warn!("{}", msg);
|
||||
if let Some(ref mut buf) = sim_error_buf {
|
||||
buf.push(SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: msg,
|
||||
tick,
|
||||
});
|
||||
}
|
||||
return;
|
||||
};
|
||||
if !defn.allowed_locations.contains(&starting_location_id) {
|
||||
let msg = format!(
|
||||
"ConfirmBookmark: starting_location_id {:?} not in allowed_locations for {:?}",
|
||||
starting_location_id, bookmark_id
|
||||
);
|
||||
tracing::warn!("{}", msg);
|
||||
if let Some(ref mut buf) = sim_error_buf {
|
||||
buf.push(SimError {
|
||||
kind: SimErrorKind::ProtocolError,
|
||||
message: msg,
|
||||
tick,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(ref mut sel) = selected_bookmark {
|
||||
sel.bookmark_id = Some(bookmark_id.clone());
|
||||
sel.starting_location_id = Some(starting_location_id.clone());
|
||||
tracing::info!(bookmark_id, starting_location_id, "ConfirmBookmark: selection recorded");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -78,6 +78,8 @@ fn snapshot_roundtrip_over_unix_socket() {
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
|
||||
bridge
|
||||
|
||||
@@ -64,6 +64,8 @@ fn snapshot_roundtrip_over_tcp() {
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
|
||||
bridge
|
||||
|
||||
@@ -307,6 +307,8 @@ fn snapshot_with_sim_errors_roundtrips() {
|
||||
}],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
|
||||
@@ -54,6 +54,8 @@ fn fixture_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +253,8 @@ fn generate_msgpack_fixtures() {
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
write_fixture(
|
||||
"snapshot_v2_full",
|
||||
@@ -418,6 +422,8 @@ fn generate_msgpack_fixtures() {
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
write_fixture(
|
||||
"snapshot_full",
|
||||
|
||||
@@ -42,6 +42,8 @@ fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +308,8 @@ fn snapshot_v2_fields_roundtrip() {
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
@@ -416,6 +420,8 @@ fn all_facing_direction_variants_roundtrip() {
|
||||
sim_errors: vec![],
|
||||
current_ticker: None,
|
||||
settings_response: None,
|
||||
economy_snapshot: None,
|
||||
bookmark_catalog: None,
|
||||
};
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
|
||||
Reference in New Issue
Block a user