const fs = require('fs') const path = require('path') const request = require('request') const ConfigManager = require('./configmanager') const logger = require('./loggerutil')('%c[DistroManager]', 'color: #a02d2a; font-weight: bold') /** * 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.classpath, 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, classpath, required, artifact, subModules, serverid) { this.identifier = id this.type = type this.classpath = classpath 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.artifactClassifier = m1[3] || undefined this.artifactVersion = m1[2] || '???' this.artifactID = m1[1] || '???' this.artifactGroup = m1[0] || '???' } catch (err) { // Improper identifier logger.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.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${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.VersionManifest: this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`) 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 } /** * @returns {string} The identifier without he version or extension. */ getVersionlessID(){ return this.getGroup() + ':' + this.getID() } /** * @returns {string} The identifier without the extension. */ getExtensionlessID(){ return this.getIdentifier().split('@')[0] } /** * @returns {string} The version of this module's artifact. */ getVersion(){ return this.artifactVersion } /** * @returns {string} The classifier of this module's artifact */ getClassifier(){ return this.artifactClassifier } /** * @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.} An array of sub modules. */ getSubModules(){ return this.subModules } /** * @returns {string} The type of the module. */ getType(){ return this.type } /** * @returns {boolean} Whether or not this library should be on the classpath. */ getClasspath(){ return this.classpath ?? true } } 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.} 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.} 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 this.mainServer != null ? this.getServer(this.mainServer) : null } } exports.DistroIndex exports.Types = { Library: 'Library', ForgeHosted: 'ForgeHosted', Forge: 'Forge', // Unimplemented LiteLoader: 'LiteLoader', ForgeMod: 'ForgeMod', LiteMod: 'LiteMod', File: 'File', VersionManifest: 'VersionManifest' } 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.} */ 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){ try { data = DistroIndex.fromJSON(JSON.parse(body)) } catch (e) { reject(e) return } fs.writeFile(distroDest, body, 'utf-8', (err) => { if(!err){ resolve(data) return } else { reject(err) return } }) } else { reject(error) return } }) }) } /** * @returns {Promise.} */ 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) return } else { reject(err) return } }) }) } exports.setDevMode = function(value){ if(value){ logger.log('Developer mode enabled.') logger.log('If you don\'t know what that means, revert immediately.') } else { logger.log('Developer mode disabled.') } DEV_MODE = value } exports.isDevMode = function(){ return DEV_MODE } /** * @returns {DistroIndex} */ exports.getDistribution = function(){ return data }