173 lines
6.7 KiB
JavaScript
173 lines
6.7 KiB
JavaScript
'use strict';
|
||
|
||
/*
|
||
* technicimport.js – Instanzen aus dem Technic Launcher übernehmen
|
||
* -------------------------------------------------------------------
|
||
* Der Technic Launcher legt jedes installierte Modpack als eigenen Ordner
|
||
* unter "<Technic-Wurzel>/modpacks/<name>" an (Standard unter Windows:
|
||
* %APPDATA%\.technic\modpacks). Darin liegen "bin" (Launcher-interner
|
||
* Jar-/Versions-Cache), "config" und "mods" als Geschwisterordner – Welten,
|
||
* Ressourcenpakete usw. landen ebenfalls direkt im Pack-Ordner, genau wie
|
||
* bei einem klassischen .minecraft.
|
||
*
|
||
* Es gibt keine für uns nutzbare, stabil dokumentierte JSON-Datei mit einem
|
||
* schönen Anzeigenamen – "installedPacks" im Wurzelordner listet Packs nur
|
||
* über ihren Ordnernamen (Slug). Wir lesen sie deshalb nur als optionale,
|
||
* fehlertolerante Ergänzung; die eigentliche Erkennung läuft – wie bei den
|
||
* anderen Importern – über die tatsächlich vorhandenen Ordner/Dateien.
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const forge = require('./forge');
|
||
|
||
// Ordner, die zum Technic-internen Verwaltungs-/Jar-Cache gehören und beim
|
||
// Übernehmen NICHT mit ins Spielverzeichnis der neuen Instanz sollen.
|
||
const SKIP_DIRS = new Set(['bin']);
|
||
|
||
function technicRoot() {
|
||
return path.join(process.env.APPDATA || '', '.technic');
|
||
}
|
||
|
||
function readJson(file) {
|
||
try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')); }
|
||
catch { return null; }
|
||
}
|
||
|
||
// findet den modpacks-Ordner, egal ob Technic-Wurzel, modpacks-Ordner direkt
|
||
// oder ein einzelner Pack-Ordner angegeben wurde
|
||
function resolveModpacksDir(inputPath) {
|
||
const root = inputPath && inputPath.trim() ? inputPath.trim() : technicRoot();
|
||
if (!fs.existsSync(root)) return null;
|
||
|
||
const direct = path.join(root, 'modpacks');
|
||
if (fs.existsSync(direct)) return direct;
|
||
if (path.basename(root).toLowerCase() === 'modpacks') return root;
|
||
return root; // z. B. ein selbst gewählter Ordner mit Pack-Unterordnern
|
||
}
|
||
|
||
// ein Pack-Ordner hat i. d. R. bin/, config/ und/oder mods/ als Geschwister
|
||
function looksLikePack(dir) {
|
||
const markers = ['bin', 'mods', 'config', 'saves', 'resourcepacks', 'options.txt'];
|
||
return markers.some((name) => fs.existsSync(path.join(dir, name)));
|
||
}
|
||
|
||
// optionale Anreicherung aus der "installedPacks"-Datei der Technic-Wurzel;
|
||
// bewusst rein additiv und fehlertolerant, da das Format nicht offiziell
|
||
// als stabile API dokumentiert ist
|
||
function readInstalledPacksMeta(root) {
|
||
const data = readJson(path.join(root, 'installedPacks'));
|
||
const packs = data && data.installedPacks;
|
||
return packs && typeof packs === 'object' ? packs : {};
|
||
}
|
||
|
||
// versucht, aus bin/modpack.jar (version.json) oder der alten bin/version
|
||
// -Textdatei Minecraft-Version und Mod-Loader zu bestimmen. Rein
|
||
// bestmöglich – schlägt die Erkennung fehl, bleibt die Instanz einfach
|
||
// ohne Version/Loader stehen und lässt sich manuell nachtragen.
|
||
function detectVersion(packDir) {
|
||
const bin = path.join(packDir, 'bin');
|
||
const jarFile = path.join(bin, 'modpack.jar');
|
||
if (fs.existsSync(jarFile)) {
|
||
try {
|
||
const buf = fs.readFileSync(jarFile);
|
||
const idx = forge.readZipIndex(buf);
|
||
const entry = idx['version.json'];
|
||
if (entry) {
|
||
// manche Jars speichern JSON mit BOM – ohne Entfernen bricht JSON.parse
|
||
const text = forge.readZipEntry(buf, entry).toString('utf8').replace(/^/, '');
|
||
const profile = JSON.parse(text);
|
||
const libs = Array.isArray(profile.libraries) ? profile.libraries : [];
|
||
let loader = 'vanilla';
|
||
let loaderVersion = '';
|
||
for (const lib of libs) {
|
||
const name = String((lib && lib.name) || '');
|
||
if (/net\.minecraftforge:forge:/i.test(name)) {
|
||
loader = 'forge';
|
||
loaderVersion = (name.split(':')[2] || '').split('-').pop() || '';
|
||
} else if (/net\.neoforged:neoforge:/i.test(name) && loader === 'vanilla') {
|
||
loader = 'neoforge';
|
||
loaderVersion = name.split(':')[2] || '';
|
||
}
|
||
}
|
||
const id = String(profile.id || profile.inheritsFrom || '');
|
||
const version = String(profile.inheritsFrom || id.split('-')[0] || '').trim();
|
||
if (version) return { version, loader, loaderVersion };
|
||
}
|
||
} catch { /* modpack.jar nicht lesbar/kein gültiges ZIP – weiter unten Fallback */ }
|
||
}
|
||
// ältere Packs hinterlegen nur eine reine Textdatei mit der MC-Version
|
||
const legacy = path.join(bin, 'version');
|
||
try {
|
||
if (fs.existsSync(legacy)) {
|
||
const version = fs.readFileSync(legacy, 'utf8').trim();
|
||
if (version) return { version, loader: 'vanilla', loaderVersion: '' };
|
||
}
|
||
} catch { /* egal, bleibt leer */ }
|
||
return { version: '', loader: 'vanilla', loaderVersion: '' };
|
||
}
|
||
|
||
function scan(inputPath) {
|
||
const modpacksDir = resolveModpacksDir(inputPath);
|
||
if (!modpacksDir || !fs.existsSync(modpacksDir)) return { ok: false, reason: 'not-found' };
|
||
|
||
const root = path.basename(modpacksDir).toLowerCase() === 'modpacks'
|
||
? path.dirname(modpacksDir) : modpacksDir;
|
||
const meta = readInstalledPacksMeta(root);
|
||
|
||
const list = [];
|
||
for (const entry of fs.readdirSync(modpacksDir, { withFileTypes: true })) {
|
||
if (!entry.isDirectory()) continue;
|
||
const dir = path.join(modpacksDir, entry.name);
|
||
if (!looksLikePack(dir)) continue;
|
||
|
||
const { version, loader, loaderVersion } = detectVersion(dir);
|
||
const info = meta[entry.name] || {};
|
||
list.push({
|
||
folder: entry.name,
|
||
dir,
|
||
name: info.displayName || entry.name,
|
||
group: '',
|
||
notes: '',
|
||
version,
|
||
loader,
|
||
loaderVersion,
|
||
gameDir: dir,
|
||
hasGameData: true,
|
||
});
|
||
}
|
||
list.sort((a, b) => a.name.localeCompare(b.name));
|
||
return { ok: true, instancesDir: modpacksDir, count: list.length, instances: list };
|
||
}
|
||
|
||
// kopiert den Pack-Ordner, lässt aber den Technic-internen bin-Cache weg
|
||
function copyInto(sourceDir, targetDir) {
|
||
fs.mkdirSync(targetDir, { recursive: true });
|
||
for (const name of fs.readdirSync(sourceDir)) {
|
||
if (SKIP_DIRS.has(name)) continue;
|
||
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
||
}
|
||
}
|
||
|
||
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,
|
||
},
|
||
});
|
||
|
||
let copied = false;
|
||
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
||
copyInto(entry.gameDir, store.gameDir(created.id));
|
||
copied = true;
|
||
}
|
||
return { id: created.id, name: created.name, copied };
|
||
}
|
||
|
||
module.exports = { scan, importOne, resolveModpacksDir, technicRoot, detectVersion };
|