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