diff --git a/README.md b/README.md index 53db327..4460b8c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # AeroMC -**Ein instanz-basierter Minecraft-Launcher für Windows** +**Ein instanz-basierter Minecraft-Launcher für Windows – à la MultiMC / Prism.** Verwalte beliebig viele isolierte Minecraft-Instanzen mit eigener Version, eigenen Mods und eigenem Java – lade Mods per Ein-Klick von Modrinth, melde dich mit deinem Microsoft-Konto an und spiele. diff --git a/package.json b/package.json index 1026701..a90983d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aeromc", - "version": "0.33.1", + "version": "0.37.1", "description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)", "main": "src/main.js", "author": "AeroMC", diff --git a/src/bedrock.js b/src/bedrock.js new file mode 100644 index 0000000..6939aa6 --- /dev/null +++ b/src/bedrock.js @@ -0,0 +1,180 @@ +'use strict'; + +/* + * bedrock.js – Unterstützung für Minecraft Bedrock (Windows-Store-App) + * -------------------------------------------------------------------- + * Bedrock ist keine Java-Anwendung, sondern eine UWP-App. Der Launcher kann + * sie daher nicht herunterladen oder in Instanzen aufteilen – wohl aber + * starten und die Spieldaten (Welten, Add-ons, Pakete, Bildschirmfotos) + * verwalten. Die Installation selbst bleibt Sache des Microsoft Store. + */ + +const fs = require('fs'); +const path = require('path'); +const { execFile, spawn } = require('child_process'); + +const PKG_FAMILY = 'Microsoft.MinecraftUWP_8wekyb3d8bbwe'; +const APP_ID = PKG_FAMILY + '!App'; + +function comMojangDir() { + return path.join(process.env.LOCALAPPDATA || '', 'Packages', PKG_FAMILY, + 'LocalState', 'games', 'com.mojang'); +} + +// --------------------------------------------------------------------------- +// Erkennung +// --------------------------------------------------------------------------- + +function runPwsh(cmd) { + return new Promise((resolve) => { + execFile('powershell', ['-NoProfile', '-NonInteractive', '-Command', cmd], + { windowsHide: true, timeout: 15000 }, + (err, stdout) => resolve(err ? '' : String(stdout || '').trim())); + }); +} + +async function detect() { + const dir = comMojangDir(); + const version = await runPwsh( + "(Get-AppxPackage -Name 'Microsoft.MinecraftUWP' -ErrorAction SilentlyContinue).Version"); + const installed = !!version; + return { + installed, + version: version || null, + dataDir: fs.existsSync(dir) ? dir : null, + }; +} + +// --------------------------------------------------------------------------- +// Start +// --------------------------------------------------------------------------- + +/* + * Startet Bedrock direkt. + * Wichtig: Der AppsFolder-Pfad darf NICHT in Anführungszeichen stehen – sonst + * öffnet der Explorer nur ein Ordnerfenster. Daher windowsVerbatimArguments. + */ +function launch() { + const target = 'shell:appsFolder\\' + APP_ID; + const versuche = [ + // 1) genau der Aufruf, der sich unter Windows als zuverlässig erwiesen hat + ['powershell', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', + `Start-Process explorer.exe -ArgumentList '${target}'`], {}], + // 2) explorer direkt, ohne zusätzliche Anführungszeichen + ['explorer.exe', [target], { windowsVerbatimArguments: true }], + // 3) über die Eingabeaufforderung + ['cmd', ['/c', 'start', '', target], { windowsVerbatimArguments: true }], + ]; + for (const [cmd, args, extra] of versuche) { + try { + const p = spawn(cmd, args, Object.assign({ detached: true, stdio: 'ignore', windowsHide: true }, extra)); + p.unref(); + return { ok: true, method: cmd }; + } catch { /* nächsten Weg versuchen */ } + } + return { ok: false, message: 'Bedrock konnte nicht gestartet werden.' }; +} + +// --------------------------------------------------------------------------- +// Welten / Pakete / Bildschirmfotos +// --------------------------------------------------------------------------- + +function readLevelName(worldDir) { + // levelname.txt enthält den im Spiel angezeigten Namen + try { + const p = path.join(worldDir, 'levelname.txt'); + if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8').trim(); + } catch { /* Ordnername verwenden */ } + return path.basename(worldDir); +} + +function dirSize(dir, depth = 3) { + let total = 0; + if (depth < 0) return 0; + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return 0; } + for (const e of entries) { + const p = path.join(dir, e.name); + try { + if (e.isDirectory()) total += dirSize(p, depth - 1); + else total += fs.statSync(p).size; + } catch { /* überspringen */ } + } + return total; +} + +function listWorlds() { + const base = path.join(comMojangDir(), 'minecraftWorlds'); + if (!fs.existsSync(base)) return []; + return fs.readdirSync(base, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => { + const dir = path.join(base, e.name); + let modified = null; + try { modified = fs.statSync(dir).mtime.toISOString(); } catch { /* egal */ } + return { id: e.name, name: readLevelName(dir), dir, modified, bytes: dirSize(dir) }; + }) + .sort((a, b) => String(b.modified).localeCompare(String(a.modified))); +} + +// Ordnerarten, die verwaltet werden können +const FOLDERS = { + worlds: 'minecraftWorlds', + resource_packs: 'resource_packs', + behavior_packs: 'behavior_packs', + skin_packs: 'skin_packs', + screenshots: 'Screenshots', +}; + +function listFolder(kind) { + const sub = FOLDERS[kind]; + if (!sub) return []; + const base = path.join(comMojangDir(), sub); + if (!fs.existsSync(base)) return []; + return fs.readdirSync(base, { withFileTypes: true }) + .filter((e) => !e.name.startsWith('.')) + .map((e) => { + const p = path.join(base, e.name); + let bytes = 0, modified = null; + try { + const st = fs.statSync(p); + modified = st.mtime.toISOString(); + bytes = e.isDirectory() ? dirSize(p, 2) : st.size; + } catch { /* egal */ } + return { name: e.name, isDir: e.isDirectory(), path: p, bytes, modified }; + }); +} + +function folderPath(kind) { + const sub = FOLDERS[kind]; + return sub ? path.join(comMojangDir(), sub) : null; +} + +function removeEntry(kind, name) { + const base = folderPath(kind); + if (!base) return false; + const target = path.join(base, name); + if (!target.startsWith(base + path.sep)) return false; // Pfad-Ausbruch verhindern + if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true }); + return true; +} + +/* + * Add-on installieren: .mcpack/.mcaddon/.mcworld sind ZIP-Dateien. + * Der Doppelklick-Weg über die Shell ist der zuverlässigste, weil Minecraft + * die Pakete selbst einsortiert (inkl. Abhängigkeiten). + */ +function installAddon(filePath) { + if (!fs.existsSync(filePath)) return { ok: false, message: 'Datei nicht gefunden.' }; + if (!/\.(mcpack|mcaddon|mcworld|mctemplate)$/i.test(filePath)) { + return { ok: false, message: 'Nur .mcpack, .mcaddon, .mcworld oder .mctemplate möglich.' }; + } + const child = spawn('cmd', ['/c', 'start', '', filePath], { detached: true, stdio: 'ignore' }); + child.unref(); + return { ok: true }; +} + +module.exports = { + detect, launch, listWorlds, listFolder, folderPath, removeEntry, installAddon, + comMojangDir, FOLDERS, +}; diff --git a/src/forge.js b/src/forge.js index 3e44623..7c5fb15 100644 --- a/src/forge.js +++ b/src/forge.js @@ -113,6 +113,20 @@ async function installForge(opts) { const libDir = path.join(sharedDir, 'libraries'); const java = javaPath && javaPath.trim() ? javaPath : 'java'; + // Bereits installiert? Dann Prozessoren überspringen (spart ~30 s pro Start) + const markName = `${loader}-${mcVersion}-${loaderVersion}.installed`; + const markPath = path.join(sharedDir, 'versions', markName); + if (fs.existsSync(markPath)) { + try { + const cachedId = fs.readFileSync(markPath, 'utf8').trim(); + const vjFile = path.join(sharedDir, 'versions', cachedId, cachedId + '.json'); + if (fs.existsSync(vjFile)) { + onProgress({ phase: 'loader', detail: `${loader} ${loaderVersion} bereits installiert` }); + return JSON.parse(fs.readFileSync(vjFile, 'utf8')); + } + } catch { /* Merker unbrauchbar -> neu installieren */ } + } + onProgress({ phase: 'loader', detail: `${loader} ${loaderVersion} – Installer laden …` }); const buf = await fetchBuffer(installerUrl(loader, mcVersion, loaderVersion)); const zip = readZipIndex(buf); @@ -226,6 +240,9 @@ async function installForge(opts) { } } + // Installation als abgeschlossen vermerken + try { fs.writeFileSync(markPath, versionJson.id); } catch { /* egal */ } + return versionJson; } diff --git a/src/index.html b/src/index.html index 1835bad..50b9c6c 100644 --- a/src/index.html +++ b/src/index.html @@ -15,6 +15,7 @@ + @@ -213,6 +214,12 @@ Hier werden alle Instanzen als Unterordner (mit ihrem Namen) gespeichert. + + + + Wird automatisch ausgeblendet, wenn Bedrock nicht installiert ist. +