Files
Unlucky-Launcher/crates/core/src/game/java/mapping.rs
T
AskaEth ff78fe2cae fix(core): 老版本 Java 按版本距离选 + 启动看门狗有界等待 + 结束归属(M214 D4/D6/D10)
D4:`pick` 的排序键从「来源序」改成「版本距离 → 来源序」,全不满足时选最接近要求的那个
(托管 25 与 JAVA_HOME/扫盘 17 之间选 17,而不是按 rank 选托管)。新增 `select::decide`:
不符且**没有人显式指定 Java** 时 `ull launch` 直接报 `java_mismatch` 不 spawn(`--java` /
实例 `javaPath` 是豁免,只告警)——1.12.2 配 Java 17/25 必然起不来,警告没有信息量。
`JavaMismatch` + `check` 拆到 `mismatch.rs` 并加 `advice()`(mapping.rs 408→282 行)。

D6:`LaunchSession::finish` 的等进程改成**有界**。判据在 `startup_watch.rs`(纯逻辑):
①窗口从未出现且**连续 N 秒没有任何日志**(还在打日志就说明还在加载,慢加载不被判死);
②日志出现关闭标记后 N 秒仍未退出(实机「关窗后 java.exe 仍存活」的形状)。
到点只**停止等待、绝不杀进程**:写 `session_end(still_running)` + 索引 + `launcher.log`,
返回 `LaunchError::Watchdog`,CLI 翻成 `launch_timeout`。两个时长可配
(默认 300s/60s;`ULL_LAUNCH_STARTUP_TIMEOUT_SECS` / `ULL_LAUNCH_SHUTDOWN_GRACE_SECS`,
生效值每次启动写进 launcher.log 与人读输出)。

D10:把「被终止」与「应用错误」分开。①我们自己杀的:`tree_kill` **送达即登记**
`kill_registry`(pid 登记簿,取走语义、上限 64),覆盖 GUI 从别的线程 `kill_tree` 的路径,
`finish` 据此判 `killed`;②外部强杀:Windows 上「退出码 1 + 零错误输出 + 无崩溃/JVM 关键字」
判 `killed` 并标 `terminated_externally`(任务管理器与 `taskkill /F` 都是
`TerminateProcess(h, 1)`,应用真崩给的是 NTSTATUS 异常码)。

顺带:`SessionHandle` 拆 `session_handle.rs`(launch_session.rs 422→343 行)、
`NativesReport::to_json` 落 core(cli/launch.rs 腾出空间,仍 399 行)。
用例:select 距离/取舍、startup_watch 状态机、watchdog_cases(4 条真进程)、
exit_cases(外部终止与「我们杀的」区分)、pipeline 跨线程 kill。
2026-11-09 17:07:22 +08:00

283 lines
10 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.
//! MC 版本 ↔ Java 版本映射(平移 HMCL 约束体系);不符判定与文案见
//! [`crate::game::java::mismatch`](其中的 `check` / `JavaMismatch` 仍从本模块转出,
//! 调用方的 `mapping::check` 写法不变)。
//!
//! 依据 `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};
pub use super::mismatch::{check, JavaMismatch};
/// 要求来源(`--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),
}
}
}
/// 静态表: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::{recommended_major, requirement_for, 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 describe_spells_out_the_interval() {
assert_eq!(requirement_for("1.12.2").describe(), "Java 8");
assert_eq!(requirement_for("1.20.5").describe(), "Java 21+");
assert_eq!(JavaRequirement::table(8, Some(16)).describe(), "Java 8–16");
}
}