Skip to main content

steel_core/server/jobs/
teleport.rs

1use super::super::{
2    Arc, BlockPos, ChunkPos, ChunkRequest, ChunkRequestHandle, ChunkRequestState, ChunkStatus,
3    ChunkStorage, ChunkTicketKind, DVec3, DomainResidenceToken, EntityBase, NetworkConnection,
4    PendingWorldChangeToken, PersistentEntity, PersistentRootVehicle, Player, PlayerSpawnSearch,
5    PlayerSpawnSearchPoll, PreparedSpawn, RelativeMovement, RemovalReason, RespawnData, Server,
6    SharedEntity, TeleportPostTransition, TeleportTransition, Uuid, World, WorldEntry,
7    change_entity_world, end_gateway, end_portal, is_allowed_to_enter_portal, nether_portal,
8    vanilla_entities,
9};
10use super::{JobPoll, ServerJob, ServerJobContext};
11use crate::entity::LivingEntity as _;
12use crate::world::player_spawn_finder::POLL_TIME_BUDGET;
13
14pub(in crate::server) struct RootVehicleRestoreJob {
15    player: Arc<Player>,
16    world: Arc<World>,
17    request: ChunkRequestHandle,
18    attach: [u8; 16],
19    root_uuid: [u8; 16],
20    residence_token: DomainResidenceToken,
21    mount_player: bool,
22}
23
24impl RootVehicleRestoreJob {
25    pub(in crate::server) fn new(
26        player: Arc<Player>,
27        world: Arc<World>,
28        root_vehicle: &PersistentRootVehicle,
29        residence_token: DomainResidenceToken,
30        mount_player: bool,
31    ) -> Option<Self> {
32        let root_chunk = persistent_entity_chunk(&root_vehicle.entity)?;
33        let request = world.chunk_map.request_chunk(
34            root_chunk,
35            ChunkStatus::StructureStarts,
36            ChunkTicketKind::PlayerSpawn,
37        );
38        Some(Self {
39            player,
40            world,
41            request,
42            attach: root_vehicle.attach,
43            root_uuid: root_vehicle.entity.uuid,
44            residence_token,
45            mount_player,
46        })
47    }
48}
49
50impl ServerJob for RootVehicleRestoreJob {
51    fn poll(&mut self, _context: &mut ServerJobContext) -> JobPoll {
52        let Some(player) = self.player.session.current_player() else {
53            return JobPoll::Finished;
54        };
55        if player.connection.closed() || !player.is_domain_residence_current(self.residence_token) {
56            return JobPoll::Finished;
57        }
58        let Some(server) = player.server.upgrade() else {
59            return JobPoll::Finished;
60        };
61        if !server.owns_online_player(&player) {
62            return JobPoll::Finished;
63        }
64        let Some(player_world) = server.live_world_for_player(&player) else {
65            // End credits temporarily detaches a connected player without ending
66            // their domain residence. Retain the payload and resume after respawn.
67            return JobPoll::Pending;
68        };
69        if !Arc::ptr_eq(&player_world, &self.world) {
70            return JobPoll::Finished;
71        }
72
73        match self.request.poll() {
74            ChunkRequestState::Pending { .. } => JobPoll::Pending,
75            ChunkRequestState::Cancelled => JobPoll::Finished,
76            ChunkRequestState::Ready => {
77                let Some(_ready) = self.request.ready_chunks() else {
78                    return JobPoll::Pending;
79                };
80                if let Some(root_vehicle) = player.take_matching_pending_root_vehicle(
81                    self.residence_token,
82                    &self.world,
83                    self.attach,
84                    self.root_uuid,
85                ) {
86                    restore_root_vehicle_for_player(
87                        &player,
88                        &self.world,
89                        root_vehicle,
90                        self.mount_player,
91                    );
92                }
93                JobPoll::Finished
94            }
95        }
96    }
97
98    fn cancel(&mut self) {
99        self.request.cancel();
100    }
101}
102
103pub(in crate::server) fn clear_pending_world_change(
104    entity: &SharedEntity,
105    pending_token: PendingWorldChangeToken,
106) {
107    if entity.finish_pending_world_change(pending_token)
108        && let Some(player) = entity.as_player()
109    {
110        player.retry_deferred_death_respawn();
111    }
112}
113
114fn finish_pending_world_change_after_transition(
115    entity: &SharedEntity,
116    pending_token: PendingWorldChangeToken,
117    changed_entity: Option<SharedEntity>,
118) {
119    match changed_entity {
120        Some(changed_entity) if Arc::ptr_eq(entity, &changed_entity) => {
121            clear_pending_world_change(&changed_entity, pending_token);
122        }
123        Some(_) => {}
124        None => {
125            clear_pending_world_change(entity, pending_token);
126        }
127    }
128}
129
130fn finish_portal_world_change(
131    entity: &SharedEntity,
132    pending_token: PendingWorldChangeToken,
133    changed_entity: Option<SharedEntity>,
134) -> JobPoll {
135    finish_pending_world_change_after_transition(entity, pending_token, changed_entity);
136    JobPoll::Finished
137}
138
139pub(in crate::server) fn portal_entity_still_valid(
140    entity: &SharedEntity,
141    source_world: &Arc<World>,
142    pending_token: PendingWorldChangeToken,
143) -> bool {
144    !entity.is_removed()
145        && entity.can_use_portal(false)
146        && entity.is_world_change_token_pending(pending_token)
147        && entity
148            .level()
149            .is_some_and(|world| Arc::ptr_eq(&world, source_world))
150        && source_world.contains_live_or_unloading_entity(entity)
151        && !entity
152            .as_player()
153            .is_some_and(|player| player.connection.closed())
154}
155
156fn poll_portal_chunks_until_ready(
157    request: &mut ChunkRequestHandle,
158    entity: &SharedEntity,
159    pending_token: PendingWorldChangeToken,
160) -> Option<JobPoll> {
161    match request.poll() {
162        ChunkRequestState::Pending { .. } => Some(JobPoll::Pending),
163        ChunkRequestState::Cancelled => {
164            clear_pending_world_change(entity, pending_token);
165            Some(JobPoll::Finished)
166        }
167        ChunkRequestState::Ready => {
168            if request.ready_chunks().is_some() {
169                None
170            } else {
171                Some(JobPoll::Pending)
172            }
173        }
174    }
175}
176
177pub(in crate::server) struct WorldSpawnTeleportJob {
178    entity: SharedEntity,
179    source_world: Arc<World>,
180    target_world: Arc<World>,
181    pending_token: PendingWorldChangeToken,
182    rotation: (f32, f32),
183    entry: WorldEntry,
184    phase: WorldSpawnTeleportPhase,
185}
186
187enum WorldSpawnTeleportPhase {
188    Searching(PlayerSpawnSearch),
189    Loading {
190        spawn: PreparedSpawn,
191        request: ChunkRequestHandle,
192    },
193}
194
195impl WorldSpawnTeleportJob {
196    pub(in crate::server) fn new(
197        entity: SharedEntity,
198        source_world: Arc<World>,
199        target_world: Arc<World>,
200        pending_token: PendingWorldChangeToken,
201        prefetch: Option<ChunkRequestHandle>,
202        entry: WorldEntry,
203    ) -> Result<Self, String> {
204        if entity.as_player().is_none() {
205            return Err("world spawn selection does not belong to a player".to_owned());
206        }
207        let (spawn_suggestion, spawn_rotation) = {
208            let level_data = target_world.level_data.read();
209            let data = level_data.data();
210            (data.spawn_pos(), (data.spawn.angle, 0.0))
211        };
212        let rotation = match entry {
213            WorldEntry::Usual => spawn_rotation,
214            WorldEntry::Rotation(turned) => turned,
215            WorldEntry::Exact(spawn) => spawn.rotation,
216        };
217        let phase = if let WorldEntry::Exact(spawn) = entry {
218            WorldSpawnTeleportPhase::Loading {
219                request: target_world.request_player_spawn_chunks(spawn.position),
220                spawn,
221            }
222        } else {
223            WorldSpawnTeleportPhase::Searching(
224                PlayerSpawnSearch::new(
225                    &target_world,
226                    spawn_suggestion,
227                    target_world.default_gamemode(),
228                )?
229                .with_safe_fallback()
230                .with_prefetch(prefetch),
231            )
232        };
233        Ok(Self {
234            entity,
235            source_world,
236            target_world,
237            pending_token,
238            rotation,
239            entry,
240            phase,
241        })
242    }
243
244    fn still_valid(&self, server: &Server) -> bool {
245        let Some(player) = self.entity.as_player() else {
246            return false;
247        };
248        !self.entity.is_removed()
249            && !player.connection.closed()
250            && player.get_health() > 0.0
251            && !player.has_won_game()
252            && self
253                .entity
254                .is_world_change_token_pending(self.pending_token)
255            && self
256                .entity
257                .level()
258                .is_some_and(|world| Arc::ptr_eq(&world, &self.source_world))
259            && self.source_world.contains_player(player)
260            && self.source_world.domain() == self.target_world.domain()
261            && server
262                .worlds
263                .get(&self.target_world.key)
264                .is_some_and(|registered| Arc::ptr_eq(registered, &self.target_world))
265    }
266
267    fn finish_pending(&self) {
268        clear_pending_world_change(&self.entity, self.pending_token);
269    }
270}
271
272impl ServerJob for WorldSpawnTeleportJob {
273    fn poll(&mut self, context: &mut ServerJobContext) -> JobPoll {
274        let Some(server) = context.server() else {
275            self.finish_pending();
276            return JobPoll::Finished;
277        };
278        if !self.still_valid(&server) {
279            self.finish_pending();
280            return JobPoll::Finished;
281        }
282
283        loop {
284            match &mut self.phase {
285                WorldSpawnTeleportPhase::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 = PreparedSpawn {
294                                position,
295                                rotation: self.rotation,
296                            };
297                            let request = self
298                                .target_world
299                                .request_player_spawn_chunks(spawn.position);
300                            self.phase = WorldSpawnTeleportPhase::Loading { spawn, request };
301                        }
302                    }
303                }
304                WorldSpawnTeleportPhase::Loading { spawn, request } => match request.poll() {
305                    ChunkRequestState::Pending { .. } => return JobPoll::Pending,
306                    ChunkRequestState::Cancelled => {
307                        self.finish_pending();
308                        return JobPoll::Finished;
309                    }
310                    ChunkRequestState::Ready => {
311                        if request.ready_chunks().is_none() {
312                            return JobPoll::Pending;
313                        }
314                        let transition = TeleportTransition {
315                            target_world: Arc::clone(&self.target_world),
316                            position: spawn.position,
317                            rotation: spawn.rotation,
318                            velocity: DVec3::ZERO,
319                            relatives: RelativeMovement::NONE,
320                            portal_cooldown: 0,
321                            as_passenger: false,
322                            post_transition: TeleportPostTransition::do_nothing(),
323                        };
324                        let changed_entity =
325                            change_entity_world(Arc::clone(&self.entity), &transition);
326                        // An exact placement lands like `/teleport`.
327                        if matches!(self.entry, WorldEntry::Exact(_))
328                            && let Some(entity) = &changed_entity
329                        {
330                            entity.land_after_teleport();
331                        }
332                        return finish_portal_world_change(
333                            &self.entity,
334                            self.pending_token,
335                            changed_entity,
336                        );
337                    }
338                },
339            }
340        }
341    }
342
343    fn cancel(&mut self) {
344        self.finish_pending();
345        if let WorldSpawnTeleportPhase::Loading { request, .. } = &mut self.phase {
346            request.cancel();
347        }
348    }
349}
350
351pub(in crate::server) struct NetherPortalTeleportJob {
352    entity: SharedEntity,
353    source_world: Arc<World>,
354    target_world: Arc<World>,
355    portal_pos: BlockPos,
356    approximate_exit_pos: BlockPos,
357    to_nether: bool,
358    pending_token: PendingWorldChangeToken,
359    request: ChunkRequestHandle,
360}
361
362impl NetherPortalTeleportJob {
363    pub(in crate::server) fn new(
364        entity: SharedEntity,
365        source_world: Arc<World>,
366        target_world: Arc<World>,
367        portal_pos: BlockPos,
368        approximate_exit_pos: BlockPos,
369        to_nether: bool,
370        pending_token: PendingWorldChangeToken,
371    ) -> Self {
372        let request = target_world.chunk_map.request_square(
373            nether_portal::prewarm_center(approximate_exit_pos),
374            nether_portal::prewarm_chunk_radius(to_nether),
375            ChunkStatus::Full,
376            ChunkTicketKind::Portal,
377        );
378        Self {
379            entity,
380            source_world,
381            target_world,
382            portal_pos,
383            approximate_exit_pos,
384            to_nether,
385            pending_token,
386            request,
387        }
388    }
389
390    fn still_valid(&self) -> bool {
391        portal_entity_still_valid(&self.entity, &self.source_world, self.pending_token)
392    }
393
394    fn clear_pending(&self) {
395        clear_pending_world_change(&self.entity, self.pending_token);
396    }
397
398    fn finish_transition(&self, changed_entity: Option<SharedEntity>) {
399        finish_pending_world_change_after_transition(
400            &self.entity,
401            self.pending_token,
402            changed_entity,
403        );
404    }
405}
406
407impl ServerJob for NetherPortalTeleportJob {
408    fn poll(&mut self, context: &mut ServerJobContext) -> JobPoll {
409        if !self.still_valid() {
410            self.clear_pending();
411            return JobPoll::Finished;
412        }
413
414        if let Some(job_poll) =
415            poll_portal_chunks_until_ready(&mut self.request, &self.entity, self.pending_token)
416        {
417            return job_poll;
418        }
419
420        let Some(server) = context.server() else {
421            self.clear_pending();
422            return JobPoll::Finished;
423        };
424        if !is_allowed_to_enter_portal(&self.source_world, &self.target_world)
425            || !server.can_teleport_between_worlds(
426                self.entity.as_ref(),
427                &self.source_world,
428                &self.target_world,
429            )
430        {
431            self.clear_pending();
432            return JobPoll::Finished;
433        }
434        let Some(transition) = nether_portal::calculate_transition(
435            &self.source_world,
436            &self.target_world,
437            self.entity.as_ref(),
438            self.portal_pos,
439            self.approximate_exit_pos,
440            self.to_nether,
441        ) else {
442            self.clear_pending();
443            return JobPoll::Finished;
444        };
445        let changed_entity = change_entity_world(Arc::clone(&self.entity), &transition);
446        self.finish_transition(changed_entity);
447        JobPoll::Finished
448    }
449
450    fn cancel(&mut self) {
451        self.clear_pending();
452        self.request.cancel();
453    }
454}
455
456struct EndPortalRespawnSpawn {
457    position: DVec3,
458    rotation: (f32, f32),
459}
460
461pub(in crate::server) struct EndPortalTeleportJob {
462    entity: SharedEntity,
463    source_world: Arc<World>,
464    pending_token: PendingWorldChangeToken,
465    phase: EndPortalTeleportPhase,
466}
467
468enum EndPortalTeleportPhase {
469    EntryToEnd {
470        target_world: Arc<World>,
471        request: ChunkRequestHandle,
472    },
473    ReturningEntity {
474        target_world: Arc<World>,
475        respawn_data: RespawnData,
476        request: ChunkRequestHandle,
477    },
478    SearchingPlayerRespawn {
479        target_world: Arc<World>,
480        respawn_data: RespawnData,
481        search: PlayerSpawnSearch,
482    },
483    LoadingPlayerRespawn {
484        target_world: Arc<World>,
485        spawn: EndPortalRespawnSpawn,
486        request: ChunkRequestHandle,
487    },
488}
489
490impl EndPortalTeleportJob {
491    pub(in crate::server) fn entry_to_end(
492        entity: SharedEntity,
493        source_world: Arc<World>,
494        target_world: Arc<World>,
495        pending_token: PendingWorldChangeToken,
496    ) -> Self {
497        let request = target_world.chunk_map.request_square(
498            end_portal::end_platform_prewarm_center(),
499            end_portal::end_platform_prewarm_chunk_radius(),
500            ChunkStatus::Full,
501            ChunkTicketKind::Portal,
502        );
503        Self {
504            entity,
505            source_world,
506            pending_token,
507            phase: EndPortalTeleportPhase::EntryToEnd {
508                target_world,
509                request,
510            },
511        }
512    }
513
514    pub(in crate::server) fn returning_entity(
515        entity: SharedEntity,
516        source_world: Arc<World>,
517        target_world: Arc<World>,
518        respawn_data: RespawnData,
519        pending_token: PendingWorldChangeToken,
520    ) -> Self {
521        let request = target_world.chunk_map.request_chunk(
522            end_portal::prewarm_center(respawn_data.pos()),
523            ChunkStatus::Full,
524            ChunkTicketKind::Portal,
525        );
526        Self {
527            entity,
528            source_world,
529            pending_token,
530            phase: EndPortalTeleportPhase::ReturningEntity {
531                target_world,
532                respawn_data,
533                request,
534            },
535        }
536    }
537
538    pub(in crate::server) fn returning_player(
539        entity: SharedEntity,
540        source_world: Arc<World>,
541        target_world: Arc<World>,
542        respawn_data: RespawnData,
543        pending_token: PendingWorldChangeToken,
544    ) -> Result<Self, String> {
545        let search = PlayerSpawnSearch::new(
546            &target_world,
547            respawn_data.pos(),
548            target_world.default_gamemode(),
549        )?;
550        Ok(Self {
551            entity,
552            source_world,
553            pending_token,
554            phase: EndPortalTeleportPhase::SearchingPlayerRespawn {
555                target_world,
556                respawn_data,
557                search,
558            },
559        })
560    }
561
562    fn still_valid(&self) -> bool {
563        portal_entity_still_valid(&self.entity, &self.source_world, self.pending_token)
564    }
565
566    fn clear_pending(&self) {
567        clear_pending_world_change(&self.entity, self.pending_token);
568    }
569}
570
571impl ServerJob for EndPortalTeleportJob {
572    fn poll(&mut self, _context: &mut ServerJobContext) -> JobPoll {
573        if !self.still_valid() {
574            self.clear_pending();
575            return JobPoll::Finished;
576        }
577
578        let entity = Arc::clone(&self.entity);
579        let pending_token = self.pending_token;
580        loop {
581            match &mut self.phase {
582                EndPortalTeleportPhase::EntryToEnd {
583                    target_world,
584                    request,
585                } => {
586                    if let Some(job_poll) =
587                        poll_portal_chunks_until_ready(request, &entity, pending_token)
588                    {
589                        return job_poll;
590                    }
591                    let Some(transition) =
592                        end_portal::calculate_entry_transition(target_world, entity.as_ref())
593                    else {
594                        clear_pending_world_change(&entity, pending_token);
595                        return JobPoll::Finished;
596                    };
597                    let changed_entity = change_entity_world(Arc::clone(&entity), &transition);
598                    return finish_portal_world_change(&entity, pending_token, changed_entity);
599                }
600                EndPortalTeleportPhase::ReturningEntity {
601                    target_world,
602                    respawn_data,
603                    request,
604                } => {
605                    if let Some(job_poll) =
606                        poll_portal_chunks_until_ready(request, &entity, pending_token)
607                    {
608                        return job_poll;
609                    }
610                    let transition = end_portal::calculate_entity_return_transition(
611                        target_world,
612                        entity.as_ref(),
613                        respawn_data,
614                    );
615                    let changed_entity = change_entity_world(Arc::clone(&entity), &transition);
616                    return finish_portal_world_change(&entity, pending_token, changed_entity);
617                }
618                EndPortalTeleportPhase::SearchingPlayerRespawn {
619                    target_world,
620                    respawn_data,
621                    search,
622                } => match search.poll_with_time_budget(target_world, POLL_TIME_BUDGET) {
623                    PlayerSpawnSearchPoll::Pending => return JobPoll::Pending,
624                    PlayerSpawnSearchPoll::Cancelled => {
625                        clear_pending_world_change(&entity, pending_token);
626                        return JobPoll::Finished;
627                    }
628                    PlayerSpawnSearchPoll::Ready(position) => {
629                        let spawn = EndPortalRespawnSpawn {
630                            position,
631                            rotation: (respawn_data.yaw, respawn_data.pitch),
632                        };
633                        let request = target_world.request_player_spawn_chunks(position);
634                        self.phase = EndPortalTeleportPhase::LoadingPlayerRespawn {
635                            target_world: Arc::clone(target_world),
636                            spawn,
637                            request,
638                        };
639                    }
640                },
641                EndPortalTeleportPhase::LoadingPlayerRespawn {
642                    target_world,
643                    spawn,
644                    request,
645                } => {
646                    if let Some(job_poll) =
647                        poll_portal_chunks_until_ready(request, &entity, pending_token)
648                    {
649                        return job_poll;
650                    }
651                    let transition = end_portal::calculate_player_return_transition(
652                        target_world,
653                        entity.as_ref(),
654                        spawn.position,
655                        spawn.rotation,
656                    );
657                    let changed_entity = change_entity_world(Arc::clone(&entity), &transition);
658                    return finish_portal_world_change(&entity, pending_token, changed_entity);
659                }
660            }
661        }
662    }
663
664    fn cancel(&mut self) {
665        self.clear_pending();
666        match &mut self.phase {
667            EndPortalTeleportPhase::EntryToEnd { request, .. }
668            | EndPortalTeleportPhase::ReturningEntity { request, .. }
669            | EndPortalTeleportPhase::LoadingPlayerRespawn { request, .. } => request.cancel(),
670            EndPortalTeleportPhase::SearchingPlayerRespawn { .. } => {}
671        }
672    }
673}
674
675pub(in crate::server) struct EndGatewayTeleportJob {
676    entity: SharedEntity,
677    source_world: Arc<World>,
678    portal_pos: BlockPos,
679    source_is_end: bool,
680    pending_token: PendingWorldChangeToken,
681    phase: EndGatewayTeleportPhase,
682}
683
684enum EndGatewayTeleportPhase {
685    LoadingReady { request: ChunkRequestHandle },
686    LoadingSearchPath { request: ChunkRequestHandle },
687}
688
689impl EndGatewayTeleportJob {
690    pub(in crate::server) fn new(
691        entity: SharedEntity,
692        source_world: Arc<World>,
693        portal_pos: BlockPos,
694        source_is_end: bool,
695        pending_token: PendingWorldChangeToken,
696    ) -> Option<Self> {
697        let preparation = end_gateway::initial_chunks(&source_world, portal_pos, source_is_end)?;
698        let phase = match preparation {
699            end_gateway::EndGatewayChunkPreparation::Ready(chunks) => {
700                EndGatewayTeleportPhase::LoadingReady {
701                    request: request_end_gateway_chunks(&source_world, chunks),
702                }
703            }
704            end_gateway::EndGatewayChunkPreparation::SearchPath(chunks) => {
705                EndGatewayTeleportPhase::LoadingSearchPath {
706                    request: request_end_gateway_chunks(&source_world, chunks),
707                }
708            }
709        };
710        Some(Self {
711            entity,
712            source_world,
713            portal_pos,
714            source_is_end,
715            pending_token,
716            phase,
717        })
718    }
719
720    fn still_valid(&self) -> bool {
721        portal_entity_still_valid(&self.entity, &self.source_world, self.pending_token)
722    }
723
724    fn clear_pending(&self) {
725        clear_pending_world_change(&self.entity, self.pending_token);
726    }
727}
728
729impl ServerJob for EndGatewayTeleportJob {
730    fn poll(&mut self, _context: &mut ServerJobContext) -> JobPoll {
731        if !self.still_valid() {
732            self.clear_pending();
733            return JobPoll::Finished;
734        }
735
736        let entity = Arc::clone(&self.entity);
737        let pending_token = self.pending_token;
738        let source_world = Arc::clone(&self.source_world);
739        let portal_pos = self.portal_pos;
740        let source_is_end = self.source_is_end;
741        loop {
742            match &mut self.phase {
743                EndGatewayTeleportPhase::LoadingReady { request } => match request.poll() {
744                    ChunkRequestState::Pending { .. } => return JobPoll::Pending,
745                    ChunkRequestState::Cancelled => {
746                        clear_pending_world_change(&entity, pending_token);
747                        return JobPoll::Finished;
748                    }
749                    ChunkRequestState::Ready => {
750                        let Some(_ready) = request.ready_chunks() else {
751                            return JobPoll::Pending;
752                        };
753                        let Some(transition) = end_gateway::calculate_transition(
754                            &source_world,
755                            entity.as_ref(),
756                            portal_pos,
757                            source_is_end,
758                        ) else {
759                            clear_pending_world_change(&entity, pending_token);
760                            return JobPoll::Finished;
761                        };
762                        let changed_entity = change_entity_world(Arc::clone(&entity), &transition);
763                        finish_pending_world_change_after_transition(
764                            &entity,
765                            pending_token,
766                            changed_entity,
767                        );
768                        return JobPoll::Finished;
769                    }
770                },
771                EndGatewayTeleportPhase::LoadingSearchPath { request } => match request.poll() {
772                    ChunkRequestState::Pending { .. } => return JobPoll::Pending,
773                    ChunkRequestState::Cancelled => {
774                        clear_pending_world_change(&entity, pending_token);
775                        return JobPoll::Finished;
776                    }
777                    ChunkRequestState::Ready => {
778                        let Some(_ready) = request.ready_chunks() else {
779                            return JobPoll::Pending;
780                        };
781                        let Some(chunks) = end_gateway::final_chunks_after_search(
782                            &source_world,
783                            portal_pos,
784                            source_is_end,
785                        ) else {
786                            clear_pending_world_change(&entity, pending_token);
787                            return JobPoll::Finished;
788                        };
789                        self.phase = EndGatewayTeleportPhase::LoadingReady {
790                            request: request_end_gateway_chunks(&source_world, chunks),
791                        };
792                    }
793                },
794            }
795        }
796    }
797
798    fn cancel(&mut self) {
799        self.clear_pending();
800        match &mut self.phase {
801            EndGatewayTeleportPhase::LoadingReady { request }
802            | EndGatewayTeleportPhase::LoadingSearchPath { request } => request.cancel(),
803        }
804    }
805}
806
807fn request_end_gateway_chunks(world: &Arc<World>, chunks: Vec<ChunkPos>) -> ChunkRequestHandle {
808    world.chunk_map.request_chunks(ChunkRequest {
809        status: ChunkStatus::Full,
810        positions: chunks,
811        ticket_kind: ChunkTicketKind::Portal,
812    })
813}
814
815fn persistent_entity_chunk(entity: &PersistentEntity) -> Option<ChunkPos> {
816    let pos = DVec3::new(entity.pos[0], entity.pos[1], entity.pos[2]);
817    if !pos.x.is_finite() || !pos.y.is_finite() || !pos.z.is_finite() {
818        tracing::warn!(
819            uuid = ?Uuid::from_bytes(entity.uuid),
820            "Skipping persisted entity with non-finite position {pos:?}",
821        );
822        return None;
823    }
824    Some(ChunkPos::from_entity_pos(pos))
825}
826
827fn restore_root_vehicle_for_player(
828    player: &Arc<Player>,
829    world: &Arc<World>,
830    root_vehicle: PersistentRootVehicle,
831    mount_player: bool,
832) {
833    let Some(root_chunk) = persistent_entity_chunk(&root_vehicle.entity) else {
834        return;
835    };
836    let level = Arc::downgrade(world);
837    let entities =
838        ChunkStorage::persistent_to_entity_tree_at_level(&root_vehicle.entity, root_chunk, &level);
839    if entities.is_empty() {
840        tracing::warn!(
841            player = %player.gameprofile.name(),
842            "Persisted RootVehicle did not recreate any runtime entities",
843        );
844        return;
845    }
846
847    let attach_uuid = Uuid::from_bytes(root_vehicle.attach);
848    let Some(attach_entity) = entities
849        .iter()
850        .find(|entity| entity.uuid() == attach_uuid)
851        .cloned()
852    else {
853        tracing::warn!(
854            player = %player.gameprofile.name(),
855            attach = ?attach_uuid,
856            "Discarding persisted RootVehicle because the attach entity is missing",
857        );
858        discard_restored_entities(&entities);
859        return;
860    };
861
862    if let Err(error) = world.register_loaded_entity_tree(&entities) {
863        tracing::warn!(
864            player = %player.gameprofile.name(),
865            attach = ?attach_uuid,
866            root = ?Uuid::from_bytes(root_vehicle.entity.uuid),
867            "Discarding persisted RootVehicle because its entity tree could not be registered: {error}",
868        );
869        discard_restored_entities(&entities);
870        return;
871    }
872
873    if mount_player {
874        let player_entity: SharedEntity = Arc::<Player>::clone(player);
875        EntityBase::restore_passenger_relationship(&attach_entity, &player_entity);
876        attach_entity.position_rider(player.as_ref());
877        player.send_restored_vehicle_mount_sync(attach_entity.as_ref());
878    }
879
880    world.mark_chunk_dirty(root_chunk);
881    for entity in &entities {
882        world.mark_chunk_dirty(ChunkPos::from_entity_pos(entity.position()));
883    }
884}
885
886fn discard_restored_entities(entities: &[SharedEntity]) {
887    for entity in entities {
888        entity.set_removed(RemovalReason::Discarded);
889    }
890}
891
892/// Re-spawns a single persisted ender pearl in its own world once the target
893/// chunk is loaded (vanilla `ServerPlayer.loadAndSpawnEnderPearl`).
894pub(in crate::server) struct EnderPearlRestoreJob {
895    player: Arc<Player>,
896    world: Arc<World>,
897    request: ChunkRequestHandle,
898    uuid: Uuid,
899    residence_token: DomainResidenceToken,
900}
901
902impl EnderPearlRestoreJob {
903    pub(in crate::server) fn new(
904        player: Arc<Player>,
905        world: Arc<World>,
906        entity: PersistentEntity,
907        residence_token: DomainResidenceToken,
908    ) -> Option<Self> {
909        let chunk = persistent_entity_chunk(&entity)?;
910        let uuid = Uuid::from_bytes(entity.uuid);
911        let request = world.chunk_map.request_chunk(
912            chunk,
913            ChunkStatus::StructureStarts,
914            ChunkTicketKind::PlayerSpawn,
915        );
916        Some(Self {
917            player,
918            world,
919            request,
920            uuid,
921            residence_token,
922        })
923    }
924}
925
926impl ServerJob for EnderPearlRestoreJob {
927    fn poll(&mut self, _context: &mut ServerJobContext) -> JobPoll {
928        let Some(player) = self.player.session.current_player() else {
929            return JobPoll::Finished;
930        };
931        // The pearl may live in another world in the same domain, so require a
932        // live same-domain owner rather than membership in the pearl's exact world.
933        if player.connection.closed() || !player.is_domain_residence_current(self.residence_token) {
934            return JobPoll::Finished;
935        }
936        let Some(server) = player.server.upgrade() else {
937            return JobPoll::Finished;
938        };
939        if !server.owns_online_player(&player) {
940            return JobPoll::Finished;
941        }
942        let Some(player_world) = server.live_world_for_player(&player) else {
943            // End credits temporarily detaches a connected player without ending
944            // their domain residence. Retain the payload and resume after respawn.
945            return JobPoll::Pending;
946        };
947        if player_world.domain() != self.world.domain() {
948            tracing::error!(
949                player = %player.gameprofile.name(),
950                player_domain = player_world.domain(),
951                pearl_domain = self.world.domain(),
952                "Discarding a pending ender pearl whose owner changed domains without a new residence"
953            );
954            player.discard_pending_ender_pearl(self.residence_token, self.uuid);
955            return JobPoll::Finished;
956        }
957
958        match self.request.poll() {
959            ChunkRequestState::Pending { .. } => JobPoll::Pending,
960            ChunkRequestState::Cancelled => JobPoll::Finished,
961            ChunkRequestState::Ready => {
962                if self.request.ready_chunks().is_none() {
963                    return JobPoll::Pending;
964                }
965                let Some(pearl) = player.take_matching_pending_ender_pearl(
966                    self.residence_token,
967                    &self.world,
968                    self.uuid,
969                ) else {
970                    return JobPoll::Finished;
971                };
972                restore_ender_pearl_for_player(&player, &self.world, &pearl.entity);
973                JobPoll::Finished
974            }
975        }
976    }
977
978    fn cancel(&mut self) {
979        self.request.cancel();
980    }
981}
982
983fn restore_ender_pearl_for_player(
984    player: &Arc<Player>,
985    world: &Arc<World>,
986    entity: &PersistentEntity,
987) -> bool {
988    let Some(chunk) = persistent_entity_chunk(entity) else {
989        return false;
990    };
991    let level = Arc::downgrade(world);
992    let entities = ChunkStorage::persistent_to_entity_tree_at_level(entity, chunk, &level);
993    let Some(pearl) = entities.first().cloned() else {
994        tracing::warn!(
995            player = %player.gameprofile.name(),
996            "Persisted ender pearl did not recreate a runtime entity",
997        );
998        return false;
999    };
1000    if pearl.entity_type() != &vanilla_entities::ENDER_PEARL {
1001        tracing::warn!(
1002            player = %player.gameprofile.name(),
1003            entity_type = ?pearl.entity_type().key,
1004            "Persisted ender pearl recreated a non-pearl root entity",
1005        );
1006        return false;
1007    }
1008
1009    let owner: SharedEntity = Arc::<Player>::clone(player);
1010    for entity in &entities {
1011        entity.restore_owner_reference(&owner);
1012    }
1013
1014    if let Err(error) = world.register_loaded_entity_tree(&entities) {
1015        tracing::warn!(
1016            player = %player.gameprofile.name(),
1017            "Discarding persisted ender pearl because it could not be registered: {error}",
1018        );
1019        discard_restored_entities(&entities);
1020        return false;
1021    }
1022
1023    player.register_ender_pearl(&pearl);
1024    world.chunk_map.place_ender_pearl_ticket(chunk);
1025    world.mark_chunk_dirty(chunk);
1026    true
1027}