const child = require("child_process"); const path = require("path"); const Handler = require("./handler"); const fs = require("fs"); const EventEmitter = require("events").EventEmitter; class PiMiCore extends EventEmitter { async launch(options) { try { this.options = { ...options }; this.options.root = path.resolve(this.options.root); this.options.overrides = { detached: true, ...this.options.overrides, url: { meta: "https://launchermeta.mojang.com", resource: "https://resources.download.minecraft.net", mavenForge: "http://files.minecraftforge.net/maven/", defaultRepoForge: "https://libraries.minecraft.net/", fallbackMaven: "https://search.maven.org/remotecontent?filepath=", ...(this.options.overrides ? this.options.overrides.url : undefined), }, fw: { baseUrl: "https://github.com/ZekerZhayard/ForgeWrapper/releases/download/", version: "1.5.5", sh1: "566dfd60aacffaa02884614835f1151d36f1f985", size: 34331, ...(this.options.overrides ? this.options.overrides.fw : undefined), }, }; this.handler = new Handler(this); this._printVersion(); const java = await this.handler.checkJava( this.options.javaPath || "java" ); if (!java.run) { this.emit( "debug", `[PiMi]: Couldn't start Minecraft due to: ${java.message}` ); this.emit("close", 1); return null; } this._createRootDirectory(); this._createGameDirectory(); await this._extractPackage(); if (this.options.installer) { // So installers that create a profile in launcher_profiles.json can run without breaking. const profilePath = path.join( this.options.root, "launcher_profiles.json" ); if ( !fs.existsSync(profilePath) || !JSON.parse(fs.readFileSync(profilePath)).profiles ) { fs.writeFileSync( profilePath, JSON.stringify({ profiles: {} }, null, 4) ); } const code = await this.handler.runInstaller(this.options.installer); if (!this.options.version.custom && code === 0) { this.emit( "debug", "[PiMi]: Installer successfully ran, but no custom version was provided" ); } this.emit("debug", `[PiMi]: Installer closed with code ${code}`); } const directory = this.options.overrides.directory || path.join( this.options.root, "versions", this.options.version.custom ? this.options.version.custom : this.options.version.number ); this.options.directory = directory; const versionFile = await this.handler.getVersion(); const mcPath = this.options.overrides.minecraftJar || (this.options.version.custom ? path.join( this.options.root, "versions", this.options.version.custom, `${this.options.version.custom}.jar` ) : path.join(directory, `${this.options.version.number}.jar`)); this.options.mcPath = mcPath; const nativePath = await this.handler.getNatives(); if (!fs.existsSync(mcPath)) { this.emit( "debug", "[PiMi]: Attempting to download Minecraft version jar" ); await this.handler.getJar(); } const modifyJson = await this._getModifyJson(); const args = []; let jvm = [ "-XX:-UseAdaptiveSizePolicy", "-XX:-OmitStackTraceInFastThrow", "-Dfml.ignorePatchDiscrepancies=true", "-Dfml.ignoreInvalidMinecraftCertificates=true", `-Djava.library.path=${nativePath}`, `-Xmx${this.handler.getMemory()[0]}`, `-Xms${this.handler.getMemory()[1]}`, ]; if (this.handler.getOS() === "osx") { if (parseInt(versionFile.id.split(".")[1]) > 12) jvm.push(await this.handler.getJVM()); } else jvm.push(await this.handler.getJVM()); if (this.options.customArgs) jvm = jvm.concat(this.options.customArgs); if (this.options.overrides.logj4ConfigurationFile) { jvm.push( `-Dlog4j.configurationFile=${path.resolve( this.options.overrides.logj4ConfigurationFile )}` ); } // https://help.minecraft.net/hc/en-us/articles/4416199399693-Security-Vulnerability-in-Minecraft-Java-Edition if (parseInt(versionFile.id.split(".")[1]) === 17) jvm.push("-Dlog4j2.formatMsgNoLookups=true"); if (parseInt(versionFile.id.split(".")[1]) < 17) { if (!jvm.find((arg) => arg.includes("Dlog4j.configurationFile"))) { const configPath = path.resolve( this.options.overrides.cwd || this.options.root ); const intVersion = parseInt(versionFile.id.split(".")[1]); if (intVersion >= 12) { await this.handler.downloadAsync( "https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml", configPath, "log4j2_112-116.xml", true, "log4j" ); jvm.push("-Dlog4j.configurationFile=log4j2_112-116.xml"); } else if (intVersion >= 7) { await this.handler.downloadAsync( "https://launcher.mojang.com/v1/objects/dd2b723346a8dcd48e7f4d245f6bf09e98db9696/log4j2_17-111.xml", configPath, "log4j2_17-111.xml", true, "log4j" ); jvm.push("-Dlog4j.configurationFile=log4j2_17-111.xml"); } } } const classes = this.options.overrides.classes || this.handler.cleanUp(await this.handler.getClasses(modifyJson)); const classPaths = ["-cp"]; const separator = this.handler.getOS() === "windows" ? ";" : ":"; this.emit("debug", `[PiMi]: Using ${separator} to separate class paths`); // Handling launch arguments. const file = modifyJson || versionFile; // So mods like fabric work. const jar = fs.existsSync(mcPath) ? `${separator}${mcPath}` : `${separator}${path.join( directory, `${this.options.version.number}.jar` )}`; classPaths.push( `${ this.options.forge ? this.options.forge + separator : "" }${classes.join(separator)}${jar}` ); classPaths.push(file.mainClass); this.emit("debug", "[PiMi]: Attempting to download assets"); await this.handler.getAssets(); // Forge -> Custom -> Vanilla const launchOptions = await this.handler.getLaunchOptions(modifyJson); const launchArguments = args.concat(jvm, classPaths, launchOptions); this.emit("arguments", launchArguments); this.emit( "debug", `[PiMi]: Launching with arguments ${launchArguments.join(" ")}` ); return this.startMinecraft(launchArguments); } catch (e) { this.emit("debug", `[PiMi]: Failed to start due to ${e}, closing...`); return null; } } _printVersion() { if (fs.existsSync(path.join(__dirname, "..", "package.json"))) { const { version } = require("../package.json"); this.emit("debug", `[PiMi]: PiMi version ${version}`); } else { this.emit( "debug", "[PiMi]: Package JSON not found, skipping PiMi version check." ); } } _createRootDirectory() { if (!fs.existsSync(this.options.root)) { this.emit("debug", "[PiMi]: Attempting to create root folder"); fs.mkdirSync(this.options.root); } } _createGameDirectory() { if (this.options.overrides.gameDirectory) { this.options.overrides.gameDirectory = path.resolve( this.options.overrides.gameDirectory ); if (!fs.existsSync(this.options.overrides.gameDirectory)) { fs.mkdirSync(this.options.overrides.gameDirectory, { recursive: true }); } } } async _extractPackage() { if (this.options.clientPackage) { this.emit( "debug", `[PiMi]: Extracting client package to ${this.options.root}` ); await this.handler.extractPackage(); } } async _getModifyJson() { let modifyJson = null; if (this.options.forge) { this.options.forge = path.resolve(this.options.forge); this.emit( "debug", "[PiMi]: Detected Forge in options, getting dependencies" ); modifyJson = await this.handler.getForgedWrapped(); } else if (this.options.version.custom) { this.emit( "debug", "[PiMi]: Detected custom in options, setting custom version file" ); modifyJson = modifyJson || JSON.parse( fs.readFileSync( path.join( this.options.root, "versions", this.options.version.custom, `${this.options.version.custom}.json` ), { encoding: "utf8" } ) ); } return modifyJson; } startMinecraft(launchArguments) { const minecraft = child.spawn( this.options.javaPath ? this.options.javaPath : "java", launchArguments, { cwd: this.options.overrides.cwd || this.options.root, detached: this.options.overrides.detached, } ); minecraft.stdout.on("data", (data) => this.emit("data", data.toString("utf-8")) ); minecraft.stderr.on("data", (data) => this.emit("data", data.toString("utf-8")) ); minecraft.on("close", (code) => this.emit("close", code)); return minecraft; } } module.exports = PiMiCore;