feat(simulation): persist SelectedBookmark across save/load (#863, #862)

Adds Serialize/Deserialize to SelectedBookmark and wires it into
SaveStateV1 so a loaded game remembers which bookmark and starting
location the player picked. Replaces the TODO at bookmark/mod.rs:95
(originally deferred to Sprint 37 alongside #614).

Also refactors BookmarkPlugin to accept an injected BookmarkRegistry
via BookmarkPlugin::new(registry) (#862). The Default constructor
still wires the canonical tycoon registry — injection is for tests
and future TOML loading. Flagged in PR #132 review as a follow-up.

Updates bookmark spec §4.4 to remove the v0.2-deferred scope note.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 08:54:21 +02:00
co-authored by Claude Opus 4.6
parent 7c40067935
commit 9f755f2a62
4 changed files with 203 additions and 33 deletions
+49 -15
View File
@@ -20,7 +20,11 @@ use crate::knowledge::{resolve_culture, CultureResolver};
///
/// Built once during `BookmarkPlugin::build`, read-only at runtime.
/// `BTreeMap` for deterministic iteration (D-010 principle 4, D-041).
#[derive(Resource, Debug, Default)]
///
/// `Clone` is required by `BookmarkPlugin::build` which moves the registry into
/// the Bevy `App` via `insert_resource` while retaining the value from `self`
/// (#862 injection pattern).
#[derive(Resource, Debug, Default, Clone)]
pub struct BookmarkRegistry {
entries: BTreeMap<String, BookmarkDefinition>,
}
@@ -88,18 +92,15 @@ impl BookmarkRegistry {
}
/// 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.
///
/// **v0.2 scope: transient only.** Not serialized — save/load of
/// `SelectedBookmark` is deferred to Sprint 37 (follow-up ticket
/// filed alongside #614). Add `Serialize`/`Deserialize` derives and
/// wire into `SaveState` when that ticket is claimed.
/// Populated when `ConfirmBookmark` is processed. `None` fields during the
/// character-creation phase (before confirm) and in a fresh session.
///
/// Serialized into `SaveStateV1.selected_bookmark` (#863) so that a loaded
/// game remembers which bookmark and starting location were chosen.
///
/// Downstream systems (apartment generator, skill seeder) read from this resource.
// TODO(sprint-37): serialize see #863
#[derive(Resource, Debug, Clone, Default)]
#[derive(Resource, Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SelectedBookmark {
pub bookmark_id: Option<String>,
pub starting_location_id: Option<String>,
@@ -109,14 +110,47 @@ pub struct SelectedBookmark {
///
/// Registers `BookmarkRegistry`, `SelectedBookmark`, and the startup system that
/// stages the initial catalog in `SnapshotBuffer` for tick-0 delivery.
pub struct BookmarkPlugin;
///
/// **Default construction** uses the canonical tycoon registry:
/// ```ignore
/// app.add_plugins(BookmarkPlugin::default());
/// ```
///
/// **Injection** substitutes a custom registry (for tests and future TOML loading):
/// ```ignore
/// let mut registry = BookmarkRegistry::default();
/// // populate ...
/// app.add_plugins(BookmarkPlugin::new(registry));
/// ```
pub struct BookmarkPlugin {
registry: BookmarkRegistry,
}
impl BookmarkPlugin {
/// Create a plugin with an injected registry.
///
/// Useful in tests (inject a minimal registry with fixed entries) and
/// for future TOML loading (caller constructs the registry from disk,
/// then hands it to the plugin).
pub fn new(registry: BookmarkRegistry) -> Self {
Self { registry }
}
}
impl Default for BookmarkPlugin {
/// Default plugin pre-populates the canonical tycoon registry via
/// `register_default_bookmarks`. Equivalent to the v0.2 behaviour before
/// injection was introduced.
fn default() -> Self {
let mut registry = BookmarkRegistry::default();
register_default_bookmarks(&mut registry);
Self { registry }
}
}
impl Plugin for BookmarkPlugin {
fn build(&self, app: &mut App) {
let mut registry = BookmarkRegistry::default();
register_default_bookmarks(&mut registry);
app.insert_resource(registry)
app.insert_resource(self.registry.clone())
.init_resource::<SelectedBookmark>()
.add_systems(Startup, prime_initial_catalog);