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_entities;
6use steel_utils::ChunkPos;
7
8use crate::{
9    entity::{
10        Entity, EntityOwnership, NullEntityCallback, PlayerEntityCallback, RemovalReason,
11        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 register_player_entity(self: &Arc<Self>, player: &Arc<Player>) {
85        self.attach_player_entity_callback(player);
86
87        let entity: SharedEntity = player.clone();
88        let lifecycle = match self
89            .entity_manager()
90            .add_live_entity(entity.clone(), EntityOwnership::External)
91        {
92            Ok(lifecycle) => lifecycle,
93            Err(error) => panic!("failed to register player entity: {error}"),
94        };
95        self.apply_entity_lifecycle_changes(lifecycle);
96    }
97
98    fn unride_player_for_removal(&self, player: &Player, store_root_vehicle: bool) {
99        for passenger in player.passengers() {
100            passenger.stop_riding();
101            self.mark_chunk_dirty(ChunkPos::from_entity_pos(passenger.position()));
102        }
103
104        if store_root_vehicle
105            && let Some(root_vehicle) = player.root_vehicle()
106            && root_vehicle.id() != player.id()
107            && root_vehicle.has_exactly_one_player_passenger()
108        {
109            Self::remove_root_vehicle_tree_stored_with_player(root_vehicle);
110            return;
111        }
112
113        if let Some(vehicle) = player.vehicle() {
114            player.stop_riding();
115            self.mark_chunk_dirty(ChunkPos::from_entity_pos(vehicle.position()));
116        }
117    }
118
119    fn remove_root_vehicle_tree_stored_with_player(entity: SharedEntity) {
120        let passengers = entity.passengers();
121        entity.set_removed(RemovalReason::StoredWithPlayer);
122
123        for passenger in passengers {
124            if passenger.entity_type() == &vanilla_entities::PLAYER {
125                continue;
126            }
127            Self::remove_root_vehicle_tree_stored_with_player(passenger);
128        }
129    }
130
131    pub(crate) fn unregister_player_entity(&self, player: &Player) {
132        let entity_id = player.id();
133        self.remove_entity_from_tracker(entity_id);
134
135        self.entity_manager()
136            .remove_live_entity(entity_id, RemovalReason::ChangedWorld);
137        player.set_level_callback(Arc::new(NullEntityCallback));
138    }
139
140    pub(crate) fn register_respawned_player_entity(self: &Arc<Self>, player: &Arc<Player>) {
141        self.register_player_entity(player);
142        self.chunk_map.update_player_status(player);
143    }
144
145    pub(crate) fn add_respawned_player(self: &Arc<Self>, player: Arc<Player>) -> bool {
146        if Self::reject_duplicate_player_membership(&player, self, "respawn") {
147            return false;
148        }
149        if !self.players.insert(player.clone()) {
150            player.connection.close();
151            return false;
152        }
153
154        self.register_respawned_player_entity(&player);
155        player.send_packet(CGameEvent {
156            event: GameEventType::LevelChunksLoadStart,
157            data: 0.0,
158        });
159        true
160    }
161
162    /// Detaches a disconnecting player from live world state and snapshots it.
163    ///
164    /// Persistence happens asynchronously after the server's pre-tick phase completes.
165    pub(crate) fn detach_player_for_disconnect(
166        self: &Arc<Self>,
167        player: Arc<Player>,
168    ) -> (Arc<Player>, String, PersistentPlayerData) {
169        assert_eq!(
170            player.remove_all_menus(),
171            MenuRemovalStatus::Complete,
172            "disconnect menu removal must run at the packet-processing safe point"
173        );
174
175        let Some(player) = self.take_player_for_removal(&player) else {
176            // End credits and failed target admission deliberately have no live
177            // world membership but still need one authoritative disconnect save.
178            let domain = self.domain().to_owned();
179            let player_data = PersistentPlayerData::from_player(&player);
180            player.store_ender_pearls_with_player();
181            return (player, domain, player_data);
182        };
183        let entity_id = player.id();
184        let domain = self.domain().to_owned();
185        let player_data = PersistentPlayerData::from_player(&player);
186
187        self.unride_player_for_removal(&player, true);
188        player.store_ender_pearls_with_player();
189        self.unregister_player_entity(&player);
190
191        // Remove player from entity tracking (stop tracking all entities for this player)
192        self.entity_tracker().on_player_leave(entity_id);
193
194        self.player_area_map.on_player_leave(&player);
195
196        (player, domain, player_data)
197    }
198
199    /// Removes a player from the world during a world change.
200    ///
201    /// Unlike `remove_player`, this is synchronous and skips player data saving and tab list
202    /// removal — the player stays in the global tab list since they are only switching worlds.
203    pub(crate) fn remove_player_for_world_change(self: &Arc<Self>, player: &Arc<Player>) {
204        let Some(player) = self.take_player_for_removal(player) else {
205            return;
206        };
207        let entity_id = player.id();
208
209        self.unride_player_for_removal(&player, false);
210        self.unregister_player_entity(&player);
211        self.entity_tracker().on_player_leave(entity_id);
212        self.player_area_map.on_player_leave(&player);
213        // Note: no CRemovePlayerInfo — player stays in the global tab list
214    }
215
216    /// Detaches a player for a domain switch and returns its persistence snapshot.
217    pub(crate) fn detach_player_for_domain_switch(
218        self: &Arc<Self>,
219        player: &Arc<Player>,
220    ) -> Option<(PersistentPlayerData, DomainResidenceToken)> {
221        let player = self.take_player_for_removal(player)?;
222        let entity_id = player.id();
223        let player_data = PersistentPlayerData::from_player(&player);
224
225        self.unride_player_for_removal(&player, true);
226        player.store_ender_pearls_with_player();
227        self.unregister_player_entity(&player);
228        self.entity_tracker().on_player_leave(entity_id);
229        self.player_area_map.on_player_leave(&player);
230        let residence_token = player.advance_domain_residence();
231        Some((player_data, residence_token))
232    }
233
234    /// Adds a player to the world.
235    ///
236    /// On `InitialJoin`, sends full tab list + entity spawn synchronization to/from all
237    /// players. On `WorldChange`, this is skipped — the player already exists in all
238    /// clients' tab lists and the entity tracker handles spawning as chunks load.
239    #[must_use]
240    pub(crate) fn add_player(self: &Arc<Self>, player: Arc<Player>, _reason: ResetReason) -> bool {
241        if Self::reject_duplicate_player_membership(&player, self, "world change") {
242            return false;
243        }
244        if !self.players.insert(player.clone()) {
245            player.connection.close();
246            return false;
247        }
248
249        self.register_player_entity(&player);
250        self.chunk_map.update_player_status(&player);
251
252        player.send_packet(CGameEvent {
253            event: GameEventType::LevelChunksLoadStart,
254            data: 0.0,
255        });
256
257        player.send_packet(CGameEvent {
258            event: GameEventType::ChangeGameMode,
259            data: player.game_mode().into(),
260        });
261
262        true
263    }
264}