const { exec, spawn } = require("child_process"); const path = require("path"); const fs = require("fs"); const { validateLibraryRules, getOS } = require("./Utils"); const http = require("http"); const https = require("https"); const nbt = require("prismarine-nbt"); const crypto = require("crypto"); const EventEmitter = require("events"); const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) async function getJSON(url) { const response = await fetch(url); return await response.json(); } class ServersDatServer { data = { hidden: { type: "byte", value: 0 }, ip: { type: "string", value: "localhost" }, name: { type: "string", value: "Minecraft Server" } }; constructor(name = "Minecraft Server", address = "localhost") { if (typeof(name) === "object") { this.data = name; }else if (typeof(name) === "string") { this.setName(name); this.setAddress(address); } } setName(name) { return this.data.name.value = name; } setAddress(address) { return this.data.ip.value = address; } getName() { return this.data.name.value; } getAddress() { return this.data.ip.value; } } class ServersDat { path = `servers.dat`; type = "big"; data = { type: "compound", name: "", value: { servers: { type: "list", value: { type: "compound", value: [] } } } } servers = []; constructor(_path) { this.path = _path; } async load() { if (!fs.existsSync(this.path)) { return this; } const buffer = await fs.promises.readFile(this.path); const { parsed, type } = await nbt.parse(buffer); // this.data = parsed; this.type = type; if (parsed && parsed.value && parsed.value.servers && parsed.value.servers.value && parsed.value.servers.value.value && parsed.value.servers.value.value.length > 0) { parsed.value.servers.value.value.forEach(server => { this.addServer(new ServersDatServer(server)); }); } //console.log("JSON serialized:", JSON.stringify(parsed.value.servers, null, 2)); return this; } async save() { this.generateData(); const ws = fs.createWriteStream(this.path); ws.write(nbt.writeUncompressed(this.data, "big")); ws.close(); } generateData() { const dataServers = this.data.value.servers.value.value = []; this.servers.forEach(server => { dataServers.push(server.data); }); return this.data; } updateOrAddToTop(server) { const oldServer = this.getServerByIp(server.getAddress()); this.removeAllServersByAddress(server.getAddress()); if (oldServer) { const serverData = oldServer.data; Object.assign(serverData, server.data); server.data = serverData; } this.addServerToTop(server); } addServerToTop(server) { this.servers.unshift(server); } addServer(server) { this.servers.push(server); } getServerByIp(address) { for (let i in this.servers) { const server = this.servers[i]; if (server.getAddress() === address) { return server; } } return null; } removeAllServersByAddress(address) { const indexes = []; for (let i in this.servers) { const server = this.servers[i]; if (server.getAddress() === address) { indexes.unshift(i); } } indexes.forEach(i => { this.servers.splice(i, 1); }); return indexes.length; } } class Instance { events = new EventEmitter(); versionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; resourcesUrl = "https://resources.download.minecraft.net"; serverListUrl = null; workDir = path.join(__dirname, "..", "minecraft"); authData = { playerName: "Cyanoure", uuid: "faceface-face-face-face-facefaceface", accessToken: "null", clientId: "0", xuid: "0", type: "mojang", isDemo: false } getAssetsDir() { return `${this.workDir}/assets`; } setWorkDirAtDefaultLocation(dirname) { this.workDir = `${sysRoot}/${dirname}`; } generateOfflineUUID() { const hash = crypto.createHash("md5").update(`OfflinePlayer:${this.authData.playerName}`).digest("hex"); this.authData.uuid = `${hash.substring(0, 8)}-${hash.substring(8, 12)}-4${hash.substring(13, 16)}-a${hash.substring(17, 20)}-${hash.substring(20)}`; } async loadVersionManifest() { this.versionManifest = await getJSON(this.versionManifestUrl); return this.versionManifest; } async downloadFile(url, path) { return new Promise(async resolve => { if (!url || typeof(url) !== "string") { console.log(`Invalid URL for '${path}'`); resolve(); return; } if (fs.existsSync(path)) { console.log(`Found '${path}'`); resolve(); return; } const pathParts = path.split("/"); const dir = pathParts.slice(0, pathParts.length - 1).join("/"); if (!fs.existsSync(dir)) { await fs.promises.mkdir(dir, { recursive: true }); } console.log(`Downloading '${path}'...`); const _http = url.startsWith("http://") ? http : https; const file = fs.createWriteStream(path); _http.get(url, response => { response.pipe(file); file.on("finish", () => { file.close(); console.log(`Downloaded '${path}'.`); resolve(); }); }); }); } async getVersion(versionId) { for(let i in this.versionManifest.versions) { const versionData = this.versionManifest.versions[i]; if (versionData.id === versionId) { if (!versionData.data) { versionData.data = await getJSON(this.versionManifest.versions[i].url); } return versionData; } } } async getLatestRelease() { return await this.getVersion(this.versionManifest.latest.release); } async getLatestSnapshot() { return await this.getVersion(this.versionManifest.latest.snapshot); } async getAssets(version) { const versionData = await this.getVersion(version); if (versionData && versionData.data && versionData.data.assetIndex && versionData.data.assetIndex.url) { return await getJSON(versionData.data.assetIndex.url); } return {}; } async downloadVersion(version) { this.events.emit("download-started", version); this.events.emit("download-process", { percent: 0, event: "preparing", file: `Version: ${version}` }); const versionData = await this.getVersion(version); const data = versionData.data; const libraries = data.libraries; const assets = await this.getAssets(version); const assetObjects = assets.objects; const assetObjectPaths = Object.keys(assetObjects); const fullDownloadLength = libraries.length + assetObjectPaths.length + 1; let downloadCounter = 0; console.log("Downloading libraries..."); for (const i in libraries) { downloadCounter++; const libraryData = libraries[i]; if (!libraryData.rules || validateLibraryRules(libraryData.rules)) { const url = libraryData.downloads.artifact.url; const filePath = `libraries/${libraryData.downloads.artifact.path}`; this.events.emit("download-process", { percent: downloadCounter/fullDownloadLength, event: "downloading", file: filePath }); await this.downloadFile(url, `${this.workDir}/${filePath}`); } } console.log("Libraries downloaded."); if (!fs.existsSync(`${this.getAssetsDir()}/indexes`)) { fs.mkdirSync(`${this.getAssetsDir()}/indexes`, {recursive: true}); } fs.writeFileSync(`${this.getAssetsDir()}/indexes/${data.assets}.json`, JSON.stringify(assets)); if (!fs.existsSync(`${this.getAssetsDir()}/objects`)) { fs.mkdirSync(`${this.getAssetsDir()}/objects`, {recursive: true}); } console.log("Downloading assets..."); for (const i in assetObjectPaths) { downloadCounter++; const assetPath = assetObjectPaths[i]; const assetObject = assetObjects[assetPath]; const assetHash = assetObject.hash; const url = `${this.resourcesUrl}/${assetHash.slice(0, 2)}/${assetHash}`; const filePath = `objects/${assetHash.slice(0, 2)}/${assetHash}`; this.events.emit("download-process", { percent: downloadCounter/fullDownloadLength, event: "downloading", file: filePath }); await this.downloadFile(url, `${this.getAssetsDir()}/${filePath}`); } console.log("Assets downloaded."); const versionPath = `versions/${version}/${version}.jar`; this.events.emit("download-process", { percent: (fullDownloadLength - 1)/fullDownloadLength, event: "downloading", file: versionPath }); await this.downloadFile(data.downloads.client.url, `${this.workDir}/${versionPath}`); fs.writeFileSync(`${this.workDir}/versions/${version}/${version}.json`, JSON.stringify(data)); this.events.emit("download-process", { percent: 1, event: "done", file: `Version: ${version}` }); } async downloadLatestRelease() { const version = this.versionManifest.latest.release; return await this.downloadVersion(version); } async startVersion(version) { if (version === "latest-release") { version = this.versionManifest.latest.release; } else if (version === "latest-snapshot") { version = this.versionManifest.latest.snapshot; } await this.downloadVersion(version); this.events.emit("starting"); const versionPath = `versions/${version}/${version}.jar`; const versionData = await this.getVersion(version); const data = versionData.data; const libraries = data.libraries; const librariesList = [versionPath]; for (const i in libraries) { const libraryData = libraries[i]; if (!libraryData.rules || validateLibraryRules(libraryData.rules)) { const filePath = `libraries/${libraryData.downloads.artifact.path}`; librariesList.push(filePath); } } const launchData = { auth_player_name: this.authData.playerName, version_name: version, game_directory: this.workDir, assets_root: this.getAssetsDir(), assets_index_name: data.assets, auth_uuid: this.authData.uuid, auth_access_token: this.authData.accessToken, clientid: this.authData.clientId, auth_xuid: this.authData.xuid, user_type: this.authData.type, version_type: versionData.type } const launchDataKeys = Object.keys(launchData); const gameArguments = []; data.arguments.game.forEach(argument => { if (typeof(argument) === "string") { let arg = argument; for (let i in launchDataKeys) { const launchDataKey = launchDataKeys[i]; const launchDataValue = launchData[launchDataKey]; arg = arg.replaceAll("${"+launchDataKey+"}", launchDataValue); } gameArguments.push(arg); } else { if (argument.rules) { let add = true; let checkedRules = 0; for (let i in argument.rules) { const rule = argument.rules[i]; if (rule.features) { if (rule.features.is_demo_user === true) { checkedRules++; add = add & this.authData.isDemo; } else if (rule.features.is_demo_user === false) { checkedRules++; add = add & !this.authData.isDemo; } } } if (checkedRules === argument.rules.length && add && argument.value) { gameArguments.push(argument.value); } } } }); const jvmArguments = [ "-Xms2G", "-Xmx4G", "-cp", `${librariesList.join(";")}`, data.mainClass, ...gameArguments ]; console.log(jvmArguments); if (this.serverListUrl && this.serverListUrl !== "") { const serverList = await getJSON(this.serverListUrl); if (serverList && serverList.length > 0) { const serversDat = new ServersDat(`${this.workDir}/servers.dat`); await serversDat.load(); serverList.forEach(server => { serversDat.updateOrAddToTop(new ServersDatServer(server.name, server.address)); }); await serversDat.save(); } } let connectingTo = ""; const mc = spawn("java", jvmArguments, { cwd: this.workDir }); this.events.emit("started", mc); mc.stdout.on('data', function (data) { const logLine = data.toString().trim(); if (connectingTo !== "" && logLine.indexOf("[System] [CHAT]") > -1) { console.log("Connected to", connectingTo); } const connectingIndex = logLine.indexOf("Connecting to "); if (connectingIndex > -1) { const d = logLine.substring(14 + connectingIndex).split(","); const address = d[0].trim(); const port = d[1].trim(); connectingTo = `${address}:${port}`; } else { connectingTo = ""; } console.log('Minecraft(stdout): ' + logLine); }); mc.stderr.on('data', function (data) { console.log('Minecraft(stderr): ' + data.toString().trim()); }); mc.on('exit', function (code) { console.log('Minecraft exited with code ' + code.toString()); }); } async startLatestRelease() { const version = this.versionManifest.latest.release; return await this.startVersion(version); } } module.exports = { Instance, ServersDat, ServersDatServer }