steel_core/server/
worlds.rs1use 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
16pub 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 #[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 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 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 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 #[must_use]
116 pub fn get(&self, key: &Identifier) -> Option<&Arc<World>> {
117 self.worlds.get(key)
118 }
119
120 pub fn values(&self) -> impl Iterator<Item = &Arc<World>> {
122 self.worlds.values()
123 }
124
125 pub fn keys(&self) -> impl Iterator<Item = &Identifier> {
127 self.worlds.keys()
128 }
129
130 pub fn iter(&self) -> impl Iterator<Item = (&Identifier, &Arc<World>)> {
132 self.worlds.iter()
133 }
134
135 #[must_use]
137 pub fn len(&self) -> usize {
138 self.worlds.len()
139 }
140
141 #[must_use]
143 pub fn is_empty(&self) -> bool {
144 self.worlds.is_empty()
145 }
146
147 #[must_use]
149 pub fn default_domain(&self) -> &str {
150 &self.default_domain
151 }
152
153 #[must_use]
155 pub fn has_domain(&self, domain: &str) -> bool {
156 self.default_worlds.contains_key(domain)
157 }
158
159 pub fn domain_names(&self) -> impl Iterator<Item = &str> {
161 self.default_worlds.keys().map(String::as_str)
162 }
163
164 #[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 #[must_use]
174 pub fn server_default_world(&self) -> Option<&Arc<World>> {
175 self.default_world(self.default_domain())
176 }
177
178 #[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 #[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 #[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 #[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}