veloren_voxygen/singleplayer/
singleplayer_world.rs

1use 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            // Isn't persisted so doesn't matter what it's set to.
115            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            // We don't care about the result here since we aren't sure the file exists.
173            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() /* .and_utc() necessary, as other fn is
202                                                     * deprecated */
203        );
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        // Step [4]
265        &[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    // Utilities
334    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}