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>
204 lines
7.0 KiB
Rust
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");
|
|
}
|
|
}
|