UNPKG

mclc_mendora

Version:

A custom Minecraft launcher core with official authentication and modded version support.

170 lines (147 loc) 6.31 kB
const { exec } = require("child_process"); const fs = require("fs"); const path = require("path"); const https = require("https"); const URLS = { meta: "https://launchermeta.mojang.com", resource: "https://resources.download.minecraft.net" }; class MinecraftLauncher { constructor(options = {}) { this.name = options.name || "Player"; this.javaPath = options.javaPath || "java"; this.maxMemory = options.maxMemory || "2G"; this.minMemory = options.minMemory || "1G"; this.directory = options.directory_minecraft || path.join(process.env.HOME || process.env.USERPROFILE, ".minecraft"); this.version = options.version || { type: "release", value: "latest" }; this.auth = options.auth; // No offline fallback } log(message) { const now = new Date().toISOString().split("T")[1].split(".")[0]; console.log(`[${now}] [main/INFO]: ${message}`); } httpsRequest(url) { return new Promise((resolve, reject) => { https.get(url, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }).on("error", reject); }); } async downloadFile(url, dest, label) { return new Promise((resolve, reject) => { if (fs.existsSync(dest)) return resolve(); fs.mkdirSync(path.dirname(dest), { recursive: true }); this.log(`Downloading ${label}...`); const file = fs.createWriteStream(dest); https.get(url, (res) => { if (res.statusCode !== 200) return reject(new Error(`Failed to download ${label}`)); res.pipe(file); file.on("finish", () => { file.close(() => { this.log(`Finished ${label}`); resolve(); }); }); }).on("error", reject); }); } async fetchVersionManifest() { return this.httpsRequest(`${URLS.meta}/mc/game/version_manifest.json`); } async getVersion() { const manifest = await this.fetchVersionManifest(); if (this.version.type === "release" || this.version.type === "snapshot") { if (this.version.value === "latest") return manifest.latest[this.version.type]; const versionData = manifest.versions.find((v) => v.id === this.version.value); if (!versionData) throw new Error(`Version ${this.version.value} not found.`); return versionData.id; } return this.version.value; } async prepareVersion(versionId) { this.log(`Preparing version: ${versionId}`); const manifest = await this.fetchVersionManifest(); const versionMeta = manifest.versions.find((v) => v.id === versionId); const versionData = await this.httpsRequest(versionMeta.url); const versionDir = path.join(this.directory, "versions", versionId); fs.mkdirSync(versionDir, { recursive: true }); const clientJar = path.join(versionDir, `${versionId}.jar`); await this.downloadFile(versionData.downloads.client.url, clientJar, "client.jar"); // Download libraries for (const lib of versionData.libraries) { if (!lib.downloads || !lib.downloads.artifact) continue; const libPath = path.join(this.directory, "libraries", lib.downloads.artifact.path); await this.downloadFile(lib.downloads.artifact.url, libPath, `library ${path.basename(lib.downloads.artifact.path)}`); } // Download assets const assetIndexPath = path.join(this.directory, "assets", "indexes", `${versionData.assetIndex.id}.json`); await this.downloadFile(versionData.assetIndex.url, assetIndexPath, "asset index"); const assetIndex = JSON.parse(fs.readFileSync(assetIndexPath, "utf-8")); this.log(`Downloading assets...`); for (const [key, asset] of Object.entries(assetIndex.objects)) { const hash = asset.hash; const assetPath = path.join(this.directory, "assets", "objects", hash.substring(0, 2), hash); const assetUrl = `${URLS.resource}/${hash.substring(0, 2)}/${hash}`; await this.downloadFile(assetUrl, assetPath, `asset ${key}`); } return { versionData, clientJar }; } async launch() { let userAuth; if (this.auth.type === "microsoft" && this.auth.accessToken && this.auth.uuid) { userAuth = { name: this.auth.name || this.name, accessToken: this.auth.accessToken, uuid: this.auth.uuid }; } else { throw new Error("Microsoft authentication required. No offline mode allowed."); } this.log(`Setting user: ${userAuth.name}`); const versionId = await this.getVersion(); const { versionData, clientJar } = await this.prepareVersion(versionId); const classpathList = [ ...versionData.libraries .filter((lib) => lib.downloads && lib.downloads.artifact) .map((lib) => path.join(this.directory, "libraries", lib.downloads.artifact.path)), clientJar ]; const classpathFile = path.join(this.directory, "classpath.txt"); fs.writeFileSync(classpathFile, classpathList.join(path.delimiter)); const mcArgs = [ `-Xmx${this.maxMemory}`, `-Xms${this.minMemory}`, "-cp", `@${classpathFile}`, "net.minecraft.client.main.Main", "--username", userAuth.name, "--version", versionId, "--gameDir", this.directory, "--assetsDir", path.join(this.directory, "assets"), "--assetIndex", versionData.assetIndex.id, "--uuid", userAuth.uuid, "--accessToken", userAuth.accessToken, "--userType", this.auth.type === "microsoft" ? "msa" : "legacy", "--versionType", this.version.type ]; const command = `"${this.javaPath}" ${mcArgs.join(" ")}`; this.log(`Launching Minecraft: ${versionId}`); return new Promise((resolve, reject) => { const mcProcess = exec(command, { cwd: this.directory, maxBuffer: 1024 * 1024 * 10 }); mcProcess.stdout.on("data", (data) => process.stdout.write(data)); mcProcess.stderr.on("data", (data) => process.stderr.write(data)); mcProcess.on("close", (code) => { if (code === 0) resolve(); else reject(new Error(`Minecraft exited with code ${code}`)); }); }); } } module.exports = { MinecraftLauncher };