Upload via GUI (146 Dateien)
This commit is contained in:
+227
-218
@@ -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\.<init>/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 };
|
||||
|
||||
+39
-4
@@ -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,<svg xmlns='http://www.w3.org/2000/svg' width='512' height='48' shape-rendering='crispEdges' fill='white'><rect x='20' y='8' width='56' height='8'/><rect x='8' y='16' width='96' height='8'/><rect x='170' y='22' width='40' height='6'/><rect x='158' y='28' width='80' height='8'/><rect x='184' y='36' width='36' height='4'/><rect x='300' y='4' width='72' height='8'/><rect x='288' y='12' width='120' height='8'/><rect x='312' y='20' width='64' height='6'/><rect x='438' y='26' width='48' height='8'/><rect x='426' y='34' width='70' height='6'/></svg>") 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}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user