// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This module manages variables, settings, as well as evaluating // "if"-conditions in chats. use bevy::window::WindowMode; use bevy::prelude::*; use std::collections::HashMap; use serde::Deserialize; use toml_edit::DocumentMut; use std::env; use std::fs; pub const SCOPE_SEPARATOR: &str = "$"; pub const TOKEN_EQUALS: &str = "=="; pub const TOKEN_EQUALS_NOT: &str = "!="; pub const TOKEN_GREATER_THAN: &str = ">"; pub const TOKEN_LESS_THAN: &str = "<"; pub const TOKEN_GREATER_EQUALS: &str = ">="; pub const TOKEN_LESS_EQUALS: &str = "<="; pub const TOKEN_NEGATE: &str = "~"; pub const DEFAULT_CHAT_SPEED: f32 = 10.0; #[derive(Resource)] pub struct Settings { pub dev_mode: bool, pub god_mode: bool, pub version: String, pub mute_sfx: bool, pub mute_music: bool, pub volume_sfx: u8, pub volume_music: u8, pub mouse_sensitivity: f32, pub fov: f32, pub fov_highspeed: f32, pub zoom_fov: f32, pub zoom_sensitivity_factor: f32, pub font_size_hud: f32, pub font_size_conversations: f32, pub font_size_choices: f32, pub font_size_console: f32, pub font_size_speedometer: f32, pub hud_color: Color, pub hud_color_console: Color, pub hud_color_console_warn: Color, pub hud_color_console_system: Color, pub hud_color_alert: Color, pub hud_color_subtitles: Color, pub hud_color_choices: Color, pub hud_color_speedometer: Color, pub chat_speed: f32, pub flashlight_active: bool, pub hud_active: bool, pub map_active: bool, pub is_zooming: bool, pub third_person: bool, pub rotation_stabilizer_active: bool, pub shadows_sun: bool, pub shadows_pointlights: bool, pub shadowmap_resolution: usize, pub large_moons: bool, pub key_selectobject: MouseButton, pub key_zoom: MouseButton, pub key_map: KeyCode, pub key_map_zoom_out: KeyCode, pub key_map_zoom_in: KeyCode, //pub key_map_zoom_out_wheel: MouseButton, //pub key_map_zoom_in_wheel: MouseButton, pub key_togglehud: KeyCode, pub key_exit: KeyCode, pub key_restart: KeyCode, pub key_fullscreen: KeyCode, pub key_help: KeyCode, pub key_forward: KeyCode, pub key_back: KeyCode, pub key_left: KeyCode, pub key_right: KeyCode, pub key_up: KeyCode, pub key_down: KeyCode, pub key_run: KeyCode, pub key_stop: KeyCode, pub key_interact: KeyCode, pub key_vehicle: KeyCode, pub key_camera: KeyCode, pub key_flashlight: KeyCode, pub key_shadows: KeyCode, pub key_rotate: KeyCode, pub key_rotation_stabilizer: KeyCode, pub key_mouseup: KeyCode, pub key_mousedown: KeyCode, pub key_mouseleft: KeyCode, pub key_mouseright: KeyCode, pub key_rotateleft: KeyCode, pub key_rotateright: KeyCode, pub key_toggle_sfx: KeyCode, pub key_toggle_music: KeyCode, pub key_reply1: KeyCode, pub key_reply2: KeyCode, pub key_reply3: KeyCode, pub key_reply4: KeyCode, pub key_reply5: KeyCode, pub key_reply6: KeyCode, pub key_reply7: KeyCode, pub key_reply8: KeyCode, pub key_reply9: KeyCode, pub key_reply10: KeyCode, pub key_cheat_god_mode: KeyCode, pub key_cheat_stop: KeyCode, pub key_cheat_speed: KeyCode, pub key_cheat_speed_backward: KeyCode, pub key_cheat_teleport: KeyCode, pub key_cheat_pizza: KeyCode, pub key_cheat_farview1: KeyCode, pub key_cheat_farview2: KeyCode, pub key_cheat_adrenaline_zero: KeyCode, pub key_cheat_adrenaline_mid: KeyCode, pub key_cheat_adrenaline_max: KeyCode, pub key_cheat_die: KeyCode, } impl Default for Settings { fn default() -> Self { let dev_mode = cfg!(feature = "dev_mode") && env::var("CARGO").is_ok(); let default_mute_sfx = false; let default_mute_music = dev_mode; let version = if let Some(version) = option_env!("CARGO_PKG_VERSION") { version.to_string() } else { "13.37".to_string() }; Settings { dev_mode, god_mode: false, version, mute_sfx: default_mute_sfx, mute_music: default_mute_music, volume_sfx: 100, volume_music: 100, mouse_sensitivity: 0.4, fov: 50.0, fov_highspeed: 25.0, zoom_fov: 15.0, zoom_sensitivity_factor: 0.25, font_size_hud: 24.0, font_size_conversations: 32.0, font_size_choices: 28.0, font_size_console: 20.0, font_size_speedometer: 34.0, hud_color: Color::hex("#BE1251").unwrap(), hud_color_console: Color::hex("#BE1251").unwrap(), hud_color_console_warn: Color::hex("#CCCCCC").unwrap(), hud_color_console_system: Color::hex("#7F7F7F").unwrap(), hud_color_alert: Color::hex("#CCCCCC").unwrap(), hud_color_subtitles: Color::hex("#CCCCCC").unwrap(), hud_color_choices: Color::hex("#727272").unwrap(), hud_color_speedometer: Color::hex("#BE1251").unwrap(), chat_speed: DEFAULT_CHAT_SPEED * if dev_mode { 2.5 } else { 1.0 }, flashlight_active: false, hud_active: true, map_active: false, is_zooming: false, third_person: true, rotation_stabilizer_active: true, shadows_sun: true, shadows_pointlights: false, shadowmap_resolution: 2048, large_moons: false, key_selectobject: MouseButton::Left, key_zoom: MouseButton::Right, key_map: KeyCode::KeyM, key_map_zoom_out: KeyCode::ShiftLeft, key_map_zoom_in: KeyCode::ControlLeft, //key_map_zoom_out_wheel: KeyCode::Shift, //key_map_zoom_in_wheel: KeyCode::Shift, key_togglehud: KeyCode::Tab, key_exit: KeyCode::Escape, key_restart: KeyCode::F7, key_fullscreen: KeyCode::F11, key_help: KeyCode::F1, key_forward: KeyCode::KeyW, key_back: KeyCode::KeyS, key_left: KeyCode::KeyA, key_right: KeyCode::KeyD, key_up: KeyCode::ShiftLeft, key_down: KeyCode::ControlLeft, key_run: KeyCode::KeyR, key_stop: KeyCode::Space, key_interact: KeyCode::KeyE, key_vehicle: KeyCode::KeyQ, key_camera: KeyCode::KeyH, key_flashlight: KeyCode::KeyF, key_shadows: KeyCode::F2, key_rotate: KeyCode::KeyR, key_rotation_stabilizer: KeyCode::KeyY, key_mouseup: KeyCode::KeyI, key_mousedown: KeyCode::KeyK, key_mouseleft: KeyCode::KeyJ, key_mouseright: KeyCode::KeyL, key_rotateleft: KeyCode::KeyU, key_rotateright: KeyCode::KeyO, key_toggle_sfx: KeyCode::F3, key_toggle_music: KeyCode::F4, key_reply1: KeyCode::Digit1, key_reply2: KeyCode::Digit2, key_reply3: KeyCode::Digit3, key_reply4: KeyCode::Digit4, key_reply5: KeyCode::Digit5, key_reply6: KeyCode::Digit6, key_reply7: KeyCode::Digit7, key_reply8: KeyCode::Digit8, key_reply9: KeyCode::Digit9, key_reply10: KeyCode::Digit0, key_cheat_god_mode: KeyCode::KeyG, key_cheat_stop: KeyCode::KeyC, key_cheat_speed: KeyCode::KeyV, key_cheat_speed_backward: KeyCode::KeyB, key_cheat_teleport: KeyCode::KeyX, key_cheat_pizza: KeyCode::F9, key_cheat_farview1: KeyCode::F10, key_cheat_farview2: KeyCode::F12, key_cheat_adrenaline_zero: KeyCode::F5, key_cheat_adrenaline_mid: KeyCode::F6, key_cheat_adrenaline_max: KeyCode::F8, key_cheat_die: KeyCode::KeyZ, } } } impl Settings { #[allow(dead_code)] pub fn reset(&mut self) { println!("Resetting settings!"); *self = Self::default(); } pub fn reset_player_settings(&mut self) { println!("Resetting player settings!"); let default = Self::default(); self.rotation_stabilizer_active = default.rotation_stabilizer_active; self.third_person = default.third_person; self.is_zooming = default.is_zooming; self.flashlight_active = default.flashlight_active; } pub fn get_reply_keys(&self) -> [KeyCode; 10] { return [ self.key_reply1, self.key_reply2, self.key_reply3, self.key_reply4, self.key_reply5, self.key_reply6, self.key_reply7, self.key_reply8, self.key_reply9, self.key_reply10, ]; } } #[derive(Resource, Deserialize, Debug, Default)] #[serde(default)] pub struct Preferences { pub fullscreen_mode: String, pub window_mode: String, pub render_mode: String, #[serde(skip)] pub source_file: Option, } impl Preferences { pub fn get_fullscreen_mode(&self) -> WindowMode { match self.fullscreen_mode.as_str() { "legacy" => WindowMode::Fullscreen, "sized" => WindowMode::SizedFullscreen, _ => WindowMode::BorderlessFullscreen, } } pub fn get_window_mode(&self) -> WindowMode { match self.window_mode.as_str() { "fullscreen" => self.get_fullscreen_mode(), _ => WindowMode::Windowed, } } pub fn render_mode_is_gl(&self) -> bool { return self.render_mode == "gl"; } } fn file_is_readable(file_path: &str) -> bool { fs::metadata(file_path).map(|metadata| metadata.is_file()).unwrap_or(false) } fn get_prefs_path() -> Option { let test = "outfly.toml"; if file_is_readable(test) { return Some(test.to_string()); } if let Ok(basedir) = env::var("XDG_CONFIG_HOME") { let test = basedir.to_string() + "/outfly/outfly.toml"; if file_is_readable(test.as_str()) { return Some(test); } } else if let Ok(basedir) = env::var("HOME") { let test = basedir.to_string() + ".config/outfly/outfly.toml"; if file_is_readable(test.as_str()) { return Some(test); } } return None; } pub fn load_prefs() -> Preferences { let (toml, path) = match get_prefs_path() { Some(path) => { let toml = fs::read_to_string(&path); match toml { Ok(toml) => (toml, Some(path)), Err(error) => { error!("Failed to open preferences file '{path}': {error}"); return Preferences::default(); } } } None => { warn!("Found no preference file, using default preferences."); (include_str!("data/outfly.toml").to_string(), None) } }; match toml.parse::() { Ok(doc) => { match toml_edit::de::from_document::(doc) { Ok(mut pref) => { if let Some(path) = &path { info!("Loaded preference file from {path}"); } else { info!("Loaded preferences from internal defaults"); } pref.source_file = path; dbg!(&pref); return pref; } Err(error) => { error!("Failed to read preference line: {error}"); return Preferences::default(); } } } Err(error) => { error!("Failed to open preferences: {error}"); return Preferences::default(); } } } #[derive(Resource)] pub struct GameVars { pub db: HashMap, } impl Default for GameVars { fn default() -> Self { Self { db: HashMap::new(), } } } impl GameVars { #[allow(dead_code)] pub fn get(&self, key: &str) -> Option { if let Some(value) = self.db.get(key) { return Some(value.clone()); } return None; } #[allow(dead_code)] pub fn getf(&self, key: &str) -> Option { if let Some(value) = self.db.get(key) { if let Ok(float) = value.parse::() { return Some(float); } } return None; } pub fn getb(&self, key: &str) -> bool { if let Some(value) = self.db.get(key) { return Self::evaluate_str_as_bool(value); } return false; } pub fn evaluate_str_as_bool(string: &str) -> bool { return string != "0"; } // This method ensures that the variable name contains a scope separator, // and if a scope is missing, it prefixes the fallback scope. // Should NOT be used on non-variable values, like plain strings. // // Some examples, assuming fallback_scope="Clippy", SCOPE_SEPARATOR="$": // // "" -> "clippy$" // "foo" -> "clippy$foo" // "FOO" -> "clippy$foo" // "$foo" -> "clippy$foo" // "$$foo" -> "$$foo" // "PizzaClippy$foo" -> "pizzaclippy$foo" (unchanged) // "$foo$foo$foo$foo" -> "$foo$foo$foo$foo" (unchanged) pub fn normalize_varname(fallback_scope: &str, key: &str) -> String { let parts: Vec<&str> = key.split(SCOPE_SEPARATOR).collect(); let key: String = if parts.len() == 1 { // we got a key like "foo", turn it into "$foo" fallback_scope.to_string() + SCOPE_SEPARATOR + key } else if parts.len() > 1 { // we got a key with at least one "$" // extract anything before the last "$": let scope_part: String = parts[0..parts.len() - 2].join(SCOPE_SEPARATOR); if scope_part.is_empty() { // we got a key like "$foo", just prefix the fallback scope fallback_scope.to_string() + key } else { // we got a key like "Ke$ha$foo" or "$$foo" (which is the convention for // global variables), leave the scope intact key.to_string() } } else { // we got an empty string. this is bad, but handle gracefully fallback_scope.to_string() + SCOPE_SEPARATOR }; return key.to_lowercase(); } pub fn set_in_scope(&mut self, fallback_scope: &str, key: &str, value: String) { let key = Self::normalize_varname(fallback_scope, key); self.db.insert(key, value); } pub fn evaluate_condition(&self, condition: &str, scope: &str) -> bool { let parts: Vec<&str> = condition.split(" ").collect(); if parts.len() == 0 { // Got an empty string, this is always false. return false; } else if parts.len() == 1 { // Got something like "if $somevar:". // Check whether the variable evaluates to true. let part = parts[0]; let (part, negate) = if part.starts_with(TOKEN_NEGATE) { (&part[1..], true) } else { (part, false) }; if part.contains(SCOPE_SEPARATOR) { let part = Self::normalize_varname(scope, part); let value_bool = self.getb(part.as_str()); return value_bool ^ negate; } else { return Self::evaluate_str_as_bool(part) ^ negate; } } else if parts.len() == 2 { // Got something like "if $something somethingelse" // Check whether the two are identical. let mut left: String = parts[0].to_string(); if left.contains(SCOPE_SEPARATOR) { left = self .get(Self::normalize_varname(scope, left.as_str()).as_str()) .unwrap_or("".to_string()); } let mut right: String = parts[1].to_string(); if right.contains(SCOPE_SEPARATOR) { right = self .get(Self::normalize_varname(scope, right.as_str()).as_str()) .unwrap_or("".to_string()); } return left == right; } else { // Got something like "if $something != somethingelse bla bla" let mut left: String = parts[0].to_string(); if left.contains(SCOPE_SEPARATOR) { left = self .get(Self::normalize_varname(scope, left.as_str()).as_str()) .unwrap_or("".to_string()); } let mut right: String = parts[2..parts.len()].join(" ").to_string(); if right.contains(SCOPE_SEPARATOR) { right = self .get(Self::normalize_varname(scope, right.as_str()).as_str()) .unwrap_or("".to_string()); } let floats = (left.parse::(), right.parse::()); let operator: &str = parts[1]; match operator { TOKEN_EQUALS => { if let (Ok(left), Ok(right)) = floats { return left == right; } return left == right; } TOKEN_EQUALS_NOT => { if let (Ok(left), Ok(right)) = floats { return left != right; } return left != right; } TOKEN_GREATER_THAN => { if let (Ok(left), Ok(right)) = floats { return left > right; } return false; } TOKEN_GREATER_EQUALS => { if let (Ok(left), Ok(right)) = floats { return left >= right; } return false; } TOKEN_LESS_THAN => { if let (Ok(left), Ok(right)) = floats { return left < right; } return false; } TOKEN_LESS_EQUALS => { if let (Ok(left), Ok(right)) = floats { return left <= right; } return false; } _ => { error!("Unknown operator '{operator}' in if-condition!"); return false; } } } } }