856 lines
30 KiB
Rust
856 lines
30 KiB
Rust
// ▄████████▄ + ███ + ▄█████████ ███ +
|
|
// ███▀ ▀███ + + ███ ███▀ + ███ + +
|
|
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
|
|
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
|
|
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
|
|
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
|
|
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
|
|
// + + + ███
|
|
// + ▀████████████████████████████████████████████████████▀
|
|
//
|
|
// This module manages variables, settings, as well as evaluating
|
|
// "if"-conditions in chats.
|
|
|
|
use crate::prelude::*;
|
|
use bevy::prelude::*;
|
|
use bevy::window::WindowMode;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::env;
|
|
use std::fs;
|
|
use toml_edit::DocumentMut;
|
|
|
|
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;
|
|
pub const DEFAULT_CONFIG_TOML: &str = include_str!("data/outfly.toml");
|
|
|
|
#[derive(Resource)]
|
|
pub struct Settings {
|
|
pub dev_mode: bool,
|
|
pub god_mode: bool,
|
|
pub version: String,
|
|
pub alive: bool,
|
|
pub window_focused: bool,
|
|
pub mute_sfx: bool,
|
|
pub noise_cancellation_mode: usize,
|
|
pub noise_cancellation_modes: Vec<(String, f32)>,
|
|
pub radio_mode: usize,
|
|
pub radio_modes: Vec<String>, // see also: settings.is_radio_playing()
|
|
pub volume_sfx: f32,
|
|
pub volume_music: f32,
|
|
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_fps: f32,
|
|
pub font_size_conversations: f32,
|
|
pub font_size_choices: f32,
|
|
pub font_size_console: f32,
|
|
pub font_size_speedometer: f32,
|
|
pub font_size_deathtext: f32,
|
|
pub font_size_deathsubtext: f32,
|
|
pub font_size_deathpoem: f32,
|
|
pub font_size_death_achievements: f32,
|
|
pub font_size_achievement: f32,
|
|
pub font_size_achievement_header: f32,
|
|
pub font_size_keybindings: f32,
|
|
pub font_size_version: f32,
|
|
pub hud_color: Color,
|
|
pub hud_color_fps: Color,
|
|
pub hud_color_console: Color,
|
|
pub hud_color_console_send: Color,
|
|
pub hud_color_console_warn: Color,
|
|
pub hud_color_console_system: Color,
|
|
pub hud_color_console_achievement: Color,
|
|
pub hud_color_alert: Color,
|
|
pub hud_color_subtitles: Color,
|
|
pub hud_color_choices: Color,
|
|
pub hud_color_speedometer: Color,
|
|
pub hud_color_deathpoem: Color,
|
|
pub hud_color_achievement: Color,
|
|
pub hud_color_achievement_header: Color,
|
|
pub hud_color_achievement_accomplished: Color,
|
|
pub hud_color_phonebook_locked: Color,
|
|
pub hud_color_phonebook_unlocked: Color,
|
|
pub hud_color_death: Color,
|
|
pub hud_color_death_achievements: Color,
|
|
pub hud_color_keybindings: Color,
|
|
pub hud_color_version: Color,
|
|
pub chat_speed: f32,
|
|
pub ar_avatar: usize,
|
|
pub flashlight_active: bool,
|
|
pub reactor_state: usize,
|
|
pub hud_active: bool,
|
|
pub map_active: bool,
|
|
pub deathscreen_active: bool,
|
|
pub menu_active: bool,
|
|
pub death_cause: String,
|
|
pub is_zooming: bool,
|
|
pub third_person: bool,
|
|
pub rotation_stabilizer_active: bool,
|
|
pub cruise_control_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_menu: 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_cruise_control: 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_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 version = if let Some(version) = option_env!("CARGO_PKG_VERSION") {
|
|
version.to_string()
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
Settings {
|
|
dev_mode,
|
|
god_mode: false,
|
|
version,
|
|
alive: true,
|
|
window_focused: true,
|
|
mute_sfx: false,
|
|
noise_cancellation_mode: 0,
|
|
noise_cancellation_modes: vec![
|
|
("Off".to_string(), 1.0),
|
|
("33%".to_string(), 0.66),
|
|
("66%".to_string(), 0.33),
|
|
("100%".to_string(), 0.0),
|
|
],
|
|
radio_mode: 1,
|
|
radio_modes: vec![
|
|
// see also: settings.is_radio_playing()
|
|
"Off".to_string(),
|
|
"Space Wave Radio".to_string(),
|
|
"Amplify outside recordings".to_string(),
|
|
],
|
|
volume_sfx: 1.0,
|
|
volume_music: 1.0,
|
|
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_fps: 14.0,
|
|
font_size_conversations: 32.0,
|
|
font_size_choices: 28.0,
|
|
font_size_console: 20.0,
|
|
font_size_speedometer: 34.0,
|
|
font_size_deathtext: 64.0,
|
|
font_size_deathsubtext: 32.0,
|
|
font_size_deathpoem: 18.0,
|
|
font_size_death_achievements: 24.0,
|
|
font_size_achievement: 24.0,
|
|
font_size_achievement_header: 32.0,
|
|
font_size_keybindings: 20.0,
|
|
font_size_version: 20.0,
|
|
hud_color: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
|
|
hud_color_fps: Srgba::hex("#181818").unwrap().into(),
|
|
hud_color_console: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
|
|
hud_color_console_send: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
|
|
hud_color_console_achievement: Srgba::hex(COLOR_SUCCESS).unwrap().into(),
|
|
hud_color_console_warn: Srgba::hex(COLOR_WARNING).unwrap().into(),
|
|
hud_color_console_system: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
|
|
hud_color_alert: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
|
|
hud_color_subtitles: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
|
|
hud_color_choices: Srgba::hex(COLOR_BODY).unwrap().into(),
|
|
hud_color_speedometer: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
|
|
hud_color_deathpoem: Srgba::hex("#CC2200").unwrap().into(),
|
|
hud_color_achievement: Srgba::hex(COLOR_DIM).unwrap().into(),
|
|
hud_color_achievement_accomplished: Srgba::hex(COLOR_SUCCESS).unwrap().into(),
|
|
hud_color_achievement_header: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
|
|
hud_color_phonebook_locked: Srgba::hex(COLOR_DIM).unwrap().into(),
|
|
hud_color_phonebook_unlocked: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
|
|
hud_color_death: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
|
|
hud_color_death_achievements: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
|
|
hud_color_keybindings: Srgba::hex(COLOR_DIM).unwrap().into(),
|
|
hud_color_version: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
|
|
chat_speed: DEFAULT_CHAT_SPEED,
|
|
ar_avatar: 0,
|
|
flashlight_active: false,
|
|
reactor_state: 1,
|
|
hud_active: true,
|
|
map_active: false,
|
|
deathscreen_active: false,
|
|
menu_active: false,
|
|
death_cause: "Unknown".to_string(),
|
|
is_zooming: false,
|
|
third_person: true,
|
|
rotation_stabilizer_active: true,
|
|
cruise_control_active: false,
|
|
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_menu: KeyCode::Escape,
|
|
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::KeyC,
|
|
key_flashlight: KeyCode::KeyF,
|
|
key_cruise_control: KeyCode::KeyT,
|
|
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_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::KeyZ,
|
|
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::F4,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Settings {
|
|
#[allow(dead_code)]
|
|
pub fn reset(&mut self) {
|
|
println!("Resetting settings!");
|
|
*self = Self::default();
|
|
}
|
|
|
|
pub fn is_game_running(&self) -> bool {
|
|
!self.menu_active && self.window_focused
|
|
}
|
|
|
|
pub fn reset_player_settings(&mut self) {
|
|
println!("Resetting player settings!");
|
|
let default = Self::default();
|
|
self.rotation_stabilizer_active = default.rotation_stabilizer_active;
|
|
self.is_zooming = default.is_zooming;
|
|
self.flashlight_active = default.flashlight_active;
|
|
self.cruise_control_active = default.cruise_control_active;
|
|
self.map_active = default.map_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,
|
|
];
|
|
}
|
|
|
|
pub fn in_control(&self) -> bool {
|
|
return self.alive && !self.menu_active;
|
|
}
|
|
|
|
pub fn is_radio_playing(&self, sfx: audio::Sfx) -> Option<bool> {
|
|
let radio = self.radio_mode;
|
|
match sfx {
|
|
audio::Sfx::BGMTakeoff => Some(radio == 1),
|
|
audio::Sfx::BGMActualJupiterRecording => Some(radio == 2),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn set_noise_cancellation_mode(&mut self, value: usize) {
|
|
let value = if value >= self.noise_cancellation_modes.len() {
|
|
warn!("Attempting to set too large noise cancellation mode: {value}");
|
|
0
|
|
} else {
|
|
value
|
|
};
|
|
self.noise_cancellation_mode = value;
|
|
self.volume_sfx = if let Some(noisecancel) = self
|
|
.noise_cancellation_modes
|
|
.get(self.noise_cancellation_mode)
|
|
{
|
|
noisecancel.1
|
|
} else {
|
|
self.noise_cancellation_modes[0].1
|
|
};
|
|
self.mute_sfx = value >= 3;
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Default, Debug)]
|
|
pub struct AchievementTracker {
|
|
pub repair_suit: bool,
|
|
pub drink_a_pizza: bool,
|
|
pub in_jupiters_shadow: bool,
|
|
pub find_earth: bool,
|
|
|
|
pub ride_every_vehicle: bool,
|
|
pub vehicles_ridden: HashSet<String>,
|
|
pub all_vehicles: HashSet<String>,
|
|
|
|
pub talk_to_everyone: bool,
|
|
pub people_talked_to: HashSet<String>,
|
|
pub all_people: HashSet<String>,
|
|
}
|
|
|
|
impl AchievementTracker {
|
|
pub fn to_bool_vec(&self) -> Vec<bool> {
|
|
vec![
|
|
self.repair_suit,
|
|
self.drink_a_pizza,
|
|
self.ride_every_vehicle,
|
|
self.talk_to_everyone,
|
|
self.find_earth,
|
|
self.in_jupiters_shadow,
|
|
]
|
|
}
|
|
pub fn achieve_all(&mut self) {
|
|
self.repair_suit = true;
|
|
self.drink_a_pizza = true;
|
|
self.ride_every_vehicle = true;
|
|
self.talk_to_everyone = true;
|
|
self.find_earth = true;
|
|
self.in_jupiters_shadow = true;
|
|
}
|
|
pub fn to_textsections(&self) -> Vec<String> {
|
|
fn collectible(current: usize, total: usize) -> String {
|
|
if current < total {
|
|
format!(" ({}/{})", current, total)
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
}
|
|
let ride = collectible(self.vehicles_ridden.len(), self.all_vehicles.len());
|
|
let talk = collectible(self.people_talked_to.len(), self.all_people.len());
|
|
vec![
|
|
"Repair Your Suit\n".to_string(),
|
|
"Enjoy A Pizza\n".to_string(),
|
|
format!("Ride Every Vehicle{ride}\n"),
|
|
format!("Talk To Everyone{talk}\n"),
|
|
"Find Earth\n".to_string(),
|
|
"Enter Jupiter's Shadow\n".to_string(),
|
|
]
|
|
}
|
|
pub fn to_overview(&self) -> Vec<(bool, String)> {
|
|
vec![
|
|
(self.repair_suit, "repair your suit".into()),
|
|
(self.drink_a_pizza, "enjoy a pizza".into()),
|
|
(self.ride_every_vehicle, "ride every vehicle".into()),
|
|
(self.talk_to_everyone, "talk to everyone".into()),
|
|
(self.find_earth, "find Earth".into()),
|
|
(self.in_jupiters_shadow, "enter Jupiter's shadow".into()),
|
|
]
|
|
}
|
|
pub fn to_summary(&self) -> String {
|
|
let list = self.to_overview();
|
|
let count = list.iter().filter(|(achieved, _)| *achieved).count();
|
|
if count == 0 {
|
|
return "".to_string();
|
|
}
|
|
let mut summary = "\n\n\nYou managed to ".to_string();
|
|
for (i, (_, text)) in list.iter().filter(|(achieved, _)| *achieved).enumerate() {
|
|
summary += text.as_str();
|
|
if i + 2 == count {
|
|
summary += ", and ";
|
|
} else if i + 1 == count {
|
|
summary += " before you perished.";
|
|
if count == list.len() {
|
|
summary += "\nA truly astounding achievement, a glimmer in the void, before it all fades, into nothingness.";
|
|
}
|
|
} else {
|
|
summary += ", ";
|
|
}
|
|
}
|
|
summary
|
|
}
|
|
}
|
|
|
|
// Used for settings that are preserved across restarts
|
|
#[derive(Resource, Serialize, Deserialize, Debug, Default)]
|
|
#[serde(default)]
|
|
pub struct Preferences {
|
|
pub fullscreen_mode: String,
|
|
pub fullscreen_on: bool,
|
|
pub render_mode: String,
|
|
pub augmented_reality: bool,
|
|
pub radio_station: usize,
|
|
pub noise_cancellation_mode: usize,
|
|
pub third_person: bool,
|
|
pub shadows_sun: bool,
|
|
pub avatar: usize,
|
|
pub pointer: hud::Pointer,
|
|
#[serde(default = "Preferences::default_light_amp")]
|
|
pub light_amp: usize, // 0-3
|
|
#[serde(default = "Preferences::default_flashlight_power")]
|
|
pub flashlight_power: usize, // 0-2
|
|
pub thruster_boost: usize, // 0-2
|
|
pub contacts: Vec<String>,
|
|
pub luna_backup: bool,
|
|
|
|
#[serde(skip)]
|
|
pub source_file: Option<String>,
|
|
}
|
|
|
|
impl Preferences {
|
|
pub fn default_light_amp() -> usize {
|
|
1
|
|
}
|
|
pub fn default_flashlight_power() -> usize {
|
|
2
|
|
}
|
|
|
|
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.fullscreen_on {
|
|
true => self.get_fullscreen_mode(),
|
|
false => WindowMode::Windowed,
|
|
}
|
|
}
|
|
pub fn render_mode_is_gl(&self) -> bool {
|
|
return self.render_mode == "gl";
|
|
}
|
|
|
|
pub fn save(&self) {
|
|
if let Some(path) = get_prefs_path() {
|
|
match toml_edit::ser::to_document::<Preferences>(self) {
|
|
Ok(doc) => match fs::write(path.clone(), doc.to_string()) {
|
|
Ok(_) => {
|
|
info!("Saved preferences to {path}.");
|
|
}
|
|
Err(error) => {
|
|
error!("Error while writing preferences: {:?}", error);
|
|
}
|
|
},
|
|
Err(error) => {
|
|
error!("Error while writing preferences: {:?}", error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn file_is_readable(file_path: &str) -> bool {
|
|
fs::metadata(file_path)
|
|
.map(|metadata| metadata.is_file())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn path_is_directory(file_path: &str) -> bool {
|
|
fs::metadata(file_path)
|
|
.map(|metadata| metadata.is_dir())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn get_prefs_path() -> Option<String> {
|
|
let test = CONF_FILE;
|
|
if file_is_readable(test) {
|
|
return Some(test.to_string());
|
|
}
|
|
if let Some(mut conf) = dirs::config_dir() {
|
|
conf.push("OutFly");
|
|
if !conf.exists() {
|
|
match fs::create_dir_all(&conf) {
|
|
Ok(_) => {}
|
|
Err(error) => {
|
|
eprintln!("Failed creating configuration directory: {error}");
|
|
}
|
|
}
|
|
}
|
|
if let Some(test) = conf.to_str() {
|
|
if !path_is_directory(test) {
|
|
eprintln!("Failed creating configuration directory");
|
|
return None;
|
|
}
|
|
}
|
|
|
|
conf.push(CONF_FILE);
|
|
if !conf.exists() {
|
|
match fs::write(&conf, DEFAULT_CONFIG_TOML.to_string()) {
|
|
Ok(_) => {}
|
|
Err(error) => {
|
|
eprintln!("Failed creating configuration file: {error}");
|
|
}
|
|
}
|
|
}
|
|
if let Some(test) = conf.to_str() {
|
|
if file_is_readable(test) {
|
|
return Some(test.to_string());
|
|
}
|
|
}
|
|
}
|
|
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) => {
|
|
eprintln!("Error: Failed to open preferences file '{path}': {error}");
|
|
return Preferences::default();
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
println!("Found no preference file, using default preferences.");
|
|
(DEFAULT_CONFIG_TOML.to_string(), None)
|
|
}
|
|
};
|
|
match toml.parse::<DocumentMut>() {
|
|
Ok(doc) => match toml_edit::de::from_document::<Preferences>(doc) {
|
|
Ok(mut prefs) => {
|
|
if let Some(path) = &path {
|
|
println!("Loaded preference file from {path}");
|
|
} else {
|
|
println!("Loaded preferences from internal defaults");
|
|
}
|
|
prefs.source_file = path;
|
|
prefs.flashlight_power = prefs.flashlight_power.clamp(0, 3);
|
|
prefs.light_amp = prefs.light_amp.clamp(0, 3);
|
|
prefs.thruster_boost = prefs.thruster_boost.clamp(0, 2);
|
|
prefs
|
|
}
|
|
Err(error) => {
|
|
eprintln!("Error: Failed to read preference line: {error}");
|
|
Preferences::default()
|
|
}
|
|
},
|
|
Err(error) => {
|
|
eprintln!("Error: Failed to open preferences: {error}");
|
|
Preferences::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, Debug)]
|
|
pub struct GameVars {
|
|
pub db: HashMap<String, String>,
|
|
}
|
|
|
|
impl Default for GameVars {
|
|
fn default() -> Self {
|
|
Self { db: HashMap::new() }
|
|
}
|
|
}
|
|
|
|
impl GameVars {
|
|
pub fn reset(&mut self) {
|
|
self.db.clear();
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn get(&self, key: &str) -> Option<String> {
|
|
if let Some(value) = self.db.get(key) {
|
|
return Some(value.clone());
|
|
}
|
|
return None;
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn getf(&self, key: &str) -> Option<f64> {
|
|
if let Some(value) = self.db.get(key) {
|
|
if let Ok(float) = value.parse::<f64>() {
|
|
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.
|
|
//
|
|
// See test_normalize_varname() for examples.
|
|
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 "<scope>$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() - 1].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) {
|
|
let key = Self::normalize_varname(scope, left.as_str());
|
|
let value = self.get(key.as_str());
|
|
left = if let Some(value) = value {
|
|
value
|
|
} else {
|
|
warn!("Couldn't find variable `{key}` on left hand side of a condition");
|
|
"".to_string()
|
|
};
|
|
}
|
|
let mut right: String = parts[1].to_string();
|
|
if right.contains(SCOPE_SEPARATOR) {
|
|
let key = Self::normalize_varname(scope, right.as_str());
|
|
let value = self.get(key.as_str());
|
|
right = if let Some(value) = value {
|
|
value
|
|
} else {
|
|
warn!("Couldn't find variable `{key}` on right hand side of a condition");
|
|
"".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) {
|
|
let key = Self::normalize_varname(scope, left.as_str());
|
|
let value = self.get(key.as_str());
|
|
left = if let Some(value) = value {
|
|
value
|
|
} else {
|
|
warn!("Couldn't find variable `{key}` on left hand side of a condition");
|
|
"".to_string()
|
|
};
|
|
}
|
|
|
|
let mut right: String = parts[2..parts.len()].join(" ").to_string();
|
|
if right.contains(SCOPE_SEPARATOR) {
|
|
let key = Self::normalize_varname(scope, right.as_str());
|
|
let value = self.get(key.as_str());
|
|
right = if let Some(value) = value {
|
|
value
|
|
} else {
|
|
warn!("Couldn't find variable `{key}` on right hand side of a condition");
|
|
"".to_string()
|
|
};
|
|
}
|
|
let floats = (left.parse::<f64>(), right.parse::<f64>());
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize_varname() {
|
|
assert_eq!(GameVars::normalize_varname("Clippy", ""), "clippy$");
|
|
assert_eq!(GameVars::normalize_varname("Clippy", "foo"), "clippy$foo");
|
|
assert_eq!(GameVars::normalize_varname("Clippy", "FOO"), "clippy$foo");
|
|
assert_eq!(GameVars::normalize_varname("Clippy", "$foo"), "clippy$foo");
|
|
assert_eq!(GameVars::normalize_varname("Clippy", "$$foo"), "$$foo");
|
|
assert_eq!(
|
|
GameVars::normalize_varname("Clippy", "PizzaClippy$foo"),
|
|
"pizzaclippy$foo"
|
|
);
|
|
assert_eq!(
|
|
GameVars::normalize_varname("Clippy", "$foo$foo$foo$foo"),
|
|
"$foo$foo$foo$foo"
|
|
);
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct CommandLineOptions {
|
|
pub window_mode_fullscreen: WindowMode,
|
|
pub window_mode_initial: WindowMode,
|
|
pub use_gl: bool,
|
|
}
|