Upload via GUI (19 Dateien)
This commit is contained in:
+308
@@ -0,0 +1,308 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* store.js – Persistenz für AeroMC
|
||||
* ------------------------------------
|
||||
* Jede Instanz ist ein isolierter Ordner (MultiMC-Prinzip):
|
||||
* <instancesDir>/<id>/instance.json – Metadaten
|
||||
* <instancesDir>/<id>/minecraft/ – das eigentliche Spielverzeichnis (.minecraft-Ersatz)
|
||||
*
|
||||
* Globale Einstellungen liegen in <userData>/settings.json.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
let USER_DATA = null; // wird von main.js via init() gesetzt
|
||||
|
||||
function init(userDataPath) {
|
||||
USER_DATA = userDataPath;
|
||||
ensureDir(USER_DATA);
|
||||
ensureDir(getInstancesDir());
|
||||
}
|
||||
|
||||
// ---------- kleine Helfer ----------
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function readJson(file, fallback) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(file, data) {
|
||||
ensureDir(path.dirname(file));
|
||||
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function newId() {
|
||||
return crypto.randomBytes(8).toString('hex');
|
||||
}
|
||||
|
||||
// ---------- Einstellungen ----------
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
instancesDir: null, // null => Standard (userData/instances)
|
||||
javaPath: '', // leer => "java" aus PATH
|
||||
defaultMinMemMb: 2048,
|
||||
defaultMaxMemMb: 4096,
|
||||
azureClientId: '', // Azure-App-ID für den Microsoft-Login (Phase 3)
|
||||
globalPreLaunch: '',
|
||||
globalPostExit: '',
|
||||
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
|
||||
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
||||
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
||||
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
||||
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
||||
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
|
||||
proxyHost: '',
|
||||
proxyPort: '',
|
||||
proxyUser: '',
|
||||
proxyPass: '',
|
||||
theme: 'dark',
|
||||
};
|
||||
|
||||
function settingsFile() {
|
||||
return path.join(USER_DATA, 'settings.json');
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
return Object.assign({}, DEFAULT_SETTINGS, readJson(settingsFile(), {}));
|
||||
}
|
||||
|
||||
function saveSettings(patch) {
|
||||
const merged = Object.assign({}, getSettings(), patch || {});
|
||||
writeJson(settingsFile(), merged);
|
||||
// Falls sich das Instanzverzeichnis geändert hat: sicherstellen, dass es existiert
|
||||
ensureDir(getInstancesDir());
|
||||
return merged;
|
||||
}
|
||||
|
||||
// ---------- Instanzen ----------
|
||||
|
||||
function getInstancesDir() {
|
||||
const s = getSettings();
|
||||
return s.instancesDir && s.instancesDir.trim()
|
||||
? s.instancesDir
|
||||
: path.join(USER_DATA, 'instances');
|
||||
}
|
||||
|
||||
function instancePath(id) {
|
||||
return path.join(getInstancesDir(), id);
|
||||
}
|
||||
|
||||
function instanceFile(id) {
|
||||
return path.join(instancePath(id), 'instance.json');
|
||||
}
|
||||
|
||||
function gameDir(id) {
|
||||
return path.join(instancePath(id), 'minecraft');
|
||||
}
|
||||
|
||||
function modsDir(id) {
|
||||
return path.join(gameDir(id), 'mods');
|
||||
}
|
||||
|
||||
function modsIndexFile(id) {
|
||||
return path.join(instancePath(id), 'mods.json');
|
||||
}
|
||||
|
||||
// ---------- installierte Mods (Metadaten je Instanz) ----------
|
||||
|
||||
function listMods(id) {
|
||||
return readJson(modsIndexFile(id), []);
|
||||
}
|
||||
|
||||
function addMod(id, entry) {
|
||||
const list = listMods(id).filter((m) => m.filename !== entry.filename);
|
||||
list.push(entry);
|
||||
writeJson(modsIndexFile(id), list);
|
||||
return list;
|
||||
}
|
||||
|
||||
function removeMod(id, filename) {
|
||||
const list = listMods(id).filter((m) => m.filename !== filename);
|
||||
writeJson(modsIndexFile(id), list);
|
||||
// zugehörige .jar löschen
|
||||
const jar = path.join(modsDir(id), filename);
|
||||
try { if (fs.existsSync(jar)) fs.unlinkSync(jar); } catch { /* egal */ }
|
||||
return list;
|
||||
}
|
||||
|
||||
function defaultInstance(name) {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: newId(),
|
||||
name: name || 'Neue Instanz',
|
||||
icon: 'grass', // Schlüssel für ein Emoji/Icon im Renderer
|
||||
customImage: null, // Dateiname eines eigenen Bildes im Instanzordner (überschreibt Emoji)
|
||||
group: '', // optionale Gruppe (MultiMC-artig)
|
||||
preLaunchCommand: '', // Befehl vor dem Start
|
||||
postExitCommand: '', // Befehl nach dem Beenden
|
||||
created: now,
|
||||
lastPlayed: null,
|
||||
totalPlaySeconds: 0,
|
||||
minecraft: {
|
||||
version: '', // z. B. "1.21.4" – wird in Phase 2 aus dem Mojang-Manifest befüllt
|
||||
loader: 'vanilla', // vanilla | fabric | forge | neoforge | quilt
|
||||
loaderVersion: '',
|
||||
},
|
||||
java: {
|
||||
path: '', // leer => globale Einstellung
|
||||
minMemMb: null, // null => globale Einstellung
|
||||
maxMemMb: null,
|
||||
extraArgs: '',
|
||||
},
|
||||
notes: '',
|
||||
};
|
||||
}
|
||||
|
||||
// liest ein eigenes Instanz-Bild als data:-URI (für die Anzeige im Renderer)
|
||||
function iconDataUri(id) {
|
||||
const meta = readJson(instanceFile(id), null);
|
||||
if (!meta || !meta.customImage) return null;
|
||||
try {
|
||||
const buf = fs.readFileSync(path.join(instancePath(id), meta.customImage));
|
||||
let ext = path.extname(meta.customImage).slice(1).toLowerCase();
|
||||
if (ext === 'jpg') ext = 'jpeg';
|
||||
return `data:image/${ext};base64,` + buf.toString('base64');
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function setCustomImage(id, sourcePath) {
|
||||
let ext = path.extname(sourcePath).toLowerCase();
|
||||
if (!ext) ext = '.png';
|
||||
const dest = 'icon' + ext;
|
||||
fs.copyFileSync(sourcePath, path.join(instancePath(id), dest));
|
||||
return updateInstance(id, { customImage: dest });
|
||||
}
|
||||
|
||||
function clearCustomImage(id) {
|
||||
const meta = getInstance(id);
|
||||
if (meta && meta.customImage) {
|
||||
try { fs.unlinkSync(path.join(instancePath(id), meta.customImage)); } catch { /* egal */ }
|
||||
}
|
||||
return updateInstance(id, { customImage: null });
|
||||
}
|
||||
|
||||
function listInstances() {
|
||||
const dir = getInstancesDir();
|
||||
ensureDir(dir);
|
||||
const result = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const meta = readJson(path.join(dir, entry.name, 'instance.json'), null);
|
||||
if (meta && meta.id) {
|
||||
// eigenes Bild direkt für die Anzeige mitliefern (nicht persistiert)
|
||||
if (meta.customImage) meta.iconData = iconDataUri(meta.id);
|
||||
result.push(meta);
|
||||
}
|
||||
}
|
||||
// zuletzt gespielt zuerst, dann alphabetisch
|
||||
result.sort((a, b) => {
|
||||
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
|
||||
if (a.lastPlayed) return -1;
|
||||
if (b.lastPlayed) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getInstance(id) {
|
||||
return readJson(instanceFile(id), null);
|
||||
}
|
||||
|
||||
// Ordnername aus dem Instanznamen ableiten (wie MultiMC), ungültige Zeichen raus
|
||||
function slugForName(name) {
|
||||
const base = String(name || 'Instanz').trim()
|
||||
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '')
|
||||
.replace(/[.\s]+$/, '')
|
||||
.trim();
|
||||
return (base || 'Instanz').slice(0, 60);
|
||||
}
|
||||
// eindeutiger Ordnername (bei Kollision "Name 2", "Name 3", …)
|
||||
function uniqueFolderId(name) {
|
||||
const dir = getInstancesDir();
|
||||
const base = slugForName(name);
|
||||
let id = base;
|
||||
let n = 2;
|
||||
while (fs.existsSync(path.join(dir, id))) { id = base + ' ' + n; n += 1; }
|
||||
return id;
|
||||
}
|
||||
|
||||
function createInstance(data) {
|
||||
const inst = defaultInstance(data && data.name);
|
||||
inst.id = uniqueFolderId(inst.name); // Ordner = Instanzname statt Zufalls-ID
|
||||
if (data) {
|
||||
if (data.icon) inst.icon = data.icon;
|
||||
if (data.minecraft) Object.assign(inst.minecraft, data.minecraft);
|
||||
if (data.notes) inst.notes = data.notes;
|
||||
if (data.group !== undefined) inst.group = data.group;
|
||||
}
|
||||
ensureDir(gameDir(inst.id));
|
||||
writeJson(instanceFile(inst.id), inst);
|
||||
return inst;
|
||||
}
|
||||
|
||||
function updateInstance(id, patch) {
|
||||
const inst = getInstance(id);
|
||||
if (!inst) throw new Error('Instanz nicht gefunden: ' + id);
|
||||
// flaches Merge mit Sonderbehandlung der verschachtelten Objekte
|
||||
const next = Object.assign({}, inst, patch);
|
||||
if (patch.minecraft) next.minecraft = Object.assign({}, inst.minecraft, patch.minecraft);
|
||||
if (patch.java) next.java = Object.assign({}, inst.java, patch.java);
|
||||
writeJson(instanceFile(id), next);
|
||||
return next;
|
||||
}
|
||||
|
||||
function deleteInstance(id) {
|
||||
const p = instancePath(id);
|
||||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
function duplicateInstance(id) {
|
||||
const src = getInstance(id);
|
||||
if (!src) throw new Error('Instanz nicht gefunden: ' + id);
|
||||
const copy = JSON.parse(JSON.stringify(src));
|
||||
copy.name = src.name + ' (Kopie)';
|
||||
copy.id = uniqueFolderId(copy.name);
|
||||
copy.created = new Date().toISOString();
|
||||
copy.lastPlayed = null;
|
||||
copy.totalPlaySeconds = 0;
|
||||
ensureDir(gameDir(copy.id));
|
||||
// Spielinhalte mitkopieren (Mods/Configs) – ohne Fehler wenn leer
|
||||
try {
|
||||
fs.cpSync(gameDir(id), gameDir(copy.id), { recursive: true });
|
||||
} catch { /* Quelle evtl. leer */ }
|
||||
writeJson(instanceFile(copy.id), copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
getSettings,
|
||||
saveSettings,
|
||||
getInstancesDir,
|
||||
instancePath,
|
||||
gameDir,
|
||||
modsDir,
|
||||
iconDataUri,
|
||||
setCustomImage,
|
||||
clearCustomImage,
|
||||
listMods,
|
||||
addMod,
|
||||
removeMod,
|
||||
listInstances,
|
||||
getInstance,
|
||||
createInstance,
|
||||
updateInstance,
|
||||
deleteInstance,
|
||||
duplicateInstance,
|
||||
};
|
||||
Reference in New Issue
Block a user