From 0d4bcfd6f680970d2fa90b1bec5a778463226002 Mon Sep 17 00:00:00 2001 From: Pierce Date: Wed, 22 May 2019 18:28:48 -0400 Subject: [PATCH 1/5] rewrite --- README.md | 157 ++++----- components/authenticator.js | 37 ++- components/handler.js | 629 +++++++++++++++++------------------- components/launcher.js | 171 +++++----- components/package.js | 14 + index.js | 6 +- package.json | 2 +- 7 files changed, 503 insertions(+), 513 deletions(-) create mode 100644 components/package.js diff --git a/README.md b/README.md index d97a187..6e8a60a 100644 --- a/README.md +++ b/README.md @@ -13,57 +13,56 @@ https://discord.gg/8uYVbXP ### Standard Example ```javascript -const launcher = require('minecraft-launcher-core'); +const { Client, Authenticator } = require('minecraft-launcher-core'); -launcher.authenticator.getAuth("email", "password").then(auth => { - // Save the auth to a file so it can be used later on! - launcher.core({ - authorization: auth, - clientPackage: null, - forge: null, - root: "C:/Users/user/AppData/Roaming/.mc", - os: "windows", - version: { - number: "1.13.2", - type: "release" - }, - memory: { - max: "3000", - min: "1000" - } - }); -}); +let opts = { + authorization: async () => { return await Authenticator.getAuth(username, password) }, + clientPackage: null, + root: "./minecraft", + os: "windows", + version: { + number: "1.14", + type: "release" + } + memory: { + max: "6000", + min: "4000" + } +} + +const launcher = new Client(opts); +launcher.launch(); + +launcher.on('debug', (e) => console.log(e)); +launcher.on('data', (e) => console.log(e)); +launcher.on('error', (e) => console.log(e)); ``` ### Usage -##### launcher.core Options - -| Parameter | Type | Description | Required | -|--------------------------|--------|-------------------------------------------------------------------------------------------|----------| -| `options.authorization` | Object | The result from `getAuth` function, allows the client to login in online or offline mode. | True | -| `options.clientPackage` | String | Path to the client package zip file. | False | -| `options.root` | String | Path where you want the launcher to work in. like `C:/Users/user/AppData/Roaming/.mc` | True | -| `options.os` | String | windows, osx or linux | True | -| `options.javaPath` | String | Path to the JRE executable file, will default to `java` if not entered. | False | -| `options.version.number` | String | Minecraft version that is going to be launched. | True | -| `options.version.type` | String | Any string. The actual Minecraft launcher uses `release` and `snapshot`. | True | -| `options.version.custom` | String | Name of the jar, json, and folder of the custom client you are launching with. (Optifine) | False | -| `options.memory.max` | String | Max amount of memory being used by Minectaft | True | -| `options.memory.min` | String | Min amount of memory being used by Minectaft | True | -| `options.forge` | String | Path to Universal Forge Jar | False | -| `options.customArgs` | String | Array of custom JVM options | False | -| `options.server.host` | String | Host url to the server, don't include the port | False | -| `options.server.port` | String | Port of the host url, will default to `25565` if not entered. | False | -| `options.proxy.host` | String | Host url to the proxy, don't include the port | False | -| `options.proxy.port` | String | Port of the host proxy, will default to `8080` if not entered. | False | -| `options.proxy.username` | String | Username for the proxy. | False | -| `options.proxy.password` | String | Password for the proxy. | False | +##### Client Options +| Parameter | Type | Description | Required | +|--------------------------|----------|-------------------------------------------------------------------------------------------|----------| +| `options.authorization` | Object | The result from `getAuth` function, allows the client to login in online or offline mode. | True | +| `options.clientPackage` | String | Path to the client package zip file. | False | +| `options.root` | String | Path where you want the launcher to work in. like `C:/Users/user/AppData/Roaming/.mc`, | True | +| `options.os` | String | windows, osx or linux, | True | +| `options.version.number` | String | Minecraft version that is going to be launched. | True | +| `options.version.type` | String | Any string. The actual Minecraft launcher uses `release` and `snapshot`. | True | +| `options.memory.max` | String | Max amount of memory being used by Minectaft. | True | +| `options.forge.path` | String | Path to Universal Forge Jar. | False | +| `options.server.host` | String | Host url to the server, don't include the port. | False | +| `options.server.port` | String | Port of the host url, will default to `25565` if not entered. | False | +| `options.proxy.host` | String | Host url to the proxy, don't include the port. | False | +| `options.proxy.port` | String | Port of the host proxy, will default to `8080` if not entered. | False | +| `options.proxy.username` | String | Username for the proxy. | False | +| `options.proxy.password` | String | Password for the proxy. | False | +| `options.timeout` | Interger | Timeout on download requests. | False | ##### Note If you are loading up a client outside of vanilla Minecraft or Forge (Optifine and for an example), you'll need to download the needed files yourself if you don't provide downloads url downloads like Forge and Fabric. Still need to provide the version jar. -#### launcher.authenticator Functions +#### Authenticator Functions ##### getAuth @@ -86,6 +85,20 @@ if you don't provide downloads url downloads like Forge and Fabric. Still need t | `client_token` | String | Token being checked if it's the same client that the access_token was created from. | True | | `selected_profile` | Object | Json Object that was returned from Mojangs auth api. | True | +##### invalidate + +| Parameter | Type | Description | Required | +|--------------|--------|-------------------------------------------------------------------|----------| +| `access_token` | String | Token being checked if it can be used to login with (online mode). | True | +| `client_token` | String | Token being checked if it's the same client that the access_token was created from. | True | + +##### signOut + +| Parameter | Type | Description | Required | +|--------------|--------|--------------------------------------|----------| +| `username` | String | Username used to login with | True | +| `password` | String | Password used to login with | True | + #### Events | Event Name | Type | Description | @@ -98,66 +111,6 @@ if you don't provide downloads url downloads like Forge and Fabric. Still need t | `download` | String | Emitted when a file successfully downloads | | `download-status` | Object | Emitted when data is received while downloading | | `debug` | String | Emitted when functions occur, made to help debug if errors occur | -#### Client Package Function - -Client Packages allow the client to run offline on setup. This function should be used outside the actual launcher. -this function is in the `handler` component. - -##### makePackage - -| Parameter | Type | Description | Required | -|------------|--------|-----------------------------------------------------------------------|----------| -| `versions` | Array | Array of the versions being downloaded and being made into a package. | True | -| `os` | String | OS that the package will be loaded on. OS specific natives need this. | True | - -### Other Examples - -##### Using Validate and Refresh - -```javascript -let auth = require("pathToUserAuthJson.json"); - -const validateCheck = await launcher.authenticator.validate(auth.access_token); -if(!validateCheck) { - auth = await launcher.authenticator.refreshAuth(auth.access_token, auth.client_token, auth.selected_profile); -} -launcher.core({ - authorization: auth, - clientPackage: null, - root: "directory", - os: "windows", - version: { - number: "1.13.2", - type: "MCC-Launcher" - }, - memory: { - max: "500", - min: "100" - } -}); -``` - -##### Using With Forge - -```js -launcher.authenticator.getAuth("email", "password").then(auth => { - launcher.core({ - authorization: auth, - clientPackage: null, - root: "C:/Users/user/AppData/Roaming/.mc", - forge: "C:/Users/user/Desktop/forge.jar", - os: "windows", - version: { - number: "1.12.2", // needs to be the same as the Forge version - type: "MCC-Launcher" - }, - memory: { - max: "500", - min: "100" - } - }); -}); -``` #### What should it look like running from console? diff --git a/components/authenticator.js b/components/authenticator.js index f318603..ab1ec31 100644 --- a/components/authenticator.js +++ b/components/authenticator.js @@ -2,7 +2,6 @@ const request = require('request'); const uuid = require('uuid/v1'); const api_url = "https://authserver.mojang.com"; - module.exports.getAuth = function (username, password) { return new Promise(resolve => { if(!password) { @@ -98,4 +97,40 @@ module.exports.refreshAuth = function (accessToken, clientToken, selectedProfile resolve(userProfile); }); }); +}; + +module.exports.invalidate = function(accessToken, clientToken) { + return new Promise(resolve => { + const requestObject = { + url: api_url + "/invalidate", + json: { + "accessToken": accessToken, + "clientToken": clientToken + } + }; + + request.post(requestObject, function(error, response, body) { + if (error) resolve(error); + + if(!body) resolve(true); else resolve(false); + }); + }); +}; + +module.exports.signOut = function(username, password) { + return new Promise(resolve => { + const requestObject = { + url: api_url + "/invalidate", + json: { + "username": username, + "password": password + } + }; + + request.post(requestObject, function(error, response, body) { + if (error) resolve(error); + + if(!body) resolve(true); else resolve(false); + }); + }); }; \ No newline at end of file diff --git a/components/handler.js b/components/handler.js index 9e036ee..ee237db 100644 --- a/components/handler.js +++ b/components/handler.js @@ -4,389 +4,366 @@ const path = require('path'); const request = require('request'); const checksum = require('checksum'); const zip = require('adm-zip'); -const event = require('./events'); +class Handler { + constructor(client) { + this.client = client; + this.options = client.options; + this.version = undefined; + } -function downloadAsync (url, directory, name) { - return new Promise(resolve => { - shelljs.mkdir('-p', directory); + downloadAsync(url, directory, name) { + return new Promise(resolve => { + shelljs.mkdir('-p', directory); - const _request = request(url, {timeout: 10000}); + const _request = request(url, {timeout: this.options.timeout || 10000}); - _request.on('error', function(error) { - resolve({ - failed: true, - asset: { - url: url, - directory: directory, - name: name + _request.on('error', function(error) { + resolve({ + failed: true, + asset: { + url: url, + directory: directory, + name: name + } + }); + }); + + _request.on('data', (data) => { + let size = 0; + if(fs.existsSync(path.join(directory, name))) size = fs.statSync(path.join(directory, name))["size"]; + this.client.emit('download-status', { + "name": name, + "current": Math.round(size / 10000), + "total": data.length + }) + }); + + const file = fs.createWriteStream(path.join(directory, name)); + _request.pipe(file); + + file.once('finish', () => { + this.client.emit('download', name); + resolve({failed: false, asset: null}); + }); + + file.on('error', (e) => { + this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${e}`); + if(fs.existsSync(path.join(directory, name))) shelljs.rm(path.join(directory, name)); + resolve({ + failed: true, + asset: { + url: url, + directory: directory, + name: name + } + }); + }); + }); + } + + checkSum(hash, file) { + return new Promise(resolve => { + checksum.file(file, (err, sum) => resolve(hash === sum)); + }); + } + + getVersion() { + return new Promise(resolve => { + if(fs.existsSync(path.join(this.options.directory, `${this.options.version}.json`))) { + this.version = require(path.join(this.options.directory, `${this.options.version}.json`)); + resolve(this.version); + return; + } + + const manifest = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; + request.get(manifest, (error, response, body) => { + if (error) resolve(error); + + const parsed = JSON.parse(body); + + for (const desiredVersion in parsed.versions) { + if(parsed.versions[desiredVersion].id === this.options.version.number) { + request.get(parsed.versions[desiredVersion].url, (error, response, body) => { + if (error) resolve(error); + + this.client.emit('debug', `[MCLC]: Parsed version from version manifest`); + this.version = JSON.parse(body); + resolve(this.version); + }); + } } }); }); + } - _request.on('data', (data) => { - let size = 0; - if(fs.existsSync(path.join(directory, name))) size = fs.statSync(path.join(directory, name))["size"]; - event.emit('download-status', { - "name": name, - "current": Math.round(size / 10000), - "total": data.length - }) + getJar() { + return new Promise(async (resolve)=> { + await this.downloadAsync(this.version.downloads.client.url, directory, `${this.options.version.number}.jar`); + + fs.writeFileSync(path.join(directory, `${this.options.version.number}.json`), JSON.stringify(this.options.version, null, 4)); + + this.client.emit('debug', '[MCLC]: Downloaded version jar and wrote version json'); + + resolve(); }); + } - const file = fs.createWriteStream(path.join(directory, name)); - _request.pipe(file); + getAssets() { + return new Promise(async(resolve) => { + const assetsUrl = 'https://resources.download.minecraft.net'; + const failed = []; - file.once('finish', function() { - event.emit('download', name); - resolve({failed: false, asset: null}); - }); - - file.on('error', (e) => { - event.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${e}`); - if(fs.existsSync(path.join(directory, name))) shelljs.rm(path.join(directory, name)); - resolve({ - failed: true, - asset: { - url: url, - directory: directory, - name: name - } - }); - }); - }); -} - -function checkSum(hash, file, size) { - return new Promise(resolve => { - checksum.file(file, (err, sum) => resolve(hash === sum)); - }); -} - -module.exports.getVersion = function (version, directory) { - return new Promise(resolve => { - if(fs.existsSync(path.join(directory, `${version}.json`))) { - resolve(require(path.join(directory, `${version}.json`))); - return; - } - - const manifest = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; - request.get(manifest, function(error, response, body) { - if (error) resolve(error); - - const parsed = JSON.parse(body); - - for (const desiredVersion in parsed.versions) { - if(parsed.versions[desiredVersion].id === version) { - request.get(parsed.versions[desiredVersion].url, function(error, response, body) { - if (error) resolve(error); - - event.emit('debug', `[MCLC]: Parsed version from version manifest`); - resolve(JSON.parse(body)); - }); - } + if(!fs.existsSync(path.join(this.options.directory, 'assets', 'indexes', `${this.version.assetIndex.id}.json`))) { + await this.downloadAsync(this.version.assetIndex.url, path.join(this.options.directory, 'assets', 'indexes'), `${this.version.assetIndex.id}.json`); } - }); - }); -}; -module.exports.getJar = function (version, number, directory) { - return new Promise(async (resolve)=> { - await downloadAsync(version.downloads.client.url, directory, `${number}.jar`); + const index = require(path.join(this.options.directory, 'assets', 'indexes',`${this.version.assetIndex.id}.json`)); - fs.writeFileSync(path.join(directory, `${number}.json`), JSON.stringify(version, null, 4)); - - event.emit('debug', '[MCLC]: Downloaded version jar and wrote version json'); - - resolve(); - }); -}; - -module.exports.getAssets = function (directory, version) { - return new Promise(async(resolve) => { - const assetsUrl = 'https://resources.download.minecraft.net'; - const failed = []; - - if(!fs.existsSync(path.join(directory, 'assets', 'indexes', `${version.assetIndex.id}.json`))) { - await downloadAsync(version.assetIndex.url, path.join(directory, 'assets', 'indexes'), `${version.assetIndex.id}.json`); - } - - const index = require(path.join(directory, 'assets', 'indexes',`${version.assetIndex.id}.json`)); - - await Promise.all(Object.keys(index.objects).map(async asset => { - const hash = index.objects[asset].hash; - const subhash = hash.substring(0,2); - const assetDirectory = path.join(directory, 'assets', 'objects', subhash); - - if(!fs.existsSync(path.join(assetDirectory, hash)) || !await checkSum(hash, path.join(assetDirectory, hash))) { - const download = await downloadAsync(`${assetsUrl}/${subhash}/${hash}`, assetDirectory, hash); - - if(download.failed) failed.push(download.asset); - } - })); - - // why do we have this? B/c sometimes Minecraft's resource site times out! - if(failed) { - await Promise.all(failed.map(async asset => await downloadAsync(asset.url, asset.directory, asset.name))) - } - - // Copy assets to legacy if it's an older Minecarft version. - if(version.assets === "legacy" || version.assets === "pre-1.6") { await Promise.all(Object.keys(index.objects).map(async asset => { const hash = index.objects[asset].hash; const subhash = hash.substring(0,2); - const assetDirectory = path.join(directory, 'assets', 'objects', subhash); + const assetDirectory = path.join(this.options.directory, 'assets', 'objects', subhash); - let legacyAsset = asset.split('/'); - legacyAsset.pop(); + if(!fs.existsSync(path.join(assetDirectory, hash)) || !await this.checkSum(hash, path.join(assetDirectory, hash))) { + const download = await this.downloadAsync(`${assetsUrl}/${subhash}/${hash}`, assetDirectory, hash); - if(!fs.existsSync(path.join(directory, 'assets', 'legacy', legacyAsset.join('/')))) { - shelljs.mkdir('-p', path.join(directory, 'assets', 'legacy', legacyAsset.join('/'))); - } - - if (!fs.existsSync(path.join(directory, 'assets', 'legacy', asset))) { - fs.copyFileSync(path.join(assetDirectory, hash), path.join(directory, 'assets', 'legacy', asset)) + if(download.failed) failed.push(download.asset); } })); - } - event.emit('debug', '[MCLC]: Downloaded assets'); - resolve(); - }); -}; + // why do we have this? B/c sometimes Minecraft's resource site times out! + if(failed) { + await Promise.all(failed.map(async asset => await this.downloadAsync(asset.url, asset.directory, asset.name))) + } -module.exports.getNatives = function (root, version, os) { - return new Promise(async(resolve) => { - let nativeDirectory; + // Copy assets to legacy if it's an older Minecarft version. + if(this.version.assets === "legacy" || this.version.assets === "pre-1.6") { + await Promise.all(Object.keys(index.objects).map(async asset => { + const hash = index.objects[asset].hash; + const subhash = hash.substring(0,2); + const assetDirectory = path.join(this.options.directory, 'assets', 'objects', subhash); - if(fs.existsSync(path.join(root, 'natives', version.id))) { - nativeDirectory = path.join(root, 'natives', version.id); - } else { - nativeDirectory = path.join(root, "natives", version.id); + let legacyAsset = asset.split('/'); + legacyAsset.pop(); - shelljs.mkdir('-p', nativeDirectory); - - await Promise.all(version.libraries.map(async function (lib) { - if (!lib.downloads.classifiers) return; - const type = `natives-${os}`; - const native = lib.downloads.classifiers[type]; - - if (native) { - const name = native.path.split('/').pop(); - await downloadAsync(native.url, nativeDirectory, name); - if(!await checkSum(native.sha1, path.join(nativeDirectory, name))) { - await downloadAsync(native.url, nativeDirectory, name); + if(!fs.existsSync(path.join(this.options.directory, 'assets', 'legacy', legacyAsset.join('/')))) { + shelljs.mkdir('-p', path.join(this.options.directory, 'assets', 'legacy', legacyAsset.join('/'))); } - try {new zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true);} catch(e) { - // Only doing a console.warn since a stupid error happens. You can basically ignore this. - // if it says Invalid file name, just means two files were downloaded and both were deleted. - // All is well. - console.warn(e); + + if (!fs.existsSync(path.join(this.options.directory, 'assets', 'legacy', asset))) { + fs.copyFileSync(path.join(assetDirectory, hash), path.join(this.options.directory, 'assets', 'legacy', asset)) } - shelljs.rm(path.join(nativeDirectory, name)); - } - })); - event.emit('debug', '[MCLC]: Downloaded and extracted natives'); - } + })); + } - event.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`); - resolve(nativeDirectory); - }); -}; - -module.exports.getForgeDependencies = async function(root, version, forgeJarPath) { - if(!fs.existsSync(path.join(root, 'forge'))) { - shelljs.mkdir('-p', path.join(root, 'forge')); + this.client.emit('debug', '[MCLC]: Downloaded assets'); + resolve(); + }); } - await new zip(forgeJarPath).extractEntryTo('version.json', path.join(root, 'forge', `${version.id}`), false, true); - const forge = require(path.join(root, 'forge', `${version.id}`, 'version.json')); - const mavenUrl = 'http://files.minecraftforge.net/maven/'; - const defaultRepo = 'https://libraries.minecraft.net/'; - const paths = []; + getNatives() { + return new Promise(async(resolve) => { + let nativeDirectory; - await Promise.all(forge.libraries.map(async library => { - const lib = library.name.split(':'); - - if(lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) return; - - let url = mavenUrl; - const jarPath = path.join(root, 'libraries', `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`); - const name = `${lib[1]}-${lib[2]}.jar`; - - if(!library.url) { - if(library.serverreq || library.clientreq) { - url = defaultRepo; + if(fs.existsSync(path.join(this.options.root, 'natives', this.version.id))) { + nativeDirectory = path.join(this.options.root, 'natives', this.version.id); } else { - return - } - } + nativeDirectory = path.join(this.options.root, "natives", this.version.id); - const downloadLink = `${url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${lib[1]}-${lib[2]}.jar`; + shelljs.mkdir('-p', nativeDirectory); - if(fs.existsSync(path.join(jarPath, name))) { - paths.push(`${jarPath}${path.sep}${name}`); - return; - } - if(!fs.existsSync(jarPath)) shelljs.mkdir('-p', jarPath); + await Promise.all(version.libraries.map(async (lib) => { + if (!lib.downloads.classifiers) return; + const type = `natives-${this.options.os}`; + const native = lib.downloads.classifiers[type]; - await downloadAsync(downloadLink, jarPath, name); - - paths.push(`${jarPath}${path.sep}${name}`); - })); - - event.emit('debug', '[MCLC]: Downloaded Forge dependencies'); - - return {paths, forge}; -}; - -module.exports.getClasses = function (options, version) { - return new Promise(async (resolve) => { - const libs = []; - - if(options.version.custom) { - const customJarJson = require(path.join(options.root, 'versions', options.version.custom, `${options.version.custom}.json`)); - await Promise.all(customJarJson.libraries.map(async library => { - const lib = library.name.split(':'); - - const jarPath = path.join(options.root, 'libraries', `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`); - const name = `${lib[1]}-${lib[2]}.jar`; - - if(!fs.existsSync(path.join(jarPath, name))) { - if(library.url) { - const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${lib[1]}-${lib[2]}.jar`; - await downloadAsync(url, jarPath, name); + if (native) { + const name = native.path.split('/').pop(); + await this.downloadAsync(native.url, nativeDirectory, name); + if(!await this.checkSum(native.sha1, path.join(nativeDirectory, name))) { + await this.downloadAsync(native.url, nativeDirectory, name); + } + try {new zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true);} catch(e) { + // Only doing a console.warn since a stupid error happens. You can basically ignore this. + // if it says Invalid file name, just means two files were downloaded and both were deleted. + // All is well. + console.warn(e); + } + shelljs.rm(path.join(nativeDirectory, name)); } - } - libs.push(`${jarPath}/${name}`); - })); - } - - await Promise.all(version.libraries.map(async (_lib) => { - if(!_lib.downloads.artifact) return; - - const libraryPath = _lib.downloads.artifact.path; - const libraryUrl = _lib.downloads.artifact.url; - const libraryHash = _lib.downloads.artifact.sha1; - const libraryDirectory = path.join(options.root, 'libraries', libraryPath); - - if(!fs.existsSync(libraryDirectory) || !await checkSum(libraryHash, libraryDirectory)) { - let directory = libraryDirectory.split(path.sep); - const name = directory.pop(); - directory = directory.join(path.sep); - - await downloadAsync(libraryUrl, directory, name); + })); + this.client.emit('debug', '[MCLC]: Downloaded and extracted natives'); } - libs.push(libraryDirectory); + this.client.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`); + resolve(nativeDirectory); + }); + } + + async getForgeDependencies() { + if(!fs.existsSync(path.join(root, 'forge'))) { + shelljs.mkdir('-p', path.join(root, 'forge')); + } + await new zip(this.options.forge).extractEntryTo('version.json', path.join(this.options.root, 'forge', `${this.version.id}`), false, true); + + const forge = require(path.join(this.options.root, 'forge', `${this.version.id}`, 'version.json')); + const mavenUrl = 'http://files.minecraftforge.net/maven/'; + const defaultRepo = 'https://libraries.minecraft.net/'; + const paths = []; + + await Promise.all(forge.libraries.map(async library => { + const lib = library.name.split(':'); + + if(lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) return; + + let url = mavenUrl; + const jarPath = path.join(root, 'libraries', `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`); + const name = `${lib[1]}-${lib[2]}.jar`; + + if(!library.url) { + if(library.serverreq || library.clientreq) { + url = defaultRepo; + } else { + return + } + } + + const downloadLink = `${url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${lib[1]}-${lib[2]}.jar`; + + if(fs.existsSync(path.join(jarPath, name))) { + paths.push(`${jarPath}${path.sep}${name}`); + return; + } + if(!fs.existsSync(jarPath)) shelljs.mkdir('-p', jarPath); + + await this.downloadAsync(downloadLink, jarPath, name); + + paths.push(`${jarPath}${path.sep}${name}`); })); - event.emit('debug', '[MCLC]: Collected class paths'); - resolve(libs) - }); -}; + this.client.emit('debug', '[MCLC]: Downloaded Forge dependencies'); -module.exports.cleanUp = async function(array) { - const newArray = []; - - for(let argument in array) { - if(newArray.includes(array[argument])) continue; - newArray.push(array[argument]); + return {paths, forge}; } - return newArray; -}; + getClasses() { + return new Promise(async (resolve) => { + const libs = []; -module.exports.getLaunchOptions = function (version, modification, options) { - return new Promise(resolve => { - let type = modification || version; + if(this.options.version.custom) { + const customJarJson = require(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`)); + await Promise.all(customJarJson.libraries.map(async library => { + const lib = library.name.split(':'); - let arguments = type.minecraftArguments ? type.minecraftArguments.split(' ') : type.arguments.game; - const assetPath = version.assets === "legacy" || version.assets === "pre-1.6" ? path.join(options.root, 'assets', 'legacy') : path.join(options.root, 'assets'); + const jarPath = path.join(this.options.root, 'libraries', `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`); + const name = `${lib[1]}-${lib[2]}.jar`; - if(arguments.length < 5) arguments = arguments.concat(version.minecraftArguments ? version.minecraftArguments.split(' ') : version.arguments.game); - - const fields = { - '${auth_access_token}': options.authorization.access_token, - '${auth_session}': options.authorization.access_token, - '${auth_player_name}': options.authorization.name, - '${auth_uuid}': options.authorization.uuid, - '${user_properties}': options.authorization.user_properties, - '${user_type}': 'mojang', - '${version_name}': options.version.number, - '${assets_index_name}': version.assetIndex.id, - '${game_directory}': path.join(options.root), - '${assets_root}': assetPath, - '${game_assets}': assetPath, - '${version_type}': options.version.type - }; - - for (let index = 0; index < arguments.length; index++) { - if (Object.keys(fields).includes(arguments[index])) { - arguments[index] = fields[arguments[index]]; + if(!fs.existsSync(path.join(jarPath, name))) { + if(library.url) { + const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${lib[1]}-${lib[2]}.jar`; + await this.downloadAsync(url, jarPath, name); + } + } + libs.push(`${jarPath}/${name}`); + })); } + + await Promise.all(this.version.libraries.map(async (_lib) => { + if(!_lib.downloads.artifact) return; + + const libraryPath = _lib.downloads.artifact.path; + const libraryUrl = _lib.downloads.artifact.url; + const libraryHash = _lib.downloads.artifact.sha1; + const libraryDirectory = path.join(this.options.root, 'libraries', libraryPath); + + if(!fs.existsSync(libraryDirectory) || !await this.checkSum(libraryHash, libraryDirectory)) { + let directory = libraryDirectory.split(path.sep); + const name = directory.pop(); + directory = directory.join(path.sep); + + await this.downloadAsync(libraryUrl, directory, name); + } + + libs.push(libraryDirectory); + })); + + this.client.emit('debug', '[MCLC]: Collected class paths'); + resolve(libs) + }); + } + + static cleanUp(array) { + const newArray = []; + + for(let argument in array) { + if(newArray.includes(array[argument])) continue; + newArray.push(array[argument]); } - if(options.server) arguments.push('--server', options.server.host, '--port', options.server.port || "25565"); - if(options.proxy) arguments.push( - '--proxyHost', - options.proxy.host, - '--proxyPort', - options.proxy.port || "8080", - '--proxyUser', - options.proxy.username, - '--proxyPass', - options.proxy.password - ); + return newArray; + } - event.emit('debug', '[MCLC]: Set launch options'); - resolve(arguments); - }); -}; + getLaunchOptions(modification) { + return new Promise(resolve => { + let type = modification || this.version; -module.exports.getJVM = function (version, options) { - return new Promise(resolve => { - switch(options.os) { + let args = type.minecraftArguments ? type.minecraftArguments.split(' ') : type.arguments.game; + const assetPath = this.version.assets === "legacy" || this.version.assets === "pre-1.6" ? path.join(this.options.root, 'assets', 'legacy') : path.join(this.options.root, 'assets'); + + if(args.length < 5) args = args.concat(this.version.minecraftArguments ? this.version.minecraftArguments.split(' ') : this.version.arguments.game); + + const fields = { + '${auth_access_token}': this.options.authorization.access_token, + '${auth_session}': this.options.authorization.access_token, + '${auth_player_name}': this.options.authorization.name, + '${auth_uuid}': this.options.authorization.uuid, + '${user_properties}': this.options.authorization.user_properties, + '${user_type}': 'mojang', + '${version_name}': this.options.version.number, + '${assets_index_name}': this.version.assetIndex.id, + '${game_directory}': path.join(this.options.root), + '${assets_root}': assetPath, + '${game_assets}': assetPath, + '${version_type}': this.options.version.type + }; + + for (let index = 0; index < args.length; index++) { + if (Object.keys(fields).includes(args[index])) { + args[index] = fields[args[index]]; + } + } + + if(this.options.server) args.push('--server', this.options.server.host, '--port', this.options.server.port || "25565"); + if(this.options.proxy) args.push( + '--proxyHost', + this.options.proxy.host, + '--proxyPort', + this.options.proxy.port || "8080", + '--proxyUser', + this.options.proxy.username, + '--proxyPass', + this.options.proxy.password + ); + + this.client.emit('debug', '[MCLC]: Set launch options'); + resolve(args); + }); + } + + async getJVM() { + switch(this.options.os) { case "windows": { - resolve("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"); - break; + return "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump" } case "osx": { - resolve("-XstartOnFirstThread"); - break; + return "-XstartOnFirstThread" } case "linux": { - resolve("-Xss1M"); - break; + return "-Xss1M" } } - }); -}; - -module.exports.makePackage = async function(versions, os) { - const directory = path.join(process.cwd(), 'clientpackage'); - - for(const version in versions) { - const versionFile = await this.getVersion(versions[version], directory); - await this.getNatives(`${directory}/natives/${versions[version]}`, versionFile, os, true); - await this.getJar(versionFile, versions[version], `${directory}/versions/${versions[version]}`); - await this.getClasses(directory, versionFile); - await this.getAssets(directory, versionFile); } +} - const archive = new zip(); - archive.addLocalFolder(directory); - archive.writeZip(`${directory}.zip`); -}; - -module.exports.extractPackage = function(root, clientPackage) { - return new Promise(async resolve => { - if(clientPackage.startsWith('http')) { - await downloadAsync(clientPackage, root, "clientPackage.zip"); - clientPackage = path.join(root, "clientPackage.zip") - } - new zip(clientPackage).extractAllTo(root, true); - event.emit('package-extract', true); - resolve(); - }); -}; +module.exports = Handler; \ No newline at end of file diff --git a/components/launcher.js b/components/launcher.js index 66a66b5..960246d 100644 --- a/components/launcher.js +++ b/components/launcher.js @@ -2,89 +2,102 @@ const child = require('child_process'); const event = require('./events'); const path = require('path'); const handler = require('./handler'); +const packager = require('./package'); const fs = require('fs'); +const EventEmitter = require('events').EventEmitter; +class MCLCore extends EventEmitter { + constructor(options) { + super(); -module.exports = async function (options) { - options.root = path.resolve(options.root); - if(!fs.existsSync(options.root)) { - event.emit('debug', '[MCLC]: Attempting to create root folder'); - fs.mkdirSync(options.root); + this.options = options; + this.handler = new handler(this); } - if(options.clientPackage) { - event.emit('debug', `[MCLC]: Extracting client package to ${options.root}`); - await handler.extractPackage(options.root, options.clientPackage); + async launch() { + this.options.root = path.resolve(this.options.root); + if(!fs.existsSync(this.options.root)) { + this.emit('debug', '[MCLC]: Attempting to create root folder'); + fs.mkdirSync(this.options.root); + } + + if(this.options.clientPackage) { + this.emit('debug', `[MCLC]: Extracting client package to ${this.options.root}`); + await packager.extractPackage(this.options.root, this.options.clientPackage); + } + + const directory = path.join(this.options.root, 'versions', this.options.version.number); + this.options.directory = directory; + + // Version JSON for the main launcher folder + const versionFile = await this.handler.getVersion(); + const mcPath = 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`); + const nativePath = await this.handler.getNatives(); + + if (!fs.existsSync(mcPath)) { + this.emit('debug', '[MCLC]: Attempting to download Minecraft version jar'); + await this.handler.getJar(); + } + + let forge = null; + let custom = null; + if(this.options.forge) { + this.emit('debug', '[MCLC]: Detected Forge in options, getting dependencies'); + forge = await this.handler.getForgeDependencies(); + } + if(this.options.version.custom) { + this.emit('debug', '[MCLC]: Detected custom in options, setting custom version file'); + custom = require(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`)); + } + + const args = []; + + // Jvm + let jvm = [ + '-XX:-UseAdaptiveSizePolicy', + '-XX:-OmitStackTraceInFastThrow', + '-Dfml.ignorePatchDiscrepancies=true', + '-Dfml.ignoreInvalidMinecraftCertificates=true', + `-Djava.library.path=${nativePath}`, + `-Xmx${this.options.memory.max}M`, + `-Xms${this.options.memory.min}M` + ]; + jvm.push(await this.handler.getJVM()); + if(this.options.customArgs) jvm = jvm.concat(this.options.customArgs); + + const classes = await this.handler.getClasses(); + let classPaths = ['-cp']; + const separator = this.options.os === "windows" ? ";" : ":"; + this.emit('debug', `[MCLC]: Using ${separator} to separate class paths`); + if(forge) { + this.emit('debug', '[MCLC]: Setting Forge class paths'); + classPaths.push(`${this.options.forge.path || this.options.forge}${separator}${forge.paths.join(separator)}${separator}${classes.join(separator)}${separator}${mcPath}`); + classPaths.push(forge.forge.mainClass) + } else { + const file = custom || versionFile; + classPaths.push(`${mcPath}${separator}${classes.join(separator)}`); + classPaths.push(file.mainClass); + } + classPaths = await handler.cleanUp(classPaths); + + // Download version's assets + this.emit('debug', '[MCLC]: Attempting to download assets'); + await this.handler.getAssets(); + + // Launch options. Thank you Lyrus for the reformat <3 + const modification = forge ? forge.forge : null || custom ? custom : null; + const launchOptions = await this.handler.getLaunchOptions(modification); + + const launchArguments = args.concat(jvm, classPaths, launchOptions); + event.emit('arguments', launchArguments); + event.emit('debug', launchArguments.join(' ')); + + const minecraft = child.spawn(this.options.javaPath ? this.options.javaPath : 'java', launchArguments); + minecraft.stdout.on('data', (data) => this.emit('data', data)); + minecraft.stderr.on('data', (data) => this.emit('error', data)); + minecraft.on('close', (code) => this.emit('close', code)); } +} - const directory = path.join(options.root, 'versions', options.version.number); - options.directory = directory; - const versionFile = await handler.getVersion(options.version.number, options.directory); - const mcPath = options.version.custom ? path.join(options.root, 'versions', options.version.custom , `${options.version.custom}.jar`): - path.join(directory, `${options.version.number}.jar`); - const nativePath = await handler.getNatives(options.root, versionFile, options.os); - - if (!fs.existsSync(mcPath)) { - event.emit('debug', '[MCLC]: Attempting to download Minecraft version jar'); - await handler.getJar(versionFile, options.version.number, directory); - } - - let forge = null; - let custom = null; - if(options.forge) { - if(options.forge.path) process.emitWarning('\'options.forge.path\' is deprecated and will be removed. Use \'options.forge\' instead'); - event.emit('debug', '[MCLC]: Detected Forge in options, getting dependencies'); - forge = await handler.getForgeDependencies(options.root, versionFile, options.forge.path || options.forge); - } - if(options.version.custom) { - event.emit('debug', '[MCLC]: Detected custom in options, setting custom version file'); - custom = require(path.join(options.root, 'versions', options.version.custom, `${options.version.custom}.json`)); - } - - const args = []; - - // Jvm - let jvm = [ - '-XX:-UseAdaptiveSizePolicy', - '-XX:-OmitStackTraceInFastThrow', - '-Dfml.ignorePatchDiscrepancies=true', - '-Dfml.ignoreInvalidMinecraftCertificates=true', - `-Djava.library.path=${nativePath}`, - `-Xmx${options.memory.max}M`, - `-Xms${options.memory.min}M` - ]; - jvm.push(await handler.getJVM(versionFile, options)); - if(options.customArgs) jvm = jvm.concat(options.customArgs); - - const classes = await handler.getClasses(options, versionFile); - let classPaths = ['-cp']; - const separator = options.os === "windows" ? ";" : ":"; - event.emit('debug', `[MCLC]: Using ${separator} to separate class paths`); - if(forge) { - event.emit('debug', '[MCLC]: Setting Forge class paths'); - classPaths.push(`${options.forge.path || options.forge}${separator}${forge.paths.join(separator)}${separator}${classes.join(separator)}${separator}${mcPath}`); - classPaths.push(forge.forge.mainClass) - } else { - const file = custom || versionFile; - classPaths.push(`${mcPath}${separator}${classes.join(separator)}`); - classPaths.push(file.mainClass); - } - classPaths = await handler.cleanUp(classPaths); - - // Download version's assets - event.emit('debug', '[MCLC]: Attempting to download assets'); - await handler.getAssets(options.root, versionFile); - - // Launch options. Thank you Lyrus for the reformat <3 - const modification = forge ? forge.forge : null || custom ? custom : null; - const launchOptions = await handler.getLaunchOptions(versionFile, modification, options); - - const launchArguments = args.concat(jvm, classPaths, launchOptions); - event.emit('arguments', launchArguments); - event.emit('debug', launchArguments.join(' ')); - - const minecraft = child.spawn(options.javaPath ? options.javaPath : 'java', launchArguments); - minecraft.stdout.on('data', (data) => event.emit('data', data)); - minecraft.stderr.on('data', (data) => event.emit('error', data)); - minecraft.on('close', (code) => event.emit('close', code)); -}; \ No newline at end of file +module.exports = MCLCore; \ No newline at end of file diff --git a/components/package.js b/components/package.js new file mode 100644 index 0000000..a76d6a8 --- /dev/null +++ b/components/package.js @@ -0,0 +1,14 @@ +const path = require('path'); +const zip = require('adm-zip'); + +module.exports.extractPackage = function(root, clientPackage) { + return new Promise(async resolve => { + if(clientPackage.startsWith('http')) { + await downloadAsync(clientPackage, root, "clientPackage.zip"); + clientPackage = path.join(root, "clientPackage.zip") + } + new zip(clientPackage).extractAllTo(root, true); + this.client.emit('package-extract', true); + resolve(); + }); +}; \ No newline at end of file diff --git a/index.js b/index.js index 16fee25..95b4b10 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,4 @@ module.exports = { - core: require('./components/launcher'), - event: require('./components/events'), - handler: require('./components/handler'), - authenticator: require('./components/authenticator'), + Client: require('./components/launcher'), + Authenticator: require('./components/authenticator'), }; \ No newline at end of file diff --git a/package.json b/package.json index b443d27..5f22525 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-launcher-core", - "version": "2.8.1", + "version": "3.0.0", "description": "Module that downloads Minecraft assets and runs Minecraft. Also Supports Forge", "main": "index.js", "dependencies": { From 3f8d989de17f665a5a8e6289391ac9abac8ab25c Mon Sep 17 00:00:00 2001 From: Pierce Date: Wed, 22 May 2019 18:32:45 -0400 Subject: [PATCH 2/5] , --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e8a60a..1e117ea 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ let opts = { version: { number: "1.14", type: "release" - } + }, memory: { max: "6000", min: "4000" From 41b28bbdd41f909e2b3e249242676943e0a75cc7 Mon Sep 17 00:00:00 2001 From: Pierce Date: Wed, 22 May 2019 20:19:23 -0400 Subject: [PATCH 3/5] Create .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d2f3209 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: node_js \ No newline at end of file From 3d5c3db29ff0322a2eb3272a61d34ad2d07b0b79 Mon Sep 17 00:00:00 2001 From: Pierce Date: Wed, 22 May 2019 20:24:53 -0400 Subject: [PATCH 4/5] messing around on dev --- .travis.yml | 3 ++- README.md | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d2f3209..92436f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,2 @@ -language: node_js \ No newline at end of file +language: node_js +script: echo "npm test temporarily disabled" \ No newline at end of file diff --git a/README.md b/README.md index 1e117ea..15b9b9a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ ![logo](https://pierce.is-serious.business/44U1xXh.png) +##### This project is near complete. +[![Build Status](https://travis-ci.com/Pierce01/MinecraftLauncher-core.svg?branch=master)](https://travis-ci.com/Pierce01/MinecraftLauncher-core) MCLC is a NodeJS solution for launching modded and vanilla Minecraft without having to download and format everything yourself. Basically a core for your Electron or script based launchers. From ae5c2149a2f329ab567fffcaf9a58ddac314251a Mon Sep 17 00:00:00 2001 From: Pierce Date: Wed, 22 May 2019 20:28:33 -0400 Subject: [PATCH 5/5] Remove console.log from refreshtoken --- components/authenticator.js | 1 - 1 file changed, 1 deletion(-) diff --git a/components/authenticator.js b/components/authenticator.js index ab1ec31..d399c9b 100644 --- a/components/authenticator.js +++ b/components/authenticator.js @@ -81,7 +81,6 @@ module.exports.refreshAuth = function (accessToken, clientToken, selectedProfile request.post(requestObject, function(error, response, body) { if (error) resolve(error); - console.log(body); if(!body.selectedProfile) { throw new Error("Validation error: " + response.statusMessage); }