Files
Unlucky-Launcher/crates/core/src/launch/game_args.rs
T
AskaEth bc41dfcf7d
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
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

207 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 参数后段:清单 JVM 参数尾部(第 20~23 项)与游戏参数(第 24~32 项)。
//!
//! 顺序本身有语义(`analyse/02` §4.1):清单 JVM 参数在用户 `javaArguments` 之后
//! **无条件追加**,因此能覆盖用户参数,但覆盖不了用户的 override 参数;
//! 用户自定义游戏参数永远最后追加,是可以覆盖前面一切游戏参数的“逃生门”。
use crate::launch::args_table::AssemblyWarning;
use crate::launch::argument::{ArgumentEntry, RuleContext};
use crate::launch::command_builder::{tokenize, CommandBuilder};
use crate::launch::defaults;
use crate::launch::game_version::GameVersion;
use crate::launch::platform::PlatformCtx;
use crate::launch::request::{LaunchRequest, QuickPlay};
/// `--graphicsBackend` 的最低版本(实测首发:`26.2-snapshot-2`,2026-04-09)。
pub const GRAPHICS_BACKEND_MIN_VERSION: &str = "26.2-snapshot-2";
/// `--graphicsBackend` 能否下发给这个版本。
///
/// 两条判据**不要合并**(`research/05-mc-vulkan.md` §3.2/§4.2 的实测结论):
///
/// - **可解析版本**走精确门限 `26.2-snapshot-2`。`26.2-snapshot-1` 已经有 `lwjgl-vulkan`
/// 库但 `Main.class` 还没声明这个参数,用能力位放行会让它 `UnrecognizedOptionException`
/// 直接起不来——库早了两天。
/// - **周快照**(`26w14a` 形态)无法与 `26.2-snapshot-N` 排序,只能信版本库表:
/// 带 `lwjgl-vulkan` 就放行(26.1 全线含周快照都没有该库,所以这个推断安全)。
pub fn graphics_backend_allowed(version: &GameVersion, has_vulkan_backend: bool) -> bool {
if version.is_known() {
return version.is_at_least(GRAPHICS_BACKEND_MIN_VERSION);
}
version.is_weekly_snapshot() && has_vulkan_backend
}
/// 应用第 20~32 项,返回告警。
pub(crate) fn apply_game_arguments(
req: &LaunchRequest,
ctx: &PlatformCtx,
builder: &mut CommandBuilder,
) -> Vec<AssemblyWarning> {
let mut warnings = Vec::new();
let rule_ctx = RuleContext::new(
ctx.os.manifest_name(),
&ctx.arch,
&ctx.os_version,
&req.features,
);
// 第 20 项:清单 JVM 参数(现代)或 legacy 默认 JVM 参数。
if req.manifest.jvm.is_empty() {
builder.add_all_default(&defaults::legacy_jvm_arguments());
} else {
let values = collect_applicable(&req.manifest.jvm, &rule_ctx);
builder.add_all(&values);
}
// 第 21 项:登录源提供的 JVM 参数(authlib-injector 等)。
for arg in &req.auth_jvm_args {
builder.add(arg);
}
// 第 22 项:`-javaagent:` × N。
for agent in &req.java_agents {
builder.add(&format!("-javaagent:{agent}"));
}
// 第 23 项:主类(args_table 已保证非空)。
if let Some(main_class) = &req.manifest.main_class {
builder.add(main_class);
}
// 第 24 项:legacy `minecraftArguments` 整串分词。
if let Some(legacy) = &req.manifest.legacy_arguments {
for token in tokenize(legacy) {
builder.add(&token);
}
}
// 第 25 项:现代 `arguments.game`(带 rules/features)。
if !req.manifest.game.is_empty() {
let values = collect_applicable(&req.manifest.game, &rule_ctx);
builder.add_all(&values);
}
// 第 26 项:legacy 默认游戏参数(分辨率)。
if req.manifest.legacy_arguments.is_some() {
builder.add_all(&defaults::legacy_game_args(req.resolution.as_ref()));
}
// 第 27 项:登录源提供的游戏参数。
for arg in &req.auth_game_args {
builder.add(arg);
}
// 第 28 项:QuickPlay / `--server --port`。
apply_quick_play(req, builder, &mut warnings);
// 第 29 项:全屏。
if req.fullscreen {
builder.add("--fullscreen");
}
// 第 30 项:SOCKS 代理的游戏侧参数。
if let Some(proxy) = &req.proxy {
builder.add("--proxyHost");
builder.add(&proxy.host);
builder.add("--proxyPort");
builder.add(&proxy.port.to_string());
if let Some(username) = &proxy.username {
builder.add("--proxyUser");
builder.add(username);
}
if let Some(password) = &proxy.password {
builder.add("--proxyPass");
builder.add(password);
}
}
// 第 31 项:图形后端(精确门限 ≥26.2-snapshot-2,周快照走库表兜底)。
if let Some(backend) = &req.graphics_backend {
if graphics_backend_allowed(&req.instance.version, req.manifest.has_vulkan_backend) {
builder.add("--graphicsBackend");
builder.add(backend);
} else {
warnings.push(AssemblyWarning::GraphicsBackendSkipped {
version: req.instance.version.raw().to_string(),
});
}
}
// 第 32 项:用户自定义游戏参数(最后追加,可覆盖一切)。
builder.add_all(&req.user.game);
warnings
}
fn apply_quick_play(
req: &LaunchRequest,
builder: &mut CommandBuilder,
warnings: &mut Vec<AssemblyWarning>,
) {
let Some(quick_play) = &req.quick_play else {
return;
};
let supports = req.instance.version.is_at_least("1.20");
match quick_play {
QuickPlay::Singleplayer { world } if supports => {
builder.add("--quickPlaySingleplayer");
builder.add(world);
}
QuickPlay::Multiplayer { address } if supports => {
builder.add("--quickPlayMultiplayer");
builder.add(address);
}
QuickPlay::Realms { realm_id } if supports => {
builder.add("--quickPlayRealms");
builder.add(realm_id);
}
QuickPlay::Multiplayer { address } => match parse_server_address(address) {
Some((host, port)) => {
builder.add("--server");
builder.add(&host);
if let Some(port) = port {
builder.add("--port");
builder.add(&port.to_string());
}
warnings.push(AssemblyWarning::QuickPlayDegraded {
address: address.clone(),
});
}
None => warnings.push(AssemblyWarning::QuickPlayAddressInvalid {
address: address.clone(),
}),
},
QuickPlay::Singleplayer { .. } | QuickPlay::Realms { .. } => {
warnings.push(AssemblyWarning::QuickPlayUnsupported {
version: req.instance.version.raw().to_string(),
});
}
}
}
/// 解析 `host[:port]`;端口非法或为空时该段视为不存在。
pub fn parse_server_address(address: &str) -> Option<(String, Option<u16>)> {
let trimmed = address.trim();
if trimmed.is_empty() {
return None;
}
match trimmed.rsplit_once(':') {
Some((host, port)) if !host.is_empty() => match port.parse::<u16>() {
Ok(port) if port > 0 => Some((host.to_string(), Some(port))),
_ => None,
},
Some(_) => None,
None => Some((trimmed.to_string(), None)),
}
}
fn collect_applicable(entries: &[ArgumentEntry], ctx: &RuleContext<'_>) -> Vec<String> {
let mut values = Vec::new();
for entry in entries {
if entry.applies(ctx) {
values.extend(entry.values.iter().cloned());
}
}
values
}