1use super::{
2 Arc, BlockPos, DomainSwitchJob, DomainSwitchRequest, EndGatewayTeleportJob,
3 EndPortalTeleportJob, Entity, MenuRemovalStatus, NetherPortalTeleportJob, NetworkConnection,
4 PendingWorldChangeToken, Player, PlayerAdmissionState, PortalKind, RespawnData, Server,
5 SharedEntity, World, WorldChangeRequest, WorldEntry, WorldSelection, WorldSpawnTeleportJob,
6 can_teleport_between_worlds, change_entity_world, clear_pending_world_change,
7 is_allowed_to_enter_portal, is_nether_dimension_type, mem, nether_portal,
8 portal_entity_still_valid,
9};
10use crate::entity::LivingEntity as _;
11use crate::world::player_spawn_finder::PlayerSpawnSearch;
12
13impl Server {
14 pub(super) fn process_world_changes(self: &Arc<Self>, tick_count: u64, runs_normally: bool) {
15 let mut changes = mem::take(&mut *self.pending_world_changes.lock());
16 for world in self.worlds.values() {
17 changes.extend(world.drain_world_changes());
18 }
19
20 for (entity, request) in changes {
21 if entity.is_removed() {
22 continue;
23 }
24 match request {
25 WorldChangeRequest::Computed(transition) => {
26 change_entity_world(entity, &transition);
27 }
28 WorldChangeRequest::WorldSpawn(selection) => {
29 self.process_player_world_selection(
30 entity,
31 selection,
32 tick_count,
33 runs_normally,
34 );
35 }
36 WorldChangeRequest::Portal {
37 portal: PortalKind::Nether,
38 source_world,
39 portal_pos,
40 pending_token,
41 } => {
42 self.queue_nether_portal_change(
43 entity,
44 source_world,
45 portal_pos,
46 pending_token,
47 tick_count,
48 runs_normally,
49 );
50 }
51 WorldChangeRequest::Portal {
52 portal: PortalKind::End,
53 source_world,
54 portal_pos: _,
55 pending_token,
56 } => {
57 self.queue_end_portal_change(
58 entity,
59 source_world,
60 pending_token,
61 tick_count,
62 runs_normally,
63 );
64 }
65 WorldChangeRequest::Portal {
66 portal: PortalKind::EndGateway,
67 source_world,
68 portal_pos,
69 pending_token,
70 } => {
71 self.queue_end_gateway_change(
72 entity,
73 source_world,
74 portal_pos,
75 pending_token,
76 tick_count,
77 runs_normally,
78 );
79 }
80 }
81 }
82 }
83
84 fn queue_nether_portal_change(
85 self: &Arc<Self>,
86 entity: SharedEntity,
87 source_world: Arc<World>,
88 portal_pos: BlockPos,
89 pending_token: PendingWorldChangeToken,
90 tick_count: u64,
91 runs_normally: bool,
92 ) {
93 if !portal_entity_still_valid(&entity, &source_world, pending_token) {
94 clear_pending_world_change(&entity, pending_token);
95 return;
96 }
97 let Some(target_world) = self.worlds.resolve_nether_portal_target(&source_world) else {
98 log::warn!(
99 "No Nether portal target world loaded for source world {}",
100 source_world.key
101 );
102 clear_pending_world_change(&entity, pending_token);
103 return;
104 };
105 if !is_allowed_to_enter_portal(&source_world, &target_world)
106 || !self.can_teleport_between_worlds(entity.as_ref(), &source_world, &target_world)
107 {
108 clear_pending_world_change(&entity, pending_token);
109 return;
110 }
111 let to_nether = is_nether_dimension_type(&target_world);
112 let approximate_exit_pos = nether_portal::approximate_exit_position(
113 &source_world,
114 &target_world,
115 entity.position(),
116 );
117 self.jobs.poll_now_or_spawn(
118 Arc::downgrade(self),
119 tick_count,
120 runs_normally,
121 NetherPortalTeleportJob::new(
122 entity,
123 source_world,
124 target_world,
125 portal_pos,
126 approximate_exit_pos,
127 to_nether,
128 pending_token,
129 ),
130 );
131 }
132
133 fn queue_end_portal_change(
134 self: &Arc<Self>,
135 entity: SharedEntity,
136 source_world: Arc<World>,
137 pending_token: PendingWorldChangeToken,
138 tick_count: u64,
139 runs_normally: bool,
140 ) {
141 if !portal_entity_still_valid(&entity, &source_world, pending_token) {
142 clear_pending_world_change(&entity, pending_token);
143 return;
144 }
145 if !source_world.is_end_dimension_type() {
146 self.queue_end_entry_portal_change(
147 entity,
148 source_world,
149 pending_token,
150 tick_count,
151 runs_normally,
152 );
153 return;
154 }
155
156 if entity.as_player().is_some() {
157 self.queue_end_portal_player_return_change(
158 entity,
159 source_world,
160 pending_token,
161 tick_count,
162 runs_normally,
163 );
164 return;
165 }
166
167 self.queue_end_portal_entity_return_change(
168 entity,
169 source_world,
170 pending_token,
171 tick_count,
172 runs_normally,
173 );
174 }
175
176 fn queue_end_entry_portal_change(
177 self: &Arc<Self>,
178 entity: SharedEntity,
179 source_world: Arc<World>,
180 pending_token: PendingWorldChangeToken,
181 tick_count: u64,
182 runs_normally: bool,
183 ) {
184 let Some(target_world) = self.worlds.resolve_end_entry_portal_target(&source_world) else {
185 log::warn!(
186 "No End portal target world loaded for source world {}",
187 source_world.key
188 );
189 clear_pending_world_change(&entity, pending_token);
190 return;
191 };
192 if !is_allowed_to_enter_portal(&source_world, &target_world)
193 || !self.can_teleport_between_worlds(entity.as_ref(), &source_world, &target_world)
194 {
195 clear_pending_world_change(&entity, pending_token);
196 return;
197 }
198 self.jobs.poll_now_or_spawn(
199 Arc::downgrade(self),
200 tick_count,
201 runs_normally,
202 EndPortalTeleportJob::entry_to_end(entity, source_world, target_world, pending_token),
203 );
204 }
205
206 fn queue_end_portal_player_return_change(
207 self: &Arc<Self>,
208 entity: SharedEntity,
209 source_world: Arc<World>,
210 pending_token: PendingWorldChangeToken,
211 tick_count: u64,
212 runs_normally: bool,
213 ) {
214 let (target_world, respawn_data) =
215 match self.strict_respawn_world_and_data_for_domain(source_world.domain()) {
216 Ok(resolved) => resolved,
217 Err(error) => {
218 log::warn!(
219 "No End portal return target world loaded for source world {}: {error}",
220 source_world.key
221 );
222 clear_pending_world_change(&entity, pending_token);
223 return;
224 }
225 };
226 if !is_allowed_to_enter_portal(&source_world, &target_world)
227 || !self.can_teleport_between_worlds(entity.as_ref(), &source_world, &target_world)
228 {
229 clear_pending_world_change(&entity, pending_token);
230 return;
231 }
232 match EndPortalTeleportJob::returning_player(
233 Arc::clone(&entity),
234 source_world,
235 target_world,
236 respawn_data,
237 pending_token,
238 ) {
239 Ok(job) => {
240 self.jobs
241 .poll_now_or_spawn(Arc::downgrade(self), tick_count, runs_normally, job);
242 }
243 Err(error) => {
244 clear_pending_world_change(&entity, pending_token);
245 log::error!("Failed to schedule End portal player return: {error}");
246 }
247 }
248 }
249
250 fn queue_end_portal_entity_return_change(
251 self: &Arc<Self>,
252 entity: SharedEntity,
253 source_world: Arc<World>,
254 pending_token: PendingWorldChangeToken,
255 tick_count: u64,
256 runs_normally: bool,
257 ) {
258 let (target_world, respawn_data) =
259 match self.strict_respawn_world_and_data_for_domain(source_world.domain()) {
260 Ok(resolved) => resolved,
261 Err(error) => {
262 log::warn!(
263 "No End portal return target world loaded for source world {}: {error}",
264 source_world.key
265 );
266 clear_pending_world_change(&entity, pending_token);
267 return;
268 }
269 };
270 if !is_allowed_to_enter_portal(&source_world, &target_world)
271 || !self.can_teleport_between_worlds(entity.as_ref(), &source_world, &target_world)
272 {
273 clear_pending_world_change(&entity, pending_token);
274 return;
275 }
276 self.jobs.poll_now_or_spawn(
277 Arc::downgrade(self),
278 tick_count,
279 runs_normally,
280 EndPortalTeleportJob::returning_entity(
281 entity,
282 source_world,
283 target_world,
284 respawn_data,
285 pending_token,
286 ),
287 );
288 }
289
290 fn queue_end_gateway_change(
291 self: &Arc<Self>,
292 entity: SharedEntity,
293 source_world: Arc<World>,
294 portal_pos: BlockPos,
295 pending_token: PendingWorldChangeToken,
296 tick_count: u64,
297 runs_normally: bool,
298 ) {
299 if !portal_entity_still_valid(&entity, &source_world, pending_token) {
300 clear_pending_world_change(&entity, pending_token);
301 return;
302 }
303 let source_is_end = source_world.is_end_dimension_type();
304 let Some(job) = EndGatewayTeleportJob::new(
305 Arc::clone(&entity),
306 source_world,
307 portal_pos,
308 source_is_end,
309 pending_token,
310 ) else {
311 tracing::debug!("End gateway world change ignored because no destination is available");
312 clear_pending_world_change(&entity, pending_token);
313 return;
314 };
315 self.jobs
316 .poll_now_or_spawn(Arc::downgrade(self), tick_count, runs_normally, job);
317 }
318
319 pub(super) fn can_teleport_between_worlds(
320 &self,
321 entity: &dyn Entity,
322 source_world: &World,
323 target_world: &World,
324 ) -> bool {
325 can_teleport_between_worlds(entity, source_world, target_world, |uuid| {
326 self.projectile_owner_seen_credits_in_domain(source_world.domain(), uuid)
327 })
328 }
329
330 fn projectile_owner_seen_credits_in_domain(
331 &self,
332 domain: &str,
333 uuid: &uuid::Uuid,
334 ) -> Option<bool> {
335 self.worlds
336 .values()
337 .filter(|world| world.domain() == domain)
338 .find_map(|world| {
339 world
340 .get_entity_by_uuid(uuid)
341 .and_then(|entity| entity.as_player().map(Player::has_seen_credits))
342 })
343 }
344
345 fn strict_respawn_world_and_data_for_domain(
346 &self,
347 domain: &str,
348 ) -> Result<(Arc<World>, RespawnData), String> {
349 let default_world = self
350 .worlds
351 .default_world(domain)
352 .cloned()
353 .ok_or_else(|| format!("domain {domain} has no default world"))?;
354 let respawn_data = {
355 let level_data = default_world.level_data.read();
356 level_data.data().respawn_data_or_local(&default_world.key)
357 };
358 let target_world = self
359 .worlds
360 .get(respawn_data.dimension())
361 .filter(|world| world.domain() == domain)
362 .cloned()
363 .ok_or_else(|| {
364 format!(
365 "respawn dimension {} is not loaded in domain {domain}",
366 respawn_data.dimension()
367 )
368 })?;
369 Ok((target_world, respawn_data))
370 }
371
372 fn begin_player_relocation(
373 &self,
374 player: &Player,
375 ) -> Result<(Arc<World>, PendingWorldChangeToken), String> {
376 let current_world = self
377 .live_world_for_player(player)
378 .ok_or_else(|| "player is not present in a live world".to_owned())?;
379 if player.connection.closed() {
380 return Err("player is disconnecting".to_owned());
381 }
382 if player.get_health() <= 0.0 {
383 return Err("cannot change worlds while dead".to_owned());
384 }
385 if player.has_won_game() {
386 return Err("cannot change worlds from the End credits screen".to_owned());
387 }
388 let Some(pending_token) = player.begin_pending_world_change() else {
389 return Err("another player relocation is already in progress".to_owned());
390 };
391 Ok((current_world, pending_token))
392 }
393
394 pub fn queue_player_world_selection(
400 &self,
401 player: Arc<Player>,
402 target_world: &Arc<World>,
403 entry: WorldEntry,
404 ) -> Result<(), String> {
405 let target_world = self
406 .worlds
407 .get(&target_world.key)
408 .filter(|registered| Arc::ptr_eq(registered, target_world))
409 .cloned()
410 .ok_or_else(|| "target world is not the registered loaded world".to_owned())?;
411 let target_domain = target_world.domain().to_owned();
412 if !self.worlds.has_domain(&target_domain) {
413 return Err(format!("unknown domain {target_domain}"));
414 }
415 let (current_world, pending_token) = self.begin_player_relocation(&player)?;
416
417 if current_world.domain() == target_domain {
418 let prefetch = if matches!(entry, WorldEntry::Exact(_)) {
419 None
420 } else {
421 let spawn_suggestion = target_world.level_data.read().data().spawn_pos();
422 PlayerSpawnSearch::prefetch_spawn_area(
423 &target_world,
424 spawn_suggestion,
425 target_world.default_gamemode(),
426 )
427 };
428 self.pending_world_changes.lock().push((
429 player,
430 WorldChangeRequest::WorldSpawn(WorldSelection {
431 target_world,
432 pending_token,
433 prefetch,
434 entry,
435 }),
436 ));
437 return Ok(());
438 }
439
440 if !player.begin_domain_switch(pending_token) {
441 player.finish_pending_world_change(pending_token);
442 return Err("domain switch already in progress".to_owned());
443 }
444 self.pending_domain_switches
445 .lock()
446 .push(DomainSwitchRequest {
447 player,
448 target_domain,
449 target_world: Some(target_world),
450 entry,
451 pending_token,
452 });
453 Ok(())
454 }
455
456 pub fn queue_domain_switch(
458 &self,
459 player: Arc<Player>,
460 target_domain: String,
461 ) -> Result<(), String> {
462 if !self.worlds.has_domain(&target_domain) {
463 return Err(format!("unknown domain {target_domain}"));
464 }
465
466 let (current_world, pending_token) = self.begin_player_relocation(&player)?;
467 let current_domain = current_world.domain().to_owned();
468 if current_domain == target_domain {
469 player.finish_pending_world_change(pending_token);
470 return Err(format!("already in domain {target_domain}"));
471 }
472 if !player.begin_domain_switch(pending_token) {
473 player.finish_pending_world_change(pending_token);
474 return Err("domain switch already in progress".to_owned());
475 }
476
477 self.pending_domain_switches
478 .lock()
479 .push(DomainSwitchRequest {
480 player,
481 target_domain,
482 target_world: None,
483 entry: WorldEntry::Usual,
484 pending_token,
485 });
486 Ok(())
487 }
488
489 pub(super) fn process_domain_switches(self: &Arc<Self>) {
490 let switches = mem::take(&mut *self.pending_domain_switches.lock());
491
492 for request in switches {
493 let player = Arc::clone(&request.player);
494 let player_name = player.gameprofile.name().to_owned();
495 let pending_token = request.pending_token;
496 if let Err(error) = self.start_domain_switch(request) {
497 player.finish_domain_switch(pending_token);
498 clear_pending_world_change(&(Arc::clone(&player) as SharedEntity), pending_token);
499 log::warn!("Did not start domain switch for {player_name}: {error}");
500 }
501 }
502 }
503
504 fn start_domain_switch(self: &Arc<Self>, request: DomainSwitchRequest) -> Result<(), String> {
505 let DomainSwitchRequest {
506 player,
507 target_domain,
508 target_world,
509 entry,
510 pending_token,
511 } = request;
512 if player.connection.closed() {
513 return Err("player is disconnecting".to_owned());
514 }
515 if !player.is_domain_switch_queued(pending_token)
516 || !player.is_world_change_token_pending(pending_token)
517 {
518 return Err("domain switch no longer owns the player relocation".to_owned());
519 }
520 if !self.worlds.has_domain(&target_domain) {
521 return Err(format!("unknown domain {target_domain}"));
522 }
523
524 let current_world = self
525 .live_world_for_player(&player)
526 .ok_or_else(|| "player is not present in a live world".to_owned())?;
527 let current_domain = current_world.domain().to_owned();
528 if current_domain == target_domain {
529 return Err(format!("already in domain {target_domain}"));
530 }
531 if player.get_health() <= 0.0 {
532 return Err("player died before the domain switch started".to_owned());
533 }
534 if player.has_won_game() {
535 return Err("player entered the End credits screen".to_owned());
536 }
537 if let Some(target_world) = target_world.as_ref() {
538 let registered = self
539 .worlds
540 .get(&target_world.key)
541 .ok_or_else(|| "target world is no longer loaded".to_owned())?;
542 if !Arc::ptr_eq(registered, target_world) || target_world.domain() != target_domain {
543 return Err("target world registration changed before the switch".to_owned());
544 }
545 }
546
547 if player.remove_all_menus() != MenuRemovalStatus::Complete {
548 return Err("cannot save domain data while a menu callback is active".to_owned());
549 }
550 if !self.reserve_player_relocation(&player) {
551 return Err("domain switch could not reserve player persistence".to_owned());
552 }
553 if !player.mark_domain_switch_detached(pending_token) {
554 self.release_player_admission(
555 player.gameprofile.id(),
556 PlayerAdmissionState::Relocating,
557 );
558 return Err("domain switch lost ownership before detaching".to_owned());
559 }
560 let Some((current_data, residence_token)) =
561 current_world.detach_player_for_domain_switch(&player)
562 else {
563 self.release_player_admission(
564 player.gameprofile.id(),
565 PlayerAdmissionState::Relocating,
566 );
567 return Err("player is not present in the current world".to_owned());
568 };
569 self.jobs.spawn(DomainSwitchJob::new(
570 self,
571 player,
572 current_domain,
573 current_data,
574 target_domain,
575 target_world,
576 entry,
577 pending_token,
578 residence_token,
579 ));
580
581 Ok(())
582 }
583
584 fn process_player_world_selection(
585 self: &Arc<Self>,
586 entity: SharedEntity,
587 selection: WorldSelection,
588 tick_count: u64,
589 runs_normally: bool,
590 ) {
591 let WorldSelection {
592 target_world,
593 pending_token,
594 prefetch,
595 entry,
596 } = selection;
597 if !entity.is_world_change_token_pending(pending_token) {
598 return;
599 }
600 let Some(player) = entity.as_player() else {
601 tracing::error!("world selection request does not belong to a player");
602 clear_pending_world_change(&entity, pending_token);
603 return;
604 };
605 let Some(source_world) = self.live_world_for_player(player) else {
606 clear_pending_world_change(&entity, pending_token);
607 return;
608 };
609 let job = match WorldSpawnTeleportJob::new(
610 Arc::clone(&entity),
611 source_world,
612 target_world,
613 pending_token,
614 prefetch,
615 entry,
616 ) {
617 Ok(job) => job,
618 Err(error) => {
619 clear_pending_world_change(&entity, pending_token);
620 log::warn!("Did not start player world selection: {error}");
621 return;
622 }
623 };
624 self.jobs
625 .poll_now_or_spawn(Arc::downgrade(self), tick_count, runs_normally, job);
626 }
627
628 pub fn queue_world_change(&self, entity: SharedEntity, request: WorldChangeRequest) {
630 self.pending_world_changes.lock().push((entity, request));
631 }
632}