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.

225 lines (224 loc) 8.52 kB
import { mkdir, writeFile, stat } from "node:fs/promises"; import { join, dirname } from "node:path"; import { EventEmitter } from "node:events"; import { createTaskLimiter } from "../Utils/Index.js"; const VERSION_MANIFEST = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; async function download(url, retries = 5) { for (let i = 0; i < retries; i++) { try { const res = await fetch(url); if (!res.ok) throw new Error(`Failed download: ${url}`); return Buffer.from(await res.arrayBuffer()); } catch (err) { if (i === retries - 1) throw err; } } throw new Error("Unreachable"); } async function prepareDirs(root) { const base = join(root, "assets"); const dirs = [ base, join(base, "objects"), join(base, "indexes"), ]; for (const d of dirs) await mkdir(d, { recursive: true }); return { base, objects: join(base, "objects"), indexes: join(base, "indexes"), }; } export class AssetsDownloader extends EventEmitter { root; version; concurry; maxRetries; paused = false; stopped = false; versionType = null; forceInstall = false; constructor(opts) { super(); this.root = opts.root; this.version = opts.version; this.concurry = opts.concurry ?? 15; this.maxRetries = opts.maxRetries ?? 10; } pause() { this.paused = true; this.emit("Paused"); } resume() { this.paused = false; this.emit("Resumed"); } stop() { this.stopped = true; this.emit("Stopped"); } // Nueva función para forzar la instalación setForceInstall(force) { this.forceInstall = force; this.emit("ForceInstallChanged", force); } async waitIfPaused() { while (this.paused) { await new Promise((res) => setTimeout(res, 40)); } if (this.stopped) throw new Error("Stopped"); } async getVersionIndex() { const manifestBuf = await download(VERSION_MANIFEST, this.maxRetries); const manifest = JSON.parse(manifestBuf.toString()); const versionMeta = manifest.versions.find((v) => v.id === this.version); if (!versionMeta) throw new Error(`Version not found: ${this.version}`); this.versionType = versionMeta.type; const versionJsonBuf = await download(versionMeta.url, this.maxRetries); const versionJson = JSON.parse(versionJsonBuf.toString()); const assetIndexName = versionJson.assetIndex?.id || this.version; const indexBuf = await download(versionJson.assetIndex.url, this.maxRetries); return { indexBuf, assetIndexName, objects: JSON.parse(indexBuf.toString()).objects }; } async getTotalBytes() { const dirs = await prepareDirs(this.root); const { objects } = await this.getVersionIndex(); let total = 0; for (const obj of Object.values(objects)) { // Si forceInstall está activado, contar todos los bytes if (this.forceInstall) { total += obj.size; continue; } const sha1 = obj.hash; const sub = sha1.slice(0, 2); const filePath = join(dirs.objects, sub, sha1); try { await stat(filePath); } catch { total += obj.size; } } return total; } downloadAllResources(objects, dirs) { const limit = createTaskLimiter(this.concurry); return Object.entries(objects).map(([name, obj]) => limit(async () => { await this.waitIfPaused(); const sha1 = obj.hash; const sub = sha1.slice(0, 2); const url = `https://resources.download.minecraft.net/${sub}/${sha1}`; const savePath = join(dirs.resources, name); await mkdir(dirname(savePath), { recursive: true }); // Verificar si el archivo ya existe (solo si no está forzado) if (!this.forceInstall) { try { await stat(savePath); // Archivo existe, emitir eventos para mantener consistencia this.emit("FileStart", { type: "resource", name, size: obj.size }); this.emit("Bytes", obj.size); // Emitir bytes para mantener el progreso this.emit("ResourceFile", name); this.emit("FileEnd", { type: "resource", name }); return; } catch { } } this.emit("FileStart", { type: "resource", name, size: obj.size }); const buf = await download(url, this.maxRetries); await writeFile(savePath, buf); this.emit("Bytes", buf.length); this.emit("ResourceFile", name); this.emit("FileEnd", { type: "resource", name }); return buf; })); } downloadAllObjects(objects, dirs) { const limit = createTaskLimiter(this.concurry); return Object.values(objects).map((obj) => limit(async () => { await this.waitIfPaused(); const sha1 = obj.hash; const sub = sha1.slice(0, 2); const savePath = join(dirs.objects, sub, sha1); // Verificar si el archivo ya existe (solo si no está forzado) if (!this.forceInstall) { try { await stat(savePath); // Archivo existe, emitir eventos para mantener consistencia this.emit("FileStart", { type: "object", hash: sha1, size: obj.size }); this.emit("Bytes", obj.size); // Emitir bytes para mantener el progreso this.emit("ObjectFile", sha1); this.emit("FileEnd", { type: "object", hash: sha1 }); return; } catch { } } await mkdir(dirname(savePath), { recursive: true }); const url = `https://resources.download.minecraft.net/${sub}/${sha1}`; this.emit("FileStart", { type: "object", hash: sha1, size: obj.size }); const buf = await download(url, this.maxRetries); await writeFile(savePath, buf); this.emit("Bytes", buf.length); this.emit("ObjectFile", sha1); this.emit("FileEnd", { type: "object", hash: sha1 }); return buf; })); } // Función para decidir si bajar resources shouldDownloadResources() { if (!this.versionType) return false; // Todas las old_alpha y old_beta usan /resources if (this.versionType === "old_alpha") return true; if (this.versionType === "old_beta") return true; if (this.versionType === "alpha") return true; if (this.versionType === "beta") return true; const parts = this.version.split(".").map(n => parseInt(n, 10)); const major = parts[0] ?? 0; const minor = parts[1] ?? 0; // 1.5.2 y anteriores → usan resources if (major === 1 && minor < 6) return true; // 1.0 → 1.5 if (major < 1) return true; // 0.x o classic return false; // 1.6+ usan assets } async start() { this.emit("Start"); const dirs = await prepareDirs(this.root); const { indexBuf, objects, assetIndexName } = await this.getVersionIndex(); const isLegacy = this.shouldDownloadResources(); let resourceTasks = []; let objectTasks = []; if (isLegacy) { const resourcesDir = join(this.root, "resources"); await mkdir(resourcesDir, { recursive: true }); dirs.resources = resourcesDir; await writeFile(join(resourcesDir, "resources.json"), indexBuf); resourceTasks = this.downloadAllResources(objects, dirs); } else { await writeFile(join(dirs.indexes, `${assetIndexName}.json`), indexBuf); objectTasks = this.downloadAllObjects(objects, dirs); } await Promise.all([ ...resourceTasks, ...objectTasks ]); this.emit("Done"); } }