Skip to main content

steel_core/server/
known_players.rs

1use super::{
2    Arc, GameProfile, KnownPlayer, KnownPlayerNameLookup, KnownPlayers, ProfileLookupError, Server,
3    Uuid, io, is_valid_player_name, lookup_online_profile, offline_uuid,
4};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub(super) enum UncachedPlayerTarget {
8    DirectUuid(Uuid),
9    OfflineName,
10    OnlineName,
11}
12
13pub(super) fn classify_uncached_player_target(
14    target: &str,
15    online_mode: bool,
16) -> UncachedPlayerTarget {
17    if let Ok(uuid) = Uuid::parse_str(target) {
18        return UncachedPlayerTarget::DirectUuid(uuid);
19    }
20    if online_mode {
21        UncachedPlayerTarget::OnlineName
22    } else {
23        UncachedPlayerTarget::OfflineName
24    }
25}
26
27pub(super) fn direct_uuid_profile(uuid: Uuid) -> KnownPlayer {
28    KnownPlayer::new(uuid, uuid.to_string())
29}
30
31pub(super) struct KnownPlayerCacheState {
32    players: KnownPlayers,
33    generation: u64,
34    worker_running: bool,
35    closed: bool,
36}
37
38impl KnownPlayerCacheState {
39    pub(super) const fn new(players: KnownPlayers) -> Self {
40        Self {
41            players,
42            generation: 0,
43            worker_running: false,
44            closed: false,
45        }
46    }
47
48    pub(super) fn record(&mut self, uuid: Uuid, name: String) -> bool {
49        if self.closed || !self.players.record(uuid, name) {
50            return false;
51        }
52        self.mark_changed()
53    }
54
55    pub(super) const fn mark_changed(&mut self) -> bool {
56        if self.closed {
57            return false;
58        }
59        self.generation = self.generation.wrapping_add(1);
60        if self.worker_running {
61            false
62        } else {
63            self.worker_running = true;
64            true
65        }
66    }
67
68    pub(super) fn snapshot(&self) -> (KnownPlayers, u64) {
69        (self.players.clone(), self.generation)
70    }
71
72    pub(super) const fn is_current(&self, generation: u64) -> bool {
73        !self.closed && self.generation == generation
74    }
75
76    pub(super) const fn finish_save(&mut self, generation: u64) -> KnownPlayerSaveStep {
77        if !self.closed && self.generation != generation {
78            KnownPlayerSaveStep::SaveAgain
79        } else {
80            self.worker_running = false;
81            KnownPlayerSaveStep::Finished
82        }
83    }
84
85    pub(super) fn close_if_idle(&mut self) -> Option<KnownPlayers> {
86        if self.worker_running {
87            return None;
88        }
89        self.closed = true;
90        Some(self.players.clone())
91    }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub(super) enum KnownPlayerSaveStep {
96    SaveAgain,
97    Finished,
98}
99
100impl Server {
101    /// Returns a snapshot of player identities known to this server.
102    #[must_use]
103    pub fn known_players(&self) -> KnownPlayers {
104        self.known_players.lock().players.clone()
105    }
106
107    /// Records a connected player identity in the persistent profile cache.
108    /// Returns the previous cached name for this UUID, if any.
109    pub fn record_known_player(self: &Arc<Self>, profile: &GameProfile) -> Option<String> {
110        let mut known = self.known_players.lock();
111        let previous = known
112            .players
113            .by_uuid(profile.id)
114            .map(|entry| entry.last_known_name().to_owned());
115        let start_worker = known.record(profile.id, profile.name.clone());
116        drop(known);
117        if start_worker {
118            self.start_known_player_save_worker();
119        }
120        previous
121    }
122
123    /// Records a UUID and last-known name in the persistent profile cache.
124    pub fn record_known_profile(self: &Arc<Self>, uuid: Uuid, last_known_name: impl Into<String>) {
125        let start_worker = self
126            .known_players
127            .lock()
128            .record(uuid, last_known_name.into());
129        if start_worker {
130            self.start_known_player_save_worker();
131        }
132    }
133
134    /// Resolves a vanilla game-profile command target by name or UUID.
135    ///
136    /// Online players and cached profiles are checked first. Uncached UUIDs remain
137    /// direct UUID targets in either server mode. Offline-mode names use vanilla's
138    /// deterministic UUID, while online mode queries the configured profile service.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error when the profile is unknown or the profile service fails.
143    pub async fn resolve_player_profile(
144        self: &Arc<Self>,
145        name: &str,
146    ) -> Result<KnownPlayer, ProfileLookupError> {
147        if let Some(profile) = self.cached_player_profile(name) {
148            return Ok(profile);
149        }
150
151        match classify_uncached_player_target(name, self.config.online_mode) {
152            UncachedPlayerTarget::DirectUuid(uuid) => {
153                // No verified name is available, so use the canonical UUID for
154                // feedback without adding a synthetic identity-cache entry.
155                return Ok(direct_uuid_profile(uuid));
156            }
157            UncachedPlayerTarget::OfflineName => {
158                let profile = KnownPlayer::new(offline_uuid(name), name.to_owned());
159                self.record_known_profile(profile.uuid(), profile.last_known_name().to_owned());
160                return Ok(profile);
161            }
162            UncachedPlayerTarget::OnlineName => {}
163        }
164        if !is_valid_player_name(name) {
165            return Err(ProfileLookupError::UnknownPlayer(name.to_owned()));
166        }
167
168        let profile = lookup_online_profile(
169            &self.profile_lookup_client,
170            self.config.profile_server.as_deref(),
171            name,
172        )
173        .await?;
174        self.record_known_profile(profile.uuid(), profile.last_known_name().to_owned());
175        Ok(profile)
176    }
177
178    fn cached_player_profile(self: &Arc<Self>, name: &str) -> Option<KnownPlayer> {
179        let uuid = Uuid::parse_str(name).ok();
180        if let Some(player) = self.get_players().into_iter().find(|player| {
181            player.gameprofile.name.eq_ignore_ascii_case(name)
182                || uuid.is_some_and(|uuid| player.gameprofile.id == uuid)
183        }) {
184            return Some(KnownPlayer::new(
185                player.gameprofile.id,
186                player.gameprofile.name.clone(),
187            ));
188        }
189
190        let mut known = self.known_players.lock();
191        if let Some(uuid) = uuid {
192            return known.players.resolve_uuid(uuid);
193        }
194        let (profile, start_worker) = match known
195            .players
196            .resolve_name(name, chrono::Utc::now().timestamp_millis())
197        {
198            KnownPlayerNameLookup::Found(profile) => (Some(profile), false),
199            KnownPlayerNameLookup::Missing => (None, false),
200            KnownPlayerNameLookup::Expired => {
201                let start_worker = known.mark_changed();
202                (None, start_worker)
203            }
204        };
205        drop(known);
206        if start_worker {
207            self.start_known_player_save_worker();
208        }
209        profile
210    }
211
212    fn start_known_player_save_worker(self: &Arc<Self>) {
213        let server = Arc::clone(self);
214        tokio::spawn(async move {
215            server.run_known_player_save_worker().await;
216        });
217    }
218
219    async fn run_known_player_save_worker(self: &Arc<Self>) {
220        loop {
221            let (players, generation) = self.known_players.lock().snapshot();
222            let result = self
223                .player_data_storage
224                .save_known_players_if_current(&players, || {
225                    self.known_players.lock().is_current(generation)
226                })
227                .await;
228            match result {
229                Ok(true | false) => {}
230                Err(error) => {
231                    tracing::error!(%error, "failed to save known player cache");
232                }
233            }
234            let step = self.known_players.lock().finish_save(generation);
235            if step == KnownPlayerSaveStep::SaveAgain {
236                continue;
237            }
238            self.known_player_save_idle.notify_one();
239            return;
240        }
241    }
242
243    /// Waits for the coalesced identity-cache writer and persists the final snapshot.
244    ///
245    /// Later identity observations are ignored because the server is shutting down.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error when the final rebuildable cache snapshot cannot be persisted.
250    pub async fn flush_known_players(&self) -> io::Result<()> {
251        let players = loop {
252            let idle = self.known_player_save_idle.notified();
253            let snapshot = self.known_players.lock().close_if_idle();
254            if let Some(players) = snapshot {
255                break players;
256            }
257            idle.await;
258        };
259        self.player_data_storage
260            .save_known_players_if_current(&players, || true)
261            .await
262            .map(|_| ())
263    }
264}