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    pub tutorial: hud::tutorial::TutorialState,
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        let profile = common::util::ron_from_path_recoverable::<Self>(&path);
79        // Save profile to add new fields or create the file if it is not already there
80        profile.save_to_file_warn(config_dir);
81        profile
82    }
83
84    /// Save the current profile to disk, warn on failure.
85    pub fn save_to_file_warn(&self, config_dir: &Path) {
86        if let Err(e) = self.save_to_file(config_dir) {
87            warn!(?e, "Failed to save profile");
88        }
89    }
90
91    /// Get the hotbar_slots for the requested character_id.
92    ///
93    /// If the server or character does not exist then the default hotbar_slots
94    /// (empty) is returned.
95    ///
96    /// # Arguments
97    ///
98    /// * server - current server the character is on.
99    /// * character_id - id of the character, passing `None` indicates the
100    ///   transient character profile should be used.
101    pub fn get_hotbar_slots(
102        &self,
103        server: &str,
104        character_id: Option<CharacterId>,
105    ) -> [Option<hud::HotbarSlotContents>; 10] {
106        match character_id {
107            Some(character_id) => self
108                .servers
109                .get(server)
110                .and_then(|s| s.characters.get(&character_id)),
111            None => self.transient_character.as_ref(),
112        }
113        .map(|c| c.hotbar_slots.clone())
114        .unwrap_or_else(default_slots)
115    }
116
117    /// Set the hotbar_slots for the requested character_id.
118    ///
119    /// If the server or character does not exist then the appropriate fields
120    /// will be initialised and the slots added.
121    ///
122    /// # Arguments
123    ///
124    /// * server - current server the character is on.
125    /// * character_id - id of the character, passing `None` indicates the
126    ///   transient character profile should be used.
127    /// * slots - array of hotbar_slots to save.
128    pub fn set_hotbar_slots(
129        &mut self,
130        server: &str,
131        character_id: Option<CharacterId>,
132        slots: [Option<hud::HotbarSlotContents>; 10],
133    ) {
134        match character_id {
135            Some(character_id) => self.servers
136              .entry(server.to_string())
137              .or_default()
138              // Get or update the CharacterProfile.
139              .characters
140              .entry(character_id)
141              .or_default(),
142            None => self.transient_character.get_or_insert_default(),
143        }
144        .hotbar_slots = slots;
145    }
146
147    /// Get the selected_character for the provided server.
148    ///
149    /// if the server does not exist then the default selected_character (None)
150    /// is returned.
151    ///
152    /// # Arguments
153    ///
154    /// * server - current server the character is on.
155    pub fn get_selected_character(&self, server: &str) -> Option<CharacterId> {
156        self.servers
157            .get(server)
158            .map(|s| s.selected_character)
159            .unwrap_or_default()
160    }
161
162    /// Set the selected_character for the provided server.
163    ///
164    /// If the server does not exist then the appropriate fields
165    /// will be initialised and the selected_character added.
166    ///
167    /// # Arguments
168    ///
169    /// * server - current server the character is on.
170    /// * selected_character - option containing selected character ID
171    pub fn set_selected_character(
172        &mut self,
173        server: &str,
174        selected_character: Option<CharacterId>,
175    ) {
176        self.servers
177            .entry(server.to_string())
178            .or_default()
179            .selected_character = selected_character;
180    }
181
182    /// Get the selected_character for the provided server.
183    ///
184    /// if the server does not exist then the default spectate_position (None)
185    /// is returned.
186    ///
187    /// # Arguments
188    ///
189    /// * server - current server the player is on.
190    pub fn get_spectate_position(&self, server: &str) -> Option<vek::Vec3<f32>> {
191        self.servers
192            .get(server)
193            .map(|s| s.spectate_position)
194            .unwrap_or_default()
195    }
196
197    /// Set the spectate_position for the provided server.
198    ///
199    /// If the server does not exist then the appropriate fields
200    /// will be initialised and the selected_character added.
201    ///
202    /// # Arguments
203    ///
204    /// * server - current server the player is on.
205    /// * spectate_position - option containing the position we're spectating
206    pub fn set_spectate_position(
207        &mut self,
208        server: &str,
209        spectate_position: Option<vek::Vec3<f32>>,
210    ) {
211        self.servers
212            .entry(server.to_string())
213            .or_default()
214            .spectate_position = spectate_position;
215    }
216
217    /// Save the current profile to disk.
218    fn save_to_file(&self, config_dir: &Path) -> std::io::Result<()> {
219        let path = Self::get_path(config_dir);
220        if let Some(dir) = path.parent() {
221            fs::create_dir_all(dir)?;
222        }
223
224        let ron = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()).unwrap();
225        fs::write(path, ron.as_bytes())
226    }
227
228    fn get_path(config_dir: &Path) -> PathBuf { config_dir.join("profile.ron") }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_get_slots_with_empty_profile() {
237        let profile = Profile::default();
238        let slots = profile.get_hotbar_slots("TestServer", Some(CharacterId(12345)));
239        assert_eq!(slots, [(); 10].map(|()| None))
240    }
241
242    #[test]
243    fn test_set_slots_with_empty_profile() {
244        let mut profile = Profile::default();
245        let slots = [(); 10].map(|()| None);
246        profile.set_hotbar_slots("TestServer", Some(CharacterId(12345)), slots);
247    }
248}