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

+213
View File
@@ -0,0 +1,213 @@
//! 依赖库模型:Maven 坐标、natives 选择、库文件相对路径。
//!
//! 依据:`DevDocs/analyse/03-version-download.md` §6.4(libraries 含 natives 规则)、
//! §6.8(目录布局速查)。
//!
//! 这里只做"数据 → 路径/候选"的纯计算,**不碰网络与文件系统**:
//! 真正的下载归 `core::download`,放置归 [`super::layout`]。
use std::collections::BTreeMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::arguments::{rules_allow, Environment, Rule};
use super::error::GameError;
use super::manifest::DownloadInfo;
/// 一条依赖库。
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Library {
/// Maven 坐标 `group:artifact:version[:classifier][@ext]`。
#[serde(default)]
pub name: String,
/// 下载信息;老 JSON/魔改 JSON 可能没有。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub downloads: Option<LibraryDownloads>,
/// natives 选择表:`{ "windows": "natives-windows-${arch}" }`。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub natives: Option<BTreeMap<String, String>>,
/// 生效规则(空 = 生效)。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<Rule>,
/// natives 解压规则。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extract: Option<Extract>,
/// Maven 仓库基地址覆盖。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// HMCL 私有:`local` 表示库放在实例私有的 `versions/<id>/libraries/` 下。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
/// 未识别字段原样保留。
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
/// 一条依赖库的下载表。
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LibraryDownloads {
/// 主构件。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artifact: Option<DownloadInfo>,
/// 分类器构件(natives 走这里)。
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub classifiers: BTreeMap<String, DownloadInfo>,
}
/// natives 解压规则:命中 `exclude` 的条目不解压。
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Extract {
/// 排除的路径模式(`META-INF/`)。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude: Vec<String>,
}
/// Maven 坐标(`group:artifact:version[:classifier][@ext]`)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MavenCoordinate {
/// group,如 `org.lwjgl`。
pub group: String,
/// artifact,如 `lwjgl`。
pub artifact: String,
/// version,如 `3.3.2`。
pub version: String,
/// classifier(natives 常用),如 `natives-windows`。
pub classifier: Option<String>,
/// 扩展名,缺省 `jar`。
pub extension: String,
}
impl MavenCoordinate {
/// 解析坐标字符串。
pub fn parse(raw: &str) -> Result<Self, GameError> {
let invalid = |reason: &'static str| GameError::InvalidCoordinate {
raw: raw.to_owned(),
reason,
};
let (body, extension) = match raw.split_once('@') {
Some((body, ext)) if !ext.is_empty() => (body, ext.to_owned()),
Some(_) => return Err(invalid("扩展名为空")),
None => (raw, String::from("jar")),
};
let parts: Vec<&str> = body.split(':').collect();
if parts.len() < 3 {
return Err(invalid("至少需要 group:artifact:version 三段"));
}
if parts.iter().any(|p| p.is_empty()) {
return Err(invalid("存在空字段"));
}
match parts.len() {
3 => Ok(Self {
group: parts[0].to_owned(),
artifact: parts[1].to_owned(),
version: parts[2].to_owned(),
classifier: None,
extension,
}),
4 => Ok(Self {
group: parts[0].to_owned(),
artifact: parts[1].to_owned(),
version: parts[2].to_owned(),
classifier: Some(parts[3].to_owned()),
extension,
}),
_ => Err(invalid("字段多于四段")),
}
}
/// group 转目录形式(`org.lwjgl` → `org/lwjgl`)。
pub fn group_path(&self) -> String {
self.group.replace('.', "/")
}
/// 文件名:`<artifact>-<version>[-<classifier>].<ext>`。
pub fn file_name(&self) -> String {
match &self.classifier {
Some(classifier) => format!(
"{}-{}-{}.{}",
self.artifact, self.version, classifier, self.extension
),
None => format!("{}-{}.{}", self.artifact, self.version, self.extension),
}
}
/// 相对 `libraries/` 的路径:
/// `<group path>/<artifact>/<version>/<artifact>-<version>[-<classifier>].<ext>`。
pub fn relative_path(&self) -> PathBuf {
PathBuf::from(self.group_path())
.join(&self.artifact)
.join(&self.version)
.join(self.file_name())
}
/// 换一个 classifier 的副本(natives 用)。
pub fn with_classifier(&self, classifier: impl Into<String>) -> Self {
Self {
classifier: Some(classifier.into()),
..self.clone()
}
}
}
impl Library {
/// 解析本库的 Maven 坐标。
pub fn coordinate(&self) -> Result<MavenCoordinate, GameError> {
MavenCoordinate::parse(&self.name)
}
/// 该库在当前环境下是否生效。
pub fn applies(&self, env: &Environment) -> bool {
rules_allow(&self.rules, env)
}
/// 是否是实例私有库(HMCL `hint=local`)。
pub fn is_local(&self) -> bool {
self.hint.as_deref() == Some("local")
}
/// 主构件下载信息。
pub fn artifact(&self) -> Option<&DownloadInfo> {
self.downloads.as_ref()?.artifact.as_ref()
}
/// natives 分类器名:按 OS 查 `natives` 表并展开 `${arch}`(64 位为 `64`)。
pub fn native_classifier(&self, env: &Environment) -> Option<String> {
let raw = self.natives.as_ref()?.get(&env.os)?;
let arch = if matches!(env.arch.as_str(), "x86_64" | "arm64") {
"64"
} else {
""
};
Some(raw.replace("${arch}", arch))
}
/// natives 构件下载信息(按分类器查 `downloads.classifiers`)。
pub fn native_artifact(&self, classifier: &str) -> Option<&DownloadInfo> {
self.downloads.as_ref()?.classifiers.get(classifier)
}
/// 主构件相对 `libraries/` 的路径。
///
/// 优先用 JSON 里给出的 `artifact.path`(Mojang 官方的权威值),否则按坐标推导。
pub fn relative_path(&self) -> Result<PathBuf, GameError> {
if let Some(path) = self.artifact().and_then(|a| a.path.as_deref()) {
return Ok(PathBuf::from(path));
}
Ok(self.coordinate()?.relative_path())
}
/// natives 构件相对 `libraries/` 的路径。
pub fn native_relative_path(&self, classifier: &str) -> Result<PathBuf, GameError> {
if let Some(path) = self
.native_artifact(classifier)
.and_then(|a| a.path.as_deref())
{
return Ok(PathBuf::from(path));
}
Ok(self
.coordinate()?
.with_classifier(classifier)
.relative_path())
}
}