Skip to main content

steel_core/player/lifecycle/
respawn.rs

1use std::ptr;
2
3use super::*;
4use crate::entity::PendingWorldChangeToken;
5use crate::player::connection::NetworkConnection as _;
6use crate::player::player_data::PersistentPlayerData;
7
8#[derive(Clone, Copy)]
9struct DeathRespawnSpawn {
10    position: DVec3,
11    rotation: (f32, f32),
12}
13
14struct PlayerRespawnJob {
15    player: Arc<Player>,
16    source_world: Arc<World>,
17    target_world: Arc<World>,
18    rotation: (f32, f32),
19    kind: RespawnRequestKind,
20    pending_token: PendingWorldChangeToken,
21    phase: PlayerRespawnJobPhase,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25enum RespawnRequestKind {
26    Death,
27    EndCredits,
28}
29
30enum PlayerRespawnJobPhase {
31    Searching(PlayerSpawnSearch),
32    LoadingSpawnChunks {
33        spawn: DeathRespawnSpawn,
34        request: ChunkRequestHandle,
35    },
36}
37
38impl PlayerRespawnJob {
39    fn new(
40        player: Arc<Player>,
41        source_world: Arc<World>,
42        target_world: Arc<World>,
43        respawn_data: RespawnData,
44        kind: RespawnRequestKind,
45        pending_token: PendingWorldChangeToken,
46    ) -> Result<Self, String> {
47        let search = PlayerSpawnSearch::new(
48            &target_world,
49            respawn_data.pos(),
50            target_world.default_gamemode,
51        )?;
52        Ok(Self {
53            player,
54            source_world,
55            target_world,
56            rotation: (respawn_data.yaw, respawn_data.pitch),
57            kind,
58            pending_token,
59            phase: PlayerRespawnJobPhase::Searching(search),
60        })
61    }
62
63    fn still_valid(&self) -> bool {
64        !self.player.connection.closed()
65            && self
66                .player
67                .is_respawn_transition_pending(self.pending_token)
68            && match self.kind {
69                RespawnRequestKind::Death => {
70                    self.source_world.contains_player(&self.player)
71                        && Player::should_process_respawn(self.player.get_health())
72                }
73                RespawnRequestKind::EndCredits => {
74                    Arc::ptr_eq(&self.player.get_world(), &self.source_world)
75                        && self.player.has_won_game()
76                }
77            }
78    }
79
80    fn finish_pending(&self) {
81        self.player.finish_respawn_request(self.pending_token);
82    }
83}
84
85impl ServerJob for PlayerRespawnJob {
86    fn poll(&mut self, _context: &mut ServerJobContext) -> JobPoll {
87        if !self.still_valid() {
88            self.finish_pending();
89            return JobPoll::Finished;
90        }
91
92        loop {
93            match &mut self.phase {
94                PlayerRespawnJobPhase::Searching(search) => {
95                    match search.poll_with_ready_candidate_budget(
96                        &self.target_world,
97                        RESPAWN_SEARCH_READY_CANDIDATE_BUDGET,
98                    ) {
99                        PlayerSpawnSearchPoll::Pending => return JobPoll::Pending,
100                        PlayerSpawnSearchPoll::Cancelled => {
101                            self.finish_pending();
102                            return JobPoll::Finished;
103                        }
104                        PlayerSpawnSearchPoll::Ready(position) => {
105                            let spawn = DeathRespawnSpawn {
106                                position,
107                                rotation: self.rotation,
108                            };
109                            let request = self.target_world.request_player_spawn_chunks(position);
110                            self.phase =
111                                PlayerRespawnJobPhase::LoadingSpawnChunks { spawn, request };
112                        }
113                    }
114                }
115                PlayerRespawnJobPhase::LoadingSpawnChunks { spawn, request } => {
116                    match request.poll() {
117                        ChunkRequestState::Pending { .. } => return JobPoll::Pending,
118                        ChunkRequestState::Cancelled => {
119                            self.finish_pending();
120                            return JobPoll::Finished;
121                        }
122                        ChunkRequestState::Ready => {
123                            if request.ready_chunks().is_none() {
124                                return JobPoll::Pending;
125                            }
126
127                            match self.kind {
128                                RespawnRequestKind::Death => self.player.finish_death_respawn(
129                                    &self.source_world,
130                                    &self.target_world,
131                                    *spawn,
132                                    self.pending_token,
133                                ),
134                                RespawnRequestKind::EndCredits => {
135                                    self.player.finish_end_credits_respawn(
136                                        &self.source_world,
137                                        &self.target_world,
138                                        *spawn,
139                                        self.pending_token,
140                                    );
141                                }
142                            }
143                            return JobPoll::Finished;
144                        }
145                    }
146                }
147            }
148        }
149    }
150
151    fn cancel(&mut self) {
152        self.finish_pending();
153    }
154}
155
156impl Player {
157    fn begin_respawn_request(&self) -> Option<PendingWorldChangeToken> {
158        let token = self.base.begin_pending_player_respawn()?;
159        if self.begin_respawn_transition(token) {
160            return Some(token);
161        }
162
163        self.finish_pending_world_change(token);
164        None
165    }
166
167    fn finish_respawn_request(&self, token: PendingWorldChangeToken) {
168        self.finish_respawn_transition(token);
169        self.finish_pending_world_change(token);
170    }
171
172    /// TODO: personal respawn blocks/anchors and noRespawnBlockAvailable.
173    pub fn respawn(&self) {
174        let health = self.get_health();
175        if !Self::should_process_respawn(health) {
176            return;
177        }
178
179        let source_world = self.get_world();
180        let Some(player_arc) = source_world.players.get_by_entity_id(self.id()) else {
181            return;
182        };
183        if !ptr::eq(player_arc.as_ref(), self) {
184            return;
185        }
186        let Some(pending_token) = self.begin_respawn_request() else {
187            if self.is_world_change_pending() {
188                self.defer_death_respawn();
189            }
190            return;
191        };
192
193        let Some(server) = self.server.upgrade() else {
194            self.finish_respawn_request(pending_token);
195            log::error!(
196                "Failed to schedule respawn for player {}: server is gone",
197                self.gameprofile.name
198            );
199            return;
200        };
201        let (target_world, respawn_data) =
202            match server.respawn_world_and_data_for_domain(source_world.domain()) {
203                Ok(resolved) => resolved,
204                Err(error) => {
205                    self.finish_respawn_request(pending_token);
206                    log::error!(
207                        "Failed to schedule respawn for player {}: {error}",
208                        self.gameprofile.name
209                    );
210                    return;
211                }
212            };
213
214        match PlayerRespawnJob::new(
215            player_arc,
216            source_world,
217            target_world,
218            respawn_data,
219            RespawnRequestKind::Death,
220            pending_token,
221        ) {
222            Ok(job) => server.jobs.spawn(job),
223            Err(error) => {
224                self.finish_respawn_request(pending_token);
225                log::error!(
226                    "Failed to schedule respawn for player {}: {error}",
227                    self.gameprofile.name
228                );
229            }
230        }
231    }
232
233    fn finish_death_respawn(
234        self: &Arc<Self>,
235        source_world: &Arc<World>,
236        target_world: &Arc<World>,
237        spawn: DeathRespawnSpawn,
238        pending_token: PendingWorldChangeToken,
239    ) {
240        if self.connection.closed()
241            || !self.is_respawn_transition_pending(pending_token)
242            || !source_world.contains_player(self)
243            || !Self::should_process_respawn(self.get_health())
244        {
245            self.finish_respawn_request(pending_token);
246            return;
247        }
248
249        let was_removed = self.base.clear_removed();
250        self.reset_state_for_death_respawn_during_world_change(pending_token);
251
252        // TODO: personal respawn blocks/anchors and NO_RESPAWN_BLOCK_AVAILABLE.
253
254        if !was_removed && Arc::ptr_eq(source_world, target_world) {
255            source_world.unregister_player_entity(self);
256        }
257
258        // Shared reset (clears transient state, sends CRespawn)
259        self.reset(target_world.clone(), ResetReason::Respawn);
260
261        self.send_difficulty();
262
263        // Handle XP and score loss on death.
264        let loses_inventory =
265            !target_world.get_game_rule(&KEEP_INVENTORY) && self.game_mode() != GameType::Spectator;
266        {
267            let mut experience = self.experience.lock();
268            if loses_inventory {
269                // TODO: drop XP orbs (min(level * 7, 100))
270                experience.clear();
271            }
272            // Re-send XP to client after respawn regardless of keepInventory
273            experience.dirty = true;
274        }
275        if loses_inventory {
276            self.set_score(0);
277        }
278
279        // TODO: send mob effect packets once effects are implemented
280
281        // Shared spawn (teleport, abilities, weather, time, chunk tracking reset)
282        if self.spawn(spawn.position, spawn.rotation, ResetReason::Respawn) {
283            self.finish_respawn_request(pending_token);
284            return;
285        }
286
287        self.finish_failed_respawn(target_world, pending_token);
288    }
289
290    fn finish_end_credits_respawn(
291        self: &Arc<Self>,
292        source_world: &Arc<World>,
293        target_world: &Arc<World>,
294        spawn: DeathRespawnSpawn,
295        pending_token: PendingWorldChangeToken,
296    ) {
297        if self.connection.closed()
298            || !self.is_respawn_transition_pending(pending_token)
299            || !Arc::ptr_eq(&self.get_world(), source_world)
300            || !self.has_won_game()
301        {
302            self.finish_respawn_request(pending_token);
303            return;
304        }
305
306        self.set_won_game(false);
307        self.reset(target_world.clone(), ResetReason::EndCredits);
308        self.send_difficulty();
309        self.experience.lock().dirty = true;
310        if self.spawn(spawn.position, spawn.rotation, ResetReason::EndCredits) {
311            self.finish_respawn_request(pending_token);
312            return;
313        }
314
315        self.finish_failed_respawn(target_world, pending_token);
316    }
317
318    fn finish_failed_respawn(
319        self: &Arc<Self>,
320        target_world: &Arc<World>,
321        pending_token: PendingWorldChangeToken,
322    ) {
323        let Some(server) = self.server.upgrade() else {
324            self.finish_respawn_request(pending_token);
325            self.cleanup();
326            return;
327        };
328        let player_data = Arc::new(PersistentPlayerData::from_player(self));
329        server.queue_detached_player_disconnect(
330            Arc::clone(self),
331            target_world.domain().to_owned(),
332            player_data,
333            pending_token,
334        );
335    }
336
337    #[cfg(test)]
338    pub(in crate::player) fn reset_state_for_death_respawn(&self) {
339        self.reset_state_for_death_respawn_inner(None);
340    }
341
342    fn reset_state_for_death_respawn_during_world_change(
343        &self,
344        pending_token: PendingWorldChangeToken,
345    ) {
346        self.reset_state_for_death_respawn_inner(Some(pending_token));
347    }
348
349    fn reset_state_for_death_respawn_inner(&self, pending_token: Option<PendingWorldChangeToken>) {
350        assert_eq!(
351            self.remove_all_menus_with_disposition(MenuItemDisposition::Drop),
352            MenuRemovalStatus::Complete,
353            "death respawn menu cleanup must run outside a menu callback"
354        );
355        self.detach_relationships_for_respawn();
356
357        self.attributes().lock().remove_all_transient();
358        self.living_base.reset_for_player_respawn();
359        if let Some(pending_token) = pending_token {
360            self.base.reset_for_player_respawn_during_world_change(
361                Self::dimensions_for_pose(EntityPose::Standing),
362                pending_token,
363            );
364        } else {
365            self.base
366                .reset_for_player_respawn(Self::dimensions_for_pose(EntityPose::Standing));
367        }
368
369        self.set_health(self.get_max_health());
370        self.set_pose(EntityPose::Standing);
371        self.reset_entity_state();
372        self.sync_base_entity_data();
373        self.update_dirty_mob_effect_entity_data();
374
375        *self.food_data.lock() = FoodData::new();
376        *self.block_breaking.lock() = BlockBreakingManager::new();
377        *self.teleport_state.lock() = TeleportState::new();
378        *self.tick_state.lock() = PlayerTickState::new();
379        *self.last_item_in_main_hand.lock() = ItemStack::empty();
380        self.health_sync.lock().reset_for_respawn();
381        self.clear_pending_root_vehicle();
382        self.movement.lock().reset_last_known_client_movement();
383    }
384
385    fn detach_relationships_for_respawn(&self) {
386        for passenger in self.passengers() {
387            passenger.stop_riding();
388        }
389        self.stop_riding();
390        self.base.set_boarding_cooldown(0);
391    }
392
393    /// Handles client commands, requestStats and `RequestGameRuleValues` are still todo
394    pub fn handle_client_command(self: &Arc<Self>, action: ClientCommandAction) {
395        match action {
396            ClientCommandAction::PerformRespawn => {
397                if self.has_won_game() {
398                    self.respawn_after_end_credits();
399                } else {
400                    self.respawn();
401                }
402            }
403            ClientCommandAction::RequestStats | ClientCommandAction::RequestGameRuleValues => {
404                // TODO: implement stats
405            }
406        }
407    }
408
409    /// Vanilla accepts a client respawn request only when player health is dead-or-dying.
410    /// Steel's death-processed guard is not respawn authority.
411    #[must_use]
412    pub(in crate::player) const fn should_process_respawn(health: f32) -> bool {
413        health <= 0.0
414    }
415
416    /// Returns vanilla `ServerPlayer.seenCredits`.
417    #[must_use]
418    pub fn has_seen_credits(&self) -> bool {
419        *self.seen_credits.lock()
420    }
421
422    /// Sets vanilla `ServerPlayer.seenCredits`.
423    pub fn set_seen_credits(&self, seen_credits: bool) {
424        *self.seen_credits.lock() = seen_credits;
425    }
426
427    /// Returns vanilla `ServerPlayer.wonGame`.
428    #[must_use]
429    pub(crate) fn has_won_game(&self) -> bool {
430        *self.won_game.lock()
431    }
432
433    fn set_won_game(&self, won_game: bool) {
434        *self.won_game.lock() = won_game;
435    }
436
437    /// Starts the vanilla End credits flow.
438    pub(crate) fn show_end_credits(&self) {
439        if self.has_won_game() {
440            return;
441        }
442
443        let world = self.get_world();
444        let Some(player) = world.players.get_by_entity_id(self.id()) else {
445            return;
446        };
447        if !ptr::eq(player.as_ref(), self) {
448            return;
449        }
450        let Some(pending_token) = player.begin_pending_world_change() else {
451            return;
452        };
453
454        assert_eq!(
455            player.remove_all_menus(),
456            MenuRemovalStatus::Complete,
457            "End credits menu removal must run outside a menu callback"
458        );
459        world.remove_player_for_world_change(&player);
460        player.finish_pending_world_change(pending_token);
461        if world.contains_player(&player) {
462            return;
463        }
464
465        player.set_won_game(true);
466        player.send_packet(CGameEvent {
467            event: GameEventType::WinGame,
468            data: 0.0,
469        });
470        player.set_seen_credits(true);
471    }
472
473    fn respawn_after_end_credits(self: &Arc<Self>) {
474        if !self.has_won_game() {
475            return;
476        }
477
478        let source_world = self.get_world();
479        let Some(server) = self.server.upgrade() else {
480            log::error!(
481                "Failed to schedule End credits respawn for player {}: server is gone",
482                self.gameprofile.name
483            );
484            return;
485        };
486        if !server.owns_online_player(self) {
487            return;
488        }
489        let Some(pending_token) = self.begin_respawn_request() else {
490            return;
491        };
492        let (target_world, respawn_data) =
493            match server.respawn_world_and_data_for_domain(source_world.domain()) {
494                Ok(resolved) => resolved,
495                Err(error) => {
496                    self.finish_respawn_request(pending_token);
497                    log::error!(
498                        "Failed to schedule End credits respawn for player {}: {error}",
499                        self.gameprofile.name
500                    );
501                    return;
502                }
503            };
504
505        match PlayerRespawnJob::new(
506            Arc::clone(self),
507            source_world,
508            target_world,
509            respawn_data,
510            RespawnRequestKind::EndCredits,
511            pending_token,
512        ) {
513            Ok(job) => server.jobs.spawn(job),
514            Err(error) => {
515                self.finish_respawn_request(pending_token);
516                log::error!(
517                    "Failed to schedule End credits respawn for player {}: {error}",
518                    self.gameprofile.name
519                );
520            }
521        }
522    }
523}