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