Skip to main content

steel_core/player/lifecycle/
spawn_sync.rs

1use super::{
2    Arc, BlockBreakingManager, CContainerClose, CGameEvent, CRespawn, CSetDefaultSpawnPosition,
3    CSetHeldSlot, CSetPassengers, DVec3, Entity, GameEventType, GameType, MenuRemovalStatus,
4    MobEffectSyncChange, MobEffectSyncPacket, Player, RegistryEntry, RelativeMovement, ResetReason,
5    World,
6};
7
8impl Player {
9    /// Resets the player's transient state and prepares them for a new world.
10    ///
11    /// This is the shared "clean slate" path used by initial join, respawn, and
12    /// world change. If the player is currently in a different world, they are
13    /// removed from the old world first.
14    ///
15    /// Vanilla creates a fresh `ServerPlayer` for death and End-credits respawns,
16    /// but reuses it for dimension changes. Steel reuses the same `Player` for
17    /// every path, so this resets only the transient state appropriate to `reason`.
18    pub(crate) fn reset(self: &Arc<Self>, new_world: Arc<World>, reason: ResetReason) {
19        self.reset_inner_after(new_world, reason, false, || {});
20    }
21
22    /// Resets a player already detached from its source domain and restores target-domain state.
23    pub(crate) fn reset_after_detached_domain_restore<F>(
24        self: &Arc<Self>,
25        new_world: Arc<World>,
26        restore_state: F,
27    ) where
28        F: FnOnce(),
29    {
30        self.reset_inner_after(new_world, ResetReason::WorldChange, true, restore_state);
31    }
32
33    fn reset_inner_after<F>(
34        self: &Arc<Self>,
35        new_world: Arc<World>,
36        reason: ResetReason,
37        source_world_detached: bool,
38        restore_state: F,
39    ) where
40        F: FnOnce(),
41    {
42        if reason != ResetReason::InitialJoin {
43            assert_eq!(
44                self.remove_all_menus(),
45                MenuRemovalStatus::Complete,
46                "player reset menu removal must run outside a menu callback"
47            );
48        }
49        if matches!(reason, ResetReason::Respawn | ResetReason::EndCredits) {
50            // Vanilla creates a fresh ServerPlayer and inventory menu for these paths.
51            self.inventory_menu
52                .lock()
53                .behavior_mut()
54                .reset_quick_craft();
55        }
56
57        let old_world = self.get_world();
58        let switching_worlds = !Arc::ptr_eq(&old_world, &new_world);
59
60        if switching_worlds {
61            self.send_packet(CContainerClose { container_id: 0 });
62            if !source_world_detached {
63                old_world.remove_player_for_world_change(self);
64            }
65            self.set_world(new_world.clone());
66        } else if !source_world_detached {
67            old_world.chunk_map.remove_player(self);
68        }
69
70        self.set_client_loaded(false);
71        self.set_velocity(DVec3::ZERO);
72        self.movement.lock().reset_last_known_client_movement();
73        self.set_on_ground(false);
74        self.reset_entity_state();
75        *self.block_breaking.lock() = BlockBreakingManager::new();
76
77        restore_state();
78
79        if reason != ResetReason::InitialJoin {
80            // 0x01 = keep attributes, 0x02 = keep entity data
81            let data_kept = reason.respawn_data_kept();
82
83            self.send_packet(CRespawn {
84                dimension_type: new_world.dimension_type.id() as i32,
85                dimension_name: new_world.key.clone(),
86                hashed_seed: new_world.obfuscated_seed(),
87                gamemode: self.game_mode() as u8,
88                previous_gamemode: nullable_game_mode_id(self.previous_game_mode()),
89                is_debug: false,
90                is_flat: new_world.is_flat,
91                has_death_location: false,
92                death_dimension_name: None,
93                death_location: None,
94                portal_cooldown_ticks: self.portal_cooldown(),
95                sea_level: new_world.sea_level,
96                data_kept,
97            });
98        }
99    }
100
101    /// Spawns the player into their current world at the given position.
102    ///
103    /// This is the shared "enter world" path used by initial join, respawn, and
104    /// world change. Sends position sync, abilities, inventory, time, weather,
105    /// and adds the player to the world as appropriate for the given reason.
106    ///
107    /// # Panics
108    /// Panics if the `advance_time` gamerule is not a bool.
109    #[must_use]
110    pub(crate) fn spawn(
111        self: &Arc<Self>,
112        position: DVec3,
113        rotation: (f32, f32),
114        reason: ResetReason,
115    ) -> bool {
116        self.spawn_with_velocity(position, rotation, DVec3::ZERO, reason)
117    }
118
119    #[must_use]
120    pub(crate) fn spawn_with_velocity(
121        self: &Arc<Self>,
122        position: DVec3,
123        rotation: (f32, f32),
124        velocity: DVec3,
125        reason: ResetReason,
126    ) -> bool {
127        self.spawn_with_velocity_packet(
128            position,
129            rotation,
130            velocity,
131            reason,
132            position,
133            rotation,
134            velocity,
135            RelativeMovement::NONE,
136        )
137    }
138
139    #[must_use]
140    #[expect(
141        clippy::too_many_arguments,
142        reason = "packet-relative teleports must keep resolved and protocol values separate"
143    )]
144    pub(crate) fn spawn_with_velocity_packet(
145        self: &Arc<Self>,
146        position: DVec3,
147        rotation: (f32, f32),
148        velocity: DVec3,
149        reason: ResetReason,
150        packet_position: DVec3,
151        packet_rotation: (f32, f32),
152        packet_velocity: DVec3,
153        relatives: RelativeMovement,
154    ) -> bool {
155        let world = self.get_world();
156
157        // Set position and rotation
158        self.base.set_position_local(position);
159        self.set_rotation(rotation);
160        self.set_old_position_to_current();
161        self.movement.lock().reset_for_position_sync(position);
162
163        // Teleport sync (sends CPlayerPosition, sets awaiting_teleport for ack)
164        if let Err(error) = self.teleport_with_velocity_packet(
165            position,
166            velocity,
167            rotation,
168            packet_position,
169            packet_velocity,
170            packet_rotation,
171            relatives,
172        ) {
173            panic!(
174                "failed to synchronize player {} spawn position: {error}",
175                self.id()
176            );
177        }
178        self.reset_flying_ticks();
179
180        self.send_spawn_state_packets(&world);
181
182        // Force health/xp resync on next tick
183        self.reset_sent_info();
184
185        // Resend client context that is not fully covered by CLogin/CRespawn.
186        self.server().resend_player_context(self);
187        self.send_active_effects_for_self();
188
189        // Add to world / re-enter chunk tracking
190        match reason {
191            ResetReason::InitialJoin | ResetReason::WorldChange => {
192                if reason == ResetReason::WorldChange {
193                    log::info!(
194                        "Player {} changed world to {}",
195                        self.gameprofile.name,
196                        world.key
197                    );
198                }
199                world.add_player(self.clone(), reason)
200            }
201            ResetReason::Respawn | ResetReason::EndCredits => {
202                if world.players.get_by_entity_id(self.id()).is_none() {
203                    return world.add_respawned_player(self.clone());
204                }
205
206                // Same world — re-enter chunk tracking
207                world.chunk_map.remove_player(self);
208                world.player_area_map.remove_by_entity_id(self.id());
209                world.entity_tracker().on_player_leave(self.id());
210
211                self.send_packet(CGameEvent {
212                    event: GameEventType::LevelChunksLoadStart,
213                    data: 0.0,
214                });
215                world.register_respawned_player_entity(self);
216                true
217            }
218        }
219    }
220
221    fn send_spawn_state_packets(&self, world: &World) {
222        self.send_abilities();
223        self.send_packet(CSetHeldSlot {
224            slot: i32::from(self.inventory.lock().get_selected_slot()),
225        });
226        self.send_time_sync(world);
227        self.send_packet(world.initialize_border_packet());
228        self.send_default_spawn_position(world);
229        self.send_weather_sync(world);
230    }
231
232    fn send_time_sync(&self, world: &World) {
233        self.send_packet(world.time_sync_packet());
234    }
235
236    fn send_default_spawn_position(&self, world: &World) {
237        if let Some(server) = self.server.upgrade() {
238            match server.respawn_data_for_domain(world.domain()) {
239                Ok(respawn_data) => {
240                    self.send_packet(CSetDefaultSpawnPosition {
241                        global_pos: respawn_data.global_pos,
242                        yaw: respawn_data.yaw,
243                        pitch: respawn_data.pitch,
244                    });
245                }
246                Err(error) => {
247                    log::error!(
248                        "Failed to send default spawn position to player {}: {error}",
249                        self.gameprofile.name
250                    );
251                }
252            }
253        }
254    }
255
256    fn send_weather_sync(&self, world: &World) {
257        if !world.can_have_weather() || !world.is_raining() {
258            return;
259        }
260
261        let (rain_level, thunder_level) = {
262            let weather = world.weather.lock();
263            (weather.rain_level, weather.thunder_level)
264        };
265
266        self.send_packet(CGameEvent {
267            event: GameEventType::StartRaining,
268            data: 0.0,
269        });
270        self.send_packet(CGameEvent {
271            event: GameEventType::RainLevelChange,
272            data: rain_level,
273        });
274        self.send_packet(CGameEvent {
275            event: GameEventType::ThunderLevelChange,
276            data: thunder_level,
277        });
278    }
279
280    pub(in crate::player) fn passenger_ids_for_packet(entity: &dyn Entity) -> Vec<i32> {
281        entity
282            .passengers()
283            .iter()
284            .map(|passenger| passenger.id())
285            .collect()
286    }
287
288    pub(in crate::player) fn send_mob_effect_sync_packet(&self, packet: MobEffectSyncPacket) {
289        match packet {
290            MobEffectSyncPacket::Update(packet) => self.send_packet(packet),
291            MobEffectSyncPacket::Remove(packet) => self.send_packet(packet),
292        }
293    }
294
295    fn send_active_effects_for_self(&self) {
296        for effect in self.living_base.active_mob_effects() {
297            self.send_mob_effect_sync_packet(
298                MobEffectSyncChange::Update {
299                    effect,
300                    blend_for_self: false,
301                }
302                .packet(self.id(), true),
303            );
304        }
305    }
306
307    pub(in crate::player) fn send_active_effects_for_vehicle(&self, vehicle: &dyn Entity) {
308        let Some(living_vehicle) = vehicle.as_living_entity() else {
309            return;
310        };
311        for effect in living_vehicle.active_mob_effects() {
312            self.send_mob_effect_sync_packet(
313                MobEffectSyncChange::Update {
314                    effect,
315                    blend_for_self: false,
316                }
317                .packet(vehicle.id(), false),
318            );
319        }
320    }
321
322    pub(crate) fn send_restored_vehicle_mount_sync(&self, vehicle: &dyn Entity) {
323        self.send_active_effects_for_vehicle(vehicle);
324        self.send_packet(CSetPassengers::new(
325            vehicle.id(),
326            Self::passenger_ids_for_packet(vehicle),
327        ));
328    }
329
330    pub(in crate::player) fn remove_active_effects_for_vehicle(&self, vehicle: &dyn Entity) {
331        let Some(living_vehicle) = vehicle.as_living_entity() else {
332            return;
333        };
334        for effect in living_vehicle.active_mob_effects() {
335            self.send_mob_effect_sync_packet(
336                MobEffectSyncChange::Remove {
337                    effect: effect.effect(),
338                }
339                .packet(vehicle.id(), false),
340            );
341        }
342    }
343}
344
345pub(in crate::player) fn nullable_game_mode_id(game_mode: Option<GameType>) -> i8 {
346    game_mode.map_or(-1, |game_mode| game_mode as i8)
347}