const fs = require('fs-extra') const { LoggerUtil } = require('helios-core') const os = require('os') const path = require('path') const logger = LoggerUtil.getLogger('ConfigManager') const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) // TODO change const dataPath = path.join(sysRoot, '.helioslauncher') // Forked processes do not have access to electron, so we have this workaround. const launcherDir = process.env.CONFIG_DIRECT_PATH || require('@electron/remote').app.getPath('userData') /** * Retrieve the absolute path of the launcher directory. * * @returns {string} The absolute path of the launcher directory. */ exports.getLauncherDirectory = function(){ return launcherDir } /** * Get the launcher's data directory. This is where all files related * to game launch are installed (common, instances, java, etc). * * @returns {string} The absolute path of the launcher's data directory. */ exports.getDataDirectory = function(def = false){ return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory } /** * Set the new data directory. * * @param {string} dataDirectory The new data directory. */ exports.setDataDirectory = function(dataDirectory){ config.settings.launcher.dataDirectory = dataDirectory } const configPath = path.join(exports.getLauncherDirectory(), 'config.json') const configPathLEGACY = path.join(dataPath, 'config.json') const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) exports.getAbsoluteMinRAM = function(){ const mem = os.totalmem() return mem >= 6000000000 ? 3 : 2 } exports.getAbsoluteMaxRAM = function(){ const mem = os.totalmem() const gT16 = mem-16000000000 return Math.floor((mem-1000000000-(gT16 > 0 ? (Number.parseInt(gT16/8) + 16000000000/4) : mem/4))/1000000000) } function resolveMaxRAM(){ const mem = os.totalmem() return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') } function resolveMinRAM(){ return resolveMaxRAM() } /** * TODO Copy pasted, should be in a utility file. * * Returns true if the actual version is greater than * or equal to the desired version. * * @param {string} desired The desired version. * @param {string} actual The actual version. */ function mcVersionAtLeast(desired, actual){ const des = desired.split('.') const act = actual.split('.') for(let i=0; i= parseInt(des[i]))){ return false } } return true } /** * Three types of values: * Static = Explicitly declared. * Dynamic = Calculated by a private function. * Resolved = Resolved externally, defaults to null. */ const DEFAULT_CONFIG = { settings: { game: { resWidth: 1280, resHeight: 720, fullscreen: false, autoConnect: true, launchDetached: true }, launcher: { allowPrerelease: false, dataDirectory: dataPath } }, newsCache: { date: null, content: null, dismissed: false }, clientToken: null, selectedServer: null, // Resolved selectedAccount: null, authenticationDatabase: {}, modConfigurations: [], javaConfig: {} } let config = null // Persistance Utility Functions /** * Save the current configuration to a file. */ exports.save = function(){ fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8') } /** * Load the configuration into memory. If a configuration file exists, * that will be read and saved. Otherwise, a default configuration will * be generated. Note that "resolved" values default to null and will * need to be externally assigned. */ exports.load = function(){ let doLoad = true if(!fs.existsSync(configPath)){ // Create all parent directories. fs.ensureDirSync(path.join(configPath, '..')) if(fs.existsSync(configPathLEGACY)){ fs.moveSync(configPathLEGACY, configPath) } else { doLoad = false config = DEFAULT_CONFIG exports.save() } } if(doLoad){ let doValidate = false try { config = JSON.parse(fs.readFileSync(configPath, 'UTF-8')) doValidate = true } catch (err){ logger.error(err) logger.info('Configuration file contains malformed JSON or is corrupt.') logger.info('Generating a new configuration file.') fs.ensureDirSync(path.join(configPath, '..')) config = DEFAULT_CONFIG exports.save() } if(doValidate){ config = validateKeySet(DEFAULT_CONFIG, config) exports.save() } } logger.info('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 * present in the source object. Assign a default value otherwise. * * @param {Object} srcObj The source object to reference against. * @param {Object} destObj The destination object. * @returns {Object} A validated destination object. */ function validateKeySet(srcObj, destObj){ if(srcObj == null){ srcObj = {} } const validationBlacklist = ['authenticationDatabase', 'javaConfig'] const keys = Object.keys(srcObj) for(let i=0; i} An array of each stored authenticated account. */ exports.getAuthAccounts = function(){ return config.authenticationDatabase } /** * Returns the authenticated account with the given uuid. Value may * be null. * * @param {string} uuid The uuid of the authenticated account. * @returns {Object} The authenticated account with the given uuid. */ exports.getAuthAccount = function(uuid){ return config.authenticationDatabase[uuid] } /** * Update the access token of an authenticated mojang account. * * @param {string} uuid The uuid of the authenticated account. * @param {string} accessToken The new Access Token. * * @returns {Object} The authenticated account object created by this action. */ exports.updateMojangAuthAccount = function(uuid, accessToken){ config.authenticationDatabase[uuid].accessToken = accessToken config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion. return config.authenticationDatabase[uuid] } /** * Adds an authenticated mojang account to the database to be stored. * * @param {string} uuid The uuid of the authenticated account. * @param {string} accessToken The accessToken of the authenticated account. * @param {string} username The username (usually email) of the authenticated account. * @param {string} displayName The in game name of the authenticated account. * * @returns {Object} The authenticated account object created by this action. */ exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){ config.selectedAccount = uuid config.authenticationDatabase[uuid] = { type: 'mojang', accessToken, username: username.trim(), uuid: uuid.trim(), displayName: displayName.trim() } return config.authenticationDatabase[uuid] } /** * Update the tokens of an authenticated microsoft account. * * @param {string} uuid The uuid of the authenticated account. * @param {string} accessToken The new Access Token. * @param {string} msAccessToken The new Microsoft Access Token * @param {string} msRefreshToken The new Microsoft Refresh Token * @param {date} msExpires The date when the microsoft access token expires * @param {date} mcExpires The date when the mojang access token expires * * @returns {Object} The authenticated account object created by this action. */ exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) { config.authenticationDatabase[uuid].accessToken = accessToken config.authenticationDatabase[uuid].expiresAt = mcExpires config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken config.authenticationDatabase[uuid].microsoft.expires_at = msExpires return config.authenticationDatabase[uuid] } /** * Adds an authenticated microsoft account to the database to be stored. * * @param {string} uuid The uuid of the authenticated account. * @param {string} accessToken The accessToken of the authenticated account. * @param {string} name The in game name of the authenticated account. * @param {date} mcExpires The date when the mojang access token expires * @param {string} msAccessToken The microsoft access token * @param {string} msRefreshToken The microsoft refresh token * @param {date} msExpires The date when the microsoft access token expires * * @returns {Object} The authenticated account object created by this action. */ exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) { config.selectedAccount = uuid config.authenticationDatabase[uuid] = { type: 'microsoft', accessToken, username: name.trim(), uuid: uuid.trim(), displayName: name.trim(), expiresAt: mcExpires, microsoft: { access_token: msAccessToken, refresh_token: msRefreshToken, expires_at: msExpires } } return config.authenticationDatabase[uuid] } /** * Remove an authenticated account from the database. If the account * was also the selected account, a new one will be selected. If there * are no accounts, the selected account will be null. * * @param {string} uuid The uuid of the authenticated account. * * @returns {boolean} True if the account was removed, false if it never existed. */ exports.removeAuthAccount = function(uuid){ if(config.authenticationDatabase[uuid] != null){ delete config.authenticationDatabase[uuid] if(config.selectedAccount === uuid){ const keys = Object.keys(config.authenticationDatabase) if(keys.length > 0){ config.selectedAccount = keys[0] } else { config.selectedAccount = null config.clientToken = null } } return true } return false } /** * Get the currently selected authenticated account. * * @returns {Object} The selected authenticated account. */ exports.getSelectedAccount = function(){ return config.authenticationDatabase[config.selectedAccount] } /** * Set the selected authenticated account. * * @param {string} uuid The UUID of the account which is to be set * as the selected account. * * @returns {Object} The selected authenticated account. */ exports.setSelectedAccount = function(uuid){ const authAcc = config.authenticationDatabase[uuid] if(authAcc != null) { config.selectedAccount = uuid } return authAcc } /** * Get an array of each mod configuration currently stored. * * @returns {Array.} An array of each stored mod configuration. */ exports.getModConfigurations = function(){ return config.modConfigurations } /** * Set the array of stored mod configurations. * * @param {Array.} configurations An array of mod configurations. */ exports.setModConfigurations = function(configurations){ config.modConfigurations = configurations } /** * Get the mod configuration for a specific server. * * @param {string} serverid The id of the server. * @returns {Object} The mod configuration for the given server. */ exports.getModConfiguration = function(serverid){ const cfgs = config.modConfigurations for(let i=0; i} An array of the additional arguments for JVM initialization. */ exports.getJVMOptions = function(serverid){ return config.javaConfig[serverid].jvmOptions } /** * Set the additional arguments for JVM initialization. Required arguments, * such as memory allocation, will be dynamically resolved and should not be * included in this value. * * @param {string} serverid The server id. * @param {Array.} jvmOptions An array of the new additional arguments for JVM * initialization. */ exports.setJVMOptions = function(serverid, jvmOptions){ config.javaConfig[serverid].jvmOptions = jvmOptions } // Game Settings /** * Retrieve the width of the game window. * * @param {boolean} def Optional. If true, the default value will be returned. * @returns {number} The width of the game window. */ exports.getGameWidth = function(def = false){ return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth } /** * Set the width of the game window. * * @param {number} resWidth The new width of the game window. */ exports.setGameWidth = function(resWidth){ config.settings.game.resWidth = Number.parseInt(resWidth) } /** * Validate a potential new width value. * * @param {number} resWidth The width value to validate. * @returns {boolean} Whether or not the value is valid. */ exports.validateGameWidth = function(resWidth){ const nVal = Number.parseInt(resWidth) return Number.isInteger(nVal) && nVal >= 0 } /** * Retrieve the height of the game window. * * @param {boolean} def Optional. If true, the default value will be returned. * @returns {number} The height of the game window. */ exports.getGameHeight = function(def = false){ return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight } /** * Set the height of the game window. * * @param {number} resHeight The new height of the game window. */ exports.setGameHeight = function(resHeight){ config.settings.game.resHeight = Number.parseInt(resHeight) } /** * Validate a potential new height value. * * @param {number} resHeight The height value to validate. * @returns {boolean} Whether or not the value is valid. */ exports.validateGameHeight = function(resHeight){ const nVal = Number.parseInt(resHeight) return Number.isInteger(nVal) && nVal >= 0 } /** * Check if the game should be launched in fullscreen mode. * * @param {boolean} def Optional. If true, the default value will be returned. * @returns {boolean} Whether or not the game is set to launch in fullscreen mode. */ exports.getFullscreen = function(def = false){ return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen } /** * Change the status of if the game should be launched in fullscreen mode. * * @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode. */ exports.setFullscreen = function(fullscreen){ config.settings.game.fullscreen = fullscreen } /** * Check if the game should auto connect to servers. * * @param {boolean} def Optional. If true, the default value will be returned. * @returns {boolean} Whether or not the game should auto connect to servers. */ exports.getAutoConnect = function(def = false){ return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect } /** * Change the status of whether or not the game should auto connect to servers. * * @param {boolean} autoConnect Whether or not the game should auto connect to servers. */ exports.setAutoConnect = function(autoConnect){ config.settings.game.autoConnect = autoConnect } /** * Check if the game should launch as a detached process. * * @param {boolean} def Optional. If true, the default value will be returned. * @returns {boolean} Whether or not the game will launch as a detached process. */ exports.getLaunchDetached = function(def = false){ return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached } /** * Change the status of whether or not the game should launch as a detached process. * * @param {boolean} launchDetached Whether or not the game should launch as a detached process. */ exports.setLaunchDetached = function(launchDetached){ config.settings.game.launchDetached = launchDetached } // Launcher Settings /** * Check if the launcher should download prerelease versions. * * @param {boolean} def Optional. If true, the default value will be returned. * @returns {boolean} Whether or not the launcher should download prerelease versions. */ exports.getAllowPrerelease = function(def = false){ return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease } /** * Change the status of Whether or not the launcher should download prerelease versions. * * @param {boolean} launchDetached Whether or not the launcher should download prerelease versions. */ exports.setAllowPrerelease = function(allowPrerelease){ config.settings.launcher.allowPrerelease = allowPrerelease }