Skip to main content

steel_core/world/
world_entities.rs

1//! This module contains the implementation of the world's entity-related methods.
2use std::{ptr, sync::Arc};
3
4use steel_protocol::packets::game::{CGameEvent, GameEventType};
5use steel_registry::{vanilla_custom_stats, vanilla_entities};
6use steel_utils::ChunkPos;
7
8use crate::{
9    entity::{
10        AddEntityError, Entity, EntityOwnership, LivingEntity, NullEntityCallback,
11        PlayerEntityCallback, RemovalReason, SharedEntity,
12    },
13    player::connection::NetworkConnection,
14    player::player_data::PersistentPlayerData,
15    player::player_inventory::MenuRemovalStatus,
16    player::{DomainResidenceToken, Player, ResetReason},
17    world::World,
18};
19
20impl World {
21    /// Returns whether this exact player is registered in this world.
22    #[must_use]
23    pub(crate) fn contains_player(&self, player: &Player) -> bool {
24        self.players
25            .get_by_entity_id(player.id())
26            .is_some_and(|registered| ptr::eq(registered.as_ref(), player))
27    }
28
29    fn loaded_world_memberships(player: &Arc<Player>) -> Vec<Arc<World>> {
30        player.server.upgrade().map_or_else(Vec::new, |server| {
31            server
32                .worlds
33                .values()
34                .filter(|world| world.contains_player(player))
35                .cloned()
36                .collect()
37        })
38    }
39
40    fn reject_duplicate_player_membership(
41        player: &Arc<Player>,
42        target_world: &Arc<World>,
43        operation: &str,
44    ) -> bool {
45        let mut memberships = Self::loaded_world_memberships(player);
46        if target_world.contains_player(player)
47            && !memberships
48                .iter()
49                .any(|world| Arc::ptr_eq(world, target_world))
50        {
51            memberships.push(Arc::clone(target_world));
52        }
53        if memberships.is_empty() {
54            return false;
55        }
56
57        tracing::error!(
58            player = %player.gameprofile.name,
59            target_world = %target_world.key,
60            membership_count = memberships.len(),
61            operation,
62            "Refusing to register a player that already belongs to a loaded world"
63        );
64        player.connection.close();
65        for world in memberships {
66            world.remove_player_for_world_change(player);
67        }
68        true
69    }
70
71    fn take_player_for_removal(&self, player: &Arc<Player>) -> Option<Arc<Player>> {
72        if !self.contains_player(player) {
73            return None;
74        }
75        self.chunk_map.remove_player(player);
76        self.players.remove_player_sync(player)
77    }
78
79    fn attach_player_entity_callback(self: &Arc<Self>, player: &Arc<Player>) {
80        let callback = Arc::new(PlayerEntityCallback::new(player.id(), Arc::downgrade(self)));
81        player.set_level_callback(callback);
82    }
83
84    fn try_register_player_entity(
85        self: &Arc<Self>,
86        player: &Arc<Player>,
87    ) -> Result<(), AddEntityError> {
88        let entity: SharedEntity = Arc::<Player>::clone(player);
89        let lifecycle = self
90            .entity_manager()
91            .add_live_entity(entity, EntityOwnership::External)?;
92        self.attach_player_entity_callback(player);
93        self.apply_entity_lifecycle_changes(lifecycle);
94        Ok(())
95    }
96
97    fn register_player_entity(self: &Arc<Self>, player: &Arc<Player>) {
98        if let Err(error) = self.try_register_player_entity(player) {
99            panic!("failed to register player entity: {error}");
100        }
101    }
102
103    fn unride_player_for_removal(&self, player: &Player, store_root_vehicle: bool) {
104        for passenger in player.passengers() {
105            passenger.stop_riding();
106            self.mark_chunk_dirty(ChunkPos::from_entity_pos(passenger.position()));
107        }
108
109        if store_root_vehicle
110            && let Some(root_vehicle) = player.root_vehicle()
111            && root_vehicle.id() != player.id()
112            && root_vehicle.has_exactly_one_player_passenger()
113        {
114            Self::remove_root_vehicle_tree_stored_with_player(root_vehicle);
115            return;
116        }
117
118        if let Some(vehicle) = player.vehicle() {
119            player.stop_riding();
120            self.mark_chunk_dirty(ChunkPos::from_entity_pos(vehicle.position()));
121        }
122    }
123
124    fn remove_root_vehicle_tree_stored_with_player(entity: SharedEntity) {
125        let passengers = entity.passengers();
126        entity.set_removed(RemovalReason::StoredWithPlayer);
127
128        for passenger in passengers {
129            if passenger.entity_type() == &vanilla_entities::PLAYER {
130                continue;
131            }
132            Self::remove_root_vehicle_tree_stored_with_player(passenger);
133        }
134    }
135
136    pub(crate) fn unregister_player_entity(&self, player: &Player) {
137        let entity_id = player.id();
138        self.remove_entity_from_tracker(entity_id);
139
140        self.entity_manager()
141            .remove_live_entity(entity_id, RemovalReason::ChangedWorld);
142        player.set_level_callback(Arc::new(NullEntityCallback));
143    }
144
145    pub(crate) fn register_respawned_player_entity(self: &Arc<Self>, player: &Arc<Player>) {
146        self.register_player_entity(player);
147        self.chunk_map.update_player_status(player);
148    }
149
150    /// Installs a fresh player incarnation in this world's live indexes.
151    ///
152    /// Same-world respawns provide the exact detached old map occupant. Cross-world
153    /// and End-credits respawns insert into an empty target-world player map.
154    pub(crate) fn install_respawned_player(
155        self: &Arc<Self>,
156        player: Arc<Player>,
157        expected_old_player: Option<&Arc<Player>>,
158    ) -> bool {
159        let installed = if let Some(expected_old_player) = expected_old_player {
160            self.players
161                .replace_player(expected_old_player, Arc::clone(&player))
162        } else {
163            self.players.insert(Arc::clone(&player))
164        };
165        if !installed {
166            return false;
167        }
168
169        if let Err(error) = self.try_register_player_entity(&player) {
170            let rolled_back = if let Some(expected_old_player) = expected_old_player {
171                self.players
172                    .replace_player(&player, Arc::clone(expected_old_player))
173            } else {
174                self.players.remove_player_sync(&player).is_some()
175            };
176            if !rolled_back {
177                tracing::error!(
178                    player = %player.gameprofile.name,
179                    world = %self.key,
180                    "Respawn entity registration failed after world player ownership changed"
181                );
182            }
183            tracing::warn!(
184                player = %player.gameprofile.name,
185                world = %self.key,
186                "Refusing to install respawned player entity: {error}"
187            );
188            return false;
189        }
190
191        self.chunk_map.update_player_status(&player);
192        self.update_sleeping_player_list();
193        player.send_packet(CGameEvent {
194            event: GameEventType::LevelChunksLoadStart,
195            data: 0.0,
196        });
197        true
198    }
199
200    pub(crate) fn add_respawned_player(self: &Arc<Self>, player: Arc<Player>) -> bool {
201        self.install_respawned_player(player, None)
202    }
203
204    /// Detaches a disconnecting player from live world state and snapshots it.
205    ///
206    /// Persistence happens asynchronously after the server's pre-tick phase completes.
207    pub(crate) fn detach_player_for_disconnect(
208        self: &Arc<Self>,
209        player: Arc<Player>,
210    ) -> (Arc<Player>, String, PersistentPlayerData) {
211        assert_eq!(
212            player.remove_all_menus(),
213            MenuRemovalStatus::Complete,
214            "disconnect menu removal must run at the packet-processing safe point"
215        );
216
217        player.award_custom_stat(&vanilla_custom_stats::LEAVE_GAME);
218        let Some(player) = self.take_player_for_removal(&player) else {
219            // End credits and failed target admission deliberately have no live
220            // world membership but still need one authoritative disconnect save.
221            let domain = self.domain().to_owned();
222            let player_data = PersistentPlayerData::from_player(&player);
223            player.store_ender_pearls_with_player();
224            return (player, domain, player_data);
225        };
226        let domain = self.domain().to_owned();
227        if player.is_sleeping() {
228            player.stop_sleep_in_bed(true, false);
229        }
230        let player_data = PersistentPlayerData::from_player(&player);
231
232        self.unride_player_for_removal(&player, true);
233        player.store_ender_pearls_with_player();
234        self.unregister_player_entity(&player);
235
236        // Remove player from entity tracking (stop tracking all entities for this player)
237        self.entity_tracker().on_player_leave(&player);
238
239        self.player_area_map.on_player_leave(&player);
240        (player, domain, player_data)
241    }
242
243    /// Removes a player from the world during a world change.
244    ///
245    /// Unlike `remove_player`, this is synchronous and skips player data saving and tab list
246    /// removal — the player stays in the global tab list since they are only switching worlds.
247    pub(crate) fn remove_player_for_world_change(self: &Arc<Self>, player: &Arc<Player>) {
248        let Some(player) = self.take_player_for_removal(player) else {
249            return;
250        };
251        player.award_custom_stat(&vanilla_custom_stats::LEAVE_GAME);
252        self.unride_player_for_removal(&player, false);
253        self.unregister_player_entity(&player);
254        self.entity_tracker().on_player_leave(&player);
255        self.player_area_map.on_player_leave(&player);
256        // Note: no CRemovePlayerInfo — player stays in the global tab list
257    }
258
259    /// Detaches the exact old player incarnation for a fresh-player respawn.
260    ///
261    /// Same-world respawns retain the old player-map entry so installation can
262    /// compare-and-swap it with the fresh player. Cross-world respawns remove it.
263    /// This does not award `LEAVE_GAME` or snapshot persistent player data.
264    pub(crate) fn detach_player_for_respawn(
265        self: &Arc<Self>,
266        player: &Arc<Player>,
267        retain_player_map_entry: bool,
268    ) -> bool {
269        if !self.contains_player(player) {
270            return false;
271        }
272        if player.is_sleeping() {
273            player.stop_sleep_in_bed(true, false);
274        }
275
276        let detached_player = if retain_player_map_entry {
277            Arc::clone(player)
278        } else {
279            let Some(detached_player) = self.players.remove_player_sync(player) else {
280                return false;
281            };
282            detached_player
283        };
284
285        self.chunk_map.remove_player(&detached_player);
286        self.unride_player_for_removal(&detached_player, false);
287        self.unregister_player_entity(&detached_player);
288        self.entity_tracker().on_player_leave(&detached_player);
289        self.player_area_map.on_player_leave(&detached_player);
290        self.update_sleeping_player_list();
291        true
292    }
293
294    /// Removes an exact fresh respawn installation during transaction cleanup.
295    ///
296    /// A stale cleanup request cannot detach a newer player incarnation with the
297    /// same UUID and numeric entity ID.
298    pub(crate) fn remove_respawned_player(self: &Arc<Self>, player: &Arc<Player>) -> bool {
299        self.detach_player_for_respawn(player, false)
300    }
301
302    /// Detaches a player for a domain switch and returns its persistence snapshot.
303    pub(crate) fn detach_player_for_domain_switch(
304        self: &Arc<Self>,
305        player: &Arc<Player>,
306    ) -> Option<(PersistentPlayerData, DomainResidenceToken)> {
307        let player = self.take_player_for_removal(player)?;
308
309        player.award_custom_stat(&vanilla_custom_stats::LEAVE_GAME);
310        let player_data = PersistentPlayerData::from_player(&player);
311
312        self.unride_player_for_removal(&player, true);
313        player.store_ender_pearls_with_player();
314        self.unregister_player_entity(&player);
315        self.entity_tracker().on_player_leave(&player);
316        self.player_area_map.on_player_leave(&player);
317        let residence_token = player.advance_domain_residence();
318        Some((player_data, residence_token))
319    }
320
321    /// Adds a player to the world.
322    ///
323    /// On `InitialJoin`, sends full tab list + entity spawn synchronization to/from all
324    /// players. On `WorldChange`, this is skipped — the player already exists in all
325    /// clients' tab lists and the entity tracker handles spawning as chunks load.
326    #[must_use]
327    pub(crate) fn add_player(self: &Arc<Self>, player: Arc<Player>, _reason: ResetReason) -> bool {
328        if Self::reject_duplicate_player_membership(&player, self, "world change") {
329            return false;
330        }
331        if !self.players.insert(player.clone()) {
332            player.connection.close();
333            return false;
334        }
335
336        self.register_player_entity(&player);
337        self.chunk_map.update_player_status(&player);
338        self.update_sleeping_player_list();
339
340        player.send_packet(CGameEvent {
341            event: GameEventType::LevelChunksLoadStart,
342            data: 0.0,
343        });
344
345        true
346    }
347}