veloren_voxygen/singleplayer/
singleplayer_world.rs1use std::{
2 fs,
3 io::Read,
4 path::{Path, PathBuf},
5};
6
7use common::{assets::ASSETS_PATH, consts::DAY_LENGTH_DEFAULT};
8use serde::{Deserialize, Serialize};
9use server::{DEFAULT_WORLD_MAP, DEFAULT_WORLD_SEED, FileOpts, GenOpts};
10use tracing::error;
11
12pub struct SingleplayerWorld {
13 pub name: String,
14 pub gen_opts: Option<GenOpts>,
15 pub day_length: f64,
16 pub seed: u32,
17 pub is_generated: bool,
18 pub path: PathBuf,
19 pub map_path: PathBuf,
20}
21
22impl SingleplayerWorld {
23 pub fn copy_default_world(&self) {
24 if let Err(e) = fs::copy(asset_path(DEFAULT_WORLD_MAP), &self.map_path) {
25 println!("Error when trying to copy default world: {e}");
26 }
27 }
28}
29
30fn load_map(path: &Path) -> Option<SingleplayerWorld> {
31 let meta_path = path.join("meta.ron");
32
33 let Ok(f) = fs::File::open(&meta_path) else {
34 error!("Failed to open {}", meta_path.to_string_lossy());
35 return None;
36 };
37
38 let Ok(bytes) = f.bytes().collect::<Result<Vec<u8>, _>>() else {
39 error!("Failed to read {}", meta_path.to_string_lossy());
40 return None;
41 };
42
43 version::try_load(std::io::Cursor::new(bytes), path)
44}
45
46fn write_world_meta(world: &SingleplayerWorld) {
47 let path = &world.path;
48
49 if let Err(e) = fs::create_dir_all(path) {
50 error!("Failed to create world folder: {e}");
51 }
52
53 match fs::File::create(path.join("meta.ron")) {
54 Ok(file) => {
55 if let Err(e) = ron::ser::to_writer_pretty(
56 file,
57 &version::Current::from_world(world),
58 ron::ser::PrettyConfig::new(),
59 ) {
60 error!("Failed to create world meta file: {e}")
61 }
62 },
63 Err(e) => error!("Failed to create world meta file: {e}"),
64 }
65}
66
67fn asset_path(asset: &str) -> PathBuf {
68 let mut s = asset.replace('.', "/");
69 s.push_str(".bin");
70 ASSETS_PATH.join(s)
71}
72
73fn migrate_old_singleplayer(from: &Path, to: &Path) {
74 if fs::metadata(from).is_ok_and(|meta| meta.is_dir()) {
75 if let Err(e) = fs::rename(from, to) {
76 error!("Failed to migrate singleplayer: {e}");
77 return;
78 }
79
80 let mut seed = DEFAULT_WORLD_SEED;
81 let mut day_length = DAY_LENGTH_DEFAULT;
82 let (map_file, gen_opts) = fs::read_to_string(to.join("server_config/settings.ron"))
83 .ok()
84 .and_then(|settings| {
85 let settings: server::Settings = ron::from_str(&settings).ok()?;
86 seed = settings.world_seed;
87 day_length = settings.day_length;
88 Some(match settings.map_file? {
89 FileOpts::LoadOrGenerate { name, opts, .. } => {
90 (Some(PathBuf::from(name)), Some(opts))
91 },
92 FileOpts::Generate(opts) => (None, Some(opts)),
93 FileOpts::LoadLegacy(_) => return None,
94 FileOpts::Load(path) => (Some(path), None),
95 FileOpts::LoadAsset(asset) => (Some(asset_path(&asset)), None),
96 FileOpts::Save(_, gen_opts) => (None, Some(gen_opts)),
97 })
98 })
99 .unwrap_or((Some(asset_path(DEFAULT_WORLD_MAP)), None));
100
101 let map_path = to.join("map.bin");
102 if let Some(map_file) = map_file {
103 if let Err(err) = fs::copy(map_file, &map_path) {
104 error!("Failed to copy map file to singleplayer world: {err}");
105 }
106 }
107
108 write_world_meta(&SingleplayerWorld {
109 name: "singleplayer world".to_string(),
110 gen_opts,
111 seed,
112 day_length,
113 path: to.to_path_buf(),
114 is_generated: false,
116 map_path,
117 });
118 }
119}
120
121fn load_worlds(path: &Path) -> Vec<SingleplayerWorld> {
122 let Ok(paths) = fs::read_dir(path) else {
123 let _ = fs::create_dir_all(path);
124 return Vec::new();
125 };
126
127 paths
128 .filter_map(|entry| {
129 let entry = entry.ok()?;
130 if entry.file_type().ok()?.is_dir() {
131 let path = entry.path();
132 load_map(&path)
133 } else {
134 None
135 }
136 })
137 .collect()
138}
139
140#[derive(Default)]
141pub struct SingleplayerWorlds {
142 pub worlds: Vec<SingleplayerWorld>,
143 pub current: Option<usize>,
144 worlds_folder: PathBuf,
145}
146
147impl SingleplayerWorlds {
148 pub fn load(userdata_folder: &Path) -> SingleplayerWorlds {
149 let worlds_folder = userdata_folder.join("singleplayer_worlds");
150
151 if let Err(e) = fs::create_dir_all(&worlds_folder) {
152 error!("Failed to create singleplayer worlds folder: {e}");
153 }
154
155 migrate_old_singleplayer(
156 &userdata_folder.join("singleplayer"),
157 &worlds_folder.join("singleplayer"),
158 );
159
160 let worlds = load_worlds(&worlds_folder);
161
162 SingleplayerWorlds {
163 worlds,
164 current: None,
165 worlds_folder,
166 }
167 }
168
169 pub fn delete_map_file(&mut self, map: usize) {
170 let w = &mut self.worlds[map];
171 if w.is_generated {
172 let _ = fs::remove_file(&w.map_path);
174 }
175 w.is_generated = false;
176 }
177
178 pub fn remove(&mut self, idx: usize) {
179 if let Some(ref mut i) = self.current {
180 match (*i).cmp(&idx) {
181 std::cmp::Ordering::Less => {},
182 std::cmp::Ordering::Equal => self.current = None,
183 std::cmp::Ordering::Greater => *i -= 1,
184 }
185 }
186 let _ = fs::remove_dir_all(&self.worlds[idx].path);
187 self.worlds.remove(idx);
188 }
189
190 fn world_folder_name(&self) -> String {
191 use chrono::{Datelike, Timelike};
192 let now = chrono::Local::now().naive_local();
193 let name = format!(
194 "world-{}-{}-{}-{}_{}_{}_{}",
195 now.year(),
196 now.month(),
197 now.day(),
198 now.hour(),
199 now.minute(),
200 now.second(),
201 now.and_utc().timestamp_subsec_millis() );
204
205 let mut test_name = name.clone();
206 let mut i = 0;
207 'fail: loop {
208 for world in self.worlds.iter() {
209 if world.path.ends_with(&test_name) {
210 test_name.clone_from(&name);
211 test_name.push('_');
212 test_name.push_str(&i.to_string());
213 i += 1;
214 continue 'fail;
215 }
216 }
217 break;
218 }
219 test_name
220 }
221
222 pub fn current(&self) -> Option<&SingleplayerWorld> {
223 self.current.and_then(|i| self.worlds.get(i))
224 }
225
226 pub fn new_world(&mut self) {
227 let folder_name = self.world_folder_name();
228 let path = self.worlds_folder.join(folder_name);
229
230 let new_world = SingleplayerWorld {
231 name: "New World".to_string(),
232 gen_opts: None,
233 day_length: DAY_LENGTH_DEFAULT,
234 seed: DEFAULT_WORLD_SEED,
235 is_generated: false,
236 map_path: path.join("map.bin"),
237 path,
238 };
239
240 write_world_meta(&new_world);
241
242 self.worlds.push(new_world)
243 }
244
245 pub fn save_current_meta(&self) {
246 if let Some(world) = self.current() {
247 write_world_meta(world);
248 }
249 }
250}
251
252mod version {
253 use std::any::{Any, type_name};
254
255 use serde::de::DeserializeOwned;
256
257 use super::*;
258
259 pub type Current = V2;
260
261 type LoadWorldFn<R> =
262 fn(R, &Path) -> Result<SingleplayerWorld, (&'static str, ron::de::SpannedError)>;
263 fn loaders<'a, R: std::io::Read + Clone>() -> &'a [LoadWorldFn<R>] {
264 &[load_raw::<V2, _>, load_raw::<V1, _>]
266 }
267
268 #[derive(Deserialize, Serialize)]
269 pub struct V1 {
270 #[serde(deserialize_with = "version::<_, 1>")]
271 version: u64,
272 name: String,
273 gen_opts: Option<GenOpts>,
274 seed: u32,
275 }
276
277 impl ToWorld for V1 {
278 fn to_world(self, path: PathBuf) -> SingleplayerWorld {
279 let map_path = path.join("map.bin");
280 let is_generated = fs::metadata(&map_path).is_ok_and(|f| f.is_file());
281
282 SingleplayerWorld {
283 name: self.name,
284 gen_opts: self.gen_opts,
285 seed: self.seed,
286 day_length: DAY_LENGTH_DEFAULT,
287 is_generated,
288 path,
289 map_path,
290 }
291 }
292 }
293
294 #[derive(Deserialize, Serialize)]
295 pub struct V2 {
296 #[serde(deserialize_with = "version::<_, 2>")]
297 version: u64,
298 name: String,
299 gen_opts: Option<GenOpts>,
300 seed: u32,
301 day_length: f64,
302 }
303
304 impl V2 {
305 pub fn from_world(world: &SingleplayerWorld) -> Self {
306 V2 {
307 version: 2,
308 name: world.name.clone(),
309 gen_opts: world.gen_opts.clone(),
310 seed: world.seed,
311 day_length: world.day_length,
312 }
313 }
314 }
315
316 impl ToWorld for V2 {
317 fn to_world(self, path: PathBuf) -> SingleplayerWorld {
318 let map_path = path.join("map.bin");
319 let is_generated = fs::metadata(&map_path).is_ok_and(|f| f.is_file());
320
321 SingleplayerWorld {
322 name: self.name,
323 gen_opts: self.gen_opts,
324 seed: self.seed,
325 day_length: self.day_length,
326 is_generated,
327 path,
328 map_path,
329 }
330 }
331 }
332
333 fn version<'de, D: serde::Deserializer<'de>, const V: u64>(de: D) -> Result<u64, D::Error> {
335 u64::deserialize(de).and_then(|x| {
336 if x == V {
337 Ok(x)
338 } else {
339 Err(serde::de::Error::invalid_value(
340 serde::de::Unexpected::Unsigned(x),
341 &"incorrect magic/version bytes",
342 ))
343 }
344 })
345 }
346
347 trait ToWorld {
348 fn to_world(self, path: PathBuf) -> SingleplayerWorld;
349 }
350
351 fn load_raw<RawWorld: Any + ToWorld + DeserializeOwned, R: std::io::Read + Clone>(
352 reader: R,
353 path: &Path,
354 ) -> Result<SingleplayerWorld, (&'static str, ron::de::SpannedError)> {
355 ron::de::from_reader::<_, RawWorld>(reader)
356 .map(|s| s.to_world(path.to_path_buf()))
357 .map_err(|e| (type_name::<RawWorld>(), e))
358 }
359
360 pub fn try_load<R: std::io::Read + Clone>(reader: R, path: &Path) -> Option<SingleplayerWorld> {
361 loaders()
362 .iter()
363 .find_map(|load_raw| match load_raw(reader.clone(), path) {
364 Ok(chunk) => Some(chunk),
365 Err((raw_name, e)) => {
366 error!(
367 "Attempt to load chunk with raw format `{}` failed: {:?}",
368 raw_name, e
369 );
370 None
371 },
372 })
373 }
374}