import { LoggerUtil } from './loggerutil' import { join } from 'path' import { pathExistsSync, writeFileSync, ensureDirSync, moveSync, readFileSync } from 'fs-extra' import { totalmem } from 'os' import { SavedAccount } from './model/internal/config/SavedAccount' import { LauncherConfig } from './model/internal/config/LauncherConfig' import { ModConfig } from './model/internal/config/ModConfig' import { NewsCache } from './model/internal/config/NewsCache' export class ConfigManager { private static readonly logger = new LoggerUtil('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold') private static readonly sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) // TODO change private static readonly dataPath = join(ConfigManager.sysRoot as string, '.westeroscraft') // Forked processes do not have access to electron, so we have this workaround. private static readonly 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. */ public static getLauncherDirectory(){ return ConfigManager.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. */ public static getDataDirectory(def = false){ return !def ? ConfigManager.config.settings.launcher.dataDirectory : ConfigManager.DEFAULT_CONFIG.settings.launcher.dataDirectory } /** * Set the new data directory. * * @param {string} dataDirectory The new data directory. */ public static setDataDirectory(dataDirectory: string){ ConfigManager.config.settings.launcher.dataDirectory = dataDirectory } private static readonly configPath = join(ConfigManager.getLauncherDirectory(), 'config.json') private static readonly configPathLEGACY = join(ConfigManager.dataPath, 'config.json') // TODO remove, it's been 1 year. private static readonly firstLaunch = !pathExistsSync(ConfigManager.configPath) && !pathExistsSync(ConfigManager.configPathLEGACY) /** * Three types of values: * Static = Explicitly declared. * Dynamic = Calculated by a private function. * Resolved = Resolved externally, defaults to null. */ private static readonly DEFAULT_CONFIG: LauncherConfig = { settings: { java: { minRAM: ConfigManager.resolveMinRAM(), maxRAM: ConfigManager.resolveMaxRAM(), // Dynamic executable: null, jvmOptions: [ '-XX:+UseConcMarkSweepGC', '-XX:+CMSIncrementalMode', '-XX:-UseAdaptiveSizePolicy', '-Xmn128M' ] }, game: { resWidth: 1280, resHeight: 720, fullscreen: false, autoConnect: true, launchDetached: true }, launcher: { allowPrerelease: false, dataDirectory: ConfigManager.dataPath } }, newsCache: { date: null, content: null, dismissed: false }, clientToken: null, selectedServer: null, // Resolved selectedAccount: null, authenticationDatabase: {}, modConfigurations: [] } private static config: LauncherConfig = null as unknown as LauncherConfig public static getAbsoluteMinRAM(){ const mem = totalmem() return mem >= 6000000000 ? 3 : 2 } public static getAbsoluteMaxRAM(){ const mem = totalmem() const gT16 = mem-16000000000 return Math.floor((mem-1000000000-(gT16 > 0 ? (Number.parseInt(gT16/8 as unknown as string) + 16000000000/4) : mem/4))/1000000000) } private static resolveMaxRAM(){ const mem = totalmem() return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') } private static resolveMinRAM(){ return ConfigManager.resolveMaxRAM() } // Persistance Utility Functions /** * Save the current configuration to a file. */ public static save(){ writeFileSync(ConfigManager.configPath, JSON.stringify(ConfigManager.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. */ public static load(){ let doLoad = true if(!pathExistsSync(ConfigManager.configPath)){ // Create all parent directories. ensureDirSync(join(ConfigManager.configPath, '..')) if(pathExistsSync(ConfigManager.configPathLEGACY)){ moveSync(ConfigManager.configPathLEGACY, ConfigManager.configPath) } else { doLoad = false ConfigManager.config = ConfigManager.DEFAULT_CONFIG ConfigManager.save() } } if(doLoad){ let doValidate = false try { ConfigManager.config = JSON.parse(readFileSync(ConfigManager.configPath, 'UTF-8')) doValidate = true } catch (err){ ConfigManager.logger.error(err) ConfigManager.logger.log('Configuration file contains malformed JSON or is corrupt.') ConfigManager.logger.log('Generating a new configuration file.') ensureDirSync(join(ConfigManager.configPath, '..')) ConfigManager.config = ConfigManager.DEFAULT_CONFIG ConfigManager.save() } if(doValidate){ ConfigManager.config = ConfigManager.validateKeySet(ConfigManager.DEFAULT_CONFIG, ConfigManager.config) ConfigManager.save() } } ConfigManager.logger.log('Successfully Loaded') } /** * @returns {boolean} Whether or not the manager has been loaded. */ public static isLoaded(): boolean { return ConfigManager.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. */ private static validateKeySet(srcObj: any, destObj: any){ if(srcObj == null){ srcObj = {} } const validationBlacklist = ['authenticationDatabase'] const keys = Object.keys(srcObj) for(let i=0; i} An array of each stored authenticated account. */ public static getAuthAccounts(): {[uuid: string]: SavedAccount} { return ConfigManager.config.authenticationDatabase } /** * Returns the authenticated account with the given uuid. Value may * be null. * * @param {string} uuid The uuid of the authenticated account. * @returns {SavedAccount} The authenticated account with the given uuid. */ public static getAuthAccount(uuid: string): SavedAccount { return ConfigManager.config.authenticationDatabase[uuid] } /** * Update the access token of an authenticated account. * * @param {string} uuid The uuid of the authenticated account. * @param {string} accessToken The new Access Token. * * @returns {SavedAccount} The authenticated account object created by this action. */ public static updateAuthAccount(uuid: string, accessToken: string): SavedAccount { ConfigManager.config.authenticationDatabase[uuid].accessToken = accessToken return ConfigManager.config.authenticationDatabase[uuid] } /** * Adds an authenticated 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 {SavedAccount} The authenticated account object created by this action. */ public static addAuthAccount( uuid: string, accessToken: string, username: string, displayName: string ): SavedAccount { ConfigManager.config.selectedAccount = uuid ConfigManager.config.authenticationDatabase[uuid] = { accessToken, username: username.trim(), uuid: uuid.trim(), displayName: displayName.trim() } return ConfigManager.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. */ public static removeAuthAccount(uuid: string): boolean { if(ConfigManager.config.authenticationDatabase[uuid] != null){ delete ConfigManager.config.authenticationDatabase[uuid] if(ConfigManager.config.selectedAccount === uuid){ const keys = Object.keys(ConfigManager.config.authenticationDatabase) if(keys.length > 0){ ConfigManager.config.selectedAccount = keys[0] } else { ConfigManager.config.selectedAccount = null ConfigManager.config.clientToken = null } } return true } return false } /** * Get the currently selected authenticated account. * * @returns {SavedAccount | null} The selected authenticated account. */ public static getSelectedAccount(): SavedAccount | null { return ConfigManager.config.selectedAccount == null ? null : ConfigManager.config.authenticationDatabase[ConfigManager.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 {SavedAccount} The selected authenticated account. */ public static setSelectedAccount(uuid: string): SavedAccount { const authAcc = ConfigManager.config.authenticationDatabase[uuid] if(authAcc != null) { ConfigManager.config.selectedAccount = uuid } return authAcc } /** * Get an array of each mod configuration currently stored. * * @returns {Array.} An array of each stored mod configuration. */ public static getModConfigurations(): ModConfig[] { return ConfigManager.config.modConfigurations } /** * Set the array of stored mod configurations. * * @param {Array.} configurations An array of mod configurations. */ public static setModConfigurations(configurations: ModConfig[]): void { ConfigManager.config.modConfigurations = configurations } /** * Get the mod configuration for a specific server. * * @param {string} serverid The id of the server. * @returns {ModConfig | null} The mod configuration for the given server. */ public static getModConfiguration(serverid: string): ModConfig | null { const cfgs = ConfigManager.config.modConfigurations for(let i=0; i} An array of the additional arguments for JVM initialization. */ public static getJVMOptions(def = false): string[] { return !def ? ConfigManager.config.settings.java.jvmOptions : ConfigManager.DEFAULT_CONFIG.settings.java.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 {Array.} jvmOptions An array of the new additional arguments for JVM * initialization. */ public static setJVMOptions(jvmOptions: string[]): void { ConfigManager.config.settings.java.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. */ public static getGameWidth(def = false): number { return !def ? ConfigManager.config.settings.game.resWidth : ConfigManager.DEFAULT_CONFIG.settings.game.resWidth } /** * Set the width of the game window. * * @param {number} resWidth The new width of the game window. */ public static setGameWidth(resWidth: number): void { ConfigManager.config.settings.game.resWidth = Number.parseInt(resWidth as unknown as string) } /** * Validate a potential new width value. * * @param {number} resWidth The width value to validate. * @returns {boolean} Whether or not the value is valid. */ public static validateGameWidth(resWidth: number): boolean { const nVal = Number.parseInt(resWidth as unknown as string) 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. */ public static getGameHeight(def = false): number { return !def ? ConfigManager.config.settings.game.resHeight : ConfigManager.DEFAULT_CONFIG.settings.game.resHeight } /** * Set the height of the game window. * * @param {number} resHeight The new height of the game window. */ public static setGameHeight(resHeight: number): void { ConfigManager.config.settings.game.resHeight = Number.parseInt(resHeight as unknown as string) } /** * Validate a potential new height value. * * @param {number} resHeight The height value to validate. * @returns {boolean} Whether or not the value is valid. */ public static validateGameHeight(resHeight: number): boolean { const nVal = Number.parseInt(resHeight as unknown as string) 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. */ public static getFullscreen(def = false): boolean { return !def ? ConfigManager.config.settings.game.fullscreen : ConfigManager.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. */ public static setFullscreen(fullscreen: boolean): void { ConfigManager.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. */ public static getAutoConnect(def = false): boolean { return !def ? ConfigManager.config.settings.game.autoConnect : ConfigManager.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. */ public static setAutoConnect(autoConnect: boolean): void { ConfigManager.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. */ public static getLaunchDetached(def = false): boolean { return !def ? ConfigManager.config.settings.game.launchDetached : ConfigManager.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. */ public static setLaunchDetached(launchDetached: boolean): void { ConfigManager.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. */ public static getAllowPrerelease(def = false): boolean { return !def ? ConfigManager.config.settings.launcher.allowPrerelease : ConfigManager.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. */ public static setAllowPrerelease(allowPrerelease: boolean): void { ConfigManager.config.settings.launcher.allowPrerelease = allowPrerelease } }