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) ![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. 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. Basically a core for your Electron or script based launchers.
@ -13,57 +15,56 @@ https://discord.gg/8uYVbXP
### Standard Example ### Standard Example
```javascript ```javascript
const launcher = require('minecraft-launcher-core'); const { Client, Authenticator } = require('minecraft-launcher-core');
launcher.authenticator.getAuth("email", "password").then(auth => { let opts = {
// Save the auth to a file so it can be used later on! authorization: async () => { return await Authenticator.getAuth(username, password) },
launcher.core({ clientPackage: null,
authorization: auth, root: "./minecraft",
clientPackage: null, os: "windows",
forge: null, version: {
root: "C:/Users/user/AppData/Roaming/.mc", number: "1.14",
os: "windows", type: "release"
version: { },
number: "1.13.2", memory: {
type: "release" max: "6000",
}, min: "4000"
memory: { }
max: "3000", }
min: "1000"
} 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 ### Usage
##### launcher.core Options ##### 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.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 |
| 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 ##### 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 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. 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 ##### 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 | | `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 | | `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 #### Events
| Event Name | Type | Description | | 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` | String | Emitted when a file successfully downloads |
| `download-status` | Object | Emitted when data is received while downloading | | `download-status` | Object | Emitted when data is received while downloading |
| `debug` | String | Emitted when functions occur, made to help debug if errors occur | | `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? #### What should it look like running from console?

View file

@ -2,7 +2,6 @@ const request = require('request');
const uuid = require('uuid/v1'); const uuid = require('uuid/v1');
const api_url = "https://authserver.mojang.com"; const api_url = "https://authserver.mojang.com";
module.exports.getAuth = function (username, password) { module.exports.getAuth = function (username, password) {
return new Promise(resolve => { return new Promise(resolve => {
if(!password) { if(!password) {
@ -82,7 +81,6 @@ module.exports.refreshAuth = function (accessToken, clientToken, selectedProfile
request.post(requestObject, function(error, response, body) { request.post(requestObject, function(error, response, body) {
if (error) resolve(error); if (error) resolve(error);
console.log(body);
if(!body.selectedProfile) { if(!body.selectedProfile) {
throw new Error("Validation error: " + response.statusMessage); 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 request = require('request');
const checksum = require('checksum'); const checksum = require('checksum');
const zip = require('adm-zip'); 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) { downloadAsync(url, directory, name) {
return new Promise(resolve => { return new Promise(resolve => {
shelljs.mkdir('-p', directory); shelljs.mkdir('-p', directory);
const _request = request(url, {timeout: 10000}); const _request = request(url, {timeout: this.options.timeout || 10000});
_request.on('error', function(error) { _request.on('error', function(error) {
resolve({ resolve({
failed: true, failed: true,
asset: { asset: {
url: url, url: url,
directory: directory, directory: directory,
name: name 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) => { getJar() {
let size = 0; return new Promise(async (resolve)=> {
if(fs.existsSync(path.join(directory, name))) size = fs.statSync(path.join(directory, name))["size"]; await this.downloadAsync(this.version.downloads.client.url, directory, `${this.options.version.number}.jar`);
event.emit('download-status', {
"name": name, fs.writeFileSync(path.join(directory, `${this.options.version.number}.json`), JSON.stringify(this.options.version, null, 4));
"current": Math.round(size / 10000),
"total": data.length this.client.emit('debug', '[MCLC]: Downloaded version jar and wrote version json');
})
resolve();
}); });
}
const file = fs.createWriteStream(path.join(directory, name)); getAssets() {
_request.pipe(file); return new Promise(async(resolve) => {
const assetsUrl = 'https://resources.download.minecraft.net';
const failed = [];
file.once('finish', function() { if(!fs.existsSync(path.join(this.options.directory, 'assets', 'indexes', `${this.version.assetIndex.id}.json`))) {
event.emit('download', name); await this.downloadAsync(this.version.assetIndex.url, path.join(this.options.directory, 'assets', 'indexes'), `${this.version.assetIndex.id}.json`);
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));
});
}
} }
});
});
};
module.exports.getJar = function (version, number, directory) { const index = require(path.join(this.options.directory, 'assets', 'indexes',`${this.version.assetIndex.id}.json`));
return new Promise(async (resolve)=> {
await downloadAsync(version.downloads.client.url, directory, `${number}.jar`);
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 => { await Promise.all(Object.keys(index.objects).map(async asset => {
const hash = index.objects[asset].hash; const hash = index.objects[asset].hash;
const subhash = hash.substring(0,2); 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('/'); if(!fs.existsSync(path.join(assetDirectory, hash)) || !await this.checkSum(hash, path.join(assetDirectory, hash))) {
legacyAsset.pop(); const download = await this.downloadAsync(`${assetsUrl}/${subhash}/${hash}`, assetDirectory, hash);
if(!fs.existsSync(path.join(directory, 'assets', 'legacy', legacyAsset.join('/')))) { if(download.failed) failed.push(download.asset);
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))
} }
})); }));
}
event.emit('debug', '[MCLC]: Downloaded assets'); // why do we have this? B/c sometimes Minecraft's resource site times out!
resolve(); 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) { // Copy assets to legacy if it's an older Minecarft version.
return new Promise(async(resolve) => { if(this.version.assets === "legacy" || this.version.assets === "pre-1.6") {
let nativeDirectory; 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))) { let legacyAsset = asset.split('/');
nativeDirectory = path.join(root, 'natives', version.id); legacyAsset.pop();
} else {
nativeDirectory = path.join(root, "natives", version.id);
shelljs.mkdir('-p', nativeDirectory); if(!fs.existsSync(path.join(this.options.directory, 'assets', 'legacy', legacyAsset.join('/')))) {
shelljs.mkdir('-p', path.join(this.options.directory, 'assets', 'legacy', legacyAsset.join('/')));
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);
} }
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 (!fs.existsSync(path.join(this.options.directory, 'assets', 'legacy', asset))) {
// if it says Invalid file name, just means two files were downloaded and both were deleted. fs.copyFileSync(path.join(assetDirectory, hash), path.join(this.options.directory, 'assets', 'legacy', asset))
// All is well.
console.warn(e);
} }
shelljs.rm(path.join(nativeDirectory, name)); }));
} }
}));
event.emit('debug', '[MCLC]: Downloaded and extracted natives');
}
event.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`); this.client.emit('debug', '[MCLC]: Downloaded assets');
resolve(nativeDirectory); resolve();
}); });
};
module.exports.getForgeDependencies = async function(root, version, forgeJarPath) {
if(!fs.existsSync(path.join(root, 'forge'))) {
shelljs.mkdir('-p', path.join(root, 'forge'));
} }
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')); getNatives() {
const mavenUrl = 'http://files.minecraftforge.net/maven/'; return new Promise(async(resolve) => {
const defaultRepo = 'https://libraries.minecraft.net/'; let nativeDirectory;
const paths = [];
await Promise.all(forge.libraries.map(async library => { if(fs.existsSync(path.join(this.options.root, 'natives', this.version.id))) {
const lib = library.name.split(':'); nativeDirectory = path.join(this.options.root, 'natives', this.version.id);
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 { } 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))) { await Promise.all(version.libraries.map(async (lib) => {
paths.push(`${jarPath}${path.sep}${name}`); if (!lib.downloads.classifiers) return;
return; const type = `natives-${this.options.os}`;
} const native = lib.downloads.classifiers[type];
if(!fs.existsSync(jarPath)) shelljs.mkdir('-p', jarPath);
await downloadAsync(downloadLink, jarPath, name); if (native) {
const name = native.path.split('/').pop();
paths.push(`${jarPath}${path.sep}${name}`); await this.downloadAsync(native.url, nativeDirectory, name);
})); if(!await this.checkSum(native.sha1, path.join(nativeDirectory, name))) {
await this.downloadAsync(native.url, nativeDirectory, name);
event.emit('debug', '[MCLC]: Downloaded Forge dependencies'); }
try {new zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true);} catch(e) {
return {paths, forge}; // 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.
module.exports.getClasses = function (options, version) { console.warn(e);
return new Promise(async (resolve) => { }
const libs = []; shelljs.rm(path.join(nativeDirectory, name));
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);
} }
} }));
libs.push(`${jarPath}/${name}`); this.client.emit('debug', '[MCLC]: Downloaded and extracted natives');
}));
}
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);
} }
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'); this.client.emit('debug', '[MCLC]: Downloaded Forge dependencies');
resolve(libs)
});
};
module.exports.cleanUp = async function(array) { return {paths, forge};
const newArray = [];
for(let argument in array) {
if(newArray.includes(array[argument])) continue;
newArray.push(array[argument]);
} }
return newArray; getClasses() {
}; return new Promise(async (resolve) => {
const libs = [];
module.exports.getLaunchOptions = function (version, modification, options) { if(this.options.version.custom) {
return new Promise(resolve => { const customJarJson = require(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`));
let type = modification || version; await Promise.all(customJarJson.libraries.map(async library => {
const lib = library.name.split(':');
let arguments = type.minecraftArguments ? type.minecraftArguments.split(' ') : type.arguments.game; const jarPath = path.join(this.options.root, 'libraries', `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`);
const assetPath = version.assets === "legacy" || version.assets === "pre-1.6" ? path.join(options.root, 'assets', 'legacy') : path.join(options.root, 'assets'); const name = `${lib[1]}-${lib[2]}.jar`;
if(arguments.length < 5) arguments = arguments.concat(version.minecraftArguments ? version.minecraftArguments.split(' ') : version.arguments.game); if(!fs.existsSync(path.join(jarPath, name))) {
if(library.url) {
const fields = { const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${lib[1]}-${lib[2]}.jar`;
'${auth_access_token}': options.authorization.access_token, await this.downloadAsync(url, jarPath, name);
'${auth_session}': options.authorization.access_token, }
'${auth_player_name}': options.authorization.name, }
'${auth_uuid}': options.authorization.uuid, libs.push(`${jarPath}/${name}`);
'${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]];
} }
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"); return newArray;
if(options.proxy) arguments.push( }
'--proxyHost',
options.proxy.host,
'--proxyPort',
options.proxy.port || "8080",
'--proxyUser',
options.proxy.username,
'--proxyPass',
options.proxy.password
);
event.emit('debug', '[MCLC]: Set launch options'); getLaunchOptions(modification) {
resolve(arguments); return new Promise(resolve => {
}); let type = modification || this.version;
};
module.exports.getJVM = function (version, options) { let args = type.minecraftArguments ? type.minecraftArguments.split(' ') : type.arguments.game;
return new Promise(resolve => { 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');
switch(options.os) {
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": { case "windows": {
resolve("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"); return "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"
break;
} }
case "osx": { case "osx": {
resolve("-XstartOnFirstThread"); return "-XstartOnFirstThread"
break;
} }
case "linux": { case "linux": {
resolve("-Xss1M"); return "-Xss1M"
break;
} }
} }
});
};
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(); module.exports = Handler;
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();
});
};

View file

@ -2,89 +2,102 @@ const child = require('child_process');
const event = require('./events'); const event = require('./events');
const path = require('path'); const path = require('path');
const handler = require('./handler'); const handler = require('./handler');
const packager = require('./package');
const fs = require('fs'); const fs = require('fs');
const EventEmitter = require('events').EventEmitter;
class MCLCore extends EventEmitter {
constructor(options) {
super();
module.exports = async function (options) { this.options = options;
options.root = path.resolve(options.root); this.handler = new handler(this);
if(!fs.existsSync(options.root)) {
event.emit('debug', '[MCLC]: Attempting to create root folder');
fs.mkdirSync(options.root);
} }
if(options.clientPackage) { async launch() {
event.emit('debug', `[MCLC]: Extracting client package to ${options.root}`); this.options.root = path.resolve(this.options.root);
await handler.extractPackage(options.root, options.clientPackage); 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); module.exports = MCLCore;
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));
};

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 = { module.exports = {
core: require('./components/launcher'), Client: require('./components/launcher'),
event: require('./components/events'), Authenticator: require('./components/authenticator'),
handler: require('./components/handler'),
authenticator: require('./components/authenticator'),
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "minecraft-launcher-core", "name": "minecraft-launcher-core",
"version": "2.8.1", "version": "3.0.0",
"description": "Module that downloads Minecraft assets and runs Minecraft. Also Supports Forge", "description": "Module that downloads Minecraft assets and runs Minecraft. Also Supports Forge",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {