Updated Distribution Index spec and impl.

Added distromanager.js to represent distro elements.
Moved all distro refresh code to distromanager.js.
Overhauled assetexec.js.
Overhauled handling of assetexec.js output in landing.js.
Overhauled events emitted by assetguard.js.
Improved doenload processing in assetguard.
Updated discord-rpc to v3.0.0.
Replaced westeroscraft.json with distribution.json.
Use npm in travis for windows + linux.
Remove file extension from imports.
Added liteloader + macromod + shaders to distribution.json.
This commit is contained in:
Daniel Scalzi 2018-07-22 11:40:15 -04:00
parent 5b0b1924cf
commit 7dcce68455
No known key found for this signature in database
GPG Key ID: 5CA2F145B63535F9
19 changed files with 1611 additions and 1207 deletions

View File

@ -40,7 +40,7 @@ script:
-v ~/.cache/electron:/root/.cache/electron \ -v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \ -v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \ electronuserland/builder:wine \
/bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn travislinux" /bin/bash -c "node -v && npm ci && npm run travislinux"
else else
npm run travisdarwin npm run travisdarwin
fi fi

View File

@ -1,38 +1,27 @@
const {AssetGuard} = require('./assetguard.js') const { AssetGuard } = require('./assetguard')
const tracker = new AssetGuard(process.argv[2], process.argv[3], process.argv[4], process.argv[5]) const tracker = new AssetGuard(process.argv[2], process.argv[3])
console.log('AssetExec Started') console.log('AssetExec Started')
// Temporary for debug purposes. // Temporary for debug purposes.
process.on('unhandledRejection', r => console.log(r)) process.on('unhandledRejection', r => console.log(r))
tracker.on('assetVal', (data) => { tracker.on('validate', (data) => {
process.send({task: 0, total: data.total, value: data.acc, content: 'validateAssets'}) process.send({context: 'validate', data})
}) })
tracker.on('progress', (data, acc, total) => {
tracker.on('totaldlprogress', (data) => { process.send({context: 'progress', data, value: acc, total, percent: parseInt((acc/total)*100)})
process.send({task: 0, total: data.total, value: data.acc, percent: parseInt((data.acc/data.total)*100), content: 'dl'})
}) })
tracker.on('complete', (data, ...args) => {
tracker.on('extracting', () => { process.send({context: 'complete', data, args})
process.send({task: 0.7, content: 'dl'})
}) })
tracker.on('error', (data, error) => {
tracker.on('dlcomplete', () => { process.send({context: 'error', data, error})
process.send({task: 1, content: 'dl'})
})
tracker.on('jExtracted', (jPath) => {
process.send({task: 2, content: 'dl', jPath})
})
tracker.on('dlerror', (err) => {
process.send({task: 0.9, content: 'dl', err})
}) })
process.on('message', (msg) => { process.on('message', (msg) => {
if(msg.task === 0){ if(msg.task === 'execute'){
const func = msg.content const func = msg.function
let nS = tracker[func] let nS = tracker[func]
let iS = AssetGuard[func] let iS = AssetGuard[func]
if(typeof nS === 'function' || typeof iS === 'function'){ if(typeof nS === 'function' || typeof iS === 'function'){
@ -40,12 +29,12 @@ process.on('message', (msg) => {
const res = f.apply(f === nS ? tracker : null, msg.argsArr) const res = f.apply(f === nS ? tracker : null, msg.argsArr)
if(res instanceof Promise){ if(res instanceof Promise){
res.then((v) => { res.then((v) => {
process.send({result: v, content: msg.content}) process.send({result: v, context: func})
}).catch((err) => { }).catch((err) => {
process.send({result: err, content: msg.content}) process.send({result: err, context: func})
}) })
} else { } else {
process.send({result: res, content: msg.content}) process.send({result: res, context: func})
} }
} }
} }

View File

@ -12,13 +12,6 @@
* assigned as the value of the identifier in the AssetGuard object. These download * assigned as the value of the identifier in the AssetGuard object. These download
* trackers will remain idle until an async process is started to process them. * 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 AssetGuard
* 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 AssetGuard instance. This can be listened to by external modules allowing for
* categorical tracking of the downloading process.
*
* @module assetguard * @module assetguard
*/ */
// Requirements // Requirements
@ -36,6 +29,9 @@ const request = require('request')
const tar = require('tar-fs') const tar = require('tar-fs')
const zlib = require('zlib') const zlib = require('zlib')
const ConfigManager = require('./configmanager')
const DistroManager = require('./distromanager')
// Constants // Constants
const PLATFORM_MAP = { const PLATFORM_MAP = {
win32: '-windows-x64.tar.gz', win32: '-windows-x64.tar.gz',
@ -161,9 +157,6 @@ class DLTracker {
} }
let distributionData = null
let launchWithLocal = false
/** /**
* Central object class used for control flow. This object stores data about * Central object class used for control flow. This object stores data about
* categories of downloads. Each category is assigned an identifier with a * categories of downloads. Each category is assigned an identifier with a
@ -180,12 +173,10 @@ class AssetGuard extends EventEmitter {
* values. Each identifier is resolved to an empty DLTracker. * values. Each identifier is resolved to an empty DLTracker.
* *
* @param {string} commonPath The common path for shared game files. * @param {string} commonPath The common path for shared game files.
* @param {string} launcherPath The root launcher directory.
* @param {string} javaexec The path to a java executable which will be used * @param {string} javaexec The path to a java executable which will be used
* to finalize installation. * to finalize installation.
* @param {string} instancePath The path to the instances directory.
*/ */
constructor(commonPath, launcherPath, javaexec, instancePath){ constructor(commonPath, javaexec){
super() super()
this.totaldlsize = 0 this.totaldlsize = 0
this.progress = 0 this.progress = 0
@ -196,73 +187,12 @@ class AssetGuard extends EventEmitter {
this.java = new DLTracker([], 0) this.java = new DLTracker([], 0)
this.extractQueue = [] this.extractQueue = []
this.commonPath = commonPath this.commonPath = commonPath
this.launcherPath = launcherPath
this.javaexec = javaexec this.javaexec = javaexec
this.instancePath = instancePath
} }
// Static Utility Functions // Static Utility Functions
// #region // #region
// Static General Resolve Functions
// #region
/**
* Resolve an artifact id into a path. For example, on windows
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
* net\minecraftforge\forge\1.11.2-13.20.0.2282\forge-1.11.2-13.20.0.2282.jar
*
* @param {string} artifactid The artifact id string.
* @param {string} extension The extension of the file at the resolved path.
* @returns {string} The resolved relative path from the artifact id.
*/
static _resolvePath(artifactid, extension){
let ps = artifactid.split(':')
let cs = ps[0].split('.')
cs.push(ps[1])
cs.push(ps[2])
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
return path.join.apply(path, cs)
}
/**
* Resolve an artifact id into a URL. For example,
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
* net/minecraftforge/forge/1.11.2-13.20.0.2282/forge-1.11.2-13.20.0.2282.jar
*
* @param {string} artifactid The artifact id string.
* @param {string} extension The extension of the file at the resolved url.
* @returns {string} The resolved relative URL from the artifact id.
*/
static _resolveURL(artifactid, extension){
let ps = artifactid.split(':')
let cs = ps[0].split('.')
cs.push(ps[1])
cs.push(ps[2])
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
return cs.join('/')
}
/**
* Resolves an artiface id without the version. For example,
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282' becomes
* 'net.minecraftforge:forge'.
*
* @param {string} artifactid The artifact id string.
* @returns {string} The resolved identifier without the version.
*/
static _resolveWithoutVersion(artifactid){
let ps = artifactid.split(':')
return ps[0] + ':' + ps[1]
}
// #endregion
// Static Hash Validation Functions // Static Hash Validation Functions
// #region // #region
@ -386,145 +316,6 @@ class AssetGuard extends EventEmitter {
// #endregion // #endregion
// Static Distribution Index Functions
// #region
/**
* Retrieve a new copy of the distribution index from our servers.
*
* @param {string} launcherPath The root launcher directory.
* @returns {Promise.<Object>} A promise which resolves to the distribution data object.
*/
static refreshDistributionDataRemote(launcherPath){
return new Promise((resolve, reject) => {
const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
const opts = {
url: distroURL,
timeout: 2500
}
const distroDest = path.join(launcherPath, 'westeroscraft.json')
request(opts, (error, resp, body) => {
if(!error){
distributionData = JSON.parse(body)
fs.writeFile(distroDest, body, 'utf-8', (err) => {
if(!err){
resolve(distributionData)
} else {
reject(err)
}
})
} else {
reject(error)
}
})
})
}
/**
* Retrieve a local copy of the distribution index asynchronously.
*
* @param {string} launcherPath The root launcher directory.
* @returns {Promise.<Object>} A promise which resolves to the distribution data object.
*/
static refreshDistributionDataLocal(launcherPath){
return new Promise((resolve, reject) => {
fs.readFile(path.join(launcherPath, 'westeroscraft.json'), 'utf-8', (err, data) => {
if(!err){
distributionData = JSON.parse(data)
resolve(distributionData)
} else {
reject(err)
}
})
})
}
/**
* Retrieve a local copy of the distribution index synchronously.
*
* @param {string} launcherPath The root launcher directory.
* @returns {Object} The distribution data object.
*/
static refreshDistributionDataLocalSync(launcherPath){
distributionData = JSON.parse(fs.readFileSync(path.join(launcherPath, 'westeroscraft.json'), 'utf-8'))
return distributionData
}
/**
* Get a cached copy of the distribution index.
*/
static getDistributionData(){
return distributionData
}
/**
* Resolve the default selected server from the distribution index.
*
* @returns {Object} An object resolving to the default selected server.
*/
static resolveSelectedServer(){
const distro = AssetGuard.getDistributionData()
const servers = distro.servers
for(let i=0; i<servers.length; i++){
if(servers[i].default_selected){
return servers[i]
}
}
// If no server declares default_selected, default to the first one declared.
return (servers.length > 0) ? servers[0] : null
}
/**
* Gets a server from the distro index which maches the provided ID.
* Returns null if the ID could not be found or the distro index has
* not yet been loaded.
*
* @param {string} serverID The id of the server to retrieve.
* @returns {Object} The server object whose id matches the parameter.
*/
static getServerById(serverID){
const distro = AssetGuard.getDistributionData()
const servers = distro.servers
let serv = null
for(let i=0; i<servers.length; i++){
if(servers[i].id === serverID){
serv = servers[i]
}
}
return serv
}
/**
* Set whether or not we should launch with a local copy of the distribution
* index. This is useful for testing experimental changes to the distribution index.
*
* @param {boolean} value True if we should launch with a local copy. Otherwise false.
*/
static launchWithLocal(value, silent = false){
if(!silent){
if(value){
console.log('%c[AssetGuard]', 'color: #a02d2a; font-weight: bold', 'Will now launch using a local copy of the distribution index.')
console.log('%c[AssetGuard]', 'color: #a02d2a; font-weight: bold', 'Unless you are a developer, revert this change immediately.')
} else {
console.log('%c[AssetGuard]', 'color: #a02d2a; font-weight: bold', 'Will now retrieve a fresh copy of the distribution index on launch.')
}
}
launchWithLocal = value
}
/**
* Check if AssetGuard is configured to launch with a local copy
* of the distribution index.
*
* @returns {boolean} True if launching with local, otherwise false.
*/
static isLocalLaunch(){
return launchWithLocal
}
// #endregion
// Miscellaneous Static Functions // Miscellaneous Static Functions
// #region // #region
@ -538,8 +329,6 @@ class AssetGuard extends EventEmitter {
console.log('[PackXZExtract] Starting') console.log('[PackXZExtract] Starting')
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let libPath let libPath
if(isDev){ if(isDev){
libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
@ -1320,11 +1109,11 @@ class AssetGuard extends EventEmitter {
//const objKeys = Object.keys(data.objects) //const objKeys = Object.keys(data.objects)
async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => { async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => {
acc++ acc++
self.emit('assetVal', {acc, total}) self.emit('progress', 'assets', acc, total)
const hash = value.hash const hash = value.hash
const assetName = path.join(hash.substring(0, 2), hash) const assetName = path.join(hash.substring(0, 2), hash)
const urlName = 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)) const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName))
if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){ if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){
dlSize += (ast.size*1) dlSize += (ast.size*1)
assetDlQueue.push(ast) assetDlQueue.push(ast)
@ -1461,30 +1250,22 @@ class AssetGuard extends EventEmitter {
/** /**
* Validate the distribution. * Validate the distribution.
* *
* @param {string} serverpackid The id of the server to validate. * @param {Server} server The Server to validate.
* @returns {Promise.<Object>} A promise which resolves to the server distribution object. * @returns {Promise.<Object>} A promise which resolves to the server distribution object.
*/ */
validateDistribution(serverpackid){ validateDistribution(server){
const self = this const self = this
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
AssetGuard.refreshDistributionDataLocal(self.launcherPath).then((v) => { self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID())
const serv = AssetGuard.getServerById(serverpackid) // Correct our workaround here.
let decompressqueue = self.forge.callback
if(serv == null) { self.extractQueue = decompressqueue
console.error('Invalid server pack id:', serverpackid) self.forge.callback = (asset, self) => {
if(asset.type === DistroManager.Types.ForgeHosted || asset.type === DistroManager.Types.Forge){
AssetGuard._finalizeForgeAsset(asset, self.commonPath).catch(err => console.log(err))
} }
}
self.forge = self._parseDistroModules(serv.modules, serv.mc_version, serv.id) resolve(server)
// Correct our workaround here.
let decompressqueue = self.forge.callback
self.extractQueue = decompressqueue
self.forge.callback = (asset, self) => {
if(asset.type === 'forge-hosted' || asset.type === 'forge'){
AssetGuard._finalizeForgeAsset(asset, self.commonPath)
}
}
resolve(serv)
})
}) })
} }
@ -1492,27 +1273,11 @@ class AssetGuard extends EventEmitter {
let alist = [] let alist = []
let asize = 0; let asize = 0;
let decompressqueue = [] let decompressqueue = []
for(let i=0; i<modules.length; i++){ for(let ob of modules){
let ob = modules[i] let obType = ob.getType
let obType = ob.type let obArtifact = ob.getArtifact()
let obArtifact = ob.artifact let obPath = obArtifact.getPath()
let obPath = obArtifact.path == null ? AssetGuard._resolvePath(ob.id, obArtifact.extension) : obArtifact.path let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, obType)
switch(obType){
case 'forge-hosted':
case 'forge':
case 'liteloader':
case 'library':
obPath = path.join(this.commonPath, 'libraries', obPath)
break
case 'forgemod':
case 'litemod':
obPath = path.join(this.commonPath, 'modstore', obPath)
break
case 'file':
default:
obPath = path.join(this.instancePath, servid, obPath)
}
let artifact = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, obType)
const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath
if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){ if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){
asize += artifact.size*1 asize += artifact.size*1
@ -1520,8 +1285,8 @@ class AssetGuard extends EventEmitter {
if(validationPath !== obPath) decompressqueue.push(obPath) if(validationPath !== obPath) decompressqueue.push(obPath)
} }
//Recursively process the submodules then combine the results. //Recursively process the submodules then combine the results.
if(ob.sub_modules != null){ if(ob.getSubModules() != null){
let dltrack = this._parseDistroModules(ob.sub_modules, version, servid) let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid)
asize += dltrack.dlsize*1 asize += dltrack.dlsize*1
alist = alist.concat(dltrack.dlqueue) alist = alist.concat(dltrack.dlqueue)
decompressqueue = decompressqueue.concat(dltrack.callback) decompressqueue = decompressqueue.concat(dltrack.callback)
@ -1535,30 +1300,19 @@ class AssetGuard extends EventEmitter {
/** /**
* Loads Forge's version.json data into memory for the specified server id. * Loads Forge's version.json data into memory for the specified server id.
* *
* @param {string} serverpack The id of the server to load Forge data for. * @param {string} server The Server to load Forge data for.
* @returns {Promise.<Object>} A promise which resolves to Forge's version.json data. * @returns {Promise.<Object>} A promise which resolves to Forge's version.json data.
*/ */
loadForgeData(serverpack){ loadForgeData(server){
const self = this const self = this
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let distro = AssetGuard.getDistributionData() const modules = server.getModules()
for(let ob of modules){
const servers = distro.servers const type = ob.getType()
let serv = null if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){
for(let i=0; i<servers.length; i++){ let obArtifact = ob.getArtifact()
if(servers[i].id === serverpack){ let obPath = obArtifact.getPath()
serv = servers[i] let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type)
break
}
}
const modules = serv.modules
for(let i=0; i<modules.length; i++){
const ob = modules[i]
if(ob.type === 'forge-hosted' || ob.type === 'forge'){
let obArtifact = ob.artifact
let obPath = obArtifact.path == null ? path.join(self.commonPath, 'libraries', AssetGuard._resolvePath(ob.id, obArtifact.extension)) : obArtifact.path
let asset = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, ob.type)
let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath) let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath)
resolve(forgeData) resolve(forgeData)
return return
@ -1602,7 +1356,7 @@ class AssetGuard extends EventEmitter {
dataDir = path.join(dataDir, 'runtime', 'x64') dataDir = path.join(dataDir, 'runtime', 'x64')
const name = combined.substring(combined.lastIndexOf('/')+1) const name = combined.substring(combined.lastIndexOf('/')+1)
const fDir = path.join(dataDir, name) const fDir = path.join(dataDir, name)
const jre = new Asset(name, null, resp.headers['content-length'], opts, fDir) const jre = new Asset(name, null, parseInt(resp.headers['content-length']), opts, fDir)
this.java = new DLTracker([jre], jre.size, (a, self) => { this.java = new DLTracker([jre], jre.size, (a, self) => {
let h = null let h = null
fs.createReadStream(a.to) fs.createReadStream(a.to)
@ -1626,7 +1380,7 @@ class AssetGuard extends EventEmitter {
h = h.substring(0, h.indexOf('/')) h = h.substring(0, h.indexOf('/'))
} }
const pos = path.join(dataDir, h) const pos = path.join(dataDir, h)
self.emit('jExtracted', AssetGuard.javaExecFromRoot(pos)) self.emit('complete', 'java', AssetGuard.javaExecFromRoot(pos))
}) })
}) })
@ -1696,74 +1450,109 @@ class AssetGuard extends EventEmitter {
* @returns {boolean} True if the process began, otherwise false. * @returns {boolean} True if the process began, otherwise false.
*/ */
startAsyncProcess(identifier, limit = 5){ startAsyncProcess(identifier, limit = 5){
const self = this const self = this
let acc = 0 const dlTracker = this[identifier]
const concurrentDlTracker = this[identifier] const dlQueue = dlTracker.dlqueue
const concurrentDlQueue = concurrentDlTracker.dlqueue.slice(0)
if(concurrentDlQueue.length === 0){ if(dlQueue.length > 0){
return false console.log('DLQueue', dlQueue)
} else {
console.log('DLQueue', concurrentDlQueue) async.eachLimit(dlQueue, limit, (asset, cb) => {
async.eachLimit(concurrentDlQueue, limit, (asset, cb) => {
let count = 0; mkpath.sync(path.join(asset.to, '..'))
mkpath.sync(path.join(asset.to, ".."))
let req = request(asset.from) let req = request(asset.from)
req.pause() req.pause()
req.on('response', (resp) => { req.on('response', (resp) => {
if(resp.statusCode === 200){ if(resp.statusCode === 200){
let doHashCheck = false
const contentLength = parseInt(resp.headers['content-length'])
if(contentLength !== asset.size){
console.log(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`)
doHashCheck = true
// Adjust download
this.totaldlsize -= asset.size
this.totaldlsize += contentLength
}
let writeStream = fs.createWriteStream(asset.to) let writeStream = fs.createWriteStream(asset.to)
writeStream.on('close', () => { writeStream.on('close', () => {
//console.log('DLResults ' + asset.size + ' ' + count + ' ', asset.size === count) if(dlTracker.callback != null){
if(concurrentDlTracker.callback != null){ dlTracker.callback.apply(dlTracker, [asset, self])
concurrentDlTracker.callback.apply(concurrentDlTracker, [asset, self])
} }
if(doHashCheck){
const v = AssetGuard._validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash)
if(v){
console.log(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`)
} else {
console.error(`Hashes do not match, ${asset.id} may be corrupted.`)
}
}
cb() cb()
}) })
req.pipe(writeStream) req.pipe(writeStream)
req.resume() req.resume()
} else { } else {
req.abort() req.abort()
const realFrom = typeof asset.from === 'object' ? asset.from.url : asset.from console.log(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`)
console.log('Failed to download ' + realFrom + '. Response code', resp.statusCode)
self.progress += asset.size*1 self.progress += asset.size*1
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize}) self.emit('progress', 'download', self.progress, self.totaldlsize)
cb() cb()
} }
}) })
req.on('error', (err) => { req.on('error', (err) => {
self.emit('dlerror', err) self.emit('error', 'download', err)
}) })
req.on('data', (chunk) => { req.on('data', (chunk) => {
count += chunk.length
self.progress += chunk.length self.progress += chunk.length
acc += chunk.length self.emit('progress', 'download', self.progress, self.totaldlsize)
self.emit(identifier + 'dlprogress', acc)
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
}) })
}, (err) => { }, (err) => {
if(err){ if(err){
self.emit(identifier + 'dlerror')
console.log('An item in ' + identifier + ' failed to process'); console.log('An item in ' + identifier + ' failed to process');
} else { } else {
self.emit(identifier + 'dlcomplete')
console.log('All ' + identifier + ' have been processed successfully') console.log('All ' + identifier + ' have been processed successfully')
} }
self.totaldlsize -= self[identifier].dlsize
self.progress -= self[identifier].dlsize //self.totaldlsize -= dlTracker.dlsize
//self.progress -= dlTracker.dlsize
self[identifier] = new DLTracker([], 0) self[identifier] = new DLTracker([], 0)
if(self.totaldlsize === 0) {
if(self.progress >= self.totaldlsize) {
if(self.extractQueue.length > 0){ if(self.extractQueue.length > 0){
self.emit('extracting') self.emit('progress', 'extract', 1, 1)
//self.emit('extracting')
AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => { AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => {
self.extractQueue = [] self.extractQueue = []
self.emit('dlcomplete') self.emit('complete', 'download')
}) })
} else { } else {
self.emit('dlcomplete') self.emit('complete', 'download')
} }
} }
}) })
return true return true
} else {
return false
} }
} }
@ -1772,32 +1561,69 @@ class AssetGuard extends EventEmitter {
* given, all identifiers will be initiated. Note that in order for files to be processed you need to run * 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 processing function corresponding to that identifier. If you run this function without processing
* the files, it is likely nothing will be enqueued in the object and processing will complete * the files, it is likely nothing will be enqueued in the object and processing will complete
* immediately. Once all downloads are complete, this function will fire the 'dlcomplete' event on the * immediately. Once all downloads are complete, this function will fire the 'complete' event on the
* global object instance. * global object instance.
* *
* @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit. * @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit.
*/ */
processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){ processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){
this.progress = 0; return new Promise((resolve, reject) => {
let shouldFire = true
let shouldFire = true // Assign dltracking variables.
this.totaldlsize = 0
this.progress = 0
// Assign dltracking variables. for(let iden of identifiers){
this.totaldlsize = 0 this.totaldlsize += this[iden.id].dlsize
this.progress = 0 }
for(let i=0; i<identifiers.length; i++){
this.totaldlsize += this[identifiers[i].id].dlsize this.once('complete', (data) => {
resolve()
})
for(let iden of identifiers){
let r = this.startAsyncProcess(iden.id, iden.limit)
if(r) shouldFire = false
}
if(shouldFire){
this.emit('complete', 'download')
}
})
}
async validateEverything(serverid, dev = false){
if(!ConfigManager.isLoaded()){
ConfigManager.load()
}
DistroManager.setDevMode(dev)
const dI = await DistroManager.pullLocal()
const server = dI.getServer(serverid)
// Validate Everything
await this.validateDistribution(server)
this.emit('validate', 'distribution')
const versionData = await this.loadVersionData(server.getMinecraftVersion())
this.emit('validate', 'version')
await this.validateAssets(versionData)
this.emit('validate', 'assets')
await this.validateLibraries(versionData)
this.emit('validate', 'libraries')
await this.validateMiscellaneous(versionData)
this.emit('validate', 'files')
await this.processDlQueues()
//this.emit('complete', 'download')
const forgeData = await this.loadForgeData(server)
return {
versionData,
forgeData
} }
for(let i=0; i<identifiers.length; i++){
let iden = identifiers[i]
let r = this.startAsyncProcess(iden.id, iden.limit)
if(r) shouldFire = false
}
if(shouldFire){
this.emit('dlcomplete')
}
} }
// #endregion // #endregion

View File

@ -9,8 +9,8 @@
* @module authmanager * @module authmanager
*/ */
// Requirements // Requirements
const ConfigManager = require('./configmanager.js') const ConfigManager = require('./configmanager')
const Mojang = require('./mojang.js') const Mojang = require('./mojang')
// Functions // Functions

View File

@ -108,6 +108,13 @@ exports.load = function(){
console.log('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold', 'Successfully Loaded') console.log('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold', 'Successfully Loaded')
} }
/**
* @returns {boolean} Whether or not the manager has been loaded.
*/
exports.isLoaded = function(){
return config != null
}
/** /**
* Validate that the destination object has at least every field * Validate that the destination object has at least every field
* present in the source object. Assign a default value otherwise. * present in the source object. Assign a default value otherwise.

View File

@ -1,49 +1,47 @@
// Work in progress // Work in progress
const {Client} = require('discord-rpc') const {Client} = require('discord-rpc')
const ConfigManager = require('./configmanager.js') const ConfigManager = require('./configmanager')
let rpc let client
let activity let activity
exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){ exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){
rpc = new Client({ transport: 'ipc' }) client = new Client({ transport: 'ipc' })
rpc.on('ready', () => { activity = {
activity = { details: initialDetails,
details: initialDetails, state: 'Server: ' + servSettings.shortId,
state: 'Server: ' + servSettings.shortId, largeImageKey: servSettings.largeImageKey,
largeImageKey: servSettings.largeImageKey, largeImageText: servSettings.largeImageText,
largeImageText: servSettings.largeImageText, smallImageKey: genSettings.smallImageKey,
smallImageKey: genSettings.smallImageKey, smallImageText: genSettings.smallImageText,
smallImageText: genSettings.smallImageText, startTimestamp: new Date().getTime() / 1000,
startTimestamp: new Date().getTime() / 1000, instance: false
instance: false }
}
rpc.setActivity(activity) client.on('ready', () => {
console.log('%c[Discord Wrapper]', 'color: #a02d2a; font-weight: bold', 'Discord RPC Connected')
client.setActivity(activity)
}) })
rpc.login(genSettings.clientID).catch(error => { client.login({clientId: genSettings.clientId}).catch(error => {
if(error.message.includes('ENOENT')) { if(error.message.includes('ENOENT')) {
console.log('Unable to initialize Discord Rich Presence, no client detected.') console.log('%c[Discord Wrapper]', 'color: #a02d2a; font-weight: bold', 'Unable to initialize Discord Rich Presence, no client detected.')
} else { } else {
console.log('Unable to initialize Discord Rich Presence: ' + error.message, error) console.log('%c[Discord Wrapper]', 'color: #a02d2a; font-weight: bold', 'Unable to initialize Discord Rich Presence: ' + error.message, error)
} }
}) })
} }
exports.updateDetails = function(details){ exports.updateDetails = function(details){
if(activity == null){
console.error('Discord RPC is not initialized and therefore cannot be updated.')
}
activity.details = details activity.details = details
rpc.setActivity(activity) client.setActivity(activity)
} }
exports.shutdownRPC = function(){ exports.shutdownRPC = function(){
if(!rpc) return if(!client) return
rpc.clearActivity() client.clearActivity()
rpc.destroy() client.destroy()
rpc = null client = null
activity = null activity = null
} }

View File

@ -0,0 +1,584 @@
const fs = require('fs')
const path = require('path')
const request = require('request')
const ConfigManager = require('./configmanager')
/**
* Represents the download information
* for a specific module.
*/
class Artifact {
/**
* Parse a JSON object into an Artifact.
*
* @param {Object} json A JSON object representing an Artifact.
*
* @returns {Artifact} The parsed Artifact.
*/
static fromJSON(json){
return Object.assign(new Artifact(), json)
}
/**
* Get the MD5 hash of the artifact. This value may
* be undefined for artifacts which are not to be
* validated and updated.
*
* @returns {string} The MD5 hash of the Artifact or undefined.
*/
getHash(){
return this.MD5
}
/**
* @returns {number} The download size of the artifact.
*/
getSize(){
return this.size
}
/**
* @returns {string} The download url of the artifact.
*/
getURL(){
return this.url
}
/**
* @returns {string} The artifact's destination path.
*/
getPath(){
return this.path
}
}
exports.Artifact
/**
* Represents a the requirement status
* of a module.
*/
class Required {
/**
* Parse a JSON object into a Required object.
*
* @param {Object} json A JSON object representing a Required object.
*
* @returns {Required} The parsed Required object.
*/
static fromJSON(json){
if(json == null){
return new Required(true, true)
} else {
return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def)
}
}
constructor(value, def){
this.value = value
this.default = def
}
/**
* Get the default value for a required object. If a module
* is not required, this value determines whether or not
* it is enabled by default.
*
* @returns {boolean} The default enabled value.
*/
isDefault(){
return this.default
}
/**
* @returns {boolean} Whether or not the module is required.
*/
isRequired(){
return this.value
}
}
exports.Required
/**
* Represents a module.
*/
class Module {
/**
* Parse a JSON object into a Module.
*
* @param {Object} json A JSON object representing a Module.
* @param {string} serverid The ID of the server to which this module belongs.
*
* @returns {Module} The parsed Module.
*/
static fromJSON(json, serverid){
return new Module(json.id, json.name, json.type, json.required, json.artifact, json.subModules, serverid)
}
/**
* Resolve the default extension for a specific module type.
*
* @param {string} type The type of the module.
*
* @return {string} The default extension for the given type.
*/
static _resolveDefaultExtension(type){
switch (type) {
case exports.Types.Library:
case exports.Types.ForgeHosted:
case exports.Types.LiteLoader:
case exports.Types.ForgeMod:
return 'jar'
case exports.Types.LiteMod:
return 'litemod'
case exports.Types.File:
default:
return 'jar' // There is no default extension really.
}
}
constructor(id, name, type, required, artifact, subModules, serverid) {
this.identifier = id
this.type = type
this._resolveMetaData()
this.name = name
this.required = Required.fromJSON(required)
this.artifact = Artifact.fromJSON(artifact)
this._resolveArtifactPath(artifact.path, serverid)
this._resolveSubModules(subModules, serverid)
}
_resolveMetaData(){
try {
const m0 = this.identifier.split('@')
this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type)
const m1 = m0[0].split(':')
this.artifactVersion = m1[2] || '???'
this.artifactID = m1[1] || '???'
this.artifactGroup = m1[0] || '???'
} catch (err) {
// Improper identifier
console.error('Improper ID for module', this.identifier, err)
}
}
_resolveArtifactPath(artifactPath, serverid){
const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}.${this.getExtension()}`) : artifactPath
switch (this.type){
case exports.Types.Library:
case exports.Types.ForgeHosted:
case exports.Types.LiteLoader:
this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth)
break
case exports.Types.ForgeMod:
case exports.Types.LiteMod:
this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth)
break
case exports.Types.File:
default:
this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth)
break
}
}
_resolveSubModules(json, serverid){
const arr = []
if(json != null){
for(let sm of json){
arr.push(Module.fromJSON(sm, serverid))
}
}
this.subModules = arr.length > 0 ? arr : null
}
/**
* @returns {string} The full, unparsed module identifier.
*/
getIdentifier(){
return this.identifier
}
/**
* @returns {string} The name of the module.
*/
getName(){
return this.name
}
/**
* @returns {Required} The required object declared by this module.
*/
getRequired(){
return this.required
}
/**
* @returns {Artifact} The artifact declared by this module.
*/
getArtifact(){
return this.artifact
}
/**
* @returns {string} The maven identifier of this module's artifact.
*/
getID(){
return this.artifactID
}
/**
* @returns {string} The maven group of this module's artifact.
*/
getGroup(){
return this.artifactGroup
}
getVersionlessID(){
return this.getGroup() + ':' + this.getID()
}
/**
* @returns {string} The version of this module's artifact.
*/
getVersion(){
return this.artifactVersion
}
/**
* @returns {string} The extension of this module's artifact.
*/
getExtension(){
return this.artifactExt
}
/**
* @returns {boolean} Whether or not this module has sub modules.
*/
hasSubModules(){
return this.subModules != null
}
/**
* @returns {Array.<Module>} An array of sub modules.
*/
getSubModules(){
return this.subModules
}
/**
* @returns {string} The type of the module.
*/
getType(){
return this.type
}
}
exports.Module
/**
* Represents a server configuration.
*/
class Server {
/**
* Parse a JSON object into a Server.
*
* @param {Object} json A JSON object representing a Server.
*
* @returns {Server} The parsed Server object.
*/
static fromJSON(json){
const mdls = json.modules
json.modules = []
const serv = Object.assign(new Server(), json)
serv._resolveModules(mdls)
return serv
}
_resolveModules(json){
const arr = []
for(let m of json){
arr.push(Module.fromJSON(m, this.getID()))
}
this.modules = arr
}
/**
* @returns {string} The ID of the server.
*/
getID(){
return this.id
}
/**
* @returns {string} The name of the server.
*/
getName(){
return this.name
}
/**
* @returns {string} The description of the server.
*/
getDescription(){
return this.description
}
/**
* @returns {string} The URL of the server's icon.
*/
getIcon(){
return this.icon
}
/**
* @returns {string} The version of the server configuration.
*/
getVersion(){
return this.version
}
/**
* @returns {string} The IP address of the server.
*/
getAddress(){
return this.address
}
/**
* @returns {string} The minecraft version of the server.
*/
getMinecraftVersion(){
return this.minecraftVersion
}
/**
* @returns {boolean} Whether or not this server is the main
* server. The main server is selected by the launcher when
* no valid server is selected.
*/
isMainServer(){
return this.mainServer
}
/**
* @returns {boolean} Whether or not the server is autoconnect.
* by default.
*/
isAutoConnect(){
return this.autoconnect
}
/**
* @returns {Array.<Module>} An array of modules for this server.
*/
getModules(){
return this.modules
}
}
exports.Server
/**
* Represents the Distribution Index.
*/
class DistroIndex {
/**
* Parse a JSON object into a DistroIndex.
*
* @param {Object} json A JSON object representing a DistroIndex.
*
* @returns {DistroIndex} The parsed Server object.
*/
static fromJSON(json){
const servers = json.servers
json.servers = []
const distro = Object.assign(new DistroIndex(), json)
distro._resolveServers(servers)
distro._resolveMainServer()
return distro
}
_resolveServers(json){
const arr = []
for(let s of json){
arr.push(Server.fromJSON(s))
}
this.servers = arr
}
_resolveMainServer(){
for(let serv of this.servers){
if(serv.mainServer){
this.mainServer = serv.id
return
}
}
// If no server declares default_selected, default to the first one declared.
this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null
}
/**
* @returns {string} The version of the distribution index.
*/
getVersion(){
return this.version
}
/**
* @returns {string} The URL to the news RSS feed.
*/
getRSS(){
return this.rss
}
/**
* @returns {Array.<Server>} An array of declared server configurations.
*/
getServers(){
return this.servers
}
/**
* Get a server configuration by its ID. If it does not
* exist, null will be returned.
*
* @param {string} id The ID of the server.
*
* @returns {Server} The server configuration with the given ID or null.
*/
getServer(id){
for(let serv of this.servers){
if(serv.id === id){
return serv
}
}
return null
}
/**
* Get the main server.
*
* @returns {Server} The main server.
*/
getMainServer(){
return getServer(this.mainServer)
}
}
exports.DistroIndex
exports.Types = {
Library: 'Library',
ForgeHosted: 'ForgeHosted',
Forge: 'Forge', // Unimplemented
LiteLoader: 'LiteLoader',
ForgeMod: 'ForgeMod',
LiteMod: 'LiteMod',
File: 'File'
}
let DEV_MODE = false
const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json')
const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json')
let data = null
/**
* @returns {Promise.<DistroIndex>}
*/
exports.pullRemote = function(){
if(DEV_MODE){
return exports.pullLocal()
}
return new Promise((resolve, reject) => {
const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
//const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/'
const opts = {
url: distroURL,
timeout: 2500
}
const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json')
request(opts, (error, resp, body) => {
if(!error){
data = DistroIndex.fromJSON(JSON.parse(body))
fs.writeFile(distroDest, body, 'utf-8', (err) => {
if(!err){
resolve(data)
} else {
reject(err)
}
})
} else {
reject(error)
}
})
})
}
/**
* @returns {Promise.<DistroIndex>}
*/
exports.pullLocal = function(){
return new Promise((resolve, reject) => {
fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => {
if(!err){
data = DistroIndex.fromJSON(JSON.parse(d))
resolve(data)
} else {
reject(err)
}
})
})
}
exports.setDevMode = function(value){
if(value){
console.log('%c[DistroManager]', 'color: #a02d2a; font-weight: bold', 'Developer mode enabled.')
console.log('%c[DistroManager]', 'color: #a02d2a; font-weight: bold', 'If you don\'t know what that means, revert immediately.')
} else {
console.log('%c[DistroManager]', 'color: #a02d2a; font-weight: bold', 'Developer mode disabled.')
}
DEV_MODE = value
}
exports.isDevMode = function(){
return DEV_MODE
}
/**
* @returns {DistroIndex}
*/
exports.getDistribution = function(){
return data
}
/*async function debug(){
const d = await exports.pullRemote()
console.log(d)
}
debug()*/
//console.log(DistroIndex.fromJSON(JSON.parse(require('fs').readFileSync('../distribution.json', 'utf-8'))))

View File

@ -1,84 +0,0 @@
{
"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",
"modules": [
{
"id": "MODNAME",
"name": "Mod Name version 1.11.2",
"type": "forgemod",
"_comment": "If no required is given, it will default to true. If a def(ault) is not give, it will default to true. If required is present it always expects a value.",
"required": {
"value": false,
"def": false
},
"artifact": {
"size": 1234,
"MD5": "e71e88c744588fdad48d3b3beb4935fc",
"path": "forgemod path is appended to {basepath}/mods",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/somemod.jar"
}
},
{
"_comment": "Forge is a special instance of library.",
"id": "net.minecraftforge.forge.forge-universal:1.11.2-13.20.0.2228",
"name": "Minecraft Forge 1.11.2-13.20.0.2228",
"type": "forge",
"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"
}
},
{
"_comment": "library path is appended to {basepath}/libraries",
"id": "net.optifine.optifine:1.11.2_HD_U_B8",
"name": "Optifine 1.11.2 HD U B8",
"type": "library",
"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"
}
},
{
"id": "chatbubbles",
"name": "Chat Bubbles 1.11.2",
"type": "litemod",
"required": {
"value": false
},
"artifact": {
"size": 37838,
"MD5": "0497a93e5429b43082282e9d9119fcba",
"path": "litemod path is appended to {basepath}/mods/{mc-version}",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/mod_chatBubbles-1.0.1_for_1.11.2.litemod"
},
"_comment": "Any module can declare submodules, even submodules.",
"sub-modules": [
{
"id": "customRegexes",
"name": "Custom Regexes for Chat Bubbles",
"type": "file",
"artifact": {
"size": 331,
"MD5": "f21b4b325f09238a3d6b2103d54351ef",
"path": "file path is appended to {basepath}",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/customRegexes.txt"
}
}
]
}
]
}
]
}

View File

@ -1,10 +1,11 @@
const {AssetGuard} = require('./assetguard.js')
const ConfigManager = require('./configmanager.js')
const {ipcRenderer} = require('electron') const {ipcRenderer} = require('electron')
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
const rimraf = require('rimraf') const rimraf = require('rimraf')
const ConfigManager = require('./configmanager')
const DistroManager = require('./distromanager')
console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Loading..') console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Loading..')
// Load ConfigManager // Load ConfigManager
@ -14,17 +15,17 @@ function onDistroLoad(data){
if(data != null){ if(data != null){
// Resolve the selected server if its value has yet to be set. // Resolve the selected server if its value has yet to be set.
if(ConfigManager.getSelectedServer() == null || AssetGuard.getServerById(ConfigManager.getSelectedServer()) == null){ if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null){
console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Determining default selected server..') console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Determining default selected server..')
ConfigManager.setSelectedServer(AssetGuard.resolveSelectedServer().id) ConfigManager.setSelectedServer(data.getMainServer().getID())
ConfigManager.save() ConfigManager.save()
} }
} }
ipcRenderer.send('distributionIndexDone', data) ipcRenderer.send('distributionIndexDone', data != null)
} }
// Ensure Distribution is downloaded and cached. // Ensure Distribution is downloaded and cached.
AssetGuard.refreshDistributionDataRemote(ConfigManager.getLauncherDirectory()).then((data) => { DistroManager.pullRemote().then((data) => {
console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Loaded distribution index.') console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Loaded distribution index.')
onDistroLoad(data) onDistroLoad(data)
@ -35,7 +36,7 @@ AssetGuard.refreshDistributionDataRemote(ConfigManager.getLauncherDirectory()).t
console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Attempting to load an older version of the distribution index.') console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Attempting to load an older version of the distribution index.')
// Try getting a local copy, better than nothing. // Try getting a local copy, better than nothing.
AssetGuard.refreshDistributionDataLocal(ConfigManager.getLauncherDirectory()).then((data) => { DistroManager.pullLocal().then((data) => {
console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Successfully loaded an older version of the distribution index.') console.log('%c[Preloader]', 'color: #a02d2a; font-weight: bold', 'Successfully loaded an older version of the distribution index.')
onDistroLoad(data) onDistroLoad(data)

View File

@ -1,7 +1,5 @@
const AdmZip = require('adm-zip') const AdmZip = require('adm-zip')
const {AssetGuard, Library} = require('./assetguard.js')
const child_process = require('child_process') const child_process = require('child_process')
const ConfigManager = require('./configmanager.js')
const crypto = require('crypto') const crypto = require('crypto')
const fs = require('fs') const fs = require('fs')
const mkpath = require('mkdirp') const mkpath = require('mkdirp')
@ -10,10 +8,14 @@ const path = require('path')
const rimraf = require('rimraf') const rimraf = require('rimraf')
const {URL} = require('url') const {URL} = require('url')
const { Library } = require('./assetguard')
const ConfigManager = require('./configmanager')
const DistroManager = require('./distromanager')
class ProcessBuilder { class ProcessBuilder {
constructor(distroServer, versionData, forgeData, authUser){ constructor(distroServer, versionData, forgeData, authUser){
this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.id) this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.getID())
this.commonDir = ConfigManager.getCommonDirectory() this.commonDir = ConfigManager.getCommonDirectory()
this.server = distroServer this.server = distroServer
this.versionData = versionData this.versionData = versionData
@ -35,7 +37,9 @@ class ProcessBuilder {
const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex')) const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex'))
process.throwDeprecation = true process.throwDeprecation = true
this.setupLiteLoader() this.setupLiteLoader()
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.id).mods, this.server.modules) console.log('using liteloader', this.usingLiteLoader)
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.getID()).mods, this.server.getModules())
console.log(modObj)
this.constructModList('forge', modObj.fMods, true) this.constructModList('forge', modObj.fMods, true)
if(this.usingLiteLoader){ if(this.usingLiteLoader){
this.constructModList('liteloader', modObj.lMods, true) this.constructModList('liteloader', modObj.lMods, true)
@ -92,20 +96,7 @@ class ProcessBuilder {
* @returns {boolean} True if the mod is enabled, false otherwise. * @returns {boolean} True if the mod is enabled, false otherwise.
*/ */
static isModEnabled(modCfg, required = null){ static isModEnabled(modCfg, required = null){
return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && modCfg.value)) : required != null && required.def != null ? required.def : true return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && modCfg.value)) : required != null ? required.isDefault() : true
}
/**
* Determine if a mod is optional.
*
* A mod is optional if its required object is not null and its 'value'
* property is false.
*
* @param {Object} mdl The mod distro module.
* @returns {boolean} True if the mod is optional, otherwise false.
*/
static isModOptional(mdl){
return mdl.required != null && mdl.required.value != null && mdl.required.value === false
} }
/** /**
@ -115,19 +106,21 @@ class ProcessBuilder {
* mod. It must not be declared as a submodule. * mod. It must not be declared as a submodule.
*/ */
setupLiteLoader(){ setupLiteLoader(){
const mdls = this.server.modules for(let ll of this.server.getModules()){
for(let i=0; i<mdls.length; i++){ if(ll.getType() === DistroManager.Types.LiteLoader){
if(mdls[i].type === 'liteloader'){ if(!ll.getRequired().isRequired()){
const ll = mdls[i] const modCfg = ConfigManager.getModConfiguration(this.server.getID()).mods
if(ProcessBuilder.isModOptional(ll)){ if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessID()], ll.getRequired())){
const modCfg = ConfigManager.getModConfiguration(this.server.id).mods if(fs.existsSync(ll.getArtifact().getPath())){
if(ProcessBuilder.isModEnabled(modCfg[AssetGuard._resolveWithoutVersion(ll.id)], ll.required)){ this.usingLiteLoader = true
this.usingLiteLoader = true this.llPath = ll.getArtifact().getPath()
this.llPath = path.join(this.libPath, ll.artifact.path == null ? AssetGuard._resolvePath(ll.id, ll.artifact.extension) : ll.artifact.path) }
} }
} else { } else {
this.usingLiteLoader = true if(fs.existsSync(ll.getArtifact().getPath())){
this.llPath = path.join(this.libPath, ll.artifact.path == null ? AssetGuard._resolvePath(ll.id, ll.artifact.extension) : ll.artifact.path) this.usingLiteLoader = true
this.llPath = ll.getArtifact().getPath()
}
} }
} }
} }
@ -146,21 +139,21 @@ class ProcessBuilder {
let fMods = [] let fMods = []
let lMods = [] let lMods = []
for(let i=0; i<mdls.length; i++){ for(let mdl of mdls){
const mdl = mdls[i] const type = mdl.getType()
if(mdl.type != null && (mdl.type === 'forgemod' || mdl.type === 'litemod' || mdl.type === 'liteloader')){ if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
const o = ProcessBuilder.isModOptional(mdl) const o = !mdl.getRequired().isRequired()
const e = ProcessBuilder.isModEnabled(modCfg[AssetGuard._resolveWithoutVersion(mdl.id)], mdl.required) const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessID()], mdl.getRequired())
if(!o || (o && e)){ if(!o || (o && e)){
if(mdl.sub_modules != null){ if(mdl.hasSubModules()){
const v = this.resolveModConfiguration(modCfg[AssetGuard._resolveWithoutVersion(mdl.id)].mods, mdl.sub_modules) const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessID()].mods, mdl.getSubModules())
fMods = fMods.concat(v.fMods) fMods = fMods.concat(v.fMods)
lMods = lMods.concat(v.lMods) lMods = lMods.concat(v.lMods)
if(mdl.type === 'liteloader'){ if(mdl.type === DistroManager.Types.LiteLoader){
continue continue
} }
} }
if(mdl.type === 'forgemod'){ if(mdl.type === DistroManager.Types.ForgeMod){
fMods.push(mdl) fMods.push(mdl)
} else { } else {
lMods.push(mdl) lMods.push(mdl)
@ -189,12 +182,12 @@ class ProcessBuilder {
const ids = [] const ids = []
if(type === 'forge'){ if(type === 'forge'){
for(let i=0; i<mods.length; ++i){ for(let mod of mods){
ids.push(mods[i].id) ids.push(mod.getIdentifier())
} }
} else { } else {
for(let i=0; i<mods.length; ++i){ for(let mod of mods){
ids.push(mods[i].id + '@' + (mods[i].artifact.extension != null ? mods[i].artifact.extension.substring(1) : 'jar')) ids.push(mod.getIdentifier() + '@' + mod.getExtension())
} }
} }
modList.modRef = ids modList.modRef = ids
@ -255,7 +248,7 @@ class ProcessBuilder {
break break
case 'version_name': case 'version_name':
//val = versionData.id //val = versionData.id
val = this.server.id val = this.server.getID()
break break
case 'game_directory': case 'game_directory':
val = this.gameDir val = this.gameDir
@ -306,8 +299,8 @@ class ProcessBuilder {
} }
// Prepare autoconnect // Prepare autoconnect
if(ConfigManager.getAutoConnect() && this.server.autoconnect){ if(ConfigManager.getAutoConnect() && this.server.isAutoConnect()){
const serverURL = new URL('my://' + this.server.server_ip) const serverURL = new URL('my://' + this.server.getAddress())
mcArgs.unshift(serverURL.hostname) mcArgs.unshift(serverURL.hostname)
mcArgs.unshift('--server') mcArgs.unshift('--server')
if(serverURL.port){ if(serverURL.port){
@ -428,16 +421,16 @@ class ProcessBuilder {
* @returns {Array.<string>} An array containing the paths of each library this server requires. * @returns {Array.<string>} An array containing the paths of each library this server requires.
*/ */
_resolveServerLibraries(mods){ _resolveServerLibraries(mods){
const mdles = this.server.modules const mdls = this.server.getModules()
let libs = [] let libs = []
// Locate Forge/Libraries // Locate Forge/Libraries
for(let i=0; i<mdles.length; i++){ for(let mdl of mdls){
if(mdles[i].type != null && (mdles[i].type === 'forge-hosted' || mdles[i].type === 'library')){ const type = mdl.getType()
let lib = mdles[i] if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Library){
libs.push(path.join(this.libPath, lib.artifact.path == null ? AssetGuard._resolvePath(lib.id, lib.artifact.extension) : lib.artifact.path)) libs.push(mdl.getArtifact().getPath())
if(lib.sub_modules != null){ if(mdl.hasSubModules()){
const res = this._resolveModuleLibraries(lib) const res = this._resolveModuleLibraries(mdl)
if(res.length > 0){ if(res.length > 0){
libs = libs.concat(res) libs = libs.concat(res)
} }
@ -461,22 +454,21 @@ class ProcessBuilder {
/** /**
* Recursively resolve the path of each library required by this module. * Recursively resolve the path of each library required by this module.
* *
* @param {Object} mdle A module object from the server distro index. * @param {Object} mdl A module object from the server distro index.
* @returns {Array.<string>} An array containing the paths of each library this module requires. * @returns {Array.<string>} An array containing the paths of each library this module requires.
*/ */
_resolveModuleLibraries(mdle){ _resolveModuleLibraries(mdl){
if(mdle.sub_modules == null){ if(!mdl.hasSubModules()){
return [] return []
} }
let libs = [] let libs = []
for(let i=0; i<mdle.sub_modules.length; i++){ for(let sm of mdl.getSubModules()){
const sm = mdle.sub_modules[i] if(sm.getType() === DistroManager.Types.Library){
if(sm.type != null && sm.type == 'library'){ libs.push(sm.getArtifact().getPath())
libs.push(path.join(this.libPath, sm.artifact.path == null ? AssetGuard._resolvePath(sm.id, sm.artifact.extension) : sm.artifact.path))
} }
// If this module has submodules, we need to resolve the libraries for those. // If this module has submodules, we need to resolve the libraries for those.
// To avoid unnecessary recursive calls, base case is checked here. // To avoid unnecessary recursive calls, base case is checked here.
if(mdle.sub_modules != null){ if(mdl.hasSubModules()){
const res = this._resolveModuleLibraries(sm) const res = this._resolveModuleLibraries(sm)
if(res.length > 0){ if(res.length > 0){
libs = libs.concat(res) libs = libs.concat(res)

View File

@ -7,10 +7,10 @@ const crypto = require('crypto')
const {URL} = require('url') const {URL} = require('url')
// Internal Requirements // Internal Requirements
const DiscordWrapper = require('./assets/js/discordwrapper.js') const DiscordWrapper = require('./assets/js/discordwrapper')
const Mojang = require('./assets/js/mojang.js') const Mojang = require('./assets/js/mojang')
const ProcessBuilder = require('./assets/js/processbuilder.js') const ProcessBuilder = require('./assets/js/processbuilder')
const ServerStatus = require('./assets/js/serverstatus.js') const ServerStatus = require('./assets/js/serverstatus')
// Launch Elements // Launch Elements
const launch_content = document.getElementById('launch_content') const launch_content = document.getElementById('launch_content')
@ -208,13 +208,13 @@ const refreshMojangStatuses = async function(){
const refreshServerStatus = async function(fade = false){ const refreshServerStatus = async function(fade = false){
console.log('Refreshing Server Status') console.log('Refreshing Server Status')
const serv = AssetGuard.getServerById(ConfigManager.getSelectedServer()) const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer())
let pLabel = 'SERVER' let pLabel = 'SERVER'
let pVal = 'OFFLINE' let pVal = 'OFFLINE'
try { try {
const serverURL = new URL('my://' + serv.server_ip) const serverURL = new URL('my://' + serv.getAddress())
const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port) const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port)
if(servStat.online){ if(servStat.online){
pLabel = 'PLAYERS' pLabel = 'PLAYERS'
@ -261,9 +261,7 @@ function asyncSystemScan(launchAfter = true){
// Fork a process to run validations. // Fork a process to run validations.
sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
ConfigManager.getCommonDirectory(), ConfigManager.getCommonDirectory(),
ConfigManager.getLauncherDirectory(), ConfigManager.getJavaExecutable()
ConfigManager.getJavaExecutable(),
ConfigManager.getInstanceDirectory()
], { ], {
stdio: 'pipe' stdio: 'pipe'
}) })
@ -277,8 +275,8 @@ function asyncSystemScan(launchAfter = true){
}) })
sysAEx.on('message', (m) => { sysAEx.on('message', (m) => {
if(m.content === 'validateJava'){
if(m.context === 'validateJava'){
if(m.result == null){ if(m.result == null){
// If the result is null, no valid Java installation was found. // If the result is null, no valid Java installation was found.
// Show this information to the user. // Show this information to the user.
@ -290,7 +288,7 @@ function asyncSystemScan(launchAfter = true){
) )
setOverlayHandler(() => { setOverlayHandler(() => {
setLaunchDetails('Preparing Java Download..') setLaunchDetails('Preparing Java Download..')
sysAEx.send({task: 0, content: '_enqueueOracleJRE', argsArr: [ConfigManager.getLauncherDirectory()]}) sysAEx.send({task: 'execute', function: '_enqueueOracleJRE', argsArr: [ConfigManager.getLauncherDirectory()]})
toggleOverlay(false) toggleOverlay(false)
}) })
setDismissHandler(() => { setDismissHandler(() => {
@ -330,14 +328,13 @@ function asyncSystemScan(launchAfter = true){
} }
sysAEx.disconnect() sysAEx.disconnect()
} }
} else if(m.context === '_enqueueOracleJRE'){
} else if(m.content === '_enqueueOracleJRE'){
if(m.result === true){ if(m.result === true){
// Oracle JRE enqueued successfully, begin download. // Oracle JRE enqueued successfully, begin download.
setLaunchDetails('Downloading Java..') setLaunchDetails('Downloading Java..')
sysAEx.send({task: 0, content: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]}) sysAEx.send({task: 'execute', function: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]})
} else { } else {
@ -357,58 +354,64 @@ function asyncSystemScan(launchAfter = true){
} }
} else if(m.content === 'dl'){ } else if(m.context === 'progress'){
if(m.task === 0){ switch(m.data){
// Downloading.. case 'download':
setDownloadPercentage(m.value, m.total, m.percent) // Downloading..
} else if(m.task === 1){ setDownloadPercentage(m.value, m.total, m.percent)
// Show installing progress bar. break
remote.getCurrentWindow().setProgressBar(2)
// Wait for extration to complete.
const eLStr = 'Extracting'
let dotStr = ''
setLaunchDetails(eLStr)
extractListener = setInterval(() => {
if(dotStr.length >= 3){
dotStr = ''
} else {
dotStr += '.'
}
setLaunchDetails(eLStr + dotStr)
}, 750)
} else if(m.task === 2){
// Download & extraction complete, remove the loading from the OS progress bar.
remote.getCurrentWindow().setProgressBar(-1)
// Extraction completed successfully.
ConfigManager.setJavaExecutable(m.jPath)
ConfigManager.save()
if(extractListener != null){
clearInterval(extractListener)
extractListener = null
}
setLaunchDetails('Java Installed!')
if(launchAfter){
dlAsync()
}
sysAEx.disconnect()
} else {
console.error('Unknown download data type.', m)
} }
} else if(m.context === 'complete'){
switch(m.data){
case 'download':
// Show installing progress bar.
remote.getCurrentWindow().setProgressBar(2)
// Wait for extration to complete.
const eLStr = 'Extracting'
let dotStr = ''
setLaunchDetails(eLStr)
extractListener = setInterval(() => {
if(dotStr.length >= 3){
dotStr = ''
} else {
dotStr += '.'
}
setLaunchDetails(eLStr + dotStr)
}, 750)
break
case 'java':
// Download & extraction complete, remove the loading from the OS progress bar.
remote.getCurrentWindow().setProgressBar(-1)
// Extraction completed successfully.
ConfigManager.setJavaExecutable(m.args[0])
ConfigManager.save()
if(extractListener != null){
clearInterval(extractListener)
extractListener = null
}
setLaunchDetails('Java Installed!')
if(launchAfter){
dlAsync()
}
sysAEx.disconnect()
break
}
} }
}) })
// Begin system Java scan. // Begin system Java scan.
setLaunchDetails('Checking system info..') setLaunchDetails('Checking system info..')
sysAEx.send({task: 0, content: 'validateJava', argsArr: [ConfigManager.getLauncherDirectory()]}) sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getLauncherDirectory()]})
} }
@ -448,9 +451,7 @@ function dlAsync(login = true){
// Start AssetExec to run validations and downloads in a forked process. // Start AssetExec to run validations and downloads in a forked process.
aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
ConfigManager.getCommonDirectory(), ConfigManager.getCommonDirectory(),
ConfigManager.getLauncherDirectory(), ConfigManager.getJavaExecutable()
ConfigManager.getJavaExecutable(),
ConfigManager.getInstanceDirectory()
], { ], {
stdio: 'pipe' stdio: 'pipe'
}) })
@ -465,136 +466,110 @@ function dlAsync(login = true){
// Establish communications between the AssetExec and current process. // Establish communications between the AssetExec and current process.
aEx.on('message', (m) => { aEx.on('message', (m) => {
if(m.content === 'validateDistribution'){
setLaunchPercentage(20, 100) if(m.context === 'validate'){
serv = m.result switch(m.data){
console.log('Validated distibution index.') case 'distribution':
setLaunchPercentage(20, 100)
// Begin version load. console.log('Validated distibution index.')
setLaunchDetails('Loading version information..') setLaunchDetails('Loading version information..')
aEx.send({task: 0, content: 'loadVersionData', argsArr: [serv.mc_version]}) break
case 'version':
} else if(m.content === 'loadVersionData'){ setLaunchPercentage(40, 100)
console.log('Version data loaded.')
setLaunchPercentage(40, 100) setLaunchDetails('Validating asset integrity..')
versionData = m.result break
console.log('Version data loaded.') case 'assets':
setLaunchPercentage(60, 100)
// Begin asset validation. console.log('Asset Validation Complete')
setLaunchDetails('Validating asset integrity..') setLaunchDetails('Validating library integrity..')
aEx.send({task: 0, content: 'validateAssets', argsArr: [versionData]}) break
case 'libraries':
} else if(m.content === 'validateAssets'){ setLaunchPercentage(80, 100)
console.log('Library validation complete.')
// Asset validation can *potentially* take longer, so let's track progress. setLaunchDetails('Validating miscellaneous file integrity..')
if(m.task === 0){ break
const perc = (m.value/m.total)*20 case 'files':
setLaunchPercentage(40+perc, 100, parseInt(40+perc)) setLaunchPercentage(100, 100)
} else { console.log('File validation complete.')
setLaunchPercentage(60, 100) setLaunchDetails('Downloading files..')
console.log('Asset Validation Complete') break
// Begin library validation.
setLaunchDetails('Validating library integrity..')
aEx.send({task: 0, content: 'validateLibraries', argsArr: [versionData]})
} }
} else if(m.context === 'progress'){
switch(m.data){
case 'assets':
const perc = (m.value/m.total)*20
setLaunchPercentage(40+perc, 100, parseInt(40+perc))
break
case 'download':
setDownloadPercentage(m.value, m.total, m.percent)
break
case 'extract':
// Show installing progress bar.
remote.getCurrentWindow().setProgressBar(2)
} else if(m.content === 'validateLibraries'){ // Download done, extracting.
const eLStr = 'Extracting libraries'
setLaunchPercentage(80, 100) let dotStr = ''
console.log('Library validation complete.') setLaunchDetails(eLStr)
progressListener = setInterval(() => {
// Begin miscellaneous validation. if(dotStr.length >= 3){
setLaunchDetails('Validating miscellaneous file integrity..') dotStr = ''
aEx.send({task: 0, content: 'validateMiscellaneous', argsArr: [versionData]}) } else {
dotStr += '.'
} else if(m.content === 'validateMiscellaneous'){ }
setLaunchDetails(eLStr + dotStr)
setLaunchPercentage(100, 100) }, 750)
console.log('File validation complete.') break
}
// Download queued files. } else if(m.context === 'complete'){
setLaunchDetails('Downloading files..') switch(m.data){
aEx.send({task: 0, content: 'processDlQueues'}) case 'download':
// Download and extraction complete, remove the loading from the OS progress bar.
} else if(m.content === 'dl'){ remote.getCurrentWindow().setProgressBar(-1)
if(progressListener != null){
if(m.task === 0){ clearInterval(progressListener)
progressListener = null
setDownloadPercentage(m.value, m.total, m.percent)
} else if(m.task === 0.7){
// Show installing progress bar.
remote.getCurrentWindow().setProgressBar(2)
// Download done, extracting.
const eLStr = 'Extracting libraries'
let dotStr = ''
setLaunchDetails(eLStr)
progressListener = setInterval(() => {
if(dotStr.length >= 3){
dotStr = ''
} else {
dotStr += '.'
} }
setLaunchDetails(eLStr + dotStr)
}, 750)
} else if(m.task === 0.9) {
console.error(m.err)
if(m.err.code === 'ENOENT'){
setOverlayContent(
'Download Error',
'Could not connect to the file server. Ensure that you are connected to the internet and try again.',
'Okay'
)
setOverlayHandler(null)
} else {
setOverlayContent(
'Download Error',
'Check the console for more details. Please try again.',
'Okay'
)
setOverlayHandler(null)
}
remote.getCurrentWindow().setProgressBar(-1)
toggleOverlay(true)
toggleLaunchArea(false)
// Disconnect from AssetExec
aEx.disconnect()
} else if(m.task === 1){
// Download and extraction complete, remove the loading from the OS progress bar.
remote.getCurrentWindow().setProgressBar(-1)
if(progressListener != null){
clearInterval(progressListener)
progressListener = null
}
setLaunchDetails('Preparing to launch..')
aEx.send({task: 0, content: 'loadForgeData', argsArr: [serv.id]})
} else {
console.error('Unknown download data type.', m)
setLaunchDetails('Preparing to launch..')
break
} }
} else if(m.context === 'error'){
switch(m.data){
case 'download':
console.error(m.error)
} else if(m.content === 'loadForgeData'){ if(m.error.code === 'ENOENT'){
setOverlayContent(
'Download Error',
'Could not connect to the file server. Ensure that you are connected to the internet and try again.',
'Okay'
)
setOverlayHandler(null)
} else {
setOverlayContent(
'Download Error',
'Check the console for more details. Please try again.',
'Okay'
)
setOverlayHandler(null)
}
forgeData = m.result remote.getCurrentWindow().setProgressBar(-1)
toggleOverlay(true)
toggleLaunchArea(false)
// Disconnect from AssetExec
aEx.disconnect()
break
}
} else if(m.context === 'validateEverything'){
forgeData = m.result.forgeData
versionData = m.result.versionData
if(login) { if(login) {
//if(!(await AuthManager.validateSelected())){
//
//}
const authUser = ConfigManager.getSelectedAccount() const authUser = ConfigManager.getSelectedAccount()
console.log('authu', authUser) console.log('authu', authUser)
let pb = new ProcessBuilder(serv, versionData, forgeData, authUser) let pb = new ProcessBuilder(serv, versionData, forgeData, authUser)
@ -623,7 +598,7 @@ function dlAsync(login = true){
if(servJoined.test(data)){ if(servJoined.test(data)){
DiscordWrapper.updateDetails('Exploring the Realm!') DiscordWrapper.updateDetails('Exploring the Realm!')
} else if(gameJoined.test(data)){ } else if(gameJoined.test(data)){
DiscordWrapper.updateDetails('Idling on Main Menu') DiscordWrapper.updateDetails('Sailing to Westeros!')
} }
} }
@ -632,7 +607,7 @@ function dlAsync(login = true){
proc.stdout.on('data', gameStateChange) proc.stdout.on('data', gameStateChange)
// Init Discord Hook // Init Discord Hook
const distro = AssetGuard.getDistributionData() const distro = DistroManager.getDistribution()
if(distro.discord != null && serv.discord != null){ if(distro.discord != null && serv.discord != null){
DiscordWrapper.initRPC(distro.discord, serv.discord) DiscordWrapper.initRPC(distro.discord, serv.discord)
hasRPC = true hasRPC = true
@ -670,14 +645,19 @@ function dlAsync(login = true){
// Validate Forge files. // Validate Forge files.
setLaunchDetails('Loading server information..') setLaunchDetails('Loading server information..')
if(AssetGuard.isLocalLaunch()){ refreshDistributionIndex(true, (data) => {
onDistroRefresh(data)
serv = data.getServer(ConfigManager.getSelectedServer())
aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]})
}, (err) => {
console.log(err)
refreshDistributionIndex(false, (data) => { refreshDistributionIndex(false, (data) => {
onDistroRefresh(data) onDistroRefresh(data)
aEx.send({task: 0, content: 'validateDistribution', argsArr: [ConfigManager.getSelectedServer()]}) serv = data.getServer(ConfigManager.getSelectedServer())
aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]})
}, (err) => { }, (err) => {
console.error('Unable to refresh distribution index.', err) console.error('Unable to refresh distribution index.', err)
if(AssetGuard.getDistributionData() == null){ if(DistroManager.getDistribution() == null){
setOverlayContent( setOverlayContent(
'Fatal Error', 'Fatal Error',
'Could not load a copy of the distribution index. See the console for more details.', 'Could not load a copy of the distribution index. See the console for more details.',
@ -691,40 +671,11 @@ function dlAsync(login = true){
// Disconnect from AssetExec // Disconnect from AssetExec
aEx.disconnect() aEx.disconnect()
} else { } else {
aEx.send({task: 0, content: 'validateDistribution', argsArr: [ConfigManager.getSelectedServer()]}) serv = data.getServer(ConfigManager.getSelectedServer())
aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]})
} }
}) })
})
} else {
refreshDistributionIndex(true, (data) => {
onDistroRefresh(data)
aEx.send({task: 0, content: 'validateDistribution', argsArr: [ConfigManager.getSelectedServer()]})
}, (err) => {
refreshDistributionIndex(false, (data) => {
onDistroRefresh(data)
}, (err) => {
console.error('Unable to refresh distribution index.', err)
if(AssetGuard.getDistributionData() == null){
setOverlayContent(
'Fatal Error',
'Could not load a copy of the distribution index. See the console for more details.',
'Okay'
)
setOverlayHandler(null)
toggleOverlay(true)
toggleLaunchArea(false)
// Disconnect from AssetExec
aEx.disconnect()
} else {
aEx.send({task: 0, content: 'validateDistribution', argsArr: [ConfigManager.getSelectedServer()]})
}
})
})
}
} }
/** /**
@ -1046,8 +997,8 @@ function displayArticle(articleObject, index){
*/ */
function loadNews(){ function loadNews(){
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const distroData = AssetGuard.getDistributionData() const distroData = DistroManager.getDistribution()
const newsFeed = distroData['news_feed'] const newsFeed = distroData.getRSS()
const newsHost = new URL(newsFeed).origin + '/' const newsHost = new URL(newsFeed).origin + '/'
$.ajax( $.ajax(
{ {

View File

@ -138,10 +138,10 @@ document.getElementById('serverSelectConfirm').addEventListener('click', () => {
const listings = document.getElementsByClassName('serverListing') const listings = document.getElementsByClassName('serverListing')
for(let i=0; i<listings.length; i++){ for(let i=0; i<listings.length; i++){
if(listings[i].hasAttribute('selected')){ if(listings[i].hasAttribute('selected')){
const serv = AssetGuard.getServerById(listings[i].getAttribute('servid')) const serv = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid'))
ConfigManager.setSelectedServer(serv != null ? serv.id : null) ConfigManager.setSelectedServer(serv != null ? serv.getID() : null)
ConfigManager.save() ConfigManager.save()
updateSelectedServer(serv != null ? serv.name : null) updateSelectedServer(serv != null ? serv.getName() : null)
setLaunchEnabled(serv != null) setLaunchEnabled(serv != null)
refreshServerStatus(true) refreshServerStatus(true)
toggleOverlay(false) toggleOverlay(false)
@ -229,20 +229,20 @@ function setAccountListingHandlers(){
} }
function populateServerListings(){ function populateServerListings(){
const distro = AssetGuard.getDistributionData() const distro = DistroManager.getDistribution()
const giaSel = ConfigManager.getSelectedServer() const giaSel = ConfigManager.getSelectedServer()
const servers = distro.servers const servers = distro.getServers()
let htmlString = `` let htmlString = ``
for(let i=0; i<servers.length; i++){ for(const serv of servers){
htmlString += `<button class="serverListing" servid="${servers[i].id}" ${servers[i].id === giaSel ? `selected` : ``}> htmlString += `<button class="serverListing" servid="${serv.getID()}" ${serv.getID() === giaSel ? `selected` : ``}>
<img class="serverListingImg" src="${servers[i].icon_url}"/> <img class="serverListingImg" src="${serv.getIcon()}"/>
<div class="serverListingDetails"> <div class="serverListingDetails">
<span class="serverListingName">${servers[i].name}</span> <span class="serverListingName">${serv.getName()}</span>
<span class="serverListingDescription">${servers[i].description}</span> <span class="serverListingDescription">${serv.getDescription()}</span>
<div class="serverListingInfo"> <div class="serverListingInfo">
<div class="serverListingVersion">${servers[i].mc_version}</div> <div class="serverListingVersion">${serv.getMinecraftVersion()}</div>
<div class="serverListingRevision">${servers[i].revision}</div> <div class="serverListingRevision">${serv.getVersion()}</div>
${servers[i].default_selected ? `<div class="serverListingStarWrapper"> ${serv.isMainServer() ? `<div class="serverListingStarWrapper">
<svg id="Layer_1" viewBox="0 0 107.45 104.74" width="20px" height="20px"> <svg id="Layer_1" viewBox="0 0 107.45 104.74" width="20px" height="20px">
<defs> <defs>
<style>.cls-1{fill:#fff;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style> <style>.cls-1{fill:#fff;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style>

View File

@ -2,6 +2,8 @@
const os = require('os') const os = require('os')
const semver = require('semver') const semver = require('semver')
const { AssetGuard } = require('./assets/js/assetguard')
const settingsState = { const settingsState = {
invalid: new Set() invalid: new Set()
} }

View File

@ -4,9 +4,10 @@
*/ */
// Requirements // Requirements
const path = require('path') const path = require('path')
const AuthManager = require('./assets/js/authmanager.js')
const {AssetGuard} = require('./assets/js/assetguard.js') const AuthManager = require('./assets/js/authmanager')
const ConfigManager = require('./assets/js/configmanager.js') const ConfigManager = require('./assets/js/configmanager')
const DistroManager = require('./assets/js/distromanager')
let rscShouldLoad = false let rscShouldLoad = false
let fatalStartupError = false let fatalStartupError = false
@ -53,14 +54,14 @@ function getCurrentView(){
return currentView return currentView
} }
function showMainUI(){ function showMainUI(data){
if(!isDev){ if(!isDev){
console.log('%c[AutoUpdater]', 'color: #a02d2a; font-weight: bold', 'Initializing..') console.log('%c[AutoUpdater]', 'color: #a02d2a; font-weight: bold', 'Initializing..')
ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease())
} }
updateSelectedServer(AssetGuard.getServerById(ConfigManager.getSelectedServer()).name) updateSelectedServer(data.getServer(ConfigManager.getSelectedServer()).getName())
refreshServerStatus() refreshServerStatus()
setTimeout(() => { setTimeout(() => {
document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
@ -131,7 +132,7 @@ function showFatalStartupError(){
* @param {Object} data The distro index object. * @param {Object} data The distro index object.
*/ */
function onDistroRefresh(data){ function onDistroRefresh(data){
updateSelectedServer(AssetGuard.getServerById(ConfigManager.getSelectedServer()).name) updateSelectedServer(data.getServer(ConfigManager.getSelectedServer()).getName())
refreshServerStatus() refreshServerStatus()
initNews() initNews()
syncModConfigurations(data) syncModConfigurations(data)
@ -146,28 +147,27 @@ function syncModConfigurations(data){
const syncedCfgs = [] const syncedCfgs = []
const servers = data.servers for(let serv of data.getServers()){
for(let i=0; i<servers.length; i++){ const id = serv.getID()
const mdls = serv.getModules()
const id = servers[i].id const cfg = ConfigManager.getModConfiguration(id)
const mdls = servers[i].modules
const cfg = ConfigManager.getModConfiguration(servers[i].id)
if(cfg != null){ if(cfg != null){
const modsOld = cfg.mods const modsOld = cfg.mods
const mods = {} const mods = {}
for(let j=0; j<mdls.length; j++){ for(let mdl of mdls){
const mdl = mdls[j] const type = mdl.getType()
if(mdl.type === 'forgemod' || mdl.type === 'litemod' || mdl.type === 'liteloader'){
if(mdl.required != null && mdl.required.value != null && mdl.required.value === false){ if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
const mdlID = AssetGuard._resolveWithoutVersion(mdl.id) if(!mdl.getRequired().isRequired()){
const mdlID = mdl.getVersionlessID()
if(modsOld[mdlID] == null){ if(modsOld[mdlID] == null){
mods[mdlID] = scanOptionalSubModules(mdl.sub_modules, mdl) mods[mdlID] = scanOptionalSubModules(mdl.getSubModules(), mdl)
} else { } else {
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.sub_modules, mdl)) mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.getSubModules(), mdl))
} }
} }
} }
@ -182,11 +182,11 @@ function syncModConfigurations(data){
const mods = {} const mods = {}
for(let j=0; j<mdls.length; j++){ for(let mdl of mdls){
const mdl = mdls[j] const type = mdl.getType()
if(mdl.type === 'forgemod' || mdl.type === 'litemod' || mdl.type === 'liteloader'){ if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
if(mdl.required != null && mdl.required.value != null && mdl.required.value === false){ if(!mdl.getRequired().isRequired()){
mods[AssetGuard._resolveWithoutVersion(mdl.id)] = scanOptionalSubModules(mdl.sub_modules, mdl) mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl)
} }
} }
} }
@ -214,25 +214,25 @@ function scanOptionalSubModules(mdls, origin){
if(mdls != null){ if(mdls != null){
const mods = {} const mods = {}
for(let i=0; i<mdls.length; i++){ for(let mdl of mdls){
const mdl = mdls[i] const type = mdl.getType()
// Optional types. // Optional types.
if(mdl.type === 'forgemod' || mdl.type === 'litemod' || mdl.type === 'liteloader'){ if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
// It is optional. // It is optional.
if(mdl.required != null && mdl.required.value != null && mdl.required.value === false){ if(!mdl.getRequired().isRequired()){
mods[AssetGuard._resolveWithoutVersion(mdl.id)] = scanOptionalSubModules(mdl.sub_modules, mdl) mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl)
} }
} }
} }
if(Object.keys(mods).length > 0){ if(Object.keys(mods).length > 0){
return { return {
value: origin.required != null && origin.required.def != null ? origin.required.def : true, value: origin.getRequired().isDefault(),
mods mods
} }
} }
} }
return origin.required != null && origin.required.def != null ? origin.required.def : true return origin.getRequired().isDefault()
} }
/** /**
@ -274,11 +274,11 @@ function mergeModConfiguration(o, n){
function refreshDistributionIndex(remote, onSuccess, onError){ function refreshDistributionIndex(remote, onSuccess, onError){
if(remote){ if(remote){
AssetGuard.refreshDistributionDataRemote(ConfigManager.getLauncherDirectory()) DistroManager.pullRemote()
.then(onSuccess) .then(onSuccess)
.catch(onError) .catch(onError)
} else { } else {
AssetGuard.refreshDistributionDataLocal(ConfigManager.getLauncherDirectory()) DistroManager.pullLocal()
.then(onSuccess) .then(onSuccess)
.catch(onError) .catch(onError)
} }
@ -347,7 +347,8 @@ document.addEventListener('readystatechange', function(){
if (document.readyState === 'complete'){ if (document.readyState === 'complete'){
if(rscShouldLoad){ if(rscShouldLoad){
if(!fatalStartupError){ if(!fatalStartupError){
showMainUI() const data = DistroManager.getDistribution()
showMainUI(data)
} else { } else {
showFatalStartupError() showFatalStartupError()
} }
@ -362,11 +363,12 @@ document.addEventListener('readystatechange', function(){
}, false) }, false)
// Actions that must be performed after the distribution index is downloaded. // Actions that must be performed after the distribution index is downloaded.
ipcRenderer.on('distributionIndexDone', (event, data) => { ipcRenderer.on('distributionIndexDone', (event, res) => {
if(data != null) { if(res) {
const data = DistroManager.getDistribution()
syncModConfigurations(data) syncModConfigurations(data)
if(document.readyState === 'complete'){ if(document.readyState === 'complete'){
showMainUI() showMainUI(data)
} else { } else {
rscShouldLoad = true rscShouldLoad = true
} }

View File

@ -1,122 +1,294 @@
# Documentation of the Launcher Distribution Index # Distribution Index
The distribution index is written in JSON. The general format of the index is as posted below. The distribution index is written in JSON. The general format of the index is as posted below.
```json ```json
{ {
"version": "1.0", "version": "1.0.0",
"discord": { "discord": {
"clientID": 12334567890, "clientId": "12334567890123456789",
"smallImageText": "WesterosCraft", "smallImageText": "WesterosCraft",
"smallImageKey": "seal-circle" "smallImageKey": "seal-circle"
}, },
"rss": "https://westeroscraft.com/articles/index.rss",
"servers": [ "servers": [
{ {
"id": "Example_Server", "id": "Example_Server",
"name": "WesterosCraft Example Client", "name": "WesterosCraft Example Client",
"news_feed": "http://westeroscraft.com/forums/example/index.rss", "description": "Example WesterosCraft server. Connect for fun!",
"icon_url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png", "icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
"revision": "0.0.1", "version": "0.0.1",
"server_ip": "mc.westeroscraft.com:1337", "address": "mc.westeroscraft.com:1337",
"mc_version": "1.11.2", "minecraftVersion": "1.11.2",
"discord": { "discord": {
"shortId": "Example", "shortId": "Example",
"largeImageText": "WesterosCraft Example Server", "largeImageText": "WesterosCraft Example Server",
"largeImageKey": "server-example" "largeImageKey": "server-example"
}, },
"default_selected": true, "mainServer": true,
"autoconnect": true, "autoconnect": true,
"modules": [ "modules": [
... "Module Objects Here"
] ]
} }
] ]
} }
``` ```
You can declare an unlimited number of servers, however you must provide valid values for the fields listed above. In addition to that, the server can declare modules. ## Distro Index Object
The discord settings are to enable the use of Rich Presence on the launcher. For more details, see [discord's documentation](https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields). #### Example
```JSON
Only one server in the array should have the `default_selected` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. This field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `default_selected` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false.
## Modules
A module is a generic representation of a file required to run the minecraft client. It takes the general form:
```json
{ {
"id": "group.id:artifact:version", "version": "1.0.0",
"name": "Artifact {version}", "discord": {
"type": "{a valid type}", "clientId": "12334567890123456789",
"artifact": { "smallImageText": "WesterosCraft",
"size": "{file size in bytes}", "smallImageKey": "seal-circle"
"MD5": "{MD5 hash for the file, string}",
"extension": ".jar",
"url": "http://files.site.com/maven/group/id/artifact/version/artifact-version.jar"
}, },
"sub_modules": [ "rss": "https://westeroscraft.com/articles/index.rss",
"servers": []
}
```
### `DistroIndex.version: string/semver`
The version of the index format. Will be used in the future to gracefully push updates.
### `DistroIndex.discord: object`
Global settings for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
**Properties**
* `discord.clientId: string` - Client ID for th Application registered with Discord.
* `discord.smallImageText: string` - Tootltip for the `smallImageKey`.
* `discord.smallImageKey: string` - Name of the uploaded image for the small profile artwork.
### `DistroIndex.rss: string/url`
A URL to a RSS feed. Used for loading news.
---
## Server Object
#### Example
```JSON
{
"id": "Example_Server",
"name": "WesterosCraft Example Client",
"description": "Example WesterosCraft server. Connect for fun!",
"icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
"version": "0.0.1",
"address": "mc.westeroscraft.com:1337",
"minecraftVersion": "1.11.2",
"discord": {
"shortId": "Example",
"largeImageText": "WesterosCraft Example Server",
"largeImageKey": "server-example"
},
"mainServer": true,
"autoconnect": true,
"modules": []
}
```
### `Server.id: string`
The ID of the server. The launcher saves mod configurations and selected servers by ID. If the ID changes, all data related to the old ID **will be wiped**.
### `Server.name: string`
The name of the server. This is what users see on the UI.
### `Server.description: string`
A brief description of the server. Displayed on the UI to provide users more information.
### `Server.icon: string/url`
A URL to the server's icon. Will be displayed on the UI.
### `Server.version: string/semver`
The version of the server configuration.
### `Server.address: string/url`
The server's IP address.
### `Server.minecraftVersion: string`
The version of minecraft that the server is running.
### `Server.discord: object`
Server specific settings used for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
**Properties**
* `discord.shortId: string` - Short ID for the server. Displayed on the second status line as `Server: shortId`
* `discord.largeImageText: string` - Ttooltip for the `largeImageKey`.
* `discord.largeImageKey: string` - Name of the uploaded image for the large profile artwork.
### `Server.mainServer: boolean`
Only one server in the array should have the `mainServer` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. If this field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `mainServer` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false.
### `Server.autoconnect: boolean`
Whether or not the server can be autoconnected to. If false, the server will not be autoconnected to even when the user has the autoconnect setting enabled.
### `Server.modules: Module[]`
An array of module objects.
---
## Module Object
A module is a generic representation of a file required to run the minecraft client.
#### Example
```JSON
{
"id": "com.example:artifact:1.0.0@jar.pack.xz",
"name": "Artifact 1.0.0",
"type": "Library",
"artifact": {
"size": 4231234,
"MD5": "7f30eefe5c51e1ae0939dab2051db75f",
"url": "http://files.site.com/maven/com/example/artifact/1.0.0/artifact-1.0.0.jar.pack.xz"
},
"subModules": [
{ {
"id": "examplefile", "id": "examplefile",
"name": "Example File", "name": "Example File",
"type": "file", "type": "File",
"artifact": { "artifact": {
"size": "{file size in bytes}", "size": 23423,
"MD5": "{MD5 hash for the file, string}", "MD5": "169a5e6cf30c2cc8649755cdc5d7bad7",
"path": "examplefile.txt", "path": "examplefile.txt",
"url": "http://files.site.com/examplefile.txt" "url": "http://files.site.com/examplefile.txt"
} }
}, }
...
] ]
} }
``` ```
As shown above, modules objects are allowed to declare submodules under the option `sub_modules`. This parameter is completely optional and can be omitted for modules which do not require submodules. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit. The parent module will be stored maven style, it's destination path will be resolved by its id. The sub module has a declared `path`, so that value will be used.
Modules of type `forgemod`, `litemod`, and `liteloader` may also declare a `required` object. ### `Module.id: string`
```json The ID of the module. The best practice for an ID is to use a maven identifier. Modules which are stored maven style use the identifier to resolve the destination path. If the `extension` is not provided, it defaults to `jar`.
"required": {
"value": false, // If the module is required
"def": false // If it's enabled by default, has no effect if value is true
}
```
If a module does not declare this object, both `value` and `def` default to true. Similarly, if a parameter is not included in the `required` object it will default to true. This will be used in the mod selection process down the line. **Template**
`my.group:arifact:version@extension`
`my/group/artifact/version/artifact-version.extension`
**Example**
`net.minecraft:launchwrapper:1.12` OR `net.minecraft:launchwrapper:1.12@jar`
`net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar`
If the module's artifact does not declare the `path` property, its path will be resolved from the ID.
### `Module.name: string`
The name of the module. Used on the UI.
### `Module.type: string`
The type of the module.
### `Module.required: Required`
**OPTIONAL**
Defines whether or not the module is required. If omitted, then the module will be required.
Only applicable for modules of type:
* `ForgeMod`
* `LiteMod`
* `LiteLoader`
### `Module.artifact: Artifact`
The download artifact for the module.
### `Module.subModules: Module[]`
**OPTIONAL**
An array of sub modules declared by this module. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit.
## Artifact Object
The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file. The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file.
Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The `id` becomes more important as it will be used to resolve the final path. The `id` must be provided in maven format, that is `group.id.maybemore:artifact:version`. From there, you need to declare the `extension` of the file in the artifact object. This effectively replaces the `path` option we used above. Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The module `id` will be used to resolve the final path, effectively replacing the `path` property. It must be provided in maven format. More information on this is provided in the documentation for the `id` property.
**It is EXTREMELY IMPORTANT that the file size is CORRECT. The launcher's download queue will not function properly otherwise.** The resolved/provided paths are appended to a base path depending on the module's declared type.
Ex. | Type | Path |
| ---- | ---- |
| `ForgeHosted` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `LiteLoader` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `Library` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `ForgeMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
| `LiteMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
| `File` | ({`instanceDirectory`}/{`Server.id`}/{`path` OR resolved}) |
```SHELL The `commonDirectory` and `instanceDirectory` values are stored in the launcher's config.json.
type = forgemod
id = com.westeroscraft:westerosblocks:1.0.0
extension = .jar
resolved_path = {commonDirectory}/modstore/com/westeroscraft/westerosblocks/1.0.0/westerosblocks-1.0.0.jar ### `Artifact.size: number`
```
The resolved path depends on the type. Currently, there are several recognized module types: The size of the artifact.
- `forge-hosted` ({commonDirectory}/libraries/{path OR resolved}) ### `Artifact.MD5: string`
- `liteloader` ({commonDirectory}/libraries/{path OR resolved})
- `library` ({commonDirectory}/libraries/{path OR resolved}) The MD5 hash of the artifact. This will be used to validate local artifacts.
- `forgemod` ({commonDirectory}/modstore/{path OR resolved})
- `litemod` ({commonDirectory}/modstore/{path OR resolved}) ### `Artifact.path: string`
- `file` ({instanceDirectory}/{serverID}/{path OR resolved})
**OPTIONAL**
A relative path to where the file will be saved. This is appended to the base path for the module's declared type.
If this is not specified, the path will be resolved based on the module's ID.
### `Artifact.url: string/url`
The artifact's download url.
## Required Object
### `Required.value: boolean`
**OPTIONAL**
If the module is required. Defaults to true if this property is omited.
### `Required.def: boolean`
**OPTIONAL**
If the module is enabled by default. Has no effect unless `Required.value` is false. Defaults to true if this property is omited.
--- ---
## Module Types
### forge-hosted ### ForgeHosted
The module type `forge-hosted` represents forge itself. Currently, the launcher only supports forge servers, as vanilla servers can be connected to via the mojang launcher. The `hosted` part is key, this means that the forge module must declare its required libraries as submodules. The module type `ForgeHosted` represents forge itself. Currently, the launcher only supports forge servers, as vanilla servers can be connected to via the mojang launcher. The `Hosted` part is key, this means that the forge module must declare its required libraries as submodules.
Ex. Ex.
@ -124,46 +296,43 @@ Ex.
{ {
"id": "net.minecraftforge:forge:1.11.2-13.20.1.2429", "id": "net.minecraftforge:forge:1.11.2-13.20.1.2429",
"name": "Minecraft Forge 1.11.2-13.20.1.2429", "name": "Minecraft Forge 1.11.2-13.20.1.2429",
"type": "forge-hosted", "type": "ForgeHosted",
"artifact": { "artifact": {
"size": 4450992, "size": 4450992,
"MD5": "3fcc9b0104f0261397d3cc897e55a1c5", "MD5": "3fcc9b0104f0261397d3cc897e55a1c5",
"extension": ".jar",
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar" "url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar"
}, },
"sub_modules": [ "subModules": [
{ {
"id": "net.minecraft:launchwrapper:1.12", "id": "net.minecraft:launchwrapper:1.12",
"name": "Mojang (LaunchWrapper)", "name": "Mojang (LaunchWrapper)",
"type": "library", "type": "Library",
"artifact": { "artifact": {
"size": 32999, "size": 32999,
"MD5": "934b2d91c7c5be4a49577c9e6b40e8da", "MD5": "934b2d91c7c5be4a49577c9e6b40e8da",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar" "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar"
} }
}, }
...
] ]
} }
``` ```
All of forge's required libraries are declared in the `version.json` file found in the root of the forge jar file. These libraries MUST be hosted and declared a submodules or forge will not work. All of forge's required libraries are declared in the `version.json` file found in the root of the forge jar file. These libraries MUST be hosted and declared a submodules or forge will not work.
There were plans to add a `forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented. There were plans to add a `Forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented.
--- ---
### liteloader ### LiteLoader
The module type `liteloader` represents liteloader. It is handled as a library and added to the classpath at runtime. Special launch conditions are executed when liteloader is present and enabled. This module can be optional and toggled similarly to `forgemod` and `litemod` modules. The module type `LiteLoader` represents liteloader. It is handled as a library and added to the classpath at runtime. Special launch conditions are executed when liteloader is present and enabled. This module can be optional and toggled similarly to `ForgeMod` and `Litemod` modules.
Ex. Ex.
```json ```json
{ {
"id": "com.mumfrey:liteloader:1.11.2", "id": "com.mumfrey:liteloader:1.11.2",
"name": "Liteloader (1.11.2)", "name": "Liteloader (1.11.2)",
"type": "liteloader", "type": "LiteLoader",
"required": { "required": {
"value": false, "value": false,
"def": false "def": false
@ -171,20 +340,19 @@ Ex.
"artifact": { "artifact": {
"size": 1685422, "size": 1685422,
"MD5": "3a98b5ed95810bf164e71c1a53be568d", "MD5": "3a98b5ed95810bf164e71c1a53be568d",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/liteloader-1.11.2.jar" "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/liteloader-1.11.2.jar"
}, },
"sub_modules": [ "subModules": [
// All litemods should be declared as submodules. "All LiteMods go here"
] ]
} }
``` ```
--- ---
### library ### Library
The module type `library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process. The module type `Library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process.
Ex. Ex.
@ -192,11 +360,10 @@ Ex.
{ {
"id": "net.sf.jopt-simple:jopt-simple:4.6", "id": "net.sf.jopt-simple:jopt-simple:4.6",
"name": "Jopt-simple 4.6", "name": "Jopt-simple 4.6",
"type": "library", "type": "Library",
"artifact": { "artifact": {
"size": 62477, "size": 62477,
"MD5": "13560a58a79b46b82057686543e8d727", "MD5": "13560a58a79b46b82057686543e8d727",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar" "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar"
} }
} }
@ -204,20 +371,19 @@ Ex.
--- ---
### forgemod ### ForgeMod
The module type `forgemod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). The module type `ForgeMod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format).
Ex. Ex.
```json ```json
{ {
"id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133", "id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133",
"name": "WesterosBlocks (3.0.0-beta-6-133)", "name": "WesterosBlocks (3.0.0-beta-6-133)",
"type": "forgemod", "type": "ForgeMod",
"artifact": { "artifact": {
"size": 16321712, "size": 16321712,
"MD5": "5a89e2ab18916c18965fc93a0766cc6e", "MD5": "5a89e2ab18916c18965fc93a0766cc6e",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar" "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar"
} }
} }
@ -225,16 +391,16 @@ Ex.
--- ---
### litemod ### LiteMod
The module type `litemod` represents a mod loaded by liteloader. These files are stored maven-style and passed to liteloader using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). Documentation for liteloader's implementation of this can be found on [this issue](http://develop.liteloader.com/liteloader/LiteLoader/issues/34). The module type `LiteMod` represents a mod loaded by liteloader. These files are stored maven-style and passed to liteloader using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). Documentation for liteloader's implementation of this can be found on [this issue](http://develop.liteloader.com/liteloader/LiteLoader/issues/34).
Ex. Ex.
```json ```json
{ {
"id": "com.mumfrey:macrokeybindmod:0.14.4-1.11.2", "id": "com.mumfrey:macrokeybindmod:0.14.4-1.11.2@litemod",
"name": "Macro/Keybind Mod (0.14.4-1.11.2)", "name": "Macro/Keybind Mod (0.14.4-1.11.2)",
"type": "litemod", "type": "LiteMod",
"required": { "required": {
"value": false, "value": false,
"def": false "def": false
@ -242,7 +408,6 @@ Ex.
"artifact": { "artifact": {
"size": 1670811, "size": 1670811,
"MD5": "16080785577b391d426c62c8d3138558", "MD5": "16080785577b391d426c62c8d3138558",
"extension": ".litemod",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/macrokeybindmod.litemod" "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/macrokeybindmod.litemod"
} }
} }
@ -250,7 +415,7 @@ Ex.
--- ---
### file ### File
The module type `file` represents a generic file required by the client, another module, etc. These files are stored in the server's instance directory. The module type `file` represents a generic file required by the client, another module, etc. These files are stored in the server's instance directory.
@ -269,7 +434,3 @@ Ex.
} }
} }
``` ```
---
This format is actively under development and is likely to change.

View File

@ -71,8 +71,8 @@ ipcMain.on('autoUpdateAction', (event, arg, data) => {
} }
}) })
// Redirect distribution index event from preloader to renderer. // Redirect distribution index event from preloader to renderer.
ipcMain.on('distributionIndexDone', (event, data) => { ipcMain.on('distributionIndexDone', (event, res) => {
event.sender.send('distributionIndexDone', data) event.sender.send('distributionIndexDone', res)
}) })
// Disable hardware acceleration. // Disable hardware acceleration.

51
package-lock.json generated
View File

@ -662,31 +662,12 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
}, },
"discord-rpc": { "discord-rpc": {
"version": "3.0.0-beta.11", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/discord-rpc/-/discord-rpc-3.0.0-beta.11.tgz", "resolved": "https://registry.npmjs.org/discord-rpc/-/discord-rpc-3.0.0.tgz",
"integrity": "sha512-0KtAOjvK9g7sRzTvPvWm6LZTBqfRAfXhFib930YDxbVqX2CGYAWfaKuRiwtPFQVV5fJPIfRfbrwlElmmqC/L9w==", "integrity": "sha512-xAJ9OSBT6AsjLgGpVkZ47SdmwS8F+RdWhnVYJbeB7Ol0vsYOGnXvk3XdsvKhw2z9Lf3LGcGIm/zRuXrtxHLNzQ==",
"requires": { "requires": {
"discord.js": "github:discordjs/discord.js#2694c0d442a008a1e40c17df147a22bc9534049a",
"snekfetch": "^3.5.8"
}
},
"discord.js": {
"version": "github:discordjs/discord.js#2694c0d442a008a1e40c17df147a22bc9534049a",
"from": "github:discordjs/discord.js",
"requires": {
"form-data": "^2.3.2",
"node-fetch": "^2.1.2", "node-fetch": "^2.1.2",
"pako": "^1.0.0", "ws": "^5.2.1"
"prism-media": "^0.3.0",
"tweetnacl": "^1.0.0",
"ws": "^4.0.0"
},
"dependencies": {
"tweetnacl": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz",
"integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins="
}
} }
}, },
"dmg-builder": { "dmg-builder": {
@ -1852,11 +1833,6 @@
"semver": "^5.1.0" "semver": "^5.1.0"
} }
}, },
"pako": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
"integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg=="
},
"parse-color": { "parse-color": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz",
@ -1993,11 +1969,6 @@
"meow": "^3.1.0" "meow": "^3.1.0"
} }
}, },
"prism-media": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.3.1.tgz",
"integrity": "sha512-ZAzpXm6n7IaMDrcm7gB6wEkhF796cFLBZPY91rse5DKsASrZZgo36y9QC4+FnlbWt14aQSZUnKMHnkg6pEDfiQ=="
},
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
@ -2286,11 +2257,6 @@
"string-width": "^1.0.1" "string-width": "^1.0.1"
} }
}, },
"snekfetch": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz",
"integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw=="
},
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -2783,12 +2749,11 @@
} }
}, },
"ws": { "ws": {
"version": "4.1.0", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"requires": { "requires": {
"async-limiter": "~1.0.0", "async-limiter": "~1.0.0"
"safe-buffer": "~5.1.0"
} }
}, },
"xdg-basedir": { "xdg-basedir": {

View File

@ -30,7 +30,7 @@
"dependencies": { "dependencies": {
"adm-zip": "^0.4.11", "adm-zip": "^0.4.11",
"async": "^2.6.1", "async": "^2.6.1",
"discord-rpc": "=3.0.0-beta.11", "discord-rpc": "^3.0.0",
"ejs": "^2.6.1", "ejs": "^2.6.1",
"ejs-electron": "^2.0.3", "ejs-electron": "^2.0.3",
"electron-is-dev": "^0.3.0", "electron-is-dev": "^0.3.0",