Matan merge

This commit is contained in:
Matan Rak 2017-05-17 09:26:46 +03:00
commit ab74dd39c5
31 changed files with 5359 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/node_modules/
/.vs/
/.vscode/
/mcfiles/
/logs/

91
README.md Normal file
View File

@ -0,0 +1,91 @@
# Getting Started #
System Requirements:
* [Node.js](https://nodejs.org/en/) v7.9.0+
This repository is dedicated to the development of the new custom launcher for the [WesterosCraft](http://www.westeroscraft.com/) server. This project is developed primarily with [Node.js](https://nodejs.org/en/) and the [Electron](https://electron.atom.io/) framework. For further reference you may view [the repository of the new launcher written in JavaFX/Java](https://gitlab.com/westeroscraft/WesteroscraftNewLauncher) which was discontinued. You may also view the repository of the [current launcher](https://gitlab.com/westeroscraft/westeroscraftlaunchercore), a modified fork of MCUpdater.
For authentication with Mojang, we are currently planning on using [node-mojang](https://github.com/jamen/node-mojang). This will automatically be downloaded if you follow the simple installation instructions below.
### Recommended IDE ###
The recommended IDE for this project is [VS Code](https://code.visualstudio.com/), an open source code editor by Microsoft. This editor is available on nearly every major platform (Windows, macOS, Linux). If you choose to use another editor, such as [Atom](https://atom.io/), please gitignore the IDE specific settings directory, if it hasn't been already.
### Installation ###
To begin working on this project clone the repository and open run the following command on the command line. This will download all of the required dependencies.
```shell
npm install
```
# Launching #
### Command Line ###
There are several different ways to launch this project. One way is simply to run the following command on the command line.
```shell
npm start
```
### Visual Studio Code ###
If you use VS Code, you can run this directly from the IDE. Copy the following code into your launch.json file. This will require you to also install [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome).
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron.cmd"
},
"program": "${workspaceRoot}\\index.js",
"console": "integratedTerminal",
"protocol": "legacy"
},
{
"name": "Debug Renderer Process",
"type": "chrome",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron.cmd"
},
"runtimeArgs": [
"${workspaceRoot}\\index.js",
"--remote-debugging-port=9222"
],
"webRoot": "${workspaceRoot}"
}
]
}
```
This will create two launch configurations from which you can debug the launcher. The first configuration, **Debug Main Process**, will allow you to debug the main Electron process. The second configuration, **Debug Renderer Process**, will allow you to debug the rendering of web pages (ie the UI).
You can find more information [here](http://code.matsu.io/1).
### Notes on DevTools Window ###
Once you run the program, you can open the DevTools window by typing the following keys in sequence on the main window.
```shell
wcdev
```
Please note that if you are debugging the application with VS Code and have launched the program using the **Debug Renderer Process** configuration you cannot open the DevTools window. If you attempt to do so, the program will crash. Remote debugging cannot be done with multiple DevTools clients.
# Issues / Further Support #
If you run into any issue which cannot be resolved via a quick google search, create an issue using the tab above.
Much of the discussion regarding this launcher is done on Discord, feel free to join us there. Click the image below to connect to our server.
[![Discord](http://i.imgur.com/wlSvq9y.png)](https://discord.gg/UACCjTK)

55
app/assets/css/global.css Executable file
View File

@ -0,0 +1,55 @@
@font-face {
font-family: ringbearer;
src: url('../fonts/ringbearer.ttf');
}
/* Logger font, found on https://fonts.google.com/specimen/Inconsolata?selection.family=Inconsolata */
@font-face {
font-family: inconsolata;
src: url('../fonts/Inconsolata-Bold.ttf');
}
html , body{
background: url('../images/BrownWithWignette.jpg') no-repeat center center fixed;
background-size: cover;
}
a, a:hover
{
color: black;
text-decoration:none;
cursor:pointer;
}
pre {border: 0; background-color: transparent;}
input{
width: 100%;
padding-left: 3px;
border: none;
border-bottom: 3px solid #ECECEC;
font-size: 20px;
outline:none;
}
p, img, pre, span, label, h1 ,h2 ,h3 {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}

46
app/assets/css/header.css Executable file
View File

@ -0,0 +1,46 @@
#header_container {
background-color: black;
padding: 5px;
font-size: 0px;
text-align: center;
border-bottom: thick solid #a02d2a;
border-bottom-width: 5px;
position: relative;
}
/* Div container for the seal image. */
#header_seal_container {
position: absolute;
}
/* Div container for the header image. */
#header_img_container {
display: inline-block;
margin: 0 auto;
}
/* Seal and header images. */
#header_seal,
#header_img {
height: 75px;
width: auto;
display: block;
}
/* Div container for the social buttons. */
#header_social_container {
position: absolute;
bottom: 0px;
right: 0px;
margin-bottom: 5px;
}
/* Social buttons. */
.header_social_img {
height: 25px;
width: auto;
display: inline-block;
cursor: pointer;
margin-right: 5px;
}

48
app/assets/css/nav.css Executable file
View File

@ -0,0 +1,48 @@
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
li {
float: left;
display: block;
padding: 12px;
padding-top: 5px;
text-align: center;
}
li:hover {
background-color: #a02d2a;
}
a{
font-size: 20px;
font-family: 'ringbearer';
}
.player_icon{
margin-top: 120px;
margin-left: -15px;
width: 100%;
}
.input_text{
font-size: 20px;
font-family: 'bitter', italic;
color: black;
text-align: center;
padding-top: 40px;
}
.input{
width: 100%;
margin: 0 auto;
border: none;
border-bottom: 3px solid #bdc3c7;
outline: none;
font-size: 20px;
}

121
app/assets/css/styles.css Normal file
View File

@ -0,0 +1,121 @@
/*******************************************************************************
* *
* Fonts *
* *
******************************************************************************/
/* Primary font for the application, found on http://www.dafont.com/ringbearer.font */
@font-face {
font-family: ringbearer;
src: url('../fonts/ringbearer.ttf');
}
/* Logger font, found on https://fonts.google.com/specimen/Inconsolata?selection.family=Inconsolata */
@font-face {
font-family: inconsolata;
src: url('../fonts/Inconsolata-Bold.ttf');
}
/*******************************************************************************
* *
* Body *
* *
******************************************************************************/
/* Reset body, html, and div presets. */
body, html, div {
margin: 0px;
padding: 0px;
}
html {
background: url('../images/BrownWithWignette.jpg') no-repeat center center fixed;
background-size: cover;
}
/*******************************************************************************
* *
* Header *
* *
******************************************************************************/
/* Main div header container. */
#header_container {
background-color: black;
padding: 5px;
font-size: 0px;
text-align: center;
border-bottom: thick solid #a02d2a;
border-bottom-width: 5px;
position: relative;
}
/* Div container for the seal image. */
#header_seal_container {
position: absolute;
}
/* Div container for the header image. */
#header_img_container {
display: inline-block;
margin: 0 auto;
}
/* Seal and header images. */
#header_seal,
#header_img {
height: 75px;
width: auto;
display: block;
}
/* Div container for the social buttons. */
#header_social_container {
position: absolute;
bottom: 0px;
right: 0px;
margin-bottom: 5px;
}
/* Social buttons. */
.header_social_img {
height: 25px;
width: auto;
display: inline-block;
cursor: pointer;
margin-right: 5px;
}
/*******************************************************************************
* *
* Left Body Container *
* *
******************************************************************************/
#body_left_container {
width: 25%;
display: inline-block;
}
/*******************************************************************************
* *
* Right Body Container *
* *
******************************************************************************/
#body_right_container {
width: 75%;
display: inline-block;
}
.mtoggle_button {
text-align:centre;
margin:5px 2px;
padding:0.4em 3em;
color:#000;
background-color:#FFF;
border-radius:10px;
display:inline-block;
border:solid 1px #CCC;
cursor:pointer;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View File

@ -0,0 +1,239 @@
const fs = require('fs')
const request = require('request')
const path = require('path')
const mkpath = require('mkdirp');
const async = require('async')
const crypto = require('crypto')
const Library = require('./library.js')
const {BrowserWindow} = require('electron')
/**
* PHASING THIS OUT, WILL BE REMOVED WHEN ASSET GUARD MODULE IS COMPLETE!
*/
function Asset(from, to, size, hash){
this.from = from
this.to = to
this.size = size
this.hash = hash
}
function AssetIndex(id, sha1, size, url, totalSize){
this.id = id
this.sha1 = sha1
this.size = size
this.url = url
this.totalSize = totalSize
}
/**
* This function will download the version index data and read it into a Javascript
* Object. This object will then be returned.
*/
parseVersionData = function(version, basePath){
const name = version + '.json'
const baseURL = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
const versionPath = path.join(basePath, 'versions', version)
return new Promise(function(fulfill, reject){
request.head(baseURL, function(err, res, body){
console.log('Preparing download of ' + version + ' assets.')
mkpath.sync(versionPath)
const stream = request(baseURL).pipe(fs.createWriteStream(path.join(versionPath, name)))
stream.on('finish', function(){
fulfill(JSON.parse(fs.readFileSync(path.join(versionPath, name))))
})
})
})
}
/**
* Download the client for version. This file is 'client.jar' although
* it must be renamed to '{version}'.jar.
*/
downloadClient = function(versionData, basePath){
const dls = versionData['downloads']
const clientData = dls['client']
const url = clientData['url']
const size = clientData['size']
const version = versionData['id']
const sha1 = clientData['sha1']
const targetPath = path.join(basePath, 'versions', version)
const targetFile = version + '.jar'
if(!validateLocalIntegrity(path.join(targetPath, targetFile), 'sha1', sha1)){
request.head(url, function(err, res, body){
console.log('Downloading ' + version + ' client..')
mkpath.sync(targetPath)
const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, targetFile)))
stream.on('finish', function(){
console.log('Finished downloading ' + version + ' client.')
})
})
}
}
downloadLogConfig = function(versionData, basePath){
const logging = versionData['logging']
const client = logging['client']
const file = client['file']
const version = versionData['id']
const sha1 = file['sha1']
const targetPath = path.join(basePath, 'assets', 'log_configs')
const name = file['id']
const url = file['url']
if(!validateLocalIntegrity(path.join(targetPath, name), 'sha1', sha1)){
request.head(url, function(err, res, body){
console.log('Downloading ' + version + ' log config..')
mkpath.sync(targetPath)
const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, name)))
stream.on('finish', function(){
console.log('Finished downloading ' + version + ' log config..')
})
})
}
}
downloadLibraries = function(versionData, basePath){
const libArr = versionData['libraries']
const libPath = path.join(basePath, 'libraries')
let win = BrowserWindow.getFocusedWindow()
const libDlQueue = []
let dlSize = 0
//Check validity of each library. If the hashs don't match, download the library.
libArr.forEach(function(lib, index){
if(Library.validateRules(lib.rules)){
let artifact = null
if(lib.natives == null){
artifact = lib.downloads.artifact
} else {
artifact = lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
}
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
if(!validateLocalIntegrity(libItm.to, 'sha1', libItm.sha1)){
dlSize += libItm.size
libDlQueue.push(libItm)
}
}
})
let acc = 0;
//Download all libraries that failed validation.
async.eachLimit(libDlQueue, 1, function(lib, cb){
mkpath.sync(path.join(lib.to, '..'))
let req = request(lib.from)
let writeStream = fs.createWriteStream(lib.to)
req.pipe(writeStream)
req.on('data', function(chunk){
acc += chunk.length
//console.log('Progress', acc/dlSize)
win.setProgressBar(acc/dlSize)
})
writeStream.on('close', cb)
}, function(err){
if(err){
console.log('A library failed to process');
} else {
console.log('All libraries have been processed successfully');
}
win.setProgressBar(-1)
})
}
/**
* Given an index url, this function will asynchonously download the
* assets associated with that version.
*/
downloadAssets = function(versionData, basePath){
//Asset index constants.
const assetIndex = versionData.assetIndex
const indexURL = assetIndex.url
const gameVersion = versionData.id
const assetVersion = assetIndex.id
const name = assetVersion + '.json'
//Asset constants
const resourceURL = 'http://resources.download.minecraft.net/'
const localPath = path.join(basePath, 'assets')
const indexPath = path.join(localPath, 'indexes')
const objectPath = path.join(localPath, 'objects')
let win = BrowserWindow.getFocusedWindow()
const assetIndexLoc = path.join(indexPath, name)
/*if(!fs.existsSync(assetIndexLoc)){
}*/
console.log('Downloading ' + gameVersion + ' asset index.')
mkpath.sync(indexPath)
const stream = request(indexURL).pipe(fs.createWriteStream(assetIndexLoc))
stream.on('finish', function() {
const data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
const assetDlQueue = []
let dlSize = 0;
Object.keys(data.objects).forEach(function(key, index){
const ob = data.objects[key]
const hash = ob.hash
const assetName = path.join(hash.substring(0, 2), hash)
const urlName = hash.substring(0, 2) + "/" + hash
const ast = new Asset(resourceURL + urlName, path.join(objectPath, assetName), ob.size, String(ob.hash))
if(!validateLocalIntegrity(ast.to, 'sha1', ast.hash)){
dlSize += ast.size
assetDlQueue.push(ast)
}
})
let acc = 0;
async.eachLimit(assetDlQueue, 5, function(asset, cb){
mkpath.sync(path.join(asset.to, ".."))
let req = request(asset.from)
let writeStream = fs.createWriteStream(asset.to)
req.pipe(writeStream)
req.on('data', function(chunk){
acc += chunk.length
console.log('Progress', acc/dlSize)
win.setProgressBar(acc/dlSize)
})
writeStream.on('close', cb)
}, function(err){
if(err){
console.log('An asset failed to process');
} else {
console.log('All assets have been processed successfully');
}
win.setProgressBar(-1)
})
})
}
validateLocalIntegrity = function(filePath, algo, hash){
if(fs.existsSync(filePath)){
let fileName = path.basename(filePath)
console.log('Validating integrity of local file', fileName)
let shasum = crypto.createHash(algo)
let content = fs.readFileSync(filePath)
shasum.update(content)
let localhash = shasum.digest('hex')
if(localhash === hash){
console.log('Hash value of ' + fileName + ' matches the index hash, woo!')
return true
} else {
console.log('Hash value of ' + fileName + ' (' + localhash + ')' + ' does not match the index hash. Redownloading..')
return false
}
}
return false;
}
module.exports = {
parseVersionData,
downloadClient,
downloadLogConfig,
downloadLibraries,
downloadAssets
}

587
app/assets/js/assetguard.js Normal file
View File

@ -0,0 +1,587 @@
/**
* AssetGuard
*
* This module aims to provide a comprehensive and stable method for processing
* and downloading game assets for the WesterosCraft server. A central object
* stores download meta for several identifiers (categories). This meta data
* is initially empty until one of the module's processing functions are called.
* That function will process the corresponding asset index and validate any exisitng
* local files. If a file is missing or fails validation, it will be placed into an
* array which acts as a queue. This queue is wrapped in a download tracker object
* so that essential information can be cached. The download tracker object is then
* assigned as the value of the identifier in the central object. These download
* trackers will remain idle until an async process is started to process them.
*
* Once the async process is started, any enqueued assets will be downloaded. The central
* object will emit events throughout the download whose name correspond to the identifier
* being processed. For example, if the 'assets' identifier was being processed, whenever
* the download stream recieves data, the event 'assetsdlprogress' will be emitted off of
* the central object instance. This can be listened to by external modules allowing for
* categorical tracking of the downloading process.
*
* @module assetguard
*/
// Requirements
const fs = require('fs')
const request = require('request')
const path = require('path')
const mkpath = require('mkdirp');
const async = require('async')
const crypto = require('crypto')
const AdmZip = require('adm-zip')
const EventEmitter = require('events');
const {remote} = require('electron')
// Classes
/** Class representing a base asset. */
class Asset{
/**
* Create an asset.
*
* @param {any} id - id of the asset.
* @param {String} hash - hash value of the asset.
* @param {Number} size - size in bytes of the asset.
* @param {String} from - url where the asset can be found.
* @param {String} to - absolute local file path of the asset.
*/
constructor(id, hash, size, from, to){
this.id = id
this.hash = hash
this.size = size
this.from = from
this.to = to
}
}
/** Class representing a mojang library. */
class Library extends Asset{
/**
* Converts the process.platform OS names to match mojang's OS names.
*/
static mojangFriendlyOS(){
const opSys = process.platform
if (opSys === 'darwin') {
return 'osx';
} else if (opSys === 'win32'){
return 'windows';
} else if (opSys === 'linux'){
return 'linux';
} else {
return 'unknown_os';
}
}
/**
* Checks whether or not a library is valid for download on a particular OS, following
* the rule format specified in the mojang version data index. If the allow property has
* an OS specified, then the library can ONLY be downloaded on that OS. If the disallow
* property has instead specified an OS, the library can be downloaded on any OS EXCLUDING
* the one specified.
*
* @param {Object} rules - the Library's download rules.
* @returns {Boolean} - true if the Library follows the specified rules, otherwise false.
*/
static validateRules(rules){
if(rules == null) return true
let result = true
rules.forEach(function(rule){
const action = rule['action']
const osProp = rule['os']
if(action != null){
if(osProp != null){
const osName = osProp['name']
const osMoj = Library.mojangFriendlyOS()
if(action === 'allow'){
result = osName === osMoj
return
} else if(action === 'disallow'){
result = osName !== osMoj
return
}
}
}
})
return result
}
}
/**
* Class representing a download tracker. This is used to store meta data
* about a download queue, including the queue itself.
*/
class DLTracker {
/**
* Create a DLTracker
*
* @param {Array.<Asset>} dlqueue - an array containing assets queued for download.
* @param {Number} dlsize - the combined size of each asset in the download queue array.
*/
constructor(dlqueue, dlsize){
this.dlqueue = dlqueue
this.dlsize = dlsize
}
}
/**
* Central object class used for control flow. This object stores data about
* categories of downloads. Each category is assigned an identifier with a
* DLTracker object as its value. Combined information is also stored, such as
* the total size of all the queued files in each category. This event is used
* to emit events so that external modules can listen into processing done in
* this module.
*/
class AssetGuard extends EventEmitter{
/**
* AssetGuard class should only ever have one instance which is defined in
* this module. On creation the object's properties are never-null default
* values. Each identifier is resolved to an empty DLTracker.
*/
constructor(){
super()
this.totaldlsize = 0;
this.progress = 0;
this.assets = new DLTracker([], 0)
this.libraries = new DLTracker([], 0)
this.files = new DLTracker([], 0)
}
}
/**
* Global static final instance of AssetGuard
*/
const instance = new AssetGuard()
// Utility Functions
/**
* Calculates the hash for a file using the specified algorithm.
*
* @param {Buffer} buf - the buffer containing file data.
* @param {String} algo - the hash algorithm.
* @returns {String} - the calculated hash in hex.
*/
function _calculateHash(buf, algo){
return crypto.createHash(algo).update(buf).digest('hex')
}
/**
* Used to parse a checksums file. This is specifically designed for
* the checksums.sha1 files found inside the forge scala dependencies.
*
* @param {String} content - the string content of the checksums file.
* @returns {Object} - an object with keys being the file names, and values being the hashes.
*/
function _parseChecksumsFile(content){
let finalContent = {}
let lines = content.split('\n')
for(let i=0; i<lines.length; i++){
let bits = lines[i].split(' ')
if(bits[1] == null) {
continue
}
finalContent[bits[1]] = bits[0]
}
return finalContent
}
/**
* Validate that a file exists and matches a given hash value.
*
* @param {String} filePath - the path of the file to validate.
* @param {String} algo - the hash algorithm to check against.
* @param {String} hash - the existing hash to check against.
* @returns {Boolean} - true if the file exists and calculated hash matches the given hash, otherwise false.
*/
function _validateLocal(filePath, algo, hash){
if(fs.existsSync(filePath)){
let fileName = path.basename(filePath)
let buf = fs.readFileSync(filePath)
let calcdhash = _calculateHash(buf, algo)
return calcdhash === hash
}
return false;
}
/**
* Validates a file in the style used by forge's version index.
*
* @param {String} filePath - the path of the file to validate.
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
* @returns {Boolean} - true if the file exists and the hashes match, otherwise false.
*/
function _validateForgeChecksum(filePath, checksums){
if(fs.existsSync(filePath)){
if(checksums == null || checksums.length === 0){
return true
}
let buf = fs.readFileSync(filePath)
let calcdhash = _calculateHash(buf, 'sha1')
let valid = checksums.includes(calcdhash)
if(!valid && filePath.endsWith('.jar')){
valid = _validateForgeJar(filePath, checksums)
}
return valid
}
return false
}
/**
* Validates a forge jar file dependency who declares a checksums.sha1 file.
* This can be an expensive task as it usually requires that we calculate thousands
* of hashes.
*
* @param {Buffer} buf - the buffer of the jar file.
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
* @returns {Boolean} - true if all hashes declared in the checksums.sha1 file match the actual hashes.
*/
function _validateForgeJar(buf, checksums){
const hashes = {}
let expected = {}
const zip = new AdmZip(buf)
const zipEntries = zip.getEntries()
//First pass
for(let i=0; i<zipEntries.length; i++){
let entry = zipEntries[i]
if(entry.entryName === 'checksums.sha1'){
expected = _parseChecksumsFile(zip.readAsText(entry))
}
hashes[entry.entryName] = _calculateHash(entry.getData(), 'sha1')
}
if(!checksums.includes(hashes['checksums.sha1'])){
return false
}
//Check against expected
const expectedEntries = Object.keys(expected)
for(let i=0; i<expectedEntries.length; i++){
if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
return false
}
}
return true
}
/**
* Initiate an async download process for an AssetGuard DLTracker.
*
* @param {String} identifier - the identifier of the AssetGuard DLTracker.
* @param {Number} limit - optional. The number of async processes to run in parallel.
* @returns {Boolean} - true if the process began, otherwise false.
*/
function startAsyncProcess(identifier, limit = 5){
let win = remote.getCurrentWindow()
let acc = 0
const concurrentDlQueue = instance[identifier].dlqueue.slice(0)
if(concurrentDlQueue.length === 0){
return false
} else {
async.eachLimit(concurrentDlQueue, limit, function(asset, cb){
mkpath.sync(path.join(asset.to, ".."))
let req = request(asset.from)
let writeStream = fs.createWriteStream(asset.to)
req.pipe(writeStream)
req.on('data', function(chunk){
instance.progress += chunk.length
acc += chunk.length
instance.emit(identifier + 'dlprogress', acc)
//console.log(identifier + ' Progress', acc/instance[identifier].dlsize)
win.setProgressBar(instance.progress/instance.totaldlsize)
})
writeStream.on('close', cb)
}, function(err){
if(err){
instance.emit(identifier + 'dlerror')
console.log('An item in ' + identifier + ' failed to process');
} else {
instance.emit(identifier + 'dlcomplete')
console.log('All ' + identifier + ' have been processed successfully')
}
instance.totaldlsize -= instance[identifier].dlsize
instance[identifier] = new DLTracker([], 0)
if(instance.totaldlsize === 0) {
win.setProgressBar(-1)
instance.emit('dlcomplete')
}
})
return true
}
}
// Validation Functions
/**
* Loads the version data for a given minecraft version.
*
* @param {String} version - the game version for which to load the index data.
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
* @param {Boolean} force - optional. If true, the version index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Object>} - Promise which resolves to the version data object.
*/
function loadVersionData(version, basePath, force = false){
return new Promise(function(fulfill, reject){
const name = version + '.json'
const url = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
const versionPath = path.join(basePath, 'versions', version)
const versionFile = path.join(versionPath, name)
if(!fs.existsSync(versionFile) || force){
//This download will never be tracked as it's essential and trivial.
request.head(url, function(err, res, body){
console.log('Preparing download of ' + version + ' assets.')
mkpath.sync(versionPath)
const stream = request(url).pipe(fs.createWriteStream(versionFile))
stream.on('finish', function(){
fulfill(JSON.parse(fs.readFileSync(versionFile)))
})
})
} else {
fulfill(JSON.parse(fs.readFileSync(versionFile)))
}
})
}
/**
* Public asset validation function. This function will handle the validation of assets.
* It will parse the asset index specified in the version data, analyzing each
* asset entry. In this analysis it will check to see if the local file exists and is valid.
* If not, it will be added to the download queue for the 'assets' identifier.
*
* @param {Object} versionData - the version data for the assets.
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
function validateAssets(versionData, basePath, force = false){
return new Promise(function(fulfill, reject){
_assetChainIndexData(versionData, basePath, force).then(() => {
fulfill()
})
})
}
//Chain the asset tasks to provide full async. The below functions are private.
/**
* Private function used to chain the asset validation process. This function retrieves
* the index data.
* @param {Object} versionData
* @param {String} basePath
* @param {Boolean} force
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
function _assetChainIndexData(versionData, basePath, force = false){
return new Promise(function(fulfill, reject){
//Asset index constants.
const assetIndex = versionData.assetIndex
const name = assetIndex.id + '.json'
const indexPath = path.join(basePath, 'assets', 'indexes')
const assetIndexLoc = path.join(indexPath, name)
let data = null
if(!fs.existsSync(assetIndexLoc) || force){
console.log('Downloading ' + versionData.id + ' asset index.')
mkpath.sync(indexPath)
const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc))
stream.on('finish', function() {
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
_assetChainValidateAssets(versionData, basePath, data).then(() => {
fulfill()
})
})
} else {
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
_assetChainValidateAssets(versionData, basePath, data).then(() => {
fulfill()
})
}
})
}
/**
* Private function used to chain the asset validation process. This function processes
* the assets and enqueues missing or invalid files.
* @param {Object} versionData
* @param {String} basePath
* @param {Boolean} force
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
function _assetChainValidateAssets(versionData, basePath, indexData){
return new Promise(function(fulfill, reject){
//Asset constants
const resourceURL = 'http://resources.download.minecraft.net/'
const localPath = path.join(basePath, 'assets')
const indexPath = path.join(localPath, 'indexes')
const objectPath = path.join(localPath, 'objects')
const assetDlQueue = []
let dlSize = 0;
//const objKeys = Object.keys(data.objects)
async.forEachOfLimit(indexData.objects, 10, function(value, key, cb){
const hash = value.hash
const assetName = path.join(hash.substring(0, 2), hash)
const urlName = hash.substring(0, 2) + "/" + hash
const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName))
if(!_validateLocal(ast.to, 'sha1', ast.hash)){
dlSize += (ast.size*1)
assetDlQueue.push(ast)
}
cb()
}, function(err){
instance.assets = new DLTracker(assetDlQueue, dlSize)
instance.totaldlsize += dlSize
fulfill()
})
})
}
/**
* Public library validation function. This function will handle the validation of libraries.
* It will parse the version data, analyzing each library entry. In this analysis, it will
* check to see if the local file exists and is valid. If not, it will be added to the download
* queue for the 'libraries' identifier.
*
* @param {Object} versionData - the version data for the assets.
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
function validateLibraries(versionData, basePath){
return new Promise(function(fulfill, reject){
const libArr = versionData.libraries
const libPath = path.join(basePath, 'libraries')
const libDlQueue = []
let dlSize = 0
//Check validity of each library. If the hashs don't match, download the library.
async.eachLimit(libArr, 5, function(lib, cb){
if(Library.validateRules(lib.rules)){
let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
if(!_validateLocal(libItm.to, 'sha1', libItm.hash)){
dlSize += (libItm.size*1)
libDlQueue.push(libItm)
}
}
cb()
}, function(err){
instance.libraries = new DLTracker(libDlQueue, dlSize)
instance.totaldlsize += dlSize
fulfill()
})
})
}
/**
* Public miscellaneous mojang file validation function. These files will be enqueued under
* the 'files' identifier.
*
* @param {Object} versionData - the version data for the assets.
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
function validateMiscellaneous(versionData, basePath){
return new Promise(async function(fulfill, reject){
await validateClient(versionData, basePath)
await validateLogConfig(versionData, basePath)
fulfill()
})
}
/**
* Validate client file - artifact renamed from client.jar to '{version}'.jar.
*
* @param {Object} versionData - the version data for the assets.
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
function validateClient(versionData, basePath, force = false){
return new Promise(function(fulfill, reject){
const clientData = versionData.downloads.client
const version = versionData.id
const targetPath = path.join(basePath, 'versions', version)
const targetFile = version + '.jar'
let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile))
if(!_validateLocal(client.to, 'sha1', client.hash) || force){
instance.files.dlqueue.push(client)
instance.files.dlsize += client.size*1
fulfill()
} else {
fulfill()
}
})
}
/**
* Validate log config.
*
* @param {Object} versionData - the version data for the assets.
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
function validateLogConfig(versionData, basePath){
return new Promise(function(fulfill, reject){
const client = versionData.logging.client
const file = client.file
const targetPath = path.join(basePath, 'assets', 'log_configs')
let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id))
if(!_validateLocal(logConfig.to, 'sha1', logConfig.hash)){
instance.files.dlqueue.push(logConfig)
instance.files.dlsize += client.size*1
fulfill()
} else {
fulfill()
}
})
}
/**
* This function will initiate the download processed for the specified identifiers. If no argument is
* given, all identifiers will be initiated. Note that in order for files to be processed you need to run
* the processing function corresponding to that identifier. If you run this function without processing
* the files, it is likely nothing will be enqueued in the global object and processing will complete
* immediately. Once all downloads are complete, this function will fire the 'dlcomplete' event on the
* global object instance.
*
* @param {Array.<{id: string, limit: number}>} identifiers - optional. The identifiers to process and corresponding parallel async task limit.
*/
function processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}]){
this.progress = 0;
let win = remote.getCurrentWindow()
let shouldFire = true
for(let i=0; i<identifiers.length; i++){
let iden = identifiers[i]
let r = startAsyncProcess(iden.id, iden.limit)
if(r) shouldFire = false
}
if(shouldFire){
instance.emit('dlcomplete')
}
}
module.exports = {
loadVersionData,
validateAssets,
validateLibraries,
validateMiscellaneous,
processDlQueues,
instance,
Asset,
Library
}

View File

@ -0,0 +1,75 @@
{
"version": "1.0",
"servers": [
{
"id": "WesterosCraft-1.11.2",
"name": "WesterosCraft Production Client",
"news-feed": "http://www.westeroscraft.com/api/rss.php?preset_id=12700544",
"icon-url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/server-prod.png",
"revision": "0.0.1",
"server-ip": "mc.westeroscraft.com",
"mc-version": "1.11.2",
"main-class": "TBD",
"libraries": [
{
"name": "net.minecraftforge.forge.forge-universal:1.11.2-13.20.0.2228",
"required": true,
"artifact": {
"size": 4123353,
"MD5": "5b9105f1a8552beac0c8228203d994ae",
"path": "net/minecraftforge/forge/1.11.2-13.20.0.2228/forge-1.11.2-13.20.0.2228-universal.jar",
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.0.2228/forge-1.11.2-13.20.0.2228-universal.jar"
},
"launch-args": ["--tweakClass cpw.mods.fml.common.launcher.FMLTweaker"]
},
{
"name": "net.optifine.optifine:1.11.2_HD_U_B8",
"required": true,
"artifact": {
"size": 2050307,
"MD5": "c18c80f8bfa2a440cc5af4ab8816bc4b",
"path": "optifine/OptiFine/1.11.2_HD_U_B8/OptiFine-1.11.2_HD_U_B8.jar",
"url": "http://optifine.net/download.php?f=OptiFine_1.11.2_HD_U_B8.jar"
},
"launch-args": []
}
],
"forgemods": [
{
"name": "MODNAME",
"required": true,
"artifact": {
"size": 1234,
"MD5": "e71e88c744588fdad48d3b3beb4935fc",
"path": "/path/to/lib/modname.jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.7.10/launchwrapper-1.11.jar"
}
}
],
"litemods": [
{
"name": "MODNAME",
"required": true,
"artifact": {
"size": 1234,
"MD5": "e71e88c744588fdad48d3b3beb4935fc",
"path": "/path/to/lib/modname.jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.7.10/launchwrapper-1.11.jar"
}
}
],
"files": [
{
"name": "FILENAME",
"required": true,
"artifact": {
"size": 1234,
"MD5": "e71e88c744588fdad48d3b3beb4935fc",
"path": "/path/to/lib/filename.jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.7.10/launchwrapper-1.11.jar"
}
}
]
}
]
}

View File

@ -0,0 +1,136 @@
const mojang = require('mojang')
const uuidV4 = require('uuid/v4')
const path = require('path')
const child_process = require('child_process')
const ag = require('./assetguard.js')
const fs = require('fs')
const mkpath = require('mkdirp');
/* TODO - convert native extraction to use adm-zip. Currently not functional due to removal of unzip module (it was bad) */
launchMinecraft = function(versionData, basePath){
const authPromise = mojang.auth('EMAIL', 'PASS', uuidV4(), {
name: 'Minecraft',
version: 1
})
authPromise.then(function(data){
const args = finalizeArguments(versionData, data, basePath)
//TODO make this dynamic
const child = child_process.spawn('C:\\Program Files\\Java\\jre1.8.0_131\\bin\\javaw.exe', args)
})
}
finalizeArguments = function(versionData, authData, basePath){
const mcArgs = versionData['minecraftArguments']
const gameProfile = authData['selectedProfile']
const regex = new RegExp('\\${*(.*)}')
const argArr = mcArgs.split(' ')
argArr.unshift('net.minecraft.client.main.Main')
argArr.unshift(classpathArg(versionData, basePath))
argArr.unshift('-cp')
argArr.unshift('-Djava.library.path=' + path.join(basePath, 'natives'))
argArr.unshift('-Xmn128M')
argArr.unshift('-XX:-UseAdaptiveSizePolicy')
argArr.unshift('-XX:+CMSIncrementalMode')
argArr.unshift('-XX:+UseConcMarkSweepGC')
argArr.unshift('-Xmx1G')
for(let i=0; i<argArr.length; i++){
if(regex.test(argArr[i])){
const identifier = argArr[i].match(regex)[1]
let newVal = argArr[i]
switch(identifier){
case 'auth_player_name':
newVal = gameProfile['name']
break
case 'version_name':
newVal = versionData['id']
break
case 'game_directory':
newVal = basePath
break
case 'assets_root':
newVal = path.join(basePath, 'assets')
break
case 'assets_index_name':
newVal = versionData['assets']
break
case 'auth_uuid':
newVal = gameProfile['id']
break
case 'auth_access_token':
newVal = authData['accessToken']
break
case 'user_type':
newVal = 'MOJANG'
break
case 'version_type':
newVal = versionData['type']
break
}
argArr[i] = newVal
}
}
return argArr
}
classpathArg = function(versionData, basePath){
const libArr = versionData['libraries']
const libPath = path.join(basePath, 'libraries')
const nativePath = path.join(basePath, 'natives')
const version = versionData['id']
const cpArgs = [path.join(basePath, 'versions', version, version + '.jar')]
libArr.forEach(function(lib){
if(ag.Library.validateRules(lib['rules'])){
if(lib['natives'] == null){
const dlInfo = lib['downloads']
const artifact = dlInfo['artifact']
const to = path.join(libPath, artifact['path'])
cpArgs.push(to)
} else {
//Now we need to extract natives.
const natives = lib['natives']
const extractInst = lib['extract']
const exclusionArr = extractInst['exclude']
const opSys = ag.Library.mojangFriendlyOS()
const indexId = natives[opSys]
const dlInfo = lib['downloads']
const classifiers = dlInfo['classifiers']
const artifact = classifiers[indexId]
const to = path.join(libPath, artifact['path'])
fs.createReadStream(to).pipe(unzip.Parse()).on('entry', function(entry){
const fileName = entry.path
const type = entry.type
const size = entry.size
console.log(fileName)
let shouldExclude = false
exclusionArr.forEach(function(exclusion){
if(exclusion.indexOf(fileName) > -1){
shouldExclude = true
}
})
if(shouldExclude){
entry.autodrain()
}
else {
mkpath.sync(path.join(nativePath, fileName, '..'))
entry.pipe(fs.createWriteStream(path.join(nativePath, fileName)))
}
})
cpArgs.push(to)
}
}
})
return cpArgs.join(';')
}
module.exports = {
launchMinecraft
}

60
app/assets/js/script.js Normal file
View File

@ -0,0 +1,60 @@
var $ = require('jQuery');
const remote = require('electron').remote
const shell = require('electron').shell
const path = require('path')
/* Open web links in the user's default browser. */
$(document).on('click', 'a[href^="http"]', function(event) {
event.preventDefault();
//testdownloads()
shell.openExternal(this.href)
});
testdownloads = async function(){
const ag = require(path.join(__dirname, 'assets', 'js', 'assetguard.js'))
const lp = require(path.join(__dirname, 'assets', 'js', 'launchprocess.js'))
const basePath = path.join(__dirname, '..', 'mcfiles')
let versionData = await ag.loadVersionData('1.11.2', basePath)
await ag.validateAssets(versionData, basePath)
console.log('assets done')
await ag.validateLibraries(versionData, basePath)
console.log('libs done')
await ag.validateMiscellaneous(versionData, basePath)
console.log('files done')
ag.instance.on('dlcomplete', function(){
lp.launchMinecraft(versionData, basePath)
})
ag.processDlQueues()
}
/*Opens DevTools window if you type "wcdev" in sequence.
This will crash the program if you are using multiple
DevTools, for example the chrome debugger in VS Code. */
const match = [87, 67, 68, 69, 86]
let at = 0;
document.addEventListener('keydown', function (e) {
switch(e.keyCode){
case match[0]:
if(at === 0) ++at
break
case match[1]:
if(at === 1) ++at
break
case match[2]:
if(at === 2) ++at
break
case match[3]:
if(at === 3) ++at
case match[4]:
if(at === 4) ++at
break
default:
at = 0
}
if(at === 5) {
var window = remote.getCurrentWindow()
window.toggleDevTools()
at = 0
}
})

View File

@ -0,0 +1,44 @@
const app = require('electron')
const remote = require('electron').BrowserWindow
/**
* Doesn't work yet.
*/
exports.setIconBadge = function(text){
if(process.platform === 'darwin'){
app.dock.setBadge('' + text)
} else if (process.platform === 'win32'){
const win = remote.getFocusedWindow()
if(text === ''){
win.setOverlayIcon(null, '')
return;
}
//Create badge
const canvas = document.createElement('canvas')
canvas.height = 140;
canvas.width = 140;
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#a02d2a'
ctx.beginPath()
ctx.ellipse(70, 70, 70, 70, 0, 0, 2 * Math.PI)
ctx.fill()
ctx.textAlign = 'center'
ctx.fillStyle = 'white'
if(text.length > 2 ){
ctx.font = '75px sans-serif'
ctx.fillText('' + text, 70, 98)
} else if (text.length > 1){
ctx.font = '100px sans-serif'
ctx.fillText('' + text, 70, 105)
} else {
ctx.font = '125px sans-serif'
ctx.fillText('' + text, 70, 112)
}
const badgeDataURL = canvas.toDataURL()
const img = NativeImage.createFromDataURL(badgeDataURL)
win.setOverlayIcon(img, '' + text)
}
}

133
app/index.html Executable file
View File

@ -0,0 +1,133 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="assets/css/global.css">
<link rel="stylesheet" type="text/css" href="assets/css/header.css">
<link rel="stylesheet" type="text/css" href="assets/css/nav.css">
</head>
<style>
.inner_div{
width:100%;
background-color: black;
border: 5px solid #a02d2a;
}
</style>
<body>
<div id="header_container">
<div id="header_seal_container">
<img id="header_seal" src="./assets/images/WesterosSealSquare.png"/>
</div>
<div id="header_img_container">
<img id="header_img" src="./assets/images/WCTextCrop.png" />
</div>
<div id="header_social_container">
<a href="http://facebook.com/westeroscraft" class="header_social_link">
<img src="./assets/images/facebook.png" class="header_social_img" />
</a>
<a href="http://reddit.com/r/westeroscraft" class="header_social_link">
<img src="./assets/images/reddit.png" class="header_social_img" />
</a>
<a href="http://twitter.com/westeroscraft" class="header_social_link">
<img src="./assets/images/twitter.png" class="header_social_img" />
</a>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-2 col-sm-2">
<div class="row">
<div class="col-md-3 col-sm-3">
<img class="player_icon" src="https://minotar.net/helm/pufferboss.png">
</div>
<div class="col-md-9 col-sm-9">
<p class="input_text">Email</p>
<input class="input" id="EMAIL">
<p class="input_text">Password</p>
<input type="password" id="PASSWORD" class="input" />
</div>
</div>
</div>
<div class="col-md-10 col-sm-10">
<ul>
<li><a href="">NEWS</a></li>
<li><a href="">MAP</a></li>
<li><a href="">MODS</a></li>
<li><a href="">FAQ</a></li>
<li><a href="">LOG</a></li>
<li><a href="">SETTINGS</a></li>
</ul>
<br>
<br>
<div class="inner_div">
<p class="title"> Post title </p>
<p class="subtite">Posted by: PERSON 23.4.17</p>
<pre style="text-align: left; word-break: break-all; overflow-wrap: break-word;"> Hello WesterosCraft Builders and Honored Guests! As you know,
progress on Kingslanding had stalled, but no longer -- we want to finish this big, beautiful, city in style and
are calling all builders to the capital!
There has been a lot of progress with Kingslanding planning and we are happy to...
</pre>
</div>
</div>
</div>
</div>
</body>
</html>

54
index.js Normal file
View File

@ -0,0 +1,54 @@
const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win
function createWindow() {
win = new BrowserWindow({ width: 925, height: 500, icon: getPlatformIcon('WesterosSealSquare')})
win.loadURL(url.format({
pathname: path.join(__dirname, 'app', 'index.html'),
protocol: 'file:',
slashes: true
}))
win.setMenu(null)
win.on('closed', () => {
win = null
})
}
function getPlatformIcon(filename){
const opSys = process.platform
if (opSys === 'darwin') {
filename = filename + '.icns'
} else if (opSys === 'win32') {
filename = filename + '.ico'
} else {
filename = filename + '.png'
}
return path.join(__dirname, 'app', 'assets', 'images', filename)
}
app.on('ready', createWindow);
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "westeroscraftlauncher",
"version": "0.0.1",
"description": "Custom modded launcher for Westeroscraft",
"main": "index.js",
"scripts": {
"start": "electron index.js"
},
"repository": {
"type": "git",
"url": "git+https://gitlab.com/westeroscraft/electronlauncher.git"
},
"author": "TheKraken7, Matan, Nightmare",
"license": "AGPL-3.0",
"bugs": {
"url": "https://gitlab.com/westeroscraft/electronlauncher/issues"
},
"homepage": "http://www.westeroscraft.com/",
"dependencies": {
"adm-zip": "^0.4.7",
"async": "^2.3.0",
"electron": "^1.6.5",
"extract-zip": "=1.6.0",
"jQuery": "^1.7.4",
"mojang": "^0.4.1",
"promise": "^7.1.1",
"uuid": "^3.0.1"
}
}