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
JavaScript
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;
}
}