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

+376
View File
@@ -0,0 +1,376 @@
//! MC 版本 ↔ Java 版本映射(平移 HMCL 约束体系)+ 不符检查。
//!
//! 依据 `DevDocs/analyse/02-launch-pipeline.md` §5.4(HMCL `GameJavaVersion.getMinimumJavaVersion`)
//! 与 §3.1 的强制约束清单(`JavaVersionConstraint` 的 `isMandatory` 项)。两条权威叠加:
//!
//! | MC 版本 | 最低 | 最高 | 出处 |
//! | --- | --- | --- | --- |
//! | ≥ 26.1 | 25 | — | `java-runtime-epsilon` |
//! | ≥ 1.20.5 | 21 | — | `java-runtime-delta` |
//! | ≥ 1.18 | 17 | — | `java-runtime-beta` |
//! | ≥ 1.17 | 16 | — | `java-runtime-alpha` |
//! | ≥ 1.13 | 8 | — | `jre-legacy` |
//! | < 1.13 | 8 | **8** | `LAUNCH_WRAPPER`:LaunchWrapper 的 `URLClassLoader` 只能吃 Java 8 |
//!
//! **版本 JSON 优先**:装了 1.17 的玩家看的是 Mojang 自己写的 `javaVersion.majorVersion`
//! (1.17 就是 16),静态表只在清单没写时兜底——这也是 HMCL 的 `GAME_JSON` 强制项语义。
//!
//! 上限(`maximum`)只对老版本存在:1.12.2 用 Java 17 会直接
//! `UnsupportedClassVersionError`-级别的失败(启动器侧是 LaunchWrapper 假设),
//! 所以「1.12.2 + Java 21」要被判成不符,而不是「19 以上都能跑」。
use std::cmp::Ordering;
use super::super::inherit::resolve;
use super::super::layout::GameLayout;
use super::super::manifest::{JavaVersion, VersionJson};
/// 要求来源(`--json` 的 `requirement_source`)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequirementSource {
/// 版本 JSON 的 `javaVersion`(含继承链解析)。
Manifest,
/// 静态表(HMCL 约束体系的平移)。
Table,
}
impl RequirementSource {
/// 稳定字符串 id。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Manifest => "manifest",
Self::Table => "table",
}
}
}
/// 一个版本对 Java 的要求(闭区间)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JavaRequirement {
/// 最低主版本号(`21`)。
pub minimum: u32,
/// 最高主版本号(`Some(8)` = 只能用 8 及以下)。
pub maximum: Option<u32>,
/// 来源。
pub source: RequirementSource,
}
impl JavaRequirement {
/// 构造。
#[must_use]
pub const fn new(minimum: u32, maximum: Option<u32>, source: RequirementSource) -> Self {
Self {
minimum,
maximum,
source,
}
}
/// 静态表来源的构造。
#[must_use]
pub const fn table(minimum: u32, maximum: Option<u32>) -> Self {
Self::new(minimum, maximum, RequirementSource::Table)
}
/// 某主版本号是否满足要求。
#[must_use]
pub fn satisfied_by(&self, major: u32) -> bool {
major >= self.minimum && self.maximum.is_none_or(|maximum| major <= maximum)
}
/// 人读描述(`Java 21+` / `Java 8` / `Java 8–16`)。
#[must_use]
pub fn describe(&self) -> String {
match self.maximum {
Some(maximum) if maximum == self.minimum => format!("Java {}", self.minimum),
Some(maximum) => format!("Java {}–{}", self.minimum, maximum),
None => format!("Java {}+", self.minimum),
}
}
}
/// 不符的两种形态(D13:只提示,不阻断)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaMismatch {
/// Java 太低。
TooOld {
/// 需要的最低主版本。
required: u32,
/// 实际主版本。
actual: u32,
},
/// Java 太高(老版本只能用 8)。
TooNew {
/// 允许的最高主版本。
maximum: u32,
/// 实际主版本。
actual: u32,
},
}
impl JavaMismatch {
/// 稳定字符串 id(`--json` 的 `mismatch.code`)。
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::TooOld { .. } => "java_too_old",
Self::TooNew { .. } => "java_too_new",
}
}
/// 人读说明(GUI 弹窗 P1 后直接复用这句)。
#[must_use]
pub fn message(self) -> String {
match self {
Self::TooOld { required, actual } => format!(
"该版本需要 Java {required} 及以上,当前用的是 Java {actual}(大概率起不来)"
),
Self::TooNew { maximum, actual } => format!(
"该版本只能用 Java {maximum} 及以下,当前用的是 Java {actual}(老加载器吃不了新 class 文件)"
),
}
}
}
/// 判需求与实际是否相符(`None` = 相符)。
#[must_use]
pub fn check(actual: u32, required: &JavaRequirement) -> Option<JavaMismatch> {
if actual < required.minimum {
return Some(JavaMismatch::TooOld {
required: required.minimum,
actual,
});
}
if let Some(maximum) = required.maximum {
if actual > maximum {
return Some(JavaMismatch::TooNew { maximum, actual });
}
}
None
}
/// 静态表:MC 版本 → Java 要求。
///
/// 无法解析的版本(周快照 `20w14a`、魔改 id)只给下限 `8`、不设上限——
/// 宁可漏判一次,也不要把「Java 21 + 1.20 快照」误锁成「必须降到 8」。
#[must_use]
pub fn requirement_for(mc_version: &str) -> JavaRequirement {
let parts = version_parts(mc_version);
if parts.is_empty() {
return JavaRequirement::table(8, None);
}
if at_least(&parts, &[26, 1]) {
return JavaRequirement::table(25, None);
}
if at_least(&parts, &[1, 20, 5]) {
return JavaRequirement::table(21, None);
}
if at_least(&parts, &[1, 18]) {
return JavaRequirement::table(17, None);
}
if at_least(&parts, &[1, 17]) {
return JavaRequirement::table(16, None);
}
if at_least(&parts, &[1, 13]) {
return JavaRequirement::table(8, None);
}
JavaRequirement::table(8, Some(8))
}
/// 静态表的最低位(`recommended_major("1.20.5") == 21`)。
#[must_use]
pub fn recommended_major(mc_version: &str) -> u32 {
requirement_for(mc_version).minimum
}
/// 版本 JSON 的 `javaVersion` → 要求(字段缺失或为 0 时返回 `None`)。
///
/// Mojang 这个字段给的是**最低**要求(`majorVersion: 21` 表示 21 起跳),
/// 不声明上限——所以这里 `maximum` 恒为 `None`;上限由 [`requirement_for`] 补
/// (见 [`requirement_for_version`] 的合成说明)。
#[must_use]
pub fn requirement_from_manifest(java: &JavaVersion) -> Option<JavaRequirement> {
let major = java.major_version.filter(|major| *major > 0)?;
Some(JavaRequirement::new(
major,
None,
RequirementSource::Manifest,
))
}
/// 已安装版本的 Java 要求:解析继承链 → 版本 JSON 的 `javaVersion` 优先 → 静态表兜底。
///
/// **下限与上限来源不同**:Mojang 的 `javaVersion` 只声明「最低要多少」,
/// 而「1.12.2 及更早吃不了 Java 9+」这条知识来自 HMCL 的 `LAUNCH_WRAPPER` 约束,
/// 不在清单里。所以合成时下限取清单、上限取静态表——否则「1.12.2 + Java 21」
/// 会被判成完全合法(真机上必崩)。
///
/// 版本 JSON 读不到(未安装 / 损坏)时直接走静态表,不报错——这是「启动前提示」的路径,
/// 不该因为清单缺件就拦住用户(真正拦人的是 `snapshot::prepare`)。
#[must_use]
pub fn requirement_for_version(layout: &GameLayout, version_id: &str) -> JavaRequirement {
let table = requirement_for(version_id);
let read = |id: &str| -> Option<VersionJson> {
let text = std::fs::read_to_string(layout.version_json(id)).ok()?;
VersionJson::from_json_str(&text).ok()
};
let Some(manifest) = read(version_id) else {
return table;
};
let resolved = resolve(&read, &manifest).unwrap_or(manifest);
let Some(declared) = resolved
.java_version
.as_ref()
.and_then(requirement_from_manifest)
else {
// 清单没写 javaVersion:按 id 的静态表(继承来的 id 可能是魔改名)。
return match resolved.id.as_deref() {
Some(id) => requirement_for(id),
None => table,
};
};
JavaRequirement {
minimum: declared.minimum,
maximum: table.maximum,
source: RequirementSource::Manifest,
}
}
/// 版本串 → 数字分量(`1.20.5-pre1` → `[1, 20, 5]`;周快照 → 空)。
fn version_parts(raw: &str) -> Vec<u32> {
let trimmed = raw.trim();
if is_weekly_snapshot(trimmed) {
return Vec::new();
}
let mut parts = Vec::new();
for piece in trimmed.split('.') {
let digits: String = piece.chars().take_while(char::is_ascii_digit).collect();
match digits.parse::<u32>() {
Ok(value) => parts.push(value),
Err(_) => break,
}
}
parts
}
/// `20w14a` / `26w14a` 形态(与 `launch::game_version` 同一套判据)。
fn is_weekly_snapshot(raw: &str) -> bool {
let bytes = raw.as_bytes();
bytes.len() == 6
&& bytes[..2].iter().all(u8::is_ascii_digit)
&& bytes[2].eq_ignore_ascii_case(&b'w')
&& bytes[3..5].iter().all(u8::is_ascii_digit)
&& bytes[5].is_ascii_alphabetic()
}
/// `parts >= floor`(缺位按 0:`1.20` < `1.20.5`)。
fn at_least(parts: &[u32], floor: &[u32]) -> bool {
let len = parts.len().max(floor.len());
for index in 0..len {
let left = parts.get(index).copied().unwrap_or(0);
let right = floor.get(index).copied().unwrap_or(0);
match left.cmp(&right) {
Ordering::Greater => return true,
Ordering::Less => return false,
Ordering::Equal => {}
}
}
true
}
#[cfg(test)]
mod tests {
use super::{check, recommended_major, requirement_for, JavaMismatch, JavaRequirement};
#[test]
fn table_covers_the_hmcl_boundaries() {
let cases: &[(&str, u32, Option<u32>)] = &[
("1.6.4", 8, Some(8)),
("1.12.2", 8, Some(8)),
("1.13", 8, None),
("1.15.2", 8, None),
("1.16.5", 8, None),
("1.17", 16, None),
("1.17.1", 16, None),
("1.18", 17, None),
("1.19.4", 17, None),
("1.20.1", 17, None),
("1.20.4", 17, None),
("1.20.5", 21, None),
("1.21.4", 21, None),
("26.1", 25, None),
("26.3", 25, None),
("26.3-snapshot-1", 25, None),
];
for (version, minimum, maximum) in cases {
let requirement = requirement_for(version);
assert_eq!(
(requirement.minimum, requirement.maximum),
(*minimum, *maximum),
"{version}"
);
assert_eq!(recommended_major(version), *minimum, "{version}");
}
}
#[test]
fn weekly_snapshots_get_no_upper_bound() {
// 20w14a 的前导数字是 20,绝不能被当成「26.x」或误锁上限。
let requirement = requirement_for("20w14a");
assert_eq!(requirement.minimum, 8);
assert_eq!(requirement.maximum, None);
assert!(requirement.satisfied_by(21));
}
#[test]
fn pre_release_suffixes_do_not_change_the_floor() {
assert_eq!(requirement_for("1.20.5-pre1").minimum, 21);
assert_eq!(requirement_for("1.20.4-rc1").minimum, 17);
}
#[test]
fn check_reports_both_directions() {
let modern = JavaRequirement::table(21, None);
assert_eq!(check(21, &modern), None);
assert_eq!(check(25, &modern), None);
assert_eq!(
check(17, &modern),
Some(JavaMismatch::TooOld {
required: 21,
actual: 17
})
);
let legacy = JavaRequirement::table(8, Some(8));
assert_eq!(check(8, &legacy), None);
assert_eq!(
check(21, &legacy),
Some(JavaMismatch::TooNew {
maximum: 8,
actual: 21
})
);
assert_eq!(legacy.describe(), "Java 8");
assert_eq!(modern.describe(), "Java 21+");
assert_eq!(JavaRequirement::table(8, Some(16)).describe(), "Java 8–16");
}
#[test]
fn mismatch_messages_name_both_sides() {
let message = JavaMismatch::TooOld {
required: 21,
actual: 8,
}
.message();
assert!(message.contains("Java 21"));
assert!(message.contains("Java 8"));
assert_eq!(
JavaMismatch::TooNew {
maximum: 8,
actual: 21
}
.code(),
"java_too_new"
);
}
}