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, WorldSpawnTeleportJob, can_teleport_between_worlds,
6 change_entity_world, clear_pending_world_change, is_allowed_to_enter_portal,
7 is_end_dimension_type, is_nether_dimension_type, mem, nether_portal, portal_entity_still_valid,
8};
9use crate::entity::LivingEntity as _;
10
11impl Server {
12 pub(super) fn process_world_changes(self: &Arc<Self>, tick_count: u64, runs_normally: bool) {
13 let mut changes = mem::take(&mut *self.pending_world_changes.lock());
14 for world in self.worlds.values() {
15 changes.extend(world.drain_world_changes());
16 }
17
18 for (entity, request) in changes {
19 if entity.is_removed() {
20 continue;
21 }
22 match request {
23 WorldChangeRequest::Computed(transition) => {
24 change_entity_world(entity, &transition);
25 }
26 WorldChangeRequest::WorldSpawn {
27 target_world,
28 pending_token,
29 } => self.process_player_world_selection(
30 entity,
31 target_world,
32 pending_token,
33 tick_count,
34 runs_normally,
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 !is_end_dimension_type(&source_world) {
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 = is_end_dimension_type(&source_world);
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(
399 &self,
400 player: Arc<Player>,
401 target_world: Arc<World>,
402 ) -> Result<(), String> {
403 let target_world = self
404 .worlds
405 .get(&target_world.key)
406 .filter(|registered| Arc::ptr_eq(registered, &target_world))
407 .cloned()
408 .ok_or_else(|| "target world is not the registered loaded world".to_owned())?;
409 let target_domain = target_world.domain().to_owned();
410 if !self.worlds.has_domain(&target_domain) {
411 return Err(format!("unknown domain {target_domain}"));
412 }
413 let (current_world, pending_token) = self.begin_player_relocation(&player)?;
414
415 if current_world.domain() == target_domain {
416 self.pending_world_changes.lock().push((
417 player,
418 WorldChangeRequest::WorldSpawn {
419 target_world,
420 pending_token,
421 },
422 ));
423 return Ok(());
424 }
425
426 if !player.begin_domain_switch(pending_token) {
427 player.finish_pending_world_change(pending_token);
428 return Err("domain switch already in progress".to_owned());
429 }
430 self.pending_domain_switches
431 .lock()
432 .push(DomainSwitchRequest {
433 player,
434 target_domain,
435 target_world: Some(target_world),
436 pending_token,
437 });
438 Ok(())
439 }
440
441 pub fn queue_domain_switch(
443 &self,
444 player: Arc<Player>,
445 target_domain: String,
446 ) -> Result<(), String> {
447 if !self.worlds.has_domain(&target_domain) {
448 return Err(format!("unknown domain {target_domain}"));
449 }
450
451 let (current_world, pending_token) = self.begin_player_relocation(&player)?;
452 let current_domain = current_world.domain().to_owned();
453 if current_domain == target_domain {
454 player.finish_pending_world_change(pending_token);
455 return Err(format!("already in domain {target_domain}"));
456 }
457 if !player.begin_domain_switch(pending_token) {
458 player.finish_pending_world_change(pending_token);
459 return Err("domain switch already in progress".to_owned());
460 }
461
462 self.pending_domain_switches
463 .lock()
464 .push(DomainSwitchRequest {
465 player,
466 target_domain,
467 target_world: None,
468 pending_token,
469 });
470 Ok(())
471 }
472
473 pub(super) fn process_domain_switches(self: &Arc<Self>) {
474 let switches = mem::take(&mut *self.pending_domain_switches.lock());
475
476 for request in switches {
477 let player = Arc::clone(&request.player);
478 let player_name = player.gameprofile.name.clone();
479 let pending_token = request.pending_token;
480 if let Err(error) = self.start_domain_switch(request) {
481 player.finish_domain_switch(pending_token);
482 clear_pending_world_change(&(Arc::clone(&player) as SharedEntity), pending_token);
483 log::warn!("Did not start domain switch for {player_name}: {error}");
484 }
485 }
486 }
487
488 fn start_domain_switch(self: &Arc<Self>, request: DomainSwitchRequest) -> Result<(), String> {
489 let DomainSwitchRequest {
490 player,
491 target_domain,
492 target_world,
493 pending_token,
494 } = request;
495 if player.connection.closed() {
496 return Err("player is disconnecting".to_owned());
497 }
498 if !player.is_domain_switch_queued(pending_token)
499 || !player.is_world_change_token_pending(pending_token)
500 {
501 return Err("domain switch no longer owns the player relocation".to_owned());
502 }
503 if !self.worlds.has_domain(&target_domain) {
504 return Err(format!("unknown domain {target_domain}"));
505 }
506
507 let current_world = self
508 .live_world_for_player(&player)
509 .ok_or_else(|| "player is not present in a live world".to_owned())?;
510 let current_domain = current_world.domain().to_owned();
511 if current_domain == target_domain {
512 return Err(format!("already in domain {target_domain}"));
513 }
514 if player.get_health() <= 0.0 {
515 return Err("player died before the domain switch started".to_owned());
516 }
517 if player.has_won_game() {
518 return Err("player entered the End credits screen".to_owned());
519 }
520 if let Some(target_world) = target_world.as_ref() {
521 let registered = self
522 .worlds
523 .get(&target_world.key)
524 .ok_or_else(|| "target world is no longer loaded".to_owned())?;
525 if !Arc::ptr_eq(registered, target_world) || target_world.domain() != target_domain {
526 return Err("target world registration changed before the switch".to_owned());
527 }
528 }
529
530 if player.remove_all_menus() != MenuRemovalStatus::Complete {
531 return Err("cannot save domain data while a menu callback is active".to_owned());
532 }
533 if !self.reserve_player_relocation(&player) {
534 return Err("domain switch could not reserve player persistence".to_owned());
535 }
536 if !player.mark_domain_switch_detached(pending_token) {
537 self.release_player_admission(player.gameprofile.id, PlayerAdmissionState::Relocating);
538 return Err("domain switch lost ownership before detaching".to_owned());
539 }
540 let Some((current_data, residence_token)) =
541 current_world.detach_player_for_domain_switch(&player)
542 else {
543 self.release_player_admission(player.gameprofile.id, PlayerAdmissionState::Relocating);
544 return Err("player is not present in the current world".to_owned());
545 };
546 self.jobs.spawn(DomainSwitchJob::new(
547 self,
548 player,
549 current_domain,
550 current_data,
551 target_domain,
552 target_world,
553 pending_token,
554 residence_token,
555 ));
556
557 Ok(())
558 }
559
560 fn process_player_world_selection(
561 self: &Arc<Self>,
562 entity: SharedEntity,
563 target_world: Arc<World>,
564 pending_token: PendingWorldChangeToken,
565 tick_count: u64,
566 runs_normally: bool,
567 ) {
568 if !entity.is_world_change_token_pending(pending_token) {
569 return;
570 }
571 let Some(player) = entity.as_player() else {
572 tracing::error!("world selection request does not belong to a player");
573 clear_pending_world_change(&entity, pending_token);
574 return;
575 };
576 let Some(source_world) = self.live_world_for_player(player) else {
577 clear_pending_world_change(&entity, pending_token);
578 return;
579 };
580 let job = match WorldSpawnTeleportJob::new(
581 Arc::clone(&entity),
582 source_world,
583 target_world,
584 pending_token,
585 ) {
586 Ok(job) => job,
587 Err(error) => {
588 clear_pending_world_change(&entity, pending_token);
589 log::warn!("Did not start player world selection: {error}");
590 return;
591 }
592 };
593 self.jobs
594 .poll_now_or_spawn(Arc::downgrade(self), tick_count, runs_normally, job);
595 }
596
597 pub fn queue_world_change(&self, entity: SharedEntity, request: WorldChangeRequest) {
599 self.pending_world_changes.lock().push((entity, request));
600 }
601}