Skip to main content

shuyi_ui/
profiles.rs

1//! Player profiles: a profile holds a player's options, keys and saves,
2//! whatever the game keeps for each player. Each of those keeps the
3//! [`Storage`] key it would have without profiles, inside the profile: a
4//! profile's key is its id's prefix and that key (`p1-settings`,
5//! `p2-slot_03`, [`ProfileId::key`]). [`ProfileStore`] is the [`Storage`]
6//! of the current profile, and the computer's own storage under it.
7//!
8//! The list of profiles and which one was used last stay with the computer,
9//! under [`PROFILES_KEY`] ([`Profiles::to_ron`]). [`load`] reads it, makes
10//! the first profile on a first launch, and moves a player's keys from
11//! before a game had profiles into it ([`migrate`]).
12
13use crate::prelude::*;
14use core::fmt;
15
16use serde::{Deserialize, Serialize};
17
18use crate::storage::{Storage, StorageError, is_valid_key};
19
20/// [`Storage`] key of the list of profiles, kept per computer.
21pub const PROFILES_KEY: &str = "profiles";
22
23/// The saved list's format version.
24pub const PROFILES_VERSION: u32 = 1;
25
26/// The most profiles there can be.
27pub const MAX_PROFILES: usize = 8;
28
29/// The longest profile name, in characters.
30pub const MAX_NAME_CHARS: usize = 12;
31
32/// A profile's number. Never reused: a deleted profile's number is never
33/// given to a new one, so nothing it left behind can turn up in another.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct ProfileId(u32);
37
38impl ProfileId {
39    /// The first profile's, made on a first launch.
40    pub const FIRST: Self = Self(1);
41
42    /// The number.
43    pub fn get(self) -> u32 {
44        self.0
45    }
46
47    /// What starts every [`Storage`] key of this profile: `p1-`.
48    pub fn prefix(self) -> String {
49        format!("p{}-", self.0)
50    }
51
52    /// The [`Storage`] key of this profile's `key`: `p1-settings`.
53    pub fn key(self, key: &str) -> String {
54        format!("{}{key}", self.prefix())
55    }
56
57    /// The profile a computer [`Storage`] key belongs to, if it is one
58    /// ([`key`](Self::key)'s form).
59    fn of_key(key: &str) -> Option<Self> {
60        let (number, rest) = key.strip_prefix('p')?.split_once('-')?;
61        let canonical = !number.starts_with('0') && number.bytes().all(|b| b.is_ascii_digit());
62        let id = number
63            .parse()
64            .ok()
65            .filter(|_| canonical && !rest.is_empty())?;
66        Some(Self(id))
67    }
68}
69
70/// One profile in the list.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct Profile {
73    /// Its number, which names its keys.
74    pub id: ProfileId,
75    /// What the player called it.
76    pub name: String,
77}
78
79/// Why a change to the list of profiles was refused.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum ProfileError {
82    /// The name is empty (or only spaces).
83    NameEmpty,
84    /// The name is over [`MAX_NAME_CHARS`].
85    NameTooLong,
86    /// Another profile has this name (ignoring case).
87    NameTaken,
88    /// There are [`MAX_PROFILES`] already.
89    TooMany,
90    /// The only profile left can't be deleted.
91    LastOne,
92    /// No profile has this id.
93    NoSuchProfile(ProfileId),
94    /// The list couldn't be saved, so nothing changed.
95    Storage(StorageError),
96}
97
98// check-text: not player text (for the log; a profile screen words its own)
99impl fmt::Display for ProfileError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::NameEmpty => f.write_str("the name is empty"),
103            Self::NameTooLong => write!(f, "the name is over {MAX_NAME_CHARS} characters"),
104            Self::NameTaken => f.write_str("another profile has that name"),
105            Self::TooMany => write!(f, "there are {MAX_PROFILES} profiles already"),
106            Self::LastOne => f.write_str("the only profile can't be deleted"),
107            Self::NoSuchProfile(id) => write!(f, "no profile {}", id.get()),
108            Self::Storage(e) => write!(f, "profiles: {e}"),
109        }
110    }
111}
112
113impl core::error::Error for ProfileError {}
114
115/// The list of profiles, in the order they were made, and the one in use.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Profiles {
118    list: Vec<Profile>,
119    current: ProfileId,
120    /// The number the next profile made gets.
121    next_id: u32,
122}
123
124/// The saved form of [`Profiles`].
125#[derive(Debug, Serialize, Deserialize)]
126#[serde(rename = "Profiles")]
127struct ProfilesFile {
128    version: u32,
129    profiles: Vec<Profile>,
130    current: ProfileId,
131    next_id: u32,
132}
133
134impl Profiles {
135    /// A list of one profile, [`ProfileId::FIRST`], called `name`, in use.
136    pub fn first(name: &str) -> Self {
137        Self {
138            list: vec![Profile {
139                id: ProfileId::FIRST,
140                name: name.to_owned(),
141            }],
142            current: ProfileId::FIRST,
143            next_id: ProfileId::FIRST.0 + 1,
144        }
145    }
146
147    /// Every profile, in the order they were made.
148    pub fn all(&self) -> &[Profile] {
149        &self.list
150    }
151
152    /// The profile in use.
153    pub fn current(&self) -> ProfileId {
154        self.current
155    }
156
157    /// The profile `id`, if there is one.
158    pub fn get(&self, id: ProfileId) -> Option<&Profile> {
159        self.list.iter().find(|p| p.id == id)
160    }
161
162    /// Makes `id` the profile in use.
163    pub fn set_current(&mut self, id: ProfileId) -> Result<(), ProfileError> {
164        self.get(id).ok_or(ProfileError::NoSuchProfile(id))?;
165        self.current = id;
166        Ok(())
167    }
168
169    /// Adds a profile called `name` (spaces around it dropped) and returns
170    /// its id. The profile in use stays the same.
171    pub fn add(&mut self, name: &str) -> Result<ProfileId, ProfileError> {
172        if self.list.len() >= MAX_PROFILES {
173            return Err(ProfileError::TooMany);
174        }
175        let name = self.check_name(name, None)?;
176        let id = ProfileId(self.next_id);
177        self.next_id += 1;
178        self.list.push(Profile { id, name });
179        Ok(id)
180    }
181
182    /// Renames the profile `id` to `name` (spaces around it dropped).
183    pub fn rename(&mut self, id: ProfileId, name: &str) -> Result<(), ProfileError> {
184        self.get(id).ok_or(ProfileError::NoSuchProfile(id))?;
185        let name = self.check_name(name, Some(id))?;
186        if let Some(profile) = self.list.iter_mut().find(|p| p.id == id) {
187            profile.name = name; // check-text: not a data name
188        }
189        Ok(())
190    }
191
192    /// Takes the profile `id` off the list (not its keys:
193    /// [`delete_keys`]). If it was in use, the first profile left is.
194    pub fn remove(&mut self, id: ProfileId) -> Result<(), ProfileError> {
195        self.get(id).ok_or(ProfileError::NoSuchProfile(id))?;
196        if self.list.len() == 1 {
197            return Err(ProfileError::LastOne);
198        }
199        self.list.retain(|p| p.id != id);
200        if self.current == id {
201            self.current = self.list[0].id;
202        }
203        Ok(())
204    }
205
206    /// `name` without the spaces around it, if a profile (other than
207    /// `except`) may be called that.
208    fn check_name(&self, name: &str, except: Option<ProfileId>) -> Result<String, ProfileError> {
209        let name = name.trim();
210        if name.is_empty() {
211            return Err(ProfileError::NameEmpty);
212        }
213        if name.chars().count() > MAX_NAME_CHARS {
214            return Err(ProfileError::NameTooLong);
215        }
216        let lower = name.to_lowercase();
217        let taken = self
218            .list
219            .iter()
220            // check-text: not a data name
221            .any(|p| Some(p.id) != except && p.name.to_lowercase() == lower);
222        if taken {
223            return Err(ProfileError::NameTaken);
224        }
225        Ok(name.to_owned())
226    }
227
228    /// The saved form, as RON text.
229    pub fn to_ron(&self) -> String {
230        let file = ProfilesFile {
231            version: PROFILES_VERSION,
232            profiles: self.list.clone(),
233            current: self.current,
234            next_id: self.next_id,
235        };
236        let config = ron::ser::PrettyConfig::new().struct_names(true);
237        // Numbers and strings always serialise.
238        ron::ser::to_string_pretty(&file, config).unwrap_or_default()
239    }
240
241    /// Reads the saved form. Text that can't be read, of another version
242    /// or with no profile in it gives `Err` with why. A `current` that
243    /// isn't on the list is the first profile; a `next_id` that some
244    /// profile already has is moved past them all.
245    // check-text: not player text (warnings for the log)
246    pub fn from_ron(text: &str) -> Result<Self, String> {
247        let file: ProfilesFile = ron::from_str(text).map_err(|e| format!("unreadable: {e}"))?;
248        if file.version != PROFILES_VERSION {
249            let version = file.version;
250            return Err(format!("version {version} isn't {PROFILES_VERSION}"));
251        }
252        let first = file.profiles.first().ok_or("no profile in it")?.id;
253        let current = if file.profiles.iter().any(|p| p.id == file.current) {
254            file.current
255        } else {
256            first
257        };
258        let past_all = file.profiles.iter().map(|p| p.id.0 + 1).max().unwrap_or(1);
259        Ok(Self {
260            current,
261            next_id: file.next_id.max(past_all),
262            list: file.profiles,
263        })
264    }
265
266    /// Saves the list under [`PROFILES_KEY`].
267    pub fn save(&self, storage: &mut dyn Storage) -> Result<(), StorageError> {
268        storage.write(PROFILES_KEY, &self.to_ron())
269    }
270}
271
272/// Whether `key` can be a key of any profile: whether the largest
273/// profile's key for it ([`ProfileId::key`]) is a valid [`Storage`] key.
274/// A key a game keeps per player must be.
275pub fn fits_every_profile(key: &str) -> bool {
276    is_valid_key(key) && is_valid_key(&ProfileId(u32::MAX).key(key))
277}
278
279/// The list of profiles in `storage`, and warnings for the platform to
280/// log.
281///
282/// - Saved: that list.
283/// - None saved (a first launch, or a player from before the game had
284///   profiles): one profile called `numbered(1)`, with the player's keys
285///   from before profiles (`old_keys`: those the game kept them under)
286///   moved into it ([`migrate`]).
287/// - Saved but unreadable: a profile for each that has keys in `storage`,
288///   called `numbered(id)`, so no save is lost; with none, as if none was
289///   saved.
290/// - `storage` can't be read: one profile, for this launch only; nothing
291///   is written.
292pub fn load(
293    storage: &mut dyn Storage,
294    old_keys: &[String],
295    numbered: impl Fn(u32) -> String,
296) -> (Profiles, Vec<String>) {
297    let mut warnings = Vec::new();
298    let profiles = match storage.read(PROFILES_KEY) {
299        Ok(Some(text)) => match Profiles::from_ron(&text) {
300            Ok(profiles) => profiles,
301            Err(why) => {
302                // check-text: not player text (a warning for the log)
303                warnings.push(format!("{PROFILES_KEY}: {why}, made again from the saves"));
304                rebuild(storage, old_keys, &numbered, &mut warnings)
305            }
306        },
307        Ok(None) => migrate(
308            storage,
309            old_keys,
310            &numbered(ProfileId::FIRST.0),
311            &mut warnings,
312        ),
313        Err(e) => {
314            // check-text: not player text (a warning for the log)
315            warnings.push(format!("{PROFILES_KEY}: can't be read: {e}"));
316            Profiles::first(&numbered(ProfileId::FIRST.0))
317        }
318    };
319    (profiles, warnings)
320}
321
322/// A list with a profile for each one that has keys in `storage`, saved;
323/// [`migrate`] if none has.
324fn rebuild(
325    storage: &mut dyn Storage,
326    old_keys: &[String],
327    numbered: &impl Fn(u32) -> String,
328    warnings: &mut Vec<String>,
329) -> Profiles {
330    let mut ids: Vec<ProfileId> = storage
331        .list()
332        .unwrap_or_default()
333        .iter()
334        .filter_map(|key| ProfileId::of_key(key))
335        .collect();
336    ids.sort_unstable();
337    ids.dedup();
338    let Some(&first) = ids.first() else {
339        return migrate(storage, old_keys, &numbered(ProfileId::FIRST.0), warnings);
340    };
341    let profiles = Profiles {
342        list: ids
343            .iter()
344            .map(|&id| Profile {
345                id,
346                name: numbered(id.0),
347            })
348            .collect(),
349        current: first,
350        next_id: ids.iter().map(|id| id.0 + 1).max().unwrap_or(1),
351    };
352    if let Err(e) = profiles.save(storage) {
353        // check-text: not player text (a warning for the log)
354        warnings.push(format!("{PROFILES_KEY}: can't be saved: {e}"));
355    }
356    profiles
357}
358
359/// Makes the first profile, called `name`, and moves the player's keys from
360/// before profiles (`old_keys`) into it, once.
361///
362/// Nothing is lost if it stops part-way: each key is copied (unless the
363/// profile has it already), then the list is saved, and only then are the
364/// old keys deleted. Until the list is saved, every launch tries again;
365/// once it is, the move is done, and an old key that couldn't be deleted is
366/// a copy nobody reads.
367pub fn migrate(
368    storage: &mut dyn Storage,
369    old_keys: &[String],
370    name: &str,
371    warnings: &mut Vec<String>,
372) -> Profiles {
373    let profiles = Profiles::first(name);
374    let id = ProfileId::FIRST;
375    let mut moved = Vec::new();
376    let mut failed = false;
377    for key in old_keys {
378        let copied = match storage.read(key) {
379            Ok(None) => continue,
380            Ok(Some(value)) => match storage.read(&id.key(key)) {
381                Ok(Some(_)) => Ok(()),
382                Ok(None) => storage.write(&id.key(key), &value),
383                Err(e) => Err(e),
384            },
385            Err(e) => Err(e),
386        };
387        match copied {
388            Ok(()) => moved.push(key),
389            Err(e) => {
390                // check-text: not player text (a warning for the log)
391                warnings.push(format!("{key}: can't be moved into the first profile: {e}"));
392                failed = true;
393            }
394        }
395    }
396    if failed {
397        // check-text: not player text (a warning for the log)
398        warnings.push(format!(
399            "{PROFILES_KEY}: not saved, so the move is tried again"
400        ));
401        return profiles;
402    }
403    if let Err(e) = profiles.save(storage) {
404        // check-text: not player text (a warning for the log)
405        warnings.push(format!("{PROFILES_KEY}: can't be saved: {e}"));
406        return profiles;
407    }
408    for key in moved {
409        if let Err(e) = storage.delete(key) {
410            // check-text: not player text (a warning for the log)
411            warnings.push(format!("{key}: moved, but can't be deleted: {e}"));
412        }
413    }
414    profiles
415}
416
417/// Deletes every key of the profile `id` from the computer's `storage`.
418/// Tries them all; the first error, if any.
419pub fn delete_keys(storage: &mut dyn Storage, id: ProfileId) -> Result<(), StorageError> {
420    let keys = storage.list()?;
421    let mut result = Ok(());
422    for key in keys.iter().filter(|k| ProfileId::of_key(k) == Some(id)) {
423        let deleted = storage.delete(key);
424        if result.is_ok() {
425            result = deleted;
426        }
427    }
428    result
429}
430
431/// The [`Storage`] of the profile in use: each key is read and written as
432/// that profile's ([`ProfileId::key`]), and [`list`](Storage::list) lists
433/// only its keys, without their prefix. What stays with the computer (the
434/// list of profiles, whatever the game keeps per computer) goes through
435/// [`computer`](Self::computer).
436#[derive(Debug)]
437pub struct ProfileStore {
438    computer: Box<dyn Storage>,
439    profile: ProfileId,
440}
441
442/// An empty [`MemoryStorage`](crate::storage::MemoryStorage), as the
443/// first profile's.
444impl Default for ProfileStore {
445    fn default() -> Self {
446        Self::new(
447            Box::new(crate::storage::MemoryStorage::new()),
448            ProfileId::FIRST,
449        )
450    }
451}
452
453impl ProfileStore {
454    /// The storage of `profile` in `computer`.
455    pub fn new(computer: Box<dyn Storage>, profile: ProfileId) -> Self {
456        Self { computer, profile }
457    }
458
459    /// The profile whose keys this reads and writes.
460    pub fn profile(&self) -> ProfileId {
461        self.profile
462    }
463
464    /// Reads and writes `profile`'s keys from now on.
465    pub(crate) fn set_profile(&mut self, profile: ProfileId) {
466        self.profile = profile;
467    }
468
469    /// The computer's own storage, for what no profile holds.
470    pub fn computer(&self) -> &dyn Storage {
471        &*self.computer
472    }
473
474    /// [`computer`](Self::computer), to write.
475    pub fn computer_mut(&mut self) -> &mut dyn Storage {
476        &mut *self.computer
477    }
478
479    /// The computer's storage, for the next launch.
480    pub fn into_computer(self) -> Box<dyn Storage> {
481        self.computer
482    }
483
484    /// The computer's key for `key`, if both are valid.
485    fn full_key(&self, key: &str) -> Result<String, StorageError> {
486        let full = self.profile.key(key);
487        if is_valid_key(key) && is_valid_key(&full) {
488            Ok(full)
489        } else {
490            Err(StorageError::InvalidKey(key.to_owned()))
491        }
492    }
493}
494
495impl Storage for ProfileStore {
496    fn read(&self, key: &str) -> Result<Option<String>, StorageError> {
497        self.computer.read(&self.full_key(key)?)
498    }
499
500    fn write(&mut self, key: &str, value: &str) -> Result<(), StorageError> {
501        let full = self.full_key(key)?;
502        self.computer.write(&full, value)
503    }
504
505    fn delete(&mut self, key: &str) -> Result<(), StorageError> {
506        let full = self.full_key(key)?;
507        self.computer.delete(&full)
508    }
509
510    fn list(&self) -> Result<Vec<String>, StorageError> {
511        let prefix = self.profile.prefix();
512        let keys = self.computer.list()?;
513        Ok(keys
514            .iter()
515            .filter_map(|key| key.strip_prefix(&prefix).map(str::to_owned))
516            .collect())
517    }
518}
519
520#[cfg(test)]
521mod tests;