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.
1,150 lines (1,149 loc) • 46.3 kB
JavaScript
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { promises as fs } from "node:fs";
import path, { basename, resolve } from "node:path";
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { LibraryBuyer } from "./LibraryBuyer.js";
;
const OS_TYPES = { windows: "windows", linux: "linux", osx: "osx" };
class PerformanceTracker {
startTimes = new Map();
phaseTimes = {};
start(phase) {
this.startTimes.set(phase, Date.now());
}
end(phase) {
const startTime = this.startTimes.get(phase);
if (!startTime)
return 0;
const duration = Date.now() - startTime;
this.phaseTimes[phase] = duration;
this.startTimes.delete(phase);
return duration;
}
getPhaseTimes() {
return { ...this.phaseTimes };
}
}
function getOS() {
switch (process.platform) {
case "win32": return OS_TYPES.windows;
case "darwin": return OS_TYPES.osx;
default: return OS_TYPES.linux;
}
}
function satisfiesRule(rule, features) {
if (!rule.os && !rule.features)
return true;
if (rule.os) {
const currentOS = getOS();
const osNameMap = {
[OS_TYPES.windows]: "windows",
[OS_TYPES.linux]: "linux",
[OS_TYPES.osx]: "osx"
};
if (rule.os.name && rule.os.name !== osNameMap[currentOS]) {
return rule.action === "disallow";
}
if (rule.os.arch) {
const currentArch = process.arch;
if (rule.os.arch !== currentArch) {
return rule.action === "disallow";
}
}
}
if (rule.features) {
for (const [feature, required] of Object.entries(rule.features)) {
const hasFeature = features?.[feature] || false;
if (required !== hasFeature) {
return rule.action === "disallow";
}
}
}
return rule.action === "allow";
}
function satisfiesAllRules(rules, features) {
if (!rules || rules.length === 0)
return true;
let allowed = true;
for (const rule of rules) {
if (!satisfiesRule(rule, features)) {
if (rule.action === "allow")
return false;
}
else {
if (rule.action === "allow")
allowed = true;
else if (rule.action === "disallow")
allowed = false;
}
}
return allowed;
}
async function loadVersionManifest(root, version, override) {
const versionPath = override?.versionJson || resolve(root, "versions", version, `${version}.json`);
const raw = await fs.readFile(versionPath, "utf8");
const manifest = JSON.parse(raw);
if (manifest.inheritsFrom) {
const parentManifest = await loadVersionManifest(root, manifest.inheritsFrom, override);
return mergeManifests(parentManifest, manifest);
}
return manifest;
}
function mergeManifests(parent, child) {
const merged = {
...parent,
...child,
id: child.id,
mainClass: child.mainClass || parent.mainClass,
libraries: [...(parent.libraries || []), ...(child.libraries || [])],
minecraftArguments: child.minecraftArguments || parent.minecraftArguments,
assets: child.assets || parent.assets,
assetIndex: child.assetIndex || parent.assetIndex,
type: child.type || parent.type,
minimumLauncherVersion: Math.max(parent.minimumLauncherVersion || 0, child.minimumLauncherVersion || 0)
};
if (parent.arguments || child.arguments) {
const gameArgs = [
...(parent.arguments?.game || []),
...(child.arguments?.game || [])
];
const jvmArgs = [
...(parent.arguments?.jvm || []),
...(child.arguments?.jvm || [])
];
if (parent.minecraftArguments && !parent.arguments) {
const legacyArgs = parent.minecraftArguments.split(' ').map(arg => ({ value: arg }));
gameArgs.push(...legacyArgs);
}
if (child.minecraftArguments && !child.arguments) {
const legacyArgs = child.minecraftArguments.split(' ').map(arg => ({ value: arg }));
gameArgs.push(...legacyArgs);
}
merged.arguments = {
game: gameArgs,
jvm: jvmArgs
};
}
else if (parent.minecraftArguments || child.minecraftArguments) {
const legacyArgs = [
...(parent.minecraftArguments?.split(' ') || []),
...(child.minecraftArguments?.split(' ') || [])
].map(arg => ({ value: arg }));
merged.arguments = {
game: legacyArgs,
jvm: []
};
}
return merged;
}
async function handleCustomVersion(options, manifest, emitter) {
const isForge = manifest.libraries?.some(lib => lib.name.includes('net.minecraftforge:forge:') ||
lib.name.includes('net.minecraftforge:fmlloader:'));
const isFabric = manifest.libraries?.some(lib => lib.name.includes('net.fabricmc:fabric-loader:'));
const isCustom = isForge || isFabric ||
manifest.mainClass?.includes('forge') ||
manifest.id?.includes('forge');
if (!isCustom)
return;
emitter.emit("debug", {
type: "custom-version-detected",
isForge,
isFabric,
mainClass: manifest.mainClass,
version: manifest.id
});
const libraryBuyer = new LibraryBuyer({
root: options.gameRoot,
version: options.version,
forceDownload: false,
concurry: 10
});
libraryBuyer.on("LibraryMissing", (data) => {
emitter.emit("progress", {
type: "library-missing",
message: `Falta: ${data.library}`
});
});
libraryBuyer.on("FileStart", (data) => {
emitter.emit("progress", {
type: "downloading",
message: `Descargando: ${basename(data.filePath)}`
});
});
libraryBuyer.on("FileSuccess", (data) => {
emitter.emit("progress", {
type: "downloaded",
message: `Listo: ${basename(data.filePath)}`
});
});
// Ejecutar descarga
await libraryBuyer.ensureLibraries();
}
function getAssetsRoot(options, manifest) {
if (options.override?.assetRoot) {
return options.override.assetRoot;
}
const assetsId = manifest.assets || manifest.assetIndex?.id || options.version;
const isLegacyAssets = assetsId.startsWith("pre-") || assetsId === "legacy" || assetsId === "virtual";
if (isLegacyAssets) {
return resolve(options.gameRoot, "resources");
}
else {
return resolve(options.gameRoot, "assets");
}
}
function getAssetsIndexName(options, manifest) {
if (options.override?.assetIndex) {
return options.override.assetIndex;
}
if (manifest.assetIndex?.id) {
return manifest.assetIndex.id;
}
if (manifest.assets) {
return manifest.assets;
}
return options.version;
}
function processJVMArgument(arg, options, nativesDir) {
const launcherName = options.launcherName || "Minecraft Launcher";
const launcherVersion = options.launcherVersion || "1.0.0";
const libraryRoot = options.override?.libraryRoot || resolve(options.gameRoot, "libraries");
return arg
.replace(/\$\{natives_directory\}/g, nativesDir)
.replace(/\$\{classpath_separator\}/g, process.platform === "win32" ? ";" : ":")
.replace(/\$\{launcher_name\}/g, launcherName)
.replace(/\$\{launcher_version\}/g, launcherVersion)
// .replace(/\$\{classpath\}/g, "::")
.replace(/\$\{library_directory\}/g, libraryRoot)
.replace(/\$\{game_directory\}/g, options.override?.gameDirectory || options.gameRoot);
}
function removeAllDuplicateArgs(args) {
const result = [];
const seenArgs = new Set();
for (let i = 0; i < args.length; i++) {
const current = args[i];
if (current?.startsWith('--')) {
if (seenArgs.has(current)) {
// Saltar este argumento y su valor (si tiene)
if (i + 1 < args.length && !args[i + 1]?.startsWith('--')) {
i++; // Saltar el valor también
}
continue;
}
seenArgs.add(current);
}
result.push(current || "");
}
return result;
}
function buildGameArgs(options, manifest, emitter) {
const gameArgs = [];
const gameDirectory = options.override?.gameDirectory || resolve(options.gameRoot);
const assetsRootPath = getAssetsRoot(options, manifest);
const assetsIndexName = getAssetsIndexName(options, manifest);
if (manifest.arguments?.game) {
for (const arg of manifest.arguments.game) {
if (typeof arg === "string") {
const processed = processGameArgument(arg, options, manifest);
if (isValidArgument(processed)) {
gameArgs.push(processed);
}
}
else if (arg.value && satisfiesAllRules(arg.rules, options.features)) {
const values = Array.isArray(arg.value) ? arg.value : [arg.value];
for (const value of values) {
if (typeof value === "string") {
const processed = processGameArgument(value, options, manifest);
if (isValidArgument(processed)) {
gameArgs.push(processed);
}
}
}
}
}
}
else if (manifest.minecraftArguments) {
const legacyArgs = manifest.minecraftArguments.split(" ");
for (const arg of legacyArgs) {
const processed = processGameArgument(arg, options, manifest);
if (isValidArgument(processed)) {
gameArgs.push(processed);
}
}
}
ensureArgumentWithValue(gameArgs, '--username', options.user.name || "Player");
ensureArgumentWithValue(gameArgs, '--version', options.version);
ensureArgumentWithValue(gameArgs, '--gameDir', gameDirectory);
ensureArgumentWithValue(gameArgs, '--assetsDir', assetsRootPath);
ensureArgumentWithValue(gameArgs, '--assetIndex', assetsIndexName);
ensureArgumentWithValue(gameArgs, '--uuid', options.user.uuid || "00000000-0000-0000-0000-000000000000");
ensureArgumentWithValue(gameArgs, '--accessToken', options.user.access_token || "0");
ensureArgumentWithValue(gameArgs, '--userType', options.user.meta?.type || "mojang");
const userPropertiesIndex = gameArgs.indexOf('--userProperties');
if (userPropertiesIndex !== -1) {
if (userPropertiesIndex < gameArgs.length - 1) {
const nextArg = gameArgs[userPropertiesIndex + 1];
if (!isArgumentName(nextArg) && isValidUserProperties(nextArg)) {
}
else {
gameArgs[userPropertiesIndex + 1] = '{}';
}
}
else {
gameArgs.push('{}');
}
}
else {
gameArgs.push('--userProperties', '{}');
}
handleWindowArguments(gameArgs, options.window);
handleCustomArguments(gameArgs, options.MC_ARGS);
const cleanedArgs = cleanArguments(gameArgs);
const finalArgs = removeAllDuplicateArgs(cleanedArgs);
if (options.enableDebug) {
emitter.emit("debug", {
type: "game-args-debug",
args: finalArgs,
gameDirectory: gameDirectory,
assetsRoot: assetsRootPath,
assetsIndexName: assetsIndexName,
originalGameRoot: options.gameRoot,
overrideGameDirectory: options.override?.gameDirectory
});
}
return finalArgs;
}
function isValidUserProperties(value) {
try {
const parsed = JSON.parse(value);
return typeof parsed === 'object' && parsed !== null;
}
catch {
return false;
}
}
function ensureArgumentWithValue(args, argName, value) {
const index = args.indexOf(argName);
if (index !== -1) {
if (index < args.length - 1) {
if (!isArgumentName(args[index + 1] || "")) {
args[index + 1] = value;
}
else {
args.splice(index + 1, 0, value);
}
}
else {
args.push(value);
}
}
else {
args.push(argName, value);
}
}
function isValidArgument(arg) {
if (arg === null || arg === undefined) {
return false;
}
const strArg = String(arg).trim();
if (strArg === "") {
return false;
}
if (strArg.includes('${') && strArg.replace(/\$\{[^}]+\}/g, '').trim() === '') {
return true;
}
return true;
}
function handleWindowArguments(gameArgs, window) {
if (!window)
return;
const hasWidth = isArgumentPresent(gameArgs, '--width');
const hasHeight = isArgumentPresent(gameArgs, '--height');
const hasFullscreen = gameArgs.includes('--fullscreen');
if (!hasWidth && window.width) {
gameArgs.push("--width", window.width.toString());
}
if (!hasHeight && window.height) {
gameArgs.push("--height", window.height.toString());
}
if (window.fullscreen && !hasFullscreen) {
gameArgs.push("--fullscreen");
}
}
function handleCustomArguments(gameArgs, MC_ARGS) {
if (!MC_ARGS)
return;
for (const [key, value] of Object.entries(MC_ARGS)) {
if (value === undefined || value === null || value === "")
continue;
const argName = key.startsWith("--") ? key : `--${key}`;
if (isCustomArgumentExists(gameArgs, argName, value)) {
continue;
}
addCustomArgument(gameArgs, argName, value);
}
}
function isArgumentPresent(args, argumentName) {
return args.includes(argumentName);
}
function isCustomArgumentExists(args, argName, value) {
const index = args.indexOf(argName);
if (index === -1)
return false;
if (typeof value === 'boolean') {
return true;
}
if (index < args.length - 1) {
return args[index + 1] === String(value);
}
return false;
}
function addCustomArgument(args, argName, value) {
if (typeof value === "boolean") {
if (value) {
args.push(argName);
}
}
else {
args.push(argName, String(value));
}
}
function cleanArguments(args) {
const cleaned = [];
for (let i = 0; i < args.length; i++) {
const current = args[i];
if (!isValidArgument(current))
continue;
const currentStr = String(current);
if (currentStr.trim().startsWith('{') || currentStr.trim().startsWith('[')) {
cleaned.push(currentStr);
continue;
}
if (isArgumentRequiresValue(currentStr)) {
if (i < args.length - 1) {
const next = args[i + 1];
if (isValidArgument(next) && !isArgumentName(String(next))) {
cleaned.push(currentStr, String(next));
i++;
}
else {
if (currentStr === '--userProperties') {
cleaned.push(currentStr, '{}');
i++;
}
else {
cleaned.push(currentStr);
}
}
}
else {
if (currentStr === '--userProperties') {
cleaned.push(currentStr, '{}');
}
else {
cleaned.push(currentStr);
}
}
}
else {
cleaned.push(currentStr);
}
}
return cleaned;
}
function isArgumentName(arg) {
return arg.startsWith('--') || arg.startsWith('-');
}
function isArgumentRequiresValue(argument) {
const valueRequiredArgs = [
'--width', '--height', '--quickPlayPath',
'--quickPlaySingleplayer', '--quickPlayMultiplayer',
'--quickPlayRealms', '--assetsDir', '--assetIndex',
'--username', '--uuid', '--accessToken', '--userProperties',
'--userType', '--version', '--gameDir', '--assetsDir',
'--assetIndex', '--clientId'
];
return valueRequiredArgs.includes(argument);
}
function processGameArgument(arg, options, manifest) {
const user = options.user;
const assetsRootPath = getAssetsRoot(options, manifest);
const assetsIndexName = getAssetsIndexName(options, manifest);
const gameDirectory = options.override?.gameDirectory || resolve(options.gameRoot);
const quickPlayPath = options.MC_ARGS?.quickPlayPath || "";
const quickPlaySingleplayer = options.MC_ARGS?.quickPlaySingleplayer || "";
const quickPlayMultiplayer = options.MC_ARGS?.quickPlayMultiplayer || "";
const quickPlayRealms = options.MC_ARGS?.quickPlayRealms || "";
if (arg.trim().startsWith('{') || arg.trim().startsWith('[')) {
return arg;
}
return arg.replace(/\$\{([^}]+)\}/g, (key) => {
const trimmedKey = key.replace(/\$\{|\}/g, '').trim();
switch (trimmedKey) {
case "auth_player_name": return user.name || "Player";
case "auth_uuid": return user.uuid || "00000000-0000-0000-0000-000000000000";
case "auth_access_token": return user.access_token || "0";
case "auth_xuid": return "0";
case "user_type": return user.meta?.type || "mojang";
case "version_name": return options.version;
case "version_type": return manifest.type || "release";
case "game_directory": return gameDirectory;
case "assetsDir":
case "game_assets_directory":
case "assets_root":
return assetsRootPath;
case "assetsIndexName":
case "assets_index_name":
case "assetIndex":
return assetsIndexName;
case "clientid": return "0";
case "resolution_width": return options.window?.width?.toString() || "854";
case "resolution_height": return options.window?.height?.toString() || "480";
case "quickPlayPath": return quickPlayPath;
case "quickPlaySingleplayer": return quickPlaySingleplayer;
case "quickPlayMultiplayer": return quickPlayMultiplayer;
case "quickPlayRealms": return quickPlayRealms;
default:
if (options.MC_ARGS && trimmedKey in options.MC_ARGS) {
return String(options.MC_ARGS[trimmedKey]);
}
return "";
}
});
}
function getClientJarPath(root, version, manifest, override) {
if (override?.minecraftJar) {
return override.minecraftJar;
}
function getBaseDir() {
if (override?.directory) {
return override.directory;
}
if (manifest.inheritsFrom) {
return resolve(root, "versions", manifest.inheritsFrom);
}
return resolve(root, "versions", version);
}
const baseDir = getBaseDir();
if (manifest.jar) {
return resolve(baseDir, `${manifest.jar}.jar`);
}
if (manifest.inheritsFrom) {
return resolve(baseDir, `${manifest.inheritsFrom}.jar`);
}
return resolve(baseDir, `${version}.jar`);
}
function getNativesDir(root, version, manifest, override) {
if (override?.natives) {
return override.natives;
}
const versionDir = override?.directory || resolve(root, "versions", version);
if (manifest.inheritsFrom) {
const parentDir = override?.directory || resolve(root, "versions", manifest.inheritsFrom);
return resolve(parentDir, "natives");
}
return resolve(versionDir, "natives");
}
function libraryNameToPath(name) {
const parts = name.split(':');
if (parts.length < 3) {
throw new Error(`Invalid library name: ${name}`);
}
const groupId = parts[0];
const artifactId = parts[1];
const version = parts[2];
const classifier = parts[3] || null;
const groupPath = groupId?.replace(/\./g, '/');
let fileName = `${artifactId}-${version}`;
if (classifier) {
fileName += `-${classifier}`;
}
fileName += '.jar';
return `${groupPath}/${artifactId}/${version}/${fileName}`;
}
async function processLibraries(root, version, libraries, manifest, override, options) {
const classpath = [];
const processedPaths = new Set();
let libraryCount = 0;
const nativesDir = getNativesDir(root, version, manifest, override);
const libraryRoot = override?.libraryRoot || resolve(root, "libraries");
const libraryConflicts = new Map();
try {
await fs.mkdir(nativesDir, { recursive: true });
}
catch (error) { }
const os = getOS();
const isLegacy = isLegacyVersion(manifest);
if (options?.enableDebug) {
console.log(`Processing libraries for ${isLegacy ? 'legacy' : 'modern'} version: ${version}`);
}
for (const lib of libraries) {
const isCriticalLWJGL = isLegacy && (lib.name.includes('org.lwjgl.lwjgl:lwjgl:') ||
lib.name.includes('org.lwjgl.lwjgl:lwjgl_util:') ||
lib.name.includes('org.lwjgl.lwjgl:lwjgl-platform:'));
let shouldInclude = satisfiesAllRules(lib.rules, options?.features);
if (isCriticalLWJGL && !shouldInclude) {
if (options?.enableDebug) {
console.log(`🔄 FORCING critical LWJGL library (rules bypassed): ${lib.name}`);
}
shouldInclude = true;
}
if (!shouldInclude) {
if (options?.enableDebug) {
console.log(`Skipping library due to rules: ${lib.name}`);
}
continue;
}
if (lib.name.includes('com.google.guava:guava:')) {
const versionMatch = lib.name.match(/com\.google\.guava:guava:([\d.]+)/);
if (versionMatch) {
const currentVersion = versionMatch[1];
// 1. Inicializar el set de registro (o usar 0.0 si no existe)
if (!libraryConflicts.has('guava')) {
libraryConflicts.set('guava', new Set());
}
const registeredVersions = libraryConflicts.get('guava');
// Determinar la versión actualmente registrada
const existingVersion = registeredVersions.size > 0
? Array.from(registeredVersions)[0]
: '0.0';
// Convertimos a entero para comparación simple (ej: '17.0' -> 17)
const currentMajor = parseInt(currentVersion?.split('.')[0] || '0');
const existingMajor = parseInt(existingVersion.split('.')[0] || '0');
// 2. Lógica de selección: Reemplazar si la actual es MAYOR
if (currentMajor > existingMajor) {
// Si encontramos una versión más moderna (ej: 17.0 después de 15.0), la registramos
if (options?.enableDebug) {
console.log(`⬆️ UPGRADING Guava: ${existingVersion} -> ${currentVersion}. Priorizando para evitar errores de lanzamiento.`);
}
// Limpiamos el registro anterior y añadimos la nueva versión
registeredVersions.clear();
registeredVersions.add(currentVersion);
}
else if (currentMajor < existingMajor) {
// Si la versión actual es antigua (ej: 15.0 después de 17.0), la saltamos
if (options?.enableDebug) {
console.log(`🔄 SKIPPING older Guava: ${currentVersion} (Keeping ${existingVersion})`);
}
continue;
}
else if (registeredVersions.size > 0 && currentMajor === existingMajor) {
// Si es la misma versión y ya está registrada, la saltamos (evita duplicados exactos)
if (options?.enableDebug) {
console.log(`🔄 SKIPPING duplicate Guava version: ${currentVersion}`);
}
continue;
}
// Si llegamos a este punto y el Set sigue vacío (solo pasa en la primera iteración
// si la versión es válida, ej: 15.0 cuando existingMajor es 0), la añadimos.
if (registeredVersions.size === 0) {
registeredVersions.add(currentVersion);
}
}
}
if (isLegacy) {
let libPath = null;
if (lib.natives && lib.downloads?.classifiers) {
const nativeKey = lib.natives[os];
if (nativeKey) {
const nativeClassifier = nativeKey.replace("${arch}", process.arch === "x64" ? "64" : "32");
const nativeArtifact = lib.downloads.classifiers[nativeClassifier];
if (nativeArtifact) {
libPath = resolve(libraryRoot, nativeArtifact.path);
if (options?.enableDebug) {
console.log(`Using native classifier for ${lib.name}: ${nativeClassifier}`);
}
}
}
}
if (!libPath && lib.downloads?.artifact) {
libPath = resolve(libraryRoot, lib.downloads.artifact.path);
}
if (!libPath) {
try {
const relativePath = libraryNameToPath(lib.name);
libPath = resolve(libraryRoot, relativePath);
}
catch (error) {
if (options?.enableDebug) {
console.warn(`No se pudo generar ruta para librería: ${lib.name}`, error);
}
continue;
}
}
if (libPath && !processedPaths.has(libPath)) {
try {
await fs.access(libPath);
classpath.push(libPath);
processedPaths.add(libPath);
libraryCount++;
if (options?.enableDebug) {
console.log(`Added to classpath (legacy): ${libPath}`);
if (lib.name.includes('lwjgl')) {
console.log(`>>> LWJGL Library Added: ${lib.name} -> ${libPath}`);
}
}
}
catch (error) {
if (options?.enableDebug) {
console.warn(`Librería no encontrada (legacy): ${libPath}`);
}
}
}
continue;
}
if (lib.natives) {
const nativeKey = lib.natives[os];
if (nativeKey && lib.downloads?.classifiers) {
const nativeClassifier = nativeKey.replace("${arch}", process.arch === "x64" ? "64" : "32");
const nativeArtifact = lib.downloads.classifiers[nativeClassifier];
if (nativeArtifact) {
const nativePath = resolve(libraryRoot, nativeArtifact.path);
try {
await fs.access(nativePath);
const fileName = path.basename(nativePath);
const destPath = resolve(nativesDir, fileName);
try {
await fs.copyFile(nativePath, destPath);
if (options?.enableDebug) {
console.log(`📦 Native library copied: ${fileName} -> ${nativesDir}`);
}
}
catch (copyError) {
if (options?.enableDebug) {
console.warn(`❌ Error copiando librería nativa: ${fileName}`, copyError);
}
}
libraryCount++;
if (options?.enableDebug) {
console.log(`Native library processed (modern): ${nativePath}`);
}
}
catch (error) {
if (options?.enableDebug) {
console.warn(`Librería nativa no encontrada: ${nativePath}`);
}
}
}
}
continue;
}
let libPath = null;
if (lib.downloads?.artifact) {
libPath = resolve(libraryRoot, lib.downloads.artifact.path);
}
else {
try {
const relativePath = libraryNameToPath(lib.name);
libPath = resolve(libraryRoot, relativePath);
}
catch (error) {
if (options?.enableDebug) {
console.warn(`No se pudo procesar librería: ${lib.name}`, error);
}
continue;
}
}
if (!processedPaths.has(libPath)) {
try {
await fs.access(libPath);
classpath.push(libPath);
processedPaths.add(libPath);
libraryCount++;
if (options?.enableDebug) {
console.log(`Added to classpath (modern): ${libPath}`);
}
}
catch (error) {
if (options?.enableDebug) {
console.warn(`Librería no encontrada: ${libPath}`);
}
}
}
}
const guavaLibraries = classpath.filter(path => path.includes('guava'));
if (guavaLibraries.length > 1 && options?.enableDebug) {
console.log(`⚠️ ADVERTENCIA: Aún hay ${guavaLibraries.length} versiones de Guava:`);
guavaLibraries.forEach(lib => console.log(` - ${lib}`));
const guava15 = classpath.find(path => path.includes('guava-15.0'));
if (guava15) {
const index = classpath.indexOf(guava15);
classpath.splice(index, 1);
libraryCount--;
console.log(`🗑️ ELIMINADO: ${guava15}`);
}
}
if (options?.enableDebug) {
try {
const nativeFiles = await fs.readdir(nativesDir);
console.log(`📁 Archivos en directorio de nativas (${nativeFiles.length}):`);
nativeFiles.forEach(file => console.log(` - ${file}`));
const criticalNatives = ['lwjgl.dll', 'OpenAL32.dll', 'jinput-dx8_64.dll'];
const missingNatives = criticalNatives.filter(native => !nativeFiles.includes(native));
if (missingNatives.length > 0) {
console.log(`❌ FALTAN archivos nativos críticos: ${missingNatives.join(', ')}`);
}
else {
console.log(`✅ Todos los archivos nativos críticos presentes`);
}
}
catch (error) {
console.warn(`❌ No se pudo leer directorio de nativas: ${nativesDir}`);
}
}
if (options?.enableDebug) {
console.log(`Final classpath for ${version}:`);
classpath.forEach((path, index) => {
console.log(` [${index + 1}] ${path}`);
});
const lwjglLibraries = classpath.filter(path => path.includes('lwjgl') || path.includes('LWJGL'));
console.log(`LWJGL libraries in classpath: ${lwjglLibraries.length}`);
lwjglLibraries.forEach(lib => console.log(` - ${lib}`));
}
return { classpath, nativesDir, libraryCount };
}
function isLegacyVersion(manifest) {
if (manifest.minecraftArguments)
return true;
if (!manifest.arguments)
return true;
if (manifest.id && (manifest.id.startsWith('1.12') ||
manifest.id.startsWith('1.11') ||
manifest.id.startsWith('1.10') ||
manifest.id.startsWith('1.9') ||
manifest.id.startsWith('1.8') ||
manifest.id.startsWith('1.7'))) {
return true;
}
return false;
}
function buildJVMArgs(options, manifest, classpath, nativesDir, emitter) {
const jvmArgs = [];
const memory = options.memory;
const jvmCustomArgs = options.JVM_ARGS || [];
const libraryRoot = options.override?.libraryRoot || resolve(options.gameRoot, "libraries");
const isLegacy = isLegacyVersion(manifest);
if (memory?.min)
jvmArgs.push(`-Xms${memory.min}`);
if (memory?.max)
jvmArgs.push(`-Xmx${memory.max}`);
jvmArgs.push(`-Djava.library.path=${nativesDir}`);
jvmArgs.push("-Dfml.ignoreInvalidMinecraftCertificates=true");
jvmArgs.push("-Dfml.ignorePatchDiscrepancies=true");
jvmArgs.push("-XX:+UseG1GC");
jvmArgs.push("-XX:+UnlockExperimentalVMOptions");
jvmArgs.push("-XX:G1NewSizePercent=20");
jvmArgs.push("-XX:G1ReservePercent=20");
jvmArgs.push("-XX:MaxGCPauseMillis=50");
jvmArgs.push("-XX:G1HeapRegionSize=32M");
jvmArgs.push("-Dorg.lwjgl.librarypath=" + nativesDir);
jvmArgs.push(`-Dminecraft.client.jar=${getClientJarPath(options.gameRoot, options.version, manifest, options.override)}`);
jvmArgs.push(`-DlibraryDirectory=${libraryRoot}`);
if (options.proxy) {
const { host, port, type } = options.proxy;
if (type === 'socks4' || type === 'socks5') {
jvmArgs.push(`-DsocksProxyHost=${host}`);
jvmArgs.push(`-DsocksProxyPort=${port.toString()}`);
if (options.proxy.username && options.proxy.password) {
jvmArgs.push(`-DsocksProxyUser=${options.proxy.username}`);
jvmArgs.push(`-Djdk.socks.auth.username=${options.proxy.username}`);
jvmArgs.push(`-Djdk.socks.auth.password=${options.proxy.password}`);
}
}
else {
jvmArgs.push(`-Dhttp.proxyHost=${host}`);
jvmArgs.push(`-Dhttp.proxyPort=${port.toString()}`);
jvmArgs.push(`-Dhttps.proxyHost=${host}`);
jvmArgs.push(`-Dhttps.proxyPort=${port.toString()}`);
if (options.proxy.username && options.proxy.password) {
jvmArgs.push(`-Dhttp.proxyUser=${options.proxy.username}`);
jvmArgs.push(`-Dhttp.proxyPassword=${options.proxy.password}`);
}
}
}
if (manifest.arguments?.jvm) {
for (const arg of manifest.arguments.jvm) {
if (typeof arg === "string") {
const processedArg = processJVMArgument(arg, options, nativesDir);
if (processedArg && processedArg.trim() !== "") {
jvmArgs.push(processedArg);
}
}
else if (arg.value && satisfiesAllRules(arg.rules, options.features)) {
const values = Array.isArray(arg.value) ? arg.value : [arg.value];
for (const value of values) {
const processedArg = processJVMArgument(value, options, nativesDir);
if (processedArg && processedArg.trim() !== "") {
jvmArgs.push(processedArg);
}
}
}
}
}
jvmArgs.push(...jvmCustomArgs);
jvmArgs.push("-cp", classpath.join(process.platform === "win32" ? ";" : ":"));
if (options.enableDebug) {
emitter.emit("debug", {
type: "jvm-args",
args: jvmArgs,
classpathCount: classpath.length,
memory: memory,
nativesDir: nativesDir,
libraryRoot: libraryRoot,
proxy: options.proxy,
isLegacy: isLegacy
});
}
return jvmArgs;
}
async function verifyCriticalFiles(options, manifest) {
const criticalPaths = [
options.override?.versionJson || resolve(options.gameRoot, "versions", options.version, `${options.version}.json`),
getClientJarPath(options.gameRoot, options.version, manifest, options.override),
options.override?.libraryRoot || resolve(options.gameRoot, "libraries")
];
const assetsRootName = getAssetsRoot(options, manifest);
const assetsRootPath = options.override?.assetRoot || resolve(options.gameRoot, assetsRootName);
criticalPaths.push(assetsRootPath);
const assetsDir = resolve(options.gameRoot, "assets");
try {
await fs.access(assetsDir);
}
catch (error) {
await fs.mkdir(assetsDir, { recursive: true });
}
for (const path of criticalPaths) {
try {
await fs.access(path);
}
catch (error) {
throw new Error(`Critical file not found: ${path}`);
}
}
}
function createProxyAgent(proxyConfig) {
if (!proxyConfig)
return null;
const { host, port, type, username, password } = proxyConfig;
const proxyUrl = `${type}://${host}:${port}`;
let agent;
if (type === 'socks4' || type === 'socks5') {
const socksUrl = username && password
? `${type}://${username}:${password}@${host}:${port}`
: proxyUrl;
agent = new SocksProxyAgent(socksUrl);
}
else {
const httpUrl = username && password
? `http://${username}:${password}@${host}:${port}`
: proxyUrl;
agent = new HttpsProxyAgent(httpUrl);
}
return agent;
}
export async function ArgumentsBuilder(options) {
const emitter = new EventEmitter();
const tracker = new PerformanceTracker();
const totalStartTime = Date.now();
const root = resolve(options.gameRoot);
const stats = {
totalTime: 0,
phaseTimes: {},
classpathCount: 0,
libraryCount: 0
};
try {
tracker.start("manifest-load");
emitter.emit("status", "Loading version manifest...");
emitter.emit("phase-start", "manifest-load");
const manifest = await loadVersionManifest(root, options.version, options.override);
await handleCustomVersion(options, manifest, emitter);
const manifestTime = tracker.end("manifest-load");
emitter.emit("phase-end", "manifest-load", manifestTime);
emitter.emit("speed", { phase: "manifest-load", time: manifestTime });
tracker.start("file-verification");
emitter.emit("status", "Verifying files...");
emitter.emit("phase-start", "file-verification");
await verifyCriticalFiles(options, manifest);
const verificationTime = tracker.end("file-verification");
emitter.emit("phase-end", "file-verification", verificationTime);
emitter.emit("speed", { phase: "file-verification", time: verificationTime });
tracker.start("libraries-processing");
emitter.emit("status", "Processing libraries...");
emitter.emit("phase-start", "libraries-processing");
const { classpath, nativesDir, libraryCount } = await processLibraries(root, options.version, manifest.libraries, manifest, options.override, options);
const clientJar = getClientJarPath(root, options.version, manifest, options.override);
try {
await fs.access(clientJar);
classpath.push(clientJar);
}
catch (error) {
throw new Error(`Client JAR not found: ${clientJar}`);
}
const librariesTime = tracker.end("libraries-processing");
stats.classpathCount = classpath.length;
stats.libraryCount = libraryCount;
emitter.emit("phase-end", "libraries-processing", librariesTime);
emitter.emit("speed", {
phase: "libraries-processing",
time: librariesTime,
classpathCount: classpath.length,
libraryCount: libraryCount,
isLegacy: isLegacyVersion(manifest)
});
if (options.enableDebug) {
const assetsRoot = getAssetsRoot(options, manifest);
const assetsIndexName = getAssetsIndexName(options, manifest);
emitter.emit("debug", {
type: "classpath",
classpath: classpath,
count: classpath.length,
nativesDir: nativesDir,
clientJar: clientJar,
assetsRoot: assetsRoot,
assetsIndexName: assetsIndexName,
manifestAssets: manifest.assets,
manifestAssetIndex: manifest.assetIndex,
override: options.override,
isLegacy: isLegacyVersion(manifest)
});
}
tracker.start("args-building");
emitter.emit("status", "Building arguments...");
emitter.emit("phase-start", "args-building");
const javaExec = options.java || "java";
const jvmArgs = buildJVMArgs(options, manifest, classpath, nativesDir, emitter);
const gameArgs = buildGameArgs(options, manifest, emitter);
if (!manifest.mainClass) {
throw new Error("mainClass does not exist in manifest");
}
const finalArgs = [...jvmArgs, manifest.mainClass, ...gameArgs];
const argsTime = tracker.end("args-building");
emitter.emit("phase-end", "args-building", argsTime);
emitter.emit("speed", { phase: "args-building", time: argsTime });
if (options.enableDebug) {
emitter.emit("debug", {
type: "final-command",
javaExec,
args: finalArgs,
mainClass: manifest.mainClass,
totalArgs: finalArgs.length,
JVM_ARGS: options.JVM_ARGS,
MC_ARGS: options.MC_ARGS,
proxy: options.proxy,
features: options.features,
isLegacy: isLegacyVersion(manifest)
});
}
tracker.start("game-launch");
emitter.emit("status", "Starting Minecraft...");
emitter.emit("phase-start", "game-launch");
const gameDirectory = options.override?.gameDirectory || root;
const proxyAgent = createProxyAgent(options.proxy);
emitter.emit("launch-start", {
javaExec,
mainClass: manifest.mainClass,
jvmArgsCount: jvmArgs.length,
gameArgsCount: gameArgs.length,
nativesDir: nativesDir,
gameDirectory: gameDirectory,
memory: options.memory,
window: options.window,
proxy: options.proxy,
proxyAgent: !!proxyAgent,
features: options.features,
isLegacy: isLegacyVersion(manifest)
});
const env = { ...process.env };
if (options.proxy) {
const { host, port, type, username, password } = options.proxy;
if (type === 'socks4' || type === 'socks5') {
env.SOCKS_PROXY = `${host}:${port}`;
if (username && password) {
env.SOCKS_USERNAME = username;
env.SOCKS_PASSWORD = password;
}
}
else {
env.HTTP_PROXY = `http://${host}:${port}`;
env.HTTPS_PROXY = `http://${host}:${port}`;
if (username && password) {
env.HTTP_PROXY_AUTH = `${username}:${password}`;
}
}
}
const child = spawn(javaExec, finalArgs, {
cwd: gameDirectory,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: false,
env: env
});
const launchTime = tracker.end("game-launch");
emitter.emit("phase-end", "game-launch", launchTime);
emitter.emit("speed", { phase: "game-launch", time: launchTime });
stats.totalTime = Date.now() - totalStartTime;
stats.phaseTimes = tracker.getPhaseTimes();
emitter.emit("launch-complete", {
pid: child.pid,
totalTime: stats.totalTime,
phaseTimes: stats.phaseTimes,
classpathCount: stats.classpathCount,
libraryCount: stats.libraryCount,
proxy: options.proxy,
isLegacy: isLegacyVersion(manifest)
});
child.stdout?.on("data", (d) => {
const output = d.toString();
emitter.emit("stdout", output);
if (output.includes("Loading")) {
emitter.emit("progress", { type: "loading", message: output.trim() });
}
else if (output.includes("Preparing")) {
emitter.emit("progress", { type: "preparing", message: output.trim() });
}
if (output.includes("Game crashed!")) {
emitter.emit("error", new Error("Game crashed"));
}
});
child.stderr?.on("data", (d) => {
const output = d.toString();
if (!output.includes("Render Extensions") &&
!output.includes("Datafixer") &&
!output.includes("OpenGL") &&
!output.includes("Failed to get system info")) {
emitter.emit("stderr", output);
}
if (output.includes("ERROR") || output.includes("Exception")) {
emitter.emit("game-error", output);
}
});
child.on("close", (code, signal) => {
emitter.emit("exit", {
code: code ?? undefined,
signal: signal ?? undefined
});
emitter.emit("game-exit", { code, signal, totalTime: stats.totalTime });
});
child.on("error", (error) => {
emitter.emit("error", error);
emitter.emit("game-error", error.message);
});
child.on("spawn", () => {
emitter.emit("game-started", { pid: child.pid });
});
return {
emitter,
pid: child.pid,
kill: () => child.kill(),
stats
};
}
catch (error) {
stats.totalTime = Date.now() - totalStartTime;
emitter.emit("error", error);
emitter.emit("launch-failed", { error, totalTime: stats.totalTime });
throw error;
}
}