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
19 changed files with 1611 additions and 1207 deletions

View File

@@ -12,13 +12,6 @@
* 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.
*
* 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
*/
// Requirements
@@ -36,6 +29,9 @@ const request = require('request')
const tar = require('tar-fs')
const zlib = require('zlib')
const ConfigManager = require('./configmanager')
const DistroManager = require('./distromanager')
// Constants
const PLATFORM_MAP = {
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
* 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.
*
* @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
* to finalize installation.
* @param {string} instancePath The path to the instances directory.
*/
constructor(commonPath, launcherPath, javaexec, instancePath){
constructor(commonPath, javaexec){
super()
this.totaldlsize = 0
this.progress = 0
@@ -196,73 +187,12 @@ class AssetGuard extends EventEmitter {
this.java = new DLTracker([], 0)
this.extractQueue = []
this.commonPath = commonPath
this.launcherPath = launcherPath
this.javaexec = javaexec
this.instancePath = instancePath
}
// Static Utility Functions
// #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
// #region
@@ -386,145 +316,6 @@ class AssetGuard extends EventEmitter {
// #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
// #region
@@ -538,8 +329,6 @@ class AssetGuard extends EventEmitter {
console.log('[PackXZExtract] Starting')
return new Promise((resolve, reject) => {
let libPath
if(isDev){
libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
@@ -1320,11 +1109,11 @@ class AssetGuard extends EventEmitter {
//const objKeys = Object.keys(data.objects)
async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => {
acc++
self.emit('assetVal', {acc, total})
self.emit('progress', 'assets', acc, total)
const hash = value.hash
const assetName = path.join(hash.substring(0, 2), hash)
const urlName = hash.substring(0, 2) + "/" + hash
const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName))
const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName))
if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){
dlSize += (ast.size*1)
assetDlQueue.push(ast)
@@ -1461,30 +1250,22 @@ class AssetGuard extends EventEmitter {
/**
* 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.
*/
validateDistribution(serverpackid){
validateDistribution(server){
const self = this
return new Promise((resolve, reject) => {
AssetGuard.refreshDistributionDataLocal(self.launcherPath).then((v) => {
const serv = AssetGuard.getServerById(serverpackid)
if(serv == null) {
console.error('Invalid server pack id:', serverpackid)
self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID())
// Correct our workaround here.
let decompressqueue = self.forge.callback
self.extractQueue = decompressqueue
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)
// 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)
})
}
resolve(server)
})
}
@@ -1492,27 +1273,11 @@ class AssetGuard extends EventEmitter {
let alist = []
let asize = 0;
let decompressqueue = []
for(let i=0; i<modules.length; i++){
let ob = modules[i]
let obType = ob.type
let obArtifact = ob.artifact
let obPath = obArtifact.path == null ? AssetGuard._resolvePath(ob.id, obArtifact.extension) : obArtifact.path
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)
for(let ob of modules){
let obType = ob.getType
let obArtifact = ob.getArtifact()
let obPath = obArtifact.getPath()
let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, obType)
const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath
if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){
asize += artifact.size*1
@@ -1520,8 +1285,8 @@ class AssetGuard extends EventEmitter {
if(validationPath !== obPath) decompressqueue.push(obPath)
}
//Recursively process the submodules then combine the results.
if(ob.sub_modules != null){
let dltrack = this._parseDistroModules(ob.sub_modules, version, servid)
if(ob.getSubModules() != null){
let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid)
asize += dltrack.dlsize*1
alist = alist.concat(dltrack.dlqueue)
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.
*
* @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.
*/
loadForgeData(serverpack){
loadForgeData(server){
const self = this
return new Promise(async (resolve, reject) => {
let distro = AssetGuard.getDistributionData()
const servers = distro.servers
let serv = null
for(let i=0; i<servers.length; i++){
if(servers[i].id === serverpack){
serv = servers[i]
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)
const modules = server.getModules()
for(let ob of modules){
const type = ob.getType()
if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){
let obArtifact = ob.getArtifact()
let obPath = obArtifact.getPath()
let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type)
let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath)
resolve(forgeData)
return
@@ -1602,7 +1356,7 @@ class AssetGuard extends EventEmitter {
dataDir = path.join(dataDir, 'runtime', 'x64')
const name = combined.substring(combined.lastIndexOf('/')+1)
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) => {
let h = null
fs.createReadStream(a.to)
@@ -1626,7 +1380,7 @@ class AssetGuard extends EventEmitter {
h = h.substring(0, h.indexOf('/'))
}
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.
*/
startAsyncProcess(identifier, limit = 5){
const self = this
let acc = 0
const concurrentDlTracker = this[identifier]
const concurrentDlQueue = concurrentDlTracker.dlqueue.slice(0)
if(concurrentDlQueue.length === 0){
return false
} else {
console.log('DLQueue', concurrentDlQueue)
async.eachLimit(concurrentDlQueue, limit, (asset, cb) => {
let count = 0;
mkpath.sync(path.join(asset.to, ".."))
const dlTracker = this[identifier]
const dlQueue = dlTracker.dlqueue
if(dlQueue.length > 0){
console.log('DLQueue', dlQueue)
async.eachLimit(dlQueue, limit, (asset, cb) => {
mkpath.sync(path.join(asset.to, '..'))
let req = request(asset.from)
req.pause()
req.on('response', (resp) => {
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)
writeStream.on('close', () => {
//console.log('DLResults ' + asset.size + ' ' + count + ' ', asset.size === count)
if(concurrentDlTracker.callback != null){
concurrentDlTracker.callback.apply(concurrentDlTracker, [asset, self])
if(dlTracker.callback != null){
dlTracker.callback.apply(dlTracker, [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()
})
req.pipe(writeStream)
req.resume()
} else {
req.abort()
const realFrom = typeof asset.from === 'object' ? asset.from.url : asset.from
console.log('Failed to download ' + realFrom + '. Response code', resp.statusCode)
console.log(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`)
self.progress += asset.size*1
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
self.emit('progress', 'download', self.progress, self.totaldlsize)
cb()
}
})
req.on('error', (err) => {
self.emit('dlerror', err)
self.emit('error', 'download', err)
})
req.on('data', (chunk) => {
count += chunk.length
self.progress += chunk.length
acc += chunk.length
self.emit(identifier + 'dlprogress', acc)
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
self.emit('progress', 'download', self.progress, self.totaldlsize)
})
}, (err) => {
if(err){
self.emit(identifier + 'dlerror')
console.log('An item in ' + identifier + ' failed to process');
} else {
self.emit(identifier + 'dlcomplete')
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)
if(self.totaldlsize === 0) {
if(self.progress >= self.totaldlsize) {
if(self.extractQueue.length > 0){
self.emit('extracting')
self.emit('progress', 'extract', 1, 1)
//self.emit('extracting')
AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => {
self.extractQueue = []
self.emit('dlcomplete')
self.emit('complete', 'download')
})
} else {
self.emit('dlcomplete')
self.emit('complete', 'download')
}
}
})
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
* 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
* 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.
*
* @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}]){
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.
this.totaldlsize = 0
this.progress = 0
for(let i=0; i<identifiers.length; i++){
this.totaldlsize += this[identifiers[i].id].dlsize
for(let iden of identifiers){
this.totaldlsize += this[iden.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