mc_bootstrap 0.1.1

A library for launching Minecraft.
Documentation
pub mod classpath;
pub mod manifest;
pub mod rules;

use manifest::{read_manifest_from_file, JvmArgument};
use rules::is_all_rules_satisfied;
use std::{path::PathBuf, process::Command};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ClientBootstrapError {
    #[error("The game directory doesn't exist.")]
    GameDirNotExist,

    #[error("The java bin doesn't exist.")]
    JavaBinNotExist,

    #[error("The version file (.json) doesn't exist.")]
    VersionFileNotFound,

    #[error("An unexpected error has ocurred.")]
    UnknownError,

    #[error("{0}")]
    Json(#[from] serde_json::Error),
}

pub struct ClientAuth {
    pub access_token: Option<String>,
    pub username: String,
    pub uuid: Option<String>,
}

pub struct ClientVersion {
    pub version: String,
    pub version_type: String,
}

pub struct ClientSettings {
    pub assets: PathBuf,
    pub auth: ClientAuth,
    pub game_dir: PathBuf,
    pub java_bin: PathBuf,
    pub libraries_dir: PathBuf,
    pub manifest_file: PathBuf,
    pub natives_dir: PathBuf,
    pub version: ClientVersion,
    pub version_jar_file: PathBuf,
}

pub struct ClientBootstrap {
    pub settings: ClientSettings,
}

impl ClientBootstrap {
    pub fn new(settings: ClientSettings) -> Self {
        Self { settings }
    }

    pub fn get_assets_dir(&self) -> PathBuf {
        return self.settings.assets.clone();
    }

    pub fn get_game_dir(&self) -> PathBuf {
        return self.settings.game_dir.clone();
    }

    pub fn get_json_file(&self) -> PathBuf {
        return self.settings.manifest_file.clone();
    }

    pub fn get_jar_file(&self) -> PathBuf {
        return self.settings.version_jar_file.clone();
    }

    pub fn get_libs_dir(&self) -> PathBuf {
        return self.settings.libraries_dir.clone();
    }

    pub fn get_natives_dir(&self) -> PathBuf {
        return self.settings.natives_dir.clone();
    }

    pub fn build_args(&self) -> Result<Vec<String>, ClientBootstrapError> {
        let auth = &self.settings.auth;
        let assets_dir = self.get_assets_dir();
        let game_dir = self.get_game_dir();
        let java_bin = self.settings.java_bin.clone();
        let json_file = self.get_json_file();
        let natives_dir = self.get_natives_dir();
        let version = &self.settings.version;

        if !game_dir.is_dir() {
            return Err(ClientBootstrapError::GameDirNotExist);
        }

        if !java_bin.is_file() {
            return Err(ClientBootstrapError::JavaBinNotExist);
        }

        if !json_file.is_file() {
            return Err(ClientBootstrapError::VersionFileNotFound);
        }

        let manifest = read_manifest_from_file(json_file).unwrap();

        let assets_index = &manifest.asset_index.id;
        let classpath = classpath::create_classpath(
            self.get_jar_file(),
            self.get_libs_dir(),
            manifest.libraries,
        );

        let mut args: Vec<String> = vec![];

        for arg in manifest.arguments.jvm {
            match arg {
                JvmArgument::String(value) => {
                    args.push(value);
                }
                JvmArgument::Struct { value, rules, .. } => {
                    if !is_all_rules_satisfied(&rules) {
                        continue;
                    }

                    if let Some(value) = value.as_str() {
                        args.push(value.to_string());
                    } else if let Some(value_arr) = value.as_array() {
                        for value in value_arr {
                            if let Some(value) = value.as_str() {
                                args.push(value.to_string());
                            }
                        }
                    }
                }
            }
        }

        args.push(manifest.main_class);

        for arg in manifest.arguments.game {
            match arg {
                JvmArgument::String(value) => {
                    args.push(value);
                }
                JvmArgument::Struct { value, rules, .. } => {
                    if !is_all_rules_satisfied(&rules) {
                        continue;
                    }

                    if let Some(value) = value.as_str() {
                        args.push(value.to_string());
                    } else if let Some(value_arr) = value.as_array() {
                        for value in value_arr {
                            if let Some(value) = value.as_str() {
                                args.push(value.to_string());
                            }
                        }
                    }
                }
            }
        }

        args = args
            .iter()
            .map(|x| {
                x.replace("${assets_root}", &assets_dir.to_str().unwrap())
                    .replace("${game_directory}", &game_dir.to_str().unwrap())
                    .replace("${natives_directory}", &natives_dir.to_str().unwrap())
                    .replace("${launcher_name}", "minecraft-rs/bootstrap")
                    .replace("${launcher_version}", "0.1.1")
                    .replace(
                        "${auth_access_token}",
                        auth.access_token
                            .clone()
                            .unwrap_or("null".to_string())
                            .as_str(),
                    )
                    .replace("${auth_player_name}", auth.username.as_str())
                    .replace(
                        "${auth_uuid}",
                        auth.uuid.clone().unwrap_or("null".to_string()).as_str(),
                    )
                    .replace("${version_type}", &version.version_type)
                    .replace("${version_name}", &version.version)
                    .replace("${assets_index_name}", &assets_index)
                    .replace("${user_properties}", "{}")
                    .replace("${classpath}", &classpath)
            })
            .collect();

        return Ok(args);
    }

    pub fn launch(&self) -> Result<i32, ClientBootstrapError> {
        let args = self.build_args().unwrap();

        let mut process = Command::new(&self.settings.java_bin)
            .args(args)
            .current_dir(&self.settings.game_dir)
            .spawn()
            .expect("command failed to start");

        let status = process.wait().unwrap().code().unwrap();
        return Ok(status);
    }
}