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
+221
View File
@@ -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 (24 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()));
}
}
+53
View File
@@ -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. 24 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,
}
+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)]
+1
View File
@@ -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;
+19
View File
@@ -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
+4
View File
@@ -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,
});
}
+92 -1
View File
@@ -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::*;
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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");
+6
View File
@@ -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",
+6
View File
@@ -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");