Files
settled-reach/server/src/settings/mod.rs
T
jpmschweitzerandClaude Opus 4.6 aa79dd97e7 fix(simulation): Clippy cleanup and CI enforcement (#635)
Fix all Clippy warnings across the server codebase (2411 insertions, 1341
deletions). Raise type-complexity-threshold to 750 and too-many-arguments
to 12 in .clippy.toml for idiomatic Bevy ECS system signatures. The server
now passes `cargo clippy -- --deny warnings` cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:33:15 +01:00

204 lines
7.0 KiB
Rust

// Settings module (#627)
// Persistent settings via SQLite on the server side.
// Architecture: settings live on the SERVER in a SQLite database with per-player
// tables. The client sends ChangeSettings commands over IPC, same as any other
// player action. The client never touches the database directly.
//
// Scope: keybindings, audio volume, display preferences, accessibility options,
// AI-Enhanced Dialogue toggle (#646).
pub mod store;
pub mod types;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use std::sync::Mutex;
pub use store::SettingsStore;
pub use types::{SettingEntry, SettingValue, SettingsResponseWire};
/// Bevy resource wrapping the SQLite settings store.
///
/// Initialized at server startup with a path derived from the save directory.
/// v0.1: single player, player_id = "default".
/// Multiplayer-ready: per-player isolation via player_id column (D-010).
///
/// Uses Mutex because rusqlite::Connection is !Sync. The lock is held only
/// for the duration of each operation (sub-millisecond for SQLite).
#[derive(Resource)]
pub struct SettingsStoreResource {
store: Mutex<SettingsStore>,
/// Player ID for settings isolation. v0.1: always "default".
player_id: String,
}
impl SettingsStoreResource {
pub fn new(store: SettingsStore, player_id: String) -> Self {
Self {
store: Mutex::new(store),
player_id,
}
}
/// Get a single setting.
pub fn get(&self, key: &str) -> Option<SettingValue> {
let store = self.store.lock().expect("settings mutex poisoned");
match store.get(&self.player_id, key) {
Ok(val) => val,
Err(e) => {
tracing::error!("settings get({}) failed: {}", key, e);
None
}
}
}
/// Set a single setting. Returns true on success.
pub fn set(&self, key: &str, value: &SettingValue) -> bool {
let store = self.store.lock().expect("settings mutex poisoned");
match store.set(&self.player_id, key, value) {
Ok(()) => {
tracing::debug!("settings set: {} = {}", key, value);
true
}
Err(e) => {
tracing::error!("settings set({}) failed: {}", key, e);
false
}
}
}
/// Delete a single setting. Returns true if it existed.
pub fn delete(&self, key: &str) -> bool {
let store = self.store.lock().expect("settings mutex poisoned");
match store.delete(&self.player_id, key) {
Ok(deleted) => deleted,
Err(e) => {
tracing::error!("settings delete({}) failed: {}", key, e);
false
}
}
}
/// Get all settings as wire entries.
pub fn get_all_entries(&self) -> Vec<SettingEntry> {
let store = self.store.lock().expect("settings mutex poisoned");
match store.get_all_entries(&self.player_id) {
Ok(entries) => entries,
Err(e) => {
tracing::error!("settings get_all failed: {}", e);
vec![]
}
}
}
}
/// Buffer for pending settings commands received via IPC.
/// Drained by `process_settings_commands` each tick.
#[derive(Resource, Debug, Default)]
pub struct SettingsCommandBuffer {
commands: Vec<SettingsCommand>,
}
impl SettingsCommandBuffer {
pub fn push(&mut self, cmd: SettingsCommand) {
self.commands.push(cmd);
}
pub fn drain(&mut self) -> Vec<SettingsCommand> {
std::mem::take(&mut self.commands)
}
}
/// Settings command variants queued from PlayerAction.
#[derive(Debug, Clone)]
pub enum SettingsCommand {
/// Change a single setting.
Change { key: String, value: SettingValue },
/// Request a full settings dump.
RequestAll,
/// Delete a single setting (restore to default).
Delete { key: String },
}
/// Process settings commands from the IPC buffer.
/// Reads SettingsCommandBuffer, writes to SettingsStoreResource,
/// and stages a response in SnapshotBuffer.pending_settings_response.
pub fn process_settings_commands(
settings_store: Option<Res<SettingsStoreResource>>,
mut cmd_buffer: ResMut<SettingsCommandBuffer>,
mut snapshot_buffer: ResMut<crate::bridge::types::SnapshotBuffer>,
) {
let commands = cmd_buffer.drain();
if commands.is_empty() {
return;
}
let Some(store) = settings_store else {
tracing::warn!("settings commands received but SettingsStoreResource not registered");
return;
};
// Process all commands, last response wins (same tick batching).
// If multiple settings commands arrive in one tick, only the final response
// reaches the client. This is acceptable: the client sends one action per tick
// in normal use, and batch changes via RequestAll after bulk updates.
for cmd in commands {
match cmd {
SettingsCommand::Change { ref key, ref value } => {
if store.set(key, value) {
snapshot_buffer.pending_settings_response = Some(SettingsResponseWire {
success: true,
kind: "ack".into(),
settings: vec![SettingEntry {
key: key.clone(),
value: value.clone(),
}],
error: None,
});
} else {
snapshot_buffer.pending_settings_response = Some(SettingsResponseWire {
success: false,
kind: "ack".into(),
settings: vec![],
error: Some(format!("failed to set setting: {}", key)),
});
}
}
SettingsCommand::RequestAll => {
let entries = store.get_all_entries();
snapshot_buffer.pending_settings_response = Some(SettingsResponseWire {
success: true,
kind: "full".into(),
settings: entries,
error: None,
});
}
SettingsCommand::Delete { ref key } => {
// Idempotent: deleting a non-existent key is a silent no-op.
let _deleted = store.delete(key);
snapshot_buffer.pending_settings_response = Some(SettingsResponseWire {
success: true,
kind: "ack".into(),
settings: vec![],
error: None,
});
}
}
}
}
/// Settings plugin. Registers resources and the processing system.
pub struct SettingsPlugin;
impl Plugin for SettingsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SettingsCommandBuffer>().add_systems(
Update,
process_settings_commands
.after(crate::simulation::input::process_player_input)
.before(crate::perception::observer::compute_observer_snapshot),
);
tracing::debug!("SettingsPlugin initialized");
}
}