veloren_voxygen/singleplayer/
singleplayer_world.rs1use 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 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 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() );
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 &[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 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}