Skip to main content

steel_core/server/
player_lifecycle.rs

1use std::ptr;
2
3use crate::player::connection::NetworkConnection;
4
5use super::{
6    Arc, CLogin, CSetDefaultSpawnPosition, CommonPlayerSpawnInfo, DVec3, DomainPlayerData,
7    DomainPlayerState, DomainResidenceToken, EnderPearlRestoreJob, Entity, IMMEDIATE_RESPAWN,
8    Identifier, LIMITED_CRAFTING, PersistentEnderPearl, PersistentRootVehicle, Player,
9    PreparedSpawn, REDUCED_DEBUG_INFO, RegistryEntry, RespawnData, RootVehicleRestoreJob, Server,
10    UnpreparedDomainPlayerData, UnpreparedDomainPlayerState, Uuid, World, apply_default_spawn,
11    local_respawn_data_for_world,
12};
13
14pub(super) struct PreparedDomainRestores {
15    pub(super) target_world: Arc<World>,
16    pub(super) root_vehicle: Option<PersistentRootVehicle>,
17    pub(super) ender_pearls: Vec<PreparedEnderPearlRestore>,
18}
19
20pub(super) struct PreparedEnderPearlRestore {
21    pub(super) world: Arc<World>,
22    pub(super) payload: PersistentEnderPearl,
23}
24
25impl Server {
26    pub(super) async fn load_join_domain(&self, player: &Player) -> Result<String, String> {
27        match self
28            .player_data_storage
29            .load_global(player.gameprofile.id)
30            .await
31        {
32            Ok(Some(global)) if self.worlds.has_domain(&global.last_active_domain) => {
33                Ok(global.last_active_domain)
34            }
35            Ok(Some(global)) => {
36                log::warn!(
37                    "Player {} last active domain {} no longer exists, using default domain",
38                    player.gameprofile.name,
39                    global.last_active_domain
40                );
41                Ok(self.worlds.default_domain().to_owned())
42            }
43            Ok(None) => Ok(self.worlds.default_domain().to_owned()),
44            Err(e) => Err(format!("failed to load global player data: {e}")),
45        }
46    }
47
48    pub(super) async fn load_domain_player_state(
49        &self,
50        player: &Player,
51        target_domain: &str,
52        explicit_target_world: Option<Arc<World>>,
53    ) -> Result<DomainPlayerState, String> {
54        let state = self
55            .load_unprepared_domain_player_state(player, target_domain, explicit_target_world)
56            .await?;
57        self.prepare_domain_player_state(target_domain, state).await
58    }
59
60    pub(in crate::server) async fn load_unprepared_domain_player_state(
61        &self,
62        player: &Player,
63        target_domain: &str,
64        explicit_target_world: Option<Arc<World>>,
65    ) -> Result<UnpreparedDomainPlayerState, String> {
66        let has_explicit_target = explicit_target_world.is_some();
67        let default_world = self
68            .worlds
69            .default_world(target_domain)
70            .cloned()
71            .ok_or_else(|| format!("domain {target_domain} has no default world"))?;
72
73        match self
74            .player_data_storage
75            .load_domain(target_domain, player.gameprofile.id)
76            .await
77        {
78            Ok(Some(saved_data)) => {
79                let saved_world = self.resolve_saved_world(
80                    &saved_data.world,
81                    target_domain,
82                    explicit_target_world.as_ref().map(|world| &world.key),
83                    &player.gameprofile.name,
84                );
85                let (world, data) = if let Some(saved_world) = saved_world {
86                    (
87                        saved_world,
88                        UnpreparedDomainPlayerData::SavedRestored {
89                            data: Box::new(saved_data),
90                        },
91                    )
92                } else {
93                    (
94                        explicit_target_world.unwrap_or(default_world),
95                        UnpreparedDomainPlayerData::SavedWithoutLocation {
96                            data: Box::new(saved_data),
97                        },
98                    )
99                };
100                log::info!("Loaded saved data for player {}", player.gameprofile.name);
101                Ok(UnpreparedDomainPlayerState {
102                    world,
103                    data,
104                    explicit_target: has_explicit_target,
105                })
106            }
107            Ok(None) => {
108                log::debug!(
109                    "No saved data for player {} in domain {}, using defaults",
110                    player.gameprofile.name,
111                    target_domain
112                );
113                Ok(UnpreparedDomainPlayerState {
114                    world: explicit_target_world.unwrap_or(default_world),
115                    data: UnpreparedDomainPlayerData::FirstVisit,
116                    explicit_target: has_explicit_target,
117                })
118            }
119            Err(e) => Err(format!(
120                "failed to load domain player data for {} in domain {}: {e}",
121                player.gameprofile.name, target_domain
122            )),
123        }
124    }
125
126    async fn prepare_domain_player_state(
127        &self,
128        target_domain: &str,
129        state: UnpreparedDomainPlayerState,
130    ) -> Result<DomainPlayerState, String> {
131        let UnpreparedDomainPlayerState {
132            mut world,
133            explicit_target,
134            data,
135        } = state;
136        let (data, spawn_position) = match data {
137            UnpreparedDomainPlayerData::SavedRestored { data } => {
138                let spawn_position = DVec3::new(data.pos[0], data.pos[1], data.pos[2]);
139                (DomainPlayerData::SavedRestored { data }, spawn_position)
140            }
141            UnpreparedDomainPlayerData::SavedWithoutLocation { data } => {
142                let (spawn_world, spawn) = self
143                    .prepare_target_spawn(target_domain, explicit_target, &world)
144                    .await?;
145                world = spawn_world;
146                (
147                    DomainPlayerData::SavedWithoutLocation { data, spawn },
148                    spawn.position,
149                )
150            }
151            UnpreparedDomainPlayerData::FirstVisit => {
152                let (spawn_world, spawn) = self
153                    .prepare_target_spawn(target_domain, explicit_target, &world)
154                    .await?;
155                world = spawn_world;
156                (DomainPlayerData::FirstVisit { spawn }, spawn.position)
157            }
158        };
159        let spawn_chunk_request = world.prepare_player_spawn_chunks(spawn_position).await?;
160        Ok(DomainPlayerState {
161            world,
162            data,
163            spawn_chunk_request,
164        })
165    }
166
167    async fn prepare_target_spawn(
168        &self,
169        target_domain: &str,
170        explicit_target_world: bool,
171        world: &Arc<World>,
172    ) -> Result<(Arc<World>, PreparedSpawn), String> {
173        if explicit_target_world {
174            return Ok((world.clone(), Self::prepare_default_spawn(world).await?));
175        }
176
177        let (world, respawn_data) = self.respawn_world_and_data_for_domain(target_domain)?;
178        let spawn = Self::prepare_respawn_spawn(&world, &respawn_data).await?;
179        Ok((world, spawn))
180    }
181
182    async fn prepare_default_spawn(world: &Arc<World>) -> Result<PreparedSpawn, String> {
183        let (spawn, spawn_pos) = {
184            let level_data = world.level_data.read();
185            (
186                level_data.data().spawn.clone(),
187                level_data.data().spawn_pos(),
188            )
189        };
190        let position = world
191            .find_adjusted_shared_spawn_pos(spawn_pos, world.default_gamemode)
192            .await?;
193        Ok(PreparedSpawn {
194            position,
195            rotation: (spawn.angle, 0.0),
196        })
197    }
198
199    async fn prepare_respawn_spawn(
200        world: &Arc<World>,
201        respawn_data: &RespawnData,
202    ) -> Result<PreparedSpawn, String> {
203        let position = world
204            .find_adjusted_shared_spawn_pos(respawn_data.pos(), world.default_gamemode)
205            .await?;
206        Ok(PreparedSpawn {
207            position,
208            rotation: (respawn_data.yaw, respawn_data.pitch),
209        })
210    }
211
212    fn resolve_saved_world(
213        &self,
214        saved_world: &str,
215        target_domain: &str,
216        explicit_target_world: Option<&Identifier>,
217        player_name: &str,
218    ) -> Option<Arc<World>> {
219        let Ok(saved_world_key) = saved_world.parse::<Identifier>() else {
220            log::warn!(
221                "Saved world {saved_world} for player {player_name} is invalid, using target spawn"
222            );
223            return None;
224        };
225        if saved_world_key.namespace.as_ref() != target_domain {
226            log::warn!(
227                "Saved world {saved_world_key} for player {player_name} is outside target domain {target_domain}, using target spawn"
228            );
229            return None;
230        }
231        if let Some(explicit_target_world) = explicit_target_world
232            && explicit_target_world != &saved_world_key
233        {
234            return None;
235        }
236        let Some(saved_world) = self.worlds.get(&saved_world_key) else {
237            log::warn!(
238                "Saved world {saved_world_key} for player {player_name} is missing, using target spawn"
239            );
240            return None;
241        };
242        Some(Arc::clone(saved_world))
243    }
244
245    pub(super) fn apply_domain_player_state(player: &Arc<Player>, state: &DomainPlayerState) {
246        match &state.data {
247            DomainPlayerData::SavedRestored { data } => {
248                data.apply_to_player(player);
249            }
250            DomainPlayerData::SavedWithoutLocation { data, spawn } => {
251                apply_default_spawn(player, &state.world, *spawn);
252                data.apply_to_player_without_location(player);
253            }
254            DomainPlayerData::FirstVisit { spawn } => {
255                player.reset_domain_data_for_first_visit();
256                apply_default_spawn(player, &state.world, *spawn);
257            }
258        }
259    }
260
261    pub(super) fn prepare_domain_restores(
262        &self,
263        player: &Player,
264        state: &DomainPlayerState,
265    ) -> PreparedDomainRestores {
266        let target_domain = state.world.domain();
267        let ender_pearls = Self::ender_pearls_to_restore(state)
268            .into_iter()
269            .filter_map(|payload| {
270                self.resolve_pearl_world(&payload.world, target_domain, player)
271                    .map(|world| PreparedEnderPearlRestore { world, payload })
272            })
273            .collect();
274        PreparedDomainRestores {
275            target_world: Arc::clone(&state.world),
276            root_vehicle: Self::root_vehicle_to_restore(state),
277            ender_pearls,
278        }
279    }
280
281    pub(super) fn install_domain_restores(
282        player: &Player,
283        residence_token: DomainResidenceToken,
284        restores: &PreparedDomainRestores,
285    ) -> bool {
286        player.install_pending_domain_restores(
287            residence_token,
288            &restores.target_world,
289            restores.root_vehicle.clone(),
290            restores
291                .ender_pearls
292                .iter()
293                .map(|restore| restore.payload.clone())
294                .collect(),
295        )
296    }
297
298    pub(super) fn schedule_domain_restores(
299        &self,
300        player: &Arc<Player>,
301        residence_token: DomainResidenceToken,
302        restores: PreparedDomainRestores,
303    ) {
304        if let Some(root_vehicle) = restores.root_vehicle {
305            if let Some(job) = RootVehicleRestoreJob::new(
306                Arc::clone(player),
307                Arc::clone(&restores.target_world),
308                &root_vehicle,
309                residence_token,
310            ) {
311                self.jobs.spawn(job);
312            } else {
313                player.take_matching_pending_root_vehicle(
314                    residence_token,
315                    &restores.target_world,
316                    root_vehicle.attach,
317                    root_vehicle.entity.uuid,
318                );
319            }
320        }
321
322        for restore in restores.ender_pearls {
323            let pearl_uuid = Uuid::from_bytes(restore.payload.entity.uuid);
324            if let Some(job) = EnderPearlRestoreJob::new(
325                Arc::clone(player),
326                restore.world,
327                restore.payload.entity,
328                residence_token,
329            ) {
330                self.jobs.spawn(job);
331            } else {
332                player.discard_pending_ender_pearl(residence_token, pearl_uuid);
333            }
334        }
335    }
336
337    pub(super) fn root_vehicle_to_restore(
338        state: &DomainPlayerState,
339    ) -> Option<PersistentRootVehicle> {
340        match &state.data {
341            DomainPlayerData::SavedRestored { data } => data.root_vehicle.clone(),
342            DomainPlayerData::SavedWithoutLocation { .. } | DomainPlayerData::FirstVisit { .. } => {
343                None
344            }
345        }
346    }
347
348    fn ender_pearls_to_restore(state: &DomainPlayerState) -> Vec<PersistentEnderPearl> {
349        match &state.data {
350            DomainPlayerData::SavedRestored { data }
351            | DomainPlayerData::SavedWithoutLocation { data, .. } => data.ender_pearls.clone(),
352            DomainPlayerData::FirstVisit { .. } => Vec::new(),
353        }
354    }
355
356    pub(super) fn resolve_pearl_world(
357        &self,
358        world_key: &str,
359        expected_domain: &str,
360        player: &Player,
361    ) -> Option<Arc<World>> {
362        let Ok(key) = world_key.parse::<Identifier>() else {
363            log::warn!(
364                "Saved ender pearl world {world_key} for player {} is invalid, skipping",
365                player.gameprofile.name
366            );
367            return None;
368        };
369        let Some(world) = self.worlds.get(&key) else {
370            log::warn!(
371                "Saved ender pearl world {key} for player {} is missing, skipping",
372                player.gameprofile.name
373            );
374            return None;
375        };
376        if world.domain() != expected_domain {
377            log::warn!(
378                "Saved ender pearl world {key} belongs to domain {}, not player domain \
379                 {expected_domain}, skipping",
380                world.domain()
381            );
382            return None;
383        }
384        Some(world.clone())
385    }
386
387    pub(super) fn send_login_packet(&self, player: &Player, world: &World) {
388        let reduced_debug_info = world.get_game_rule(&REDUCED_DEBUG_INFO);
389        let immediate_respawn = world.get_game_rule(&IMMEDIATE_RESPAWN);
390        let do_limited_crafting = world.get_game_rule(&LIMITED_CRAFTING);
391
392        // Get world data
393        let hashed_seed = world.obfuscated_seed();
394
395        player.send_packet(CLogin {
396            player_id: player.id(),
397            hardcore: false,
398            levels: self.worlds.keys().cloned().collect(),
399            max_players: self.config.max_players as i32,
400            chunk_radius: self.config.view_distance.into(),
401            simulation_distance: self.config.simulation_distance.into(),
402            reduced_debug_info,
403            show_death_screen: !immediate_respawn,
404            do_limited_crafting,
405            common_player_spawn_info: CommonPlayerSpawnInfo {
406                dimension_type: world.dimension_type.id() as i32,
407                dimension: world.key.clone(),
408                seed: hashed_seed,
409                game_type: player.game_mode(),
410                previous_game_type: player.previous_game_mode(),
411                is_debug: false,
412                is_flat: world.is_flat,
413                last_death_location: None,
414                portal_cooldown: 0,
415                sea_level: world.sea_level,
416            },
417            online_mode: self.config.online_mode,
418            enforces_secure_chat: self.config.enforce_secure_chat,
419        });
420    }
421
422    /// Gets all the players on the server
423    pub fn get_players(&self) -> Vec<Arc<Player>> {
424        let mut players = vec![];
425        self.online_players.iter_players(|_, p: &Arc<Player>| {
426            players.push(p.clone());
427            true
428        });
429        players
430    }
431
432    /// Returns whether this exact player owns the online session for its UUID.
433    pub(crate) fn owns_online_player(&self, player: &Player) -> bool {
434        self.online_players
435            .get_by_uuid(&player.gameprofile.id)
436            .is_some_and(|online| ptr::eq(online.as_ref(), player))
437    }
438
439    /// Returns the world that owns this exact online player.
440    ///
441    /// The player's stored world pointer intentionally remains unchanged while
442    /// detached domain data is loading, so live membership must also be present
443    /// in that world's player index.
444    pub(crate) fn live_world_for_player(&self, player: &Player) -> Option<Arc<World>> {
445        if !self.owns_online_player(player) {
446            return None;
447        }
448
449        let world = player.get_world();
450        world.contains_player(player).then_some(world)
451    }
452
453    /// Returns the live world in which a player may participate in gameplay commands.
454    ///
455    /// A queued domain switch still has source-world membership until the safe
456    /// point, but gameplay work must stop as soon as that transition owns the
457    /// player. Finalization is live again and therefore remains available.
458    pub(crate) fn command_world_for_player(&self, player: &Player) -> Option<Arc<World>> {
459        if player.connection.closed() || player.domain_switch_blocks_gameplay() {
460            return None;
461        }
462        self.live_world_for_player(player)
463    }
464
465    /// Returns the total number of players currently online across all worlds.
466    #[must_use]
467    pub fn player_count(&self) -> usize {
468        self.online_players.len()
469    }
470
471    /// Returns a sample of up to 12 online players for the server list ping.
472    #[must_use]
473    pub fn player_sample(&self) -> Vec<(String, String)> {
474        const MAX_SAMPLE: usize = 12;
475
476        let players = self.get_players();
477        if players.is_empty() {
478            return vec![];
479        }
480
481        let sample_size = players.len().min(MAX_SAMPLE);
482        // Random starting offset into the player list
483        let offset = if players.len() > sample_size {
484            (rand::random::<u64>() as usize) % (players.len() - sample_size + 1)
485        } else {
486            0
487        };
488
489        let mut sample: Vec<(String, String)> = players[offset..offset + sample_size]
490            .iter()
491            .map(|p| {
492                (
493                    p.gameprofile.name.clone(),
494                    p.gameprofile.id.hyphenated().to_string(),
495                )
496            })
497            .collect();
498
499        // Shuffle using Fisher-Yates with random indices
500        for i in (1..sample.len()).rev() {
501            let j = (rand::random::<u64>() as usize) % (i + 1);
502            sample.swap(i, j);
503        }
504
505        sample
506    }
507
508    /// Returns the server default world or if not exists the first world.
509    /// # Panics
510    /// if no world exists on this server crisis is there!
511    pub fn overworld(&self) -> &Arc<World> {
512        self.worlds.server_default_world().unwrap_or_else(|| {
513            self.worlds
514                .values()
515                .next()
516                .expect("At least one world must exist")
517        })
518    }
519
520    /// Resolves the default respawn world and data for a domain.
521    pub fn respawn_world_and_data_for_domain(
522        &self,
523        domain: &str,
524    ) -> Result<(Arc<World>, RespawnData), String> {
525        let default_world = self
526            .worlds
527            .default_world(domain)
528            .cloned()
529            .ok_or_else(|| format!("domain {domain} has no default world"))?;
530        let respawn_data = {
531            let level_data = default_world.level_data.read();
532            level_data.data().respawn_data_or_local(&default_world.key)
533        };
534
535        let Some(target_world) = self
536            .worlds
537            .get(respawn_data.dimension())
538            .filter(|world| world.domain() == domain)
539            .cloned()
540        else {
541            let respawn_data = default_world
542                .world_border_adjusted_respawn_data(local_respawn_data_for_world(&default_world));
543            return Ok((default_world.clone(), respawn_data));
544        };
545
546        let respawn_data = target_world.world_border_adjusted_respawn_data(respawn_data);
547        Ok((target_world, respawn_data))
548    }
549
550    /// Returns the default respawn data sent to clients in the given domain.
551    pub fn respawn_data_for_domain(&self, domain: &str) -> Result<RespawnData, String> {
552        self.respawn_world_and_data_for_domain(domain)
553            .map(|(_, respawn_data)| respawn_data)
554    }
555
556    /// Sets the default respawn data for the respawn data's domain and broadcasts it.
557    pub fn set_respawn_data(&self, respawn_data: RespawnData) -> Result<(), String> {
558        let domain = respawn_data.dimension().namespace.as_ref();
559        let default_world = self
560            .worlds
561            .default_world(domain)
562            .cloned()
563            .ok_or_else(|| format!("domain {domain} has no default world"))?;
564        let target_world = self
565            .worlds
566            .get(respawn_data.dimension())
567            .filter(|world| world.domain() == domain)
568            .cloned()
569            .ok_or_else(|| {
570                format!(
571                    "respawn dimension {} is not loaded in domain {domain}",
572                    respawn_data.dimension()
573                )
574            })?;
575
576        if Arc::ptr_eq(&default_world, &target_world) {
577            let mut level_data = default_world.level_data.write();
578            let data = level_data.data_mut();
579            data.set_spawn_pos(respawn_data.pos());
580            data.spawn.angle = respawn_data.yaw;
581            data.set_respawn_data(respawn_data.clone());
582        } else {
583            default_world
584                .level_data
585                .write()
586                .data_mut()
587                .set_respawn_data(respawn_data.clone());
588
589            let mut level_data = target_world.level_data.write();
590            let data = level_data.data_mut();
591            data.set_spawn_pos(respawn_data.pos());
592            data.spawn.angle = respawn_data.yaw;
593        }
594
595        let packet = CSetDefaultSpawnPosition {
596            global_pos: respawn_data.global_pos.clone(),
597            yaw: respawn_data.yaw,
598            pitch: respawn_data.pitch,
599        };
600        for world in self
601            .worlds
602            .values()
603            .filter(|world| world.domain() == domain)
604        {
605            world.broadcast_to_all(packet.clone());
606        }
607
608        Ok(())
609    }
610
611    /// Returns the default domain's conventional nether world, if present.
612    pub fn nether(&self) -> Option<&Arc<World>> {
613        let key = Identifier::new(self.worlds.default_domain().to_owned(), "the_nether");
614        self.worlds.get(&key)
615    }
616
617    /// Returns the default domain's conventional end world, if present.
618    pub fn the_end(&self) -> Option<&Arc<World>> {
619        let key = Identifier::new(self.worlds.default_domain().to_owned(), "the_end");
620        self.worlds.get(&key)
621    }
622}