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
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:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,253 @@
|
||||
//! 命令行组装总表:`analyse/02` §4.1 的 33 项顺序表的 Rust 化。
|
||||
//!
|
||||
//! 顺序本身有语义(后加的默认值会被先加的显式值抑制):
|
||||
//!
|
||||
//! | # | 内容 | 本文件落点 |
|
||||
//! |---|------|-----------|
|
||||
//! | 1~3 | `nice` / 用户包装器 / java 可执行文件 | [`assemble`] |
|
||||
//! | 4 | 最高优先级 JVM 参数(含 `@argfile` 展开) | [`assemble`] |
|
||||
//! | 5~7 | `-Xmx`/`-Xms`、Metaspace、用户 JVM 参数(按默认值) | [`assemble`] |
|
||||
//! | 8~17 | 编码、Log4j、主 jar、代理、自动优化、版本开关、FML、渲染器 | `jvm_args` |
|
||||
//! | 18~19 | classpath 计算与占位符表(`${classpath}` 等 26 项) | [`assemble`] + `placeholders` |
|
||||
//! | 20~32 | 清单 JVM 参数、主类、游戏参数、QuickPlay、代理、图形后端、用户参数 | `game_args` |
|
||||
//! | 33 | 删除被禁止参数(`-Xincgc`) | [`assemble`] |
|
||||
//!
|
||||
//! 组装是**纯函数**:给定 `LaunchRequest` + `PlatformCtx` 一定得到同样的命令行,
|
||||
//! 因此可以整体快照测试(`crates/core/tests/launch/cmdline_snapshot.rs`)。
|
||||
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::launch::command_builder::{quote_for_display, CommandBuilder};
|
||||
use crate::launch::defaults;
|
||||
use crate::launch::error::{AssemblyError, Result};
|
||||
use crate::launch::game_args;
|
||||
use crate::launch::jvm_args;
|
||||
use crate::launch::placeholders::{join_classpath, PlaceholderMap};
|
||||
use crate::launch::platform::PlatformCtx;
|
||||
use crate::launch::request::{LaunchRequest, MemoryOptions};
|
||||
|
||||
/// 组装过程中的“非致命”告警(不中断启动,但要告诉用户我们改了什么)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AssemblyWarning {
|
||||
/// 注入了 log4j 参数却没有配置文件。
|
||||
Log4jConfigMissing,
|
||||
/// 用户自己写了代理参数,我们跳过自动代理参数。
|
||||
ProxyJvmArgsSkipped,
|
||||
/// `-Xms` 大于 `-Xmx`,已忽略 `-Xms`。
|
||||
MemoryMinIgnored {
|
||||
/// 请求的最小堆。
|
||||
min_mb: u32,
|
||||
/// 请求的最大堆。
|
||||
max_mb: u32,
|
||||
},
|
||||
/// 版本不支持 QuickPlay,已退化成 `--server/--port`。
|
||||
QuickPlayDegraded {
|
||||
/// 原始地址。
|
||||
address: String,
|
||||
},
|
||||
/// 服务器地址非法,QuickPlay 被丢弃。
|
||||
QuickPlayAddressInvalid {
|
||||
/// 原始地址。
|
||||
address: String,
|
||||
},
|
||||
/// 版本不支持 QuickPlay 且无法退化(存档/Realms)。
|
||||
QuickPlayUnsupported {
|
||||
/// 游戏版本。
|
||||
version: String,
|
||||
},
|
||||
/// 版本用不了 `--graphicsBackend`,已丢弃。
|
||||
///
|
||||
/// 两种来源:可解析版本低于 `26.2-snapshot-2`;或周快照(`26w14a`)但版本库表里
|
||||
/// 没有 `lwjgl-vulkan`(见 `game_args::graphics_backend_allowed`)。
|
||||
GraphicsBackendSkipped {
|
||||
/// 游戏版本。
|
||||
version: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for AssemblyWarning {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Log4jConfigMissing => f.write_str("log4j 注入已启用但缺少配置文件,已跳过"),
|
||||
Self::ProxyJvmArgsSkipped => f.write_str("用户已指定代理参数,跳过自动代理 JVM 参数"),
|
||||
Self::MemoryMinIgnored { min_mb, max_mb } => {
|
||||
write!(f, "-Xms{min_mb}m 大于 -Xmx{max_mb}m,已忽略 -Xms")
|
||||
}
|
||||
Self::QuickPlayDegraded { address } => {
|
||||
write!(f, "版本不支持 QuickPlay,已退化为 --server {address}")
|
||||
}
|
||||
Self::QuickPlayAddressInvalid { address } => {
|
||||
write!(f, "服务器地址非法,已丢弃 QuickPlay:{address}")
|
||||
}
|
||||
Self::QuickPlayUnsupported { version } => {
|
||||
write!(f, "版本 {version} 不支持该 QuickPlay 目标,已丢弃")
|
||||
}
|
||||
Self::GraphicsBackendSkipped { version } => {
|
||||
write!(f, "版本 {version} 不支持 --graphicsBackend,已丢弃")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 组装结果。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AssembledCommand {
|
||||
/// 实际要 spawn 的程序(通常是 java,带包装器时是包装器)。
|
||||
pub program: PathBuf,
|
||||
/// 参数(不含 `program`)。
|
||||
pub args: Vec<String>,
|
||||
/// 完整 classpath(库 + 主 jar 末位)。
|
||||
pub classpath: Vec<PathBuf>,
|
||||
/// 日志解码编码(决定 pump 用什么解码子进程输出)。
|
||||
pub encoding: String,
|
||||
/// 内存设置(与 `-Xmx`/`-Xms` 一致)。
|
||||
pub memory: MemoryOptions,
|
||||
/// 环境变量(实例标识/渲染/加载器/用户自定义)。
|
||||
pub env: Vec<(String, String)>,
|
||||
/// 组装告警。
|
||||
pub warnings: Vec<AssemblyWarning>,
|
||||
/// 主类。
|
||||
pub main_class: String,
|
||||
}
|
||||
|
||||
impl AssembledCommand {
|
||||
/// 完整 argv(`program` 在第一个)。
|
||||
pub fn argv(&self) -> Vec<String> {
|
||||
let mut argv = Vec::with_capacity(self.args.len() + 1);
|
||||
argv.push(self.program.display().to_string());
|
||||
argv.extend(self.args.iter().cloned());
|
||||
argv
|
||||
}
|
||||
|
||||
/// 人读形式(含空格的参数加引号),用于日志与快照测试。
|
||||
pub fn display(&self) -> String {
|
||||
let mut parts = Vec::with_capacity(self.args.len() + 1);
|
||||
parts.push(quote_for_display(&self.program.display().to_string()));
|
||||
for arg in &self.args {
|
||||
parts.push(quote_for_display(arg));
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/// 组装完整命令行。
|
||||
pub fn assemble(req: &LaunchRequest, ctx: &PlatformCtx) -> Result<AssembledCommand> {
|
||||
let main_class = req
|
||||
.manifest
|
||||
.main_class
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or(AssemblyError::MissingMainClass)?
|
||||
.to_string();
|
||||
if req.java.binary.as_os_str().is_empty() {
|
||||
return Err(AssemblyError::EmptyJavaPath.into());
|
||||
}
|
||||
if !req.missing_components.is_empty() {
|
||||
return Err(AssemblyError::MissingComponents(req.missing_components.clone()).into());
|
||||
}
|
||||
let classpath = effective_classpath(req);
|
||||
if classpath.is_empty() {
|
||||
return Err(AssemblyError::EmptyClasspath.into());
|
||||
}
|
||||
|
||||
// 第 19 项:占位符表(`${classpath}` 用最终 classpath,主 jar 已在末位)。
|
||||
let mut placeholders = PlaceholderMap::from_request(req, ctx);
|
||||
placeholders.insert(
|
||||
"classpath",
|
||||
join_classpath(&classpath, ctx.path_separator()),
|
||||
);
|
||||
|
||||
// 第 1~3 项:优先级包装 → 用户包装器 → java 可执行文件。
|
||||
let mut head: Vec<String> = Vec::new();
|
||||
if let Some(nice) = req.nice {
|
||||
if ctx.os.is_posix() {
|
||||
head.push("nice".to_string());
|
||||
head.push("-n".to_string());
|
||||
head.push(nice.to_string());
|
||||
}
|
||||
}
|
||||
head.extend(req.wrapper.iter().cloned());
|
||||
head.push(req.java.binary.display().to_string());
|
||||
let program = PathBuf::from(head.remove(0));
|
||||
|
||||
let mut builder = CommandBuilder::with_placeholders(placeholders);
|
||||
for token in &head {
|
||||
builder.add_without_parsing(token.clone());
|
||||
}
|
||||
|
||||
// 第 4 项:最高优先级 JVM 参数(原样追加,且登记 @argfile 展开结果)。
|
||||
builder.add_all_without_parsing_and_read_external(
|
||||
&req.user.override_jvm,
|
||||
&crate::launch::command_builder::read_argfile,
|
||||
);
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
// 第 5 项:`-Xmx` / `-Xms`(`-Xms` 必须 ≤ `-Xmx`)。
|
||||
if let Some(max_mb) = req.memory.max_mb {
|
||||
let _ = builder.add_default("-Xmx", &format!("{max_mb}m"));
|
||||
}
|
||||
if let Some(min_mb) = req.memory.min_mb {
|
||||
match req.memory.max_mb {
|
||||
Some(max_mb) if min_mb > max_mb => {
|
||||
warnings.push(AssemblyWarning::MemoryMinIgnored { min_mb, max_mb });
|
||||
}
|
||||
_ => {
|
||||
let _ = builder.add_default("-Xms", &format!("{min_mb}m"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第 6 项:Metaspace(Java ≥8)或 PermSize(更早)。
|
||||
if req.java.major >= 8 {
|
||||
let _ = builder.add_default("-XX:MetaspaceSize=", "64m");
|
||||
} else {
|
||||
let _ = builder.add_default("-XX:PermSize=", "64m");
|
||||
}
|
||||
|
||||
// 第 7 项:用户 JVM 参数——整体按“默认值”处理。
|
||||
builder.add_all_default(&req.user.jvm);
|
||||
|
||||
// 第 8~17 项。
|
||||
let (encoding, jvm_warnings) = jvm_args::apply_jvm_arguments(req, ctx, &mut builder);
|
||||
warnings.extend(jvm_warnings);
|
||||
|
||||
// 第 20~32 项。
|
||||
warnings.extend(game_args::apply_game_arguments(req, ctx, &mut builder));
|
||||
|
||||
// 第 33 项:删除禁用参数。
|
||||
builder.remove_forbidden(&defaults::forbidden_args(req.java.major));
|
||||
|
||||
let env = defaults::environment(req, ctx.os);
|
||||
Ok(AssembledCommand {
|
||||
program,
|
||||
args: builder.into_args(),
|
||||
classpath,
|
||||
encoding,
|
||||
memory: req.memory,
|
||||
env,
|
||||
warnings,
|
||||
main_class,
|
||||
})
|
||||
}
|
||||
|
||||
/// 最终 classpath:库按给定顺序去重,主 jar 永远追加在末位(Mojang 惯例,
|
||||
/// 影响同名类优先级,`analyse/02` §4.5)。
|
||||
pub fn effective_classpath(req: &LaunchRequest) -> Vec<PathBuf> {
|
||||
let mut classpath: Vec<PathBuf> = Vec::new();
|
||||
let candidates = req
|
||||
.classpath
|
||||
.iter()
|
||||
.chain(std::iter::once(&req.dirs.primary_jar));
|
||||
for path in candidates {
|
||||
if path.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !classpath.contains(path) {
|
||||
classpath.push(path.clone());
|
||||
}
|
||||
}
|
||||
classpath
|
||||
}
|
||||
Reference in new issue
Block a user