Files
AeroMc-Launcher/src/mmcimport.js
T
2026-10-04 18:46:00 +00:00

200 lines
7.1 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';
/*
* mmcimport.js – Instanzen aus MultiMC/Prism übernehmen
* ------------------------------------------------------
* Es genügt der Pfad zum MultiMC-Ordner (oder direkt zum instances-Ordner).
* Ausgelesen werden:
* instance.cfg – Name, Gruppe, Java-Pfad, Arbeitsspeicher, Notizen
* mmc-pack.json – Minecraft-Version und Mod-Loader
* .minecraft/ – Spielverzeichnis (Welten, Mods, Konfigurationen)
*/
const fs = require('fs');
const path = require('path');
// uid der Loader in mmc-pack.json -> unsere Bezeichnung
const LOADER_UIDS = {
'net.fabricmc.fabric-loader': 'fabric',
'org.quiltmc.quilt-loader': 'quilt',
'net.minecraftforge': 'forge',
'net.neoforged': 'neoforge',
};
function parseCfg(file) {
const out = {};
try {
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
const i = line.indexOf('=');
if (i > 0) out[line.slice(0, i).trim()] = line.slice(i + 1).trim();
}
} catch { /* Datei fehlt */ }
return out;
}
// Gruppen stehen zentral in instances/instgroups.json
function readGroups(instancesDir) {
const map = {};
try {
const raw = JSON.parse(fs.readFileSync(path.join(instancesDir, 'instgroups.json'), 'utf8'));
for (const [group, data] of Object.entries(raw.groups || {})) {
for (const inst of data.instances || []) map[inst] = group;
}
} catch { /* keine Gruppen hinterlegt */ }
return map;
}
function readPack(dir) {
const res = { version: '', loader: 'vanilla', loaderVersion: '' };
try {
const pack = JSON.parse(fs.readFileSync(path.join(dir, 'mmc-pack.json'), 'utf8'));
for (const c of pack.components || []) {
if (c.uid === 'net.minecraft') res.version = c.version || c.cachedVersion || '';
const loader = LOADER_UIDS[c.uid];
if (loader) { res.loader = loader; res.loaderVersion = c.version || ''; }
}
} catch { /* alte Instanz ohne mmc-pack.json */ }
return res;
}
// findet den instances-Ordner, egal ob MultiMC-Wurzel oder instances direkt angegeben wurde
function resolveInstancesDir(inputPath) {
if (!inputPath) return null;
const direct = path.join(inputPath, 'instances');
if (fs.existsSync(direct)) return direct;
if (path.basename(inputPath).toLowerCase() === 'instances' && fs.existsSync(inputPath)) return inputPath;
return fs.existsSync(inputPath) ? inputPath : null;
}
/*
* Liest einen einzelnen Instanzordner aus. null, wenn dort keine Instanz liegt.
* Wird sowohl beim Ordner-Import als auch beim ZIP-Import verwendet.
*/
function readInstance(dir, folderName, group) {
const cfgFile = path.join(dir, 'instance.cfg');
if (!fs.existsSync(cfgFile)) return null; // kein Instanzordner
const cfg = parseCfg(cfgFile);
const pack = readPack(dir);
// MultiMC nutzt .minecraft, Prism minecraft
const gameDir = ['.minecraft', 'minecraft']
.map((n) => path.join(dir, n))
.find((p) => fs.existsSync(p)) || null;
return {
folder: folderName,
dir,
name: cfg.name || folderName,
group: group || '',
notes: cfg.notes || '',
version: pack.version || cfg.IntendedVersion || '',
loader: pack.loader,
loaderVersion: pack.loaderVersion,
javaPath: cfg.OverrideJavaLocation === 'true' ? (cfg.JavaPath || '') : '',
minMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MinMemAlloc) || null : null,
maxMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MaxMemAlloc) || null : null,
gameDir,
hasGameData: !!gameDir,
};
}
/*
* Listet die gefundenen Instanzen auf (ohne etwas zu kopieren).
*/
function scan(inputPath) {
const instancesDir = resolveInstancesDir(inputPath);
if (!instancesDir) return { ok: false, reason: 'not-found' };
const groups = readGroups(instancesDir);
const list = [];
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const inst = readInstance(path.join(instancesDir, entry.name), entry.name, groups[entry.name]);
if (inst) list.push(inst);
}
list.sort((a, b) => a.name.localeCompare(b.name));
return { ok: true, instancesDir, count: list.length, instances: list };
}
/*
* Sucht einen Instanzordner unterhalb von root. Deckt beide Zip-Varianten ab:
* mit umgebendem Ordner ("26.2/instance.cfg") und ohne ("instance.cfg").
* Die Tiefe ist begrenzt, damit nicht das ganze .minecraft durchsucht wird.
*/
function findInstanceDir(root, tiefe = 2) {
if (!root || !fs.existsSync(root)) return null;
if (fs.existsSync(path.join(root, 'instance.cfg'))) return root;
if (tiefe <= 0) return null;
let eintraege = [];
try { eintraege = fs.readdirSync(root, { withFileTypes: true }); } catch { return null; }
for (const e of eintraege) {
if (!e.isDirectory()) continue;
// .minecraft enthält nie eine Instanz und ist der mit Abstand größte Ordner
if (e.name === '.minecraft' || e.name === 'minecraft') continue;
const treffer = findInstanceDir(path.join(root, e.name), tiefe - 1);
if (treffer) return treffer;
}
return null;
}
/*
* Liest eine einzelne, ausgepackte MultiMC/Prism-Instanz.
* -> { ok, instance } oder { ok: false, reason: 'not-found' }
*/
function scanSingle(root) {
const dir = findInstanceDir(root);
if (!dir) return { ok: false, reason: 'not-found' };
const inst = readInstance(dir, path.basename(dir), '');
return inst ? { ok: true, instance: inst } : { ok: false, reason: 'not-found' };
}
/*
* Übernimmt eine Instanz in den Launcher.
* store: unser Datenspeicher; copyData: Spielverzeichnis mitkopieren
*/
function importOne(store, entry, copyData = true) {
const created = store.createOrReplaceImportedInstance({
name: entry.name,
group: entry.group,
notes: entry.notes,
minecraft: {
version: entry.version,
loader: entry.loader,
loaderVersion: entry.loaderVersion,
},
});
const patch = {};
if (entry.javaPath || entry.minMemMb || entry.maxMemMb) {
patch.java = {
path: entry.javaPath || '',
minMemMb: entry.minMemMb || null,
maxMemMb: entry.maxMemMb || null,
extraArgs: '',
};
}
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
let copied = false;
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
try {
fs.cpSync(entry.gameDir, store.gameDir(created.id), { recursive: true });
copied = true;
} catch { /* Spieldaten konnten nicht kopiert werden */ }
}
return { id: created.id, name: created.name, copied };
}
/*
* Was beim Übernehmen kopiert wird: [{ von, name }] – `name` ist der Zielname
* im Spielordner der neuen Instanz. Das Kopieren selbst erledigt kopieren.js
* asynchron mit Fortschritt (main.js → fuehreImportAus).
*/
function kopierQuellen(entry) {
if (!entry.gameDir || !fs.existsSync(entry.gameDir)) return [];
return fs.readdirSync(entry.gameDir).map((name) => ({ von: path.join(entry.gameDir, name), name }));
}
module.exports = { scan, scanSingle, importOne, kopierQuellen, resolveInstancesDir, readInstance, findInstanceDir };