UNPKG

minecraft-core-master

Version:

Núcleo avanzado para launchers de Minecraft. Descarga, instala y ejecuta versiones de Minecraft, assets, librerías, Java y loaders de forma automática y eficiente.

372 lines (371 loc) 13.9 kB
import { mkdir } from "node:fs/promises"; import { createWriteStream, existsSync, unlinkSync } from "node:fs"; import { join, dirname } from "node:path"; import { EventEmitter } from "node:events"; import https from "node:https"; import { createTaskLimiter } from "../Utils/Index.js"; import { Unzipper } from "../Utils/Unzipper.js"; const agent = new https.Agent({ keepAlive: true, maxSockets: 200, maxFreeSockets: 100, }); export class NativesDownloader extends EventEmitter { version; root; concurry; maxRetries; limiter; paused = false; stopped = false; pendingQueue = []; runningTasks = 0; doneEmitted = false; unzipper; downloadedBytes = 0; constructor(opts) { super(); this.version = opts.version; this.root = opts.root; this.concurry = opts.concurry ?? 16; this.maxRetries = opts.maxRetries ?? 5; this.unzipper = new Unzipper(); this.limiter = createTaskLimiter(this.concurry); } async start() { this.emit("Start"); this.stopped = false; this.doneEmitted = false; this.downloadedBytes = 0; const manifest = await this.fetchVersionManifest(); const versionMeta = manifest.versions.find((v) => v.id === this.version); if (!versionMeta) throw new Error("Versión no encontrada: " + this.version); const versionJson = await this.downloadJSON(versionMeta.url); const nativeLibs = this.getNativeLibraries(versionJson.libraries); this.pendingQueue = []; for (const lib of nativeLibs) { const downloadInfo = this.getNativeDownloadInfo(lib); if (downloadInfo) { this.pendingQueue.push({ url: downloadInfo.url, sha1: downloadInfo.sha1, path: join(this.root, "libraries", downloadInfo.path), size: downloadInfo.size, retries: 0, libraryName: lib.name }); } } this.processQueue(); } pause() { this.paused = true; this.emit("Paused"); } resume() { if (!this.paused) return; this.paused = false; this.emit("Resumed"); this.processQueue(); } stop() { this.stopped = true; this.pendingQueue = []; this.emit("Stopped"); } async getTotalBytes() { const manifest = await this.fetchVersionManifest(); const versionMeta = manifest.versions.find((v) => v.id === this.version); if (!versionMeta) throw new Error("Versión no encontrada"); const versionJson = await this.downloadJSON(versionMeta.url); const nativeLibs = this.getNativeLibraries(versionJson.libraries); let total = 0; for (const lib of nativeLibs) { const downloadInfo = this.getNativeDownloadInfo(lib); if (downloadInfo && typeof downloadInfo.size === "number") { total += downloadInfo.size; } } return total; } processQueue() { if (this.paused || this.stopped) return; if (this.pendingQueue.length === 0) return this.checkDone(); while (!this.paused && !this.stopped && this.pendingQueue.length > 0 && this.runningTasks < this.concurry) { const task = this.pendingQueue.shift(); if (!task) continue; this.runningTasks++; this.limiter(() => this.downloadAndExtractNative(task)) .then(() => { // Éxito - no hacer nada }) .catch((error) => { console.error(`Error en descarga: ${error.message}`); }) .finally(() => { this.runningTasks--; this.checkDone(); if (!this.paused && !this.stopped) this.processQueue(); }); } } checkDone() { if (this.doneEmitted) return; if (this.pendingQueue.length === 0 && this.runningTasks === 0 && !this.paused && !this.stopped) { this.doneEmitted = true; this.emit("Done"); } } async downloadAndExtractNative(task) { await mkdir(dirname(task.path), { recursive: true }); await this.downloadFile(task); await this.extractNative(task); } async downloadFile(task) { return new Promise((resolve, reject) => { if (existsSync(task.path)) { unlinkSync(task.path); } const file = createWriteStream(task.path); let downloaded = 0; const req = https.get(task.url, { agent }, (res) => { if (res.statusCode !== 200) { file.destroy(); return this.retryOrFail(task, reject, `HTTP ${res.statusCode}`); } res.on("data", (chunk) => { if (this.paused || this.stopped) { res.destroy(); file.destroy(); return; } downloaded += chunk.length; this.downloadedBytes += chunk.length; this.emit("Bytes", chunk.length); }); res.pipe(file); file.on("finish", () => { file.close(() => { resolve(); }); }); }); file.on("error", (error) => { file.destroy(); this.retryOrFail(task, reject, error.message); }); req.on("error", (error) => { file.destroy(); this.retryOrFail(task, reject, error.message); }); req.setTimeout(30000, () => { req.destroy(); file.destroy(); this.retryOrFail(task, reject, "Timeout"); }); }); } async extractNative(task) { const nativesDir = join(this.root, "versions", this.version, "natives"); await mkdir(nativesDir, { recursive: true }); try { await this.unzipper.extract({ src: task.path, dest: nativesDir, validExts: ['.dll', '.so', '.dylib', '.jnilib'], flattenNatives: true, cleanAfter: true, ignoreFolders: ['META-INF'] }); } catch (error) { console.error(`Error extrayendo nativo ${task.libraryName}:`, error); } } retryOrFail(task, reject, reason) { if (existsSync(task.path)) { unlinkSync(task.path); } if (task.retries < this.maxRetries) { task.retries++; this.pendingQueue.push(task); reject(new Error(`Retrying: ${reason}`)); } else { reject(new Error(`No se pudo descargar: ${task.libraryName} - ${reason}`)); } } getOS() { const platform = process.platform; if (platform === 'win32') return 'windows'; if (platform === 'darwin') return 'osx'; if (platform === 'linux') return 'linux'; return platform; } getArchitecture() { const arch = process.arch; if (arch === 'x64') return 'x86_64'; if (arch === 'arm64') return 'arm64'; if (arch === 'ia32') return 'x86'; return arch; } shouldIncludeLibrary(library) { if (!library.rules) return true; const currentOS = this.getOS(); let allowed = true; for (const rule of library.rules) { if (rule.os) { const osMatches = rule.os.name === currentOS; const versionMatches = !rule.os.version || new RegExp(rule.os.version).test(process.version); if (rule.action === 'allow') { allowed = osMatches && versionMatches; } else if (rule.action === 'disallow' && osMatches && versionMatches) { allowed = false; } } else { // Reglas sin OS específico if (rule.action === 'allow') { allowed = true; } else if (rule.action === 'disallow') { allowed = false; } } } return allowed; } getNativeLibraries(libraries) { const currentOS = this.getOS(); const currentArch = this.getArchitecture(); return libraries.filter(lib => { if (!this.shouldIncludeLibrary(lib)) { return false; } // Patrón 1: Tiene natives definidos explícitamente if (lib.natives && lib.natives[currentOS]) { return true; } // Patrón 2: Tiene classifiers con nativos para este SO if (lib.downloads?.classifiers) { const hasNativeClassifier = Object.keys(lib.downloads.classifiers).some(key => key.includes(`natives-${currentOS}`)); if (hasNativeClassifier) return true; } // Patrón 3: Artifact directo con natives en el path (versiones modernas) if (lib.downloads?.artifact?.path.includes(`natives-${currentOS}`)) { return this.matchesArchitecture(lib.name + lib.downloads.artifact.path, currentArch); } // Patrón 4: Nombre de librería indica que es nativa if (lib.name.includes(`:natives-${currentOS}`)) { return this.matchesArchitecture(lib.name, currentArch); } return false; }); } matchesArchitecture(name, currentArch) { // Para arquitectura x86_64, debe coincidir con nombres que NO tengan arm64 if (currentArch === 'x86_64') { return !name.includes('arm64'); } // Para ARM64, debe coincidir explícitamente if (currentArch === 'arm64') { return name.includes('arm64'); } // Para x86, debe coincidir explícitamente if (currentArch === 'x86') { return name.includes('x86') || name.includes('32'); } // Para otras arquitecturas return name.includes(currentArch); } getNativeDownloadInfo(library) { const currentOS = this.getOS(); const currentArch = this.getArchitecture(); // CASO 1: Versiones antiguas con natives object y classifiers if (library.natives?.[currentOS]) { let classifierName = library.natives[currentOS]; // Manejar sustitución de arquitectura en nombres antiguos if (classifierName.includes('${arch}')) { const archSubstitution = currentArch === 'x86_64' ? '64' : '32'; classifierName = classifierName.replace('${arch}', archSubstitution); } const classifier = library.downloads.classifiers?.[classifierName]; if (classifier) { return classifier; } } // CASO 2: Classifiers directos (sin natives object) if (library.downloads?.classifiers) { // Buscar classifier que coincida con SO y arquitectura const nativeClassifierKey = Object.keys(library.downloads.classifiers).find(key => { if (!key.includes(`natives-${currentOS}`)) return false; return this.matchesArchitecture(key, currentArch); }); if (nativeClassifierKey) { return library.downloads.classifiers[nativeClassifierKey]; } // Fallback: cualquier classifier para el SO si no hay coincidencia de arquitectura const fallbackClassifierKey = Object.keys(library.downloads.classifiers).find(key => key.includes(`natives-${currentOS}`)); if (fallbackClassifierKey) { return library.downloads.classifiers[fallbackClassifierKey]; } } // CASO 3: Versiones modernas con artifact directo if (library.downloads?.artifact?.path.includes(`natives-${currentOS}`) && this.matchesArchitecture(library.downloads.artifact.path, currentArch)) { return library.downloads.artifact; } // CASO 4: Último recurso - artifact directo con natives en el path if (library.downloads?.artifact?.path.includes(`natives-${currentOS}`)) { return library.downloads.artifact; } return null; } async fetchVersionManifest() { const url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; return await this.downloadJSON(url); } async downloadJSON(url) { return new Promise((resolve, reject) => { https .get(url, { agent }, (res) => { if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}: ${url}`)); return; } let data = ""; res.on("data", (c) => (data += c)); res.on("end", () => { try { resolve(JSON.parse(data)); } catch (error) { reject(new Error(`Invalid JSON from ${url}: ${error}`)); } }); }) .on("error", reject); }); } getDownloadedBytes() { return this.downloadedBytes; } }