Files
AeroMc-Launcher/src/bedrock.js
T

243 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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';
// Der Teil nach "!" (Application Id) ist NICHT stabil – Microsoft hat ihn
// schon mindestens einmal geändert (früher "App", inzwischen "Game").
// Deshalb zur Laufzeit über Get-StartApps auflösen statt fest zu verdrahten;
// dieser Wert dient nur als letzter Rückfallwert, falls die Auflösung
// fehlschlägt (z. B. PowerShell blockiert).
const FALLBACK_APP_ID = PKG_FAMILY + '!Game';
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
// ---------------------------------------------------------------------------
let cachedAppId = null;
/*
* Ermittelt die aktuell gültige AUMID über Get-StartApps – das ist derselbe
* Weg, den auch das Startmenü nutzt, und bildet daher immer die Application
* Id ab, die Windows gerade tatsächlich zum Starten akzeptiert.
*/
async function resolveAppId() {
if (cachedAppId) return cachedAppId;
const out = await runPwsh(
`(Get-StartApps | Where-Object { $_.AppID -like '${PKG_FAMILY}*' } `
+ `| Select-Object -First 1 -ExpandProperty AppID)`);
cachedAppId = out || FALLBACK_APP_ID;
return cachedAppId;
}
/*
* Startet Bedrock direkt.
* Wichtig: Der AppsFolder-Pfad darf NICHT in Anführungszeichen stehen – sonst
* öffnet der Explorer nur ein Ordnerfenster. Daher windowsVerbatimArguments.
*/
async function launch() {
const appId = await resolveAppId();
const target = 'shell:appsFolder\\' + appId;
// Wichtig: Es darf pro Klick nur EINE Aktivierung tatsächlich lostreten.
// Die App-Aktivierung lässt sich nicht abbrechen – wenn wir bei einer
// Verzögerung (Xbox-Dienste, Kaltstart) automatisch einen zweiten Weg
// probieren, laufen im Zweifel beide an und Bedrock öffnet sich doppelt.
// Die anderen Wege dienen daher nur als Rückfall, falls das Spawnen
// selbst scheitert (Programm nicht gefunden o. Ä.), nicht als
// Reaktion auf einen bloß langsamen Start.
const versuche = [
// 1) explorer direkt, ohne zusätzliche Anführungszeichen – entspricht
// exakt dem manuellen "explorer.exe shell:appsFolder\...", der
// nachweislich zuverlässig funktioniert
['explorer.exe', [target], { windowsVerbatimArguments: true }],
// 2) über die Eingabeaufforderung
['cmd', ['/c', 'start', '', target], { windowsVerbatimArguments: true }],
// 3) Rückfallweg über eine versteckte PowerShell – aus einem
// windowless/detached Kindprozess heraus offenbar nicht überall
// zuverlässig, daher nur letzter Versuch
['powershell', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command',
`Start-Process explorer.exe -ArgumentList '${target}'`], {}],
];
let methode = null;
for (const [cmd, args, extra] of versuche) {
try {
const p = spawn(cmd, args, Object.assign({ detached: true, stdio: 'ignore', windowsHide: true }, extra));
p.unref();
methode = cmd;
break; // nur diesen einen Weg tatsächlich auslösen
} catch { /* Spawnen selbst fehlgeschlagen – nächsten Weg versuchen */ }
}
if (!methode) return { ok: false, message: 'Bedrock konnte nicht gestartet werden.' };
// Aktivierung kann dauern (v. a. beim ersten Start nach längerer Pause) –
// hier nur zur ehrlichen Rückmeldung abwarten, OHNE bei Zeitüberschreitung
// einen weiteren Versuch zu starten (siehe Hinweis oben).
const bestaetigt = await waitForBedrockProcess(15000);
return { ok: true, method: methode, confirmed: bestaetigt };
}
function isBedrockRunning() {
return new Promise((resolve) => {
execFile('tasklist', ['/FI', 'IMAGENAME eq Minecraft.Windows.exe', '/FO', 'CSV', '/NH'],
{ windowsHide: true, timeout: 5000 },
(err, stdout) => resolve(!err && /Minecraft\.Windows\.exe/i.test(String(stdout || ''))));
});
}
function warten(ms) {
return new Promise((resolve) => { setTimeout(resolve, ms); });
}
async function waitForBedrockProcess(timeoutMs) {
const start = Date.now();
// sofort prüfen (falls schon ein Fenster lief) und danach im Kurztakt
for (;;) {
if (await isBedrockRunning()) return true;
if (Date.now() - start >= timeoutMs) return false;
await warten(500);
}
}
// ---------------------------------------------------------------------------
// 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,
};