BREAKING CHANGES: v3

Version 3 merge
This commit is contained in:
Pierce 2019-05-22 20:31:33 -04:00 committed by GitHub
commit e5afd60ba3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 507 additions and 514 deletions

2
.travis.yml Normal file
View file

@ -0,0 +1,2 @@
language: node_js
script: echo "npm test temporarily disabled"

159
README.md
View file

@ -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.
@ -13,57 +15,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 +87,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 +113,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?

View file

@ -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) {
@ -82,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);
}
@ -99,3 +97,39 @@ module.exports.refreshAuth = function (accessToken, clientToken, selectedProfile
});
});
};
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);
});
});
};

View file

@ -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;

View file

@ -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));
};
module.exports = MCLCore;

14
components/package.js Normal file
View file

@ -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();
});
};

View file

@ -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'),
};

View file

@ -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": {