Rust入门实战 编写Minecraft启动器#5启动游戏

61 阅读2分钟

首发于Enaium的个人博客


好了,我们已经完成了所有的准备工作,现在我们可以开始编写启动游戏的代码了。

首先我们需要添加几个依赖。


model = { path = "../model" }

parse = { path = "../parse" }

download = { path = "../download" }

clap = { version = "4.5" }

zip = "2.1"

clap用于解析命令行参数,zip用于解压文件。

首先创建一个cli函数用于构建我们的命令行。


fn cli() -> Command {

    Command::new("rmcl")

        .about("A Minecraft launcher written in Rust")

        .version("0.1.0")

        .author("Enaium")

        .subcommand_required(true)

        .arg_required_else_help(true)

        .allow_external_subcommands(true)

        .subcommand(

            Command::new("search")

                .about("Search Game")

                .arg(arg!([VERSION] "Game version"))

                .arg(

                    arg!(-t --type <TYPE> "Game type")

                        .value_parser(["release", "snapshot", "old_beta", "old_alpha"])

                        .require_equals(true)

                        .default_value("release")

                        .default_missing_value("release"),

                ),

        )

        .subcommand(

            Command::new("download")

                .about("Download Game")

                .arg(arg!(<VERSION> "Game Version"))

                .arg_required_else_help(true),

        )

        .subcommand(

            Command::new("launch")

                .about("Launch Game")

                .arg(arg!(<VERSION> "Game Version"))

                .arg_required_else_help(true),

        )

}

接着创建一个get_version_manifest函数用于获取游戏的所有版本清单。


fn get_version_manifest() -> model::version_manifest::VersionManifest {

    return get("https://launchermeta.mojang.com/mc/game/version_manifest.json")

        .unwrap()

        .json::<version_manifest::VersionManifest>()

        .unwrap();

}

之后编写每个子命令的处理函数。


fn search(sub_matches: &clap::ArgMatches) {

    let version = sub_matches.get_one::<String>("VERSION");

    let type_ = sub_matches.get_one::<String>("type").unwrap();

    let versions = get_version_manifest().versions;

  


    let versions = versions.iter().filter(|v| {

        (if version.is_some() {

            v.id.contains(version.unwrap())

        } else {

            true

        }) && v.type_.eq(type_)

    });

    for version in versions {

        println!("Version:{}", version.id);

    }

}


fn download(sub_matches: &clap::ArgMatches) {

    let game_dir = std::env::current_dir().unwrap().join(".minecraft");

    let version = sub_matches.get_one::<String>("VERSION").unwrap();

    let versions = get_version_manifest().versions;

  


    if let Some(version) = versions.iter().find(|v| v.id.eq(version)) {

        version.download(&game_dir).unwrap_or_else(|err| {

            panic!("Version download error:{:?}", err);

        });

    } else {

        println!("Version:{} not found", version);

    }

}


fn launch(sub_matches: &clap::ArgMatches) {

    let game_dir = std::env::current_dir().unwrap().join(".minecraft");

    let libraries_dir = game_dir.join("libraries");

    let assets_dir = game_dir.join("assets");

    let version = sub_matches.get_one::<String>("VERSION").unwrap();

    let version_dir = game_dir.join("versions").join(version);

    let natives_dir = version_dir.join("natives");

    let config_path = version_dir.join(format!("{}.json", version));

    let version_path = version_dir.join(format!("{}.jar", version));

  


    if !version_path.exists() || !config_path.exists() {

        println!("Version:{} not found", version_path.display());

        return;

    }

  


    let version = &Version::parse(&std::fs::read_to_string(&config_path).unwrap()).unwrap();

  


    for library in &version.libraries {

        if library.allowed() && library.name.contains("natives") {

            extract_jar(

                &libraries_dir.join(&library.downloads.artifact.path),

                &natives_dir,

            );

        }

    }

  


    let classpath = format!(

        "{}{}",

        &version

            .libraries

            .iter()

            .map(|library| {

                format!(

                    "{}{}",

                    libraries_dir

                        .join(&library.downloads.artifact.path)

                        .display(),

                    if cfg!(windows) { ";" } else { ":" }

                )

            })

            .collect::<String>(),

        version_path.display()

    );

  


    std::process::Command::new("java")

        .current_dir(&game_dir)

        .arg(format!("-Djava.library.path={}", natives_dir.display()))

        .arg("-Dminecraft.launcher.brand=rmcl")

        .arg("-cp")

        .arg(classpath)

        .arg(&version.main_class)

        .arg("--username")

        .arg("Enaium")

        .arg("--version")

        .arg(&version.id)

        .arg("--gameDir")

        .arg(game_dir)

        .arg("--assetsDir")

        .arg(assets_dir)

        .arg("--assetIndex")

        .arg(&version.asset_index.id)

        .arg("--accessToken")

        .arg("0")

        .arg("--versionType")

        .arg("RMCL 0.1.0")

        .status()

        .unwrap();

}

解压natives文件。


fn extract_jar(jar: &Path, dir: &Path) {

    if !dir.exists() {

        std::fs::create_dir_all(dir).unwrap();

    }

  


    let mut archive = zip::ZipArchive::new(std::fs::File::open(jar).unwrap()).unwrap();

    for i in 0..archive.len() {

        let mut entry = archive.by_index(i).unwrap();

        if entry.is_file() && !entry.name().contains("META-INF") {

            let mut name = entry.name();

  


            if name.contains("/") {

                name = &name[entry.name().rfind('/').unwrap() + 1..];

            }

  


            let path = dir.join(name);

  


            if path.exists() {

                std::fs::remove_file(&path).unwrap();

            }

  


            let mut file = std::fs::File::create(&path).unwrap();

  


            std::io::copy(&mut entry, &mut file).unwrap();

        }

    }

}

最后在main函数中调用cli函数。


fn main() {

    let matches = cli().get_matches();

  


    match matches.subcommand() {

        Some(("search", sub_matches)) => {

            search(sub_matches);

        }

        Some(("download", sub_matches)) => {

            download(sub_matches);

        }

        Some(("launch", sub_matches)) => {

            launch(sub_matches);

        }

        _ => unreachable!(),

    }

}

现在我们可以使用rmcl命令来搜索、下载、启动游戏了。

20240705202955

项目地址