200 lines
7.1 KiB
JavaScript
200 lines
7.1 KiB
JavaScript
'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 };
|