mclc_mendora
Version:
A custom Minecraft launcher core with official authentication and modded version support.
170 lines (147 loc) • 6.31 kB
JavaScript
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 };