veloren/voxygen/src/settings.rs
2021-04-06 09:16:17 -07:00

840 lines
30 KiB
Rust

use crate::{
hud::{BarNumbers, BuffPosition, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar},
i18n,
render::RenderMode,
ui::ScaleMode,
window::{FullScreenSettings, GameInput, KeyMouse},
};
use directories_next::UserDirs;
use hashbrown::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use std::{fmt, fs, path::PathBuf};
use tracing::warn;
use vek::*;
use winit::event::{MouseButton, VirtualKeyCode};
// ControlSetting-like struct used by Serde, to handle not serializing/building
// post-deserializing the inverse_keybindings hashmap
#[derive(Serialize, Deserialize)]
struct ControlSettingsSerde {
keybindings: HashMap<GameInput, KeyMouse>,
}
impl From<ControlSettings> for ControlSettingsSerde {
fn from(control_settings: ControlSettings) -> Self {
let mut user_bindings: HashMap<GameInput, KeyMouse> = HashMap::new();
// Do a delta between default() ControlSettings and the argument, and let
// keybindings be only the custom keybindings chosen by the user.
for (k, v) in control_settings.keybindings {
if ControlSettings::default_binding(k) != v {
// Keybinding chosen by the user
user_bindings.insert(k, v);
}
}
ControlSettingsSerde {
keybindings: user_bindings,
}
}
}
/// `ControlSettings` contains keybindings.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(from = "ControlSettingsSerde", into = "ControlSettingsSerde")]
pub struct ControlSettings {
pub keybindings: HashMap<GameInput, KeyMouse>,
pub inverse_keybindings: HashMap<KeyMouse, HashSet<GameInput>>, // used in event loop
}
impl From<ControlSettingsSerde> for ControlSettings {
fn from(control_serde: ControlSettingsSerde) -> Self {
let user_keybindings = control_serde.keybindings;
let mut control_settings = ControlSettings::default();
for (k, v) in user_keybindings {
control_settings.modify_binding(k, v);
}
control_settings
}
}
/// Since Macbook trackpads lack middle click, on OS X we default to LShift
/// instead It is an imperfect heuristic, but hopefully it will be a slightly
/// better default, and the two places we default to middle click currently
/// (roll and wall jump) are both situations where you cannot glide (the other
/// default mapping for LShift).
#[cfg(target_os = "macos")]
const MIDDLE_CLICK_KEY: KeyMouse = KeyMouse::Key(VirtualKeyCode::LShift);
#[cfg(not(target_os = "macos"))]
const MIDDLE_CLICK_KEY: KeyMouse = KeyMouse::Mouse(MouseButton::Middle);
impl ControlSettings {
pub fn get_binding(&self, game_input: GameInput) -> Option<KeyMouse> {
self.keybindings.get(&game_input).copied()
}
pub fn get_associated_game_inputs(&self, key_mouse: &KeyMouse) -> Option<&HashSet<GameInput>> {
self.inverse_keybindings.get(key_mouse)
}
pub fn insert_binding(&mut self, game_input: GameInput, key_mouse: KeyMouse) {
self.keybindings.insert(game_input, key_mouse);
self.inverse_keybindings
.entry(key_mouse)
.or_default()
.insert(game_input);
}
pub fn modify_binding(&mut self, game_input: GameInput, key_mouse: KeyMouse) {
// For the KeyMouse->GameInput hashmap, we first need to remove the GameInput
// from the old binding
if let Some(old_binding) = self.get_binding(game_input) {
self.inverse_keybindings
.entry(old_binding)
.or_default()
.remove(&game_input);
}
// then we add the GameInput to the proper key
self.inverse_keybindings
.entry(key_mouse)
.or_default()
.insert(game_input);
// For the GameInput->KeyMouse hashmap, just overwrite the value
self.keybindings.insert(game_input, key_mouse);
}
/// Return true if this key is used for multiple GameInputs that aren't
/// expected to be safe to have bound to the same key at the same time
pub fn has_conflicting_bindings(&self, key_mouse: KeyMouse) -> bool {
if let Some(game_inputs) = self.inverse_keybindings.get(&key_mouse) {
for a in game_inputs.iter() {
for b in game_inputs.iter() {
if !GameInput::can_share_bindings(*a, *b) {
return true;
}
}
}
}
false
}
pub fn default_binding(game_input: GameInput) -> KeyMouse {
// If a new GameInput is added, be sure to update GameInput::iterator() too!
match game_input {
GameInput::Primary => KeyMouse::Mouse(MouseButton::Left),
GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right),
GameInput::ToggleCursor => KeyMouse::Key(VirtualKeyCode::Comma),
GameInput::Escape => KeyMouse::Key(VirtualKeyCode::Escape),
GameInput::Chat => KeyMouse::Key(VirtualKeyCode::Return),
GameInput::Command => KeyMouse::Key(VirtualKeyCode::Slash),
GameInput::MoveForward => KeyMouse::Key(VirtualKeyCode::W),
GameInput::MoveLeft => KeyMouse::Key(VirtualKeyCode::A),
GameInput::MoveBack => KeyMouse::Key(VirtualKeyCode::S),
GameInput::MoveRight => KeyMouse::Key(VirtualKeyCode::D),
GameInput::Jump => KeyMouse::Key(VirtualKeyCode::Space),
GameInput::Sit => KeyMouse::Key(VirtualKeyCode::K),
GameInput::Dance => KeyMouse::Key(VirtualKeyCode::J),
GameInput::Glide => KeyMouse::Key(VirtualKeyCode::LShift),
GameInput::Climb => KeyMouse::Key(VirtualKeyCode::Space),
GameInput::ClimbDown => KeyMouse::Key(VirtualKeyCode::LControl),
GameInput::SwimUp => KeyMouse::Key(VirtualKeyCode::Space),
GameInput::SwimDown => KeyMouse::Key(VirtualKeyCode::LShift),
GameInput::Fly => KeyMouse::Key(VirtualKeyCode::H),
GameInput::Sneak => KeyMouse::Key(VirtualKeyCode::LControl),
GameInput::ToggleLantern => KeyMouse::Key(VirtualKeyCode::G),
GameInput::Mount => KeyMouse::Key(VirtualKeyCode::F),
GameInput::Map => KeyMouse::Key(VirtualKeyCode::M),
GameInput::Bag => KeyMouse::Key(VirtualKeyCode::B),
GameInput::Trade => KeyMouse::Key(VirtualKeyCode::R),
GameInput::Social => KeyMouse::Key(VirtualKeyCode::O),
GameInput::Crafting => KeyMouse::Key(VirtualKeyCode::C),
GameInput::Spellbook => KeyMouse::Key(VirtualKeyCode::P),
GameInput::Settings => KeyMouse::Key(VirtualKeyCode::N),
GameInput::Help => KeyMouse::Key(VirtualKeyCode::F1),
GameInput::ToggleInterface => KeyMouse::Key(VirtualKeyCode::F2),
GameInput::ToggleDebug => KeyMouse::Key(VirtualKeyCode::F3),
GameInput::Fullscreen => KeyMouse::Key(VirtualKeyCode::F11),
GameInput::Screenshot => KeyMouse::Key(VirtualKeyCode::F4),
GameInput::ToggleIngameUi => KeyMouse::Key(VirtualKeyCode::F6),
GameInput::Roll => MIDDLE_CLICK_KEY,
GameInput::Respawn => KeyMouse::Key(VirtualKeyCode::Space),
GameInput::Interact => KeyMouse::Key(VirtualKeyCode::E),
GameInput::ToggleWield => KeyMouse::Key(VirtualKeyCode::T),
GameInput::FreeLook => KeyMouse::Key(VirtualKeyCode::L),
GameInput::AutoWalk => KeyMouse::Key(VirtualKeyCode::Period),
GameInput::CameraClamp => KeyMouse::Key(VirtualKeyCode::Apostrophe),
GameInput::CycleCamera => KeyMouse::Key(VirtualKeyCode::Key0),
GameInput::Slot1 => KeyMouse::Key(VirtualKeyCode::Key1),
GameInput::Slot2 => KeyMouse::Key(VirtualKeyCode::Key2),
GameInput::Slot3 => KeyMouse::Key(VirtualKeyCode::Key3),
GameInput::Slot4 => KeyMouse::Key(VirtualKeyCode::Key4),
GameInput::Slot5 => KeyMouse::Key(VirtualKeyCode::Key5),
GameInput::Slot6 => KeyMouse::Key(VirtualKeyCode::Key6),
GameInput::Slot7 => KeyMouse::Key(VirtualKeyCode::Key7),
GameInput::Slot8 => KeyMouse::Key(VirtualKeyCode::Key8),
GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9),
GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q),
GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::Tab),
GameInput::Select => KeyMouse::Key(VirtualKeyCode::Y),
GameInput::AcceptGroupInvite => KeyMouse::Key(VirtualKeyCode::U),
GameInput::DeclineGroupInvite => KeyMouse::Key(VirtualKeyCode::I),
}
}
}
impl Default for ControlSettings {
fn default() -> Self {
let mut new_settings = Self {
keybindings: HashMap::new(),
inverse_keybindings: HashMap::new(),
};
// Sets the initial keybindings for those GameInputs.
for game_input in GameInput::iterator() {
new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));
}
new_settings
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GamepadSettings {
pub game_buttons: con_settings::GameButtons,
pub menu_buttons: con_settings::MenuButtons,
pub game_axis: con_settings::GameAxis,
pub menu_axis: con_settings::MenuAxis,
pub game_analog_buttons: con_settings::GameAnalogButton,
pub menu_analog_buttons: con_settings::MenuAnalogButton,
pub pan_sensitivity: u32,
pub pan_invert_y: bool,
pub axis_deadzones: HashMap<crate::controller::Axis, f32>,
pub button_deadzones: HashMap<crate::controller::AnalogButton, f32>,
pub mouse_emulation_sensitivity: u32,
pub inverted_axes: Vec<crate::controller::Axis>,
}
impl Default for GamepadSettings {
fn default() -> Self {
Self {
game_buttons: con_settings::GameButtons::default(),
menu_buttons: con_settings::MenuButtons::default(),
game_axis: con_settings::GameAxis::default(),
menu_axis: con_settings::MenuAxis::default(),
game_analog_buttons: con_settings::GameAnalogButton::default(),
menu_analog_buttons: con_settings::MenuAnalogButton::default(),
pan_sensitivity: 10,
pan_invert_y: false,
axis_deadzones: HashMap::new(),
button_deadzones: HashMap::new(),
mouse_emulation_sensitivity: 12,
inverted_axes: Vec::new(),
}
}
}
pub mod con_settings {
use crate::controller::*;
use gilrs::{Axis as GilAxis, Button as GilButton};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GameButtons {
pub primary: Button,
pub secondary: Button,
pub toggle_cursor: Button,
pub escape: Button,
pub enter: Button,
pub command: Button,
pub move_forward: Button,
pub move_left: Button,
pub move_back: Button,
pub move_right: Button,
pub jump: Button,
pub sit: Button,
pub dance: Button,
pub glide: Button,
pub climb: Button,
pub climb_down: Button,
pub swimup: Button,
pub swimdown: Button,
pub sneak: Button,
pub toggle_lantern: Button,
pub mount: Button,
pub map: Button,
pub bag: Button,
pub quest_log: Button,
pub character_window: Button,
pub social: Button,
pub crafting: Button,
pub spellbook: Button,
pub settings: Button,
pub help: Button,
pub toggle_interface: Button,
pub toggle_debug: Button,
pub fullscreen: Button,
pub screenshot: Button,
pub toggle_ingame_ui: Button,
pub roll: Button,
pub respawn: Button,
pub interact: Button,
pub toggle_wield: Button,
pub swap_loadout: Button,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct MenuButtons {
pub up: Button,
pub down: Button,
pub left: Button,
pub right: Button,
pub scroll_up: Button,
pub scroll_down: Button,
pub scroll_left: Button,
pub scroll_right: Button,
pub home: Button,
pub end: Button,
pub apply: Button,
pub back: Button,
pub exit: Button,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GameAxis {
pub movement_x: Axis,
pub movement_y: Axis,
pub camera_x: Axis,
pub camera_y: Axis,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct MenuAxis {
pub move_x: Axis,
pub move_y: Axis,
pub scroll_x: Axis,
pub scroll_y: Axis,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GameAnalogButton {}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct MenuAnalogButton {}
impl Default for GameButtons {
fn default() -> Self {
// binding to unknown = getting skipped from processing
Self {
primary: Button::Simple(GilButton::RightTrigger2),
secondary: Button::Simple(GilButton::LeftTrigger2),
toggle_cursor: Button::Simple(GilButton::Unknown),
escape: Button::Simple(GilButton::Start),
enter: Button::Simple(GilButton::Unknown),
command: Button::Simple(GilButton::Unknown),
move_forward: Button::Simple(GilButton::Unknown),
move_left: Button::Simple(GilButton::Unknown),
move_back: Button::Simple(GilButton::Unknown),
move_right: Button::Simple(GilButton::Unknown),
jump: Button::Simple(GilButton::South),
sit: Button::Simple(GilButton::Unknown),
dance: Button::Simple(GilButton::Unknown),
glide: Button::Simple(GilButton::LeftTrigger),
climb: Button::Simple(GilButton::South),
climb_down: Button::Simple(GilButton::East),
swimup: Button::Simple(GilButton::South),
swimdown: Button::Simple(GilButton::East),
sneak: Button::Simple(GilButton::East),
toggle_lantern: Button::Simple(GilButton::DPadLeft),
mount: Button::Simple(GilButton::North),
map: Button::Simple(GilButton::Select),
bag: Button::Simple(GilButton::DPadRight),
quest_log: Button::Simple(GilButton::Unknown),
character_window: Button::Simple(GilButton::Unknown),
social: Button::Simple(GilButton::Unknown),
crafting: Button::Simple(GilButton::DPadDown),
spellbook: Button::Simple(GilButton::Unknown),
settings: Button::Simple(GilButton::Unknown),
help: Button::Simple(GilButton::Unknown),
toggle_interface: Button::Simple(GilButton::Unknown),
toggle_debug: Button::Simple(GilButton::Unknown),
fullscreen: Button::Simple(GilButton::Unknown),
screenshot: Button::Simple(GilButton::Unknown),
toggle_ingame_ui: Button::Simple(GilButton::Unknown),
roll: Button::Simple(GilButton::RightTrigger),
respawn: Button::Simple(GilButton::South),
interact: Button::Simple(GilButton::North),
toggle_wield: Button::Simple(GilButton::West),
swap_loadout: Button::Simple(GilButton::LeftThumb),
}
}
}
impl Default for MenuButtons {
fn default() -> Self {
Self {
up: Button::Simple(GilButton::Unknown),
down: Button::Simple(GilButton::Unknown),
left: Button::Simple(GilButton::Unknown),
right: Button::Simple(GilButton::Unknown),
scroll_up: Button::Simple(GilButton::Unknown),
scroll_down: Button::Simple(GilButton::Unknown),
scroll_left: Button::Simple(GilButton::Unknown),
scroll_right: Button::Simple(GilButton::Unknown),
home: Button::Simple(GilButton::DPadUp),
end: Button::Simple(GilButton::DPadDown),
apply: Button::Simple(GilButton::South),
back: Button::Simple(GilButton::East),
exit: Button::Simple(GilButton::Mode),
}
}
}
impl Default for GameAxis {
fn default() -> Self {
Self {
movement_x: Axis::Simple(GilAxis::LeftStickX),
movement_y: Axis::Simple(GilAxis::LeftStickY),
camera_x: Axis::Simple(GilAxis::RightStickX),
camera_y: Axis::Simple(GilAxis::RightStickY),
}
}
}
impl Default for MenuAxis {
fn default() -> Self {
Self {
move_x: Axis::Simple(GilAxis::RightStickX),
move_y: Axis::Simple(GilAxis::RightStickY),
scroll_x: Axis::Simple(GilAxis::LeftStickX),
scroll_y: Axis::Simple(GilAxis::LeftStickY),
}
}
}
impl Default for GameAnalogButton {
fn default() -> Self { Self {} }
}
impl Default for MenuAnalogButton {
fn default() -> Self { Self {} }
}
}
/// `InterfaceSettings` contains UI, HUD and Map options.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct InterfaceSettings {
pub toggle_debug: bool,
pub sct: bool,
pub sct_player_batch: bool,
pub sct_damage_batch: bool,
pub speech_bubble_dark_mode: bool,
pub speech_bubble_icon: bool,
pub crosshair_transp: f32,
pub chat_transp: f32,
pub chat_character_name: bool,
pub crosshair_type: CrosshairType,
pub intro_show: Intro,
pub xp_bar: XpBar,
pub shortcut_numbers: ShortcutNumbers,
pub buff_position: BuffPosition,
pub bar_numbers: BarNumbers,
pub ui_scale: ScaleMode,
pub map_zoom: f64,
pub map_drag: Vec2<f64>,
pub map_show_topo_map: bool,
pub map_show_difficulty: bool,
pub map_show_towns: bool,
pub map_show_dungeons: bool,
pub map_show_castles: bool,
pub loading_tips: bool,
pub map_show_caves: bool,
pub map_show_trees: bool,
pub minimap_show: bool,
pub minimap_face_north: bool,
}
impl Default for InterfaceSettings {
fn default() -> Self {
Self {
toggle_debug: false,
sct: true,
sct_player_batch: false,
sct_damage_batch: false,
speech_bubble_dark_mode: false,
speech_bubble_icon: true,
crosshair_transp: 0.6,
chat_transp: 0.4,
chat_character_name: true,
crosshair_type: CrosshairType::Round,
intro_show: Intro::Show,
xp_bar: XpBar::Always,
shortcut_numbers: ShortcutNumbers::On,
buff_position: BuffPosition::Bar,
bar_numbers: BarNumbers::Values,
ui_scale: ScaleMode::RelativeToWindow([1920.0, 1080.0].into()),
map_zoom: 10.0,
map_drag: Vec2 { x: 0.0, y: 0.0 },
map_show_topo_map: false,
map_show_difficulty: true,
map_show_towns: true,
map_show_dungeons: true,
map_show_castles: true,
loading_tips: true,
map_show_caves: true,
map_show_trees: true,
minimap_show: true,
minimap_face_north: false,
}
}
}
/// `GameplaySettings` contains sensitivity and gameplay options.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GameplaySettings {
pub pan_sensitivity: u32,
pub zoom_sensitivity: u32,
pub camera_clamp_angle: u32,
pub zoom_inversion: bool,
pub mouse_y_inversion: bool,
pub smooth_pan_enable: bool,
pub free_look_behavior: PressBehavior,
pub auto_walk_behavior: PressBehavior,
pub camera_clamp_behavior: PressBehavior,
pub stop_auto_walk_on_input: bool,
pub auto_camera: bool,
}
impl Default for GameplaySettings {
fn default() -> Self {
Self {
pan_sensitivity: 100,
zoom_sensitivity: 100,
camera_clamp_angle: 45,
zoom_inversion: false,
mouse_y_inversion: false,
smooth_pan_enable: false,
free_look_behavior: PressBehavior::Toggle,
auto_walk_behavior: PressBehavior::Toggle,
camera_clamp_behavior: PressBehavior::Toggle,
stop_auto_walk_on_input: true,
auto_camera: false,
}
}
}
/// `NetworkingSettings` stores server and networking settings.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct NetworkingSettings {
pub username: String,
pub servers: Vec<String>,
pub default_server: String,
pub trusted_auth_servers: HashSet<String>,
}
impl Default for NetworkingSettings {
fn default() -> Self {
Self {
username: "".to_string(),
servers: vec!["server.veloren.net".to_string()],
default_server: "server.veloren.net".to_string(),
trusted_auth_servers: ["https://auth.veloren.net"]
.iter()
.map(|s| s.to_string())
.collect(),
}
}
}
/// `Log` stores whether we should create a log file
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Log {
// Whether to create a log file or not.
// Default is to create one.
pub log_to_file: bool,
// The path on which the logs will be stored
pub logs_path: PathBuf,
}
impl Default for Log {
fn default() -> Self {
// Chooses a path to store the logs by the following order:
// - The VOXYGEN_LOGS environment variable
// - The ProjectsDirs data local directory
// This function is only called if there isn't already an entry in the settings
// file. However, the VOXYGEN_LOGS environment variable always overrides
// the log file path if set.
let logs_path = std::env::var_os("VOXYGEN_LOGS")
.map(PathBuf::from)
.unwrap_or_else(|| {
let mut path = voxygen_data_dir();
path.push("logs");
path
});
Self {
log_to_file: true,
logs_path,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq)]
pub enum Fps {
Max(u32),
Unlimited,
}
pub fn get_fps(max_fps: Fps) -> u32 {
match max_fps {
Fps::Max(x) => x,
Fps::Unlimited => u32::MAX,
}
}
impl fmt::Display for Fps {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Fps::Max(x) => write!(f, "{}", x),
Fps::Unlimited => write!(f, "Unlimited"),
}
}
}
/// `GraphicsSettings` contains settings related to framerate and in-game
/// visuals.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GraphicsSettings {
pub view_distance: u32,
pub sprite_render_distance: u32,
pub particles_enabled: bool,
pub figure_lod_render_distance: u32,
pub max_fps: Fps,
pub fov: u16,
pub gamma: f32,
pub exposure: f32,
pub ambiance: f32,
pub render_mode: RenderMode,
pub window_size: [u16; 2],
pub fullscreen: FullScreenSettings,
pub lod_detail: u32,
}
impl Default for GraphicsSettings {
fn default() -> Self {
Self {
view_distance: 10,
sprite_render_distance: 100,
particles_enabled: true,
figure_lod_render_distance: 300,
max_fps: Fps::Max(60),
fov: 70,
gamma: 1.0,
exposure: 1.0,
ambiance: 10.0,
render_mode: RenderMode::default(),
window_size: [1280, 720],
fullscreen: FullScreenSettings::default(),
lod_detail: 250,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AudioOutput {
/// Veloren's audio system wont work on some systems,
/// so you can use this to disable it, and allow the
/// game to function
// If this option is disabled, functions in the rodio
// library MUST NOT be called.
Off,
#[serde(other)]
Automatic,
}
impl AudioOutput {
pub fn is_enabled(&self) -> bool { !matches!(self, Self::Off) }
}
/// `AudioSettings` controls the volume of different audio subsystems and which
/// device is used.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct AudioSettings {
pub master_volume: f32,
pub music_volume: f32,
pub sfx_volume: f32,
pub max_sfx_channels: usize,
/// Audio Device that Voxygen will use to play audio.
pub output: AudioOutput,
}
impl Default for AudioSettings {
fn default() -> Self {
Self {
master_volume: 1.0,
music_volume: 0.4,
sfx_volume: 0.6,
max_sfx_channels: 30,
output: AudioOutput::Automatic,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct LanguageSettings {
pub selected_language: String,
}
impl Default for LanguageSettings {
fn default() -> Self {
Self {
selected_language: i18n::REFERENCE_LANG.to_string(),
}
}
}
/// `Settings` contains everything that can be configured in the settings.ron
/// file.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Settings {
pub controls: ControlSettings,
pub interface: InterfaceSettings,
pub gameplay: GameplaySettings,
pub networking: NetworkingSettings,
pub log: Log,
pub graphics: GraphicsSettings,
pub audio: AudioSettings,
pub show_disclaimer: bool,
pub send_logon_commands: bool,
// TODO: Remove at a later date, for dev testing
pub logon_commands: Vec<String>,
pub language: LanguageSettings,
pub screenshots_path: PathBuf,
pub controller: GamepadSettings,
}
impl Default for Settings {
fn default() -> Self {
let user_dirs = UserDirs::new().expect("System's $HOME directory path not found!");
// Chooses a path to store the screenshots by the following order:
// - The VOXYGEN_SCREENSHOT environment variable
// - The user's picture directory
// - The executable's directory
// This only selects if there isn't already an entry in the settings file
let screenshots_path = std::env::var_os("VOXYGEN_SCREENSHOT")
.map(PathBuf::from)
.or_else(|| user_dirs.picture_dir().map(|dir| dir.join("veloren")))
.or_else(|| {
std::env::current_exe()
.ok()
.and_then(|dir| dir.parent().map(PathBuf::from))
})
.expect("Couldn't choose a place to store the screenshots");
Settings {
controls: ControlSettings::default(),
interface: InterfaceSettings::default(),
gameplay: GameplaySettings::default(),
networking: NetworkingSettings::default(),
log: Log::default(),
graphics: GraphicsSettings::default(),
audio: AudioSettings::default(),
show_disclaimer: true,
send_logon_commands: false,
logon_commands: Vec::new(),
language: LanguageSettings::default(),
screenshots_path,
controller: GamepadSettings::default(),
}
}
}
impl Settings {
pub fn load() -> Self {
let path = Self::get_settings_path();
if let Ok(file) = fs::File::open(&path) {
match ron::de::from_reader::<_, Self>(file) {
Ok(mut s) => {
// Override the logs path if it is explicitly set using the VOXYGEN_LOGS
// environment variable. This is needed to support package managers that enforce
// strict application confinement (e.g. snap). In fact, the veloren snap package
// relies on this environment variable to be respected in
// order to communicate a path where the snap package is
// allowed to write to.
if let Some(logs_path_override) =
std::env::var_os("VOXYGEN_LOGS").map(PathBuf::from)
{
s.log.logs_path = logs_path_override;
}
return s;
},
Err(e) => {
warn!(?e, "Failed to parse setting file! Fallback to default.");
// Rename the corrupted settings file
let mut new_path = path.to_owned();
new_path.pop();
new_path.push("settings.invalid.ron");
if let Err(e) = std::fs::rename(&path, &new_path) {
warn!(?e, ?path, ?new_path, "Failed to rename settings file.");
}
},
}
}
// This is reached if either:
// - The file can't be opened (presumably it doesn't exist)
// - Or there was an error parsing the file
let default_settings = Self::default();
default_settings.save_to_file_warn();
default_settings
}
pub fn save_to_file_warn(&self) {
if let Err(e) = self.save_to_file() {
warn!(?e, "Failed to save settings");
}
}
pub fn save_to_file(&self) -> std::io::Result<()> {
let path = Self::get_settings_path();
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
let ron = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()).unwrap();
fs::write(path, ron.as_bytes())
}
pub fn get_settings_path() -> PathBuf {
if let Some(path) = std::env::var_os("VOXYGEN_CONFIG") {
let settings = PathBuf::from(&path).join("settings.ron");
if settings.exists() || settings.parent().map(|x| x.exists()).unwrap_or(false) {
return settings;
}
warn!(?path, "VOXYGEN_CONFIG points to invalid path.");
}
let mut path = voxygen_data_dir();
path.push("settings.ron");
path
}
}
pub fn voxygen_data_dir() -> PathBuf {
// Note: since voxygen is technically a lib we made need to lift this up to
// run.rs
let mut path = common_base::userdata_dir_workspace!();
path.push("voxygen");
path
}