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::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}
24
25impl WorldMap {
26 #[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 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 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 pub fn insert(&mut self, key: Identifier, world: Arc<World>) {
96 self.worlds.insert(key, world);
97 }
98
99 #[must_use]
101 pub fn get(&self, key: &Identifier) -> Option<&Arc<World>> {
102 self.worlds.get(key)
103 }
104
105 pub fn values(&self) -> impl Iterator<Item = &Arc<World>> {
107 self.worlds.values()
108 }
109
110 pub fn keys(&self) -> impl Iterator<Item = &Identifier> {
112 self.worlds.keys()
113 }
114
115 pub fn iter(&self) -> impl Iterator<Item = (&Identifier, &Arc<World>)> {
117 self.worlds.iter()
118 }
119
120 #[must_use]
122 pub fn len(&self) -> usize {
123 self.worlds.len()
124 }
125
126 #[must_use]
128 pub fn is_empty(&self) -> bool {
129 self.worlds.is_empty()
130 }
131
132 #[must_use]
134 pub fn default_domain(&self) -> &str {
135 &self.default_domain
136 }
137
138 #[must_use]
140 pub fn has_domain(&self, domain: &str) -> bool {
141 self.default_worlds.contains_key(domain)
142 }
143
144 pub fn domain_names(&self) -> impl Iterator<Item = &str> {
146 self.default_worlds.keys().map(String::as_str)
147 }
148
149 #[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 #[must_use]
159 pub fn server_default_world(&self) -> Option<&Arc<World>> {
160 self.default_world(self.default_domain())
161 }
162
163 #[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 #[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 #[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 #[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}