Skip to main content

steel_core/world/player_index/
map.rs

1//! Thread-safe player storage with dual indexing by UUID and entity ID.
2
3use std::sync::Arc;
4
5use scc::HashMap;
6use steel_utils::locks::SyncMutex;
7use uuid::Uuid;
8
9use crate::{entity::Entity, player::Player};
10
11/// Thread-safe player storage with dual indexing.
12///
13/// Maintains two synchronized maps for O(1) lookup by either UUID or entity ID.
14/// All operations keep both maps in sync automatically.
15pub struct PlayerMap {
16    /// Primary index by UUID (persistent identifier)
17    by_uuid: HashMap<Uuid, Arc<Player>>,
18    /// Secondary index by entity ID (session-local identifier)
19    by_entity_id: HashMap<i32, Arc<Player>>,
20    /// Player UUIDs in insertion order for vanilla-visible iteration.
21    order: SyncMutex<Vec<Uuid>>,
22}
23
24impl Default for PlayerMap {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl PlayerMap {
31    /// Creates a new empty player map.
32    #[must_use]
33    pub fn new() -> Self {
34        Self {
35            by_uuid: HashMap::new(),
36            by_entity_id: HashMap::new(),
37            order: SyncMutex::new(Vec::new()),
38        }
39    }
40
41    /// Inserts a player into both maps.
42    ///
43    /// Returns `true` if the player was inserted, `false` if a player with the same UUID already exists.
44    ///
45    /// # Panics
46    ///
47    /// Panics if another player already has the same entity ID. Entity IDs are
48    /// session-unique; accepting a duplicate would break entity lookup and
49    /// packet routing invariants.
50    pub fn insert(&self, player: Arc<Player>) -> bool {
51        let uuid = player.gameprofile.id;
52        let entity_id = player.id();
53
54        if self.by_uuid.insert_sync(uuid, player.clone()).is_err() {
55            return false;
56        }
57
58        if self.by_entity_id.insert_sync(entity_id, player).is_err() {
59            let _ = self.by_uuid.remove_sync(&uuid);
60            panic!("player entity id {entity_id} is already registered");
61        }
62        self.order.lock().push(uuid);
63        true
64    }
65
66    /// Removes a player by UUID from both maps.
67    ///
68    /// Returns the removed player if found.
69    pub async fn remove(&self, uuid: &Uuid) -> Option<Arc<Player>> {
70        if let Some((_, player)) = self.by_uuid.remove_async(uuid).await {
71            let _ = self.by_entity_id.remove_async(&player.id()).await;
72            self.order.lock().retain(|player_uuid| player_uuid != uuid);
73            Some(player)
74        } else {
75            None
76        }
77    }
78
79    /// Removes this exact player from both maps.
80    ///
81    /// Returns the removed player if the UUID still maps to this same player
82    /// handle. A stale duplicate-login cleanup must not remove the accepted
83    /// player that owns the UUID.
84    pub async fn remove_player(&self, player: &Arc<Player>) -> Option<Arc<Player>> {
85        let uuid = player.gameprofile.id;
86        let (_, removed) = self
87            .by_uuid
88            .remove_if_async(&uuid, |current| Arc::ptr_eq(current, player))
89            .await?;
90        let _ = self
91            .by_entity_id
92            .remove_if_async(&removed.id(), |current| Arc::ptr_eq(current, &removed))
93            .await;
94        self.order
95            .lock()
96            .retain(|uuid| *uuid != removed.gameprofile.id);
97        Some(removed)
98    }
99
100    /// Removes a player by UUID from both maps synchronously.
101    ///
102    /// Returns the removed player if found. Use this when async is not available
103    /// (e.g., during world changes on the tick thread).
104    pub fn remove_sync(&self, uuid: &Uuid) -> Option<Arc<Player>> {
105        if let Some((_, player)) = self.by_uuid.remove_sync(uuid) {
106            let _ = self.by_entity_id.remove_sync(&player.id());
107            self.order.lock().retain(|player_uuid| player_uuid != uuid);
108            Some(player)
109        } else {
110            None
111        }
112    }
113
114    /// Removes this exact player from both maps synchronously.
115    pub fn remove_player_sync(&self, player: &Arc<Player>) -> Option<Arc<Player>> {
116        let uuid = player.gameprofile.id;
117        let (_, removed) = self
118            .by_uuid
119            .remove_if_sync(&uuid, |current| Arc::ptr_eq(current, player))?;
120        let _ = self
121            .by_entity_id
122            .remove_if_sync(&removed.id(), |current| Arc::ptr_eq(current, &removed));
123        self.order
124            .lock()
125            .retain(|uuid| *uuid != removed.gameprofile.id);
126        Some(removed)
127    }
128
129    /// Gets a player by UUID.
130    #[must_use]
131    pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<Arc<Player>> {
132        self.by_uuid.read_sync(uuid, |_, p| p.clone())
133    }
134
135    /// Gets a player by entity ID.
136    #[must_use]
137    pub fn get_by_entity_id(&self, entity_id: i32) -> Option<Arc<Player>> {
138        self.by_entity_id.read_sync(&entity_id, |_, p| p.clone())
139    }
140
141    /// Iterates over all players.
142    ///
143    /// The callback returns `true` to continue iteration, `false` to stop.
144    pub fn iter_players<F>(&self, mut f: F)
145    where
146        F: FnMut(&Uuid, &Arc<Player>) -> bool,
147    {
148        let order = self.order.lock().iter().copied().collect::<Vec<_>>();
149        for uuid in order {
150            let Some(player) = self.get_by_uuid(&uuid) else {
151                continue;
152            };
153            if !f(&uuid, &player) {
154                return;
155            }
156        }
157    }
158
159    /// Returns the number of players.
160    #[must_use]
161    pub fn len(&self) -> usize {
162        self.by_uuid.len()
163    }
164
165    /// Returns true if there are no players.
166    #[must_use]
167    pub fn is_empty(&self) -> bool {
168        self.by_uuid.is_empty()
169    }
170}