Skip to main content

steel_core/player/lifecycle/
spawn_sync.rs

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