mod save_game;
use crate::error::{CobbleError, CobbleResult};
use flate2::read::GzDecoder;
use futures::TryStreamExt;
pub use save_game::*;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
use tokio::fs::read_dir;
use tokio::task;
use tokio_stream::wrappers::ReadDirStream;
#[cfg_attr(doc_cfg, doc(cfg(feature = "save-games")))]
#[instrument(
name = "load_save_games",
level = "debug",
skip_all,
fields(minecraft_path)
)]
pub async fn load_save_games(saves_path: impl AsRef<Path> + Send) -> CobbleResult<Vec<SaveGame>> {
if !saves_path.as_ref().is_dir() {
trace!("Save games directory is empty");
return Ok(vec![]);
}
trace!("Loading save game folders...");
let file_stream = ReadDirStream::new(read_dir(saves_path).await?);
let save_games = file_stream
.map_err(CobbleError::from)
.try_filter_map(|e| parse_save_game(e.path()))
.try_collect()
.await?;
Ok(save_games)
}
#[instrument(name = "parse_save_game", level = "trace", skip_all, fields(path,))]
pub(crate) async fn parse_save_game(path: impl AsRef<Path>) -> CobbleResult<Option<SaveGame>> {
if !path.as_ref().is_dir() {
trace!("Entry is not a directory.");
return Ok(None);
}
let mut level_path = PathBuf::from(path.as_ref());
level_path.push("level.dat");
if !level_path.is_file() {
trace!("Entry does not contain level.dat file.");
return Ok(None);
}
let path = PathBuf::from(path.as_ref());
let save_game = task::spawn_blocking(move || -> CobbleResult<_> {
let nbt_file = std::fs::File::open(level_path)?;
let decoder = GzDecoder::new(nbt_file);
let level_dat = fastnbt::from_reader::<_, LevelDat>(decoder)?;
let difficulty = match level_dat.data.difficulty {
Some(0) => Some(Difficulty::Peaceful),
Some(1) => Some(Difficulty::Easy),
Some(2) => Some(Difficulty::Normal),
Some(3) => Some(Difficulty::Hard),
_ => None,
};
let game_type = match level_dat.data.game_type {
1 => GameType::Creative,
2 => GameType::Adventure,
3 => GameType::Spectator,
_ => GameType::Survival,
};
let seed = level_dat
.data
.random_seed
.or_else(|| level_dat.data.world_gen_settings.map(|wgs| wgs.seed));
Ok(SaveGame {
name: level_dat.data.level_name,
path,
difficulty,
game_type,
game_version: level_dat.data.version.map(|v| v.name),
seed,
last_played: OffsetDateTime::from_unix_timestamp(level_dat.data.last_played / 1000)
.unwrap_or(OffsetDateTime::UNIX_EPOCH),
})
})
.await??;
Ok(Some(save_game))
}