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 arc_swap::ArcSwap;
6use scc::HashMap;
7use steel_utils::locks::SyncMutex;
8use uuid::Uuid;
9
10use crate::{entity::Entity, player::Player};
11
12struct PlayerSlot {
13    player: ArcSwap<Player>,
14}
15
16impl PlayerSlot {
17    fn new(player: Arc<Player>) -> Self {
18        Self {
19            player: ArcSwap::new(player),
20        }
21    }
22
23    fn load(&self) -> Arc<Player> {
24        self.player.load_full()
25    }
26
27    fn replace(&self, expected: &Arc<Player>, replacement: Arc<Player>) -> bool {
28        let previous = self.player.compare_and_swap(expected, replacement);
29        Arc::ptr_eq(&previous, expected)
30    }
31}
32
33/// Thread-safe player storage with dual indexing.
34///
35/// Both indexes point to the same player slot, so replacing a player updates
36/// UUID and entity ID lookups as one operation.
37pub struct PlayerMap {
38    /// Primary index by UUID (persistent identifier)
39    by_uuid: HashMap<Uuid, Arc<PlayerSlot>>,
40    /// Secondary index by entity ID (session-local identifier)
41    by_entity_id: HashMap<i32, Arc<PlayerSlot>>,
42    /// Player UUIDs in insertion order for vanilla-visible iteration.
43    order: SyncMutex<Vec<Uuid>>,
44    /// Serializes changes to the indexes and their shared slots.
45    mutations: SyncMutex<()>,
46}
47
48impl Default for PlayerMap {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl PlayerMap {
55    /// Creates a new empty player map.
56    #[must_use]
57    pub fn new() -> Self {
58        Self {
59            by_uuid: HashMap::new(),
60            by_entity_id: HashMap::new(),
61            order: SyncMutex::new(Vec::new()),
62            mutations: SyncMutex::new(()),
63        }
64    }
65
66    /// Inserts a player into both maps.
67    ///
68    /// Returns `true` if the player was inserted, `false` if a player with the same UUID already exists.
69    ///
70    /// # Panics
71    ///
72    /// Panics if another player already has the same entity ID. Entity IDs are
73    /// session-unique; accepting a duplicate would break entity lookup and
74    /// packet routing invariants.
75    pub fn insert(&self, player: Arc<Player>) -> bool {
76        let uuid = player.gameprofile.id;
77        let entity_id = player.id();
78        let _mutation = self.mutations.lock();
79        let slot = Arc::new(PlayerSlot::new(player));
80
81        if self.by_uuid.insert_sync(uuid, Arc::clone(&slot)).is_err() {
82            return false;
83        }
84
85        if self.by_entity_id.insert_sync(entity_id, slot).is_err() {
86            let _ = self.by_uuid.remove_sync(&uuid);
87            panic!("player entity id {entity_id} is already registered");
88        }
89        self.order.lock().push(uuid);
90        true
91    }
92
93    /// Removes a player by UUID from both maps.
94    ///
95    /// Returns the removed player if found.
96    #[expect(
97        clippy::unused_async,
98        clippy::unused_async_trait_impl,
99        reason = "keeps the existing asynchronous PlayerMap API while mutation is synchronized internally"
100    )]
101    pub async fn remove(&self, uuid: &Uuid) -> Option<Arc<Player>> {
102        self.remove_sync(uuid)
103    }
104
105    /// Removes this exact player from both maps.
106    ///
107    /// Returns the removed player if the UUID still maps to this same player
108    /// handle. A stale duplicate-login cleanup must not remove the accepted
109    /// player that owns the UUID.
110    #[expect(
111        clippy::unused_async,
112        clippy::unused_async_trait_impl,
113        reason = "keeps the existing asynchronous PlayerMap API while mutation is synchronized internally"
114    )]
115    pub async fn remove_player(&self, player: &Arc<Player>) -> Option<Arc<Player>> {
116        self.remove_player_sync(player)
117    }
118
119    /// Removes a player by UUID from both maps synchronously.
120    ///
121    /// Returns the removed player if found. Use this when async is not available
122    /// (e.g., during world changes on the tick thread).
123    pub fn remove_sync(&self, uuid: &Uuid) -> Option<Arc<Player>> {
124        let _mutation = self.mutations.lock();
125        let (_, slot) = self.by_uuid.remove_sync(uuid)?;
126        let player = slot.load();
127        let _ = self
128            .by_entity_id
129            .remove_if_sync(&player.id(), |current| Arc::ptr_eq(current, &slot));
130        self.order.lock().retain(|player_uuid| player_uuid != uuid);
131        Some(player)
132    }
133
134    /// Removes this exact player from both maps synchronously.
135    pub fn remove_player_sync(&self, player: &Arc<Player>) -> Option<Arc<Player>> {
136        let uuid = player.gameprofile.id;
137        let _mutation = self.mutations.lock();
138        let slot = self
139            .by_uuid
140            .read_sync(&uuid, |_, current| Arc::clone(current))?;
141        let current = slot.load();
142        if !Arc::ptr_eq(&current, player) {
143            return None;
144        }
145
146        let (_, removed_slot) = self
147            .by_uuid
148            .remove_if_sync(&uuid, |current| Arc::ptr_eq(current, &slot))?;
149        let _ = self
150            .by_entity_id
151            .remove_if_sync(&current.id(), |indexed| Arc::ptr_eq(indexed, &removed_slot));
152        self.order
153            .lock()
154            .retain(|indexed_uuid| *indexed_uuid != uuid);
155        Some(current)
156    }
157
158    /// Replaces this exact player while retaining its UUID, entity ID, and
159    /// insertion-order position.
160    ///
161    /// Returns `false` if the replacement has different index keys, either
162    /// index no longer points to the same slot, or `expected` is stale. The
163    /// pointer comparison and replacement form one compare-and-swap operation,
164    /// so concurrent attempts using the same expected player cannot both
165    /// succeed.
166    pub fn replace_player(&self, expected: &Arc<Player>, replacement: Arc<Player>) -> bool {
167        let uuid = expected.gameprofile.id;
168        let entity_id = expected.id();
169        if replacement.gameprofile.id != uuid || replacement.id() != entity_id {
170            return false;
171        }
172
173        let _mutation = self.mutations.lock();
174        let Some(slot) = self
175            .by_uuid
176            .read_sync(&uuid, |_, current| Arc::clone(current))
177        else {
178            return false;
179        };
180        let Some(entity_slot) = self
181            .by_entity_id
182            .read_sync(&entity_id, |_, current| Arc::clone(current))
183        else {
184            return false;
185        };
186        if !Arc::ptr_eq(&slot, &entity_slot) {
187            return false;
188        }
189
190        slot.replace(expected, replacement)
191    }
192
193    /// Gets a player by UUID.
194    #[must_use]
195    pub fn get_by_uuid(&self, uuid: &Uuid) -> Option<Arc<Player>> {
196        self.by_uuid.read_sync(uuid, |_, slot| slot.load())
197    }
198
199    /// Gets a player by entity ID.
200    #[must_use]
201    pub fn get_by_entity_id(&self, entity_id: i32) -> Option<Arc<Player>> {
202        self.by_entity_id
203            .read_sync(&entity_id, |_, slot| slot.load())
204    }
205
206    /// Iterates over all players.
207    ///
208    /// The callback returns `true` to continue iteration, `false` to stop.
209    pub fn iter_players<F>(&self, mut f: F)
210    where
211        F: FnMut(&Uuid, &Arc<Player>) -> bool,
212    {
213        let order = self.order.lock().iter().copied().collect::<Vec<_>>();
214        for uuid in order {
215            let Some(player) = self.get_by_uuid(&uuid) else {
216                continue;
217            };
218            if !f(&uuid, &player) {
219                return;
220            }
221        }
222    }
223
224    /// Returns the number of players.
225    #[must_use]
226    pub fn len(&self) -> usize {
227        self.by_uuid.len()
228    }
229
230    /// Returns true if there are no players.
231    #[must_use]
232    pub fn is_empty(&self) -> bool {
233        self.by_uuid.is_empty()
234    }
235}
236
237#[cfg(test)]
238mod tests;