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 and world
12    /// changes that preserve the player incarnation. If the player is currently
13    /// in a different world, they are removed from the old world first.
14    pub(crate) fn reset(self: &Arc<Self>, new_world: Arc<World>, reason: ResetReason) {
15        self.reset_inner_after(new_world, reason, false, || {});
16    }
17
18    /// Resets a player already detached from its source domain and restores target-domain state.
19    pub(crate) fn reset_after_detached_domain_restore<F>(
20        self: &Arc<Self>,
21        new_world: Arc<World>,
22        restore_state: F,
23    ) where
24        F: FnOnce(),
25    {
26        self.reset_inner_after(new_world, ResetReason::WorldChange, true, || {
27            restore_state();
28            // Damage timestamps belong to the source domain's clock.
29            self.living_base.clear_last_damage_source();
30        });
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            self.send_respawn_packet(&new_world, reason);
81        }
82    }
83
84    /// Prepares a newly allocated player for a death or End-credits respawn.
85    pub(crate) fn prepare_respawn_replacement(&self, reason: ResetReason) {
86        debug_assert!(matches!(
87            reason,
88            ResetReason::Respawn | ResetReason::EndCredits
89        ));
90        self.set_client_loaded(false);
91        self.send_respawn_packet(&self.get_world(), reason);
92    }
93
94    fn send_respawn_packet(&self, world: &World, reason: ResetReason) {
95        // 0x01 = keep attributes, 0x02 = keep entity data
96        let data_kept = reason.respawn_data_kept();
97
98        self.send_packet(CRespawn {
99            dimension_type: world.dimension_type.id() as i32,
100            dimension_name: world.key.clone(),
101            hashed_seed: world.obfuscated_seed(),
102            gamemode: self.game_mode() as u8,
103            previous_gamemode: nullable_game_mode_id(self.previous_game_mode()),
104            is_debug: false,
105            is_flat: world.is_flat,
106            has_death_location: false,
107            death_dimension_name: None,
108            death_location: None,
109            portal_cooldown_ticks: self.portal_cooldown(),
110            sea_level: world.sea_level,
111            data_kept,
112        });
113    }
114
115    /// Spawns the player into their current world at the given position.
116    ///
117    /// This is the shared "enter world" path used by initial join, respawn, and
118    /// world change. Sends position sync, abilities, inventory, time, weather,
119    /// and adds the player to the world as appropriate for the given reason.
120    ///
121    /// # Panics
122    /// Panics if the `advance_time` gamerule is not a bool.
123    #[must_use]
124    pub(crate) fn spawn(
125        self: &Arc<Self>,
126        position: DVec3,
127        rotation: (f32, f32),
128        reason: ResetReason,
129    ) -> bool {
130        self.spawn_with_velocity(position, rotation, DVec3::ZERO, reason)
131    }
132
133    #[must_use]
134    pub(crate) fn spawn_with_velocity(
135        self: &Arc<Self>,
136        position: DVec3,
137        rotation: (f32, f32),
138        velocity: DVec3,
139        reason: ResetReason,
140    ) -> bool {
141        self.spawn_with_velocity_packet(
142            position,
143            rotation,
144            velocity,
145            reason,
146            position,
147            rotation,
148            velocity,
149            RelativeMovement::NONE,
150        )
151    }
152
153    #[must_use]
154    #[expect(
155        clippy::too_many_arguments,
156        reason = "packet-relative teleports must keep resolved and protocol values separate"
157    )]
158    pub(crate) fn spawn_with_velocity_packet(
159        self: &Arc<Self>,
160        position: DVec3,
161        rotation: (f32, f32),
162        velocity: DVec3,
163        reason: ResetReason,
164        packet_position: DVec3,
165        packet_rotation: (f32, f32),
166        packet_velocity: DVec3,
167        relatives: RelativeMovement,
168    ) -> bool {
169        let world = self.synchronize_spawn_with_velocity_packet(
170            position,
171            velocity,
172            rotation,
173            packet_position,
174            packet_velocity,
175            packet_rotation,
176            relatives,
177            true,
178        );
179
180        // Add to world / re-enter chunk tracking
181        match reason {
182            ResetReason::InitialJoin | ResetReason::WorldChange => {
183                if reason == ResetReason::WorldChange {
184                    log::info!(
185                        "Player {} changed world to {}",
186                        self.gameprofile.name,
187                        world.key
188                    );
189                }
190                world.add_player(self.clone(), reason)
191            }
192            ResetReason::Respawn | ResetReason::EndCredits => {
193                if world.players.get_by_entity_id(self.id()).is_none() {
194                    return world.add_respawned_player(self.clone());
195                }
196
197                // Same world — re-enter chunk tracking
198                world.chunk_map.remove_player(self);
199                world.player_area_map.remove_by_entity_id(self.id());
200                world.entity_tracker().on_player_leave(self);
201
202                self.send_packet(CGameEvent {
203                    event: GameEventType::LevelChunksLoadStart,
204                    data: 0.0,
205                });
206                world.register_respawned_player_entity(self);
207                true
208            }
209        }
210    }
211
212    /// Sends the spawn synchronization for a fresh respawn replacement without
213    /// inserting it into world indexes. The replacement transaction owns that step.
214    pub(crate) fn synchronize_respawn_replacement(
215        self: &Arc<Self>,
216        position: DVec3,
217        rotation: (f32, f32),
218    ) {
219        self.synchronize_spawn_with_velocity_packet(
220            position,
221            DVec3::ZERO,
222            rotation,
223            position,
224            DVec3::ZERO,
225            rotation,
226            RelativeMovement::NONE,
227            false,
228        );
229    }
230
231    #[expect(
232        clippy::too_many_arguments,
233        reason = "packet-relative teleports must keep resolved and protocol values separate"
234    )]
235    fn synchronize_spawn_with_velocity_packet(
236        self: &Arc<Self>,
237        position: DVec3,
238        velocity: DVec3,
239        rotation: (f32, f32),
240        packet_position: DVec3,
241        packet_velocity: DVec3,
242        packet_rotation: (f32, f32),
243        relatives: RelativeMovement,
244        resend_player_context: bool,
245    ) -> Arc<World> {
246        let world = self.get_world();
247
248        self.base.set_position_local(position);
249        self.set_rotation(rotation);
250        self.set_old_position_to_current();
251        self.movement.lock().reset_for_position_sync(position);
252
253        if let Err(error) = self.teleport_with_velocity_packet(
254            position,
255            velocity,
256            rotation,
257            packet_position,
258            packet_velocity,
259            packet_rotation,
260            relatives,
261        ) {
262            panic!(
263                "failed to synchronize player {} spawn position: {error}",
264                self.id()
265            );
266        }
267        self.reset_flying_ticks();
268        self.send_spawn_state_packets(&world);
269        self.reset_sent_info();
270        if resend_player_context {
271            self.server().resend_player_context(self);
272        }
273        self.send_active_effects_for_self();
274        world
275    }
276
277    fn send_spawn_state_packets(&self, world: &World) {
278        self.send_abilities();
279        self.send_packet(CSetHeldSlot {
280            slot: i32::from(self.inventory.lock().get_selected_slot()),
281        });
282        self.send_time_sync(world);
283        self.send_packet(world.initialize_border_packet());
284        self.send_default_spawn_position(world);
285        self.send_weather_sync(world);
286    }
287
288    fn send_time_sync(&self, world: &World) {
289        self.send_packet(world.time_sync_packet());
290    }
291
292    fn send_default_spawn_position(&self, world: &World) {
293        if let Some(server) = self.server.upgrade() {
294            match server.respawn_data_for_domain(world.domain()) {
295                Ok(respawn_data) => {
296                    self.send_packet(CSetDefaultSpawnPosition {
297                        global_pos: respawn_data.global_pos,
298                        yaw: respawn_data.yaw,
299                        pitch: respawn_data.pitch,
300                    });
301                }
302                Err(error) => {
303                    log::error!(
304                        "Failed to send default spawn position to player {}: {error}",
305                        self.gameprofile.name
306                    );
307                }
308            }
309        }
310    }
311
312    fn send_weather_sync(&self, world: &World) {
313        if !world.can_have_weather() || !world.is_raining() {
314            return;
315        }
316
317        let (rain_level, thunder_level) = {
318            let weather = world.weather.lock();
319            (weather.rain_level, weather.thunder_level)
320        };
321
322        self.send_packet(CGameEvent {
323            event: GameEventType::StartRaining,
324            data: 0.0,
325        });
326        self.send_packet(CGameEvent {
327            event: GameEventType::RainLevelChange,
328            data: rain_level,
329        });
330        self.send_packet(CGameEvent {
331            event: GameEventType::ThunderLevelChange,
332            data: thunder_level,
333        });
334    }
335
336    pub(in crate::player) fn passenger_ids_for_packet(entity: &dyn Entity) -> Vec<i32> {
337        entity
338            .passengers()
339            .iter()
340            .map(|passenger| passenger.id())
341            .collect()
342    }
343
344    pub(in crate::player) fn send_mob_effect_sync_packet(&self, packet: MobEffectSyncPacket) {
345        match packet {
346            MobEffectSyncPacket::Update(packet) => self.send_packet(packet),
347            MobEffectSyncPacket::Remove(packet) => self.send_packet(packet),
348        }
349    }
350
351    fn send_active_effects_for_self(&self) {
352        for effect in self.living_base.active_mob_effects() {
353            self.send_mob_effect_sync_packet(
354                MobEffectSyncChange::Update {
355                    effect,
356                    blend_for_self: false,
357                }
358                .packet(self.id(), true),
359            );
360        }
361    }
362
363    pub(in crate::player) fn send_active_effects_for_vehicle(&self, vehicle: &dyn Entity) {
364        let Some(living_vehicle) = vehicle.as_living_entity() else {
365            return;
366        };
367        for effect in living_vehicle.active_mob_effects() {
368            self.send_mob_effect_sync_packet(
369                MobEffectSyncChange::Update {
370                    effect,
371                    blend_for_self: false,
372                }
373                .packet(vehicle.id(), false),
374            );
375        }
376    }
377
378    pub(crate) fn send_restored_vehicle_mount_sync(&self, vehicle: &dyn Entity) {
379        self.send_active_effects_for_vehicle(vehicle);
380        self.send_packet(CSetPassengers::new(
381            vehicle.id(),
382            Self::passenger_ids_for_packet(vehicle),
383        ));
384    }
385
386    pub(in crate::player) fn remove_active_effects_for_vehicle(&self, vehicle: &dyn Entity) {
387        let Some(living_vehicle) = vehicle.as_living_entity() else {
388            return;
389        };
390        for effect in living_vehicle.active_mob_effects() {
391            self.send_mob_effect_sync_packet(
392                MobEffectSyncChange::Remove {
393                    effect: effect.effect(),
394                }
395                .packet(vehicle.id(), false),
396            );
397        }
398    }
399}
400
401pub(in crate::player) fn nullable_game_mode_id(game_mode: Option<GameType>) -> i8 {
402    game_mode.map_or(-1, |game_mode| game_mode as i8)
403}