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