Skip to main content

steel_core/server/
world_changes.rs

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    /// Queues a selected loaded world through the player's relocation lease.
395    ///
396    /// Same-domain selections move to the world's spawn. Cross-domain
397    /// selections restore that domain while honoring the explicit target.
398    /// `entry` can instead place the player exactly, or only change their rotation.
399    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    /// Queues a player domain switch for processing at the server tick safe point.
457    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    /// Queues a world change to be processed after the current tick.
629    pub fn queue_world_change(&self, entity: SharedEntity, request: WorldChangeRequest) {
630        self.pending_world_changes.lock().push((entity, request));
631    }
632}