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    /// Verifies primary ownership and sharing before the server publishes its worlds.
57    pub(crate) fn validate_game_times(&self) -> Result<(), String> {
58        for domain in self.domain_names() {
59            let primary = self
60                .default_world(domain)
61                .ok_or_else(|| format!("domain {domain} has no loaded primary"))?;
62            if primary.domain() != domain || !primary.level_data.read().owns_game_time() {
63                return Err(format!("domain {domain} has an invalid game-time primary"));
64            }
65            for world in self.values().filter(|world| world.domain() == domain) {
66                if !Arc::ptr_eq(&primary.game_time, &world.game_time)
67                    || (world.key != primary.key && world.level_data.read().owns_game_time())
68                {
69                    return Err(format!(
70                        "world {} is not bound to domain {domain}'s primary clock",
71                        world.key
72                    ));
73                }
74            }
75        }
76        for world in self.values() {
77            if !self.has_domain(world.domain()) {
78                return Err(format!("world {} has no configured domain", world.key));
79            }
80        }
81        Ok(())
82    }
83
84    /// Publishes one simulation increment for every domain before any worker dispatch.
85    pub(crate) fn advance_domain_game_times(&self) {
86        for key in self.default_worlds.values() {
87            let Some(primary) = self.worlds.get(key) else {
88                panic!("validated domain primary is missing: {key}");
89            };
90            primary.level_data.write().advance_game_time();
91        }
92    }
93
94    /// Inserts a loaded world.
95    pub fn insert(&mut self, key: Identifier, world: Arc<World>) {
96        self.worlds.insert(key, world);
97    }
98
99    /// Returns a world by loaded world identifier.
100    #[must_use]
101    pub fn get(&self, key: &Identifier) -> Option<&Arc<World>> {
102        self.worlds.get(key)
103    }
104
105    /// Iterates loaded world values.
106    pub fn values(&self) -> impl Iterator<Item = &Arc<World>> {
107        self.worlds.values()
108    }
109
110    /// Iterates loaded world keys.
111    pub fn keys(&self) -> impl Iterator<Item = &Identifier> {
112        self.worlds.keys()
113    }
114
115    /// Iterates loaded world key/value pairs.
116    pub fn iter(&self) -> impl Iterator<Item = (&Identifier, &Arc<World>)> {
117        self.worlds.iter()
118    }
119
120    /// Returns number of loaded worlds.
121    #[must_use]
122    pub fn len(&self) -> usize {
123        self.worlds.len()
124    }
125
126    /// Returns whether there are no loaded worlds.
127    #[must_use]
128    pub fn is_empty(&self) -> bool {
129        self.worlds.is_empty()
130    }
131
132    /// Returns the default domain name.
133    #[must_use]
134    pub fn default_domain(&self) -> &str {
135        &self.default_domain
136    }
137
138    /// Returns whether a domain exists.
139    #[must_use]
140    pub fn has_domain(&self, domain: &str) -> bool {
141        self.default_worlds.contains_key(domain)
142    }
143
144    /// Iterates domain names.
145    pub fn domain_names(&self) -> impl Iterator<Item = &str> {
146        self.default_worlds.keys().map(String::as_str)
147    }
148
149    /// Returns a domain's default world.
150    #[must_use]
151    pub fn default_world(&self, domain: &str) -> Option<&Arc<World>> {
152        self.default_worlds
153            .get(domain)
154            .and_then(|key| self.worlds.get(key))
155    }
156
157    /// Returns the server default world.
158    #[must_use]
159    pub fn server_default_world(&self) -> Option<&Arc<World>> {
160        self.default_world(self.default_domain())
161    }
162
163    /// Returns loaded worlds in the given domain.
164    #[must_use]
165    pub fn worlds_in_domain(&self, domain: &str) -> Vec<Arc<World>> {
166        self.worlds
167            .values()
168            .filter(|world| world.domain() == domain)
169            .cloned()
170            .collect()
171    }
172
173    /// Resolves a conventional portal target name in the source world's domain.
174    #[must_use]
175    pub fn resolve_portal_target(
176        &self,
177        source_world: &World,
178        target_world_name: &str,
179    ) -> Option<Arc<World>> {
180        let key = Identifier::new(
181            source_world.domain().to_owned(),
182            target_world_name.to_owned(),
183        );
184        self.worlds.get(&key).cloned()
185    }
186
187    /// Resolves the vanilla Nether portal target in the source world's domain.
188    #[must_use]
189    pub fn resolve_nether_portal_target(&self, source_world: &World) -> Option<Arc<World>> {
190        if let Some(target) = self.nether_portal_targets.get(&source_world.key) {
191            return self.worlds.get(target).cloned();
192        }
193
194        self.resolve_portal_target(
195            source_world,
196            nether_portal_target_world_name(source_world.key.path.as_ref()),
197        )
198    }
199
200    /// Resolves the vanilla End portal target for non-End source worlds.
201    ///
202    /// End-to-respawn-world transitions depend on the source world's respawn data,
203    /// so that branch is intentionally left to the destination calculator.
204    #[must_use]
205    pub fn resolve_end_entry_portal_target(&self, source_world: &World) -> Option<Arc<World>> {
206        if let Some(target) = self.end_portal_targets.get(&source_world.key) {
207            return self.worlds.get(target).cloned();
208        }
209
210        end_entry_portal_target_world_name(source_world.key.path.as_ref())
211            .and_then(|target| self.resolve_portal_target(source_world, target))
212    }
213}
214
215fn nether_portal_target_world_name(source_world_name: &str) -> &'static str {
216    if source_world_name == NETHER_WORLD_NAME {
217        OVERWORLD_WORLD_NAME
218    } else {
219        NETHER_WORLD_NAME
220    }
221}
222
223fn end_entry_portal_target_world_name(source_world_name: &str) -> Option<&'static str> {
224    if source_world_name == END_WORLD_NAME {
225        None
226    } else {
227        Some(END_WORLD_NAME)
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::{end_entry_portal_target_world_name, nether_portal_target_world_name};
234
235    #[test]
236    fn nether_portal_target_names_follow_vanilla_level_keys() {
237        assert_eq!(nether_portal_target_world_name("overworld"), "the_nether");
238        assert_eq!(nether_portal_target_world_name("the_end"), "the_nether");
239        assert_eq!(nether_portal_target_world_name("the_nether"), "overworld");
240    }
241
242    #[test]
243    fn end_entry_portal_target_name_is_only_for_non_end_sources() {
244        assert_eq!(
245            end_entry_portal_target_world_name("overworld"),
246            Some("the_end")
247        );
248        assert_eq!(
249            end_entry_portal_target_world_name("the_nether"),
250            Some("the_end")
251        );
252        assert_eq!(end_entry_portal_target_world_name("the_end"), None);
253    }
254}