1use std::ptr;
2
3use super::*;
4use crate::entity::PendingWorldChangeToken;
5use crate::player::connection::NetworkConnection as _;
6use crate::{
7 behavior::{
8 BlockStateBehaviorExt as _,
9 blocks::{BedBlock, RespawnAnchorBlock},
10 },
11 chunk::{
12 chunk_request::{ChunkRequest, ChunkTicketKind},
13 status::ChunkStatus,
14 },
15 player::Player,
16};
17use steel_math::{DEGREE_90, wrap_degrees};
18use steel_protocol::packets::game::{CSound, SoundSource};
19use steel_registry::blocks::{
20 block_state_ext::BlockStateExt as _, properties::BlockStateProperties,
21};
22use steel_registry::{sound_events, vanilla_blocks};
23
24const PERSONAL_RESPAWN_SEARCH_BLOCK_RADIUS: i32 = 3;
26
27#[derive(Debug, Clone, PartialEq)]
29pub struct PlayerRespawnConfig {
30 pub respawn_data: RespawnData,
32 pub forced: bool,
34}
35
36impl PlayerRespawnConfig {
37 #[must_use]
38 pub(crate) const fn new(respawn_data: RespawnData, forced: bool) -> Self {
39 Self {
40 respawn_data,
41 forced,
42 }
43 }
44
45 #[must_use]
46 pub(crate) fn is_same_position(&self, other: Option<&Self>) -> bool {
47 other.is_some_and(|other| self.respawn_data.global_pos == other.respawn_data.global_pos)
48 }
49}
50
51#[derive(Clone, Copy)]
52struct DeathRespawnSpawn {
53 position: DVec3,
54 rotation: (f32, f32),
55 anchor_deplete_sound_pos: Option<BlockPos>,
56 missing_respawn_block: bool,
57}
58
59struct PlayerRespawnJob {
60 player: Arc<Player>,
61 source_world: Arc<World>,
62 fallback_respawn: Option<(Arc<World>, RespawnData)>,
63 target_world: Arc<World>,
64 rotation: (f32, f32),
65 missing_respawn_block: bool,
66 kind: RespawnRequestKind,
67 pending_token: PendingWorldChangeToken,
68 packet_transition: Option<PlayerPacketTransition>,
69 phase: PlayerRespawnJobPhase,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73enum RespawnRequestKind {
74 Death,
75 EndCredits,
76}
77
78enum RespawnFinish {
79 Completed,
80 Aborted,
81 Failed,
82}
83
84enum PlayerRespawnJobPhase {
85 LoadingPersonalRespawnBlock {
86 config: PlayerRespawnConfig,
87 request: ChunkRequestHandle,
88 },
89 Searching(PlayerSpawnSearch),
90}
91
92impl PlayerRespawnJob {
93 fn new(
94 player: Arc<Player>,
95 source_world: Arc<World>,
96 fallback_world: Arc<World>,
97 fallback_respawn_data: RespawnData,
98 personal_respawn: Option<(Arc<World>, PlayerRespawnConfig)>,
99 kind: RespawnRequestKind,
100 pending_token: PendingWorldChangeToken,
101 ) -> Result<Self, String> {
102 let fallback_rotation = (fallback_respawn_data.yaw, fallback_respawn_data.pitch);
103 let (target_world, rotation, phase, fallback_respawn) =
104 if let Some((personal_world, config)) = personal_respawn {
105 let pos = config.respawn_data.pos();
106 let request = Self::request_personal_respawn_chunks(&personal_world, pos);
107 (
108 personal_world,
109 (config.respawn_data.yaw, config.respawn_data.pitch),
110 PlayerRespawnJobPhase::LoadingPersonalRespawnBlock { config, request },
111 Some((fallback_world, fallback_respawn_data)),
112 )
113 } else {
114 let fallback_search = PlayerSpawnSearch::new(
115 &fallback_world,
116 fallback_respawn_data.pos(),
117 fallback_world.default_gamemode,
118 )?;
119 (
120 fallback_world,
121 fallback_rotation,
122 PlayerRespawnJobPhase::Searching(fallback_search),
123 None,
124 )
125 };
126 Ok(Self {
127 player,
128 source_world,
129 fallback_respawn,
130 target_world,
131 rotation,
132 missing_respawn_block: false,
133 kind,
134 pending_token,
135 packet_transition: None,
136 phase,
137 })
138 }
139
140 fn request_personal_respawn_chunks(world: &Arc<World>, pos: BlockPos) -> ChunkRequestHandle {
141 world.chunk_map.request_chunks(ChunkRequest {
142 status: ChunkStatus::Full,
143 positions: Self::personal_respawn_chunk_positions(pos),
144 ticket_kind: ChunkTicketKind::PlayerSpawn,
145 })
146 }
147
148 fn personal_respawn_chunk_positions(pos: BlockPos) -> Vec<ChunkPos> {
149 let min = ChunkPos::from_block_pos(pos.offset(
150 -PERSONAL_RESPAWN_SEARCH_BLOCK_RADIUS,
151 0,
152 -PERSONAL_RESPAWN_SEARCH_BLOCK_RADIUS,
153 ));
154 let max = ChunkPos::from_block_pos(pos.offset(
155 PERSONAL_RESPAWN_SEARCH_BLOCK_RADIUS,
156 0,
157 PERSONAL_RESPAWN_SEARCH_BLOCK_RADIUS,
158 ));
159 let mut positions = Vec::new();
160 for x in min.0.x..=max.0.x {
161 for z in min.0.y..=max.0.y {
162 positions.push(ChunkPos::new(x, z));
163 }
164 }
165 positions
166 }
167
168 fn still_valid(&self) -> bool {
169 !self.player.connection.closed()
170 && self.player.session.is_current_player(&self.player)
171 && self
172 .player
173 .server
174 .upgrade()
175 .is_some_and(|server| server.owns_online_player(&self.player))
176 && self
177 .player
178 .is_respawn_transition_pending(self.pending_token)
179 && match self.kind {
180 RespawnRequestKind::Death => {
181 self.source_world.contains_player(&self.player)
182 && Player::should_process_respawn(self.player.get_health())
183 }
184 RespawnRequestKind::EndCredits => {
185 Arc::ptr_eq(&self.player.get_world(), &self.source_world)
186 && self.player.has_won_game()
187 }
188 }
189 }
190
191 fn finish_pending(&mut self) {
192 self.player.finish_respawn_request(self.pending_token);
193 self.finish_packet_transition();
194 }
195
196 fn finish_packet_transition(&mut self) {
197 let Some(transition) = self.packet_transition.take() else {
198 return;
199 };
200 let Some(server) = self.player.server.upgrade() else {
201 return;
202 };
203 let Some(current_player) = self.player.session.current_player() else {
204 server.discard_player_packets(&self.player);
205 return;
206 };
207 if current_player.connection.closed() || !server.owns_online_player(¤t_player) {
208 server.discard_player_packets(¤t_player);
209 return;
210 }
211 if !server.finish_player_packet_transition(transition) {
212 tracing::warn!(
213 player = %current_player.gameprofile.name,
214 "Player respawn packet transition was no longer current"
215 );
216 }
217 }
218
219 fn discard_packet_transition(&mut self) {
220 if self.packet_transition.take().is_some()
221 && let Some(server) = self.player.server.upgrade()
222 {
223 server.discard_player_packets(&self.player);
224 }
225 }
226}
227
228impl ServerJob for PlayerRespawnJob {
229 fn poll(&mut self, _context: &mut ServerJobContext) -> JobPoll {
230 if !self.still_valid() {
231 self.finish_pending();
232 return JobPoll::Finished;
233 }
234
235 loop {
236 match &mut self.phase {
237 PlayerRespawnJobPhase::LoadingPersonalRespawnBlock { config, request } => {
238 match request.poll() {
239 ChunkRequestState::Pending { .. } => return JobPoll::Pending,
240 ChunkRequestState::Cancelled => {
241 self.finish_pending();
242 return JobPoll::Finished;
243 }
244 ChunkRequestState::Ready if request.ready_chunks().is_none() => {
245 return JobPoll::Pending;
246 }
247 ChunkRequestState::Ready => {}
248 }
249 if let Some(spawn) = Self::resolve_personal_respawn(
250 &self.target_world,
251 &self.player,
252 config,
253 matches!(self.kind, RespawnRequestKind::Death),
254 ) {
255 self.finish(spawn);
256 return JobPoll::Finished;
257 }
258
259 let Some((fallback_world, fallback_respawn_data)) =
260 self.fallback_respawn.take()
261 else {
262 self.finish_pending();
263 return JobPoll::Finished;
264 };
265 let fallback_search = match PlayerSpawnSearch::new(
266 &fallback_world,
267 fallback_respawn_data.pos(),
268 fallback_world.default_gamemode,
269 ) {
270 Ok(search) => search,
271 Err(error) => {
272 log::error!(
273 "Failed to prepare fallback respawn for player {}: {error}",
274 self.player.gameprofile.name
275 );
276 self.finish_pending();
277 return JobPoll::Finished;
278 }
279 };
280 self.target_world = fallback_world;
281 self.rotation = (fallback_respawn_data.yaw, fallback_respawn_data.pitch);
282 self.missing_respawn_block = true;
283 self.phase = PlayerRespawnJobPhase::Searching(fallback_search);
284 }
285 PlayerRespawnJobPhase::Searching(search) => {
286 match search.poll_with_ready_candidate_budget(
287 &self.target_world,
288 RESPAWN_SEARCH_READY_CANDIDATE_BUDGET,
289 ) {
290 PlayerSpawnSearchPoll::Pending => return JobPoll::Pending,
291 PlayerSpawnSearchPoll::Cancelled => {
292 self.finish_pending();
293 return JobPoll::Finished;
294 }
295 PlayerSpawnSearchPoll::Ready(position) => {
296 let spawn = DeathRespawnSpawn {
297 position,
298 rotation: self.rotation,
299 anchor_deplete_sound_pos: None,
300 missing_respawn_block: self.missing_respawn_block,
301 };
302 self.finish(spawn);
303 return JobPoll::Finished;
304 }
305 }
306 }
307 }
308 }
309 }
310
311 fn cancel(&mut self) {
312 self.finish_pending();
313 }
314}
315
316impl PlayerRespawnJob {
317 fn finish(&mut self, spawn: DeathRespawnSpawn) {
318 let outcome = self.player.finish_respawn_replacement(
319 &self.source_world,
320 &self.target_world,
321 spawn,
322 self.pending_token,
323 self.kind,
324 );
325 self.player.finish_respawn_request(self.pending_token);
326
327 match outcome {
328 RespawnFinish::Completed => {
329 let removal_reason = match self.kind {
330 RespawnRequestKind::Death => RemovalReason::Killed,
331 RespawnRequestKind::EndCredits => RemovalReason::ChangedWorld,
332 };
333 self.player.set_removed(removal_reason);
334 self.finish_packet_transition();
335 }
336 RespawnFinish::Aborted => self.finish_packet_transition(),
337 RespawnFinish::Failed => {
338 self.discard_packet_transition();
339 }
340 }
341 }
342
343 fn resolve_personal_respawn(
344 world: &Arc<World>,
345 player: &Player,
346 config: &PlayerRespawnConfig,
347 consume_spawn_block: bool,
348 ) -> Option<DeathRespawnSpawn> {
349 let pos = config.respawn_data.pos();
350 let state = world.get_block_state(pos);
351
352 if RespawnAnchorBlock::can_use_for_respawn(world, pos, state, config.forced) {
353 let position = RespawnAnchorBlock::find_standup_position(world, player, pos)?;
354 if RespawnAnchorBlock::should_consume_charge_after_respawn(
355 config.forced,
356 consume_spawn_block,
357 true,
358 ) {
359 let _ = RespawnAnchorBlock::consume_charge(world, pos, state);
360 }
361 return Some(DeathRespawnSpawn {
362 position,
363 rotation: (calculate_respawn_look_at_yaw(position, pos), 0.0),
364 anchor_deplete_sound_pos: Some(pos),
365 missing_respawn_block: false,
366 });
367 }
368
369 if state.is_bed()
370 && Player::bed_rule_value_allows_in_world(
371 world,
372 world.dimension_type.bed_rule.can_set_spawn,
373 )
374 {
375 let facing = state.get_value(&BlockStateProperties::HORIZONTAL_FACING);
376 let position = BedBlock::find_standup_position_with_yaw(
377 world,
378 player,
379 facing,
380 pos,
381 config.respawn_data.yaw,
382 )?;
383 return Some(DeathRespawnSpawn {
384 position,
385 rotation: (calculate_respawn_look_at_yaw(position, pos), 0.0),
386 anchor_deplete_sound_pos: None,
387 missing_respawn_block: false,
388 });
389 }
390
391 if !config.forced {
392 return None;
393 }
394 let top_state = world.get_block_state(pos.above());
395 Self::resolve_forced_respawn_fallback(
396 pos,
397 state,
398 top_state,
399 (config.respawn_data.yaw, config.respawn_data.pitch),
400 )
401 }
402
403 fn resolve_forced_respawn_fallback(
404 pos: BlockPos,
405 state: BlockStateId,
406 top_state: BlockStateId,
407 rotation: (f32, f32),
408 ) -> Option<DeathRespawnSpawn> {
409 if !state.is_possible_to_respawn_in_this() || !top_state.is_possible_to_respawn_in_this() {
410 return None;
411 }
412 Some(DeathRespawnSpawn {
413 position: DVec3::new(
414 f64::from(pos.x()) + 0.5,
415 f64::from(pos.y()) + 0.1,
416 f64::from(pos.z()) + 0.5,
417 ),
418 rotation,
419 anchor_deplete_sound_pos: None,
420 missing_respawn_block: false,
421 })
422 }
423}
424
425fn calculate_respawn_look_at_yaw(position: DVec3, look_at_block_pos: BlockPos) -> f32 {
426 let bed_center = DVec3::new(
427 f64::from(look_at_block_pos.x()) + 0.5,
428 f64::from(look_at_block_pos.y()),
429 f64::from(look_at_block_pos.z()) + 0.5,
430 );
431 let look_direction = (bed_center - position).normalize_or_zero();
432 wrap_degrees(
433 (look_direction.z.atan2(look_direction.x).to_degrees() - f64::from(DEGREE_90)) as f32,
434 )
435}
436
437impl Player {
438 fn begin_respawn_request(&self) -> Option<PendingWorldChangeToken> {
439 let token = self.base.begin_pending_player_respawn()?;
440 if self.begin_respawn_transition(token) {
441 return Some(token);
442 }
443
444 self.finish_pending_world_change(token);
445 None
446 }
447
448 fn finish_respawn_request(&self, token: PendingWorldChangeToken) {
449 self.finish_respawn_transition(token);
450 self.finish_pending_world_change(token);
451 }
452
453 pub fn respawn(&self) {
455 let health = self.get_health();
456 if !Self::should_process_respawn(health) {
457 return;
458 }
459
460 let source_world = self.get_world();
461 let Some(player_arc) = source_world.players.get_by_entity_id(self.id()) else {
462 return;
463 };
464 if !ptr::eq(player_arc.as_ref(), self) {
465 return;
466 }
467 let Some(pending_token) = self.begin_respawn_request() else {
468 if self.is_world_change_pending() {
469 self.defer_death_respawn();
470 }
471 return;
472 };
473
474 let Some(server) = self.server.upgrade() else {
475 self.finish_respawn_request(pending_token);
476 log::error!(
477 "Failed to schedule respawn for player {}: server is gone",
478 self.gameprofile.name
479 );
480 return;
481 };
482 let (fallback_world, fallback_respawn_data) =
483 match server.respawn_world_and_data_for_domain(source_world.domain()) {
484 Ok(resolved) => resolved,
485 Err(error) => {
486 self.finish_respawn_request(pending_token);
487 log::error!(
488 "Failed to schedule respawn for player {}: {error}",
489 self.gameprofile.name
490 );
491 return;
492 }
493 };
494 let personal_respawn = self.personal_respawn(&server, &source_world);
495
496 let mut job = match PlayerRespawnJob::new(
497 Arc::clone(&player_arc),
498 source_world,
499 fallback_world,
500 fallback_respawn_data,
501 personal_respawn,
502 RespawnRequestKind::Death,
503 pending_token,
504 ) {
505 Ok(job) => job,
506 Err(error) => {
507 self.finish_respawn_request(pending_token);
508 log::error!(
509 "Failed to schedule respawn for player {}: {error}",
510 self.gameprofile.name
511 );
512 return;
513 }
514 };
515 let Some(packet_transition) = server.begin_player_packet_transition(&player_arc) else {
516 self.finish_respawn_request(pending_token);
517 return;
518 };
519 job.packet_transition = Some(packet_transition);
520 server.jobs.spawn(job);
521 }
522
523 fn personal_respawn(
524 &self,
525 server: &Server,
526 source_world: &World,
527 ) -> Option<(Arc<World>, PlayerRespawnConfig)> {
528 self.respawn_config().and_then(|config| {
529 server
530 .worlds
531 .get(config.respawn_data.dimension())
532 .filter(|world| world.domain() == source_world.domain())
533 .cloned()
534 .map(|world| (world, config))
535 })
536 }
537
538 #[expect(
539 clippy::too_many_lines,
540 reason = "keeping the respawn transaction linear makes commit and rollback order explicit"
541 )]
542 fn finish_respawn_replacement(
543 self: &Arc<Self>,
544 source_world: &Arc<World>,
545 target_world: &Arc<World>,
546 spawn: DeathRespawnSpawn,
547 pending_token: PendingWorldChangeToken,
548 kind: RespawnRequestKind,
549 ) -> RespawnFinish {
550 let Some(server) = self.server.upgrade() else {
551 return RespawnFinish::Aborted;
552 };
553 let valid_source = match kind {
554 RespawnRequestKind::Death => {
555 source_world.contains_player(self)
556 && Self::should_process_respawn(self.get_health())
557 }
558 RespawnRequestKind::EndCredits => {
559 !source_world.contains_player(self)
560 && Arc::ptr_eq(&self.get_world(), source_world)
561 && self.has_won_game()
562 }
563 };
564 if self.connection.closed()
565 || !self.is_respawn_transition_pending(pending_token)
566 || !self.session.is_current_player(self)
567 || !server.owns_online_player(self)
568 || !valid_source
569 {
570 return RespawnFinish::Aborted;
571 }
572
573 assert_eq!(
574 self.remove_all_menus_with_disposition(MenuItemDisposition::Drop),
575 MenuRemovalStatus::Complete,
576 "respawn menu cleanup must run outside a menu callback"
577 );
578 self.detach_relationships_for_respawn();
579
580 if kind == RespawnRequestKind::EndCredits {
581 self.set_won_game(false);
582 }
583
584 let restore_all = kind == RespawnRequestKind::EndCredits;
585 let transfer_inventory = restore_all
586 || target_world.get_game_rule(&KEEP_INVENTORY)
587 || self.game_mode() == GameType::Spectator;
588 let replacement = self.new_respawn_replacement(
589 Arc::clone(target_world),
590 restore_all,
591 transfer_inventory,
592 !spawn.missing_respawn_block,
593 );
594
595 let same_world = Arc::ptr_eq(source_world, target_world);
596 if kind == RespawnRequestKind::Death
597 && !source_world.detach_player_for_respawn(self, same_world)
598 {
599 self.connection.close();
600 return RespawnFinish::Failed;
601 }
602
603 replacement.handle_missing_respawn_block(spawn.missing_respawn_block);
604 let reason = match kind {
605 RespawnRequestKind::Death => ResetReason::Respawn,
606 RespawnRequestKind::EndCredits => ResetReason::EndCredits,
607 };
608 replacement.prepare_respawn_replacement(reason);
609 replacement.experience.lock().dirty = true;
610 replacement.synchronize_respawn_replacement(spawn.position, spawn.rotation);
611 replacement.send_experience_packet_if_dirty();
612
613 let expected_world_player =
614 (kind == RespawnRequestKind::Death && same_world).then_some(self);
615 if !target_world.install_respawned_player(Arc::clone(&replacement), expected_world_player) {
616 self.connection.close();
617 return RespawnFinish::Failed;
618 }
619
620 if !server.replace_online_player(self, Arc::clone(&replacement)) {
621 self.connection.close();
622 if !target_world.remove_respawned_player(&replacement) {
623 tracing::error!(
624 player = %self.gameprofile.name,
625 world = %target_world.key,
626 "Failed to remove a respawn replacement after online-player commit failed"
627 );
628 }
629 return RespawnFinish::Failed;
630 }
631
632 let _ = replacement.mark_joined_world();
633 if !self.session.replace_player(self, &replacement) {
634 self.connection.close();
635 if !server.rollback_respawn_online_player(&replacement, Arc::clone(self))
636 && server.remove_online_player_sync(&replacement).is_none()
637 {
638 tracing::error!(
639 player = %self.gameprofile.name,
640 "Failed to remove or roll back online-player ownership after respawn session bind failed"
641 );
642 }
643 if !target_world.remove_respawned_player(&replacement) {
644 tracing::error!(
645 player = %self.gameprofile.name,
646 world = %target_world.key,
647 "Failed to remove a respawn replacement after session bind failed"
648 );
649 }
650 return RespawnFinish::Failed;
651 }
652
653 self.rebind_ender_pearls_to(&replacement);
654
655 server.resend_player_context(&replacement);
656
657 if kind == RespawnRequestKind::Death
658 && let Some(pos) = spawn.anchor_deplete_sound_pos
659 && target_world.get_block_state(pos).get_block() == &vanilla_blocks::RESPAWN_ANCHOR
660 {
661 replacement.send_packet(CSound::new(
662 &sound_events::BLOCK_RESPAWN_ANCHOR_DEPLETE,
663 SoundSource::Blocks,
664 pos.0.as_dvec3(),
665 1.0,
666 1.0,
667 rand::random(),
668 ));
669 }
670
671 RespawnFinish::Completed
672 }
673
674 fn handle_missing_respawn_block(&self, missing_respawn_block: bool) {
675 if !missing_respawn_block {
676 return;
677 }
678 self.set_respawn_position(None, false);
679 self.send_packet(CGameEvent {
680 event: GameEventType::NoRespawnBlockAvailable,
681 data: 0.0,
682 });
683 }
684
685 fn detach_relationships_for_respawn(&self) {
686 for passenger in self.passengers() {
687 passenger.stop_riding();
688 }
689 self.stop_riding();
690 self.base.set_boarding_cooldown(0);
691 }
692
693 pub fn handle_client_command(self: &Arc<Self>, action: ClientCommandAction) {
695 self.reset_last_action_time();
696 match action {
697 ClientCommandAction::PerformRespawn => {
698 if self.has_won_game() {
699 self.respawn_after_end_credits();
700 } else {
701 self.respawn();
702 }
703 }
704 ClientCommandAction::RequestStats => self.send_stats(),
705 ClientCommandAction::RequestGameRuleValues => {
706 }
708 }
709 }
710
711 #[must_use]
714 pub(in crate::player) const fn should_process_respawn(health: f32) -> bool {
715 health <= 0.0
716 }
717
718 #[must_use]
720 pub fn has_seen_credits(&self) -> bool {
721 *self.seen_credits.lock()
722 }
723
724 pub fn set_seen_credits(&self, seen_credits: bool) {
726 *self.seen_credits.lock() = seen_credits;
727 }
728
729 #[must_use]
731 pub(crate) fn has_won_game(&self) -> bool {
732 *self.won_game.lock()
733 }
734
735 fn set_won_game(&self, won_game: bool) {
736 *self.won_game.lock() = won_game;
737 }
738
739 pub(crate) fn show_end_credits(&self) {
741 if self.has_won_game() {
742 return;
743 }
744
745 let world = self.get_world();
746 let Some(player) = world.players.get_by_entity_id(self.id()) else {
747 return;
748 };
749 if !ptr::eq(player.as_ref(), self) {
750 return;
751 }
752 let Some(pending_token) = player.begin_pending_world_change() else {
753 return;
754 };
755
756 assert_eq!(
757 player.remove_all_menus(),
758 MenuRemovalStatus::Complete,
759 "End credits menu removal must run outside a menu callback"
760 );
761 let detached = world.detach_player_for_respawn(&player, false);
762 player.finish_pending_world_change(pending_token);
763 if !detached {
764 return;
765 }
766
767 player.set_won_game(true);
768 player.send_packet(CGameEvent {
769 event: GameEventType::WinGame,
770 data: 0.0,
771 });
772 player.set_seen_credits(true);
773 }
774
775 fn respawn_after_end_credits(self: &Arc<Self>) {
776 if !self.has_won_game() {
777 return;
778 }
779
780 let source_world = self.get_world();
781 let Some(server) = self.server.upgrade() else {
782 log::error!(
783 "Failed to schedule End credits respawn for player {}: server is gone",
784 self.gameprofile.name
785 );
786 return;
787 };
788 if !server.owns_online_player(self) {
789 return;
790 }
791 let Some(pending_token) = self.begin_respawn_request() else {
792 return;
793 };
794 let (target_world, respawn_data) =
795 match server.respawn_world_and_data_for_domain(source_world.domain()) {
796 Ok(resolved) => resolved,
797 Err(error) => {
798 self.finish_respawn_request(pending_token);
799 log::error!(
800 "Failed to schedule End credits respawn for player {}: {error}",
801 self.gameprofile.name
802 );
803 return;
804 }
805 };
806 let personal_respawn = self.personal_respawn(&server, &source_world);
807
808 let mut job = match PlayerRespawnJob::new(
809 Arc::clone(self),
810 source_world,
811 target_world,
812 respawn_data,
813 personal_respawn,
814 RespawnRequestKind::EndCredits,
815 pending_token,
816 ) {
817 Ok(job) => job,
818 Err(error) => {
819 self.finish_respawn_request(pending_token);
820 log::error!(
821 "Failed to schedule End credits respawn for player {}: {error}",
822 self.gameprofile.name
823 );
824 return;
825 }
826 };
827 let Some(packet_transition) = server.begin_player_packet_transition(self) else {
828 self.finish_respawn_request(pending_token);
829 return;
830 };
831 job.packet_transition = Some(packet_transition);
832 server.jobs.spawn(job);
833 }
834}
835
836#[cfg(test)]
837mod tests {
838 use steel_utils::{BlockPos, ChunkPos};
839
840 use super::PlayerRespawnJob;
841
842 #[test]
843 fn personal_respawn_loads_only_chunks_touched_by_standup_search() {
844 assert_eq!(
845 PlayerRespawnJob::personal_respawn_chunk_positions(BlockPos::new(8, 64, 8)),
846 [ChunkPos::new(0, 0)]
847 );
848
849 assert_eq!(
850 PlayerRespawnJob::personal_respawn_chunk_positions(BlockPos::new(13, 64, 13)),
851 [
852 ChunkPos::new(0, 0),
853 ChunkPos::new(0, 1),
854 ChunkPos::new(1, 0),
855 ChunkPos::new(1, 1),
856 ]
857 );
858 }
859}