From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Modrinth Enhanced Date: Tue, 15 Sep 2026 09:42:35 +0200 Subject: [PATCH] Explain what went wrong when the game crashed The Logs tab now says what a crash was, without a connection. After a run ends, the launcher reads the game's crash report, the JVM's own hs_err file and the end of the log, and matches them against rules for the usual causes: out of memory, the wrong Java version, missing or duplicate mods, mods for another game version, failed mixins, a software renderer and more. Each finding gets a plain explanation, the line it was found on, and where it helps a button to the Java settings or the instance folder. Picking an older log from the list runs the same rules against it. Files from an earlier run than the log are ignored, so an old crash report is not blamed for a new crash. The rules report an id and what they captured; the words are in the frontend, so they can be translated. mclo.gs analysis in the console is unchanged. Adapted from Noctrinth (https://github.com/Everelsu/Noctrinth). --- .../src/components/ui/CrashDiagnosis.vue | 641 ++++++++++ .../src/helpers/crash-analysis.ts | 84 ++ .../src/pages/instance/logs/index.vue | 44 +- apps/app/build.rs | 11 + apps/app/capabilities/plugins.json | 3 +- apps/app/src/api/crash_analysis.rs | 37 + apps/app/src/api/mod.rs | 1 + apps/app/src/main.rs | 1 + packages/app-lib/src/api/crash_analysis.rs | 1087 +++++++++++++++++ packages/app-lib/src/api/mod.rs | 1 + 10 files changed, 1908 insertions(+), 2 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/CrashDiagnosis.vue create mode 100644 apps/app-frontend/src/helpers/crash-analysis.ts create mode 100644 apps/app/src/api/crash_analysis.rs create mode 100644 packages/app-lib/src/api/crash_analysis.rs diff --git a/apps/app-frontend/src/components/ui/CrashDiagnosis.vue b/apps/app-frontend/src/components/ui/CrashDiagnosis.vue new file mode 100644 index 0000000..26b04eb --- /dev/null +++ b/apps/app-frontend/src/components/ui/CrashDiagnosis.vue @@ -0,0 +1,641 @@ + + + diff --git a/apps/app-frontend/src/helpers/crash-analysis.ts b/apps/app-frontend/src/helpers/crash-analysis.ts new file mode 100644 index 0000000..1c8a37f --- /dev/null +++ b/apps/app-frontend/src/helpers/crash-analysis.ts @@ -0,0 +1,84 @@ +/** + * The launcher's own reading of a crash. + * + * Upstream sends the live console to mclo.gs and shows what it makes of it, + * which is the better answer for anything unusual — and no answer at all + * without a connection, or for the crash report and the JVM's own error file, + * which is where the cause is usually written in plain sight. + * + * This is the local half: rules that run against those files as well, on this + * machine, and fixes that are pages of this launcher rather than advice. See + * `packages/app-lib/src/api/crash_analysis.rs` for why the words a player reads + * are not in the Rust. + */ +import { invoke } from '@tauri-apps/api/core' + +export type CrashSeverity = 'note' | 'warning' | 'critical' +export type CrashSourceKind = 'crash_report' | 'log' | 'jvm_error' + +export interface CrashFinding { + rule: string + severity: CrashSeverity + source: CrashSourceKind + source_name: string + evidence: string + values: Record +} + +export interface CrashSourceFile { + kind: CrashSourceKind + name: string + modified: number +} + +export interface CrashDiagnosis { + findings: CrashFinding[] + sources: CrashSourceFile[] +} + +const EMPTY: CrashDiagnosis = { findings: [], sources: [] } + +/** Reads the files the last run left behind and reports what it recognised. */ +export async function analyzeInstanceCrash(instanceId: string): Promise { + try { + return await invoke('plugin:crash-analysis|crash_analyze_instance', { + instanceId, + }) + } catch (error) { + // A diagnosis that cannot be made is not worth a notification: the log + // the player came here to read is on screen either way. + console.warn('Could not read the crash files for a diagnosis', error) + return EMPTY + } +} + +/** The same rules, against text already in hand. */ +export async function analyzeCrashText( + text: string, + kind: CrashSourceKind, + sourceName: string, +): Promise { + if (!text.trim()) return EMPTY + + try { + return await invoke('plugin:crash-analysis|crash_analyze_text', { + text, + kind, + sourceName, + }) + } catch (error) { + console.warn('Could not analyse the log', error) + return EMPTY + } +} + +/** Which of two diagnoses to lead with, worst first. */ +const SEVERITY_ORDER: Record = { + critical: 0, + warning: 1, + note: 2, +} + +export function sortFindings(findings: CrashFinding[]): CrashFinding[] { + return [...findings].sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]) +} diff --git a/apps/app-frontend/src/pages/instance/logs/index.vue b/apps/app-frontend/src/pages/instance/logs/index.vue index e2a59fd..4fcddea 100644 --- a/apps/app-frontend/src/pages/instance/logs/index.vue +++ b/apps/app-frontend/src/pages/instance/logs/index.vue @@ -1,5 +1,16 @@