veloren_voxygen/singleplayer/
singleplayer_world.rs

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