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。
283 lines
10 KiB
Rust
283 lines
10 KiB
Rust
//! 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");
|
||
}
|
||
}
|