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

+205
View File
@@ -0,0 +1,205 @@
//! 实例 → [`LaunchRequest`] 的装配:launch 命令与 `--dry-run` 共用这一段。
//!
//! 全部走 core 的既有公共面(`game::resolve` / `arguments.expand` / `GameLayout` /
//! `launch::adapter::build_request`),CLI 不复制任何继承/参数展开逻辑。
use std::path::PathBuf;
use crate::context::Ctx;
use crate::error::CliError;
use unlucky_core::game::arguments::Environment;
use unlucky_core::game::{self, ProfileId, VersionJson};
use unlucky_core::launch::adapter::{
self, AdapterOptions, AdapterWarning, LibraryInput, SnapshotInput,
};
use unlucky_core::launch::request::{AuthPlaceholders, LaunchRequest, MemoryOptions};
/// 装配输入(来自命令行 + 实例档案)。
#[derive(Debug, Clone, Default)]
pub struct PlanInput {
/// 版本 id(实例绑定的 `versions/<id>/`)。
pub version_id: String,
/// 实例 id(写进会话索引;无档案时为空)。
pub instance_id: String,
/// 实例显示名。
pub instance_name: String,
/// 已派生的运行目录(D20 隔离覆盖链在 `super::launch` 求值完,这里只消费)。
pub game_dir: Option<PathBuf>,
/// Java 可执行文件覆盖。
pub java: Option<PathBuf>,
/// 离线账户名(无账户时生效;有账户时身份由 `auth` 决定)。
pub username: Option<String>,
/// 已解析好的账户占位(`None` = 退回 `username` 的离线占位)。
pub auth: Option<AuthPlaceholders>,
/// 最大堆 MB。
pub max_memory: Option<u32>,
}
/// 装配结果。
#[derive(Debug, Clone)]
pub struct Plan {
/// 实际使用的版本 id。
pub version_id: String,
/// 装配好的启动请求。
pub request: LaunchRequest,
/// 非致命告警(缺主类/缺资源索引等)。
pub warnings: Vec<String>,
}
/// 装配一个 [`LaunchRequest`]。
///
/// # Errors
///
/// 版本 JSON 缺失/非法、继承链断裂(缺父版本/成环)时返回用户错误。
pub fn prepare(ctx: &Ctx, input: &PlanInput) -> Result<Plan, CliError> {
let game = ctx.game();
let version_id = input.version_id.clone();
let manifest_path = game.version_json(&version_id);
let manifest = read_version_json(&manifest_path)?;
let source = |id: &str| -> Option<VersionJson> {
let path = game.version_json(id);
read_version_json(&path).ok()
};
let resolved = game::resolve(&source, &manifest).map_err(|error| {
CliError::user("inherit", format!("解析继承链失败:{error}"))
.with_hint(format!("检查 {}", manifest_path.display()))
})?;
let environment = Environment::host();
let expanded = resolved
.arguments
.as_ref()
.map(|arguments| arguments.expand(&environment))
.unwrap_or_default();
let mut libraries: Vec<LibraryInput> = Vec::new();
for library in resolved.libraries.as_deref().unwrap_or_default() {
if !library.applies(&environment) || library.native_classifier(&environment).is_some() {
continue;
}
let Ok(coordinate) = library.coordinate() else {
continue;
};
let path = if library.is_local() {
game.version_library(&version_id, &coordinate)
} else {
game.library(&coordinate)
};
libraries.push(LibraryInput::new(path, library.name.clone()));
}
// natives 分类器构件(≤1.18.2 才有):收集口径归 core,CLI 只搬运。
let natives = unlucky_core::launch::natives::collect(
&game,
&version_id,
resolved.libraries.as_deref().unwrap_or_default(),
&environment,
);
let jar_id = resolved.jar.clone().unwrap_or_else(|| version_id.clone());
let primary_jar = game.version_jar(&jar_id);
// 运行目录:调用方(`ull launch`)已按隔离覆盖链派生;`None` 兜底到游戏根
// (库/其它消费者直接构造 `PlanInput` 时保持旧行为)。
let game_dir = input
.game_dir
.clone()
.unwrap_or_else(|| ctx.data.game_home.clone());
let assets_root = game.assets_dir();
let assets_index = resolved
.assets
.clone()
.or_else(|| resolved.asset_index.as_ref().map(|index| index.id.clone()));
let snapshot = SnapshotInput {
version_id: &version_id,
version_name: Some(&input.instance_name),
version_type: None,
game_version: None,
main_class: resolved.main_class.as_deref(),
jvm_arguments: &expanded.jvm,
game_arguments: &expanded.game,
legacy_arguments: resolved.minecraft_arguments.as_deref(),
libraries: &libraries,
natives: &natives,
primary_jar,
game_dir,
assets_index: assets_index.as_deref(),
natives_dir: game.natives_dir(&version_id),
library_dir: game.libraries_dir(),
assets_root: assets_root.clone(),
game_assets: assets_root,
};
let options = AdapterOptions {
java: super::java::selection(input.java.as_deref()),
memory: MemoryOptions {
min_mb: None,
max_mb: input.max_memory,
},
auth: input.auth.clone().unwrap_or_else(|| AuthPlaceholders {
player_name: input
.username
.clone()
.unwrap_or_else(|| "Player".to_string()),
uuid: ProfileId::generate().as_str().replace('-', ""),
user_type: "legacy".to_string(),
user_properties: "{}".to_string(),
..AuthPlaceholders::default()
}),
launcher_name: super::meta::LAUNCHER_NAME.to_string(),
launcher_version: env!("CARGO_PKG_VERSION").to_string(),
language: "zh-CN".to_string(),
..AdapterOptions::strict()
};
let output = adapter::build_request(&snapshot, options);
let warnings = output.warnings.iter().map(describe_warning).collect();
let mut request = output.request;
// 会话索引要用实例身份(adapter 默认拿版本 id/名)。
if !input.instance_id.is_empty() {
request.instance.id = input.instance_id.clone();
}
request.instance.name = input.instance_name.clone();
Ok(Plan {
version_id,
request,
warnings,
})
}
fn read_version_json(path: &std::path::Path) -> Result<VersionJson, CliError> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Err(CliError::user(
"version_missing",
format!("版本 JSON 不存在:{}", path.display()),
)
.with_hint("先准备好版本本体文件(下载装配属 P1 后续 mission)"));
}
Err(error) => return Err(CliError::io("读版本 JSON", path, &error)),
};
VersionJson::from_json_str(&text)
.map_err(|error| CliError::user("version_parse", format!("{}:{error}", path.display())))
}
/// 适配层告警 → 人读一行(`AdapterWarning` 没有 Display,这里集中翻译)。
fn describe_warning(warning: &AdapterWarning) -> String {
match warning {
AdapterWarning::MissingLibrary { name, path } => {
format!("缺少库文件 {name}({})", path.display())
}
AdapterWarning::MissingGameArguments => {
"版本 JSON 既没有 arguments.game 也没有 minecraftArguments".to_string()
}
AdapterWarning::MissingAssetsIndex => {
"版本 JSON 没有资源索引 id(${assets_index_name} 为空)".to_string()
}
AdapterWarning::MissingMainClass => "版本 JSON 没有 mainClass".to_string(),
AdapterWarning::MissingNativeJar { name, path } => {
format!("缺少 natives 构件 {name}({})", path.display())
}
}
}