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