Skip to main content

steel_core/server/
worlds.rs

1//! Domain-aware loaded world map.
2
3use std::sync::Arc;
4
5use rustc_hash::FxHashMap;
6use small_map::FxSmallMap;
7use steel_utils::Identifier;
8
9use crate::config::{ResolvedDomainConfig, ResolvedWorldConfig};
10use crate::world::World;
11
12pub(crate) const OVERWORLD_WORLD_NAME: &str = "overworld";
13pub(crate) const NETHER_WORLD_NAME: &str = "the_nether";
14pub(crate) const END_WORLD_NAME: &str = "the_end";
15
16/// Loaded worlds plus domain defaults.
17pub struct WorldMap {
18    worlds: FxSmallMap<8, Identifier, Arc<World>>,
19    default_domain: String,
20    default_worlds: FxHashMap<String, Identifier>,
21    nether_portal_targets: FxHashMap<Identifier, Identifier>,
22    end_portal_targets: FxHashMap<Identifier, Identifier>,
23}
24
25impl WorldMap {
26    /// Creates a world map from resolved domain config.
27    #[must_use]
28    pub fn new(
29        default_domain: String,
30        domains: &[ResolvedDomainConfig],
31        world_configs: &[ResolvedWorldConfig],
32    ) -> Self {
33        let mut default_worlds = FxHashMap::default();
34        for domain in domains {
35            default_worlds.insert(domain.name.clone(), domain.default_world.clone());
36        }
37        let mut nether_portal_targets = FxHashMap::default();
38        let mut end_portal_targets = FxHashMap::default();
39        for world in world_configs {
40            if let Some(target) = &world.nether_portal_target {
41                nether_portal_targets.insert(world.key.clone(), target.clone());
42            }
43            if let Some(target) = &world.end_portal_target {
44                end_portal_targets.insert(world.key.clone(), target.clone());
45            }
46        }
47        Self {
48            worlds: FxSmallMap::default(),
49            default_domain,
50            default_worlds,
51            nether_portal_targets,
52            end_portal_targets,
53        }
54    }
55
56    /// Inserts a loaded world.
57    pub fn insert(&mut self, key: Identifier, world: Arc<World>) {
58        self.worlds.insert(key, world);
59    }
60
61    /// Returns a world by loaded world identifier.
62    #[must_use]
63    pub fn get(&self, key: &Identifier) -> Option<&Arc<World>> {
64        self.worlds.get(key)
65    }
66
67    /// Iterates loaded world values.
68    pub fn values(&self) -> impl Iterator<Item = &Arc<World>> {
69        self.worlds.values()
70    }
71
72    /// Iterates loaded world keys.
73    pub fn keys(&self) -> impl Iterator<Item = &Identifier> {
74        self.worlds.keys()
75    }
76
77    /// Iterates loaded world key/value pairs.
78    pub fn iter(&self) -> impl Iterator<Item = (&Identifier, &Arc<World>)> {
79        self.worlds.iter()
80    }
81
82    /// Returns number of loaded worlds.
83    #[must_use]
84    pub fn len(&self) -> usize {
85        self.worlds.len()
86    }
87
88    /// Returns whether there are no loaded worlds.
89    #[must_use]
90    pub fn is_empty(&self) -> bool {
91        self.worlds.is_empty()
92    }
93
94    /// Returns the default domain name.
95    #[must_use]
96    pub fn default_domain(&self) -> &str {
97        &self.default_domain
98    }
99
100    /// Returns whether a domain exists.
101    #[must_use]
102    pub fn has_domain(&self, domain: &str) -> bool {
103        self.default_worlds.contains_key(domain)
104    }
105
106    /// Iterates domain names.
107    pub fn domain_names(&self) -> impl Iterator<Item = &str> {
108        self.default_worlds.keys().map(String::as_str)
109    }
110
111    /// Returns a domain's default world.
112    #[must_use]
113    pub fn default_world(&self, domain: &str) -> Option<&Arc<World>> {
114        self.default_worlds
115            .get(domain)
116            .and_then(|key| self.worlds.get(key))
117    }
118
119    /// Returns the server default world.
120    #[must_use]
121    pub fn server_default_world(&self) -> Option<&Arc<World>> {
122        self.default_world(self.default_domain())
123    }
124
125    /// Returns loaded worlds in the given domain.
126    #[must_use]
127    pub fn worlds_in_domain(&self, domain: &str) -> Vec<Arc<World>> {
128        self.worlds
129            .values()
130            .filter(|world| world.domain() == domain)
131            .cloned()
132            .collect()
133    }
134
135    /// Resolves a conventional portal target name in the source world's domain.
136    #[must_use]
137    pub fn resolve_portal_target(
138        &self,
139        source_world: &World,
140        target_world_name: &str,
141    ) -> Option<Arc<World>> {
142        let key = Identifier::new(
143            source_world.domain().to_owned(),
144            target_world_name.to_owned(),
145        );
146        self.worlds.get(&key).cloned()
147    }
148
149    /// Resolves the vanilla Nether portal target in the source world's domain.
150    #[must_use]
151    pub fn resolve_nether_portal_target(&self, source_world: &World) -> Option<Arc<World>> {
152        if let Some(target) = self.nether_portal_targets.get(&source_world.key) {
153            return self.worlds.get(target).cloned();
154        }
155
156        self.resolve_portal_target(
157            source_world,
158            nether_portal_target_world_name(source_world.key.path.as_ref()),
159        )
160    }
161
162    /// Resolves the vanilla End portal target for non-End source worlds.
163    ///
164    /// End-to-respawn-world transitions depend on the source world's respawn data,
165    /// so that branch is intentionally left to the destination calculator.
166    #[must_use]
167    pub fn resolve_end_entry_portal_target(&self, source_world: &World) -> Option<Arc<World>> {
168        if let Some(target) = self.end_portal_targets.get(&source_world.key) {
169            return self.worlds.get(target).cloned();
170        }
171
172        end_entry_portal_target_world_name(source_world.key.path.as_ref())
173            .and_then(|target| self.resolve_portal_target(source_world, target))
174    }
175}
176
177fn nether_portal_target_world_name(source_world_name: &str) -> &'static str {
178    if source_world_name == NETHER_WORLD_NAME {
179        OVERWORLD_WORLD_NAME
180    } else {
181        NETHER_WORLD_NAME
182    }
183}
184
185fn end_entry_portal_target_world_name(source_world_name: &str) -> Option<&'static str> {
186    if source_world_name == END_WORLD_NAME {
187        None
188    } else {
189        Some(END_WORLD_NAME)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::{end_entry_portal_target_world_name, nether_portal_target_world_name};
196
197    #[test]
198    fn nether_portal_target_names_follow_vanilla_level_keys() {
199        assert_eq!(nether_portal_target_world_name("overworld"), "the_nether");
200        assert_eq!(nether_portal_target_world_name("the_end"), "the_nether");
201        assert_eq!(nether_portal_target_world_name("the_nether"), "overworld");
202    }
203
204    #[test]
205    fn end_entry_portal_target_name_is_only_for_non_end_sources() {
206        assert_eq!(
207            end_entry_portal_target_world_name("overworld"),
208            Some("the_end")
209        );
210        assert_eq!(
211            end_entry_portal_target_world_name("the_nether"),
212            Some("the_end")
213        );
214        assert_eq!(end_entry_portal_target_world_name("the_end"), None);
215    }
216}