bloop_server_framework/
player.rs

1//! Player management module.
2//!
3//! This module provides traits and structures for managing player information,
4//! including read-only access ([`PlayerInfo`]), mutation capabilities
5//! ([`PlayerMutator`]), and a concurrent registry ([`PlayerRegistry`]) to store
6//! and lookup players by both UUID and NFC UID.
7//!
8//! The `PlayerRegistry` maintains internal consistency by updating NFC UID
9//! mappings automatically when players' NFC UIDs change, enabling safe
10//! concurrent access via `Arc<RwLock<Player>>` wrappers.
11//!
12//! It supports operations to add, remove, mutate, and query players efficiently.
13
14use crate::nfc_uid::NfcUid;
15use chrono::{DateTime, Utc};
16use std::collections::HashMap;
17use std::sync::{Arc, RwLock, RwLockReadGuard};
18use uuid::Uuid;
19
20/// Provides read-only information about a player.
21pub trait PlayerInfo {
22    /// Returns the unique player ID.
23    fn id(&self) -> Uuid;
24
25    /// Returns the player's NFC UID.
26    fn nfc_uid(&self) -> NfcUid;
27
28    /// Returns the total number of bloops the player has collected.
29    fn total_bloops(&self) -> usize;
30
31    /// Returns a reference to the map of awarded achievements and their timestamps.
32    fn awarded_achievements(&self) -> &HashMap<Uuid, DateTime<Utc>>;
33}
34
35/// Provides mutation methods for a player.
36pub trait PlayerMutator {
37    /// Increments the bloop counter by one.
38    fn increment_bloops(&mut self);
39
40    /// Adds an awarded achievement to the player.
41    fn add_awarded_achievement(&mut self, achievement_id: Uuid, awarded_at: DateTime<Utc>);
42}
43
44/// A registry for managing players by both their UUID and NFC UID.
45///
46/// The registry ensures that updates to players' NFC UIDs automatically keep
47/// internal lookup maps in sync.
48#[derive(Debug)]
49pub struct PlayerRegistry<Player: PlayerInfo> {
50    by_id: HashMap<Uuid, Arc<RwLock<Player>>>,
51    by_nfc_uid: HashMap<NfcUid, Arc<RwLock<Player>>>,
52}
53
54impl<Player: PlayerInfo> From<Vec<Player>> for PlayerRegistry<Player> {
55    fn from(players: Vec<Player>) -> Self {
56        Self::new(players)
57    }
58}
59
60impl<Player: PlayerInfo> PlayerRegistry<Player> {
61    /// Creates a new [`PlayerRegistry`] from a list of players
62    pub fn new(players: Vec<Player>) -> Self {
63        let mut by_id = HashMap::new();
64        let mut by_nfc_uid = HashMap::new();
65
66        for player in players {
67            let id = player.id();
68            let nfc_uid = player.nfc_uid();
69            let player = Arc::new(RwLock::new(player));
70
71            by_id.insert(id, player.clone());
72            by_nfc_uid.insert(nfc_uid, player);
73        }
74
75        Self { by_id, by_nfc_uid }
76    }
77
78    /// Returns the shared player reference (Arc) by their UUID.
79    pub(crate) fn get_by_nfc_uid(&self, nfc_uid: NfcUid) -> Option<Arc<RwLock<Player>>> {
80        self.by_nfc_uid.get(&nfc_uid).cloned()
81    }
82
83    /// Returns a read-only lock on a player by their UUID.
84    ///
85    /// Returns `None` if no player with the given ID exists.
86    pub fn read_by_id(&self, id: Uuid) -> Option<RwLockReadGuard<Player>> {
87        self.by_id.get(&id).map(|player| player.read().unwrap())
88    }
89
90    /// Returns a read-only lock on a player by their NFC UID.
91    ///
92    /// Returns `None` if no player with the given NFC UID exists.
93    pub fn read_by_nfc_uid(&self, nfc_uid: NfcUid) -> Option<RwLockReadGuard<Player>> {
94        self.by_nfc_uid
95            .get(&nfc_uid)
96            .map(|player| player.read().unwrap())
97    }
98
99    /// Returns `true` if a player with the given UUID exists in the registry.
100    pub fn contains_id(&self, id: Uuid) -> bool {
101        self.by_id.contains_key(&id)
102    }
103
104    /// Mutates a player by their UUID.
105    ///
106    /// Automatically updates the internal NFC UID lookup map if the player's
107    /// NFC UID changes.
108    ///
109    /// If no player with the given ID exists, this method does nothing.
110    pub fn mutate_by_id<F>(&mut self, id: Uuid, mutator: F)
111    where
112        F: FnOnce(&mut Player),
113    {
114        if let Some(player_arc) = self.by_id.get(&id) {
115            let mut player = player_arc.write().unwrap();
116            let old_nfc_uid = player.nfc_uid();
117
118            mutator(&mut player);
119
120            let new_nfc_uid = player.nfc_uid();
121
122            if old_nfc_uid != new_nfc_uid {
123                self.by_nfc_uid.remove(&old_nfc_uid);
124                self.by_nfc_uid.insert(new_nfc_uid, player_arc.clone());
125            }
126        }
127    }
128
129    /// Adds a new player to the registry.
130    ///
131    /// This method inserts the player into both internal lookup maps.
132    /// If a player with the same ID or NFC UID already exists, it will be
133    /// overwritten.
134    pub fn add(&mut self, player: Player) {
135        let id = player.id();
136        let nfc_uid = player.nfc_uid();
137        let player = Arc::new(RwLock::new(player));
138
139        self.by_id.insert(id, player.clone());
140        self.by_nfc_uid.insert(nfc_uid, player);
141    }
142
143    /// Removes a player from the registry by their UUID.
144    ///
145    /// This also removes the player from the NFC UID lookup map.
146    /// If no such player exists, this method does nothing.
147    pub fn remove(&mut self, id: Uuid) {
148        let Some(player) = self.by_id.remove(&id) else {
149            return;
150        };
151
152        let player = player.read().unwrap();
153        self.by_nfc_uid.remove(&player.nfc_uid());
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::test_utils::MockPlayer;
161
162    #[test]
163    fn adding_and_reading_player_by_nfc_uid_works() {
164        let (player, id) = MockPlayer::builder().bloops_count(5).build();
165        let player = Arc::try_unwrap(player).unwrap().into_inner().unwrap();
166
167        let registry = PlayerRegistry::new(vec![player]);
168
169        let player_read = registry.read_by_nfc_uid(NfcUid::default()).unwrap();
170        assert_eq!(player_read.total_bloops(), 5);
171        assert!(registry.contains_id(id));
172    }
173
174    #[test]
175    fn mutating_player_updates_nfc_uid_and_bloops_correctly() {
176        let (player, id) = MockPlayer::builder().bloops_count(0).build();
177        let player = Arc::try_unwrap(player).unwrap().into_inner().unwrap();
178
179        let mut registry = PlayerRegistry::new(vec![player]);
180
181        registry.mutate_by_id(id, |player| {
182            player.bloops_count = 42;
183        });
184
185        let player_read = registry.read_by_id(id).unwrap();
186        assert_eq!(player_read.total_bloops(), 42);
187    }
188
189    #[test]
190    fn removing_player_removes_from_all_indexes() {
191        let (player, id) = MockPlayer::builder().bloops_count(1).build();
192        let nfc_uid = NfcUid::default();
193        let player = Arc::try_unwrap(player).unwrap().into_inner().unwrap();
194
195        let mut registry = PlayerRegistry::new(vec![player]);
196
197        registry.remove(id);
198
199        assert!(registry.read_by_id(id).is_none());
200        assert!(registry.read_by_nfc_uid(nfc_uid).is_none());
201    }
202
203    #[test]
204    fn adding_player_overwrites_existing_player_with_same_id_or_nfc_uid() {
205        let (player1, id) = MockPlayer::builder().bloops_count(5).build();
206        let player1 = Arc::try_unwrap(player1).unwrap().into_inner().unwrap();
207
208        let (player2, _) = MockPlayer::builder().bloops_count(10).build();
209        let mut player2 = Arc::try_unwrap(player2).unwrap().into_inner().unwrap();
210
211        player2.id = id;
212        player2.registration_number = 99;
213
214        let mut registry = PlayerRegistry::new(vec![player1]);
215        registry.add(player2);
216
217        let player_read = registry.read_by_id(id).unwrap();
218        assert_eq!(player_read.total_bloops(), 10);
219        assert_eq!(player_read.registration_number, 99);
220    }
221}