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:
2026-04-19 13:12:43 +02:00
co-authored by Claude Sonnet 4.6
parent 1282454b8d
commit c19d84f2e3
14 changed files with 797 additions and 2 deletions
+2
View File
@@ -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"));
+68 -1
View File
@@ -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)]