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 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 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
892pub(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 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 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}