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