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.

266 lines (265 loc) 10.9 kB
import { createWriteStream, mkdirSync, existsSync, readFileSync } from 'fs'; import { stat, unlink } from 'node:fs/promises'; import { join, dirname, resolve, basename } from 'path'; import { EventEmitter } from 'node:events'; import { createTaskLimiter } from "../Utils/Index.js"; import https from 'https'; import http from 'http'; export class LibraryBuyer extends EventEmitter { root; version; versionJsonPath; forceDownload; concurry; maxRetries; paused = false; stopped = false; constructor(opts) { super(); this.root = opts.root; this.version = opts.version; this.versionJsonPath = opts.versionJsonPath || resolve(this.root, "versions", this.version, `${this.version}.json`); this.forceDownload = opts.forceDownload ?? false; this.concurry = opts.concurry ?? 8; this.maxRetries = opts.maxRetries ?? 3; } pause() { this.paused = true; this.emit("Paused"); } resume() { this.paused = false; this.emit("Resumed"); } stop() { this.stopped = true; this.emit("Stopped"); } async waitIfPaused() { while (this.paused) await new Promise(res => setTimeout(res, 40)); if (this.stopped) throw new Error("Stopped"); } libraryToUrl(library) { const urls = []; if (library.downloads?.artifact?.url) urls.push(library.downloads.artifact.url); if (library.downloads?.classifiers) { const os = this.getCurrentOS(); const arch = process.arch === 'x64' ? '64' : '32'; for (const [classifier, artifact] of Object.entries(library.downloads.classifiers)) { if (classifier.includes(`natives-${os}`) || classifier.includes(`natives-windows-${arch}`)) { urls.push(artifact.url); } } } if (!library.downloads && library.name) { const baseUrl = library.url || "https://libraries.minecraft.net/"; const path = this.libraryNameToPath(library.name); urls.push(baseUrl + path); } if (library.url && library.name && !library.downloads) { const path = this.libraryNameToPath(library.name); urls.push(library.url + path); } return urls; } libraryNameToPath(name) { const parts = name.split(':'); if (parts.length < 3) return `${name.replace(/:/g, '/')}.jar`; const [group, artifact, version] = parts; const groupPath = group.replace(/\./g, '/'); return `${groupPath}/${artifact}/${version}/${artifact}-${version}.jar`; } getCurrentOS() { switch (process.platform) { case 'win32': return 'windows'; case 'darwin': return 'osx'; default: return 'linux'; } } async safeUnlink(filePath) { if (existsSync(filePath)) { try { await unlink(filePath); } catch { /* ignora */ } } } downloadFile(url, filePath) { return new Promise((resolve) => { const dir = dirname(filePath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); const file = createWriteStream(filePath); const protocol = url.startsWith('https') ? https : http; const request = protocol.get(url, (response) => { if (response.statusCode === 200) { response.pipe(file); file.on('finish', async () => { file.close(); try { const stats = await stat(filePath); resolve({ success: true, filePath, size: stats.size }); } catch { resolve({ success: true, filePath, size: 0 }); } }); file.on('error', async (err) => { file.close(); await this.safeUnlink(filePath); resolve({ success: false, filePath, size: 0, error: err.message }); }); } else { file.close(); this.safeUnlink(filePath); resolve({ success: false, filePath, size: 0, error: `HTTP ${response.statusCode}` }); } }); request.on('error', async (err) => { file.close(); await this.safeUnlink(filePath); resolve({ success: false, filePath, size: 0, error: err.message }); }); request.setTimeout(30000, async () => { request.destroy(); file.close(); await this.safeUnlink(filePath); resolve({ success: false, filePath, size: 0, error: 'Timeout' }); }); }); } async loadVersionJson() { try { if (!existsSync(this.versionJsonPath)) throw new Error(`Version JSON not found: ${this.versionJsonPath}`); const data = readFileSync(this.versionJsonPath, 'utf8'); const versionJson = JSON.parse(data); if (versionJson.inheritsFrom) { const parentPath = resolve(this.root, "versions", versionJson.inheritsFrom, `${versionJson.inheritsFrom}.json`); if (existsSync(parentPath)) { const parentData = readFileSync(parentPath, 'utf8'); const parentJson = JSON.parse(parentData); versionJson.libraries = [...(parentJson.libraries || []), ...(versionJson.libraries || [])]; } } return versionJson; } catch (error) { throw new Error(`Error loading version JSON: ${error.message}`); } } urlToLocalPath(url) { try { if (url.includes('libraries.minecraft.net') || url.includes('maven.minecraftforge.net')) { const urlObj = new URL(url); return join(this.root, 'libraries', urlObj.pathname.substring(1)); } else { const fileName = basename(url); return join(this.root, 'libraries', fileName); } } catch { const fileName = basename(url); return join(this.root, 'libraries', fileName); } } async checkMissingLibraries() { this.emit("StartCheck"); try { const versionJson = await this.loadVersionJson(); const missingUrls = []; for (const library of versionJson.libraries || []) { const urls = this.libraryToUrl(library); for (const url of urls) { const filePath = this.urlToLocalPath(url); if (this.forceDownload || !existsSync(filePath)) { missingUrls.push(url); this.emit("LibraryMissing", { library: library.name, url, filePath: basename(filePath) }); } else { this.emit("LibraryExists", { library: library.name, filePath: basename(filePath) }); } } } this.emit("CheckComplete", { total: versionJson.libraries?.length || 0, missing: missingUrls.length }); return { missing: missingUrls, total: versionJson.libraries?.length || 0 }; } catch (error) { this.emit("CheckError", { error: error.message }); throw error; } } async downloadMissingLibraries() { this.emit("StartDownload"); const { missing: missingUrls } = await this.checkMissingLibraries(); if (missingUrls.length === 0) { this.emit("DownloadComplete", { success: 0, failed: 0, total: 0 }); return { success: 0, failed: 0, total: 0 }; } const limit = createTaskLimiter(this.concurry); let successCount = 0; let failedCount = 0; const downloadTasks = missingUrls.map(url => limit(async () => { await this.waitIfPaused(); const filePath = this.urlToLocalPath(url); const fileName = basename(filePath); this.emit("FileStart", { url, filePath: fileName }); try { const result = await this.downloadFile(url, filePath); if (result.success) { successCount++; this.emit("Bytes", result.size); this.emit("FileSuccess", { filePath: fileName, size: result.size, url }); } else { failedCount++; this.emit("FileError", { filePath: fileName, error: result.error, url }); } return result; } catch (error) { failedCount++; this.emit("FileError", { filePath: fileName, error: error.message, url }); return { success: false, filePath, size: 0, error: error.message }; } })); await Promise.all(downloadTasks); this.emit("DownloadComplete", { success: successCount, failed: failedCount, total: missingUrls.length }); return { success: successCount, failed: failedCount, total: missingUrls.length }; } async getTotalDownloadSize() { const { missing: missingUrls } = await this.checkMissingLibraries(); let totalSize = 0; for (const url of missingUrls) { try { const headResponse = await fetch(url, { method: 'HEAD' }); if (headResponse.ok) { const contentLength = headResponse.headers.get('content-length'); totalSize += contentLength ? parseInt(contentLength) : 1024 * 1024; } else { totalSize += 1024 * 1024; } } catch { totalSize += 1024 * 1024; } } return totalSize; } async ensureLibraries() { this.emit("Start"); try { const { missing, total } = await this.checkMissingLibraries(); if (missing.length === 0) { this.emit("AllLibrariesExist", { total }); return true; } this.emit("LibrariesMissing", { missing: missing.length, total }); const result = await this.downloadMissingLibraries(); this.emit("Complete", result); return result.failed === 0; } catch (error) { this.emit("Error", { error: error.message }); throw error; } } }