Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s

Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
AskaEth committed 2026-10-31 21:54:10 +08:00
commit bc41dfcf7d
517 files changed
+92844

No files matched your search

+258
View File
@@ -0,0 +1,258 @@
//! 依赖库展开:主构件 + natives 分类器构件 → 待下载文件。
//!
//! 依据:`DevDocs/analyse/03-version-download.md` §6.4(libraries 含 natives 规则)、
//! §6.8(目录布局速查)。
//!
//! 三条容易踩的规则:
//! 1. **主构件与 natives 构件都要下**:老版本(lwjgl 2.x)一条库里同时有
//! `downloads.artifact` 与 `natives` 表,HMCL 也是两者都取;
//! 2. **路径优先用 JSON 给的 `path`**(Mojang 权威值),缺了才按 Maven 坐标推导;
//! 3. **`hint=local` 的库落到实例私有目录** `versions/<id>/libraries/`,其余一律进
//! 恒共享的 `libraries/`(D20:任何模式都不按实例复制)。
use std::path::{Path, PathBuf};
use super::super::arguments::Environment;
use super::super::layout::GameLayout;
use super::super::library::Library;
use super::plan::{FileRole, PlanFile};
use crate::download::ResourceKind;
/// 没写 `url` 的库兜底用的 Maven 基址(Mojang 官方库仓)。
pub const DEFAULT_MAVEN_BASE: &str = "https://libraries.minecraft.net/";
/// 展开一层(已合并)清单里的库列表。
///
/// 返回(文件清单,人读告警)。告警不致命:缺下载地址的库会被跳过并告知,
/// 让用户知道"缺哪个文件"而不是拿到一个静默不完整的实例。
#[must_use]
pub fn plan(
layout: &GameLayout,
version_id: &str,
libraries: &[Library],
environment: &Environment,
) -> (Vec<PlanFile>, Vec<String>) {
let mut files = Vec::new();
let mut warnings = Vec::new();
for library in libraries {
if !library.applies(environment) {
continue;
}
if library.coordinate().is_err() {
warnings.push(format!("库坐标不合法,已跳过:{}", library.name));
continue;
}
let Ok(relative) = library.relative_path() else {
warnings.push(format!("库路径推导失败,已跳过:{}", library.name));
continue;
};
let dest = library_dest(layout, version_id, library, &relative);
match library.artifact() {
Some(artifact) if artifact.has_url() => files.push(PlanFile {
role: FileRole::Library,
name: library.name.clone(),
url: artifact.url.clone().unwrap_or_default(),
sha1: artifact.sha1.clone(),
size: artifact.size,
dest,
resource: ResourceKind::Library,
}),
// natives-only 库(1.6 时代的 `lwjgl-platform` / `jinput-platform`)**没有主构件**,
// 只要 `downloads.classifiers`。这里若按坐标硬拼 URL 会拼出一个不存在的 jar
// (实测:官方与 BMCLAPI 双双 404)。判据是 `natives` 在场。
None if library.natives.is_some() => {}
// 魔改/现代 loader 的库常见"只给 name 不给 downloads",按坐标拼仓库地址。
_ => files.push(PlanFile {
role: FileRole::Library,
name: library.name.clone(),
url: maven_url(library, &relative),
sha1: None,
size: None,
dest,
resource: ResourceKind::Library,
}),
}
// natives 分类器构件(存在才下;解压发生在启动期,不归安装管)。
let Some(classifier) = library.native_classifier(environment) else {
continue;
};
let Some(artifact) = library.native_artifact(&classifier) else {
warnings.push(format!(
"{} 的 natives 分类器 {classifier} 没有下载信息,已跳过",
library.name
));
continue;
};
let Ok(native_relative) = library.native_relative_path(&classifier) else {
warnings.push(format!("{} 的 natives 路径推导失败,已跳过", library.name));
continue;
};
if !artifact.has_url() {
warnings.push(format!(
"{} 的 natives 构件 {classifier} 没有下载地址,已跳过",
library.name
));
continue;
}
files.push(PlanFile {
role: FileRole::Native,
name: format!("{}:{classifier}", library.name),
url: artifact.url.clone().unwrap_or_default(),
sha1: artifact.sha1.clone(),
size: artifact.size,
dest: layout.libraries_dir().join(native_relative),
resource: ResourceKind::Library,
});
}
(files, warnings)
}
/// 库文件落点:实例私有(`hint=local`)或恒共享 `libraries/`。
fn library_dest(
layout: &GameLayout,
version_id: &str,
library: &Library,
relative: &Path,
) -> PathBuf {
if library.is_local() {
layout.version_libraries_dir(version_id).join(relative)
} else {
layout.libraries_dir().join(relative)
}
}
/// 按库声明(或默认)的 Maven 基址拼下载地址。
fn maven_url(library: &Library, relative: &Path) -> String {
let base = library
.url
.as_deref()
.map(str::trim)
.filter(|base| !base.is_empty())
.unwrap_or(DEFAULT_MAVEN_BASE);
let path = relative.to_string_lossy().replace('\\', "/");
if base.ends_with('/') {
format!("{base}{path}")
} else {
format!("{base}/{path}")
}
}
/// 供上层判错用:库展开不产生硬失败,缺地址一律降级成告警。
#[cfg(test)]
mod tests {
use super::plan;
use crate::game::arguments::Environment;
use crate::game::layout::GameLayout;
use crate::game::library::Library;
fn environment() -> Environment {
Environment {
os: "windows".to_string(),
os_version: String::new(),
arch: "x86_64".to_string(),
..Environment::host()
}
}
fn library_with_artifact(json: &str) -> Library {
serde_json::from_str(json).expect("库 JSON 应可解析")
}
#[test]
fn plans_artifact_at_maven_path() {
let library = library_with_artifact(
r#"{"name":"org.ow2.asm:asm:9.6","downloads":{"artifact":{"path":"org/ow2/asm/asm/9.6/asm-9.6.jar","sha1":"aa","size":122,"url":"https://libraries.minecraft.net/org/ow2/asm/asm/9.6/asm-9.6.jar"}}}"#,
);
let layout = GameLayout::new("/g/.minecraft");
let (files, warnings) = plan(&layout, "1.20.1", &[library], &environment());
assert!(warnings.is_empty());
assert_eq!(files.len(), 1);
assert_eq!(files[0].sha1.as_deref(), Some("aa"));
assert_eq!(files[0].size, Some(122));
assert!(files[0]
.dest
.ends_with("libraries/org/ow2/asm/asm/9.6/asm-9.6.jar"));
}
#[test]
fn natives_only_library_plans_no_artifact() {
// 1.6 时代的 lwjgl-platform / jinput-platform:有 natives 表、只有 classifiers,
// **没有 downloads.artifact**。按坐标硬拼主构件会拼出不存在的 jar(实测双源 404)。
let library = library_with_artifact(
r#"{"name":"org.lwjgl.lwjgl:lwjgl-platform:2.9.0",
"natives":{"windows":"natives-windows","linux":"natives-linux","osx":"natives-osx"},
"downloads":{"classifiers":{"natives-windows":{"path":"org/lwjgl/lwjgl/lwjgl-platform/2.9.0/lwjgl-platform-2.9.0-natives-windows.jar","sha1":"3f11","size":609967,"url":"https://libraries.minecraft.net/natives-windows.jar"}}}}"#,
);
let layout = GameLayout::new("/g/.minecraft");
let (files, warnings) = plan(&layout, "1.6.4", &[library], &environment());
assert!(warnings.is_empty(), "不该有告警:{warnings:?}");
assert_eq!(files.len(), 1, "只应有 natives 构件:{files:?}");
assert_eq!(files[0].role.as_str(), "native");
assert_eq!(files[0].sha1.as_deref(), Some("3f11"));
}
#[test]
fn plans_both_artifact_and_native_jar() {
let library = library_with_artifact(
r#"{"name":"org.lwjgl.lwjgl:lwjgl-platform:2.9.4","natives":{"windows":"natives-windows"},
"downloads":{"artifact":{"path":"org/lwjgl/lwjgl/lwjgl-platform/2.9.4/lwjgl-platform-2.9.4.jar","sha1":"bb","size":1,"url":"https://libraries.minecraft.net/a.jar"},
"classifiers":{"natives-windows":{"path":"org/lwjgl/lwjgl/lwjgl-platform/2.9.4/lwjgl-platform-2.9.4-natives-windows.jar","sha1":"cc","size":2,"url":"https://libraries.minecraft.net/b.jar"}}}}"#,
);
let layout = GameLayout::new("/g/.minecraft");
let (files, _) = plan(&layout, "1.7.10", &[library], &environment());
assert_eq!(files.len(), 2);
let native = files
.iter()
.find(|f| f.role.as_str() == "native")
.expect("应有 natives 构件");
assert_eq!(native.sha1.as_deref(), Some("cc"));
assert!(native
.dest
.ends_with("lwjgl-platform-2.9.4-natives-windows.jar"));
}
#[test]
fn derives_url_for_libraries_without_downloads() {
let library = library_with_artifact(
r#"{"name":"net.minecraftforge:forge:1.20.1-47.2.0:universal","url":"https://maven.minecraftforge.net/"}"#,
);
let layout = GameLayout::new("/g/.minecraft");
let (files, warnings) = plan(&layout, "1.20.1-forge", &[library], &environment());
assert!(warnings.is_empty());
assert_eq!(files.len(), 1);
assert_eq!(
files[0].url,
"https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.1-47.2.0/forge-1.20.1-47.2.0-universal.jar"
);
assert!(files[0].sha1.is_none());
}
#[test]
fn skips_libraries_filtered_by_rules() {
let library = library_with_artifact(
r#"{"name":"org.lwjgl:lwjgl:3.3.2","rules":[{"action":"allow","os":{"name":"linux"}}],
"downloads":{"artifact":{"url":"https://libraries.minecraft.net/x.jar","sha1":"dd"}}}"#,
);
let layout = GameLayout::new("/g/.minecraft");
let (files, _) = plan(&layout, "1.20.1", &[library], &environment());
assert!(files.is_empty());
}
#[test]
fn local_hint_libraries_land_in_instance_dir() {
let library = library_with_artifact(
r#"{"name":"net.minecraftforge:forge:1.20.1-47.2.0","hint":"local",
"downloads":{"artifact":{"path":"net/minecraftforge/forge/1.20.1-47.2.0.jar","url":"https://libraries.minecraft.net/y.jar"}}}"#,
);
let layout = GameLayout::new("/g/.minecraft");
let (files, _) = plan(&layout, "inst", &[library], &environment());
assert_eq!(files.len(), 1);
assert!(files[0]
.dest
.ends_with("versions/inst/libraries/net/minecraftforge/forge/1.20.1-47.2.0.jar"));
}
}