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, 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    /// 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    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    /// Queues a player domain switch for processing at the server tick safe point.
442    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    /// Queues a world change to be processed after the current tick.
598    pub fn queue_world_change(&self, entity: SharedEntity, request: WorldChangeRequest) {
599        self.pending_world_changes.lock().push((entity, request));
600    }
601}