steel_core/player/
session.rs1use std::sync::{
2 Arc, Weak,
3 atomic::{AtomicU64, Ordering},
4};
5
6use steel_utils::locks::{SyncMutex, SyncRwLock};
7
8use super::{
9 DROP_SPAM_THROTTLER_INCREMENT_STEP, DROP_SPAM_THROTTLER_THRESHOLD, Player, chat::ChatState,
10 chunk_sender::ChunkSender, spam_throttler::TickThrottler,
11};
12
13static LAST_PLAYER_SESSION_ID: AtomicU64 = AtomicU64::new(0);
14
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub(crate) struct PlayerSessionId(u64);
18
19pub struct PlayerSession {
21 id: PlayerSessionId,
22 current_player: SyncRwLock<CurrentPlayerSlot>,
23 pub(crate) chunk_sender: SyncMutex<ChunkSender>,
24 pub(crate) chat: SyncMutex<ChatState>,
25 pub(super) drop_spam_throttler: SyncMutex<TickThrottler>,
27}
28
29enum CurrentPlayerSlot {
30 Unbound,
31 Bound(Weak<Player>),
32 Closed,
33}
34
35impl PlayerSession {
36 #[must_use]
42 pub fn new(chat_spam_threshold_seconds: i32, command_spam_threshold_seconds: i32) -> Self {
43 let Ok(previous) =
44 LAST_PLAYER_SESSION_ID.try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
45 current.checked_add(1)
46 })
47 else {
48 panic!("player session ID space exhausted");
49 };
50
51 Self {
52 id: PlayerSessionId(previous + 1),
53 current_player: SyncRwLock::new(CurrentPlayerSlot::Unbound),
54 chunk_sender: SyncMutex::new(ChunkSender::default()),
55 chat: SyncMutex::new(ChatState::new(
56 chat_spam_threshold_seconds,
57 command_spam_threshold_seconds,
58 )),
59 drop_spam_throttler: SyncMutex::new(TickThrottler::new(
60 DROP_SPAM_THROTTLER_INCREMENT_STEP,
61 DROP_SPAM_THROTTLER_THRESHOLD,
62 )),
63 }
64 }
65
66 #[must_use]
67 pub(crate) const fn id(&self) -> PlayerSessionId {
68 self.id
69 }
70
71 pub(crate) const fn chunk_sender(&self) -> &SyncMutex<ChunkSender> {
72 &self.chunk_sender
73 }
74
75 #[must_use]
77 pub(crate) fn current_player(&self) -> Option<Arc<Player>> {
78 match &*self.current_player.read() {
79 CurrentPlayerSlot::Bound(player) => player.upgrade(),
80 CurrentPlayerSlot::Unbound | CurrentPlayerSlot::Closed => None,
81 }
82 }
83
84 #[must_use]
86 pub(crate) fn is_current_player(&self, player: &Arc<Player>) -> bool {
87 self.owns(player)
88 && matches!(
89 &*self.current_player.read(),
90 CurrentPlayerSlot::Bound(current) if current.ptr_eq(&Arc::downgrade(player))
91 )
92 }
93
94 pub fn bind_initial_player(&self, player: &Arc<Player>) -> bool {
96 if !self.owns(player) {
97 return false;
98 }
99
100 let mut current = self.current_player.write();
101 if !matches!(*current, CurrentPlayerSlot::Unbound) {
102 return false;
103 }
104 *current = CurrentPlayerSlot::Bound(Arc::downgrade(player));
105 true
106 }
107
108 pub(crate) fn replace_player(&self, expected: &Arc<Player>, replacement: &Arc<Player>) -> bool {
110 if !self.owns(expected) || !self.owns(replacement) {
111 return false;
112 }
113
114 let mut current = self.current_player.write();
115 if !matches!(
116 &*current,
117 CurrentPlayerSlot::Bound(player) if player.ptr_eq(&Arc::downgrade(expected))
118 ) {
119 return false;
120 }
121 *current = CurrentPlayerSlot::Bound(Arc::downgrade(replacement));
122 true
123 }
124
125 pub(crate) fn clear_player(&self, expected: &Arc<Player>) -> bool {
127 if !self.owns(expected) {
128 return false;
129 }
130
131 let mut current = self.current_player.write();
132 if !matches!(
133 &*current,
134 CurrentPlayerSlot::Bound(player) if player.ptr_eq(&Arc::downgrade(expected))
135 ) {
136 return false;
137 }
138 *current = CurrentPlayerSlot::Closed;
139 true
140 }
141
142 fn owns(&self, player: &Player) -> bool {
143 self.id == player.session.id
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use std::sync::Arc;
150
151 use crate::{
152 entity::Entity as _,
153 player::{ClientInformation, Player},
154 test_support::{TestPlayerBuilder, fresh_test_world},
155 };
156
157 use super::PlayerSession;
158
159 fn replacement_for(player: &Arc<Player>, session: Arc<PlayerSession>) -> Arc<Player> {
160 Arc::new(Player::new(
161 player.gameprofile.clone(),
162 Arc::clone(&player.connection),
163 session,
164 player.get_world(),
165 player.server.clone(),
166 Arc::clone(&player.config),
167 player.id(),
168 ClientInformation::default(),
169 ))
170 }
171
172 #[test]
173 fn replacement_requires_session_ownership_and_exact_current_player() {
174 let world = fresh_test_world("player_session_exact_replacement");
175 let original = TestPlayerBuilder::new(Arc::clone(&world), "Original", 1).build();
176 let session = Arc::clone(&original.session);
177 let replacement = replacement_for(&original, Arc::clone(&session));
178 let stale_replacement = replacement_for(&original, Arc::clone(&session));
179 let foreign = TestPlayerBuilder::new(world, "Foreign", 2).build();
180 original.chat().lock().messages_sent = 7;
181
182 assert!(!session.replace_player(&original, &foreign));
183 assert!(session.replace_player(&original, &replacement));
184 assert!(session.is_current_player(&replacement));
185 assert!(!session.is_current_player(&original));
186 assert!(!session.replace_player(&original, &stale_replacement));
187 assert_eq!(replacement.chat().lock().messages_sent, 7);
188
189 let Some(current) = session.current_player() else {
190 panic!("replacement should remain bound to the session");
191 };
192 assert!(Arc::ptr_eq(¤t, &replacement));
193 }
194
195 #[test]
196 fn closed_session_cannot_be_bound_again() {
197 let session = Arc::new(PlayerSession::new(10, 10));
198 let foreign = TestPlayerBuilder::new(
199 fresh_test_world("player_session_foreign_initial_bind"),
200 "Foreign",
201 3,
202 )
203 .build();
204 assert!(!session.bind_initial_player(&foreign));
205
206 let player = replacement_for(&foreign, Arc::clone(&session));
207 assert!(session.bind_initial_player(&player));
208 assert!(session.clear_player(&player));
209 assert!(session.current_player().is_none());
210
211 let replacement = replacement_for(&player, Arc::clone(&session));
212 assert!(!session.bind_initial_player(&replacement));
213 assert!(!session.replace_player(&player, &replacement));
214 assert!(!session.clear_player(&player));
215 }
216}