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_time_budget(&self.target_world, POLL_TIME_BUDGET) {
287 PlayerSpawnSearchPoll::Pending => return JobPoll::Pending,
288 PlayerSpawnSearchPoll::Cancelled => {
289 self.finish_pending();
290 return JobPoll::Finished;
291 }
292 PlayerSpawnSearchPoll::Ready(position) => {
293 let spawn = DeathRespawnSpawn {
294 position,
295 rotation: self.rotation,
296 anchor_deplete_sound_pos: None,
297 missing_respawn_block: self.missing_respawn_block,
298 };
299 self.finish(spawn);
300 return JobPoll::Finished;
301 }
302 }
303 }
304 }
305 }
306 }
307
308 fn cancel(&mut self) {
309 self.finish_pending();
310 }
311}
312
313impl PlayerRespawnJob {
314 fn finish(&mut self, spawn: DeathRespawnSpawn) {
315 let outcome = self.player.finish_respawn_replacement(
316 &self.source_world,
317 &self.target_world,
318 spawn,
319 self.pending_token,
320 self.kind,
321 );
322 self.player.finish_respawn_request(self.pending_token);
323
324 match outcome {
325 RespawnFinish::Completed => {
326 let removal_reason = match self.kind {
327 RespawnRequestKind::Death => RemovalReason::Killed,
328 RespawnRequestKind::EndCredits => RemovalReason::ChangedWorld,
329 };
330 self.player.set_removed(removal_reason);
331 self.finish_packet_transition();
332 }
333 RespawnFinish::Aborted => self.finish_packet_transition(),
334 RespawnFinish::Failed => {
335 self.discard_packet_transition();
336 }
337 }
338 }
339
340 fn resolve_personal_respawn(
341 world: &Arc<World>,
342 player: &Player,
343 config: &PlayerRespawnConfig,
344 consume_spawn_block: bool,
345 ) -> Option<DeathRespawnSpawn> {
346 let pos = config.respawn_data.pos();
347 let state = world.get_block_state(pos);
348
349 if RespawnAnchorBlock::can_use_for_respawn(world, pos, state, config.forced) {
350 let position = RespawnAnchorBlock::find_standup_position(world, player, pos)?;
351 if RespawnAnchorBlock::should_consume_charge_after_respawn(
352 config.forced,
353 consume_spawn_block,
354 true,
355 ) {
356 let _ = RespawnAnchorBlock::consume_charge(world, pos, state);
357 }
358 return Some(DeathRespawnSpawn {
359 position,
360 rotation: (calculate_respawn_look_at_yaw(position, pos), 0.0),
361 anchor_deplete_sound_pos: Some(pos),
362 missing_respawn_block: false,
363 });
364 }
365
366 if state.is_bed()
367 && Player::bed_rule_value_allows_in_world(
368 world,
369 Player::bed_rule_for(world, state).can_set_spawn,
370 )
371 {
372 let facing = state.get_value(&BlockStateProperties::HORIZONTAL_FACING);
373 let position = BedBlock::find_standup_position_with_yaw(
374 world,
375 player,
376 facing,
377 pos,
378 config.respawn_data.yaw,
379 )?;
380 return Some(DeathRespawnSpawn {
381 position,
382 rotation: (calculate_respawn_look_at_yaw(position, pos), 0.0),
383 anchor_deplete_sound_pos: None,
384 missing_respawn_block: false,
385 });
386 }
387
388 if !config.forced {
389 return None;
390 }
391 let top_state = world.get_block_state(pos.above());
392 Self::resolve_forced_respawn_fallback(
393 pos,
394 state,
395 top_state,
396 (config.respawn_data.yaw, config.respawn_data.pitch),
397 )
398 }
399
400 fn resolve_forced_respawn_fallback(
401 pos: BlockPos,
402 state: BlockStateId,
403 top_state: BlockStateId,
404 rotation: (f32, f32),
405 ) -> Option<DeathRespawnSpawn> {
406 if !state.is_possible_to_respawn_in_this() || !top_state.is_possible_to_respawn_in_this() {
407 return None;
408 }
409 Some(DeathRespawnSpawn {
410 position: DVec3::new(
411 f64::from(pos.x()) + 0.5,
412 f64::from(pos.y()) + 0.1,
413 f64::from(pos.z()) + 0.5,
414 ),
415 rotation,
416 anchor_deplete_sound_pos: None,
417 missing_respawn_block: false,
418 })
419 }
420}
421
422fn calculate_respawn_look_at_yaw(position: DVec3, look_at_block_pos: BlockPos) -> f32 {
423 let bed_center = DVec3::new(
424 f64::from(look_at_block_pos.x()) + 0.5,
425 f64::from(look_at_block_pos.y()),
426 f64::from(look_at_block_pos.z()) + 0.5,
427 );
428 let look_direction = (bed_center - position).normalize_or_zero();
429 wrap_degrees(
430 (look_direction.z.atan2(look_direction.x).to_degrees() - f64::from(DEGREE_90)) as f32,
431 )
432}
433
434impl Player {
435 fn begin_respawn_request(&self) -> Option<PendingWorldChangeToken> {
436 let token = self.base.begin_pending_player_respawn()?;
437 if self.begin_respawn_transition(token) {
438 return Some(token);
439 }
440
441 self.finish_pending_world_change(token);
442 None
443 }
444
445 fn finish_respawn_request(&self, token: PendingWorldChangeToken) {
446 self.finish_respawn_transition(token);
447 self.finish_pending_world_change(token);
448 }
449
450 pub fn respawn(&self) {
452 let health = self.get_health();
453 if !Self::should_process_respawn(health) {
454 return;
455 }
456
457 let source_world = self.get_world();
458 let Some(player_arc) = source_world.players.get_by_entity_id(self.id()) else {
459 return;
460 };
461 if !ptr::eq(player_arc.as_ref(), self) {
462 return;
463 }
464 let Some(pending_token) = self.begin_respawn_request() else {
465 if self.is_world_change_pending() {
466 self.defer_death_respawn();
467 }
468 return;
469 };
470
471 let Some(server) = self.server.upgrade() else {
472 self.finish_respawn_request(pending_token);
473 log::error!(
474 "Failed to schedule respawn for player {}: server is gone",
475 self.gameprofile.name()
476 );
477 return;
478 };
479 let (fallback_world, fallback_respawn_data) =
480 match server.respawn_world_and_data_for_domain(source_world.domain()) {
481 Ok(resolved) => resolved,
482 Err(error) => {
483 self.finish_respawn_request(pending_token);
484 log::error!(
485 "Failed to schedule respawn for player {}: {error}",
486 self.gameprofile.name()
487 );
488 return;
489 }
490 };
491 let personal_respawn = self.personal_respawn(&server, &source_world);
492
493 let mut job = match PlayerRespawnJob::new(
494 Arc::clone(&player_arc),
495 source_world,
496 fallback_world,
497 fallback_respawn_data,
498 personal_respawn,
499 RespawnRequestKind::Death,
500 pending_token,
501 ) {
502 Ok(job) => job,
503 Err(error) => {
504 self.finish_respawn_request(pending_token);
505 log::error!(
506 "Failed to schedule respawn for player {}: {error}",
507 self.gameprofile.name()
508 );
509 return;
510 }
511 };
512 let Some(packet_transition) = server.begin_player_packet_transition(&player_arc) else {
513 self.finish_respawn_request(pending_token);
514 return;
515 };
516 job.packet_transition = Some(packet_transition);
517 server.jobs.spawn(job);
518 }
519
520 fn personal_respawn(
521 &self,
522 server: &Server,
523 source_world: &World,
524 ) -> Option<(Arc<World>, PlayerRespawnConfig)> {
525 self.respawn_config().and_then(|config| {
526 server
527 .worlds
528 .get(config.respawn_data.dimension())
529 .filter(|world| world.domain() == source_world.domain())
530 .cloned()
531 .map(|world| (world, config))
532 })
533 }
534
535 #[expect(
536 clippy::too_many_lines,
537 reason = "keeping the respawn transaction linear makes commit and rollback order explicit"
538 )]
539 fn finish_respawn_replacement(
540 self: &Arc<Self>,
541 source_world: &Arc<World>,
542 target_world: &Arc<World>,
543 spawn: DeathRespawnSpawn,
544 pending_token: PendingWorldChangeToken,
545 kind: RespawnRequestKind,
546 ) -> RespawnFinish {
547 let Some(server) = self.server.upgrade() else {
548 return RespawnFinish::Aborted;
549 };
550 let valid_source = match kind {
551 RespawnRequestKind::Death => {
552 source_world.contains_player(self)
553 && Self::should_process_respawn(self.get_health())
554 }
555 RespawnRequestKind::EndCredits => {
556 !source_world.contains_player(self)
557 && Arc::ptr_eq(&self.get_world(), source_world)
558 && self.has_won_game()
559 }
560 };
561 if self.connection.closed()
562 || !self.is_respawn_transition_pending(pending_token)
563 || !self.session.is_current_player(self)
564 || !server.owns_online_player(self)
565 || !valid_source
566 {
567 return RespawnFinish::Aborted;
568 }
569
570 assert_eq!(
571 self.remove_all_menus_with_disposition(MenuItemDisposition::Drop),
572 MenuRemovalStatus::Complete,
573 "respawn menu cleanup must run outside a menu callback"
574 );
575 self.detach_relationships_for_respawn();
576
577 if kind == RespawnRequestKind::EndCredits {
578 self.set_won_game(false);
579 }
580
581 let restore_all = kind == RespawnRequestKind::EndCredits;
582 let transfer_inventory = restore_all
583 || target_world.get_game_rule(&KEEP_INVENTORY)
584 || self.game_mode() == GameType::Spectator;
585 let replacement = self.new_respawn_replacement(
586 Arc::clone(target_world),
587 restore_all,
588 transfer_inventory,
589 !spawn.missing_respawn_block,
590 );
591
592 let same_world = Arc::ptr_eq(source_world, target_world);
593 if kind == RespawnRequestKind::Death
594 && !source_world.detach_player_for_respawn(self, same_world)
595 {
596 self.connection.close();
597 return RespawnFinish::Failed;
598 }
599
600 replacement.handle_missing_respawn_block(spawn.missing_respawn_block);
601 let reason = match kind {
602 RespawnRequestKind::Death => ResetReason::Respawn,
603 RespawnRequestKind::EndCredits => ResetReason::EndCredits,
604 };
605 replacement.prepare_respawn_replacement(reason);
606 replacement.experience.lock().dirty = true;
607 replacement.synchronize_respawn_replacement(spawn.position, spawn.rotation);
608 replacement.send_experience_packet_if_dirty();
609
610 let expected_world_player =
611 (kind == RespawnRequestKind::Death && same_world).then_some(self);
612 if !target_world.install_respawned_player(Arc::clone(&replacement), expected_world_player) {
613 self.connection.close();
614 return RespawnFinish::Failed;
615 }
616
617 if !server.replace_online_player(self, Arc::clone(&replacement)) {
618 self.connection.close();
619 if !target_world.remove_respawned_player(&replacement) {
620 tracing::error!(
621 player = %self.gameprofile.name(),
622 world = %target_world.key,
623 "Failed to remove a respawn replacement after online-player commit failed"
624 );
625 }
626 return RespawnFinish::Failed;
627 }
628
629 let _ = replacement.mark_joined_world();
630 if !self.session.replace_player(self, &replacement) {
631 self.connection.close();
632 if !server.rollback_respawn_online_player(&replacement, Arc::clone(self))
633 && server.remove_online_player_sync(&replacement).is_none()
634 {
635 tracing::error!(
636 player = %self.gameprofile.name(),
637 "Failed to remove or roll back online-player ownership after respawn session bind failed"
638 );
639 }
640 if !target_world.remove_respawned_player(&replacement) {
641 tracing::error!(
642 player = %self.gameprofile.name(),
643 world = %target_world.key,
644 "Failed to remove a respawn replacement after session bind failed"
645 );
646 }
647 return RespawnFinish::Failed;
648 }
649
650 self.rebind_ender_pearls_to(&replacement);
651
652 server.resend_player_context(&replacement);
653
654 if kind == RespawnRequestKind::Death
655 && let Some(pos) = spawn.anchor_deplete_sound_pos
656 && target_world.get_block_state(pos).get_block() == &vanilla_blocks::RESPAWN_ANCHOR
657 {
658 replacement.send_packet(CSound::new(
659 &sound_events::BLOCK_RESPAWN_ANCHOR_DEPLETE,
660 SoundSource::Blocks,
661 pos.0.as_dvec3(),
662 1.0,
663 1.0,
664 rand::random(),
665 ));
666 }
667
668 RespawnFinish::Completed
669 }
670
671 fn handle_missing_respawn_block(&self, missing_respawn_block: bool) {
672 if !missing_respawn_block {
673 return;
674 }
675 self.set_respawn_position(None, false);
676 self.send_packet(CGameEvent {
677 event: GameEventType::NoRespawnBlockAvailable,
678 data: 0.0,
679 });
680 }
681
682 fn detach_relationships_for_respawn(&self) {
683 for passenger in self.passengers() {
684 passenger.stop_riding();
685 }
686 self.stop_riding();
687 self.base.set_boarding_cooldown(0);
688 }
689
690 pub fn handle_client_command(self: &Arc<Self>, action: ClientCommandAction) {
692 self.reset_last_action_time();
693 match action {
694 ClientCommandAction::PerformRespawn => {
695 if self.has_won_game() {
696 self.respawn_after_end_credits();
697 } else {
698 self.respawn();
699 }
700 }
701 ClientCommandAction::RequestStats => self.send_stats(),
702 ClientCommandAction::RequestGameRuleValues => {
703 }
705 }
706 }
707
708 #[must_use]
711 pub(in crate::player) const fn should_process_respawn(health: f32) -> bool {
712 health <= 0.0
713 }
714
715 #[must_use]
717 pub fn has_seen_credits(&self) -> bool {
718 *self.seen_credits.lock()
719 }
720
721 pub fn set_seen_credits(&self, seen_credits: bool) {
723 *self.seen_credits.lock() = seen_credits;
724 }
725
726 #[must_use]
727 pub(crate) fn has_won_game(&self) -> bool {
728 *self.won_game.lock()
729 }
730
731 fn set_won_game(&self, won_game: bool) {
732 *self.won_game.lock() = won_game;
733 }
734
735 pub(crate) fn show_end_credits(&self) {
737 if self.has_won_game() {
738 return;
739 }
740
741 let world = self.get_world();
742 let Some(player) = world.players.get_by_entity_id(self.id()) else {
743 return;
744 };
745 if !ptr::eq(player.as_ref(), self) {
746 return;
747 }
748 let Some(pending_token) = player.begin_pending_world_change() else {
749 return;
750 };
751
752 assert_eq!(
753 player.remove_all_menus(),
754 MenuRemovalStatus::Complete,
755 "End credits menu removal must run outside a menu callback"
756 );
757 let detached = world.detach_player_for_respawn(&player, false);
758 player.finish_pending_world_change(pending_token);
759 if !detached {
760 return;
761 }
762
763 player.set_won_game(true);
764 player.send_packet(CGameEvent {
765 event: GameEventType::WinGame,
766 data: 0.0,
767 });
768 player.set_seen_credits(true);
769 }
770
771 fn respawn_after_end_credits(self: &Arc<Self>) {
772 if !self.has_won_game() {
773 return;
774 }
775
776 let source_world = self.get_world();
777 let Some(server) = self.server.upgrade() else {
778 log::error!(
779 "Failed to schedule End credits respawn for player {}: server is gone",
780 self.gameprofile.name()
781 );
782 return;
783 };
784 if !server.owns_online_player(self) {
785 return;
786 }
787 let Some(pending_token) = self.begin_respawn_request() else {
788 return;
789 };
790 let (target_world, respawn_data) =
791 match server.respawn_world_and_data_for_domain(source_world.domain()) {
792 Ok(resolved) => resolved,
793 Err(error) => {
794 self.finish_respawn_request(pending_token);
795 log::error!(
796 "Failed to schedule End credits respawn for player {}: {error}",
797 self.gameprofile.name()
798 );
799 return;
800 }
801 };
802 let personal_respawn = self.personal_respawn(&server, &source_world);
803
804 let mut job = match PlayerRespawnJob::new(
805 Arc::clone(self),
806 source_world,
807 target_world,
808 respawn_data,
809 personal_respawn,
810 RespawnRequestKind::EndCredits,
811 pending_token,
812 ) {
813 Ok(job) => job,
814 Err(error) => {
815 self.finish_respawn_request(pending_token);
816 log::error!(
817 "Failed to schedule End credits respawn for player {}: {error}",
818 self.gameprofile.name()
819 );
820 return;
821 }
822 };
823 let Some(packet_transition) = server.begin_player_packet_transition(self) else {
824 self.finish_respawn_request(pending_token);
825 return;
826 };
827 job.packet_transition = Some(packet_transition);
828 server.jobs.spawn(job);
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use steel_utils::{BlockPos, ChunkPos};
835
836 use super::PlayerRespawnJob;
837
838 #[test]
839 fn personal_respawn_loads_only_chunks_touched_by_standup_search() {
840 assert_eq!(
841 PlayerRespawnJob::personal_respawn_chunk_positions(BlockPos::new(8, 64, 8)),
842 [ChunkPos::new(0, 0)]
843 );
844
845 assert_eq!(
846 PlayerRespawnJob::personal_respawn_chunk_positions(BlockPos::new(13, 64, 13)),
847 [
848 ChunkPos::new(0, 0),
849 ChunkPos::new(0, 1),
850 ChunkPos::new(1, 0),
851 ChunkPos::new(1, 1),
852 ]
853 );
854 }
855}