veloren_voxygen/
profile.rs

1use crate::hud;
2use common::{character::CharacterId, uuid::Uuid};
3use hashbrown::HashMap;
4use serde::{Deserialize, Serialize};
5use std::{
6    fs,
7    io::Write,
8    path::{Path, PathBuf},
9};
10use tracing::warn;
11
12/// Represents a character in the profile.
13#[derive(Clone, Debug, Serialize, Deserialize)]
14#[serde(default)]
15pub struct CharacterProfile {
16    /// Array representing a character's hotbar.
17    pub hotbar_slots: [Option<hud::HotbarSlotContents>; 10],
18}
19
20const fn default_slots() -> [Option<hud::HotbarSlotContents>; 10] {
21    [None, None, None, None, None, None, None, None, None, None]
22}
23
24impl Default for CharacterProfile {
25    fn default() -> Self {
26        CharacterProfile {
27            hotbar_slots: default_slots(),
28        }
29    }
30}
31
32/// Represents a server in the profile.
33#[derive(Clone, Debug, Serialize, Deserialize)]
34#[serde(default)]
35pub struct ServerProfile {
36    /// A map of character's by id to their CharacterProfile.
37    pub characters: HashMap<CharacterId, CharacterProfile>,
38    /// Selected character in the chararacter selection screen
39    pub selected_character: Option<CharacterId>,
40    /// Last spectate position
41    pub spectate_position: Option<vek::Vec3<f32>>,
42    /// Hash of left-accepted server rules
43    pub accepted_rules: Option<u64>,
44}
45
46impl Default for ServerProfile {
47    fn default() -> Self {
48        ServerProfile {
49            characters: HashMap::new(),
50            selected_character: None,
51            spectate_position: None,
52            accepted_rules: None,
53        }
54    }
55}
56
57/// `Profile` contains everything that can be configured in the profile.ron
58///
59/// Initially it is just for persisting things that don't belong in
60/// settings.ron - like the state of hotbar and any other character level
61/// configuration.
62#[derive(Default, Clone, Debug, Serialize, Deserialize)]
63#[serde(default)]
64pub struct Profile {
65    pub servers: HashMap<String, ServerProfile>,
66    pub mutelist: HashMap<Uuid, String>,
67    /// Temporary character profile, used when it should
68    /// not be persisted to the disk.
69    #[serde(skip)]
70    pub transient_character: Option<CharacterProfile>,
71}
72
73impl Profile {
74    /// Load the profile.ron file from the standard path or create it.
75    pub fn load(config_dir: &Path) -> Self {
76        let path = Profile::get_path(config_dir);
77
78        if let Ok(file) = fs::File::open(&path) {
79            match ron::de::from_reader(file) {
80                Ok(profile) => return profile,
81                Err(e) => {
82                    warn!(
83                        ?e,
84                        ?path,
85                        "Failed to parse profile file! Falling back to default."
86                    );
87                    // Rename the corrupted profile file.
88                    let new_path = path.with_extension("invalid.ron");
89                    if let Err(e) = fs::rename(path.clone(), new_path.clone()) {
90                        warn!(?e, ?path, ?new_path, "Failed to rename profile file.");
91                    }
92                },
93            }
94        }
95        // This is reached if either:
96        // - The file can't be opened (presumably it doesn't exist)
97        // - Or there was an error parsing the file
98        let default_profile = Self::default();
99        default_profile.save_to_file_warn(config_dir);
100        default_profile
101    }
102
103    /// Save the current profile to disk, warn on failure.
104    pub fn save_to_file_warn(&self, config_dir: &Path) {
105        if let Err(e) = self.save_to_file(config_dir) {
106            warn!(?e, "Failed to save profile");
107        }
108    }
109
110    /// Get the hotbar_slots for the requested character_id.
111    ///
112    /// If the server or character does not exist then the default hotbar_slots
113    /// (empty) is returned.
114    ///
115    /// # Arguments
116    ///
117    /// * server - current server the character is on.
118    /// * character_id - id of the character, passing `None` indicates the
119    ///   transient character profile should be used.
120    pub fn get_hotbar_slots(
121        &self,
122        server: &str,
123        character_id: Option<CharacterId>,
124    ) -> [Option<hud::HotbarSlotContents>; 10] {
125        match character_id {
126            Some(character_id) => self
127                .servers
128                .get(server)
129                .and_then(|s| s.characters.get(&character_id)),
130            None => self.transient_character.as_ref(),
131        }
132        .map(|c| c.hotbar_slots.clone())
133        .unwrap_or_else(default_slots)
134    }
135
136    /// Set the hotbar_slots for the requested character_id.
137    ///
138    /// If the server or character does not exist then the appropriate fields
139    /// will be initialised and the slots added.
140    ///
141    /// # Arguments
142    ///
143    /// * server - current server the character is on.
144    /// * character_id - id of the character, passing `None` indicates the
145    ///   transient character profile should be used.
146    /// * slots - array of hotbar_slots to save.
147    pub fn set_hotbar_slots(
148        &mut self,
149        server: &str,
150        character_id: Option<CharacterId>,
151        slots: [Option<hud::HotbarSlotContents>; 10],
152    ) {
153        match character_id {
154            Some(character_id) => self.servers
155              .entry(server.to_string())
156              .or_default()
157              // Get or update the CharacterProfile.
158              .characters
159              .entry(character_id)
160              .or_default(),
161            None => self.transient_character.get_or_insert_default(),
162        }
163        .hotbar_slots = slots;
164    }
165
166    /// Get the selected_character for the provided server.
167    ///
168    /// if the server does not exist then the default selected_character (None)
169    /// is returned.
170    ///
171    /// # Arguments
172    ///
173    /// * server - current server the character is on.
174    pub fn get_selected_character(&self, server: &str) -> Option<CharacterId> {
175        self.servers
176            .get(server)
177            .map(|s| s.selected_character)
178            .unwrap_or_default()
179    }
180
181    /// Set the selected_character for the provided server.
182    ///
183    /// If the server does not exist then the appropriate fields
184    /// will be initialised and the selected_character added.
185    ///
186    /// # Arguments
187    ///
188    /// * server - current server the character is on.
189    /// * selected_character - option containing selected character ID
190    pub fn set_selected_character(
191        &mut self,
192        server: &str,
193        selected_character: Option<CharacterId>,
194    ) {
195        self.servers
196            .entry(server.to_string())
197            .or_default()
198            .selected_character = selected_character;
199    }
200
201    /// Get the selected_character for the provided server.
202    ///
203    /// if the server does not exist then the default spectate_position (None)
204    /// is returned.
205    ///
206    /// # Arguments
207    ///
208    /// * server - current server the player is on.
209    pub fn get_spectate_position(&self, server: &str) -> Option<vek::Vec3<f32>> {
210        self.servers
211            .get(server)
212            .map(|s| s.spectate_position)
213            .unwrap_or_default()
214    }
215
216    /// Set the spectate_position for the provided server.
217    ///
218    /// If the server does not exist then the appropriate fields
219    /// will be initialised and the selected_character added.
220    ///
221    /// # Arguments
222    ///
223    /// * server - current server the player is on.
224    /// * spectate_position - option containing the position we're spectating
225    pub fn set_spectate_position(
226        &mut self,
227        server: &str,
228        spectate_position: Option<vek::Vec3<f32>>,
229    ) {
230        self.servers
231            .entry(server.to_string())
232            .or_default()
233            .spectate_position = spectate_position;
234    }
235
236    /// Save the current profile to disk.
237    fn save_to_file(&self, config_dir: &Path) -> std::io::Result<()> {
238        let path = Profile::get_path(config_dir);
239        if let Some(dir) = path.parent() {
240            fs::create_dir_all(dir)?;
241        }
242        let mut config_file = fs::File::create(path)?;
243
244        let s: &str = &ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()).unwrap();
245        config_file.write_all(s.as_bytes()).unwrap();
246        Ok(())
247    }
248
249    fn get_path(config_dir: &Path) -> PathBuf { config_dir.join("profile.ron") }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_get_slots_with_empty_profile() {
258        let profile = Profile::default();
259        let slots = profile.get_hotbar_slots("TestServer", Some(CharacterId(12345)));
260        assert_eq!(slots, [(); 10].map(|()| None))
261    }
262
263    #[test]
264    fn test_set_slots_with_empty_profile() {
265        let mut profile = Profile::default();
266        let slots = [(); 10].map(|()| None);
267        profile.set_hotbar_slots("TestServer", Some(CharacterId(12345)), slots);
268    }
269}