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
+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::*;