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::{DomainEntityDirectory, 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    entity_directories: FxHashMap<String, Arc<DomainEntityDirectory>>,
24}
25
26impl WorldMap {
27    /// Creates a world map from resolved domain config.
28    #[must_use]
29    pub fn new(
30        default_domain: String,
31        domains: &[ResolvedDomainConfig],
32        world_configs: &[ResolvedWorldConfig],
33    ) -> Self {
34        let mut default_worlds = FxHashMap::default();
35        for domain in domains {
36            default_worlds.insert(domain.name.clone(), domain.default_world.clone());
37        }
38        let mut nether_portal_targets = FxHashMap::default();
39        let mut end_portal_targets = FxHashMap::default();
40        for world in world_configs {
41            if let Some(target) = &world.nether_portal_target {
42                nether_portal_targets.insert(world.key.clone(), target.clone());
43            }
44            if let Some(target) = &world.end_portal_target {
45                end_portal_targets.insert(world.key.clone(), target.clone());
46            }
47        }
48        Self {
49            worlds: FxSmallMap::default(),
50            default_domain,
51            default_worlds,
52            nether_portal_targets,
53            end_portal_targets,
54            entity_directories: FxHashMap::default(),
55        }
56    }
57
58    /// Verifies primary ownership and sharing before the server publishes its worlds.
59    pub(crate) fn validate_game_times(&self) -> Result<(), String> {
60        for domain in self.domain_names() {
61            let primary = self
62                .default_world(domain)
63                .ok_or_else(|| format!("domain {domain} has no loaded primary"))?;
64            if primary.domain() != domain || !primary.level_data.read().owns_game_time() {
65                return Err(format!("domain {domain} has an invalid game-time primary"));
66            }
67            for world in self.values().filter(|world| world.domain() == domain) {
68                if !Arc::ptr_eq(&primary.game_time, &world.game_time)
69                    || (world.key != primary.key && world.level_data.read().owns_game_time())
70                {
71                    return Err(format!(
72                        "world {} is not bound to domain {domain}'s primary clock",
73                        world.key
74                    ));
75                }
76            }
77        }
78        for world in self.values() {
79            if !self.has_domain(world.domain()) {
80                return Err(format!("world {} has no configured domain", world.key));
81            }
82        }
83        Ok(())
84    }
85
86    /// Publishes one simulation increment for every domain before any worker dispatch.
87    ///
88    /// # Panics
89    /// Panics if a validated domain primary world is missing from the map.
90    pub fn advance_domain_game_times(&self) {
91        for key in self.default_worlds.values() {
92            let Some(primary) = self.worlds.get(key) else {
93                panic!("validated domain primary is missing: {key}");
94            };
95            primary.level_data.write().advance_game_time();
96        }
97    }
98
99    /// Inserts a loaded world.
100    pub fn insert(&mut self, key: Identifier, world: Arc<World>) {
101        if let Some(replaced) = self.worlds.insert(key, Arc::clone(&world))
102            && let Some(directory) = self.entity_directories.get(replaced.domain())
103        {
104            directory.remove_world(&replaced);
105        }
106        let directory = self
107            .entity_directories
108            .entry(world.domain().to_owned())
109            .or_insert_with(|| Arc::new(DomainEntityDirectory::new()));
110        directory.add_world(&world);
111        world.set_domain_entity_directory(Arc::clone(directory));
112    }
113
114    /// Returns a world by loaded world identifier.
115    #[must_use]
116    pub fn get(&self, key: &Identifier) -> Option<&Arc<World>> {
117        self.worlds.get(key)
118    }
119
120    /// Iterates loaded world values.
121    pub fn values(&self) -> impl Iterator<Item = &Arc<World>> {
122        self.worlds.values()
123    }
124
125    /// Iterates loaded world keys.
126    pub fn keys(&self) -> impl Iterator<Item = &Identifier> {
127        self.worlds.keys()
128    }
129
130    /// Iterates loaded world key/value pairs.
131    pub fn iter(&self) -> impl Iterator<Item = (&Identifier, &Arc<World>)> {
132        self.worlds.iter()
133    }
134
135    /// Returns number of loaded worlds.
136    #[must_use]
137    pub fn len(&self) -> usize {
138        self.worlds.len()
139    }
140
141    /// Returns whether there are no loaded worlds.
142    #[must_use]
143    pub fn is_empty(&self) -> bool {
144        self.worlds.is_empty()
145    }
146
147    /// Returns the default domain name.
148    #[must_use]
149    pub fn default_domain(&self) -> &str {
150        &self.default_domain
151    }
152
153    /// Returns whether a domain exists.
154    #[must_use]
155    pub fn has_domain(&self, domain: &str) -> bool {
156        self.default_worlds.contains_key(domain)
157    }
158
159    /// Iterates domain names.
160    pub fn domain_names(&self) -> impl Iterator<Item = &str> {
161        self.default_worlds.keys().map(String::as_str)
162    }
163
164    /// Returns a domain's default world.
165    #[must_use]
166    pub fn default_world(&self, domain: &str) -> Option<&Arc<World>> {
167        self.default_worlds
168            .get(domain)
169            .and_then(|key| self.worlds.get(key))
170    }
171
172    /// Returns the server default world.
173    #[must_use]
174    pub fn server_default_world(&self) -> Option<&Arc<World>> {
175        self.default_world(self.default_domain())
176    }
177
178    /// Returns loaded worlds in the given domain.
179    #[must_use]
180    pub fn worlds_in_domain(&self, domain: &str) -> Vec<Arc<World>> {
181        self.worlds
182            .values()
183            .filter(|world| world.domain() == domain)
184            .cloned()
185            .collect()
186    }
187
188    /// Resolves a conventional portal target name in the source world's domain.
189    #[must_use]
190    pub fn resolve_portal_target(
191        &self,
192        source_world: &World,
193        target_world_name: &str,
194    ) -> Option<Arc<World>> {
195        let key = Identifier::new(
196            source_world.domain().to_owned(),
197            target_world_name.to_owned(),
198        );
199        self.worlds.get(&key).cloned()
200    }
201
202    /// Resolves the vanilla Nether portal target in the source world's domain.
203    #[must_use]
204    pub fn resolve_nether_portal_target(&self, source_world: &World) -> Option<Arc<World>> {
205        if let Some(target) = self.nether_portal_targets.get(&source_world.key) {
206            return self.worlds.get(target).cloned();
207        }
208
209        self.resolve_portal_target(
210            source_world,
211            nether_portal_target_world_name(source_world.key.path.as_ref()),
212        )
213    }
214
215    /// Resolves the vanilla End portal target for non-End source worlds.
216    ///
217    /// End-to-respawn-world transitions depend on the source world's respawn data,
218    /// so that branch is intentionally left to the destination calculator.
219    #[must_use]
220    pub fn resolve_end_entry_portal_target(&self, source_world: &World) -> Option<Arc<World>> {
221        if let Some(target) = self.end_portal_targets.get(&source_world.key) {
222            return self.worlds.get(target).cloned();
223        }
224
225        end_entry_portal_target_world_name(source_world.key.path.as_ref())
226            .and_then(|target| self.resolve_portal_target(source_world, target))
227    }
228}
229
230fn nether_portal_target_world_name(source_world_name: &str) -> &'static str {
231    if source_world_name == NETHER_WORLD_NAME {
232        OVERWORLD_WORLD_NAME
233    } else {
234        NETHER_WORLD_NAME
235    }
236}
237
238fn end_entry_portal_target_world_name(source_world_name: &str) -> Option<&'static str> {
239    if source_world_name == END_WORLD_NAME {
240        None
241    } else {
242        Some(END_WORLD_NAME)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::{end_entry_portal_target_world_name, nether_portal_target_world_name};
249
250    #[test]
251    fn domain_uuid_lookup_excludes_replaced_worlds_without_retaining_worlds() {
252        use crate::entity::{SharedEntity, entities::PigEntity};
253        use crate::test_support::{
254            fresh_test_derived_world, fresh_test_world_in_domain, insert_ready_full_chunk,
255        };
256        use glam::DVec3;
257        use std::sync::Arc;
258        use steel_registry::vanilla_entities;
259        use steel_utils::ChunkPos;
260
261        let primary_fixture = fresh_test_world_in_domain("directory", "overworld");
262        let primary = &primary_fixture.world;
263        let other_fixture = fresh_test_derived_world(primary, "other");
264        let other = &other_fixture.world;
265        insert_ready_full_chunk(other, ChunkPos::new(0, 0));
266        let owner: SharedEntity = Arc::new(PigEntity::new(
267            &vanilla_entities::PIG,
268            1,
269            DVec3::ZERO,
270            Arc::downgrade(other),
271        ));
272        other
273            .try_add_entity(Arc::clone(&owner))
274            .expect("register owner");
275        let mut worlds = super::WorldMap::new("directory".to_owned(), &[], &[]);
276        worlds.insert(primary.key.clone(), Arc::clone(primary));
277        worlds.insert(other.key.clone(), Arc::clone(other));
278        assert!(Arc::ptr_eq(
279            &primary
280                .get_entity_in_domain_by_uuid(&owner.uuid())
281                .expect("other world owner"),
282            &owner
283        ));
284
285        let replacement_fixture = fresh_test_derived_world(primary, "other");
286        let replacement = &replacement_fixture.world;
287        worlds.insert(replacement.key.clone(), Arc::clone(replacement));
288        assert!(
289            primary
290                .get_entity_in_domain_by_uuid(&owner.uuid())
291                .is_none()
292        );
293        assert!(
294            other.get_entity_by_uuid(&owner.uuid()).is_some(),
295            "old world is still alive but no longer loaded in the map"
296        );
297        let weak = Arc::downgrade(replacement);
298        drop(replacement_fixture);
299        drop(worlds);
300        assert!(
301            weak.upgrade().is_none(),
302            "domain directories must not keep worlds alive"
303        );
304    }
305
306    #[test]
307    fn nether_portal_target_names_follow_vanilla_level_keys() {
308        assert_eq!(nether_portal_target_world_name("overworld"), "the_nether");
309        assert_eq!(nether_portal_target_world_name("the_end"), "the_nether");
310        assert_eq!(nether_portal_target_world_name("the_nether"), "overworld");
311    }
312
313    #[test]
314    fn end_entry_portal_target_name_is_only_for_non_end_sources() {
315        assert_eq!(
316            end_entry_portal_target_world_name("overworld"),
317            Some("the_end")
318        );
319        assert_eq!(
320            end_entry_portal_target_world_name("the_nether"),
321            Some("the_end")
322        );
323        assert_eq!(end_entry_portal_target_world_name("the_end"), None);
324    }
325}