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

+267
View File
@@ -0,0 +1,267 @@
//! 五类信号采集接口:退出码 / 日志环形缓冲 / crash-reports / hs_err / 环境快照(+ mod 清单)。
//!
//! 分工(与 `launch-接口约定.md` 对齐):
//! - **推送类**([`ExitCollector`] / [`EnvironmentCollector`]):launch 已经算出事实,直接塞进 [`SignalBundle`];
//! - **磁盘类**([`CrashReportCollector`] / [`HsErrCollector`] / [`LatestLogCollector`]):本模块自己按
//! mtime 取最新一份,并可用 [`CollectionContext::started_at`] 过滤历史崩溃;
//! - 日志行由 launch 逐行喂 [`LogBuffer::push`],diagnose 不解析会话 JSONL(两边零类型耦合)。
//!
//! 采集是**尽力而为**:[`collect_all`] 把单个来源的失败收集成 [`CollectError`] 而不是中断整体——
//! 崩溃现场本来就残缺,少一个来源不该失去全部结论。
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[path = "environment.rs"]
pub mod environment;
#[path = "signals.rs"]
pub mod signals;
#[path = "sources.rs"]
pub mod sources;
pub use environment::{ComponentVersion, EnvironmentSnapshot, ModRecord};
pub use signals::{
ExitSignal, ExitType, LogBuffer, LogLine, SignalOrigin, CRASH_KEYWORDS, EXIT_ACCESS_VIOLATION,
EXIT_DLL_NOT_FOUND, EXIT_SIGKILL, EXIT_SIGTERM, EXIT_STACK_BUFFER_OVERRUN, JVM_ERROR_KEYWORDS,
};
pub use sources::{
CrashReportCollector, EnvironmentCollector, ExitCollector, HsErrCollector, LatestLogCollector,
};
use super::engine::DiagnosisInput;
use super::hs_err::HsErrReport;
/// 信号种类(可观测性 / 采集计划编排用)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SignalKind {
/// 退出码 + 退出类型。
Exit,
/// stdout/stderr 环形缓冲。
Log,
/// `crash-reports/crash-*.txt`。
CrashReport,
/// `hs_err_pid*.log`。
HsErr,
/// 环境快照。
Environment,
/// mod 清单。
Mods,
}
impl SignalKind {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Exit => "exit",
Self::Log => "log",
Self::CrashReport => "crash_report",
Self::HsErr => "hs_err",
Self::Environment => "environment",
Self::Mods => "mods",
}
}
}
/// 采集上下文。
#[derive(Debug, Clone, Default)]
pub struct CollectionContext {
/// 游戏工作目录(`crash-reports/`、`hs_err_pid*.log`、`logs/latest.log` 都在它下面)。
pub run_dir: PathBuf,
/// 进程启动时刻:只采集修改时间 ≥ 它的文件,避免把上次崩溃算进本次。
pub started_at: Option<SystemTime>,
}
impl CollectionContext {
/// 以工作目录构造。
#[must_use]
pub fn new(run_dir: impl Into<PathBuf>) -> Self {
Self {
run_dir: run_dir.into(),
started_at: None,
}
}
/// 带「只认本次会话文件」的时间下界。
#[must_use]
pub fn since(mut self, started_at: SystemTime) -> Self {
self.started_at = Some(started_at);
self
}
}
/// 采集失败(尽力而为:单源失败不影响其它来源)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CollectError {
/// 出错的信号种类。
pub kind: SignalKind,
/// 相关路径(可能为空,例如纯逻辑错误)。
pub path: Option<PathBuf>,
/// 人类可读原因。
pub message: String,
}
impl CollectError {
fn io(kind: SignalKind, path: &Path, error: &io::Error) -> Self {
Self {
kind,
path: Some(path.to_path_buf()),
message: error.to_string(),
}
}
}
impl fmt::Display for CollectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path = self
.path
.as_deref()
.map_or_else(String::new, |path| format!("({})", path.display()));
write!(f, "{} 采集失败{path}:{}", self.kind.as_str(), self.message)
}
}
impl std::error::Error for CollectError {}
/// 采到的源文件列表(诊断包导出 / 可观测性)。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CollectedSources {
/// 文件路径。
pub paths: Vec<PathBuf>,
}
impl CollectedSources {
/// 空。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 记一个源文件。
pub fn push(&mut self, path: impl Into<PathBuf>) {
self.paths.push(path.into());
}
/// 是否什么都没采到。
#[must_use]
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
}
/// 一个来源的采到的文本(含来源文件与时间)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapturedText {
/// 来源文件。
pub path: PathBuf,
/// 文本。
pub text: String,
/// 修改时间。
pub modified: Option<SystemTime>,
}
/// 采集到的全部信号。
#[derive(Debug, Clone, Default)]
pub struct SignalBundle {
/// 退出信号。
pub exit: Option<ExitSignal>,
/// 日志环形缓冲(launch 逐行喂;`latest.log` 兜底也进这里)。
pub log: LogBuffer,
/// 崩溃报告全文。
pub crash_report: Option<CapturedText>,
/// hs_err 解析结果。
pub hs_err: Option<HsErrReport>,
/// 环境快照。
pub environment: Option<EnvironmentSnapshot>,
/// mod 清单。
pub mods: Vec<ModRecord>,
/// 本次采到的源文件。
pub sources: Vec<PathBuf>,
}
impl SignalBundle {
/// 空包(日志缓冲默认 2000 行)。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 是否没有任何信号。
#[must_use]
pub fn is_empty(&self) -> bool {
self.exit.is_none()
&& self.log.is_empty()
&& self.crash_report.is_none()
&& self.hs_err.is_none()
&& self.environment.is_none()
&& self.mods.is_empty()
}
/// 转成规则引擎的输入。
///
/// 日志按来源分组成多份待匹配文本(stdout/stderr 分开),这样证据里能保留来源;
/// crash-report 与 hs_err 各占一份,取证优先级由 `SignalOrigin::evidence_rank` 决定。
#[must_use]
pub fn diagnosis_input(&self) -> DiagnosisInput {
let mut input = DiagnosisInput::new();
input.exit = self.exit.clone();
for (origin, text) in grouped_log_text(&self.log) {
input.push_text(origin, text);
}
if let Some(crash_report) = &self.crash_report {
input.push_text(SignalOrigin::CrashReport, crash_report.text.clone());
}
if let Some(report) = &self.hs_err {
input.push_hs_err(report.clone());
}
input
}
}
fn grouped_log_text(log: &LogBuffer) -> Vec<(SignalOrigin, String)> {
let mut groups: Vec<(SignalOrigin, String)> = Vec::new();
for line in log.lines() {
match groups.iter_mut().find(|(origin, _)| *origin == line.origin) {
Some((_, text)) => {
text.push_str(&line.text);
text.push('\n');
}
None => groups.push((line.origin, format!("{}\n", line.text))),
}
}
groups
}
/// 信号采集器。
pub trait SignalCollector {
/// 采哪种信号。
fn kind(&self) -> SignalKind;
/// 采集并写入 `out`;返回本次采到的源文件。
///
/// # Errors
///
/// 读盘失败 / 解析失败时返回 [`CollectError`];调用方([`collect_all`])会收集错误继续。
fn collect(
&self,
ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError>;
}
/// 按顺序跑一批采集器,单源失败只记录不中断。
pub fn collect_all(
collectors: &[&dyn SignalCollector],
ctx: &CollectionContext,
) -> (SignalBundle, Vec<CollectError>) {
let mut bundle = SignalBundle::new();
let mut errors = Vec::new();
for collector in collectors {
if let Err(error) = collector.collect(ctx, &mut bundle) {
errors.push(error);
}
}
(bundle, errors)
}
+321
View File
@@ -0,0 +1,321 @@
//! 规则引擎:全量匹配 → 去重 → 抑制关系 → 置信度排序 → 结构化报告。
//!
//! 与 HMCL `CrashReportAnalyzer.analyze` + `GameCrashWindow.merge` 的差异(analyse/02 §7.6 §7.10):
//! 1. 同一条规则只留**最优证据来源**的那次命中(崩溃报告 > hs_err > 日志 > stdout > stderr);
//! 2. 结论带 `priority`/`confidence`,主因在 `findings[0]`,被抑制的次生结论进 `secondary`;
//! 3. 输出是纯数据 [`DiagnosisRecord`](可序列化、可跨线程),不再泄漏匹配器状态;
//! 4. 退出码 / hs_err 这类**结构化信号**由内置结论补位(见 `synthetic.rs`)。
//!
//! 性能取舍:`scope: line` 逐行扫(59 条 × 行数,弱机可接受);`scope: file` 的跨行规则
//! 只在 ≤1 MiB 文本上跑,超长日志取首尾各 512 KiB。
use std::collections::BTreeMap;
use crate::diagnose::collectors::{ExitSignal, SignalOrigin};
use crate::diagnose::hs_err::HsErrReport;
use crate::diagnose::rules::{CompiledRule, MatchScope};
#[path = "finding.rs"]
mod finding;
#[path = "synthetic.rs"]
mod synthetic;
pub use finding::{
Candidate, DiagnosisRecord, DiagnosisReport, Evidence, ExitRecord, Finding,
CONCLUSIVE_CONFIDENCE,
};
pub use synthetic::synthetic_findings;
/// 超过这个字节数就不再整份扫描 `scope: file` 规则(取首尾窗口)。
const FILE_SCOPE_LIMIT: usize = 1024 * 1024;
/// 超长文本的扫描窗口(首尾各一份)。
const FILE_SCOPE_WINDOW: usize = 512 * 1024;
/// 证据行截断长度(避免把整份栈塞进诊断输出)。
const EVIDENCE_MAX_CHARS: usize = 300;
/// 待匹配的一段文本(一个信号来源一份)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Haystack {
/// 来源(决定证据前缀与取证优先级)。
pub origin: SignalOrigin,
/// 文本。
pub text: String,
}
impl Haystack {
/// 构造。
#[must_use]
pub fn new(origin: SignalOrigin, text: impl Into<String>) -> Self {
Self {
origin,
text: text.into(),
}
}
}
/// 引擎输入:退出信号 + 若干待匹配文本 +(可选)解析好的 hs_err。
#[derive(Debug, Clone, Default)]
pub struct DiagnosisInput {
/// 退出信号。
pub exit: Option<ExitSignal>,
/// 待匹配文本(按 `SignalOrigin::evidence_rank` 优先取证据)。
pub haystacks: Vec<Haystack>,
/// 解析好的 `hs_err_pid*.log`(产生内置结论,其摘要也已进 `haystacks`)。
pub hs_err: Option<HsErrReport>,
}
impl DiagnosisInput {
/// 空输入。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 带退出信号。
#[must_use]
pub fn with_exit(mut self, exit: ExitSignal) -> Self {
self.exit = Some(exit);
self
}
/// 追加一段待匹配文本。
pub fn push_text(&mut self, origin: SignalOrigin, text: impl Into<String>) {
self.haystacks.push(Haystack::new(origin, text));
}
/// 追加解析好的 hs_err:摘要进待匹配文本,结构留给内置结论。
pub fn push_hs_err(&mut self, report: HsErrReport) {
self.haystacks
.push(Haystack::new(SignalOrigin::HsErr, report.digest()));
self.hs_err = Some(report);
}
/// 是否什么信号都没有。
#[must_use]
pub fn is_empty(&self) -> bool {
self.exit.is_none() && self.haystacks.is_empty()
}
}
/// 规则引擎(规则只编译一次,可跨线程共享)。
#[derive(Debug, Clone, Default)]
pub struct Diagnoser {
rules: Vec<CompiledRule>,
}
impl Diagnoser {
/// 用一组已编译的规则构造。
#[must_use]
pub fn new(rules: Vec<CompiledRule>) -> Self {
Self { rules }
}
/// 规则表。
#[must_use]
pub fn rules(&self) -> &[CompiledRule] {
&self.rules
}
/// 跑一遍诊断。
#[must_use]
pub fn analyze(&self, input: &DiagnosisInput) -> DiagnosisReport {
let mut ordered = input.haystacks.iter().collect::<Vec<_>>();
ordered.sort_by_key(|haystack| haystack.origin.evidence_rank());
let mut findings = Vec::new();
for rule in &self.rules {
for haystack in &ordered {
if let Some(found) = scan(rule, haystack) {
findings.push(build_finding(rule, haystack, &found));
break;
}
}
}
findings.extend(synthetic_findings(input));
let (mut findings, mut secondary) = split_suppressed(findings);
findings.sort_by(Finding::rank_cmp);
secondary.sort_by(Finding::rank_cmp);
DiagnosisReport {
findings,
secondary,
exit: input.exit.clone(),
rules_evaluated: self.rules.len(),
hs_err: input.hs_err.clone(),
}
}
}
/// 一次正则命中(绝对偏移 + 命名捕获组)。
#[derive(Debug)]
struct RawMatch {
groups: BTreeMap<String, String>,
offset: usize,
}
fn scan(rule: &CompiledRule, haystack: &Haystack) -> Option<RawMatch> {
match rule.scope() {
MatchScope::Line => scan_lines(rule, &haystack.text),
MatchScope::Block => scan_blocks(rule, &haystack.text),
MatchScope::File => scan_file(rule, &haystack.text),
}
}
/// 逐行匹配(返回命中行的起始偏移,行号即证据行号)。
fn scan_lines(rule: &CompiledRule, text: &str) -> Option<RawMatch> {
let mut offset = 0;
for chunk in text.split_inclusive('\n') {
let line = chunk.trim_end_matches(['\r', '\n']);
if let Some(caps) = rule.regex().captures(line) {
return Some(capture_groups(rule, &caps, offset));
}
offset += chunk.len();
}
None
}
/// 按空行切块匹配(一段异常 + 其上下文)。
///
/// 分隔符是**空行**而不是字面 `"\n\n"`:日志文件可能是 CRLF(Windows 上 `latest.log` 常见),
/// 按字节找 `\n\n` 会把整份日志当成一个块,`scope: block` 的规则就退化成整份扫描。
fn scan_blocks(rule: &CompiledRule, text: &str) -> Option<RawMatch> {
let mut block_start = 0;
let mut offset = 0;
for chunk in text.split_inclusive('\n') {
offset += chunk.len();
if chunk.trim().is_empty() {
if let Some(caps) = rule.regex().captures(&text[block_start..offset]) {
return Some(capture_groups(rule, &caps, block_start));
}
block_start = offset;
}
}
if block_start < text.len() {
let caps = rule.regex().captures(&text[block_start..])?;
return Some(capture_groups(rule, &caps, block_start));
}
None
}
/// 整份匹配;超长文本只看首尾窗口(跨行贪婪规则的性能边界)。
fn scan_file(rule: &CompiledRule, text: &str) -> Option<RawMatch> {
if text.len() <= FILE_SCOPE_LIMIT {
let caps = rule.regex().captures(text)?;
let offset = caps.get(0).map_or(0, |found| found.start());
return Some(capture_groups(rule, &caps, offset));
}
let head_end = ceil_boundary(text, FILE_SCOPE_WINDOW);
if let Some(caps) = rule.regex().captures(&text[..head_end]) {
let offset = caps.get(0).map_or(0, |found| found.start());
return Some(capture_groups(rule, &caps, offset));
}
let tail_start = floor_boundary(text, text.len() - FILE_SCOPE_WINDOW);
let caps = rule.regex().captures(&text[tail_start..])?;
let offset = tail_start + caps.get(0).map_or(0, |found| found.start());
Some(capture_groups(rule, &caps, offset))
}
fn capture_groups(rule: &CompiledRule, caps: &regex::Captures<'_>, offset: usize) -> RawMatch {
let mut groups = BTreeMap::new();
for name in rule.regex().capture_names().flatten() {
if let Some(found) = caps.name(name) {
groups.insert(name.to_owned(), found.as_str().trim().to_owned());
}
}
RawMatch { groups, offset }
}
fn build_finding(compiled: &CompiledRule, haystack: &Haystack, found: &RawMatch) -> Finding {
let rule = compiled.rule();
let (message, detail) = rule.cause.render(&found.groups);
Finding {
rule_id: rule.id.clone(),
category: compiled.category(),
severity: rule.severity,
confidence: rule.confidence.clamp(0.0, 1.0),
priority: rule.priority,
message_key: rule.cause.key.clone(),
message,
detail,
evidence: vec![Evidence::new(
haystack.origin,
line_of(&haystack.text, found.offset),
snippet(&haystack.text, found.offset),
)],
fix_actions: rule.fix_action.clone(),
groups: found.groups.clone(),
suppress: rule.suppress.clone(),
suppressed_by: None,
}
}
/// 把被 suppress 的结论拆到次生列表(不丢弃,UI 折叠展示)。
fn split_suppressed(findings: Vec<Finding>) -> (Vec<Finding>, Vec<Finding>) {
let suppressers = findings
.iter()
.map(|finding| {
(
finding.rule_id.clone(),
finding.priority,
finding.suppress.clone(),
)
})
.collect::<Vec<_>>();
let mut kept = Vec::new();
let mut suppressed = Vec::new();
for mut finding in findings {
let mut best: Option<(&str, u8)> = None;
for (id, priority, suppress) in &suppressers {
if *id == finding.rule_id || !suppress.iter().any(|item| item == &finding.rule_id) {
continue;
}
if best.is_none_or(|(_, best_priority)| *priority > best_priority) {
best = Some((id.as_str(), *priority));
}
}
match best {
Some((id, _)) => {
finding.suppressed_by = Some(id.to_owned());
suppressed.push(finding);
}
None => kept.push(finding),
}
}
(kept, suppressed)
}
fn line_of(text: &str, offset: usize) -> usize {
let offset = offset.min(text.len());
1 + text[..offset].matches('\n').count()
}
fn snippet(text: &str, offset: usize) -> String {
let offset = offset.min(text.len());
let start = text[..offset].rfind('\n').map_or(0, |index| index + 1);
let end = text[start..]
.find('\n')
.map_or(text.len(), |index| start + index);
let line = text[start..end].trim();
let mut out = line.chars().take(EVIDENCE_MAX_CHARS).collect::<String>();
if line.chars().count() > EVIDENCE_MAX_CHARS {
out.push('…');
}
out
}
fn ceil_boundary(text: &str, mut index: usize) -> usize {
index = index.min(text.len());
while index < text.len() && !text.is_char_boundary(index) {
index += 1;
}
index
}
fn floor_boundary(text: &str, mut index: usize) -> usize {
index = index.min(text.len());
while index > 0 && !text.is_char_boundary(index) {
index -= 1;
}
index
}
+155
View File
@@ -0,0 +1,155 @@
//! 环境快照与 mod 清单(analyse/02 §7.1 信号 E)。
//!
//! HMCL 只在崩溃窗口里散着展示这些信息;我们做成结构化数据,
//! 既进诊断输出(`environment.json` 一样的机器可读上下文),也让规则能按版本号做前置判断。
//! 由 `collectors.rs` 用 `#[path]` 声明并统一导出。
use std::path::PathBuf;
/// 一个已装组件的版本(Forge/Fabric/OptiFine…)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentVersion {
/// 组件名(`forge`/`fabric`/`optifine`/`minecraft`…)。
pub name: String,
/// 版本串。
pub version: String,
}
impl ComponentVersion {
/// 构造。
#[must_use]
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
}
}
}
/// 一个已装 mod(供交叉验证「冲突/mixin 失败」类结论,analyse/02 §8.4-10)。
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ModRecord {
/// modid(解析不出时为空)。
pub id: String,
/// 展示名。
pub name: Option<String>,
/// 版本。
pub version: Option<String>,
/// jar 文件名(诊断包里给人看)。
pub file: Option<String>,
/// 是否启用(`.disabled` 视为未启用)。
pub enabled: bool,
}
impl ModRecord {
/// 从文件名构造(不解 jar,P0 足够:冲突类结论要的是「装了哪些文件」)。
#[must_use]
pub fn from_file_name(file: &str) -> Self {
let enabled = !file.ends_with(".disabled");
let stem = file
.trim_end_matches(".disabled")
.trim_end_matches(".jar")
.to_owned();
let (name, version) = stem
.rsplit_once('-')
.filter(|(_, version)| version.chars().next().is_some_and(|ch| ch.is_ascii_digit()))
.map_or((stem.clone(), None), |(name, version)| {
(name.to_owned(), Some(version.to_owned()))
});
Self {
id: name.to_lowercase(),
name: Some(name),
version,
file: Some(file.to_owned()),
enabled,
}
}
}
/// 启动环境快照(Java/内存/OS/组件/mod 清单)。
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EnvironmentSnapshot {
/// 启动器版本(`ull 0.1.0`)。
pub launcher: Option<String>,
/// 实例 id。
pub instance_id: Option<String>,
/// 游戏版本。
pub game_version: Option<String>,
/// 操作系统名(`Windows 10.0.19045`)。
pub os_name: Option<String>,
/// 架构(`x86_64` / `aarch64`)。
pub os_arch: Option<String>,
/// 物理内存(MB)。
pub physical_memory_mb: Option<u64>,
/// Java 可执行文件。
pub java_path: Option<PathBuf>,
/// Java 大版本。
pub java_major: Option<u32>,
/// Java 位数(32 / 64)。
pub java_bits: Option<u32>,
/// `-Xmx`(MB)。
pub xmx_mb: Option<u32>,
/// `-Xms`(MB)。
pub xms_mb: Option<u32>,
/// 工作目录(跑游戏的 cwd)。
pub run_dir: Option<PathBuf>,
/// 已装组件版本。
pub components: Vec<ComponentVersion>,
/// 已装 mod。
pub mods: Vec<ModRecord>,
}
impl EnvironmentSnapshot {
/// 一行人类可读摘要(证据行 / UI 头部)。
#[must_use]
pub fn summary(&self) -> String {
let os = format!(
"{} {}",
self.os_name.as_deref().unwrap_or("未知系统"),
self.os_arch.as_deref().unwrap_or("")
);
let java = self.java_major.map_or_else(
|| "Java 未知".to_owned(),
|major| match self.java_bits {
Some(bits) => format!("Java {major}({bits} 位)"),
None => format!("Java {major}"),
},
);
let memory = match (self.xmx_mb, self.physical_memory_mb) {
(Some(xmx), Some(physical)) => format!("-Xmx {xmx} MB / 物理 {physical} MB"),
(Some(xmx), None) => format!("-Xmx {xmx} MB"),
(None, Some(physical)) => format!("物理 {physical} MB"),
(None, None) => "内存未知".to_owned(),
};
let mods = self.mods.len();
format!("{} / {} / {} / {} 个 mod", os.trim(), java, memory, mods)
}
/// 是不是 32 位 Java(`-Xmx` 超过 ~1.5 GB 必崩的直接证据)。
#[must_use]
pub fn is_java_32bit(&self) -> bool {
self.java_bits == Some(32)
}
/// 组件版本(按名字查,`optifine` 之类的冲突规则用)。
#[must_use]
pub fn component(&self, name: &str) -> Option<&str> {
self.components
.iter()
.find(|component| component.name.eq_ignore_ascii_case(name))
.map(|component| component.version.as_str())
}
/// 是否装了某个 mod(按 id 或文件名模糊匹配)。
#[must_use]
pub fn has_mod(&self, id: &str) -> bool {
let needle = id.to_ascii_lowercase();
self.mods.iter().any(|record| {
record.id.contains(&needle)
|| record
.file
.as_deref()
.is_some_and(|file| file.to_ascii_lowercase().contains(&needle))
})
}
}
+256
View File
@@ -0,0 +1,256 @@
//! 结论与报告的数据结构(纯数据,可序列化,不持有匹配器状态)。
//!
//! HMCL 的 `CrashReportAnalyzer.Result` 直接把 `Matcher` 传出去(不可序列化、不能跨线程),
//! 这里改成「证据 + 已渲染文案 + 物化动作」的结构化记录(analyse/02 §7.10-6)。
//!
//! 由 `engine.rs` 用 `#[path]` 声明并统一导出;拆分只为守单文件 400 行上限。
use std::collections::BTreeMap;
use serde::Serialize;
use crate::diagnose::collectors::{ExitSignal, SignalOrigin};
use crate::diagnose::hs_err::HsErrReport;
use crate::diagnose::rules::{FixAction, MaterializedFixAction, RuleCategory, Severity};
/// 「结论足够确定」的可信度门槛:低于它就该交给 AI 深化(PlanDoc/06 §2.3)。
pub const CONCLUSIVE_CONFIDENCE: f32 = 0.8;
/// 一条证据(哪个来源的第几行的哪段文本)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Evidence {
/// 来源。
pub origin: SignalOrigin,
/// 行号(1 起;结构化信号为 0)。
pub line: usize,
/// 文本片段(≤300 字符)。
pub text: String,
}
impl Evidence {
/// 构造。
#[must_use]
pub fn new(origin: SignalOrigin, line: usize, text: impl Into<String>) -> Self {
Self {
origin,
line,
text: text.into(),
}
}
/// 诊断输出里的字符串形式:`"<origin>:<line> <text>"`。
#[must_use]
pub fn render(&self) -> String {
format!("{}:{} {}", self.origin, self.line, self.text)
}
}
/// 一条结论。
#[derive(Debug, Clone)]
pub struct Finding {
/// 规则 id(内置结论用 `signal.*` / `hs_err.*`)。
pub rule_id: String,
/// 分类。
pub category: RuleCategory,
/// 严重度。
pub severity: Severity,
/// 可信度。
pub confidence: f32,
/// 主因权重。
pub priority: u8,
/// i18n / AI 引用 id。
pub message_key: String,
/// 渲染后的中文结论(M16 `diagnose_crash` 直接给模型看)。
pub message: String,
/// 细化说明 / 处理建议。
pub detail: Option<String>,
/// 证据。
pub evidence: Vec<Evidence>,
/// 六维修复动作。
pub fix_actions: Vec<FixAction>,
/// 规则捕获组(供动作模板替换)。
pub groups: BTreeMap<String, String>,
/// 本结论会抑制哪些规则 id。
pub suppress: Vec<String>,
/// 被哪条结论抑制(`Some` 即次生结论)。
pub suppressed_by: Option<String>,
}
impl Finding {
/// 一条只有骨架的结论(内置结论的构造起点,其余字段由调用方填)。
#[must_use]
pub fn skeleton(
rule_id: &str,
category: RuleCategory,
severity: Severity,
confidence: f32,
priority: u8,
message_key: &str,
message: &str,
) -> Self {
Self {
rule_id: rule_id.to_owned(),
category,
severity,
confidence,
priority,
message_key: message_key.to_owned(),
message: message.to_owned(),
detail: None,
evidence: Vec::new(),
fix_actions: Vec::new(),
groups: BTreeMap::new(),
suppress: Vec::new(),
suppressed_by: None,
}
}
/// 物化后的修复动作(模板已替换)。
#[must_use]
pub fn materialized_actions(&self) -> Vec<MaterializedFixAction> {
self.fix_actions
.iter()
.map(|action| action.materialize(&self.groups))
.collect()
}
/// 转成诊断输出里的候选。
#[must_use]
pub fn to_candidate(&self) -> Candidate {
Candidate {
id: self.rule_id.clone(),
severity: self.severity.as_str(),
category: self.category.as_str(),
confidence: self.confidence,
message_key: self.message_key.clone(),
message: self.message.clone(),
detail: self.detail.clone(),
evidence: self.evidence.iter().map(Evidence::render).collect(),
fix_actions: self.materialized_actions(),
}
}
/// 排序:priority 高 → confidence 高 → id 字典序(保证确定性)。
#[must_use]
pub fn rank_cmp(&self, other: &Self) -> std::cmp::Ordering {
other
.priority
.cmp(&self.priority)
.then_with(|| other.confidence.total_cmp(&self.confidence))
.then_with(|| self.rule_id.cmp(&other.rule_id))
}
}
/// 诊断输出里的一条候选结论(字段名与 `launch-接口约定.md` §3 `diagnosis` 对齐)。
#[derive(Debug, Clone, Serialize)]
pub struct Candidate {
/// 规则 id。
pub id: String,
/// 严重度。
pub severity: &'static str,
/// 分类。
pub category: &'static str,
/// 可信度。
pub confidence: f32,
/// i18n / AI 引用 id。
pub message_key: String,
/// 渲染后的中文结论。
pub message: String,
/// 细化说明。
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
/// 证据字符串。
pub evidence: Vec<String>,
/// 六维修复动作(M16/UI 一键修复入口)。
pub fix_actions: Vec<MaterializedFixAction>,
}
/// 退出信号摘要(诊断输出用)。
#[derive(Debug, Clone, Serialize)]
pub struct ExitRecord {
/// 退出码。
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<i32>,
/// 退出类型(与 launch 约定同值)。
pub exit_type: &'static str,
/// 疑似静默失败。
pub silent_failure: bool,
/// 存活毫秒数。
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_ms: Option<u64>,
}
/// 交给 launch 写入 `diagnosis` 事件的结构化结果。
#[derive(Debug, Clone, Serialize)]
pub struct DiagnosisRecord {
/// 结论来源(`rules` / `ai`)。
pub source: &'static str,
/// 规则没把握、需要 AI 或用户补充信息。
pub needs_more_info: bool,
/// 结论(`candidates[0]` 是主因)。
pub candidates: Vec<Candidate>,
/// 被抑制的次生结论。
pub secondary: Vec<Candidate>,
/// 本次判定用的退出信号摘要。
#[serde(skip_serializing_if = "Option::is_none")]
pub exit: Option<ExitRecord>,
}
/// 一次诊断的完整结果。
#[derive(Debug, Clone, Default)]
pub struct DiagnosisReport {
/// 结论(已排序,主因在前)。
pub findings: Vec<Finding>,
/// 被抑制的次生结论。
pub secondary: Vec<Finding>,
/// 本次判定用的退出信号。
pub exit: Option<ExitSignal>,
/// 评估了多少条规则(可观测性)。
pub rules_evaluated: usize,
/// 解析出的 hs_err(若有)。
pub hs_err: Option<HsErrReport>,
}
impl DiagnosisReport {
/// 主因(可能没有)。
#[must_use]
pub fn primary(&self) -> Option<&Finding> {
self.findings.first()
}
/// 规则层有没有把握:没有任何结论,或主因可信度 < [`CONCLUSIVE_CONFIDENCE`]。
#[must_use]
pub fn needs_more_info(&self) -> bool {
self.primary()
.is_none_or(|finding| finding.confidence < CONCLUSIVE_CONFIDENCE)
}
/// 去重后的修复动作(主因优先,供 UI 一键修复 / agent 直接执行)。
#[must_use]
pub fn fix_actions(&self) -> Vec<MaterializedFixAction> {
let mut out: Vec<MaterializedFixAction> = Vec::new();
for action in self.findings.iter().flat_map(Finding::materialized_actions) {
if !out.contains(&action) {
out.push(action);
}
}
out
}
/// 转成可序列化的诊断记录。
#[must_use]
pub fn to_record(&self) -> DiagnosisRecord {
DiagnosisRecord {
source: "rules",
needs_more_info: self.needs_more_info(),
candidates: self.findings.iter().map(Finding::to_candidate).collect(),
secondary: self.secondary.iter().map(Finding::to_candidate).collect(),
exit: self.exit.as_ref().map(|exit| ExitRecord {
code: exit.code,
exit_type: exit.exit_type.as_str(),
silent_failure: exit.silent_failure,
uptime_ms: exit.uptime_ms,
}),
}
}
}
+213
View File
@@ -0,0 +1,213 @@
//! 六维修复动作本体:枚举、维度、以及「代入捕获组 → agent 工具参数」的物化。
//!
//! 六个维度与 analyse/02 §7.7 的动作分类一一对应,也是 M16 `ToolSpec` 的参数来源
//! (契约见同目录 `CONTRACT.md` §3)。
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::fix_params::{AdjustMemory, ChangeGraphics, ChangeJava, Reinstall, RemoveMod, SeekHelp};
use super::substitute;
/// 六维修复动作(YAML 里 `kind:` 选维度)。
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FixAction {
/// 换 Java。
ChangeJava(ChangeJava),
/// 调内存。
AdjustMemory(AdjustMemory),
/// 改图形设置。
ChangeGraphics(ChangeGraphics),
/// 删/禁/换 mod。
RemoveMod(RemoveMod),
/// 重装/修组件。
Reinstall(Reinstall),
/// 求助上报。
SeekHelp(SeekHelp),
}
/// 六维动作的维度枚举(不含参数,便于判等/去重/映射工具)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FixActionKind {
/// 换 Java。
ChangeJava,
/// 调内存。
AdjustMemory,
/// 改图形设置。
ChangeGraphics,
/// 删/禁/换 mod。
RemoveMod,
/// 重装/修组件。
Reinstall,
/// 求助上报。
SeekHelp,
}
impl FixActionKind {
/// 稳定字符串(诊断输出的 `kind` 字段,与 YAML 的 `kind:` 一致)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ChangeJava => "change_java",
Self::AdjustMemory => "adjust_memory",
Self::ChangeGraphics => "change_graphics",
Self::RemoveMod => "remove_mod",
Self::Reinstall => "reinstall",
Self::SeekHelp => "seek_help",
}
}
/// 建议的 agent 工具 id。
///
/// M16 的 P0 工具只有 5 个(`get_config`/`set_config`/`list_game_sessions`/`read_game_log`/
/// `diagnose_crash`),因此它按 `kind` 桥接;这里的 id 是 P1 落地细分工具时的直接映射。
#[must_use]
pub const fn agent_tool(self) -> &'static str {
match self {
Self::ChangeJava => "java.set",
Self::AdjustMemory => "memory.set",
Self::ChangeGraphics => "graphics.set",
Self::RemoveMod => "mod.remove",
Self::Reinstall => "component.reinstall",
Self::SeekHelp => "diagnostics.report",
}
}
/// 全部六维(测试与 UI 遍历用)。
#[must_use]
pub const fn all() -> [Self; 6] {
[
Self::ChangeJava,
Self::AdjustMemory,
Self::ChangeGraphics,
Self::RemoveMod,
Self::Reinstall,
Self::SeekHelp,
]
}
}
/// 已做模板替换/单位换算的动作,可直接当 agent 工具参数。
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MaterializedFixAction {
/// 维度。
pub kind: FixActionKind,
/// agent 工具 id(见 [`FixActionKind::agent_tool`])。
pub tool: &'static str,
/// 扁平字符串参数表。
pub args: BTreeMap<String, String>,
}
impl FixAction {
/// 维度。
#[must_use]
pub const fn kind(&self) -> FixActionKind {
match self {
Self::ChangeJava(_) => FixActionKind::ChangeJava,
Self::AdjustMemory(_) => FixActionKind::AdjustMemory,
Self::ChangeGraphics(_) => FixActionKind::ChangeGraphics,
Self::RemoveMod(_) => FixActionKind::RemoveMod,
Self::Reinstall(_) => FixActionKind::Reinstall,
Self::SeekHelp(_) => FixActionKind::SeekHelp,
}
}
/// 代入规则命名捕获组,产出 agent 工具可直接用的参数表。
#[must_use]
pub fn materialize(&self, groups: &BTreeMap<String, String>) -> MaterializedFixAction {
let kind = self.kind();
let mut args = BTreeMap::new();
match self {
Self::ChangeJava(p) => {
let derived = p
.class_file_version_group
.as_ref()
.and_then(|group| groups.get(group))
.and_then(|raw| raw.trim().parse::<u32>().ok())
.and_then(class_file_to_java_major);
put_opt(&mut args, "required_major", p.required_major.or(derived));
put_opt(&mut args, "max_major", p.max_major);
put_str(&mut args, "vendor", p.vendor.as_str());
put_opt_str(&mut args, "note", render_opt(&p.note, groups));
}
Self::AdjustMemory(p) => {
put_opt(&mut args, "suggested_xmx_mb", p.suggested_xmx_mb);
put_opt(&mut args, "min_xmx_mb", p.min_xmx_mb);
put_bool(&mut args, "need_64bit_java", p.need_64bit_java);
put_bool(&mut args, "increase_system_swap", p.increase_system_swap);
put_opt_str(&mut args, "note", render_opt(&p.note, groups));
}
Self::ChangeGraphics(p) => {
put_str(&mut args, "setting", p.setting.as_str());
put_bool(&mut args, "update_driver", p.update_driver);
put_opt_str(&mut args, "suggested_value", p.suggested_value.clone());
put_opt_str(&mut args, "note", render_opt(&p.note, groups));
}
Self::RemoveMod(p) => {
let ids = p
.mod_ids
.iter()
.map(|id| substitute(id, groups))
.collect::<Vec<_>>();
if !ids.is_empty() {
args.insert("mod_ids".to_owned(), ids.join(","));
}
put_str(&mut args, "action", p.action.as_str());
put_opt_str(&mut args, "note", render_opt(&p.note, groups));
}
Self::Reinstall(p) => {
put_str(&mut args, "component", p.component.as_str());
put_opt_str(&mut args, "path_hint", render_opt(&p.path_hint, groups));
put_opt_str(&mut args, "note", render_opt(&p.note, groups));
}
Self::SeekHelp(p) => {
put_bool(&mut args, "export_diagnostics", p.export_diagnostics);
if !p.needs_more_info.is_empty() {
args.insert("needs_more_info".to_owned(), p.needs_more_info.join("; "));
}
put_opt_str(&mut args, "note", render_opt(&p.note, groups));
}
}
MaterializedFixAction {
kind,
tool: kind.agent_tool(),
args,
}
}
}
/// 可选的模板字段:`Some` 时做 `{group}` 替换,`None` 保持 `None`。
fn render_opt(value: &Option<String>, groups: &BTreeMap<String, String>) -> Option<String> {
value.as_ref().map(|text| substitute(text, groups))
}
fn put_str(args: &mut BTreeMap<String, String>, key: &str, value: &str) {
args.insert(key.to_owned(), value.to_owned());
}
fn put_opt_str(args: &mut BTreeMap<String, String>, key: &str, value: Option<String>) {
if let Some(value) = value {
args.insert(key.to_owned(), value);
}
}
fn put_opt(args: &mut BTreeMap<String, String>, key: &str, value: Option<u32>) {
if let Some(value) = value {
args.insert(key.to_owned(), value.to_string());
}
}
fn put_bool(args: &mut BTreeMap<String, String>, key: &str, value: bool) {
args.insert(key.to_owned(), value.to_string());
}
/// `class file version 52` → `Java 8`(HMCL `getJavaVersionFromMajorVersion` 同式)。
#[must_use]
pub fn class_file_to_java_major(class_file_version: u32) -> Option<u32> {
class_file_version
.checked_sub(44)
.filter(|major| *major > 0)
}
+252
View File
@@ -0,0 +1,252 @@
//! 六维修复动作的参数类型(`fix_action.rs` 的载荷 + 几个小枚举)。
//!
//! 由 `rules.rs` 用 `#[path]` 声明并统一导出;文件拆分只为守 400 行上限。
use serde::Deserialize;
/// 建议的 JVM 实现。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JavaVendor {
/// 不限定(HotSpot 优先)。
#[default]
Any,
/// 必须 HotSpot(OpenJ9 等不兼容实现的反面)。
Hotspot,
}
impl JavaVendor {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Any => "any",
Self::Hotspot => "hotspot",
}
}
}
/// 图形侧要修的那一类设置。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GraphicsSetting {
/// 不指定具体项,只表达「图形相关」。
#[default]
Any,
/// 渲染器(OpenGL 核心/兼容、ANGLE、软渲染)。
Renderer,
/// 显卡驱动。
Driver,
/// 图形 API(MC 26.2+ 的「默认 / 优先 Vulkan / 优先 OpenGL」三选一)。
GraphicsApi,
/// 分辨率 / 资源包分辨率。
Resolution,
/// 覆盖层类软件(RTSS、Discord 等)。
Overlay,
/// 软件渲染兜底。
SoftwareRenderer,
}
impl GraphicsSetting {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Any => "any",
Self::Renderer => "renderer",
Self::Driver => "driver",
Self::GraphicsApi => "graphics_api",
Self::Resolution => "resolution",
Self::Overlay => "overlay",
Self::SoftwareRenderer => "software_renderer",
}
}
}
/// 对 mod 的处理方式。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModAction {
/// 移出 mods 目录。
#[default]
Remove,
/// 暂时禁用(挪到 `.disabled`)。
Disable,
/// 换一个版本 / 换替代品。
Replace,
/// 升级到兼容版本。
Upgrade,
}
impl ModAction {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Remove => "remove",
Self::Disable => "disable",
Self::Replace => "replace",
Self::Upgrade => "upgrade",
}
}
}
/// 要重装/修复的组件。粒度对齐 download/game 侧的组件概念,便于 agent 直接调用。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComponentKind {
/// Java 运行时本身。
Java,
/// 游戏主 jar。
GameJar,
/// 版本清单(version JSON)。
GameJson,
/// 未指定具体 loader。
Loader,
/// Fabric loader。
Fabric,
/// Forge。
Forge,
/// NeoForge。
#[serde(rename = "neoforge")]
NeoForge,
/// OptiFine。
#[serde(rename = "optifine")]
OptiFine,
/// natives 原生库。
Natives,
/// mods 目录(含「jar 被解压成目录」的修复)。
Mods,
/// libraries 库文件。
Libraries,
/// assets 资源。
Assets,
/// MixinBootstrap 等前置。
MixinBootstrap,
}
impl ComponentKind {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Java => "java",
Self::GameJar => "game_jar",
Self::GameJson => "game_json",
Self::Loader => "loader",
Self::Fabric => "fabric",
Self::Forge => "forge",
Self::NeoForge => "neoforge",
Self::OptiFine => "optifine",
Self::Natives => "natives",
Self::Mods => "mods",
Self::Libraries => "libraries",
Self::Assets => "assets",
Self::MixinBootstrap => "mixin_bootstrap",
}
}
}
/// 换 Java 的参数。
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ChangeJava {
/// 建议的大版本(17 / 21 …)。
#[serde(default)]
pub required_major: Option<u32>,
/// 允许的最高大版本(老 Forge 只能用 Java 8 时给 `8`)。
#[serde(default)]
pub max_major: Option<u32>,
/// 从该命名捕获组取 `class file version N`,按 `N - 44` 换算成 Java 大版本。
#[serde(default)]
pub class_file_version_group: Option<String>,
/// JVM 实现偏好。
#[serde(default)]
pub vendor: JavaVendor,
/// 给用户看的补充说明。
#[serde(default)]
pub note: Option<String>,
}
/// 调内存的参数。
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AdjustMemory {
/// 建议的 `-Xmx`(MB);规则不确定时留空,由 UI 按物理内存算。
#[serde(default)]
pub suggested_xmx_mb: Option<u32>,
/// `-Xmx` 的下限(MB)。
#[serde(default)]
pub min_xmx_mb: Option<u32>,
/// 需要 32 位 → 64 位 Java(「Could not reserve enough space」类)。
#[serde(default)]
pub need_64bit_java: bool,
/// 需要用户扩大系统虚拟内存 / 关掉占内存的程序。
#[serde(default)]
pub increase_system_swap: bool,
/// 补充说明。
#[serde(default)]
pub note: Option<String>,
}
/// 改图形设置的参数。
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ChangeGraphics {
/// 要改的设置类别。
#[serde(default)]
pub setting: GraphicsSetting,
/// 建议更新显卡驱动。
#[serde(default)]
pub update_driver: bool,
/// 建议的目标值(如 `854x480`、`OpenGL 兼容模式`)。
#[serde(default)]
pub suggested_value: Option<String>,
/// 补充说明。
#[serde(default)]
pub note: Option<String>,
}
/// 删/禁/换 mod 的参数。
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoveMod {
/// modid 列表,允许 `{group}` 模板(由规则命名捕获组代入)。
#[serde(default)]
pub mod_ids: Vec<String>,
/// 处理方式。
#[serde(default)]
pub action: ModAction,
/// 补充说明。
#[serde(default)]
pub note: Option<String>,
}
/// 重装/修组件的参数。
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Reinstall {
/// 要修的组件。
pub component: ComponentKind,
/// 出问题的路径提示(来自规则捕获组或原文)。
#[serde(default)]
pub path_hint: Option<String>,
/// 补充说明。
#[serde(default)]
pub note: Option<String>,
}
/// 求助/上报的参数。
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SeekHelp {
/// 建议导出诊断包(日志 + 崩溃报告 + 清单 + 环境)。
#[serde(default)]
pub export_diagnostics: bool,
/// 还需要用户补充什么(「把日志上限调到 20000 行再复现」)。
#[serde(default)]
pub needs_more_info: Vec<String>,
/// 补充说明。
#[serde(default)]
pub note: Option<String>,
}
+298
View File
@@ -0,0 +1,298 @@
//! `hs_err_pid*.log`(JVM 致命错误日志)解析器。
//!
//! HMCL 完全不解析它(analyse/02 §7.10-8),而 `EXCEPTION_ACCESS_VIOLATION` 这类问题
//! 只有这份文件能给出权威证据(失败帧落在显卡驱动 DLL 里就是驱动问题)。
//! 格式稳定:**标题行 + 线程段 + 内存段**,本模块只做容错抽取,字段全部 `Option`,永不失败。
//!
//! 用法(launch 对接见 `CONTRACT.md` §5):
//! - `parse(&text)`:已有全文时用(首选,避免二次截断);
//! - `parse_file(&path)`:给路径时用;
//! - [`HsErrReport::digest`] 产出的摘要既是规则匹配的文本,也是给它人看的证据。
use std::fs;
use std::io;
use std::path::Path;
#[path = "hs_err_report.rs"]
mod report;
pub use report::{FailedFrame, FrameKind, HsErrKind, HsErrReport, MemorySummary};
/// 解析文本(永不失败;无法识别的行直接跳过)。
#[must_use]
pub fn parse(text: &str) -> HsErrReport {
let mut report = HsErrReport::default();
let lines = text.lines().collect::<Vec<_>>();
for (index, raw) in lines.iter().enumerate() {
let line = raw.trim_end_matches('\r');
let body = line.trim_start_matches('#').trim();
if report.header.is_none() && is_header(body) {
report.header = Some(body.to_owned());
}
if report.exception.is_none() {
report.exception = extract_exception(body);
}
if report.siginfo.is_none() && body.starts_with("siginfo:") {
report.siginfo = Some(body.to_owned());
}
if report.signal.is_none() {
report.signal = body
.contains("si_signo:")
.then(|| body.to_owned())
.or_else(|| extract_signal(body));
}
if report.jre_version.is_none() && body.contains("JRE version:") {
report.jre_version = Some(body.to_owned());
}
if report.java_vm.is_none() && body.contains("Java VM:") {
report.java_vm = Some(body.to_owned());
}
if report.os.is_none() && body.starts_with("OS:") {
report.os = Some(body.to_owned());
}
if report.command_line.is_none() && body.starts_with("java_command:") {
report.command_line = Some(body.to_owned());
}
if report.current_thread.is_none() && body.starts_with("Current thread (") {
report.current_thread = Some(body.to_owned());
}
if report.memory.is_none() && body.starts_with("Memory:") {
report.memory = parse_memory(body);
}
if report.failed_frame.is_none() && body.eq_ignore_ascii_case("Problematic frame:") {
report.failed_frame = lines
.iter()
.skip(index + 1)
.map(|next| next.trim_end_matches('\r'))
.find(|next| !next.trim().is_empty() && next.trim() != "#")
.map(|next| parse_frame(next.trim_start_matches('#').trim()));
}
if report.pc.is_none() && body.contains("pc=") {
report.pc = field(body, "pc=");
}
if report.pid.is_none() && body.contains("pid=") {
report.pid = field(body, "pid=").and_then(|raw| raw.parse::<u32>().ok());
}
if report.tid.is_none() && body.contains("tid=") {
report.tid = field(body, "tid=");
}
if body.eq_ignore_ascii_case("VM Arguments:") {
// 真实 hs_err 里 jvm_args/java_command 是顶格写的,不缩进
report.vm_arguments = collect_unindented(&lines, index + 1, 32);
}
if body.starts_with("Heap:") {
// "Heap:" 之后的缩进行才是内容(garbage-first heap total … used …)
report.heap = collect_block(&lines, index + 1, 6);
}
}
report.header_lines = collect_header_block(&lines);
report.pc = report
.exception
.as_deref()
.and_then(|line| field(line, "pc="))
.or(report.pc);
// pid/tid 在「异常行」和「Out of Memory Error (os_*.cpp:NNN), pid=…」两种标题里都出现,
// 所以两种形态都认,先到先得。
if report.pid.is_none() {
report.pid = report
.exception
.as_deref()
.and_then(|line| field(line, "pid="))
.and_then(|raw| raw.parse::<u32>().ok());
}
if report.tid.is_none() {
report.tid = report
.exception
.as_deref()
.and_then(|line| field(line, "tid="));
}
report.kind = classify(&report);
report
}
/// 读文件并解析(非 UTF-8 字节按 U+FFFD 替换,不报错)。
pub fn parse_file(path: impl AsRef<Path>) -> io::Result<HsErrReport> {
let path = path.as_ref();
let bytes = fs::read(path)?;
let text = String::from_utf8_lossy(&bytes);
let mut report = parse(&text);
report.path = Some(path.to_path_buf());
Ok(report)
}
fn is_header(body: &str) -> bool {
const MARKERS: [&str; 5] = [
"insufficient memory",
"A fatal error has been detected",
"Out of Memory Error",
"Internal Error",
"SIGSEGV",
];
MARKERS.iter().any(|marker| body.contains(marker))
}
fn extract_exception(body: &str) -> Option<String> {
let marker = "EXCEPTION_";
let start = body.find(marker)?;
let rest = &body[start..];
let end = rest
.find(" at pc=")
.or_else(|| rest.find(", pid="))
.unwrap_or(rest.len());
Some(rest[..end].trim().to_owned())
}
fn extract_signal(body: &str) -> Option<String> {
let start = body.find("SIG")?;
let rest = &body[start..];
let end = rest.find(')').map_or(rest.len(), |index| index + 1);
Some(rest[..end].trim().to_owned())
}
/// 从 `key=value` 形态里取值(到下一个空格/逗号为止)。
fn field(line: &str, key: &str) -> Option<String> {
let start = line.find(key)? + key.len();
let rest = &line[start..];
let end = rest.find([' ', ',', ']']).unwrap_or(rest.len());
let value = rest[..end].trim();
(!value.is_empty()).then(|| value.to_owned())
}
fn parse_frame(raw: &str) -> FailedFrame {
let kind = match raw.chars().next() {
Some('C') => FrameKind::Native,
Some('J' | 'j') => FrameKind::Java,
Some('V') => FrameKind::VmStub,
_ => FrameKind::Unknown,
};
let (module, offset) = raw
.find('[')
.and_then(|start| {
let inner = &raw[start + 1..];
let inner = &inner[..inner.find(']')?];
let mut parts = inner.split('+');
let module = parts.next().map(str::trim).filter(|m| !m.is_empty())?;
Some((
Some(module.to_owned()),
parts.next().map(|o| o.trim().to_owned()),
))
})
.unwrap_or((None, None));
FailedFrame {
raw: raw.to_owned(),
kind,
module,
offset,
}
}
fn parse_memory(body: &str) -> Option<MemorySummary> {
let mut summary = MemorySummary {
raw: body.to_owned(),
..MemorySummary::default()
};
for part in body.split(',') {
let part = part.trim();
if part.contains("page") {
summary.page_size = part
.trim_start_matches("Memory:")
.split_whitespace()
.next()
.map(|value| value.trim_end_matches(',').to_owned());
} else if part.starts_with("physical") {
let (total, free) = parse_kb_pair(part);
summary.physical_kb = total;
summary.physical_free_kb = free;
} else if part.starts_with("swap") {
let (total, free) = parse_kb_pair(part);
summary.swap_kb = total;
summary.swap_free_kb = free;
}
}
Some(summary)
}
/// `physical 16741480k(4716856k free)` → (16741480, 4716856)。
fn parse_kb_pair(part: &str) -> (Option<u64>, Option<u64>) {
let numbers = part
.split(|ch: char| !ch.is_ascii_digit())
.filter(|token| !token.is_empty())
.filter_map(|token| token.parse::<u64>().ok())
.collect::<Vec<_>>();
(numbers.first().copied(), numbers.get(1).copied())
}
/// 取从 `start` 开始、到第一个空行为止的连续行(VM Arguments / java_command 段)。
fn collect_unindented(lines: &[&str], start: usize, limit: usize) -> Vec<String> {
lines
.iter()
.skip(start)
.map(|line| line.trim_end_matches('\r'))
.take_while(|line| !line.trim().is_empty())
.take(limit)
.map(|line| line.trim().to_owned())
.collect()
}
/// 取从 `start` 开始、以空白/制表缩进的连续行(最多 `limit` 行)。
fn collect_block(lines: &[&str], start: usize, limit: usize) -> Vec<String> {
lines
.iter()
.skip(start)
.map(|line| line.trim_end_matches('\r'))
.skip_while(|line| line.trim().is_empty())
.take_while(|line| line.starts_with([' ', '\t']))
.take(limit)
.map(|line| line.trim().to_owned())
.collect()
}
/// 文件开头的 `#` 注释块(真实 hs_err 的标题/异常/失败帧都在这里)。
fn collect_header_block(lines: &[&str]) -> Vec<String> {
let mut out = Vec::new();
for line in lines {
let line = line.trim_end_matches('\r');
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
if trimmed.starts_with('#') {
out.push(trimmed.to_owned());
}
} else {
break;
}
if out.len() >= 64 {
break;
}
}
out
}
fn classify(report: &HsErrReport) -> HsErrKind {
let haystack = [
report.header.as_deref(),
report.exception.as_deref(),
report.siginfo.as_deref(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join("\n");
if haystack.contains("EXCEPTION_ACCESS_VIOLATION") || haystack.contains("SIGSEGV") {
HsErrKind::AccessViolation
} else if haystack.contains("insufficient memory")
|| haystack.contains("Out of Memory")
|| haystack.contains("OutOfMemory")
{
HsErrKind::OutOfMemory
} else if haystack.contains("StackOverflow") || haystack.contains("stack overflow") {
HsErrKind::StackOverflow
} else if haystack.contains("Internal Error")
|| haystack.contains("A fatal error has been detected")
{
HsErrKind::InternalError
} else {
HsErrKind::Other
}
}
+210
View File
@@ -0,0 +1,210 @@
//! `hs_err_pid*.log` 解析结果的数据结构(标题行 / 线程段 / 内存段 / 失败帧)。
//!
//! 由 `hs_err.rs` 用 `#[path]` 声明并重新导出;拆分只为守单文件 400 行上限。
use std::fmt;
use std::path::PathBuf;
/// hs_err 的崩溃类别(标题行/异常名归并成少数几类,供内置结论判定)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HsErrKind {
/// `EXCEPTION_ACCESS_VIOLATION` / `SIGSEGV`:野指针,显卡驱动/原生库最常见。
AccessViolation,
/// 内存不足(`insufficient memory` / `Out of Memory Error`)。
OutOfMemory,
/// 栈溢出。
StackOverflow,
/// JVM 内部致命错误(`Internal Error` / `A fatal error has been detected`)。
InternalError,
/// 其它 / 无法归类。
#[default]
Other,
}
impl HsErrKind {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::AccessViolation => "access_violation",
Self::OutOfMemory => "out_of_memory",
Self::StackOverflow => "stack_overflow",
Self::InternalError => "internal_error",
Self::Other => "other",
}
}
}
impl fmt::Display for HsErrKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 失败帧的类型(`# C` 原生码、`# J`/`# j` Java 帧、`# V` VM 生成桩帧)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FrameKind {
/// 原生(C/C++)帧:出现在显卡驱动/OpenGL 里基本可判定驱动问题。
Native,
/// Java 帧(`# J` / `# j`,二者区别是解释执行还是编译执行,诊断不需要再细分)。
Java,
/// VM 生成桩帧(`# V`):JIT/解释器生成的运行时代码,不是用户代码。
VmStub,
/// 未识别。
#[default]
Unknown,
}
impl FrameKind {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Native => "native",
Self::Java => "java",
Self::VmStub => "vm_stub",
Self::Unknown => "unknown",
}
}
}
/// `# Problematic frame:` 之后的那一行。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FailedFrame {
/// 原始文本(`C [ig8d64.dll+0x9b3c4d]`)。
pub raw: String,
/// 帧类型。
pub kind: FrameKind,
/// 命中的模块/DLL(`ig8d64.dll`)。
pub module: Option<String>,
/// 模块内偏移(`0x9b3c4d`)。
pub offset: Option<String>,
}
/// 内存段摘要(`Memory: 4k page, physical …, swap …`)。
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct MemorySummary {
/// 页大小(`4k`)。
pub page_size: Option<String>,
/// 物理内存 kB。
pub physical_kb: Option<u64>,
/// 物理可用 kB。
pub physical_free_kb: Option<u64>,
/// 交换区 kB。
pub swap_kb: Option<u64>,
/// 交换区可用 kB。
pub swap_free_kb: Option<u64>,
/// 原始行。
pub raw: String,
}
impl MemorySummary {
/// 一行人类可读摘要(GB 保留一位小数)。
#[must_use]
pub fn summary(&self) -> String {
let gb = |kb: Option<u64>| {
kb.map(|kb| format!("{:.1} GB", kb as f64 / 1024.0 / 1024.0))
.unwrap_or_else(|| "?".to_owned())
};
format!(
"物理内存 {}(可用 {}),交换区 {}(可用 {})",
gb(self.physical_kb),
gb(self.physical_free_kb),
gb(self.swap_kb),
gb(self.swap_free_kb)
)
}
}
/// 解析结果。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HsErrReport {
/// 来源文件(`parse` 时为 `None`)。
pub path: Option<PathBuf>,
/// 标题行(`#[derive(Default)]` 之外的语义见 `parse`)。
pub header: Option<String>,
/// 崩溃类别。
pub kind: HsErrKind,
/// 异常名(`EXCEPTION_ACCESS_VIOLATION (0xc0000005)`)。
pub exception: Option<String>,
/// `siginfo:` 行(Unix 侧带 si_signo / si_addr)。
pub siginfo: Option<String>,
/// 信号名(`SIGSEGV (0xb)`)。
pub signal: Option<String>,
/// 出错时的指令指针。
pub pc: Option<String>,
/// 进程 pid。
pub pid: Option<u32>,
/// 线程 tid(保留原始十六进制/十进制串)。
pub tid: Option<String>,
/// 失败帧。
pub failed_frame: Option<FailedFrame>,
/// 当前线程(`JavaThread "Render thread" [_thread_in_native, id=…]`)。
pub current_thread: Option<String>,
/// JRE 版本行内容。
pub jre_version: Option<String>,
/// JVM 实现行内容。
pub java_vm: Option<String>,
/// 操作系统行内容。
pub os: Option<String>,
/// `java_command` 行。
pub command_line: Option<String>,
/// `VM Arguments:` 段的参数行。
pub vm_arguments: Vec<String>,
/// 内存段摘要。
pub memory: Option<MemorySummary>,
/// `Heap:` 段的前几行(原样,含 total/used)。
pub heap: Vec<String>,
/// 标题块原文(逐行保留 `#` 前缀,规则可以直接在上面匹配)。
pub header_lines: Vec<String>,
}
impl HsErrReport {
/// 供规则匹配 / 证据展示的摘要文本。
///
/// 保留标题块原文(`EXCEPTION_ACCESS_VIOLATION` 与 `# C [ig…dll]` 的相对位置不变,
/// 因此 `graphics.graphics_driver` 这类跨行规则可以直接命中),再补上线程/内存段要点。
#[must_use]
pub fn digest(&self) -> String {
let mut out = String::new();
for line in &self.header_lines {
out.push_str(line);
out.push('\n');
}
for line in [
self.siginfo.as_deref(),
self.current_thread.as_deref(),
self.jre_version.as_deref(),
self.java_vm.as_deref(),
self.os.as_deref(),
self.command_line.as_deref(),
]
.into_iter()
.flatten()
{
out.push_str(line);
out.push('\n');
}
for line in &self.heap {
out.push_str(line);
out.push('\n');
}
if let Some(memory) = &self.memory {
out.push_str(&memory.raw);
out.push('\n');
}
out
}
/// 一句话摘要(诊断输出 / UI 头部)。
#[must_use]
pub fn summary(&self) -> String {
let kind = self.kind.as_str();
let frame = self
.failed_frame
.as_ref()
.map_or_else(String::new, |frame| format!(",失败帧 {}", frame.raw));
let header = self.header.as_deref().unwrap_or("JVM 致命错误");
format!("{header}({kind}{frame})")
}
}
+398
View File
@@ -0,0 +1,398 @@
//! 规则库加载:YAML 分组文件 + 内嵌兜底 + 热重载。
//!
//! 对应 PlanDoc/06 §2.2「规则文件 YAML 外置可热更」(修 HMCL 编译期枚举不可热更的债):
//! 1. 内嵌一份(`include_str!`):任何情况下都能诊断,不依赖安装目录完整;
//! 2. 用户目录 `data/diagnose/rules.d/*.yaml` 覆盖内嵌的同名文件(缺哪个用内嵌补哪个);
//! 3. 热重载是 **mtime+size 指纹轮询**([`RuleLoader::refresh`]),不引 `notify` 依赖;
//! 解析失败时**保留上一份好规则**并返回错误列表——用户手改错 YAML 不会让诊断能力归零。
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::fmt;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use serde::Deserialize;
use super::rules::{CompiledRule, Rule, RuleCategory};
/// 规则目录(相对数据根,由 platform 模块解析)。
pub const RULES_DIR: &str = "diagnose/rules.d";
/// 规则 schema 版本(文件里 `version: 1`)。
pub const SCHEMA_VERSION: u32 = 1;
/// 内嵌规则库:`(文件名, 内容)`。按分类拆文件,每文件守 400 行上限。
pub const EMBEDDED_RULES: &[(&str, &str)] = &[
("jvm.yaml", include_str!("rules.d/jvm.yaml")),
("memory.yaml", include_str!("rules.d/memory.yaml")),
("graphics.yaml", include_str!("rules.d/graphics.yaml")),
("integrity.yaml", include_str!("rules.d/integrity.yaml")),
("fabric.yaml", include_str!("rules.d/fabric.yaml")),
("forge.yaml", include_str!("rules.d/forge.yaml")),
("mod_runtime.yaml", include_str!("rules.d/mod_runtime.yaml")),
(
"mod_conflict.yaml",
include_str!("rules.d/mod_conflict.yaml"),
),
("world.yaml", include_str!("rules.d/world.yaml")),
("other.yaml", include_str!("rules.d/other.yaml")),
];
/// 一个 YAML 文件的 schema。
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RuleFile {
#[serde(default)]
version: u32,
category: RuleCategory,
rules: Vec<Rule>,
}
/// 编译好的规则集(字段私有,只能经 loader 构造,保证「id 唯一 + 正则可编译」)。
#[derive(Debug, Clone, Default)]
pub struct RuleSet {
rules: Vec<CompiledRule>,
}
impl RuleSet {
/// 规则表。
#[must_use]
pub fn rules(&self) -> &[CompiledRule] {
&self.rules
}
/// 规则条数。
#[must_use]
pub fn len(&self) -> usize {
self.rules.len()
}
/// 是否空规则集。
#[must_use]
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
/// 按 id 查规则(测试 / 调试用)。
#[must_use]
pub fn get(&self, id: &str) -> Option<&CompiledRule> {
self.rules.iter().find(|rule| rule.id() == id)
}
/// 从 `(文件名, 内容)` 列表编译;有任何错误就整体失败(调用方决定是否回退)。
pub fn from_sources(sources: &[(String, String)]) -> Result<Self, Vec<LoadError>> {
let mut errors = Vec::new();
let mut rules: Vec<CompiledRule> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for (file, text) in sources {
let parsed = match serde_yaml_ng::from_str::<RuleFile>(text) {
Ok(parsed) => parsed,
Err(error) => {
errors.push(LoadError::yaml(file, &error));
continue;
}
};
if parsed.version > SCHEMA_VERSION {
errors.push(LoadError {
file: file.clone(),
kind: LoadErrorKind::UnsupportedVersion,
message: format!(
"规则文件 version={} 高于本程序支持的 {}",
parsed.version, SCHEMA_VERSION
),
});
continue;
}
for rule in parsed.rules {
if !seen.insert(rule.id.clone()) {
errors.push(LoadError {
file: file.clone(),
kind: LoadErrorKind::DuplicateId,
message: format!("规则 id 重复:{}", rule.id),
});
continue;
}
match CompiledRule::compile(rule, parsed.category) {
Ok(compiled) => rules.push(compiled),
Err(error) => errors.push(LoadError {
file: file.clone(),
kind: LoadErrorKind::Regex,
message: error.to_string(),
}),
}
}
}
if errors.is_empty() {
Ok(Self { rules })
} else {
Err(errors)
}
}
}
/// 加载错误分类。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadErrorKind {
/// 读盘失败。
Io,
/// YAML 语法/schema 错误(`serde_yaml_ng` 的报错带行号)。
Yaml,
/// 正则编译失败。
Regex,
/// 规则 id 跨文件重复。
DuplicateId,
/// 规则文件版本高于本程序。
UnsupportedVersion,
}
impl LoadErrorKind {
/// 稳定字符串。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Io => "io",
Self::Yaml => "yaml",
Self::Regex => "regex",
Self::DuplicateId => "duplicate_id",
Self::UnsupportedVersion => "unsupported_version",
}
}
}
/// 一条加载错误(带文件名,用户改 YAML 时能定位)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadError {
/// 文件名(磁盘目录加载时是完整路径)。
pub file: String,
/// 分类。
pub kind: LoadErrorKind,
/// 报错原文(YAML 错误含行号)。
pub message: String,
}
impl LoadError {
fn yaml(file: &str, error: &serde_yaml_ng::Error) -> Self {
Self {
file: file.to_owned(),
kind: LoadErrorKind::Yaml,
message: error.to_string(),
}
}
fn io(file: &Path, error: &std::io::Error) -> Self {
Self {
file: file.display().to_string(),
kind: LoadErrorKind::Io,
message: error.to_string(),
}
}
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {}:{}",
self.kind.as_str(),
self.file,
self.message
)
}
}
impl std::error::Error for LoadError {}
/// 热重载结果。
#[derive(Debug, Clone, PartialEq)]
pub enum ReloadOutcome {
/// 指纹没变,规则未动。
Unchanged,
/// 重新加载成功。
Reloaded {
/// 新规则条数。
rules: usize,
},
/// 新文件有错:**保留旧规则**,错误交给调用方展示。
Rejected {
/// 错误列表。
errors: Vec<LoadError>,
},
}
/// 规则库加载器(持有当前生效的规则集 + 目录指纹)。
#[derive(Debug, Clone)]
pub struct RuleLoader {
dir: Option<PathBuf>,
set: RuleSet,
fingerprint: u64,
last_errors: Vec<LoadError>,
}
impl Default for RuleLoader {
fn default() -> Self {
Self::embedded()
}
}
impl RuleLoader {
/// 只用内嵌规则(无磁盘目录)。
#[must_use]
pub fn embedded() -> Self {
let sources = embedded_sources();
let (set, errors) = compile_sources(&sources);
Self {
dir: None,
set,
fingerprint: 0,
last_errors: errors,
}
}
/// 以磁盘目录为准、内嵌补齐缺文件。
///
/// 磁盘规则有任何错误时**退回内嵌规则**并记录错误(保证永远有可用规则集)。
#[must_use]
pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
let dir = dir.into();
let (sources, mut errors) = merged_sources(Some(&dir));
let set = match RuleSet::from_sources(&sources) {
Ok(set) => set,
Err(mut load_errors) => {
errors.append(&mut load_errors);
RuleSet::from_sources(&embedded_sources()).unwrap_or_default()
}
};
Self {
fingerprint: dir_fingerprint(&dir),
dir: Some(dir),
set,
last_errors: errors,
}
}
/// 当前生效的规则。
#[must_use]
pub fn rules(&self) -> &[CompiledRule] {
self.set.rules()
}
/// 当前生效的规则集。
#[must_use]
pub fn rule_set(&self) -> &RuleSet {
&self.set
}
/// 上次加载留下的错误(UI 可提示「你的规则文件有错,已用内置规则」)。
#[must_use]
pub fn last_errors(&self) -> &[LoadError] {
&self.last_errors
}
/// 规则目录(内嵌模式为 `None`)。
#[must_use]
pub fn dir(&self) -> Option<&Path> {
self.dir.as_deref()
}
/// 目录指纹(测试用:确认 mtime/size 变化真的改变了指纹)。
#[must_use]
pub fn fingerprint(&self) -> u64 {
self.fingerprint
}
/// 轮询热重载:指纹变了才重新解析。
pub fn refresh(&mut self) -> ReloadOutcome {
let Some(dir) = self.dir.clone() else {
return ReloadOutcome::Unchanged;
};
let fingerprint = dir_fingerprint(&dir);
if fingerprint == self.fingerprint {
return ReloadOutcome::Unchanged;
}
self.fingerprint = fingerprint;
let (sources, mut errors) = merged_sources(Some(&dir));
match RuleSet::from_sources(&sources) {
Ok(set) => {
self.set = set;
self.last_errors = errors;
ReloadOutcome::Reloaded {
rules: self.set.len(),
}
}
Err(mut load_errors) => {
errors.append(&mut load_errors);
self.last_errors = errors.clone();
ReloadOutcome::Rejected { errors }
}
}
}
}
fn embedded_sources() -> Vec<(String, String)> {
EMBEDDED_RULES
.iter()
.map(|(name, text)| ((*name).to_owned(), (*text).to_owned()))
.collect()
}
/// 磁盘文件覆盖内嵌:磁盘有的用磁盘,没的用内嵌。
fn merged_sources(dir: Option<&Path>) -> (Vec<(String, String)>, Vec<LoadError>) {
let mut sources = Vec::new();
let mut errors = Vec::new();
for (name, embedded) in EMBEDDED_RULES {
let path = dir.map(|dir| dir.join(name));
match path {
Some(path) if path.is_file() => match fs::read_to_string(&path) {
Ok(text) => sources.push((path.display().to_string(), text)),
Err(error) => {
errors.push(LoadError::io(&path, &error));
sources.push(((*name).to_owned(), (*embedded).to_owned()));
}
},
_ => sources.push(((*name).to_owned(), (*embedded).to_owned())),
}
}
(sources, errors)
}
fn compile_sources(sources: &[(String, String)]) -> (RuleSet, Vec<LoadError>) {
match RuleSet::from_sources(sources) {
Ok(set) => (set, Vec::new()),
Err(errors) => (RuleSet::default(), errors),
}
}
/// 目录指纹:所有 `*.yaml` 的(文件名, 长度, mtime)之和。
///
/// 长度 + mtime 足以发现「保存了但秒级 mtime 未变」之外的一切编辑;漏检时用户点刷新即可。
fn dir_fingerprint(dir: &Path) -> u64 {
let mut entries = fs::read_dir(dir)
.map(|entries| {
entries
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("yaml"))
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
entries.sort();
let mut hasher = DefaultHasher::new();
for path in entries {
path.file_name().unwrap_or_default().hash(&mut hasher);
if let Ok(metadata) = fs::metadata(&path) {
metadata.len().hash(&mut hasher);
metadata
.modified()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.map(|duration| duration.as_nanos())
.hash(&mut hasher);
}
}
hasher.finish()
}
+17
View File
@@ -0,0 +1,17 @@
//! 崩溃诊断:五类信号采集(含 hs_err_pid 解析)、YAML 规则引擎(59 条规则起步,可热更新)、
//! exit0 静默失败检测、结构化诊断结论。
//!
//! 设计定稿:`DevDocs/PlanDoc/06-launch-and-diagnostics.md` §2(采集 / 规则 / 流水线)。
//! 定位:规则引擎为主、AI 补位;AI 拿到的应是「规则结论 + 相关片段」而非裸日志
//! (`DevDocs/PlanDoc/02-ai-agent-design.md` §3.2)。
//!
//! 规划中的公共面(M15):信号采集、规则匹配、诊断报告、规则热加载。
// 只有这五个顶层模块;子模块(collectors 下的 signals/sources/environment、engine 下的
// finding/synthetic、hs_err 下的 hs_err_report、rules 下的 fix_action/fix_params)由各自的
// 父文件用 #[path] 声明。这里**不要**重复声明,否则同一文件会被编译成两份不同类型。
pub mod collectors;
pub mod engine;
pub mod hs_err;
pub mod loader;
pub mod rules;
@@ -0,0 +1,141 @@
# 规则库 · Fabric mod 解析(8 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的 Fabric 组(analyse/02 §7.3 第 5 组)。
# Fabric 的 ModResolutionException 家族特征:异常后面跟一段以 " - " 开头的列表,
# 因此这些规则用 file scope(跨行)。
version: 1
category: fabric
rules:
- id: fabric.mod_resolution
severity: critical
scope: file
pattern: 'ModResolutionException: (?<reason>(.*)[\n\r]*( - (.*)[\n\r]*)+)'
confidence: 0.75
priority: 70
cause:
key: diagnose.cause.fabric.mod_resolution
summary: 'Fabric 解析 mod 依赖失败(缺依赖/版本冲突/模组不配套)'
detail: '按下方列出的 mod 名单逐个处理:缺依赖就补装,冲突就删一个'
fix_action:
- kind: remove_mod
action: disable
note: '按 Fabric 报的 mod 名单逐个禁用排查'
sample: samples/fabric/mod_resolution.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_RESOLUTION'
- id: fabric.mod_resolution_conflict
severity: critical
pattern: 'ModResolutionException: Found conflicting mods: (?<sourcemod>.*) conflicts with (?<destmod>.*)'
confidence: 0.92
priority: 86
suppress:
- integrity.no_class_def_found
- integrity.no_such_method
cause:
key: diagnose.cause.fabric.mod_resolution_conflict
summary: 'Fabric 报告 mod 冲突:{sourcemod} 与 {destmod} 不能共存'
detail: '两个只能留一个(或都升级到互相兼容的版本)'
fix_action:
- kind: remove_mod
mod_ids:
- '{sourcemod}'
- '{destmod}'
action: remove
sample: samples/fabric/mod_resolution_conflict.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_RESOLUTION_CONFLICT'
- id: fabric.mod_resolution_missing
severity: critical
pattern: 'ModResolutionException: Could not find required mod: (?<sourcemod>.*) requires (?<destmod>.*)'
confidence: 0.9
priority: 85
cause:
key: diagnose.cause.fabric.mod_resolution_missing
summary: '缺少前置 mod:{sourcemod} 需要 {destmod}'
detail: '装上 {destmod} 的对应版本;装不上就删掉 {sourcemod}'
fix_action:
- kind: remove_mod
mod_ids:
- '{sourcemod}'
action: replace
note: '补装前置 {destmod},或换掉 {sourcemod}'
sample: samples/fabric/mod_resolution_missing.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_RESOLUTION_MISSING'
- id: fabric.mod_resolution_missing_minecraft
severity: critical
pattern: 'ModResolutionException: Could not find required mod: (?<mod>.*) requires \{minecraft @ (?<version>.*)\}'
confidence: 0.9
priority: 85
cause:
key: diagnose.cause.fabric.mod_resolution_missing_minecraft
summary: '{mod} 要求 Minecraft {version},与当前实例版本不一致'
fix_action:
- kind: remove_mod
mod_ids:
- '{mod}'
action: replace
note: '换该 mod 的 {version} 版本,或改实例版本'
sample: samples/fabric/mod_resolution_missing_minecraft.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_RESOLUTION_MISSING_MINECRAFT'
- id: fabric.mod_resolution_collection
severity: critical
pattern: 'ModResolutionException: Could not resolve valid mod collection \(at: (?<sourcemod>.*) requires (?<destmod>.*)\)'
confidence: 0.88
priority: 84
cause:
key: diagnose.cause.fabric.mod_resolution_collection
summary: 'mod 集合无法解析:{sourcemod} 与 {destmod} 的版本组合不成立'
fix_action:
- kind: remove_mod
mod_ids:
- '{sourcemod}'
action: upgrade
sample: samples/fabric/mod_resolution_collection.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_RESOLUTION_COLLECTION'
- id: fabric.mod_resolution_details
severity: warning
pattern: '(\tMod File:|-- MOD |\tFailure message:)'
confidence: 0.5
priority: 45
cause:
key: diagnose.cause.fabric.mod_resolution_details
summary: 'Fabric 打印了 mod 解析明细(Mod File / Failure message):具体结论见上方规则'
detail: '这条只表示「日志里有解析失败明细」,用于把证据片段指给 AI/用户'
fix_action:
- kind: seek_help
export_diagnostics: true
sample: samples/fabric/mod_resolution_details.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_RESOLUTION0'
- id: fabric.loader_0_12
severity: critical
pattern: 'java\.lang\.NoClassDefFoundError: org/spongepowered/asm/mixin/transformer/FabricMixinTransformerProxy'
confidence: 0.85
priority: 80
cause:
key: diagnose.cause.fabric.loader_0_12
summary: 'Fabric Loader 0.12 与旧 mixin 类不兼容(loader 删掉了 deprecated 类)'
fix_action:
- kind: reinstall
component: fabric
note: '换匹配的 Fabric Loader 版本'
sample: samples/fabric/loader_0_12.log
source: 'HMCL CrashReportAnalyzer.Rule.FABRIC_VERSION_0_12'
- id: fabric.warnings
severity: warning
scope: file
pattern: '(Warnings were found!|Incompatible mod set!|Incompatible mods found!)(.*?)[\n\r]+(?<reason>[^\[]+)\['
confidence: 0.7
priority: 62
cause:
key: diagnose.cause.fabric.warnings
summary: 'Fabric 报告 mod 集合不兼容:{reason}'
fix_action:
- kind: remove_mod
action: disable
note: '按警告列表逐个禁用'
sample: samples/fabric/warnings.log
source: 'HMCL CrashReportAnalyzer.Rule.FABRIC_WARNINGS'
+136
View File
@@ -0,0 +1,136 @@
# 规则库 · Forge mod 解析(8 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的 Forge 组(analyse/02 §7.3 第 6 组)。
version: 1
category: forge
rules:
- id: forge.missing_dependencies
severity: critical
scope: file
pattern: 'Missing or unsupported mandatory dependencies:(?<reason>(.*)[\n\r]*(\t(.*)[\n\r]*)+)'
confidence: 0.85
priority: 80
cause:
key: diagnose.cause.forge.missing_dependencies
summary: 'Forge 报「缺少或不支持强制依赖」:有 mod 的前置没装或版本不对'
detail: '按报错里的 mod 名补齐前置;装不上就删掉依赖它的 mod'
fix_action:
- kind: remove_mod
action: disable
note: '按报错逐条补齐/禁用'
sample: samples/forge/missing_dependencies.log
source: 'HMCL CrashReportAnalyzer.Rule.FORGEMOD_RESOLUTION'
- id: forge.duplicate_mods
severity: critical
scope: file
pattern: 'Found duplicate mods:(?<reason>(.*)(?:\r\n|\n|\r)*(\t(.*)(?:\r\n|\n|\r)*)+)'
confidence: 0.9
priority: 86
cause:
key: diagnose.cause.forge.duplicate_mods
summary: '同一个 mod 装了两份(mods 目录里重复的 jar)'
detail: '删掉重复的那一份,只留一个版本'
fix_action:
- kind: remove_mod
action: remove
note: '保留一个版本'
sample: samples/forge/duplicate_mods.log
source: 'HMCL CrashReportAnalyzer.Rule.FORGE_FOUND_DUPLICATE_MODS'
- id: forge.duplicated_mod
severity: critical
pattern: 'Found a duplicate mod (?<name>.*) at (?<path>.*)'
confidence: 0.92
priority: 88
cause:
key: diagnose.cause.forge.duplicated_mod
summary: 'mod 重复:{name} 同时出现在 {path}'
fix_action:
- kind: remove_mod
mod_ids:
- '{name}'
action: remove
note: '删除路径 {path} 那一份'
sample: samples/forge/duplicated_mod.log
source: 'HMCL CrashReportAnalyzer.Rule.DUPLICATED_MOD'
- id: forge.repeat_installation
severity: critical
pattern: 'MultipleArgumentsForOptionException: Found multiple arguments for option (.*?), but you asked for only one'
confidence: 0.85
priority: 80
cause:
key: diagnose.cause.forge.repeat_installation
summary: 'Forge 被装了两次(同一版本出现多个安装参数)'
detail: '删掉重复的 Forge 安装/版本 JSON,再重装一次'
fix_action:
- kind: reinstall
component: forge
sample: samples/forge/repeat_installation.log
source: 'HMCL CrashReportAnalyzer.Rule.FORGE_REPEAT_INSTALLATION'
- id: forge.optifine_repeat_installation
severity: critical
pattern: 'ResolutionException: Module optifine reads another module named optifine'
confidence: 0.88
priority: 82
cause:
key: diagnose.cause.forge.optifine_repeat_installation
summary: 'OptiFine 装了两份(mods 里一个 + 自动安装一个)'
fix_action:
- kind: remove_mod
mod_ids:
- optifine
action: remove
note: '只保留一种安装方式'
sample: samples/forge/optifine_repeat_installation.log
source: 'HMCL CrashReportAnalyzer.Rule.OPTIFINE_REPEAT_INSTALLATION'
- id: forge.too_many_mods
severity: critical
pattern: 'maximum id range exceeded'
confidence: 0.9
priority: 80
cause:
key: diagnose.cause.forge.too_many_mods
summary: '装的 mod 太多,方块/物品 ID 超出上限(1.12 及更早的经典问题)'
detail: '删掉一部分 mod,或换用 1.13+ 的版本(不再有 ID 上限)'
fix_action:
- kind: remove_mod
action: disable
note: '减量或换版本'
sample: samples/forge/too_many_mods.log
source: 'HMCL CrashReportAnalyzer.Rule.TOO_MANY_MODS_LEAD_TO_EXCEEDING_THE_ID_LIMIT'
- id: forge.module_name_invalid
severity: warning
pattern: >-
Invalid module name: '' is not a Java identifier
confidence: 0.8
priority: 70
cause:
key: diagnose.cause.forge.module_name_invalid
summary: '某个 mod 的 jar 文件名不含英文字母/数字,Forge 把它当成非法 JPMS 模块名(如「拔刀剑.jar」)'
detail: '把该 jar 改成英文文件名(保留 .jar 后缀)再启动'
fix_action:
- kind: remove_mod
action: replace
note: '把 jar 改成英文文件名'
sample: samples/forge/module_name_invalid.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_NAME'
- id: forge.bootstrap_failed
severity: critical
pattern: 'Failed to create mod instance\. ModID: (?<id>.*?),'
confidence: 0.85
priority: 80
cause:
key: diagnose.cause.forge.bootstrap_failed
summary: 'mod {id} 的实例创建失败(初始化就抛异常)'
fix_action:
- kind: remove_mod
mod_ids:
- '{id}'
action: disable
sample: samples/forge/bootstrap_failed.log
source: 'HMCL CrashReportAnalyzer.Rule.BOOTSTRAP_FAILED'
@@ -0,0 +1,221 @@
# 规则库 · 显卡 / 图形(11 条 = HMCL 移植 6 + Vulkan 组 5)
# 移植自 HMCL CrashReportAnalyzer.Rule 的图形组(analyse/02 §7.3 第 3 组);
# Vulkan 组见文件末尾(依据 research/05-mc-vulkan.md §3.4 的 renderpearl 原因码)。
version: 1
category: graphics
rules:
- id: graphics.gl_operation_failure
severity: warning
pattern: '(1282: Invalid operation|Maybe try a lower resolution resourcepack\?)'
confidence: 0.7
priority: 60
cause:
key: diagnose.cause.graphics.gl_operation_failure
summary: '某个 mod/光影包做了非法的 OpenGL 操作(GL_INVALID_OPERATION)'
detail: '换掉最近装的光影/资源包,或把资源包分辨率降下来'
fix_action:
- kind: change_graphics
setting: renderer
suggested_value: 'OpenGL 兼容模式'
note: '先低分辨率资源包验证'
sample: samples/graphics/gl_operation_failure.log
source: 'HMCL CrashReportAnalyzer.Rule.GL_OPERATION_FAILURE'
- id: graphics.opengl_not_supported
severity: critical
pattern: 'The driver does not appear to support OpenGL'
confidence: 0.85
priority: 75
cause:
key: diagnose.cause.graphics.opengl_not_supported
summary: '显卡驱动不支持 OpenGL(多半是没装显卡驱动,走了软件渲染)'
detail: '装/更新显卡厂商驱动;虚拟机里需要开 3D 加速'
fix_action:
- kind: change_graphics
setting: driver
update_driver: true
sample: samples/graphics/opengl_not_supported.log
source: 'HMCL CrashReportAnalyzer.Rule.OPENGL_NOT_SUPPORTED'
- id: graphics.graphics_driver
severity: critical
# HMCL 的跨行贪婪模式(EXCEPTION_ACCESS_VIOLATION … # C [ig/nvoglv/atio)→ file scope。
# 末四段是 SDL 变体:26.3-snapshot-4 起窗口层 GLFW→SDL(lwjgl-glfw 消失、lwjgl-sdl 出现),
# 同类故障的原文会变成 SDL_GetError 的文本;旧 GLFW 模式保留(老版本日志仍需要)。
scope: file
pattern: '(Pixel format not accelerated|GLX: Failed to create context: GLXBadFBConfig|Couldn''t set pixel format|net\.minecraftforge\.fml\.client\.SplashProgress|org\.lwjgl\.LWJGLException|EXCEPTION_ACCESS_VIOLATION(.|\n|\r)+# C {2}\[(ig|atio|nvoglv)|SDL_CreateWindow failed|Couldn''t find matching render driver|No available video device)'
confidence: 0.8
priority: 78
cause:
key: diagnose.cause.graphics.graphics_driver
summary: '显卡驱动侧创建 OpenGL 上下文失败(像素格式/驱动崩溃)'
detail: '更新显卡驱动并重启;老显卡可用软渲染或换 OpenGL 兼容模式'
fix_action:
- kind: change_graphics
setting: driver
update_driver: true
- kind: change_graphics
setting: software_renderer
note: '驱动更新无效时用软渲染兜底(性能会掉)'
sample: samples/graphics/graphics_driver.log
source: 'HMCL CrashReportAnalyzer.Rule.GRAPHICS_DRIVER'
- id: graphics.macos_display_port
severity: critical
# 原 GLFW 模式保留。后两段是 SDL 变体(26.3 起窗口层换 SDL,无显示器/远程会话在 SDL 侧
# 报 SDL_Init 失败)——**但它必须和 `cocoa` 同时出现才命中**:SDL 在 macOS 上的视频驱动
# 就叫 `cocoa`,用它把本规则的 macOS 特异性钉住,免得 Windows/Linux 的 SDL_Init 失败被
# 误归因成「macOS 显示器问题」。⚠️ SDL 侧原文未在 26.3 实机验证(预测模式),拿到真实
# 日志后按实际文本收紧;宁可漏诊也不要跨平台误归因。
pattern: '(java\.lang\.IllegalStateException: GLFW error before init: \[0x10008\]Cocoa: Failed to find service port for display|(?i:cocoa).*SDL_Init.*failed|SDL_Init.*failed.*(?i:cocoa))'
confidence: 0.85
priority: 78
cause:
key: diagnose.cause.graphics.macos_display_port
summary: 'macOS 上 GLFW 初始化窗口失败(Cocoa 找不到 display service port)'
detail: '多见于远程桌面/无显示器会话;换成本机登录或升级 macOS 后再试'
fix_action:
- kind: change_graphics
setting: any
note: 'macOS 会话/显示器问题'
sample: samples/graphics/macos_display_port.log
source: 'HMCL CrashReportAnalyzer.Rule.MACOS_FAILED_TO_FIND_SERVICE_PORT_FOR_DISPLAY'
- id: graphics.resolution_too_high
severity: warning
pattern: 'Maybe try a (lower resolution|lowerresolution) (resourcepack|texturepack)\?'
confidence: 0.8
priority: 65
cause:
key: diagnose.cause.graphics.resolution_too_high
summary: '资源包分辨率过高(游戏直接提示换低分辨率资源包)'
fix_action:
- kind: change_graphics
setting: resolution
suggested_value: '低分辨率资源包'
sample: samples/graphics/resolution_too_high.log
source: 'HMCL CrashReportAnalyzer.Rule.RESOLUTION_TOO_HIGH'
- id: graphics.rtss_sodium
severity: warning
pattern: 'RivaTuner Statistics Server \(RTSS\) is not compatible with Sodium'
confidence: 0.9
priority: 70
cause:
key: diagnose.cause.graphics.rtss_sodium
summary: 'RTSS(微星小飞机/帧数显示)与 Sodium 不兼容'
detail: '关掉或卸载 RTSS 的 OSD 覆盖层,或改用 Sodium 兼容的帧数显示'
fix_action:
- kind: change_graphics
setting: overlay
suggested_value: '关闭 RTSS 覆盖层'
sample: samples/graphics/rtss_sodium.log
source: 'HMCL CrashReportAnalyzer.Rule.RTSS_FOREST_SODIUM'
# ── Vulkan 后端组(MC 26.2+ 的实验性 Vulkan 渲染)────────────────────────────
# 证据:research/05-mc-vulkan.md §3.4。renderpearl 的
# `BackendCreationException$Reason` 有五个稳定原因码(四个 vulkan_* + opengl_missing),
# 它们同时出现在 latest.log 的 `Backend failure reason: <码>` 与崩溃报告的
# `Backend failure reason` 行,是最可靠的落点(比英文正文稳,正文会随版本改)。
# 注意语义:26.2/26.3 的 Vulkan 首选失败会**自动回落 OpenGL**,所以单条 vulkan_* 命中
# 通常不是崩溃主因(warning);只有五个后端全挂才是 critical(见最后一条)。
- id: graphics.vulkan_loader_missing
severity: warning
pattern: '(Vulkan loader library is missing|vulkan_loader_missing)'
confidence: 0.85
priority: 80
cause:
key: diagnose.cause.graphics.vulkan_loader_missing
summary: '机器上没有 Vulkan loader(Vulkan 后端起不来,游戏已回落 OpenGL)'
detail: '装/更新显卡驱动(Windows 需要 vulkan-1.dll,Linux 需要 libvulkan.so.1);装不上就改用 OpenGL 后端'
fix_action:
- kind: change_graphics
setting: graphics_api
suggested_value: 'opengl'
note: 'Vulkan loader 缺失时强制 OpenGL,避免每次启动都白试一遍'
- kind: change_graphics
setting: driver
update_driver: true
sample: samples/graphics/vulkan_loader_missing.log
source: 'research/05-mc-vulkan.md §3.4(renderpearl BackendCreationException.Reason)'
- id: graphics.vulkan_device_version_too_low
severity: warning
pattern: '(vulkan_device_version_too_low|Vulkan [0-9]\.[0-9].{0,40}at least [0-9]\.[0-9] required)'
confidence: 0.85
priority: 78
cause:
key: diagnose.cause.graphics.vulkan_device_version_too_low
summary: '显卡/驱动支持的 Vulkan 版本低于 1.2(Vulkan 后端起不来)'
detail: '更新显卡驱动;驱动停更的老卡(10 年以上)没有 Vulkan 1.2,只能继续用 OpenGL'
fix_action:
- kind: change_graphics
setting: graphics_api
suggested_value: 'opengl'
- kind: change_graphics
setting: driver
update_driver: true
sample: samples/graphics/vulkan_device_version_too_low.log
source: 'research/05-mc-vulkan.md §3.4'
- id: graphics.vulkan_missing_extension
severity: warning
pattern: '(vulkan_missing_extension|missing: .*VK_KHR_(dynamic_rendering|push_descriptor))'
confidence: 0.85
priority: 76
cause:
key: diagnose.cause.graphics.vulkan_missing_extension
summary: '驱动缺 Vulkan 必需扩展(VK_KHR_dynamic_rendering / VK_KHR_push_descriptor)'
detail: 'MC 26.2+ 的 Vulkan 后端要求 VK 1.2 + dynamic_rendering + push_descriptor,三者缺一就跑不了;Mesa Dozen 也不支持 push_descriptor'
fix_action:
- kind: change_graphics
setting: graphics_api
suggested_value: 'opengl'
- kind: change_graphics
setting: driver
update_driver: true
note: '只有在驱动有新版本时才可能补上扩展'
sample: samples/graphics/vulkan_missing_extension.log
source: 'research/05-mc-vulkan.md §3.4 + §4.4(Dozen 反例)'
- id: graphics.vulkan_missing_feature
severity: warning
pattern: '(vulkan_missing_feature|missing capabilities: .*VULKAN_CORE_1_2)'
confidence: 0.8
priority: 74
cause:
key: diagnose.cause.graphics.vulkan_missing_feature
summary: '驱动缺 Vulkan 必需特性集(VULKAN_CORE_1_2)'
detail: '同上,属于驱动能力不足;老核显/虚拟机里常见,用 OpenGL 后端即可'
fix_action:
- kind: change_graphics
setting: graphics_api
suggested_value: 'opengl'
sample: samples/graphics/vulkan_missing_feature.log
source: 'research/05-mc-vulkan.md §3.4'
- id: graphics.no_graphics_backend
severity: critical
pattern: 'No supported graphics backend was found'
confidence: 0.9
priority: 82
suppress:
- graphics.vulkan_loader_missing
- graphics.vulkan_device_version_too_low
- graphics.vulkan_missing_extension
- graphics.vulkan_missing_feature
cause:
key: diagnose.cause.graphics.no_graphics_backend
summary: 'Vulkan 与 OpenGL 两个后端都创建失败(26.2+ 只有这两条路)'
detail: '硬件/驱动两侧都不满足:先更新显卡驱动;虚拟机需要开 3D 加速;实在不行只能换机器'
fix_action:
- kind: change_graphics
setting: graphics_api
suggested_value: 'opengl'
note: '先确认 Vulkan 失败原因再决定是否强推 OpenGL'
- kind: change_graphics
setting: software_renderer
note: '软渲染兜底能起来但基本没法玩,只用于导数据/排查'
sample: samples/graphics/no_graphics_backend.log
source: 'research/05-mc-vulkan.md §1.5(两个后端全失败才崩)'
@@ -0,0 +1,138 @@
# 规则库 · 文件与安装完整性(8 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的完整性组(analyse/02 §7.3 第 4 组)。
# 组内约定:通用异常(NoSuchMethodError / NoClassDefFoundError)优先级低,
# 让「安装不完整 / jar 被解压 / mod 冲突」这类能定位根因的规则通过 suppress 把它们降级为次生结论。
version: 1
category: integrity
rules:
- id: integrity.file_changed
severity: critical
pattern: 'java\.lang\.SecurityException: SHA1 digest error for (?<file>.*)|signer information does not match signer information of other classes in the same package'
confidence: 0.85
priority: 85
cause:
key: diagnose.cause.integrity.file_changed
summary: '游戏文件被改过(SHA1 校验不一致):jar 内容与清单哈希对不上'
detail: '重新下载/校验该文件;不要手改 jar 里的内容'
fix_action:
- kind: reinstall
component: game_jar
path_hint: '{file}'
sample: samples/integrity/file_changed.log
source: 'HMCL CrashReportAnalyzer.Rule.FILE_CHANGED'
- id: integrity.no_such_method
severity: warning
pattern: 'java\.lang\.NoSuchMethodError: (?<class>.*?)'
confidence: 0.6
priority: 40
cause:
key: diagnose.cause.integrity.no_such_method
summary: 'NoSuchMethodError:加载器/coremod 注入的东西与当前库版本不匹配'
detail: '常见于 mod 与游戏版本/Forge 版本不配套;重装加载器或删掉冲突 mod'
fix_action:
- kind: reinstall
component: loader
note: '先重装加载器;仍失败再逐个禁用 mod'
sample: samples/integrity/no_such_method.log
source: 'HMCL CrashReportAnalyzer.Rule.NO_SUCH_METHOD_ERROR'
- id: integrity.no_class_def_found
severity: warning
pattern: 'java\.lang\.NoClassDefFoundError: (?<class>.*)'
confidence: 0.6
priority: 40
cause:
key: diagnose.cause.integrity.no_class_def_found
summary: 'NoClassDefFoundError:缺少类文件(库没下全 / jar 被解压 / mod 不配套)'
detail: '先检查 libraries 是否完整,再排查 mods 里的解压目录'
fix_action:
- kind: reinstall
component: libraries
sample: samples/integrity/no_class_def_found.log
source: 'HMCL CrashReportAnalyzer.Rule.NO_CLASS_DEF_FOUND_ERROR'
- id: integrity.illegal_access
severity: warning
pattern: 'java\.lang\.IllegalAccessError: tried to access class (.*?) from class (?<class>.*?)'
confidence: 0.55
priority: 45
cause:
key: diagnose.cause.integrity.illegal_access
summary: 'IllegalAccessError:某个 coremod 想反射访问别的类的内部成员(在新 JDK 上被封了)'
fix_action:
- kind: reinstall
component: loader
note: '或换该 coremod/mod 的兼容版本'
sample: samples/integrity/illegal_access.log
source: 'HMCL CrashReportAnalyzer.Rule.ILLEGAL_ACCESS_ERROR'
- id: integrity.file_already_exists
severity: warning
pattern: 'java\.nio\.file\.FileAlreadyExistsException: (?<file>.*)'
confidence: 0.75
priority: 60
cause:
key: diagnose.cause.integrity.file_already_exists
summary: '某个 mod 要求一个「不该存在」的文件/目录,导致写入失败'
detail: '按提示删掉该文件(通常是玩家手建的空目录/unzip 残留)'
fix_action:
- kind: reinstall
component: mods
path_hint: '{file}'
note: '按路径删掉多余文件'
sample: samples/integrity/file_already_exists.log
source: 'HMCL CrashReportAnalyzer.Rule.FILE_ALREADY_EXISTS'
- id: integrity.mod_files_decompressed
severity: critical
pattern: '(The directories below appear to be extracted jar files\. Fix this before you continue|Extracted mod jars found, loading will NOT continue)'
confidence: 0.95
priority: 90
suppress:
- integrity.no_class_def_found
- integrity.no_such_method
cause:
key: diagnose.cause.integrity.mod_files_decompressed
summary: 'mods 目录里有被解压成文件夹的 jar:加载器会直接拒绝启动'
detail: '把解压出来的目录删掉,只保留 .jar 文件(很多压缩软件双击就解压了)'
fix_action:
- kind: reinstall
component: mods
note: '删掉解压出来的目录'
sample: samples/integrity/mod_files_decompressed.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_FILES_ARE_DECOMPRESSED'
- id: integrity.incomplete_forge
severity: critical
pattern: '(java\.io\.UncheckedIOException: java\.io\.IOException: Invalid paths argument, contained no existing paths: \[(.*?)(forge-(.*?)-client\.jar|fmlcore-(.*?)\.jar)\]|Failed to find Minecraft resource version (.*?) at (.*?)forge-(.*?)-client\.jar|Cannot find launch target fmlclient, unable to launch|java\.lang\.IllegalStateException: Could not find net/minecraft/client/Minecraft\.class in classloader SecureModuleClassLoader)'
confidence: 0.9
priority: 88
suppress:
- integrity.no_class_def_found
- integrity.no_such_method
cause:
key: diagnose.cause.integrity.incomplete_forge
summary: 'Forge 安装不完整(缺 forge-*-client.jar / fmlcore)'
detail: '重装该版本的 Forge,并检查 libraries 是否下全'
fix_action:
- kind: reinstall
component: forge
sample: samples/integrity/incomplete_forge.log
source: 'HMCL CrashReportAnalyzer.Rule.INCOMPLETE_FORGE_INSTALLATION'
- id: integrity.unsatisfied_link
severity: critical
pattern: 'java\.lang\.UnsatisfiedLinkError: Failed to locate library: (?<name>.*)'
confidence: 0.85
priority: 82
cause:
key: diagnose.cause.integrity.unsatisfied_link
summary: '找不到原生库(natives):{name} 没解压出来或被杀软删了'
detail: '重装组件(重新解压 natives);Windows 上检查杀软隔离区'
fix_action:
- kind: reinstall
component: natives
path_hint: '{name}'
sample: samples/integrity/unsatisfied_link.log
source: 'HMCL CrashReportAnalyzer.Rule.UNSATISFIED_LINK_ERROR'
+142
View File
@@ -0,0 +1,142 @@
# 规则库 · JVM / Java 版本(8 条)
# 移植自 HMCL CrashReportAnalyzer.Rule(analyse/02 §7.3 第 1 组)。
# schema 见 ../CONTRACT.md §2:pattern(regex) -> {cause, confidence, fix_action[]}
version: 1
category: jvm
rules:
- id: jvm.openj9
severity: critical
pattern: '(Open J9 is not supported|OpenJ9 is incompatible|\.J9VMInternals\.)'
confidence: 0.9
priority: 88
cause:
key: diagnose.cause.jvm.openj9
summary: '当前用的是 OpenJ9 虚拟机:Minecraft 与绝大多数 mod 只支持 HotSpot(Oracle/OpenJDK)'
detail: '请换成 HotSpot 的 Java 8/17/21(Adoptium、Zulu、Microsoft OpenJDK 的 hotspot 版本都可以)'
fix_action:
- kind: change_java
vendor: hotspot
note: 'OpenJ9 不兼容,换 HotSpot'
sample: samples/jvm/openj9.log
source: 'HMCL CrashReportAnalyzer.Rule.OPENJ9'
- id: jvm.need_jdk11
severity: critical
pattern: '(no such method: sun\.misc\.Unsafe\.defineAnonymousClass\(Class,byte\[\],Object\[\]\)Class/invokeVirtual|java\.lang\.UnsupportedClassVersionError: icyllis/modernui/forge/MixinConnector has been compiled by a more recent version of the Java Runtime \(class file version 55\.0\), this version of the Java Runtime only recognizes class file versions up to 52\.0|java\.lang\.IllegalArgumentException: The requested compatibility level JAVA_11 could not be set\. Level is not supported by the active JRE or ASM version)'
confidence: 0.92
priority: 90
cause:
key: diagnose.cause.jvm.need_jdk11
summary: '模组/加载器需要 Java 11,当前 Java 版本不够'
detail: '把实例的 Java 换到 11 或更高(1.17+ 的游戏通常要 17/21)'
fix_action:
- kind: change_java
required_major: 11
sample: samples/jvm/need_jdk11.log
source: 'HMCL CrashReportAnalyzer.Rule.NEED_JDK11'
- id: jvm.too_old_java
severity: critical
pattern: 'java\.lang\.UnsupportedClassVersionError: (.*?) version (?<expected>\d+)\.0'
confidence: 0.92
priority: 88
cause:
key: diagnose.cause.jvm.too_old_java
summary: 'Java 版本过低:无法加载用 class file version {expected} 编译的类,请换用更高版本的 Java'
detail: '这个 class 版本号减去 44 就是所需的 Java 大版本(52→Java 8、61→Java 17)'
fix_action:
- kind: change_java
class_file_version_group: expected
sample: samples/jvm/too_old_java.log
source: 'HMCL CrashReportAnalyzer.Rule.TOO_OLD_JAVA'
- id: jvm.32bit
severity: critical
pattern: '(Could not reserve enough space for (.*?)KB object heap|The specified size exceeds the maximum representable size|Invalid maximum heap size)'
scope: line
confidence: 0.9
priority: 92
suppress:
- memory.out_of_memory
cause:
key: diagnose.cause.jvm.32bit
summary: '分配堆内存失败:多半是 32 位 Java 或 -Xmx 超过系统可用内存'
detail: '32 位 Java 最多只能用约 1.5 GB;换 64 位 Java 或把 -Xmx 降到 2048 MB 以下'
fix_action:
- kind: adjust_memory
need_64bit_java: true
min_xmx_mb: 1024
- kind: change_java
required_major: 17
note: '32 位换 64 位'
sample: samples/jvm/jvm_32bit.log
source: 'HMCL CrashReportAnalyzer.Rule.JVM_32BIT'
- id: jvm.jdk_9
severity: critical
pattern: 'java\.lang\.ClassCastException: (java\.base/jdk|class jdk)'
confidence: 0.8
priority: 80
cause:
key: diagnose.cause.jvm.jdk_9
summary: '游戏只能在 Java 8 上跑(Forge/旧核心库不兼容 Java 9+),当前 Java 版本太高'
fix_action:
- kind: change_java
max_major: 8
sample: samples/jvm/jdk_9.log
source: 'HMCL CrashReportAnalyzer.Rule.JDK_9'
- id: jvm.version_too_high
severity: critical
pattern: '(Unable to make protected final java\.lang\.Class java\.lang\.ClassLoader\.defineClass|java\.lang\.NoSuchFieldException: ucp|Unsupported class file major version|because module java\.base does not export|java\.lang\.ClassNotFoundException: jdk\.nashorn\.api\.scripting\.NashornScriptEngineFactory|java\.lang\.ClassNotFoundException: java\.lang\.invoke\.LambdaMetafactory|Exception in thread "main" java\.lang\.NullPointerException: Cannot read the array length because "urls" is null)'
confidence: 0.85
priority: 84
cause:
key: diagnose.cause.jvm.version_too_high
summary: 'Java 版本过高:老版本 Forge/核心库用了被新 JDK 移除或封装的接口'
detail: '1.16 及更早优先用 Java 8;1.17–1.20.4 用 Java 17;1.20.5+ 用 Java 21'
fix_action:
- kind: change_java
vendor: hotspot
note: '降到该游戏版本要求的 Java'
- kind: reinstall
component: loader
note: '或把 Forge/加载器升级到支持新 Java 的版本'
sample: samples/jvm/java_version_is_too_high.log
source: 'HMCL CrashReportAnalyzer.Rule.JAVA_VERSION_IS_TOO_HIGH'
- id: jvm.modlauncher_8
severity: critical
pattern: 'java\.lang\.NoSuchMethodError: (''void sun\.security\.util\.ManifestEntryVerifier\.<init>\(java\.util\.jar\.Manifest\)''|sun\.security\.util\.ManifestEntryVerifier\.<init>\(Ljava/util/jar/Manifest;\)V)'
confidence: 0.9
priority: 86
cause:
key: diagnose.cause.jvm.modlauncher_8
summary: 'ModLauncher 8 与当前 Java 不兼容(JDK-8273826):1.16+ Forge 在新 JDK 上会这样崩'
fix_action:
- kind: change_java
max_major: 8
- kind: reinstall
component: forge
note: '或升级 Forge 到新版'
sample: samples/jvm/modlauncher_8.log
source: 'HMCL CrashReportAnalyzer.Rule.MODLAUNCHER_8'
- id: jvm.mac_jdk8u261
severity: critical
pattern: >-
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
confidence: 0.85
priority: 82
cause:
key: diagnose.cause.jvm.mac_jdk8u261
summary: 'macOS 上 JDK 8u261+ 用新 Xcode 编译导致的 Forge/OptiFine 崩溃(sp614x/optifine#4824)'
detail: '换 Java 8u252 及更早,或升级 Forge/OptiFine'
fix_action:
- kind: change_java
max_major: 8
note: '优先试 8u252'
- kind: reinstall
component: optifine
sample: samples/jvm/mac_jdk8u261.log
source: 'HMCL CrashReportAnalyzer.Rule.MAC_JDK_8U261'
@@ -0,0 +1,39 @@
# 规则库 · 内存(2 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的内存组(analyse/02 §7.3 第 2 组)。
version: 1
category: memory
rules:
- id: memory.out_of_memory
severity: critical
# 最后一条是跨行组合("Error occurred during initialization of VM" + "Too small maximum heap"),
# 因此整条规则用 file scope;HMCL 的 \R 在 Rust regex 里写成 (?:\r\n|\n|\r)。
scope: file
pattern: '(java\.lang\.OutOfMemoryError|The system is out of physical RAM or swap space|Out of Memory Error|Error occurred during initialization of VM(?:\r\n|\n|\r)Too small maximum heap)'
confidence: 0.9
priority: 80
cause:
key: diagnose.cause.memory.out_of_memory
summary: '游戏内存不足(OutOfMemoryError):-Xmx 太小,或装的 mod 太多/整合包太大'
detail: '把 -Xmx 调到物理内存的一半左右(一般 4096–8192 MB),装光影/大量 mod 时适度加大'
fix_action:
- kind: adjust_memory
min_xmx_mb: 2048
note: '优先加大 -Xmx;若已经是 32 位 Java,先换 64 位'
sample: samples/memory/out_of_memory.log
source: 'HMCL CrashReportAnalyzer.Rule.OUT_OF_MEMORY'
- id: memory.memory_exceeded
severity: critical
pattern: 'There is insufficient memory for the Java Runtime Environment to continue'
confidence: 0.88
priority: 82
cause:
key: diagnose.cause.memory.memory_exceeded
summary: '系统物理内存/交换区耗尽:JVM 连续分配失败,整机内存不够'
detail: '关掉占内存的程序、降低 -Xmx,或扩大系统虚拟内存(页面文件)'
fix_action:
- kind: adjust_memory
increase_system_swap: true
note: '整机内存不足,不只是 -Xmx 的问题'
sample: samples/memory/memory_exceeded.log
source: 'HMCL CrashReportAnalyzer.Rule.MEMORY_EXCEEDED'
@@ -0,0 +1,152 @@
# 规则库 · 模组互斥(8 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的互斥组(analyse/02 §7.3 第 8 组):
# 具体到某个知名 mod 与 OptiFine / Sodium 的成名冲突,修复动作几乎都是「删 OptiFine 或其对手」。
version: 1
category: mod_conflict
rules:
- id: conflict.optifine_forge
severity: critical
scope: file
# 移植说明:HMCL 原文里的 \> / \< 在这里改成裸 > / <(正则里 \< \> 是词边界断言,要字面量得写裸字符)。
# 末尾 alternative 保持 upstream 语义:`\(Reflector\.java)` 的前置 \( 是**字面括号**,尾部裸 ) 才是**闭合外层分组**,
# 因此它是前缀匹配。这里显式写成 `\(Reflector\.java(?::\d+)?\))`:裸 `(Reflector.java)` 与带行号的
# `(Reflector.java:455)` 两种真实栈帧形态都能命中(P2-1 回归,见 tests/diagnose/engine_rank.rs)。
pattern: >-
(java\.lang\.NoSuchMethodError: 'java\.lang\.Class sun\.misc\.Unsafe\.defineAnonymousClass\(java\.lang\.Class, byte\[\], java\.lang\.Object\[\]\)'|java\.lang\.NoSuchMethodError: 'void net\.minecraft\.client\.renderer\.texture\.SpriteContents\.<init>\(net\.minecraft\.resources\.ResourceLocation, |java\.lang\.NoSuchMethodError: 'void net\.minecraftforge\.client\.gui\.overlay\.ForgeGui\.renderSelectedItemName\(net\.minecraft\.client\.gui\.GuiGraphics, int\)'|java\.lang\.NoSuchMethodError: 'java\.lang\.String com\.mojang\.blaze3d\.systems\.RenderSystem\.getBackendDescription\(\)'|java\.lang\.NoSuchMethodError: 'net\.minecraft\.network\.chat\.FormattedText net\.minecraft\.client\.gui\.Font\.ellipsize\(net\.minecraft\.network\.chat\.FormattedText, int\)'|java\.lang\.NoSuchMethodError: 'void net\.minecraft\.server\.level\.DistanceManager\.(.*?)\(net\.minecraft\.server\.level\.TicketType, net\.minecraft\.world\.level\.ChunkPos, int, java\.lang\.Object, boolean\)'|java\.lang\.NoSuchMethodError: 'void net\.minecraft\.client\.renderer\.block\.model\.BakedQuad\.<init>\(int\[\], int, net\.minecraft\.core\.Direction, net\.minecraft\.client\.renderer\.texture\.TextureAtlasSprite, boolean, boolean\)'|TRANSFORMER/net\.optifine/net\.optifine\.reflect\.Reflector\.<clinit>\(Reflector\.java(?::\d+)?\))
confidence: 0.9
priority: 87
suppress:
- integrity.no_such_method
- integrity.no_class_def_found
cause:
key: diagnose.cause.conflict.optifine_forge
summary: 'OptiFine 与 Forge(或某个 Forge mod)不兼容:NoSuchMethodError 指向被 OptiFine 改写的原版方法'
detail: '删掉 OptiFine,改用同类替代(Sodium/Iris、Embeddium、Oculus 等),或换 OptiFine 的匹配版本'
fix_action:
- kind: remove_mod
mod_ids:
- optifine
action: remove
note: 'OptiFine 与 Forge 冲突,删掉其一'
sample: samples/mod_conflict/optifine_forge.log
source: 'HMCL CrashReportAnalyzer.Rule.OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE'
- id: conflict.shaders_mod
severity: critical
pattern: 'java\.lang\.RuntimeException: Shaders Mod detected\. Please remove it, OptiFine has built-in support for shaders\.'
confidence: 0.95
priority: 85
cause:
key: diagnose.cause.conflict.shaders_mod
summary: '装了独立的 Shaders Mod,但 OptiFine 自带光影支持,两者不能共存'
fix_action:
- kind: remove_mod
mod_ids:
- shadersmod
action: remove
sample: samples/mod_conflict/shaders_mod.log
source: 'HMCL CrashReportAnalyzer.Rule.SHADERS_MOD'
- id: conflict.multipart_optifine
severity: critical
pattern: 'Error occurred applying transform of coremod META-INF/asm/multipart\.js function render'
confidence: 0.8
priority: 80
cause:
key: diagnose.cause.conflict.multipart_optifine
summary: 'Forge Multipart 的 coremod 转换失败(多为 OptiFine 同时存在导致)'
fix_action:
- kind: remove_mod
mod_ids:
- optifine
action: remove
note: '删 OptiFine 或 ForgeMultipart 之一'
sample: samples/mod_conflict/multipart_optifine.log
source: 'HMCL CrashReportAnalyzer.Rule.MOD_FOREST_OPTIFINE'
- id: conflict.performant_optifine
severity: critical
pattern: 'org\.spongepowered\.asm\.mixin\.injection\.throwables\.InjectionError: Critical injection failure: Redirector OnisOnLadder\(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/entity/LivingEntity;\)Z in performant\.mixins\.json:entity\.LivingEntityMixin failed injection check, \(0/1\) succeeded\. Scanned 1 target\(s\)\. Using refmap performant\.refmap\.json'
confidence: 0.9
priority: 86
cause:
key: diagnose.cause.conflict.performant_optifine
summary: 'Performant 与 OptiFine 的 mixin 冲突(LivingEntityMixin 注入失败)'
fix_action:
- kind: remove_mod
mod_ids:
- performant
- optifine
action: remove
note: '两者只能留一个'
sample: samples/mod_conflict/performant_optifine.log
source: 'HMCL CrashReportAnalyzer.Rule.PERFORMANT_FOREST_OPTIFINE'
- id: conflict.twilight_optifine
severity: warning
pattern: 'java\.lang\.IllegalArgumentException: (.*) outside of image bounds (.*)'
confidence: 0.6
priority: 70
cause:
key: diagnose.cause.conflict.twilight_optifine
summary: '贴图越界(outside of image bounds):1.16 上 Twilight Forest 与 OptiFine 的经典冲突'
fix_action:
- kind: remove_mod
mod_ids:
- optifine
action: remove
note: '删 OptiFine 或 Twilight Forest 的兼容版本'
sample: samples/mod_conflict/twilight_optifine.log
source: 'HMCL CrashReportAnalyzer.Rule.TWILIGHT_FOREST_OPTIFINE'
- id: conflict.jade_optifine
severity: critical
pattern: 'Critical injection failure: LVT in net/minecraft/client/renderer/GameRenderer::m_109093_\(FJZ\)V has incompatible changes at opcode 760 in callback jade\.mixins\.json:GameRendererMixin->@Inject::jade\$runTick\(FJZLorg/spongepowered/asm/mixin/injection/callback/CallbackInfo;IILcom/mojang/blaze3d/platform/Window;Lorg/joml/Matrix4f;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/gui/GuiGraphics;\)V\.'
confidence: 0.9
priority: 86
cause:
key: diagnose.cause.conflict.jade_optifine
summary: 'Jade 与 OptiFine 在 1.20+ 冲突(GameRendererMixin 注入失败)'
fix_action:
- kind: remove_mod
mod_ids:
- optifine
- jade
action: remove
note: '两者只能留一个'
sample: samples/mod_conflict/jade_optifine.log
source: 'HMCL CrashReportAnalyzer.Rule.JADE_FOREST_OPTIFINE'
- id: conflict.neoforge_optifine
severity: critical
pattern: 'cpw\.mods\.modlauncher\.InvalidLauncherSetupException: Invalid Services found OptiFine'
confidence: 0.9
priority: 86
cause:
key: diagnose.cause.conflict.neoforge_optifine
summary: 'NeoForge 与 OptiFine 不兼容(launcher setup 阶段就拒绝)'
fix_action:
- kind: remove_mod
mod_ids:
- optifine
action: remove
sample: samples/mod_conflict/neoforge_optifine.log
source: 'HMCL CrashReportAnalyzer.Rule.NEOFORGE_FOREST_OPTIFINE'
- id: conflict.optifine_world_load
severity: critical
# 移植修正:HMCL 原文写的是 ChunkManager$ProxyTicketManager,Java 正则里 $ 是行尾锚点,
# 那条规则实际上永远命中不了。这里转义成字面量 $,规则才能真正生效。
pattern: 'java\.lang\.NoSuchMethodError: net\.minecraft\.world\.server\.ChunkManager\$ProxyTicketManager\.shouldForceTicks\(J\)Z'
confidence: 0.85
priority: 84
cause:
key: diagnose.cause.conflict.optifine_world_load
summary: 'OptiFine 导致世界无法加载(ChunkManager$ProxyTicketManager 方法被改写)'
fix_action:
- kind: remove_mod
mod_ids:
- optifine
action: remove
sample: samples/mod_conflict/optifine_world_load.log
source: 'HMCL CrashReportAnalyzer.Rule.OPTIFINE_CAUSES_THE_WORLD_TO_FAIL_TO_LOAD'
@@ -0,0 +1,136 @@
# 规则库 · mod 运行期崩(8 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的运行期组(analyse/02 §7.3 第 7 组)。
version: 1
category: mod_runtime
rules:
- id: mod.loading_crashed_forge
severity: critical
pattern: 'LoaderExceptionModCrash: Caught exception from (?<name>.*?) \((?<id>.*)\)'
confidence: 0.9
priority: 85
cause:
key: diagnose.cause.mod.loading_crashed_forge
summary: 'Forge 抓到 {name}({id})在加载时抛异常'
detail: '禁用该 mod 或换它的兼容版本;同一条栈里往往还有具体异常类型'
fix_action:
- kind: remove_mod
mod_ids:
- '{id}'
action: disable
sample: samples/mod_runtime/loading_crashed_forge.log
source: 'HMCL CrashReportAnalyzer.Rule.LOADING_CRASHED_FORGE'
- id: mod.loading_crashed_fabric
severity: critical
pattern: >-
Could not execute entrypoint stage '(.*?)' due to errors, provided by '(?<id>.*)'!
confidence: 0.9
priority: 85
cause:
key: diagnose.cause.mod.loading_crashed_fabric
summary: 'Fabric 在入口点阶段执行 mod {id} 时出错'
fix_action:
- kind: remove_mod
mod_ids:
- '{id}'
action: disable
sample: samples/mod_runtime/loading_crashed_fabric.log
source: 'HMCL CrashReportAnalyzer.Rule.LOADING_CRASHED_FABRIC'
- id: mod.config_failed
severity: warning
pattern: 'Failed loading config file (?<file>.*?) of type (.*?) for modid (?<id>.*)'
confidence: 0.7
priority: 60
cause:
key: diagnose.cause.mod.config_failed
summary: 'mod {id} 的配置文件 {file} 解析失败(多半是被手改坏或版本升级后格式变了)'
detail: '删掉该配置文件让 mod 重新生成默认值'
fix_action:
- kind: remove_mod
mod_ids:
- '{id}'
action: disable
note: '删配置文件 {file} 后重试(不一定要禁用 mod)'
sample: samples/mod_runtime/config_failed.log
source: 'HMCL CrashReportAnalyzer.Rule.CONFIG'
- id: mod.mixin_failure
severity: critical
pattern: '(MixinApplyError|Mixin prepare failed |Mixin apply failed |mixin\.injection\.throwables\.|\.mixins\.json\] FAILED during \))'
confidence: 0.7
priority: 65
cause:
key: diagnose.cause.mod.mixin_failure
summary: 'Mixin 注入失败:某个 mod 的 mixin 与当前游戏/其他 mod 的代码不匹配'
detail: '常见于 mod 与游戏版本不配套;看日志里 mixin 的 target 类名能定位到 mod'
fix_action:
- kind: remove_mod
action: disable
note: '按 mixin 配置文件名(xxx.mixins.json)定位 mod'
sample: samples/mod_runtime/mixin_failure.log
source: 'HMCL CrashReportAnalyzer.Rule.MODMIXIN_FAILURE'
- id: mod.mixin_apply_failed
severity: critical
pattern: 'Mixin apply for mod (?<id>.*) failed'
confidence: 0.85
priority: 82
cause:
key: diagnose.cause.mod.mixin_apply_failed
summary: 'mod {id} 的 mixin 应用失败'
fix_action:
- kind: remove_mod
mod_ids:
- '{id}'
action: disable
sample: samples/mod_runtime/mixin_apply_failed.log
source: 'HMCL CrashReportAnalyzer.Rule.MIXIN_APPLY_MOD_FAILED'
- id: mod.forge_error
severity: warning
scope: file
pattern: 'An exception was thrown, the game will display an error screen and halt\.(?:\r\n|\n|\r)*(?<reason>.*(?:\r\n|\n|\r)*(\s*at .*(?:\r\n|\n|\r))+)'
confidence: 0.6
priority: 55
cause:
key: diagnose.cause.mod.forge_error
summary: 'Forge 捕获到未处理异常并已弹出错误界面(Forge 自己给了栈)'
detail: '栈顶的 at 行指向哪个 mod 的包名,就去禁用/升级那个 mod'
fix_action:
- kind: seek_help
export_diagnostics: true
sample: samples/mod_runtime/forge_error.log
source: 'HMCL CrashReportAnalyzer.Rule.FORGE_ERROR'
- id: mod.night_config_fixes
severity: warning
pattern: 'com\.electronwill\.nightconfig\.core\.io\.ParsingException: Not enough data available'
confidence: 0.85
priority: 75
cause:
key: diagnose.cause.mod.night_config_fixes
summary: 'night-config 配置文件读坏了(常见于 nightconfigfixes 相关 mod 组合)'
detail: '删掉对应 mod 的配置文件,或移除 nightconfigfixes'
fix_action:
- kind: remove_mod
mod_ids:
- nightconfigfixes
action: remove
sample: samples/mod_runtime/night_config_fixes.log
source: 'HMCL CrashReportAnalyzer.Rule.NIGHT_CONFIG_FIXES'
- id: mod.install_mixinbootstrap
severity: critical
pattern: 'java\.lang\.ClassNotFoundException: org\.spongepowered\.asm\.launch\.MixinTweaker'
confidence: 0.8
priority: 75
cause:
key: diagnose.cause.mod.install_mixinbootstrap
summary: '缺少 MixinBootstrap 前置(Forge 1.12.2 上很多 mod 需要它)'
fix_action:
- kind: reinstall
component: mixin_bootstrap
note: '装上 MixinBootstrap 0.8+'
sample: samples/mod_runtime/install_mixinbootstrap.log
source: 'HMCL CrashReportAnalyzer.Rule.INSTALL_MIXINBOOTSTRAP'
@@ -0,0 +1,22 @@
# 规则库 · 其它(1 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的杂项组(analyse/02 §7.3 第 10 组)。
# 将来「用户反馈沉淀的新规则」如果归不了类,也先落这里(标记 source 与需要复核)。
version: 1
category: other
rules:
- id: other.debug_crash
severity: info
pattern: 'Manually triggered debug crash'
confidence: 0.95
priority: 70
cause:
key: diagnose.cause.other.debug_crash
summary: '这次崩溃是手动触发的(游戏里按 F3+C),不是真的故障'
detail: '如果用户点了「导出诊断包」做测试,可以忽略这条'
fix_action:
- kind: seek_help
export_diagnostics: false
needs_more_info:
- '确认是否手动触发的调试崩溃'
sample: samples/other/debug_crash.log
source: 'HMCL CrashReportAnalyzer.Rule.DEBUG_CRASH'
@@ -0,0 +1,2 @@
[12:00:40] [main/ERROR]: Failed to start Fabric loader
java.lang.NoClassDefFoundError: org/spongepowered/asm/mixin/transformer/FabricMixinTransformerProxy
@@ -0,0 +1,3 @@
net.fabricmc.loader.impl.FormattedException: net.fabricmc.loader.impl.discovery.ModResolutionException: Mod resolution encountered an incompatible mod set!
- Mod 'Sodium' (sodium) 0.5.8 requires version 0.6.0 or later of mod 'Iris' (iris)
- Mod 'Iris' (iris) 0.5.0 is present but is incompatible
@@ -0,0 +1 @@
net.fabricmc.loader.impl.discovery.ModResolutionException: Could not resolve valid mod collection (at: iris requires sodium)
@@ -0,0 +1 @@
net.fabricmc.loader.impl.discovery.ModResolutionException: Found conflicting mods: sodium conflicts with sodium-extra
@@ -0,0 +1,3 @@
Mod File: /mods/sodium-fabric-0.5.0.jar
Failure message: Mod resolution failed
net.fabricmc.loader.impl.discovery.ModResolutionException: see above
@@ -0,0 +1 @@
net.fabricmc.loader.impl.discovery.ModResolutionException: Could not find required mod: sodium requires iris
@@ -0,0 +1 @@
net.fabricmc.loader.impl.discovery.ModResolutionException: Could not find required mod: sodium requires {minecraft @ 1.20.1}
@@ -0,0 +1,3 @@
Warnings were found!
- Sodium 0.4.10 is incompatible with Iris 1.6.0
[
@@ -0,0 +1 @@
[12:00:55] [main/ERROR]: Failed to create mod instance. ModID: alexsmobs, Class: com.github.alexthe666.alexsmobs.AlexsMobs
@@ -0,0 +1,3 @@
Found duplicate mods:
jei-1.20.1-15.2.0.27.jar
jei-1.20.1-15.2.0.28.jar
@@ -0,0 +1 @@
[12:00:50] [main/ERROR]: Found a duplicate mod jei at /mods/jei-1.20.1-15.2.0.28.jar
@@ -0,0 +1,2 @@
Missing or unsupported mandatory dependencies:
Mod ID: citadel, Requested by: alexsmobs, Expected range: [1.20.1-2.4.0,), Actual version: MISSING
@@ -0,0 +1 @@
[12:00:54] [main/ERROR]: java.lang.IllegalArgumentException: Invalid module name: '' is not a Java identifier
@@ -0,0 +1 @@
[12:00:52] [main/ERROR]: org.spongepowered.asm.launch.platform.MixinPlatformManager threw ResolutionException: Module optifine reads another module named optifine
@@ -0,0 +1 @@
[12:00:51] [main/ERROR]: joptsimple.MultipleArgumentsForOptionException: Found multiple arguments for option net.minecraftforge, but you asked for only one
@@ -0,0 +1 @@
[12:00:53] [main/ERROR]: java.lang.RuntimeException: maximum id range exceeded
@@ -0,0 +1,3 @@
[12:00:20] [Render thread/ERROR]: ########## GL ERROR ##########
[12:00:20] [Render thread/ERROR]: @ Pre render
[12:00:20] [Render thread/ERROR]: 1282: Invalid operation
@@ -0,0 +1,7 @@
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffb1a2b3c4d, pid=12344, tid=0x0000000000002a1c
#
# Problematic frame:
# C [ig8d64.dll+0x9b3c4d]
@@ -0,0 +1 @@
[12:00:22] [main/ERROR]: java.lang.IllegalStateException: GLFW error before init: [0x10008]Cocoa: Failed to find service port for display
@@ -0,0 +1,15 @@
---- Minecraft Crash Report ----
// I let you down. Sorry :(
Time: 2026-10-30 12:00:21
Description: Initializing game
net.minecraft.client.Minecraft$GameLoadException: No supported graphics backend was found.
at net.minecraft.client.Minecraft.<init>(Minecraft.java:612)
at net.minecraft.client.main.Main.main(Main.java:184)
-- Graphics Backend --
Backend: VULKAN
Backend failure reason: vulkan_loader_missing
Backend: OPENGL
Backend failure reason: opengl_missing
@@ -0,0 +1 @@
[12:00:21] [main/ERROR]: The driver does not appear to support OpenGL
@@ -0,0 +1 @@
[12:00:23] [Render thread/WARN]: Maybe try a lower resolution resourcepack?
@@ -0,0 +1 @@
[12:00:24] [main/ERROR]: RivaTuner Statistics Server (RTSS) is not compatible with Sodium
@@ -0,0 +1,5 @@
[12:00:21] [Render thread/INFO]: Using graphics backend VULKAN, using drivers: Intel proprietary driver
[12:00:21] [Render thread/ERROR]: Backend failure reason: vulkan_device_version_too_low
[12:00:21] [Render thread/ERROR]: Backend failure message: Device NVIDIA GeForce GT 710 reports Vulkan 1.0.0, at least 1.2 required
[12:00:21] [Render thread/ERROR]: Failed to create backend VULKAN
[12:00:21] [Render thread/INFO]: Falling back to OpenGL
@@ -0,0 +1,5 @@
[12:00:21] [Render thread/INFO]: Backend library: LWJGL version 3.4.3+13
[12:00:21] [Render thread/INFO]: Using graphics backend VULKAN, using drivers: AMD proprietary driver
[12:00:21] [Render thread/ERROR]: Vulkan loader library is missing
[12:00:21] [Render thread/ERROR]: Failed to create backend VULKAN: Backend failure reason: vulkan_loader_missing
[12:00:21] [Render thread/INFO]: Falling back to OpenGL
@@ -0,0 +1,5 @@
[12:00:21] [Render thread/INFO]: Using graphics backend VULKAN, using drivers: Mesa (zink)
[12:00:21] [Render thread/ERROR]: Backend failure reason: vulkan_missing_extension
[12:00:21] [Render thread/ERROR]: Device [AMD Radeon RX 580] does not support required extensions from FeatureSet [VULKAN_12], missing: [VK_KHR_dynamic_rendering, VK_KHR_push_descriptor]
[12:00:21] [Render thread/ERROR]: Failed to create backend VULKAN
[12:00:21] [Render thread/INFO]: Falling back to OpenGL
@@ -0,0 +1,5 @@
[12:00:21] [Render thread/INFO]: Using graphics backend VULKAN, using drivers: llvmpipe (Mesa)
[12:00:21] [Render thread/ERROR]: Backend failure reason: vulkan_missing_feature
[12:00:21] [Render thread/ERROR]: Backend failure missing capabilities: VULKAN_CORE_1_2
[12:00:21] [Render thread/ERROR]: Failed to create backend VULKAN
[12:00:21] [Render thread/INFO]: Falling back to OpenGL
@@ -0,0 +1,34 @@
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffb1a2b3c4d, pid=12344, tid=0x0000000000002a1c
#
# JRE version: Java(TM) SE Runtime Environment (8.0_51-b16) (build 1.8.0_51-b16)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.51-b03 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C [ig8d64.dll+0x9b3c4d]
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# If you would like to submit a bug report, please visit:
# http://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
--------------- T H R E A D ---------------
Current thread (0x000000001f3b8000): JavaThread "Render thread" [_thread_in_native, id=10780, stack(0x000000001a000000,0x000000001a100000)]
siginfo: ExceptionCode=0xc0000005, ExceptionInformation=0x0000000000000000 0x0000000000000000 0x0000000000000000
VM Arguments:
jvm_args: -Xmx4096m -Xms1024m -Djava.library.path=C:\Users\player\.minecraft\natives
java_command: net.minecraft.launchwrapper.Launch --gameDir . --assetsDir assets
--------------- H E A P ---------------
Heap:
garbage-first heap total 4194304K, used 1048576K [0x0000000700000000, 0x0000000800000000)
Memory: 4k page, physical 16741480k(4716856k free), swap 18672384k(9433608k free)
@@ -0,0 +1,19 @@
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 1073741824 bytes for committing reserved memory.
#
# Out of Memory Error (os_windows.cpp:3521), pid=9876, tid=0x0000000000001f44
#
# JRE version: Java(TM) SE Runtime Environment (17.0.9+9) (build 17.0.9+9-LTS-211)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (17.0.9+9-LTS-211, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
#
--------------- T H R E A D ---------------
Current thread (0x0000021d6f82b1a0): JavaThread "Render thread" [_thread_in_vm, id=14120, stack(0x000000a1b0000000,0x000000a1b0100000)]
VM Arguments:
jvm_args: -Xmx8192m -Xms2048m
java_command: net.fabricmc.loader.impl.launch.knot.KnotClient --gameDir .
Memory: 4k page, physical 8134564k(120944k free), swap 9723904k(550664k free)
@@ -0,0 +1 @@
[12:00:34] [main/ERROR]: java.nio.file.FileAlreadyExistsException: config/example-mod.toml
@@ -0,0 +1,2 @@
[12:00:30] [main/ERROR]: Failed to verify class file
java.lang.SecurityException: SHA1 digest error for net/minecraft/client/main/Main.class
@@ -0,0 +1,2 @@
[12:00:33] [main/ERROR]: coremod failed
java.lang.IllegalAccessError: tried to access class net.minecraft.client.Minecraft from class com.example.coremod.Hook
@@ -0,0 +1 @@
[12:00:36] [main/ERROR]: java.io.UncheckedIOException: java.io.IOException: Invalid paths argument, contained no existing paths: [/home/user/.minecraft/libraries/net/minecraftforge/forge/1.20.1-47.2.0/forge-1.20.1-47.2.0-client.jar]
@@ -0,0 +1,2 @@
[12:00:35] [main/ERROR]: Extracted mod jars found, loading will NOT continue
[12:00:35] [main/ERROR]: The directories below appear to be extracted jar files. Fix this before you continue
@@ -0,0 +1,2 @@
[12:00:32] [main/ERROR]: A mod crashed on startup
java.lang.NoClassDefFoundError: net/minecraftforge/fml/loading/FMLEnvironment
@@ -0,0 +1,3 @@
[12:00:31] [main/ERROR]: Failed to start
java.lang.NoSuchMethodError: net.minecraft.world.World.getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;
at com.example.mod.Foo.bar(Foo.java:42)
@@ -0,0 +1,2 @@
[12:00:37] [main/ERROR]: Failed to initialise LWJGL
java.lang.UnsatisfiedLinkError: Failed to locate library: lwjgl.dll
@@ -0,0 +1,3 @@
[12:00:06] [main/ERROR]: Unable to launch
java.lang.NoSuchFieldException: ucp
at java.base/java.lang.ClassLoader$UnsafeAccessor.getUnsafe(ClassLoader.java:123)
@@ -0,0 +1,2 @@
[12:00:05] [main/ERROR]: Failed to launch
java.lang.ClassCastException: java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to java.net.URLClassLoader
@@ -0,0 +1,3 @@
[12:00:04] [main/INFO]: Starting JVM with -Xmx4096m
Error occurred during initialization of VM
Could not reserve enough space for 4194304KB object heap
@@ -0,0 +1 @@
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
@@ -0,0 +1,3 @@
[12:00:07] [main/ERROR]: ModLauncher failed
java.lang.NoSuchMethodError: 'void sun.security.util.ManifestEntryVerifier.<init>(java.util.jar.Manifest)'
at cpw.mods.modlauncher.TransformingClassLoader.buildTransformedClassNodeFor(TransformingClassLoader.java:151)
@@ -0,0 +1,2 @@
[12:00:02] [main/ERROR]: Failed to start the game
java.lang.IllegalArgumentException: The requested compatibility level JAVA_11 could not be set. Level is not supported by the active JRE or ASM version
@@ -0,0 +1,2 @@
[12:00:01] [main/INFO]: Loading Minecraft 1.20.1 with Forge 47.2.0
OpenJ9 is incompatible with the authentication libraries used by this launcher
@@ -0,0 +1,2 @@
[12:00:03] [main/ERROR]: Could not find or load main class net.fabricmc.loader.impl.launch.knot.KnotClient
java.lang.UnsupportedClassVersionError: net/fabricmc/loader/impl/launch/knot/KnotClient has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0
@@ -0,0 +1,3 @@
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 67108864 bytes for committing reserved memory.
@@ -0,0 +1,3 @@
[12:00:10] [Render thread/ERROR]: Encountered an unexpected exception
java.lang.OutOfMemoryError: Java heap space
at net.minecraft.client.renderer.chunk.RenderChunk.rebuildChunk(RenderChunk.java:123)
@@ -0,0 +1 @@
[12:01:15] [main/ERROR]: Critical injection failure: LVT in net/minecraft/client/renderer/GameRenderer::m_109093_(FJZ)V has incompatible changes at opcode 760 in callback jade.mixins.json:GameRendererMixin->@Inject::jade$runTick(FJZLorg/spongepowered/asm/mixin/injection/callback/CallbackInfo;IILcom/mojang/blaze3d/platform/Window;Lorg/joml/Matrix4f;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/gui/GuiGraphics;)V.
@@ -0,0 +1 @@
[12:01:12] [main/ERROR]: Error occurred applying transform of coremod META-INF/asm/multipart.js function render
@@ -0,0 +1 @@
[12:01:16] [main/ERROR]: cpw.mods.modlauncher.InvalidLauncherSetupException: Invalid Services found OptiFine
@@ -0,0 +1,4 @@
[12:01:10] [main/ERROR]: Failed to launch Forge
java.lang.NoSuchMethodError: 'java.lang.String com.mojang.blaze3d.systems.RenderSystem.getBackendDescription()'
at net.optifine.reflect.Reflector.<clinit>(Reflector.java:455)
TRANSFORMER/net.optifine/net.optifine.reflect.Reflector.<clinit>(Reflector.java:455)
@@ -0,0 +1 @@
[12:01:17] [main/ERROR]: java.lang.NoSuchMethodError: net.minecraft.world.server.ChunkManager$ProxyTicketManager.shouldForceTicks(J)Z
@@ -0,0 +1 @@
[12:01:13] [main/ERROR]: org.spongepowered.asm.mixin.injection.throwables.InjectionError: Critical injection failure: Redirector OnisOnLadder(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/entity/LivingEntity;)Z in performant.mixins.json:entity.LivingEntityMixin failed injection check, (0/1) succeeded. Scanned 1 target(s). Using refmap performant.refmap.json
@@ -0,0 +1 @@
[12:01:11] [main/ERROR]: java.lang.RuntimeException: Shaders Mod detected. Please remove it, OptiFine has built-in support for shaders.
@@ -0,0 +1 @@
[12:01:14] [main/ERROR]: java.lang.IllegalArgumentException: 512 outside of image bounds 256
@@ -0,0 +1 @@
[12:01:02] [main/ERROR]: Failed loading config file alexsmobs-common.toml of type COMMON for modid alexsmobs
@@ -0,0 +1,3 @@
[12:01:05] [main/ERROR]: An exception was thrown, the game will display an error screen and halt.
java.lang.NullPointerException: Cannot invoke "net.minecraft.world.level.Level.getBlockState()" because "level" is null
at com.example.mod.Foo.tick(Foo.java:123)
@@ -0,0 +1 @@
[12:01:07] [main/ERROR]: java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker
@@ -0,0 +1 @@
[12:01:01] [main/ERROR]: Could not execute entrypoint stage 'main' due to errors, provided by 'sodium'!
@@ -0,0 +1 @@
[12:01:00] [main/ERROR]: LoaderExceptionModCrash: Caught exception from Alex's Mobs (alexsmobs)
@@ -0,0 +1 @@
[12:01:04] [main/ERROR]: Mixin apply for mod examplemod failed examplemod.mixins.json:MixinFoo from mod examplemod->@Inject::handler
@@ -0,0 +1 @@
[12:01:03] [main/ERROR]: org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException: Critical injection failure: @Inject annotation could not find target method
@@ -0,0 +1 @@
[12:01:06] [main/ERROR]: com.electronwill.nightconfig.core.io.ParsingException: Not enough data available
@@ -0,0 +1 @@
[12:01:30] [Render thread/INFO]: Manually triggered debug crash
@@ -0,0 +1,6 @@
[12:01:21] [Worker-Main-1/ERROR]: Unable to bake model
java.lang.RuntimeException: Failed to tessellate
-- Block being tessellated --
Details:
Block: minecraft:grass_block
Block location: World: (12,64,-34)
@@ -0,0 +1,6 @@
[12:01:20] [Server thread/ERROR]: Encountered an unexpected exception
java.lang.NullPointerException: Ticking entity
-- Entity being ticked --
Details:
Entity Type: minecraft:cow (net.minecraft.world.entity.animal.Cow)
Entity's Exact location: 123.45, 64.00, -67.89
@@ -0,0 +1,39 @@
# 规则库 · 游戏实体/方块(2 条)
# 移植自 HMCL CrashReportAnalyzer.Rule 的实体/方块组(analyse/02 §7.3 第 9 组):
# 崩在 tick 时能拿到实体类型/方块类型与坐标,是「哪个 mod 的实体坏了」的直接线索。
version: 1
category: world
rules:
- id: world.entity_tick
severity: critical
scope: file
pattern: "Entity Type: (?<type>.*)[\\w\\W\\n\\r]*?Entity's Exact location: (?<location>.*)"
confidence: 0.75
priority: 70
cause:
key: diagnose.cause.world.entity_tick
summary: '崩在实体 tick:实体 {type}(坐标 {location})'
detail: '看看这个实体来自哪个 mod(血条/怪物增强/刷怪类 mod 居多),禁用该 mod 或删掉存档里这个实体'
fix_action:
- kind: remove_mod
action: disable
note: '按实体类型定位 mod'
sample: samples/world/entity_tick.log
source: 'HMCL CrashReportAnalyzer.Rule.ENTITY'
- id: world.block_model
severity: warning
scope: file
pattern: 'Block: (?<type>.*)[\w\W\n\r]*?Block location: (?<location>.*)'
confidence: 0.65
priority: 65
cause:
key: diagnose.cause.world.block_model
summary: '崩在方块({type},坐标 {location}):多半是方块模型/贴图相关 mod 的问题'
detail: '定位该方块所属 mod;资源包也可能导致(先换回原版资源包验证)'
fix_action:
- kind: remove_mod
action: disable
note: '按方块类型定位 mod / 先换回原版资源包'
sample: samples/world/block_model.log
source: 'HMCL CrashReportAnalyzer.Rule.BLOCK'
+319
View File
@@ -0,0 +1,319 @@
//! 诊断规则模型:YAML schema + 编译后的规则。
//!
//! 设计定稿:`DevDocs/PlanDoc/06-launch-and-diagnostics.md` §2.2–§2.3;
//! 规则来源:HMCL `CrashReportAnalyzer.Rule` 59 条(`DevDocs/analyse/02-launch-pipeline.md` §7.3)。
//! 字段说明与给 M16/M18 的契约见同目录 `CONTRACT.md`。
//!
//! 相对 HMCL「枚举即签名库」的四处升级(analyse/02 §7.10):
//! 1. 规则是**数据**不是代码:加规则只改 `rules.d/*.yaml`,不重编译;
//! 2. 每条规则带 `confidence`/`priority`/`suppress`,可做主因排序(HMCL 只能平铺);
//! 3. 命名捕获组由正则自声明,不再有 `groupNames` 顺序契约;
//! 4. 每条规则绑定六维修复动作,直接成为 agent 工具的参数来源。
use std::collections::BTreeMap;
use std::fmt;
use regex::Regex;
use serde::Deserialize;
// 六维修复动作按体量拆两个文件(本目录文件上限 400 行),从本模块统一导出公开面。
#[path = "fix_action.rs"]
mod fix_action;
#[path = "fix_params.rs"]
mod fix_params;
pub use fix_action::{class_file_to_java_major, FixAction, FixActionKind, MaterializedFixAction};
pub use fix_params::{
AdjustMemory, ChangeGraphics, ChangeJava, ComponentKind, GraphicsSetting, JavaVendor,
ModAction, Reinstall, RemoveMod, SeekHelp,
};
/// 规则分类:HMCL 59 条规则的 10 个根因分组,与 `rules.d/<category>.yaml` 一一对应。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuleCategory {
/// JVM 实现 / Java 版本不兼容。
Jvm,
/// 堆内存不足、系统内存耗尽。
Memory,
/// 显卡驱动 / OpenGL / 分辨率 / 图形覆盖层。
Graphics,
/// 文件被改、安装不完整、原生库缺失。
Integrity,
/// Fabric 的 ModResolutionException 家族。
Fabric,
/// Forge 的依赖解析 / 重复安装 / mod 元数据。
Forge,
/// mod 加载期或运行期崩溃(含 mixin)。
ModRuntime,
/// 具体 mod 与 OptiFine 等成名冲突。
ModConflict,
/// 崩在某个实体 / 方块的 tick。
World,
/// 其它(手动触发调试崩溃等)。
Other,
}
impl RuleCategory {
/// 稳定字符串(也用于诊断输出的 `category` 字段)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Jvm => "jvm",
Self::Memory => "memory",
Self::Graphics => "graphics",
Self::Integrity => "integrity",
Self::Fabric => "fabric",
Self::Forge => "forge",
Self::ModRuntime => "mod_runtime",
Self::ModConflict => "mod_conflict",
Self::World => "world",
Self::Other => "other",
}
}
/// 全部分类(loader 校验规则文件归属、测试遍历用)。
#[must_use]
pub const fn all() -> [Self; 10] {
[
Self::Jvm,
Self::Memory,
Self::Graphics,
Self::Integrity,
Self::Fabric,
Self::Forge,
Self::ModRuntime,
Self::ModConflict,
Self::World,
Self::Other,
]
}
}
impl fmt::Display for RuleCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 结论严重度(UI 配色与「是否弹窗」的依据)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
/// 提示性:能跑但不最佳。
Info,
/// 警告:可疑但结论不确定。
#[default]
Warning,
/// 严重:基本可以确定崩溃原因。
Critical,
}
impl Severity {
/// 稳定字符串(诊断输出用)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Critical => "critical",
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 匹配范围:决定引擎怎么切文本,也是性能开关(analyse/02 §8.3 第 1 条)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchScope {
/// 逐行匹配(默认,绝大多数 Java 异常都是单行签名)。
#[default]
Line,
/// 按空行切块匹配(一段异常 + 上下文)。
Block,
/// 整份文本匹配(仅跨行贪婪模式:mod 解析列表、实体 tick 上下文等)。
File,
}
/// 结论文案(`summary` 支持 `{group}` 占位符,`key` 是稳定的 i18n/AI 引用 id)。
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Cause {
/// 稳定 id,如 `diagnose.cause.jvm.too_old_java`。
pub key: String,
/// 一句话结论(中文,可直接给用户看)。
pub summary: String,
/// 更细的解释 / 处理建议。
#[serde(default)]
pub detail: Option<String>,
}
impl Cause {
/// 代入捕获组后的(结论, 细化说明)。
#[must_use]
pub fn render(&self, groups: &BTreeMap<String, String>) -> (String, Option<String>) {
(
substitute(&self.summary, groups),
self.detail
.as_ref()
.map(|detail| substitute(detail, groups)),
)
}
}
/// 一条规则的 YAML schema。
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Rule {
/// 全局唯一 id(`命名空间.短名`)。
pub id: String,
/// 严重度。
#[serde(default)]
pub severity: Severity,
/// 正则(Rust `regex` 语法,命名组用 `(?<name>…)`)。
pub pattern: String,
/// 匹配范围。
#[serde(default)]
pub scope: MatchScope,
/// 命中可信度 0.0~1.0。
#[serde(default = "default_confidence")]
pub confidence: f32,
/// 主因排序权重 0~100。
#[serde(default = "default_priority")]
pub priority: u8,
/// 命中本规则时把列出的规则降级为次生结论。
#[serde(default)]
pub suppress: Vec<String>,
/// 结论文案。
pub cause: Cause,
/// 六维修复动作。
#[serde(default)]
pub fix_action: Vec<FixAction>,
/// 回归样本路径(相对 `rules.d/`)。
#[serde(default)]
pub sample: Option<String>,
/// 出处(HMCL 枚举名 / issue 链接)。
#[serde(default)]
pub source: Option<String>,
}
fn default_confidence() -> f32 {
0.5
}
fn default_priority() -> u8 {
50
}
/// 编译后的规则(正则只编译一次)。
#[derive(Debug, Clone)]
pub struct CompiledRule {
rule: Rule,
category: RuleCategory,
regex: Regex,
}
impl CompiledRule {
/// 编译一条规则。
pub fn compile(rule: Rule, category: RuleCategory) -> Result<Self, RuleError> {
let regex = Regex::new(&rule.pattern).map_err(|err| RuleError::InvalidPattern {
id: rule.id.clone(),
message: err.to_string(),
})?;
Ok(Self {
rule,
category,
regex,
})
}
/// 原始规则。
#[must_use]
pub const fn rule(&self) -> &Rule {
&self.rule
}
/// 规则 id。
#[must_use]
pub fn id(&self) -> &str {
&self.rule.id
}
/// 所属分类。
#[must_use]
pub const fn category(&self) -> RuleCategory {
self.category
}
/// 匹配范围。
#[must_use]
pub const fn scope(&self) -> MatchScope {
self.rule.scope
}
/// 编译好的正则。
#[must_use]
pub const fn regex(&self) -> &Regex {
&self.regex
}
/// 命名捕获组名(文档/测试用;顺序无关,取代 HMCL 的 `groupNames` 契约)。
#[must_use]
pub fn named_groups(&self) -> Vec<&str> {
self.regex.capture_names().flatten().collect()
}
}
/// 规则编译错误。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RuleError {
/// 正则非法(规则 id + 正则库报错原文)。
InvalidPattern {
/// 出错的规则 id。
id: String,
/// 正则库报错。
message: String,
},
}
impl fmt::Display for RuleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidPattern { id, message } => {
write!(f, "规则 {id} 的正则非法:{message}")
}
}
}
}
impl std::error::Error for RuleError {}
/// 把 `{group}` 占位符替换成捕获组值;未知占位符原样保留(便于发现规则写错)。
#[must_use]
pub fn substitute(template: &str, groups: &BTreeMap<String, String>) -> String {
if !template.contains('{') {
return template.to_owned();
}
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(start) = rest.find('{') {
let Some(close) = rest[start..].find('}') else {
break;
};
let end = start + close;
let name = &rest[start + 1..end];
out.push_str(&rest[..start]);
match groups.get(name) {
Some(value) => out.push_str(value),
None => out.push_str(&rest[start..=end]),
}
rest = &rest[end + 1..];
}
out.push_str(rest);
out
}
+396
View File
@@ -0,0 +1,396 @@
//! 采集到的原子信号:日志环形缓冲 + 退出信号。
//!
//! 五类信号里的「退出码」和「日志环形缓冲」两种(analyse/02 §7.1 信号 A/B)。
//! 环境快照(信号 E)与 mod 清单在 [`crate::diagnose::collectors`] 里,因为它带采集动作。
//!
//! 环形缓冲相对 HMCL `Log.java`(固定 2000 行、一律淘汰)的升级:
//! 命中 `Exception|Error|Caused by|OutOfMemory` 的行**永久保留**,
//! 解决「崩在启动早期、根因被后续输出挤出缓冲区」(analyse/02 §7.10-3)。
use std::collections::VecDeque;
use std::fmt;
/// 一条日志文本的来源(诊断输出的 `evidence[]` 前缀也是它)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SignalOrigin {
/// 游戏进程 stdout。
Stdout,
/// 游戏进程 stderr。
Stderr,
/// `logs/latest.log`(内存缓冲不够长时的兜底)。
LatestLog,
/// `crash-reports/crash-*.txt`。
CrashReport,
/// `hs_err_pid*.log` 的摘要文本。
HsErr,
/// 启动器自身日志 / 结构化事件(`cmd_built`、`assembly_failed` 等)。
Launcher,
}
impl SignalOrigin {
/// 稳定字符串(evidence 前缀 / 序列化用)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Stdout => "stdout",
Self::Stderr => "stderr",
Self::LatestLog => "latest_log",
Self::CrashReport => "crash_report",
Self::HsErr => "hs_err",
Self::Launcher => "launcher",
}
}
/// `launch` 侧 `LineSink` 的 `stream` 字段映射(`stdout` / `stderr`)。
#[must_use]
pub fn from_stream_name(stream: &str) -> Self {
match stream.trim().to_ascii_lowercase().as_str() {
"stderr" => Self::Stderr,
_ => Self::Stdout,
}
}
/// 取证优先级:同一规则在多个来源命中时保留 rank 最小者(崩溃报告 > hs_err > 日志)。
#[must_use]
pub const fn evidence_rank(self) -> u8 {
match self {
Self::CrashReport => 0,
Self::HsErr => 1,
Self::Launcher => 2,
Self::LatestLog => 3,
Self::Stdout => 4,
Self::Stderr => 5,
}
}
}
impl fmt::Display for SignalOrigin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// 环形缓冲里的一行。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogLine {
/// 来源。
pub origin: SignalOrigin,
/// 行文本(不含行尾换行)。
pub text: String,
}
impl LogLine {
/// 构造一行。
#[must_use]
pub fn new(origin: SignalOrigin, text: impl Into<String>) -> Self {
Self {
origin,
text: text.into(),
}
}
}
/// 是否「关键行」:不随环形淘汰(HMCL 局限 3 的正面修正)。
#[must_use]
pub fn is_key_line(text: &str) -> bool {
const MARKERS: [&str; 4] = ["Exception", "Error", "Caused by", "OutOfMemory"];
MARKERS.iter().any(|marker| text.contains(marker))
}
/// 日志环形缓冲:最近 N 行 + 被判为关键行的历史永久保留。
#[derive(Debug, Clone)]
pub struct LogBuffer {
capacity: usize,
key_capacity: usize,
recent: VecDeque<LogLine>,
/// 已从环形区淘汰、但因为是关键行而留下来的行(按时间顺序)。
kept: VecDeque<LogLine>,
pushed: usize,
dropped: usize,
}
impl Default for LogBuffer {
fn default() -> Self {
Self::new(2000)
}
}
impl LogBuffer {
/// 默认容量 2000 行(与 HMCL 的 `Log` 一致)。
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
key_capacity: capacity.max(1),
recent: VecDeque::new(),
kept: VecDeque::new(),
pushed: 0,
dropped: 0,
}
}
/// 关键行保留上限(默认 = 环形容量)。
#[must_use]
pub fn with_key_capacity(mut self, key_capacity: usize) -> Self {
self.key_capacity = key_capacity.max(1);
self
}
/// 记一行(`launch` 的 stdout/stderr 事件逐行喂进来)。
pub fn push(&mut self, origin: SignalOrigin, line: &str) {
let text = line.trim_end_matches(['\r', '\n']).to_owned();
self.pushed += 1;
if self.recent.len() >= self.capacity {
if let Some(evicted) = self.recent.pop_front() {
self.dropped += 1;
if is_key_line(&evicted.text) {
if self.kept.len() >= self.key_capacity {
self.kept.pop_front();
}
self.kept.push_back(evicted);
}
}
}
self.recent.push_back(LogLine::new(origin, text));
}
/// 一次性喂多行(导入日志、`latest.log` 兜底)。
pub fn extend<'a, I>(&mut self, origin: SignalOrigin, lines: I)
where
I: IntoIterator<Item = &'a str>,
{
for line in lines {
self.push(origin, line);
}
}
/// 按时间顺序返回全部保留行(关键行历史在前,最近 N 行在后)。
pub fn lines(&self) -> impl Iterator<Item = &LogLine> {
self.kept.iter().chain(self.recent.iter())
}
/// 拼成可供规则匹配的文本(`\n` 连接)。
#[must_use]
pub fn text(&self) -> String {
let mut out = String::with_capacity(self.byte_len());
for line in self.lines() {
out.push_str(&line.text);
out.push('\n');
}
out
}
/// 当前保留行数。
#[must_use]
pub fn len(&self) -> usize {
self.kept.len() + self.recent.len()
}
/// 是否一行都没有。
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// 一共推入多少行。
#[must_use]
pub const fn pushed(&self) -> usize {
self.pushed
}
/// 被淘汰多少行(不含被永久保留的关键行)。
#[must_use]
pub const fn dropped(&self) -> usize {
self.dropped
}
/// 因关键行规则而保留下来的历史行数。
#[must_use]
pub fn kept_key_lines(&self) -> usize {
self.kept.len()
}
fn byte_len(&self) -> usize {
self.lines().map(|line| line.text.len() + 1).sum()
}
}
/// 退出类型。字符串值与 `launch-接口约定.md` §3 `exit.exit_type` 完全一致。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum ExitType {
/// 正常退出(但可能是静默失败,见 [`ExitSignal::silent_failure`])。
#[default]
Normal,
/// 应用层崩溃(有崩溃关键字 / 非 0 退出码)。
ApplicationError,
/// JVM 没起来(`-Xmx` 过大、参数非法、JVM 致命错误)。
JvmError,
/// 被 SIGKILL(Linux 137 / 系统 OOM killer)。
Sigkill,
/// 被 SIGTERM(143)。
Sigterm,
/// 被启动器主动停止(用户取消 / watchdog)。
Killed,
/// 等待被中断(会话被丢弃)。
Interrupted,
}
impl ExitType {
/// 稳定字符串(与 launch 约定同值)。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::ApplicationError => "application_error",
Self::JvmError => "jvm_error",
Self::Sigkill => "sigkill",
Self::Sigterm => "sigterm",
Self::Killed => "killed",
Self::Interrupted => "interrupted",
}
}
/// 从 launch 约定字符串反解(未知值回落 `ApplicationError`,绝不 panic)。
#[must_use]
pub fn from_name(name: &str) -> Self {
match name.trim().to_ascii_lowercase().as_str() {
"normal" => Self::Normal,
"jvm_error" => Self::JvmError,
"sigkill" => Self::Sigkill,
"sigterm" => Self::Sigterm,
"killed" => Self::Killed,
"interrupted" => Self::Interrupted,
_ => Self::ApplicationError,
}
}
/// 是否异常结束(非 `Normal`)。
#[must_use]
pub const fn is_abnormal(self) -> bool {
!matches!(self, Self::Normal)
}
}
impl fmt::Display for ExitType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// JVM 没起来的三个签名(analyse/02 §7.2,`JVM_ERROR` 判定依据)。
pub const JVM_ERROR_KEYWORDS: [&str; 3] = [
"Could not create the Java Virtual Machine",
"Error occurred during initialization of VM",
"A fatal exception has occurred. Program will exit.",
];
/// 应用层崩溃关键字(HMCL `ExitWaiter` 的 5 条 + 我们能拿到的补充)。
pub const CRASH_KEYWORDS: [&str; 5] = [
"Game crashed! Crash report saved to",
"Exception in thread",
"Unhandled exception",
"Unable to launch",
"An exception was thrown, the game will display an error screen and halt",
];
/// Windows 常见退出码(`0xC0000005` 等按 `i32` 表示,analyse/02 §8.4-2)。
pub const EXIT_ACCESS_VIOLATION: i32 = -1_073_741_819;
/// `0xC0000409` STACK_BUFFER_OVERRUN。
pub const EXIT_STACK_BUFFER_OVERRUN: i32 = -1_073_740_791;
/// `0xC0000135` DLL_NOT_FOUND。
pub const EXIT_DLL_NOT_FOUND: i32 = -1_073_741_515;
/// POSIX SIGKILL(128 + 9)。
pub const EXIT_SIGKILL: i32 = 137;
/// POSIX SIGTERM(128 + 15)。
pub const EXIT_SIGTERM: i32 = 143;
/// 进程退出信号(`launch` 的 `exit` 事件映射过来;离线诊断时由 [`ExitSignal::classify`] 反推)。
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ExitSignal {
/// 退出码(被信号杀死时为负或 128+N)。
pub code: Option<i32>,
/// 退出类型。
pub exit_type: ExitType,
/// 进程存活毫秒数。
pub uptime_ms: Option<u64>,
/// stderr/错误行条数。
pub error_lines: u32,
/// 退出码 0 但疑似静默失败(存活 <30s 或 stderr 有错误特征)。
pub silent_failure: bool,
/// Windows 状态码原始串(`0xc0000005`)。
pub windows_status: Option<String>,
/// 命中的崩溃关键字。
pub crash_keywords: Vec<String>,
}
impl ExitSignal {
/// 从原始退出事实推断类型(离线诊断 / 导入日志用;launch 侧有权威版本)。
#[must_use]
pub fn classify(
code: Option<i32>,
uptime_ms: Option<u64>,
error_lines: u32,
error_text: &str,
) -> Self {
let has = |keywords: &[&str]| keywords.iter().any(|kw| error_text.contains(kw));
let mut crash_keywords = Vec::new();
for keyword in CRASH_KEYWORDS {
if error_text.contains(keyword) {
crash_keywords.push(keyword.to_owned());
}
}
let exit_type = match code {
Some(EXIT_SIGKILL) => ExitType::Sigkill,
Some(EXIT_SIGTERM) => ExitType::Sigterm,
// 与 HMCL `ExitType` 一致:只有「非 0 退出码 + JVM 关键字」才算 JVM_ERROR。
// exit 0 却打出这些关键字的场景归到靜默失败(silent_failure)而不是 JVM 错误。
Some(0) => ExitType::Normal,
Some(_) if has(&JVM_ERROR_KEYWORDS) => ExitType::JvmError,
Some(_) => ExitType::ApplicationError,
None => ExitType::ApplicationError,
};
let short_lived = uptime_ms.is_some_and(|ms| ms < 30_000);
let silent_failure = exit_type == ExitType::Normal
&& (short_lived || error_lines > 0 || has(&CRASH_KEYWORDS));
Self {
code,
exit_type,
uptime_ms,
error_lines,
silent_failure,
windows_status: code.and_then(hex_status),
crash_keywords,
}
}
/// Windows 状态码的十六进制串(如 `0xc0000005`)。
#[must_use]
pub fn status_hex(&self) -> Option<String> {
self.windows_status
.clone()
.or_else(|| self.code.and_then(hex_status))
}
/// 退出码是否是 Windows 访问违例(`0xC0000005`)。
#[must_use]
pub fn is_access_violation(&self) -> bool {
self.code == Some(EXIT_ACCESS_VIOLATION)
}
/// 一句话摘要(写进诊断报告 / 证据行)。
#[must_use]
pub fn summary(&self) -> String {
let code = self
.code
.map_or_else(|| "未知".to_owned(), |code| code.to_string());
let uptime = self
.uptime_ms
.map_or_else(String::new, |ms| format!(",存活 {} ms", ms));
format!("退出码 {code}({}){uptime}", self.exit_type)
}
}
fn hex_status(code: i32) -> Option<String> {
// 只有 Windows 的 NTSTATUS 形态(负数高位为 1)才有意义。
(code < 0).then(|| format!("0x{:08x}", code as u32))
}
+299
View File
@@ -0,0 +1,299 @@
//! 五类信号的具体采集器(推送类 2 个 + 磁盘类 3 个)。
//!
//! 由 `collectors.rs` 用 `#[path]` 声明并统一导出;拆分只为守单文件 400 行上限。
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::diagnose::hs_err;
use super::{
CapturedText, CollectError, CollectedSources, CollectionContext, EnvironmentSnapshot,
ExitSignal, SignalBundle, SignalCollector, SignalKind, SignalOrigin,
};
/// 推送类:退出信号(launch 的 `exit` 事件已算好)。
#[derive(Debug, Clone)]
pub struct ExitCollector {
signal: ExitSignal,
}
impl ExitCollector {
/// 构造。
#[must_use]
pub fn new(signal: ExitSignal) -> Self {
Self { signal }
}
}
impl SignalCollector for ExitCollector {
fn kind(&self) -> SignalKind {
SignalKind::Exit
}
fn collect(
&self,
_ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
out.exit = Some(self.signal.clone());
Ok(CollectedSources::new())
}
}
/// 推送类:环境快照 + mod 清单(launch/game 侧已有数据)。
#[derive(Debug, Clone, Default)]
pub struct EnvironmentCollector {
snapshot: EnvironmentSnapshot,
}
impl EnvironmentCollector {
/// 构造。
#[must_use]
pub fn new(snapshot: EnvironmentSnapshot) -> Self {
Self { snapshot }
}
}
impl SignalCollector for EnvironmentCollector {
fn kind(&self) -> SignalKind {
SignalKind::Environment
}
fn collect(
&self,
_ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
out.mods = self.snapshot.mods.clone();
out.environment = Some(self.snapshot.clone());
Ok(CollectedSources::new())
}
}
/// 磁盘类:`crash-reports/crash-*.txt`(双通道里的「权威全文」)。
#[derive(Debug, Clone, Default)]
pub struct CrashReportCollector {
dir: Option<PathBuf>,
}
impl CrashReportCollector {
/// 默认扫 `<run_dir>/crash-reports`。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 指定目录。
#[must_use]
pub fn with_dir(dir: impl Into<PathBuf>) -> Self {
Self {
dir: Some(dir.into()),
}
}
}
impl SignalCollector for CrashReportCollector {
fn kind(&self) -> SignalKind {
SignalKind::CrashReport
}
fn collect(
&self,
ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
let dir = self
.dir
.clone()
.unwrap_or_else(|| ctx.run_dir.join("crash-reports"));
let Some(path) = newest_file(&dir, ctx.started_at, |name| {
name.starts_with("crash-") && name.ends_with(".txt")
}) else {
return Ok(CollectedSources::new());
};
let text = read_lossy(SignalKind::CrashReport, &path)?;
let modified = modified_at(&path);
let mut sources = CollectedSources::new();
sources.push(&path);
out.sources.push(path.clone());
out.crash_report = Some(CapturedText {
path,
text,
modified,
});
Ok(sources)
}
}
/// 磁盘类:`hs_err_pid*.log`(HMCL 的空白区,显卡驱动类崩溃的唯一权威证据)。
#[derive(Debug, Clone, Default)]
pub struct HsErrCollector {
dirs: Vec<PathBuf>,
}
impl HsErrCollector {
/// 默认扫 `<run_dir>`(JVM 把 hs_err 写在进程工作目录)。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 指定候选目录表(**整表替换**,不是追加;给出后不再自动扫 `run_dir`)。
///
/// 典型用法:`HsErrCollector::with_dirs([run_dir.clone(), home_dir])`。
#[must_use]
pub fn with_dirs(dirs: impl IntoIterator<Item = PathBuf>) -> Self {
Self {
dirs: dirs.into_iter().collect(),
}
}
}
impl SignalCollector for HsErrCollector {
fn kind(&self) -> SignalKind {
SignalKind::HsErr
}
fn collect(
&self,
ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
let dirs = if self.dirs.is_empty() {
vec![ctx.run_dir.clone()]
} else {
self.dirs.clone()
};
let mut newest: Option<PathBuf> = None;
for dir in dirs {
if let Some(path) = newest_file(&dir, ctx.started_at, |name| {
name.starts_with("hs_err_pid") && name.ends_with(".log")
}) {
newest = match newest {
Some(current) => match (modified_at(&current), modified_at(&path)) {
(Some(a), Some(b)) if b <= a => Some(current),
_ => Some(path),
},
None => Some(path),
};
}
}
let Some(path) = newest else {
return Ok(CollectedSources::new());
};
let report = hs_err::parse_file(&path).map_err(|error| CollectError {
kind: SignalKind::HsErr,
path: Some(path.clone()),
message: error.to_string(),
})?;
let mut sources = CollectedSources::new();
sources.push(&path);
out.sources.push(path);
out.hs_err = Some(report);
Ok(sources)
}
}
/// 磁盘类:`logs/latest.log` 尾巴(内存缓冲不够长时的兜底)。
#[derive(Debug, Clone)]
pub struct LatestLogCollector {
path: Option<PathBuf>,
max_lines: usize,
}
impl Default for LatestLogCollector {
fn default() -> Self {
Self::new()
}
}
impl LatestLogCollector {
/// 默认读 `<run_dir>/logs/latest.log` 的最后 5000 行。
#[must_use]
pub fn new() -> Self {
Self {
path: None,
max_lines: 5000,
}
}
/// 保留行数上限。
#[must_use]
pub fn with_max_lines(mut self, max_lines: usize) -> Self {
self.max_lines = max_lines.max(1);
self
}
/// 指定文件。
#[must_use]
pub fn with_path(path: impl Into<PathBuf>) -> Self {
Self {
path: Some(path.into()),
max_lines: 5000,
}
}
}
impl SignalCollector for LatestLogCollector {
fn kind(&self) -> SignalKind {
SignalKind::Log
}
fn collect(
&self,
ctx: &CollectionContext,
out: &mut SignalBundle,
) -> Result<CollectedSources, CollectError> {
let path = self
.path
.clone()
.unwrap_or_else(|| ctx.run_dir.join("logs").join("latest.log"));
if !path.is_file() {
return Ok(CollectedSources::new());
}
let text = read_lossy(SignalKind::Log, &path)?;
let lines = text.lines().collect::<Vec<_>>();
let start = lines.len().saturating_sub(self.max_lines);
out.log
.extend(SignalOrigin::LatestLog, lines[start..].iter().copied());
let mut sources = CollectedSources::new();
sources.push(&path);
out.sources.push(path);
Ok(sources)
}
}
fn newest_file(
dir: &Path,
since: Option<SystemTime>,
matches: impl Fn(&str) -> bool,
) -> Option<PathBuf> {
let entries = fs::read_dir(dir).ok()?;
entries
.flatten()
.filter_map(|entry| {
let name = entry.file_name().to_string_lossy().into_owned();
if !matches(&name) {
return None;
}
let path = entry.path();
let modified = modified_at(&path)?;
if since.is_some_and(|since| modified < since) {
return None;
}
Some((modified, path))
})
.max_by(|a, b| a.0.cmp(&b.0))
.map(|(_, path)| path)
}
fn modified_at(path: &Path) -> Option<SystemTime> {
fs::metadata(path).ok()?.modified().ok()
}
fn read_lossy(kind: SignalKind, path: &Path) -> Result<String, CollectError> {
let bytes = fs::read(path).map_err(|error| CollectError::io(kind, path, &error))?;
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
+279
View File
@@ -0,0 +1,279 @@
//! 引擎内置结论:退出码 / hs_err 这类**结构化信号**没有文本签名,塞 YAML 别扭,放在这里。
//!
//! 它们参与和其它规则一样的排序与抑制流程(`priority`/`confidence`/`suppress` 语义相同),
//! 所以「主因是驱动、次生是图形规则」这种关系能自动成立。
//! 由 `engine.rs` 用 `#[path]` 声明,拆分只为守单文件 400 行上限。
use super::{DiagnosisInput, Evidence, Finding};
use crate::diagnose::collectors::{
ExitSignal, ExitType, SignalOrigin, EXIT_DLL_NOT_FOUND, EXIT_STACK_BUFFER_OVERRUN,
};
use crate::diagnose::hs_err::{HsErrKind, HsErrReport};
use crate::diagnose::rules::{
AdjustMemory, ChangeGraphics, ChangeJava, ComponentKind, FixAction, GraphicsSetting,
JavaVendor, Reinstall, RuleCategory, SeekHelp, Severity,
};
/// 所有内置结论。
#[must_use]
pub fn synthetic_findings(input: &DiagnosisInput) -> Vec<Finding> {
let mut out = Vec::new();
if let Some(exit) = &input.exit {
out.extend(exit_findings(exit));
}
if let Some(report) = &input.hs_err {
out.extend(hs_err_findings(report));
}
out
}
fn exit_findings(exit: &ExitSignal) -> Vec<Finding> {
let mut out = Vec::new();
if exit.is_access_violation() {
let mut finding = base(
"signal.access_violation",
RuleCategory::Graphics,
Severity::Critical,
0.55,
82,
"diagnose.cause.signal.access_violation",
&format!(
"进程以 0xC0000005(访问违例)退出:多半是显卡驱动/渲染栈崩溃,建议先更新显卡驱动或换渲染器({})",
exit.summary()
),
);
finding.fix_actions = vec![FixAction::ChangeGraphics(ChangeGraphics {
setting: GraphicsSetting::Driver,
update_driver: true,
suggested_value: None,
note: Some("访问违例优先怀疑显卡驱动".to_owned()),
})];
finding.suppress = vec!["graphics.graphics_driver".to_owned()];
finding.evidence = vec![Evidence::new(SignalOrigin::Launcher, 0, exit.summary())];
out.push(finding);
}
if exit.code == Some(EXIT_DLL_NOT_FOUND) {
let mut finding = base(
"signal.dll_not_found",
RuleCategory::Integrity,
Severity::Critical,
0.7,
85,
"diagnose.cause.signal.dll_not_found",
"进程以 0xC0000135(找不到 DLL)退出:缺原生库或 Java 安装损坏,重装 natives/Java 组件",
);
finding.fix_actions = vec![FixAction::Reinstall(Reinstall {
component: ComponentKind::Natives,
path_hint: None,
note: Some("找不到 DLL:优先修 natives,其次重装 Java".to_owned()),
})];
finding.evidence = vec![Evidence::new(SignalOrigin::Launcher, 0, exit.summary())];
out.push(finding);
}
if exit.code == Some(EXIT_STACK_BUFFER_OVERRUN) {
let mut finding = base(
"signal.stack_buffer_overrun",
RuleCategory::Jvm,
Severity::Warning,
0.5,
60,
"diagnose.cause.signal.stack_buffer_overrun",
"进程以 0xC0000409(栈缓冲区溢出)退出:JVM/原生库内部错误,需要 hs_err 或崩溃报告进一步判断",
);
finding.fix_actions = vec![FixAction::SeekHelp(SeekHelp {
export_diagnostics: true,
needs_more_info: vec!["run 目录下的 hs_err_pid*.log".to_owned()],
note: None,
})];
finding.evidence = vec![Evidence::new(SignalOrigin::Launcher, 0, exit.summary())];
out.push(finding);
}
if exit.exit_type == ExitType::Sigkill {
let mut finding = base(
"signal.sigkill",
RuleCategory::Memory,
Severity::Warning,
0.6,
70,
"diagnose.cause.signal.sigkill",
"进程被 SIGKILL 强杀:常见于系统内存耗尽(OOM killer)或外部清理进程,建议降低 -Xmx 或扩大虚拟内存",
);
finding.fix_actions = vec![FixAction::AdjustMemory(AdjustMemory {
increase_system_swap: true,
note: Some("被强杀:先确认系统内存是否够".to_owned()),
..AdjustMemory::default()
})];
finding.evidence = vec![Evidence::new(SignalOrigin::Launcher, 0, exit.summary())];
out.push(finding);
}
if exit.exit_type == ExitType::JvmError {
let mut finding = base(
"signal.jvm_error",
RuleCategory::Jvm,
Severity::Critical,
0.55,
72,
"diagnose.cause.signal.jvm_error",
"JVM 未能启动(游戏根本没跑起来):先查 -Xmx 是否超过可用内存、JVM 参数是否非法",
);
finding.fix_actions = vec![
FixAction::AdjustMemory(AdjustMemory {
increase_system_swap: true,
note: Some("JVM 初始化失败,优先怀疑 -Xmx 过大".to_owned()),
..AdjustMemory::default()
}),
FixAction::ChangeJava(ChangeJava {
vendor: JavaVendor::Hotspot,
note: Some("换成 HotSpot 的官方 Java 试试".to_owned()),
..ChangeJava::default()
}),
];
finding.evidence = vec![Evidence::new(SignalOrigin::Launcher, 0, exit.summary())];
out.push(finding);
}
if exit.silent_failure {
let mut finding = base(
"signal.silent_failure",
RuleCategory::Other,
Severity::Warning,
0.4,
55,
"diagnose.cause.signal.silent_failure",
&format!(
"退出码为 0 但疑似静默失败({} 条错误行):游戏可能刚起来就被 mod 异常终止,需要更长的日志确认",
exit.error_lines
),
);
finding.fix_actions = vec![FixAction::SeekHelp(SeekHelp {
export_diagnostics: true,
needs_more_info: vec!["把日志行数上限调到 20000 后复现一次".to_owned()],
note: None,
})];
finding.evidence = vec![Evidence::new(SignalOrigin::Launcher, 0, exit.summary())];
out.push(finding);
}
out
}
fn hs_err_findings(report: &HsErrReport) -> Vec<Finding> {
let mut out = Vec::new();
match report.kind {
HsErrKind::AccessViolation => {
let module = report
.failed_frame
.as_ref()
.and_then(|frame| frame.module.clone())
.unwrap_or_else(|| "未知模块".to_owned());
let mut finding = base(
"hs_err.access_violation",
RuleCategory::Graphics,
Severity::Critical,
0.75,
88,
"diagnose.cause.hs_err.access_violation",
&format!(
"JVM 致命错误 {module}(访问违例,失败帧 {}):原生层崩溃,显卡驱动/渲染栈是首要嫌疑",
report
.failed_frame
.as_ref()
.map_or("未知", |frame| frame.raw.as_str())
),
);
finding.fix_actions = vec![
FixAction::ChangeGraphics(ChangeGraphics {
setting: GraphicsSetting::Driver,
update_driver: true,
suggested_value: None,
note: Some(format!("失败帧落在 {module}")),
}),
FixAction::SeekHelp(SeekHelp {
export_diagnostics: true,
needs_more_info: Vec::new(),
note: Some("把 hs_err 一并上报,便于确认驱动版本".to_owned()),
}),
];
finding.suppress = vec!["graphics.graphics_driver".to_owned()];
finding.evidence = vec![Evidence::new(SignalOrigin::HsErr, 1, report.summary())];
out.push(finding);
}
HsErrKind::OutOfMemory => {
let memory = report
.memory
.as_ref()
.map_or_else(String::new, |memory| memory.summary());
let mut finding = base(
"hs_err.out_of_memory",
RuleCategory::Memory,
Severity::Critical,
0.85,
86,
"diagnose.cause.hs_err.out_of_memory",
&format!("JVM 因内存不足终止:{memory},降低 -Xmx 或扩大系统虚拟内存"),
);
finding.fix_actions = vec![FixAction::AdjustMemory(AdjustMemory {
increase_system_swap: true,
note: Some(memory),
..AdjustMemory::default()
})];
finding.evidence = vec![Evidence::new(SignalOrigin::HsErr, 1, report.summary())];
out.push(finding);
}
HsErrKind::StackOverflow => {
let mut finding = base(
"hs_err.stack_overflow",
RuleCategory::ModRuntime,
Severity::Warning,
0.6,
62,
"diagnose.cause.hs_err.stack_overflow",
"JVM 栈溢出导致的致命错误:通常是某个 mod 的递归/死循环,禁用最近装的 mod 再试",
);
finding.fix_actions = vec![FixAction::SeekHelp(SeekHelp {
export_diagnostics: true,
needs_more_info: vec!["最近安装/更新的 mod 列表".to_owned()],
note: None,
})];
finding.evidence = vec![Evidence::new(SignalOrigin::HsErr, 1, report.summary())];
out.push(finding);
}
HsErrKind::InternalError => {
let mut finding = base(
"hs_err.internal_error",
RuleCategory::Jvm,
Severity::Warning,
0.5,
62,
"diagnose.cause.hs_err.internal_error",
"JVM 内部致命错误:优先换官方 HotSpot Java,其次怀疑原生库(natives)损坏",
);
finding.fix_actions = vec![FixAction::ChangeJava(ChangeJava {
vendor: JavaVendor::Hotspot,
..ChangeJava::default()
})];
finding.evidence = vec![Evidence::new(SignalOrigin::HsErr, 1, report.summary())];
out.push(finding);
}
HsErrKind::Other => {}
}
out
}
fn base(
rule_id: &str,
category: RuleCategory,
severity: Severity,
confidence: f32,
priority: u8,
message_key: &str,
message: &str,
) -> Finding {
Finding::skeleton(
rule_id,
category,
severity,
confidence,
priority,
message_key,
message,
)
}