From 93c74f94a11345f8fb7961fdc4f0f3a9f0e5a982 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Mon, 5 Oct 2026 17:20:30 +0000 Subject: [PATCH] Upload via GUI (146 Dateien) --- src/crashhelp.js | 445 ++++++++++++++++++++++++----------------------- src/designs.css | 43 ++++- src/erfolge.js | 73 ++++++++ src/garderobe.js | 106 +++++++++++ 4 files changed, 445 insertions(+), 222 deletions(-) create mode 100644 src/erfolge.js create mode 100644 src/garderobe.js diff --git a/src/crashhelp.js b/src/crashhelp.js index 60116fc..b28f46a 100644 --- a/src/crashhelp.js +++ b/src/crashhelp.js @@ -1,218 +1,227 @@ -'use strict'; - -/* - * crashhelp.js – Absturz-Assistent - * --------------------------------- - * Liest Absturzbericht, latest.log und die letzte Konsolenausgabe eines Starts - * und erkennt die häufigsten Ursachen. Jeder Fund bekommt eine verständliche - * Erklärung und – wo möglich – eine Aktion, die der Launcher mit einem Klick - * ausführen kann: - * - * { typ: 'mod-installieren', projekt, name } fehlende Abhängigkeit von Modrinth holen - * { typ: 'mod-deaktivieren', dateiname, name } Mod abschalten (umbenennen, nichts gelöscht) - * { typ: 'ram', mb } Arbeitsspeicher der Instanz ändern - * { typ: 'java-auto' } passendes Java automatisch wählen lassen - * { typ: 'mods-ordner' } Mods-Ordner öffnen - * - * Bewusst nur Muster, die eindeutig sind – lieber keinen Fund als einen falschen. - */ - -// Mod-IDs, die keine installierbaren Mods sind -const KEINE_MODS = new Set(['minecraft', 'java', 'fabricloader', 'fabric-loader', 'forge', 'neoforge', 'quilt_loader', 'quilt-loader', 'fml', 'mixinextras']); - -// Mod-ID -> Modrinth-Projekt, wo sich beides unterscheidet -const PROJEKT_FUER_ID = { - fabric: 'fabric-api', - 'fabric-api-base': 'fabric-api', - quilted_fabric_api: 'qsl', - cloth_config: 'cloth-config', - 'cloth-config2': 'cloth-config', - architectury: 'architectury-api', - geckolib3: 'geckolib', -}; - -// Java-Hauptversion aus der "class file version" (52 = Java 8, 61 = 17, 65 = 21 …) -const javaAusKlassenversion = (v) => Number(v) - 44; - -function projektFuer(modId) { - const id = String(modId || '').toLowerCase(); - return PROJEKT_FUER_ID[id] || id.replace(/_/g, '-'); -} - -// installierte Mod zu einer ID/einem Namen finden (für "abschalten") -function findeMod(mods, modId, name) { - const id = String(modId || '').toLowerCase(); - const kurz = id.replace(/[-_]/g, ''); - const n = String(name || '').toLowerCase().replace(/[^a-z0-9]/g, ''); - return (mods || []).find((m) => { - if (m.enabled === false) return false; - const datei = String(m.filename || '').toLowerCase(); - const titel = String(m.title || '').toLowerCase().replace(/[^a-z0-9]/g, ''); - return (id && (datei.startsWith(id) || datei.replace(/[-_]/g, '').startsWith(kurz))) - || (n && titel && (titel === n || datei.replace(/[^a-z0-9]/g, '').startsWith(n))); - }) || null; -} - -/* - * text: zusammengeführter Text aus Bericht, Log und Konsole - * ctx: { loader, version, maxMemMb, gesamtMb, mods: [{ filename, title, projectId, enabled }] } - * -> [{ schluessel, titel, text, aktion? }] - */ -function analysiere(text, ctx = {}) { - const t = String(text || ''); - const funde = []; - const schonDa = new Set(); - const fund = (schluessel, titel, erklaerung, aktion) => { - if (schonDa.has(schluessel)) return; - schonDa.add(schluessel); - funde.push({ schluessel, titel, text: erklaerung, aktion: aktion || null }); - }; - const mods = ctx.mods || []; - - // --- Fehlende Abhängigkeiten (Fabric/Quilt) --- - // "Mod 'Mod Menu' (modmenu) 7.2.2 requires any version of fabric-api, which is missing!" - // neuere Fassung: "... requires any version of mod 'Fabric API' (fabric-api), which is missing!" - for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version ([^\n,]+?)|any version) of (?:mod '([^']+)' \(([\w.-]+)\)|([\w.-]+)), which is missing/gi)) { - const [, name, , version, fehltName, fehltIdNeu, fehltIdAlt] = m; - const fehlt = fehltIdNeu || fehltIdAlt; - if (KEINE_MODS.has(fehlt.toLowerCase())) continue; - const anzeige = fehltName || fehlt; - fund(`fehlt:${fehlt}`, `„${name}“ braucht ${anzeige}`, - `Der Mod „${name}“ funktioniert nur zusammen mit „${anzeige}“${version ? ` (Version ${version.trim()})` : ''} – und der fehlt in dieser Instanz.`, - { typ: 'mod-installieren', projekt: projektFuer(fehlt), name: anzeige }); - } - // Lösungsvorschlag von Fabric: "- Install fabric-api, any version." - for (const m of t.matchAll(/^\s*-\s*Install ([\w.-]+), (?:any version|version [^\n.]+)/gim)) { - const fehlt = m[1]; - if (KEINE_MODS.has(fehlt.toLowerCase())) continue; - fund(`fehlt:${fehlt}`, `Es fehlt „${fehlt}“`, - `Ein installierter Mod braucht „${fehlt}“, der in dieser Instanz nicht installiert ist.`, - { typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt }); - } - - // --- Fehlende Abhängigkeiten (Forge/NeoForge) --- - // "Mod ID: 'architectury', Requested by: 'rei', Expected range: '[9.1.12,)', Actual version: '[MISSING]'" - for (const m of t.matchAll(/Mod ID: '([\w.-]+)', Requested by: '([\w.-]+)', Expected range: '([^']*)', Actual version: '([^']*)'/gi)) { - const [, fehlt, von, bereich, ist] = m; - if (fehlt.toLowerCase() === 'minecraft') { - const mod = findeMod(mods, von); - fund(`mcversion:${von}`, `„${von}“ passt nicht zu dieser Minecraft-Version`, - `Der Mod „${von}“ ist für Minecraft ${bereich} gemacht, deine Instanz hat ${ctx.version || ist}.`, - mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || von } : null); - continue; - } - if (KEINE_MODS.has(fehlt.toLowerCase())) continue; - if (/MISSING/i.test(ist)) { - fund(`fehlt:${fehlt}`, `„${von}“ braucht ${fehlt}`, - `Der Mod „${von}“ funktioniert nur zusammen mit „${fehlt}“ – und der fehlt in dieser Instanz.`, - { typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt }); - } else { - fund(`alt:${fehlt}`, `„${fehlt}“ ist zu alt oder zu neu`, - `„${von}“ braucht „${fehlt}“ in der Version ${bereich}, installiert ist ${ist}. Aktualisiere „${fehlt}“ im Inhalts-Dialog („Nach Updates suchen“).`); - } - } - - // --- Mod für eine andere Minecraft-Version (Fabric) --- - // "Mod 'X' (x) 1.0 requires version 1.20.4 of minecraft, but only the wrong version is present: 1.21!" - for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version |any version of )?([^\n]*?) of (?:mod )?'?minecraft'?/gi)) { - const [, name, id, fuer] = m; - const mod = findeMod(mods, id, name); - fund(`mcversion:${id}`, `„${name}“ passt nicht zu dieser Minecraft-Version`, - `„${name}“ ist für Minecraft ${fuer.replace(/^version\s+/i, '').trim() || 'eine andere Version'} gemacht, deine Instanz hat ${ctx.version || 'eine andere'}. Abschalten oder eine passende Version des Mods installieren.`, - mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null); - } - // Lösungsvorschlag: "- Remove mod 'X' (x)." - for (const m of t.matchAll(/^\s*-\s*Remove mod '([^']+)' \(([\w.-]+)\)/gim)) { - const [, name, id] = m; - const mod = findeMod(mods, id, name); - fund(`entfernen:${id}`, `„${name}“ verursacht den Fehler`, - `Der Mod-Loader schlägt vor, „${name}“ zu entfernen – er verträgt sich nicht mit den anderen Mods oder dieser Version.`, - mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null); - } - // "Mod 'A' (a) is incompatible with any version of mod 'B' (b)" - for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? is incompatible with [^\n]*?mod '([^']+)' \(([\w.-]+)\)/gi)) { - const [, a, idA, b] = m; - const mod = findeMod(mods, idA, a); - fund(`inkompatibel:${idA}`, `„${a}“ und „${b}“ vertragen sich nicht`, - `Diese beiden Mods können nicht zusammen installiert sein. Schalte einen der beiden ab.`, - mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: a } : null); - } - - // --- Doppelte Mods --- - if (/Duplicate mods found|Found duplicate mods|is provided by multiple mods|Mod ID ['"]?[\w.-]+['"]? is (?:provided|present) multiple times/i.test(t)) { - fund('doppelt', 'Ein Mod ist doppelt installiert', - 'Mindestens ein Mod liegt zweimal im Mods-Ordner (z. B. zwei Versionen desselben Mods). Lösche die ältere Datei.', - { typ: 'mods-ordner' }); - } - - // --- Falsche Java-Version --- - const klasse = t.match(/UnsupportedClassVersionError[\s\S]{0,300}?class file version (\d+)\.\d+/i); - const javaPflicht = t.match(/requires (?:at least )?Java (\d+)|Java (\d+) (?:or (?:newer|later|above)) is required|Unsupported Java detected \((\d+)/i); - if (klasse || javaPflicht) { - const noetig = klasse ? javaAusKlassenversion(klasse[1]) : Number(javaPflicht[1] || javaPflicht[2] || 0); - fund('java', noetig ? `Diese Instanz braucht Java ${noetig}` : 'Falsche Java-Version', - `Minecraft oder ein Mod wurde für ${noetig ? `Java ${noetig}` : 'eine andere Java-Version'} gebaut, gestartet wurde mit einer älteren. AeroMC kann das passende Java automatisch wählen und bei Bedarf herunterladen.`, - { typ: 'java-auto' }); - } - - // --- Zu wenig Arbeitsspeicher --- - if (/java\.lang\.OutOfMemoryError|There is insufficient memory for the Java Runtime/i.test(t)) { - const jetzt = Number(ctx.maxMemMb) || 2048; - const grenze = Math.max(2048, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.6)); - const neu = Math.min(grenze, jetzt + 2048); - fund('ram', 'Minecraft hatte zu wenig Arbeitsspeicher', - `Der Instanz waren ${Math.round(jetzt / 102.4) / 10} GB zugewiesen – zu wenig für diese Mods oder Welt.` - + (neu > jetzt ? ` Empfohlen: ${Math.round(neu / 102.4) / 10} GB.` : ' Mehr lässt dein PC kaum zu – weniger Mods oder eine kleinere Sichtweite helfen.'), - neu > jetzt ? { typ: 'ram', mb: neu } : null); - } - if (/Could not reserve enough space for (?:\d+KB )?object heap|Invalid maximum heap size|Initial heap size set to a larger value than the maximum/i.test(t)) { - const jetzt = Number(ctx.maxMemMb) || 4096; - const neu = Math.max(2048, Math.min(jetzt - 1024, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.5))); - fund('ram-zuviel', 'Zu viel Arbeitsspeicher eingestellt', - `Java konnte die eingestellten ${Math.round(jetzt / 102.4) / 10} GB nicht reservieren – entweder ist gerade nicht genug frei oder es läuft ein 32-Bit-Java.`, - neu < jetzt ? { typ: 'ram', mb: neu } : { typ: 'java-auto' }); - } - - // --- Mixin-Fehler: ein bestimmter Mod passt nicht --- - for (const m of t.matchAll(/Mixin (?:apply|transformation)[^\n]*? (?:for|from) mod ([\w.-]+) failed|from mod ([\w.-]+) failed injection check|mixins?\.json:[^\n]*? from mod ([\w.-]+)/gi)) { - const id = m[1] || m[2] || m[3]; - if (!id || KEINE_MODS.has(id.toLowerCase())) continue; - const mod = findeMod(mods, id); - fund(`mixin:${id}`, `„${mod ? (mod.title || id) : id}“ passt nicht zu dieser Version`, - `Der Mod „${id}“ greift tief in Minecraft ein und ist mit dieser Minecraft-Version oder einem anderen Mod nicht kompatibel. Schalte ihn ab oder installiere eine passende Version.`, - mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || id } : null); - } - - // --- Grafiktreiber --- - if (/Pixel format not accelerated|WGL: The driver does not appear to support OpenGL|GLFW error 65542|OpenGL 3\.2 (?:is )?not supported|Couldn't set pixel format|No OpenGL context found/i.test(t)) { - fund('treiber', 'Problem mit dem Grafiktreiber', - 'Minecraft konnte OpenGL nicht starten. Meist hilft es, den Grafiktreiber direkt vom Hersteller (NVIDIA, AMD oder Intel) zu aktualisieren. Bei Laptops: in den Windows-Grafikeinstellungen Java auf „Hohe Leistung“ stellen.'); - } - - // --- Bekannte Unverträglichkeiten anhand der installierten Mods --- - const aktive = mods.filter((m) => m.enabled !== false); - const hat = (re) => aktive.find((m) => re.test(String(m.filename || '') + ' ' + String(m.title || ''))); - const optifine = hat(/optifine|optifabric/i); - const sodium = hat(/sodium|embeddium|rubidium/i); - if (optifine && sodium) { - fund('optifine-sodium', 'OptiFine und Sodium zusammen installiert', - 'OptiFine und Sodium (bzw. Embeddium/Rubidium) machen dasselbe und vertragen sich nicht. Schalte OptiFine ab – für Shader gibt es Iris als Ergänzung zu Sodium.', - { typ: 'mod-deaktivieren', dateiname: optifine.filename, name: optifine.title || 'OptiFine' }); - } - - // --- Forge nennt einen Verdächtigen, aber sonst nichts Eindeutiges --- - if (!funde.length) { - const verdacht = t.match(/^Suspected Mods?:\s*([^\n]+)$/im); - if (verdacht && !/^\s*NONE\s*$/i.test(verdacht[1])) { - const name = verdacht[1].split(/[,(]/)[0].trim(); - const mod = findeMod(mods, '', name); - fund('verdacht', `Vermutlich verursacht von „${name}“`, - `Laut Absturzbericht steckt wahrscheinlich „${verdacht[1].trim()}“ dahinter. Schalte den Mod testweise ab und starte erneut.`, - mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || name } : null); - } - } - - return funde; -} - -module.exports = { analysiere, projektFuer }; +'use strict'; + +/* + * crashhelp.js – Absturz-Assistent + * --------------------------------- + * Liest Absturzbericht, latest.log und die letzte Konsolenausgabe eines Starts + * und erkennt die häufigsten Ursachen. Jeder Fund bekommt eine verständliche + * Erklärung und – wo möglich – eine Aktion, die der Launcher mit einem Klick + * ausführen kann: + * + * { typ: 'mod-installieren', projekt, name } fehlende Abhängigkeit von Modrinth holen + * { typ: 'mod-deaktivieren', dateiname, name } Mod abschalten (umbenennen, nichts gelöscht) + * { typ: 'ram', mb } Arbeitsspeicher der Instanz ändern + * { typ: 'java-auto' } passendes Java automatisch wählen lassen + * { typ: 'mods-ordner' } Mods-Ordner öffnen + * + * Bewusst nur Muster, die eindeutig sind – lieber keinen Fund als einen falschen. + */ + +// Mod-IDs, die keine installierbaren Mods sind +const KEINE_MODS = new Set(['minecraft', 'java', 'fabricloader', 'fabric-loader', 'forge', 'neoforge', 'quilt_loader', 'quilt-loader', 'fml', 'mixinextras']); + +// Mod-ID -> Modrinth-Projekt, wo sich beides unterscheidet +const PROJEKT_FUER_ID = { + fabric: 'fabric-api', + 'fabric-api-base': 'fabric-api', + quilted_fabric_api: 'qsl', + cloth_config: 'cloth-config', + 'cloth-config2': 'cloth-config', + architectury: 'architectury-api', + geckolib3: 'geckolib', +}; + +// Java-Hauptversion aus der "class file version" (52 = Java 8, 61 = 17, 65 = 21 …) +const javaAusKlassenversion = (v) => Number(v) - 44; + +function projektFuer(modId) { + const id = String(modId || '').toLowerCase(); + return PROJEKT_FUER_ID[id] || id.replace(/_/g, '-'); +} + +// installierte Mod zu einer ID/einem Namen finden (für "abschalten") +function findeMod(mods, modId, name) { + const id = String(modId || '').toLowerCase(); + const kurz = id.replace(/[-_]/g, ''); + const n = String(name || '').toLowerCase().replace(/[^a-z0-9]/g, ''); + return (mods || []).find((m) => { + if (m.enabled === false) return false; + const datei = String(m.filename || '').toLowerCase(); + const titel = String(m.title || '').toLowerCase().replace(/[^a-z0-9]/g, ''); + return (id && (datei.startsWith(id) || datei.replace(/[-_]/g, '').startsWith(kurz))) + || (n && titel && (titel === n || datei.replace(/[^a-z0-9]/g, '').startsWith(n))); + }) || null; +} + +/* + * text: zusammengeführter Text aus Bericht, Log und Konsole + * ctx: { loader, version, maxMemMb, gesamtMb, mods: [{ filename, title, projectId, enabled }] } + * -> [{ schluessel, titel, text, aktion? }] + */ +function analysiere(text, ctx = {}) { + const t = String(text || ''); + const funde = []; + const schonDa = new Set(); + const fund = (schluessel, titel, erklaerung, aktion) => { + if (schonDa.has(schluessel)) return; + schonDa.add(schluessel); + funde.push({ schluessel, titel, text: erklaerung, aktion: aktion || null }); + }; + const mods = ctx.mods || []; + + // --- Fehlende Abhängigkeiten (Fabric/Quilt) --- + // "Mod 'Mod Menu' (modmenu) 7.2.2 requires any version of fabric-api, which is missing!" + // neuere Fassung: "... requires any version of mod 'Fabric API' (fabric-api), which is missing!" + for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version ([^\n,]+?)|any version) of (?:mod '([^']+)' \(([\w.-]+)\)|([\w.-]+)), which is missing/gi)) { + const [, name, , version, fehltName, fehltIdNeu, fehltIdAlt] = m; + const fehlt = fehltIdNeu || fehltIdAlt; + if (KEINE_MODS.has(fehlt.toLowerCase())) continue; + const anzeige = fehltName || fehlt; + fund(`fehlt:${fehlt}`, `„${name}“ braucht ${anzeige}`, + `Der Mod „${name}“ funktioniert nur zusammen mit „${anzeige}“${version ? ` (Version ${version.trim()})` : ''} – und der fehlt in dieser Instanz.`, + { typ: 'mod-installieren', projekt: projektFuer(fehlt), name: anzeige }); + } + // Lösungsvorschlag von Fabric: "- Install fabric-api, any version." + for (const m of t.matchAll(/^\s*-\s*Install ([\w.-]+), (?:any version|version [^\n.]+)/gim)) { + const fehlt = m[1]; + if (KEINE_MODS.has(fehlt.toLowerCase())) continue; + fund(`fehlt:${fehlt}`, `Es fehlt „${fehlt}“`, + `Ein installierter Mod braucht „${fehlt}“, der in dieser Instanz nicht installiert ist.`, + { typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt }); + } + + // --- Fehlende Abhängigkeiten (Forge/NeoForge) --- + // "Mod ID: 'architectury', Requested by: 'rei', Expected range: '[9.1.12,)', Actual version: '[MISSING]'" + for (const m of t.matchAll(/Mod ID: '([\w.-]+)', Requested by: '([\w.-]+)', Expected range: '([^']*)', Actual version: '([^']*)'/gi)) { + const [, fehlt, von, bereich, ist] = m; + if (fehlt.toLowerCase() === 'minecraft') { + const mod = findeMod(mods, von); + fund(`mcversion:${von}`, `„${von}“ passt nicht zu dieser Minecraft-Version`, + `Der Mod „${von}“ ist für Minecraft ${bereich} gemacht, deine Instanz hat ${ctx.version || ist}.`, + mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || von } : null); + continue; + } + if (KEINE_MODS.has(fehlt.toLowerCase())) continue; + if (/MISSING/i.test(ist)) { + fund(`fehlt:${fehlt}`, `„${von}“ braucht ${fehlt}`, + `Der Mod „${von}“ funktioniert nur zusammen mit „${fehlt}“ – und der fehlt in dieser Instanz.`, + { typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt }); + } else { + fund(`alt:${fehlt}`, `„${fehlt}“ ist zu alt oder zu neu`, + `„${von}“ braucht „${fehlt}“ in der Version ${bereich}, installiert ist ${ist}. Aktualisiere „${fehlt}“ im Inhalts-Dialog („Nach Updates suchen“).`); + } + } + + // --- Mod für eine andere Minecraft-Version (Fabric) --- + // "Mod 'X' (x) 1.0 requires version 1.20.4 of minecraft, but only the wrong version is present: 1.21!" + for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version |any version of )?([^\n]*?) of (?:mod )?'?minecraft'?/gi)) { + const [, name, id, fuer] = m; + const mod = findeMod(mods, id, name); + fund(`mcversion:${id}`, `„${name}“ passt nicht zu dieser Minecraft-Version`, + `„${name}“ ist für Minecraft ${fuer.replace(/^version\s+/i, '').trim() || 'eine andere Version'} gemacht, deine Instanz hat ${ctx.version || 'eine andere'}. Abschalten oder eine passende Version des Mods installieren.`, + mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null); + } + // Lösungsvorschlag: "- Remove mod 'X' (x)." + for (const m of t.matchAll(/^\s*-\s*Remove mod '([^']+)' \(([\w.-]+)\)/gim)) { + const [, name, id] = m; + const mod = findeMod(mods, id, name); + fund(`entfernen:${id}`, `„${name}“ verursacht den Fehler`, + `Der Mod-Loader schlägt vor, „${name}“ zu entfernen – er verträgt sich nicht mit den anderen Mods oder dieser Version.`, + mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null); + } + // "Mod 'A' (a) is incompatible with any version of mod 'B' (b)" + for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? is incompatible with [^\n]*?mod '([^']+)' \(([\w.-]+)\)/gi)) { + const [, a, idA, b] = m; + const mod = findeMod(mods, idA, a); + fund(`inkompatibel:${idA}`, `„${a}“ und „${b}“ vertragen sich nicht`, + `Diese beiden Mods können nicht zusammen installiert sein. Schalte einen der beiden ab.`, + mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: a } : null); + } + + // --- Doppelte Mods --- + if (/Duplicate mods found|Found duplicate mods|is provided by multiple mods|Mod ID ['"]?[\w.-]+['"]? is (?:provided|present) multiple times/i.test(t)) { + fund('doppelt', 'Ein Mod ist doppelt installiert', + 'Mindestens ein Mod liegt zweimal im Mods-Ordner (z. B. zwei Versionen desselben Mods). Lösche die ältere Datei.', + { typ: 'mods-ordner' }); + } + + // --- Falsche Java-Version --- + const klasse = t.match(/UnsupportedClassVersionError[\s\S]{0,300}?class file version (\d+)\.\d+/i); + const javaPflicht = t.match(/requires (?:at least )?Java (\d+)|Java (\d+) (?:or (?:newer|later|above)) is required|Unsupported Java detected \((\d+)/i); + if (klasse || javaPflicht) { + const noetig = klasse ? javaAusKlassenversion(klasse[1]) : Number(javaPflicht[1] || javaPflicht[2] || 0); + fund('java', noetig ? `Diese Instanz braucht Java ${noetig}` : 'Falsche Java-Version', + `Minecraft oder ein Mod wurde für ${noetig ? `Java ${noetig}` : 'eine andere Java-Version'} gebaut, gestartet wurde mit einer älteren. AeroMC kann das passende Java automatisch wählen und bei Bedarf herunterladen.`, + { typ: 'java-auto' }); + } + + // --- Alte Forge-Version verträgt aktuelles Java 8 nicht --- + // Forge 1.16.5 vor 36.2.26 (und ähnlich alte Fassungen) nutzt eine interne + // Java-Klasse, die es seit Java 8u321 so nicht mehr gibt. + if (/sun\.security\.util\.ManifestEntryVerifier\./i.test(t) && /forge/i.test(String(ctx.loader || ''))) { + fund('forge-alt', 'Diese Forge-Version ist zu alt für das aktuelle Java', + 'Ältere Forge-Versionen laufen mit aktuellem Java 8 nicht mehr (Fehler „ManifestEntryVerifier“). Eine neuere Forge-Version für dieselbe Minecraft-Version behebt das – Mods und Welten bleiben erhalten.', + { typ: 'forge-aktualisieren' }); + } + + // --- Zu wenig Arbeitsspeicher --- + if (/java\.lang\.OutOfMemoryError|There is insufficient memory for the Java Runtime/i.test(t)) { + const jetzt = Number(ctx.maxMemMb) || 2048; + const grenze = Math.max(2048, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.6)); + const neu = Math.min(grenze, jetzt + 2048); + fund('ram', 'Minecraft hatte zu wenig Arbeitsspeicher', + `Der Instanz waren ${Math.round(jetzt / 102.4) / 10} GB zugewiesen – zu wenig für diese Mods oder Welt.` + + (neu > jetzt ? ` Empfohlen: ${Math.round(neu / 102.4) / 10} GB.` : ' Mehr lässt dein PC kaum zu – weniger Mods oder eine kleinere Sichtweite helfen.'), + neu > jetzt ? { typ: 'ram', mb: neu } : null); + } + if (/Could not reserve enough space for (?:\d+KB )?object heap|Invalid maximum heap size|Initial heap size set to a larger value than the maximum/i.test(t)) { + const jetzt = Number(ctx.maxMemMb) || 4096; + const neu = Math.max(2048, Math.min(jetzt - 1024, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.5))); + fund('ram-zuviel', 'Zu viel Arbeitsspeicher eingestellt', + `Java konnte die eingestellten ${Math.round(jetzt / 102.4) / 10} GB nicht reservieren – entweder ist gerade nicht genug frei oder es läuft ein 32-Bit-Java.`, + neu < jetzt ? { typ: 'ram', mb: neu } : { typ: 'java-auto' }); + } + + // --- Mixin-Fehler: ein bestimmter Mod passt nicht --- + for (const m of t.matchAll(/Mixin (?:apply|transformation)[^\n]*? (?:for|from) mod ([\w.-]+) failed|from mod ([\w.-]+) failed injection check|mixins?\.json:[^\n]*? from mod ([\w.-]+)/gi)) { + const id = m[1] || m[2] || m[3]; + if (!id || KEINE_MODS.has(id.toLowerCase())) continue; + const mod = findeMod(mods, id); + fund(`mixin:${id}`, `„${mod ? (mod.title || id) : id}“ passt nicht zu dieser Version`, + `Der Mod „${id}“ greift tief in Minecraft ein und ist mit dieser Minecraft-Version oder einem anderen Mod nicht kompatibel. Schalte ihn ab oder installiere eine passende Version.`, + mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || id } : null); + } + + // --- Grafiktreiber --- + if (/Pixel format not accelerated|WGL: The driver does not appear to support OpenGL|GLFW error 65542|OpenGL 3\.2 (?:is )?not supported|Couldn't set pixel format|No OpenGL context found/i.test(t)) { + fund('treiber', 'Problem mit dem Grafiktreiber', + 'Minecraft konnte OpenGL nicht starten. Meist hilft es, den Grafiktreiber direkt vom Hersteller (NVIDIA, AMD oder Intel) zu aktualisieren. Bei Laptops: in den Windows-Grafikeinstellungen Java auf „Hohe Leistung“ stellen.'); + } + + // --- Bekannte Unverträglichkeiten anhand der installierten Mods --- + const aktive = mods.filter((m) => m.enabled !== false); + const hat = (re) => aktive.find((m) => re.test(String(m.filename || '') + ' ' + String(m.title || ''))); + const optifine = hat(/optifine|optifabric/i); + const sodium = hat(/sodium|embeddium|rubidium/i); + if (optifine && sodium) { + fund('optifine-sodium', 'OptiFine und Sodium zusammen installiert', + 'OptiFine und Sodium (bzw. Embeddium/Rubidium) machen dasselbe und vertragen sich nicht. Schalte OptiFine ab – für Shader gibt es Iris als Ergänzung zu Sodium.', + { typ: 'mod-deaktivieren', dateiname: optifine.filename, name: optifine.title || 'OptiFine' }); + } + + // --- Forge nennt einen Verdächtigen, aber sonst nichts Eindeutiges --- + if (!funde.length) { + const verdacht = t.match(/^Suspected Mods?:\s*([^\n]+)$/im); + if (verdacht && !/^\s*NONE\s*$/i.test(verdacht[1])) { + const name = verdacht[1].split(/[,(]/)[0].trim(); + const mod = findeMod(mods, '', name); + fund('verdacht', `Vermutlich verursacht von „${name}“`, + `Laut Absturzbericht steckt wahrscheinlich „${verdacht[1].trim()}“ dahinter. Schalte den Mod testweise ab und starte erneut.`, + mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || name } : null); + } + } + + return funde; +} + +module.exports = { analysiere, projektFuer }; diff --git a/src/designs.css b/src/designs.css index 79032d6..8aa58ee 100644 --- a/src/designs.css +++ b/src/designs.css @@ -9,8 +9,8 @@ /* ===================== Design-Auswahl in den Optionen ===================== */ .design-raster{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px} -.settings-card-wide .design-raster{grid-template-columns:repeat(5,minmax(0,1fr))} -@media (max-width:900px){.settings-card-wide .design-raster{grid-template-columns:repeat(2,minmax(0,1fr))}} +.settings-card .design-raster{grid-template-columns:repeat(5,minmax(0,1fr))} +@media (max-width:900px){.settings-card .design-raster{grid-template-columns:repeat(2,minmax(0,1fr))}} .design-wahl{display:flex;flex-direction:column;gap:5px;padding:6px;border:1px solid var(--border);border-radius:8px;background:none;color:var(--text);font-family:inherit;font-size:12px;cursor:pointer;text-align:left} .design-wahl:hover{border-color:var(--sel-br)} .design-wahl.on{border-color:var(--sel-br);box-shadow:0 0 0 1px var(--sel-br);background:var(--sel)} @@ -94,6 +94,10 @@ :root:is([data-design="aero"],[data-design="lila"]) .card.sel{background:linear-gradient(180deg,rgba(var(--p-akzent),.24),rgba(var(--p-tief),.14));border-color:rgba(var(--p-akzent2),.55);box-shadow:0 10px 28px -14px rgba(var(--p-akzent),.95),0 0 0 1px rgba(var(--p-hell),.08) inset} :root:is([data-design="aero"],[data-design="lila"]) .card.sel .card-name{background:linear-gradient(120deg,rgb(var(--p-akzent2)),rgb(var(--p-akzent)) 60%,rgb(var(--p-tief)));color:#fff;border-radius:999px} :root:is([data-design="aero"],[data-design="lila"]) .card-icon-img{border-radius:10px} +:root:is([data-design="aero"],[data-design="lila"]) .card.laeuft::after{border-color:rgb(var(--p-akzent2));--accent:rgb(var(--p-akzent2))} +:root.ui-animiert:is([data-design="aero"],[data-design="lila"]) .lichtpunkte{display:block} +:root:is([data-design="aero"],[data-design="lila"]) .lichtpunkte i{background:rgb(var(--p-hell));box-shadow:0 0 calc(var(--g) * 3) 1px rgba(var(--p-akzent2),.85)} +:root:is([data-design="aero"],[data-design="lila"]) .erfolg .funke{box-shadow:0 0 5px 2px rgb(var(--p-hell)),0 0 14px 4px rgba(var(--p-akzent2),.8)} :root:is([data-design="aero"],[data-design="lila"]) .card-taste{border-radius:999px;background:linear-gradient(120deg,rgb(var(--p-akzent2)),rgb(var(--p-akzent)))} :root:is([data-design="aero"],[data-design="lila"]) .empty h2{background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 60%,rgb(var(--p-tief)));-webkit-background-clip:text;background-clip:text;color:transparent;font-size:26px} @@ -132,7 +136,7 @@ :root:is([data-design="aero"],[data-design="lila"]) .mods-filters select, :root:is([data-design="aero"],[data-design="lila"]) .mmc-bar select, :root:is([data-design="aero"],[data-design="lila"]) .clip-length select, -:root:is([data-design="aero"],[data-design="lila"]) .path-row input, +:root:is([data-design="aero"],[data-design="lila"]) .path-row input,:root:is([data-design="aero"],[data-design="lila"]) .path-row select,:root:is([data-design="aero"],[data-design="lila"]) select.s-wert,:root:is([data-design="aero"],[data-design="lila"]) input.s-wert,:root:is([data-design="aero"],[data-design="lila"]) .s-paar input,:root:is([data-design="aero"],[data-design="lila"]) .s-einheit input, :root:is([data-design="aero"],[data-design="lila"]) .se-farbzeile input[type=text], :root:is([data-design="aero"],[data-design="lila"]) .se-feld select{background:var(--input);color:var(--text);border-color:var(--border);border-radius:10px} :root:is([data-design="aero"],[data-design="lila"]) .search-wrap input{border-radius:999px} @@ -145,6 +149,9 @@ :root:is([data-design="aero"],[data-design="lila"]) input[type=checkbox],:root:is([data-design="aero"],[data-design="lila"]) input[type=radio],:root:is([data-design="aero"],[data-design="lila"]) input[type=range]{accent-color:rgb(var(--p-akzent))} :root:is([data-design="aero"],[data-design="lila"]) .login-usercode{background:var(--input);color:var(--text);border-color:var(--border);border-radius:12px} :root:is([data-design="aero"],[data-design="lila"]) .upd-notes{background:var(--input);border-radius:10px} +:root:is([data-design="aero"],[data-design="lila"]) .upd-notes h5{color:rgb(var(--p-hell))} +:root:is([data-design="aero"],[data-design="lila"]) .upd-notes li::marker{color:rgb(var(--p-akzent2))} +:root:is([data-design="aero"],[data-design="lila"]) .upd-notes a{color:rgb(var(--p-hell))} /* Knöpfe – rund wie auf der Webseite */ :root:is([data-design="aero"],[data-design="lila"]) .btn{border-radius:999px;padding:7px 15px;background:var(--surface);border-color:var(--border);color:var(--text);transition:background .15s,border-color .15s,transform .15s} @@ -190,6 +197,7 @@ :root:is([data-design="aero"],[data-design="lila"]) .snav:hover{background:var(--surface)} :root:is([data-design="aero"],[data-design="lila"]) .snav.on{background:linear-gradient(120deg,rgba(var(--p-akzent),.30),rgba(var(--p-tief),.18));border-color:rgba(var(--p-akzent2),.40);box-shadow:none} :root:is([data-design="aero"],[data-design="lila"]) .settings-card{background:var(--surface);border-color:var(--border);border-radius:16px} +:root:is([data-design="aero"],[data-design="lila"]) .schalter input:checked+i{background:linear-gradient(120deg,rgb(var(--p-akzent)),rgb(var(--p-akzent2)));box-shadow:0 0 10px -2px rgba(var(--p-akzent),.8)} :root:is([data-design="aero"],[data-design="lila"]) .settings-kv-row, :root:is([data-design="aero"],[data-design="lila"]) .about-brand{border-bottom-color:var(--border2)} :root:is([data-design="aero"],[data-design="lila"]) .appinfo-item{background:rgba(var(--p-linie),.04);border-color:var(--border2);border-radius:12px} @@ -197,6 +205,12 @@ :root:is([data-design="aero"],[data-design="lila"]) .settings-hero h4{background:linear-gradient(120deg,rgb(var(--p-hell)),rgb(var(--p-akzent)) 70%);-webkit-background-clip:text;background-clip:text;color:transparent} /* Kontextmenü, Hinweis, Bildlaufleisten */ +/* Erfolg-Meldung: Glas-Karte mit Leuchtrand wie die Webseite */ +:root:is([data-design="aero"],[data-design="lila"]) .erfolg{background:linear-gradient(135deg,rgba(var(--p-akzent),.22),var(--surface) 55%);border:1px solid rgba(var(--p-akzent),.45);border-radius:14px; + box-shadow:0 0 0 1px rgba(255,255,255,.04) inset,0 18px 40px -12px rgba(0,0,0,.8),0 0 24px -8px rgba(var(--p-akzent),.7)} +:root:is([data-design="aero"],[data-design="lila"]) .erfolg-symbol{border-radius:10px;background:rgba(var(--p-akzent),.18)} +:root:is([data-design="aero"],[data-design="lila"]) .erfolg-kopf{color:rgb(var(--p-hell))} +:root:is([data-design="aero"],[data-design="lila"]) .erfolg-karte{border-radius:12px;background:var(--surface)} :root:is([data-design="aero"],[data-design="lila"]) .ctx{background:var(--surface);border-color:var(--border);border-radius:12px;box-shadow:0 18px 40px -12px rgba(0,0,0,.8)} :root:is([data-design="aero"],[data-design="lila"]) .ctx-item{border-radius:8px} :root:is([data-design="aero"],[data-design="lila"]) .ctx-item:hover{background:rgba(var(--p-akzent),.22)} @@ -267,6 +281,10 @@ :root[data-design="minecraft"] .toolbar{ background:url(assets/design/gras.png) 0 0/32px 8px repeat-x,url(assets/design/schiefer.png) 0 0/64px 64px; image-rendering:pixelated;border-bottom:2px solid #000;padding:11px 8px 6px} +:root[data-design="minecraft"] .toolbar{position:relative;isolation:isolate} +:root.ui-animiert[data-design="minecraft"] .toolbar::before{content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;opacity:.18; + background:url("data:image/svg+xml;utf8,") 0 0/512px 100% repeat-x;animation:wolken-ziehen 70s steps(512) infinite} +@keyframes wolken-ziehen{to{background-position:512px 0}} :root[data-design="minecraft"] .tbtn{color:#fff;border-radius:0;text-shadow:1px 1px 0 #1a1a1a;image-rendering:auto} :root[data-design="minecraft"] .tbtn:hover{background:rgba(0,0,0,.35);border-color:#fff} :root[data-design="minecraft"] .tbtn:active{background:rgba(0,0,0,.5)} @@ -288,7 +306,7 @@ :root[data-design="minecraft"] .mods-filters select, :root[data-design="minecraft"] .mmc-bar select, :root[data-design="minecraft"] .clip-length select, -:root[data-design="minecraft"] .path-row input, +:root[data-design="minecraft"] .path-row input,:root[data-design="minecraft"] .path-row select,:root[data-design="minecraft"] select.s-wert,:root[data-design="minecraft"] input.s-wert,:root[data-design="minecraft"] .s-paar input,:root[data-design="minecraft"] .s-einheit input, :root[data-design="minecraft"] .login-usercode, :root[data-design="minecraft"] .upd-notes, :root[data-design="minecraft"] .se-farbzeile input[type=text], @@ -319,6 +337,11 @@ :root[data-design="minecraft"] .card.sel .card-name{background:none;color:#ffff7f;border-radius:0} :root[data-design="minecraft"] .card-taste{border-radius:0;background:#3c8527;box-shadow:inset -1px -1px 0 #1d4d13} :root[data-design="minecraft"] .card-icon-img{border-radius:0} +:root[data-design="minecraft"] .card.laeuft::after{border-color:#7fd858;--accent:#7fd858;border-radius:0} +/* Abbau-Risse: Download-Fortschritt auf dem Block, beim Fertigwerden springt er kurz */ +:root[data-design="minecraft"] .card.abbau .card-riss{display:block} +:root.ui-animiert[data-design="minecraft"] .card.abgebaut .card-icon,:root.ui-animiert[data-design="minecraft"] .card.abgebaut .card-icon-img{animation:block-bricht .45s cubic-bezier(.3,.7,.4,1)} +@keyframes block-bricht{0%{transform:scale(.8)}45%{transform:scale(1.14)}100%{transform:none}} :root[data-design="minecraft"] .empty h2{font-family:var(--pixel);font-weight:400;font-size:24px;color:#fff;text-shadow:3px 3px 0 #3f3f3f} :root[data-design="minecraft"] .empty p{color:#d0d0d0} @@ -332,6 +355,8 @@ :root[data-design="minecraft"] .hr{background:#000;height:2px;margin:6px 0} :root[data-design="minecraft"] .sbtn{margin-bottom:3px;padding:5px 2px} :root[data-design="minecraft"] .sbtn.play{font-family:var(--pixel);font-size:16px;font-weight:400;padding:6px 4px 7px;text-shadow:var(--mc-schatten)} +:root[data-design="minecraft"] .sbtn.play::before,:root:is([data-design="aero"],[data-design="lila"]) .sbtn.play::before{background:#fff;opacity:.28} +:root[data-design="minecraft"] .sbtn.play.laedt .play-text{font-size:13px} /* Statusleiste */ :root[data-design="minecraft"] .statusbar{background:url(assets/design/schiefer.png) 0 0/64px 64px;image-rendering:pixelated;border-top:2px solid #000} @@ -391,6 +416,10 @@ :root[data-design="minecraft"] .settings-hero h4{font-family:var(--pixel);font-size:24px;font-weight:400;text-shadow:3px 3px 0 #3f3f3f} :root[data-design="minecraft"] .settings-hero p{color:#d0d0d0} :root[data-design="minecraft"] .settings-card{background:#313233;border:2px solid #000;border-radius:0;box-shadow:inset 2px 2px 0 #5a5b5d,inset -2px -2px 0 #1e1e1f} +:root[data-design="minecraft"] .schalter i{border-radius:0;background:#1e1e1f;box-shadow:inset 2px 2px 0 #000} +:root[data-design="minecraft"] .schalter i::after{border-radius:0;background:#c6c6c6;box-shadow:inset -2px -2px 0 #555,inset 2px 2px 0 #fff} +:root[data-design="minecraft"] .schalter input:checked+i{background:#3c8527} +:root[data-design="minecraft"] .s-zeile{border-top-color:rgba(0,0,0,.55)} :root[data-design="minecraft"] .settings-card h5{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)} :root[data-design="minecraft"] .settings-kv-row, :root[data-design="minecraft"] .about-brand{border-bottom-color:#1a1a1b} @@ -399,6 +428,12 @@ :root[data-design="minecraft"] .spane h4{font-family:var(--pixel);font-size:16px;font-weight:400;text-shadow:var(--mc-schatten)} /* Kontextmenü, Hinweis, Bildlaufleisten */ +/* Erfolg-Meldung wie im Spiel („Erfolg erzielt!“) */ +:root[data-design="minecraft"] .erfolg{background:#212121;border:2px solid #555;border-radius:0;box-shadow:inset 2px 2px 0 #373737,inset -2px -2px 0 #141414,0 10px 24px rgba(0,0,0,.45);color:#fff;font-family:"AeroPixel","Segoe UI",sans-serif} +:root[data-design="minecraft"] .erfolg-symbol{border-radius:0;background:#8b8b8b;box-shadow:inset 2px 2px 0 #373737,inset -2px -2px 0 #fff} +:root[data-design="minecraft"] .erfolg-kopf{color:#ffff55;font-size:14px;font-weight:400;letter-spacing:0;text-transform:none} +:root[data-design="minecraft"] .erfolg-name{font-size:14px;font-weight:400} +:root[data-design="minecraft"] .erfolg-karte{border-radius:0;border:2px solid #000;background:rgba(0,0,0,.35)} :root[data-design="minecraft"] .ctx{background:#1e1e1f;border:2px solid #000;border-radius:0;box-shadow:inset 1px 1px 0 #48494a,0 8px 24px rgba(0,0,0,.5)} :root[data-design="minecraft"] .ctx-item{border-radius:0;text-shadow:1px 1px 0 #000} :root[data-design="minecraft"] .ctx-item:hover{background:#3c8527} diff --git a/src/erfolge.js b/src/erfolge.js new file mode 100644 index 0000000..2ad5f74 --- /dev/null +++ b/src/erfolge.js @@ -0,0 +1,73 @@ +/* + * Erfolge im Minecraft-Stil („Erfolg erzielt!“). + * + * Bewertet wird aus dem, was der Launcher ohnehin weiß (Instanzen, Spielzeit, + * Starts, Mods), plus ein paar Merkern für einzelne Aktionen (Umhang + * hochgeladen, Skin im Editor übernommen, Leistungs-Paket …). Freigeschaltete + * Erfolge landen mit Datum in userData/erfolge.json. + */ +const fs = require('fs'); +const path = require('path'); + +const STUNDE = 3600; +const LISTE = [ + { id: 'erster-start', symbol: 'grass', titel: 'Los geht’s!', text: 'Starte zum ersten Mal ein Spiel mit AeroMC.', pruefe: (w) => w.starts >= 1 }, + { id: 'starts-10', symbol: 'pickaxe', titel: 'Stammgast', text: 'Starte 10-mal ein Spiel.', pruefe: (w) => w.starts >= 10 }, + { id: 'starts-100', symbol: 'gold', titel: 'Ohne mich geht nichts', text: 'Starte 100-mal ein Spiel.', pruefe: (w) => w.starts >= 100 }, + { id: 'zeit-1', symbol: 'map', titel: 'Erste Stunde', text: 'Spiele insgesamt eine Stunde.', pruefe: (w) => w.sekunden >= STUNDE }, + { id: 'zeit-10', symbol: 'sword', titel: 'Zeit vergeht wie im Flug', text: 'Spiele insgesamt 10 Stunden.', pruefe: (w) => w.sekunden >= 10 * STUNDE }, + { id: 'zeit-50', symbol: 'diamond', titel: 'Abenteurer', text: 'Spiele insgesamt 50 Stunden.', pruefe: (w) => w.sekunden >= 50 * STUNDE }, + { id: 'zeit-100', symbol: 'star', titel: 'Lebt in Minecraft', text: 'Spiele insgesamt 100 Stunden.', pruefe: (w) => w.sekunden >= 100 * STUNDE }, + { id: 'instanzen-5', symbol: 'chest', titel: 'Sammler', text: 'Lege 5 Instanzen an.', pruefe: (w) => w.instanzen >= 5 }, + { id: 'instanzen-10', symbol: 'book', titel: 'Archivar', text: 'Lege 10 Instanzen an.', pruefe: (w) => w.instanzen >= 10 }, + { id: 'versionen-3', symbol: 'end', titel: 'Zeitreisender', text: 'Spiele 3 verschiedene Minecraft-Versionen.', pruefe: (w) => w.versionen >= 3 }, + { id: 'erste-mod', symbol: 'redstone', titel: 'Bastler', text: 'Habe die erste Mod in einer Instanz.', pruefe: (w) => w.mods >= 1 }, + { id: 'mods-50', symbol: 'potion', titel: 'Mod-Meister', text: 'Habe 50 Mods in deinen Instanzen.', pruefe: (w) => w.mods >= 50 }, + { id: 'modpack', symbol: 'nether', titel: 'Das volle Paket', text: 'Installiere ein Modpack.', pruefe: (w) => w.modpacks >= 1 }, + { id: 'freund', symbol: 'creeper', titel: 'Gemeinsam statt einsam', text: 'Füge deinen ersten Freund hinzu.', pruefe: (w) => w.freunde >= 1 }, + { id: 'umhang', symbol: 'star', titel: 'Modeschöpfer', text: 'Lade einen eigenen AeroMC-Umhang hoch.', pruefe: (w) => w.merker.umhang }, + { id: 'skin-editor', symbol: 'map', titel: 'Pixelkünstler', text: 'Übernimm einen im Skin-Editor gemalten Skin.', pruefe: (w) => w.merker['skin-editor'] }, + { id: 'leistung', symbol: 'tnt', titel: 'Turbo', text: 'Installiere das Leistungs-Paket.', pruefe: (w) => w.merker.leistung }, +]; + +let datei = null; +function init(userDataDir) { datei = path.join(userDataDir, 'erfolge.json'); } + +function lies() { + try { return JSON.parse(fs.readFileSync(datei, 'utf8')); } catch { return null; } +} +function schreibe(stand) { + try { fs.writeFileSync(datei, JSON.stringify(stand, null, 1)); } catch { /* nicht schlimm */ } +} + +/** + * werte: { starts, sekunden, instanzen, versionen, mods, modpacks, freunde } + * merker: zusätzliche Einzel-Aktionen ({ umhang: true } …) + * -> { neu: [Erfolge], ersteAuswertung } + */ +function pruefe(werte, merker = {}) { + const vorher = lies(); + const stand = vorher || { freigeschaltet: {}, merker: {} }; + Object.assign(stand.merker, merker); + const w = Object.assign({ starts: 0, sekunden: 0, instanzen: 0, versionen: 0, mods: 0, modpacks: 0, freunde: 0 }, werte, { merker: stand.merker }); + const neu = []; + const jetzt = new Date().toISOString(); + for (const e of LISTE) { + if (stand.freigeschaltet[e.id]) continue; + let erreicht = false; + try { erreicht = !!e.pruefe(w); } catch { /* Wert fehlt */ } + if (erreicht) { stand.freigeschaltet[e.id] = jetzt; neu.push(oeffentlich(e, jetzt)); } + } + if (neu.length || !vorher || Object.keys(merker).length) schreibe(stand); + return { neu, ersteAuswertung: !vorher }; +} + +const oeffentlich = (e, am) => ({ id: e.id, symbol: e.symbol, titel: e.titel, text: e.text, am: am || null }); + +/** Alle Erfolge mit Datum (null = noch offen). */ +function liste() { + const stand = lies() || { freigeschaltet: {} }; + return LISTE.map((e) => oeffentlich(e, stand.freigeschaltet[e.id] || null)); +} + +module.exports = { init, pruefe, liste }; diff --git a/src/garderobe.js b/src/garderobe.js new file mode 100644 index 0000000..a9a6b5d --- /dev/null +++ b/src/garderobe.js @@ -0,0 +1,106 @@ +/* + * Garderobe: gespeicherte Skins und Umhänge zum schnellen Wechseln. + * + * Mojang merkt sich keine früheren Skins – wer einen neuen hochlädt, verliert + * den alten. Die Garderobe hält Skins (samt Armform) und eigene AeroMC-Umhänge + * als PNG-Dateien im Launcher-Datenordner fest; vor jedem Skinwechsel im + * Launcher wird der bisherige Skin automatisch gesichert. Gleiche Dateien + * (gleiche Prüfsumme) werden nicht doppelt abgelegt. + */ +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const MAX_AUTOMATISCH = 20; // ältere automatische Sicherungen fliegen raus +let ordner = null; + +function init(userDataDir) { + ordner = path.join(userDataDir, 'garderobe'); +} + +const indexDatei = () => path.join(ordner, 'garderobe.json'); +function lies() { + try { return JSON.parse(fs.readFileSync(indexDatei(), 'utf8')); } catch { return []; } +} +function schreibe(liste) { + fs.mkdirSync(ordner, { recursive: true }); + fs.writeFileSync(indexDatei(), JSON.stringify(liste, null, 1)); +} + +/** Alle Einträge, neueste zuerst, jeweils mit Bild als data:-Adresse. */ +function liste() { + return lies() + .filter((e) => fs.existsSync(path.join(ordner, e.datei))) + .sort((a, b) => String(b.erstellt).localeCompare(String(a.erstellt))) + .map((e) => Object.assign({}, e, { + dataUri: 'data:image/png;base64,' + fs.readFileSync(path.join(ordner, e.datei)).toString('base64'), + })); +} + +/** + * Legt einen Skin/Umhang ab. art: 'skin' | 'umhang', daten: PNG-Buffer. + * Gibt es dieselbe Datei schon, wird nur ggf. aus "automatisch" ein fester Eintrag. + */ +function hinzufuegen({ art, daten, name, variante, automatisch }) { + const hash = crypto.createHash('sha1').update(daten).digest('hex'); + const eintraege = lies(); + const vorhanden = eintraege.find((e) => e.art === art && e.hash === hash); + if (vorhanden) { + if (!automatisch && vorhanden.automatisch) { + vorhanden.automatisch = false; + if (name) vorhanden.name = name; + schreibe(eintraege); + } + return { eintrag: vorhanden, neu: false }; + } + fs.mkdirSync(ordner, { recursive: true }); + const id = Date.now().toString(36) + crypto.randomBytes(3).toString('hex'); + const datei = `${art}-${id}.png`; + fs.writeFileSync(path.join(ordner, datei), daten); + const eintrag = { + id, art, hash, datei, + name: String(name || '').trim().slice(0, 40) || (art === 'skin' ? 'Skin' : 'Umhang'), + variante: art === 'skin' ? (variante === 'slim' ? 'slim' : 'classic') : undefined, + automatisch: !!automatisch, + erstellt: new Date().toISOString(), + }; + eintraege.push(eintrag); + // automatische Sicherungen begrenzen (feste Einträge bleiben immer) + const auto = eintraege.filter((e) => e.automatisch).sort((a, b) => String(b.erstellt).localeCompare(String(a.erstellt))); + for (const alt of auto.slice(MAX_AUTOMATISCH)) loescheIntern(eintraege, alt.id); + schreibe(eintraege); + return { eintrag, neu: true }; +} + +function loescheIntern(eintraege, id) { + const i = eintraege.findIndex((e) => e.id === id); + if (i < 0) return false; + try { fs.unlinkSync(path.join(ordner, eintraege[i].datei)); } catch { /* schon weg */ } + eintraege.splice(i, 1); + return true; +} + +function loeschen(id) { + const eintraege = lies(); + const ok = loescheIntern(eintraege, id); + if (ok) schreibe(eintraege); + return ok; +} + +function umbenennen(id, name) { + const eintraege = lies(); + const e = eintraege.find((x) => x.id === id); + if (!e) return false; + e.name = String(name || '').trim().slice(0, 40) || e.name; + e.automatisch = false; // umbenannt = bewusst behalten + schreibe(eintraege); + return true; +} + +/** Eintrag samt Dateipfad (zum Anziehen). */ +function holen(id) { + const e = lies().find((x) => x.id === id); + return e ? Object.assign({}, e, { pfad: path.join(ordner, e.datei) }) : null; +} + +module.exports = { init, liste, hinzufuegen, loeschen, umbenennen, holen };