cobble-core 1.2.0

Library for managing, installing and launching Minecraft instances and more.
Documentation
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;

/// Loads all save games from the minecraft folder.
#[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>> {
    // Check if dir
    if !path.as_ref().is_dir() {
        trace!("Entry is not a directory.");
        return Ok(None);
    }

    // Check if level.dat
    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);
    }

    // Parse
    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))
}