diff --git a/src/dto/OpenJDKData.ts b/src/dto/OpenJDKData.ts new file mode 100644 index 00000000..91de1d43 --- /dev/null +++ b/src/dto/OpenJDKData.ts @@ -0,0 +1,5 @@ +export interface OpenJDKData { + uri: string, + size: number, + name: string +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/manager/ConfigManager.ts b/src/manager/ConfigManager.ts new file mode 100644 index 00000000..5023bf82 --- /dev/null +++ b/src/manager/ConfigManager.ts @@ -0,0 +1,825 @@ +import { ensureDirSync, moveSync } from "fs-extra"; +import { LoggerUtil } from "helios-core/."; +import { existsSync, readFileSync, writeFileSync } from "fs-extra"; +import os from 'os'; +import { join } from 'path'; +import { mcVersionAtLeast } from "../util/MinecraftUtil"; +import { resolveMaxRAM, resolveMinRAM } from "../util/System"; + +const logger = LoggerUtil.getLogger("ConfigManager"); + +type Config = { + settings: { + game: { + resWidth: number, + resHeight: number, + fullscreen: boolean, + autoConnect: boolean, + launchDetached: boolean, + }, + launcher: { + allowPrerelease: boolean, + dataDirectory: string, + }, + + }, + newsCache: { + date?: any, + content?: any, + dismissed: boolean, + }, + clientToken?: string, + selectedServer?: any, // Resolved + selectedAccount?: any, + authenticationDatabase: any, + modConfigurations: any[], + javaConfig: any, +} + + +export class ConfigManager { + + private static sysRoot = process.env.APPDATA ?? (process.platform == "darwin" ? process.env.HOME + "/Library/Application Support" : process.env.HOME) ?? ""; + // TODO change + private static dataPath = join(this.sysRoot, ".randomia"); + private static configPath = join(exports.getLauncherDirectory(), "config.json"); + private static configPathLEGACY = join(this.dataPath, "config.json"); + private static firstLaunch = !existsSync(this.configPath) && !existsSync(this.configPathLEGACY); + // Forked processes do not have access to electron, so we have this workaround. + private static launcherDir = process.env.CONFIG_DIRECT_PATH ?? require("@electron/remote").app.getPath("userData"); + public static readonly DistributionURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'; + public static readonly launcherName = 'Helios-Launcher' + + /** + * Three types of values: + * Static = Explicitly declared. + * Dynamic = Calculated by a private static function. + * Resolved = Resolved externally, defaults to null. + */ + private static DEFAULT_CONFIG: Config = { + settings: { + game: { + resWidth: 1280, + resHeight: 720, + fullscreen: false, + autoConnect: true, + launchDetached: true, + }, + launcher: { + allowPrerelease: false, + dataDirectory: this.dataPath, + }, + }, + newsCache: { + date: undefined, + content: undefined, + dismissed: false, + }, + clientToken: undefined, + selectedServer: undefined, // Resolved + selectedAccount: undefined, + authenticationDatabase: {}, + modConfigurations: [], + javaConfig: {}, + } + + private static config: Config; + + /** + * Retrieve the absolute path of the launcher directory. + * + * @returns {string} The absolute path of the launcher directory. + */ + public static getLauncherDirectory() { + return this.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 ? this.config.settings.launcher.dataDirectory : this.DEFAULT_CONFIG.settings.launcher.dataDirectory; + }; + + /** + * Set the new data directory. + * + * @param {string} dataDirectory The new data directory. + */ + public static setDataDirectory(dataDirectory: string) { + this.config.settings.launcher.dataDirectory = dataDirectory; + }; + + + + public static getAbsoluteMinRAM() { + const mem = os.totalmem(); + return mem >= 6000000000 ? 3 : 2; + }; + + public static getAbsoluteMaxRAM() { + const mem = os.totalmem(); + const gT16 = mem - 16000000000; + return Math.floor((mem - 1000000000 - (gT16 > 0 ? gT16 / 8 + 16000000000 / 4 : mem / 4)) / 1000000000); + }; + + /** + * Save the current configuration to a file. + */ + public static save() { + writeFileSync(this.configPath, JSON.stringify(this.config, null, 4), { encoding: "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 (!existsSync(this.configPath)) { + // Create all parent directories. + ensureDirSync(join(this.configPath, "..")); + if (existsSync(this.configPathLEGACY)) { + moveSync(this.configPathLEGACY, this.configPath); + } else { + doLoad = false; + this.config = this.DEFAULT_CONFIG; + exports.save(); + } + } + if (doLoad) { + let doValidate = false; + try { + this.config = JSON.parse(readFileSync(this.configPath, { encoding: "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."); + ensureDirSync(join(this.configPath, "..")); + this.config = this.DEFAULT_CONFIG; + exports.save(); + } + if (doValidate) { + this.config = this.validateKeySet(this.DEFAULT_CONFIG, this.config); + exports.save(); + } + } + logger.info("Successfully Loaded"); + }; + + /** + * @returns {boolean} Whether or not the manager has been loaded. + */ + public static get isLoaded() { + return this.config != null; + }; + + + + /** + * Check to see if this is the first time the user has launched the + * application. This is determined by the existance of the data + * + * @returns {boolean} True if this is the first launch, otherwise false. + */ + public static get isFirstLaunch() { + return this.firstLaunch; + }; + + /** + * Returns the name of the folder in the OS temp directory which we + * will use to extract and store native dependencies for game launch. + * + * @returns {string} The name of the folder. + */ + public static get tempNativeFolder() { + return "WCNatives"; + }; + + // System Settings (Unconfigurable on UI) + + /** + * Retrieve the news cache to determine + * whether or not there is newer news. + * + * @returns {Object} The news cache object. + */ + public static get getNewsCache() { + return this.config.newsCache; + }; + + /** + * Set the new news cache object. + * + * @param {Object} newsCache The new news cache object. + */ + public static setNewsCache(newsCache: { + date?: any; + content?: any; + dismissed: boolean; + }) { + this.config.newsCache = newsCache; + }; + + /** + * Set whether or not the news has been dismissed (checked) + * + * @param {boolean} dismissed Whether or not the news has been dismissed (checked). + */ + public static setNewsCacheDismissed(dismissed: boolean) { + this.config.newsCache.dismissed = dismissed; + }; + + /** + * Retrieve the common directory for shared + * game files (assets, libraries, etc). + * + * @returns {string} The launcher's common directory. + */ + public static get commonDirectory() { + return join(exports.getDataDirectory(), "common"); + }; + + /** + * Retrieve the instance directory for the per + * server game directories. + * + * @returns {string} The launcher's instance directory. + */ + public static get instanceDirectory() { + return join(exports.getDataDirectory(), "instances"); + }; + + /** + * Retrieve the launcher's Client Token. + * There is no default client token. + * + * @returns {string} The launcher's Client Token. + */ + public static get clientToken() { + return this.config.clientToken; + }; + + /** + * Set the launcher's Client Token. + * + * @param {string} clientToken The launcher's new Client Token. + */ + public static set clientToken(clientToken) { + this.config.clientToken = clientToken; + }; + + /** + * Retrieve the ID of the selected serverpack. + * + * @param {boolean} def Optional. If true, the default value will be returned. + * @returns {string} The ID of the selected serverpack. + */ + public static getSelectedServer(def = false) { + return !def ? this.config.selectedServer : this.DEFAULT_CONFIG.clientToken; + }; + + /** + * Set the ID of the selected serverpack. + * + * @param {string} serverID The ID of the new selected serverpack. + */ + public static set selectedServer(serverID: string) { + this.config.selectedServer = serverID; + }; + + /** + * Get an array of each account currently authenticated by the launcher. + * + * @returns {Array.} An array of each stored authenticated account. + */ + public static get authAccounts() { + return this.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. + */ + public static getAuthAccountByUuid(uuid: string) { + return this.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. + */ + public static updateMojangAuthAccount(uuid: string, accessToken: string) { + this.config.authenticationDatabase[uuid].accessToken = accessToken; + this.config.authenticationDatabase[uuid].type = "mojang"; // For gradual conversion. + return this.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. + */ + public static addMojangAuthAccount(uuid: string, accessToken: string, username: string, displayName: string) { + this.config.selectedAccount = uuid; + this.config.authenticationDatabase[uuid] = { + type: "mojang", + accessToken, + username: username.trim(), + uuid: uuid.trim(), + displayName: displayName.trim(), + }; + return this.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. + */ + public static updateMicrosoftAuthAccount(uuid: string, accessToken: string, msAccessToken: string, msRefreshToken: string, msExpires: string, mcExpires: string) { + this.config.authenticationDatabase[uuid].accessToken = accessToken; + this.config.authenticationDatabase[uuid].expiresAt = mcExpires; + this.config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken; + this.config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken; + this.config.authenticationDatabase[uuid].microsoft.expires_at = msExpires; + return this.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. + */ + public static addMicrosoftAuthAccount( + uuid: string, + accessToken: string, + name: string, + mcExpires: string, + msAccessToken: string, + msRefreshToken: string, + msExpires: string + ) { + this.config.selectedAccount = uuid; + this.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 this.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) { + if (this.config.authenticationDatabase[uuid] != null) { + delete this.config.authenticationDatabase[uuid]; + if (this.config.selectedAccount === uuid) { + const keys = Object.keys(this.config.authenticationDatabase); + if (keys.length > 0) { + this.config.selectedAccount = keys[0]; + } else { + this.config.selectedAccount = null; + this.config.clientToken = undefined; + } + } + return true; + } + return false; + }; + + /** + * Get the currently selected authenticated account. + * + * @returns {Object} The selected authenticated account. + */ + public static getSelectedAccount() { + return this.config.authenticationDatabase[this.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. + */ + public static setSelectedAccount(uuid: string) { + const authAcc = this.config.authenticationDatabase[uuid]; + if (authAcc != null) { + this.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 get modConfigurations() { + return this.config.modConfigurations; + }; + + /** + * Set the array of stored mod configurations. + * + * @param {Array.} configurations An array of mod configurations. + */ + public static set modConfigurations(configurations) { + this.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. + */ + public static getModConfigurationForServer(serverid: string) { + const cfgs = this.config.modConfigurations; + for (let i = 0; i < cfgs.length; i++) { + if (cfgs[i].id === serverid) { + return cfgs[i]; + } + } + return null; + }; + + /** + * Set the mod configuration for a specific server. This overrides any existing value. + * + * @param {string} serverid The id of the server for the given mod configuration. + * @param {Object} configuration The mod configuration for the given server. + */ + public static setModConfigurationForServer(serverid: string, configuration) { + const cfgs = this.config.modConfigurations; + for (let i = 0; i < cfgs.length; i++) { + if (cfgs[i].id === serverid) { + cfgs[i] = configuration; + return; + } + } + cfgs.push(configuration); + }; + + + ///////////////////////////////////// JAVA CONFIG //////////////////////////////////////////// + + + // User Configurable Settings + + // Java Settings + + /** + * Ensure a java config property is set for the given server. + * + * @param {string} serverid The server id. + * @param {*} mcVersion The minecraft version of the server. + */ + public static ensureJavaConfig(serverid: string, mcVersion: string) { + if (!Object.prototype.hasOwnProperty.call(this.config.javaConfig, serverid)) { + this.config.javaConfig[serverid] = this.defaultJavaConfig(mcVersion); + } + }; + + /** + * Retrieve the minimum amount of memory for JVM initialization. This value + * contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = + * 1024 MegaBytes, etc. + * + * @param {string} serverid The server id. + * @returns {string} The minimum amount of memory for JVM initialization. + */ + public static getMinRAM(serverid: string) { + return this.config.javaConfig[serverid].minRAM; + }; + + /** + * Set the minimum amount of memory for JVM initialization. This value should + * contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = + * 1024 MegaBytes, etc. + * + * @param {string} serverid The server id. + * @param {string} minRAM The new minimum amount of memory for JVM initialization. + */ + public static setMinRAM(serverid: string, minRAM: string) { + this.config.javaConfig[serverid].minRAM = minRAM; + }; + + /** + * Retrieve the maximum amount of memory for JVM initialization. This value + * contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = + * 1024 MegaBytes, etc. + * + * @param {string} serverid The server id. + * @returns {string} The maximum amount of memory for JVM initialization. + */ + public static getMaxRAM(serverid: string) { + return this.config.javaConfig[serverid].maxRAM; + }; + + /** + * Set the maximum amount of memory for JVM initialization. This value should + * contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = + * 1024 MegaBytes, etc. + * + * @param {string} serverid The server id. + * @param {string} maxRAM The new maximum amount of memory for JVM initialization. + */ + public static setMaxRAM(serverid: string, maxRAM: string) { + this.config.javaConfig[serverid].maxRAM = maxRAM; + }; + + /** + * Retrieve the path of the Java Executable. + * + * This is a resolved configuration value and defaults to null until externally assigned. + * + * @param {string} serverid The server id. + * @returns {string} The path of the Java Executable. + */ + public static getJavaExecutable(serverid: string) { + return this.config.javaConfig[serverid].executable; + }; + + /** + * Set the path of the Java Executable. + * + * @param {string} serverid The server id. + * @param {string} executable The new path of the Java Executable. + */ + public static setJavaExecutable(serverid: string, executable: string) { + this.config.javaConfig[serverid].executable = executable; + }; + + /** + * Retrieve the additional arguments for JVM initialization. Required arguments, + * such as memory allocation, will be dynamically resolved and will not be included + * in this value. + * + * @param {string} serverid The server id. + * @returns {Array.} An array of the additional arguments for JVM initialization. + */ + public static getJVMOptions(serverid: string) { + return this.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. + */ + public static setJVMOptions(serverid: string, jvmOptions: string[]) { + this.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. + */ + public static getGameWidth(def = false) { + return !def ? this.config.settings.game.resWidth : this.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) { + if (typeof resWidth !== "number") throw new Error("Only Accept Number") + this.config.settings.game.resWidth = resWidth; + }; + + /** + * 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) { + if (typeof resWidth !== "number") throw new Error("Only Accept Number") + return Number.isInteger(resWidth) && resWidth >= 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) { + return !def ? this.config.settings.game.resHeight : this.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) { + if (typeof resHeight !== "number") throw new Error("Only Accept Number") + this.config.settings.game.resHeight = resHeight; + }; + + /** + * 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) { + if (typeof resHeight !== "number") throw new Error("Only Accept Number") + return Number.isInteger(resHeight) && resHeight >= 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) { + return !def ? this.config.settings.game.fullscreen : this.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) { + this.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) { + return !def ? this.config.settings.game.autoConnect : this.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) { + this.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) { + return !def ? this.config.settings.game.launchDetached : this.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) { + this.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) { + return !def ? this.config.settings.launcher.allowPrerelease : this.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) { + this.config.settings.launcher.allowPrerelease = allowPrerelease; + }; + + private static defaultJavaConfig(mcVersion: string) { + if (mcVersionAtLeast("1.17", mcVersion)) { + return this.defaultJavaConfig117(); + } else { + return this.defaultJavaConfigBelow117(); + } + } + + private static defaultJavaConfigBelow117() { + return { + minRAM: resolveMinRAM(), + maxRAM: resolveMaxRAM(), // Dynamic + executable: null, + jvmOptions: ["-XX:+UseConcMarkSweepGC", "-XX:+CMSIncrementalMode", "-XX:-UseAdaptiveSizePolicy", "-Xmn128M"], + }; + } + + private static defaultJavaConfig117() { + return { + minRAM: resolveMinRAM(), + maxRAM: resolveMaxRAM(), // Dynamic + executable: null, + jvmOptions: ["-XX:+UnlockExperimentalVMOptions", "-XX:+UseG1GC", "-XX:G1NewSizePercent=20", "-XX:G1ReservePercent=20", "-XX:MaxGCPauseMillis=50", "-XX:G1HeapRegionSize=32M"], + }; + } + + /** + * 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, destObj) { + if (srcObj == null) { + srcObj = {}; + } + const validationBlacklist = ["authenticationDatabase", "javaConfig"]; + const keys = Object.keys(srcObj); + for (let i = 0; i < keys.length; i++) { + if (typeof destObj[keys[i]] === "undefined") { + destObj[keys[i]] = srcObj[keys[i]]; + } else if (typeof srcObj[keys[i]] === "object" && srcObj[keys[i]] != null && !(srcObj[keys[i]] instanceof Array) && validationBlacklist.indexOf(keys[i]) === -1) { + destObj[keys[i]] = this.validateKeySet(srcObj[keys[i]], destObj[keys[i]]); + } + } + return destObj; + } + + +} + diff --git a/src/manager/DistroManager.ts b/src/manager/DistroManager.ts new file mode 100644 index 00000000..e6d53148 --- /dev/null +++ b/src/manager/DistroManager.ts @@ -0,0 +1,94 @@ +import { readFile, writeFile } from "fs-extra" +import { LoggerUtil } from "helios-core/."; +import { DevUtil } from '../util/isDev'; +import request from "request"; +import { ConfigManager } from "./ConfigManager"; +import { join } from 'path'; +import { DistroIndex } from '../models/DistroIndex'; + +const logger = LoggerUtil.getLogger('DistroManager') +export enum DistroTypes { + Library, + ForgeHosted, + Forge, // Unimplemented + LiteLoader, + ForgeMod, + LiteMod, + File, + VersionManifest, +} + +export class DistroManager { + + public distribution!: DistroIndex; + private readonly DISTRO_PATH = join(ConfigManager.getLauncherDirectory(), 'distribution.json') + private readonly DEV_PATH = join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json') + + /** + * @returns {Promise.} + */ + public pullRemote() { + if (DevUtil.IsDev) { + return exports.pullLocal() + } + return new Promise((resolve, reject) => { + const opts = { + url: ConfigManager.DistributionURL, + timeout: 2500 + } + const distroDest = join(ConfigManager.getLauncherDirectory(), 'distribution.json') + request(opts, (error: Error, _resp: any, body: string) => { + if (!error) { + + try { + this.distribution = DistroIndex.fromJSON(JSON.parse(body)) + } catch (e) { + reject(e) + return + } + + writeFile(distroDest, body, 'utf-8', (err) => { + if (!err) { + resolve(this.distribution) + return + } else { + reject(err) + return + } + }) + } else { + reject(error) + return + } + }) + }) + } + + /** + * @returns {Promise.} + */ + public pullLocal() { + return new Promise((resolve, reject) => { + readFile(DevUtil.IsDev ? this.DEV_PATH : this.DISTRO_PATH, 'utf-8', (err, d) => { + if (!err) { + this.distribution = DistroIndex.fromJSON(JSON.parse(d)) + resolve(this.distribution) + return + } else { + reject(err) + return + } + }) + }) + } + + public setDevMode(value: boolean) { + if (value) { + logger.info('Developer mode enabled.') + logger.info('If you don\'t know what that means, revert immediately.') + } else { + logger.info('Developer mode disabled.') + } + DevUtil.IsDev = value + } +} \ No newline at end of file diff --git a/src/models/Artifact.ts b/src/models/Artifact.ts new file mode 100644 index 00000000..9b62a2f0 --- /dev/null +++ b/src/models/Artifact.ts @@ -0,0 +1,62 @@ +/** + * Represents the download information + * for a specific module. + */ +export class Artifact { + + /** + * Parse a JSON object into an Artifact. + * + * @param {Object} json A JSON object representing an Artifact + * + * @returns {Artifact} The parsed Artifact. + */ + public static fromJSON(json: { + MD5: string, + size: string, + url: string, + path: string, + }) { + return new Artifact(json.MD5, json.size, json.url, json.path) + } + + constructor( + public MD5: string, + public size: string, + public url: string, + public path: string, + ) { } + + /** + * 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. + */ + public getHash() { + return this.MD5 + } + + /** + * @returns {number} The download size of the artifact. + */ + public getSize() { + return this.size + } + + /** + * @returns {string} The download url of the artifact. + */ + public getURL() { + return this.url + } + + /** + * @returns {string} The artifact's destination path. + */ + public getPath() { + return this.path + } + +} \ No newline at end of file diff --git a/src/models/Asset.ts b/src/models/Asset.ts new file mode 100644 index 00000000..04f2952d --- /dev/null +++ b/src/models/Asset.ts @@ -0,0 +1,20 @@ +/** Class representing a base asset. */ +export class Asset { + /** + * Create an asset. + * + * @param {any} id The id of the asset. + * @param {string} hash The hash value of the asset. + * @param {number} size The size in bytes of the asset. + * @param {string} from The url where the asset can be found. + * @param {string} to The absolute local file path of the asset. + */ + constructor( + public id: any, + public hash: string, + public size: number, + public from: string, + public to: string + ) { + } +} \ No newline at end of file diff --git a/src/models/DLTracker.ts b/src/models/DLTracker.ts new file mode 100644 index 00000000..03dfdd21 --- /dev/null +++ b/src/models/DLTracker.ts @@ -0,0 +1,22 @@ +import { Asset } from './Asset'; +/** + * Class representing a download tracker. This is used to store meta data + * about a download queue, including the queue itself. + */ +export class DLTracker { + + /** + * Create a DLTracker + * + * @param {Array.} dlqueue An array containing assets queued for download. + * @param {number} dlsize The combined size of each asset in the download queue array. + * @param {function(Asset)} callback Optional callback which is called when an asset finishes downloading. + */ + constructor( + public dlqueue: Asset[], + public dlsize: number, + public callback?: (asset: Asset) => void) { + + } + +} \ No newline at end of file diff --git a/src/models/DistroAsset.ts b/src/models/DistroAsset.ts new file mode 100644 index 00000000..283fe720 --- /dev/null +++ b/src/models/DistroAsset.ts @@ -0,0 +1,24 @@ +import { Asset } from "./Asset" + +export class DistroAsset extends Asset { + + /** + * Create a DistroModule. This is for processing, + * not equivalent to the module objects in the + * distro index. + * + * @param {any} id The id of the asset. + * @param {string} hash The hash value of the asset. + * @param {number} size The size in bytes of the asset. + * @param {string} from The url where the asset can be found. + * @param {string} to The absolute local file path of the asset. + * @param {string} type The the module type. + */ + constructor(id: any, hash: string, size: number, from: string, to: string, + public type + ) { + super(id, hash, size, from, to) + this.type = type + } + +} \ No newline at end of file diff --git a/src/models/DistroIndex.ts b/src/models/DistroIndex.ts new file mode 100644 index 00000000..895c1ad0 --- /dev/null +++ b/src/models/DistroIndex.ts @@ -0,0 +1,47 @@ +import { IServer, Server } from './Server'; + +interface IDistroIndex { + version: string, + rss: string, + servers: IServer[] +} + +export class DistroIndex { + + /** + * Parse a JSON object into a DistroIndex. + * + * @param {Object} json A JSON object representing a DistroIndex. + * + * @returns {DistroIndex} The parsed Server object. + */ + public static fromJSON(json: IDistroIndex) { + return new DistroIndex( + json.version, + json.rss, + json.servers + ) + } + + public servers: Server[] = [] + public get mainServer() { + return this.servers.find(x => x.isMainServer)?.id ?? this.servers[0].id ?? null; + } + + constructor( + public version: string, + public rss: string, + servers: IServer[], + ) { + this.resolveServers(servers); + } + + private resolveServers(serverJsons: IServer[]) { + const servers: Server[] = [] + for (let serverJson of serverJsons) { + servers.push(Server.fromJSON(serverJson)) + } + this.servers = servers + } + +} \ No newline at end of file diff --git a/src/models/Library.ts b/src/models/Library.ts new file mode 100644 index 00000000..02641da8 --- /dev/null +++ b/src/models/Library.ts @@ -0,0 +1,54 @@ +import { Asset } from './Asset'; +export class Library extends Asset { + + /** + * Converts the process.platform OS names to match mojang's OS names. + */ + public static mojangFriendlyOS() { + switch (process.platform) { + case "darwin": + return 'osx'; + case "linux": + return 'linux'; + case "win32": + return 'windows'; + default: + return 'unknown_os' + } + } + + /** + * Checks whether or not a library is valid for download on a particular OS, following + * the rule format specified in the mojang version data index. If the allow property has + * an OS specified, then the library can ONLY be downloaded on that OS. If the disallow + * property has instead specified an OS, the library can be downloaded on any OS EXCLUDING + * the one specified. + * + * If the rules are undefined, the natives property will be checked for a matching entry + * for the current OS. + * + * @param {Array.} rules The Library's download rules. + * @param {Object} natives The Library's natives object. + * @returns {boolean} True if the Library follows the specified rules, otherwise false. + */ + public static validateRules(rules, natives) { + if (rules == null) { + return natives ? natives[Library.mojangFriendlyOS()] != null : true; + } + + for (let rule of rules) { + const action = rule.action + const osProp = rule.os + if (action != null && osProp != null) { + const osName = osProp.name + const osMoj = Library.mojangFriendlyOS() + if (action === 'allow') { + return osName === osMoj + } else if (action === 'disallow') { + return osName !== osMoj + } + } + } + return true + } +} \ No newline at end of file diff --git a/src/models/Module.ts b/src/models/Module.ts new file mode 100644 index 00000000..b76c0587 --- /dev/null +++ b/src/models/Module.ts @@ -0,0 +1,141 @@ +import { LoggerUtil } from 'helios-core/.'; +import { ConfigManager } from '../manager/ConfigManager'; +import { Artifact } from './Artifact'; +import { join } from 'path'; +import { DistroTypes } from '../manager/DistroManager'; + +const logger = LoggerUtil.getLogger('Module') + +export 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. + */ + public 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. + */ + private 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. + } + } + + + public artifactExt: string; + public artifactClassifier?: string; + public artifactVersion: string; + public artifactID: string; + public artifactGroup: string; + + public subModules: Module[] = [] + + /** + * @returns {string} The identifier without he version or extension. + */ + public get versionlessID() { + return this.artifactGroup + ':' + this.artifactID + } + + /** + * @returns {string} The identifier without the extension. + */ + public get extensionlessID() { + return this.identifier.split('@')[0] + } + + public get hasSubModules() { + return this.subModules.length > 0; + } + + + constructor(public identifier: string, + public name: string, + public type: DistroTypes, + public classpath: boolean = true, + public required = Required.fromJSON(required), + public artifact = Artifact.fromJSON(artifact), + subModules, + serverid + ) { + this.resolveMetaData() + this.resolveArtifactPath(artifact.path, serverid) + this.resolveSubModules(subModules, serverid) + } + + private 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) + } + } + + private resolveArtifactPath(artifactPath: string, serverid) { + const pth = artifactPath == null ? join(...this.artifactGroup.split('.'), this.artifactID, this.artifactVersion, `${this.artifactID}-${this.artifactVersion}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.artifactExt}`) : artifactPath + + switch (this.type) { + case exports.Types.Library: + case exports.Types.ForgeHosted: + case exports.Types.LiteLoader: + this.artifact.path = join(ConfigManager.commonDirectory, 'libraries', pth) + break + case exports.Types.ForgeMod: + case exports.Types.LiteMod: + this.artifact.path = join(ConfigManager.commonDirectory, 'modstore', pth) + break + case exports.Types.VersionManifest: + this.artifact.path = join(ConfigManager.commonDirectory, 'versions', this.identifier, `${this.identifier}.json`) + break + case exports.Types.File: + default: + this.artifact.path = join(ConfigManager.instanceDirectory, serverid, pth) + break + } + + } + + private resolveSubModules(json, serverid) { + if (json == null) return; + + const subModules: Module[] = [] + for (let sm of json) { + subModules.push(Module.fromJSON(sm, serverid)) + } + this.subModules = subModules + } + +} \ No newline at end of file diff --git a/src/models/Required.ts b/src/models/Required.ts new file mode 100644 index 00000000..ce589e17 --- /dev/null +++ b/src/models/Required.ts @@ -0,0 +1,54 @@ +/** + * Represents a the requirement status + * of a module. + */ + +export interface IRequired { + value: any, + def: any, +} + +export 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: IRequired) { + if (json == null) { + return new Required(true, true) + } else { + return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def) + } + } + + public default: any; + constructor( + public value: any, + def: any + ) { + 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. + */ + public get isDefault() { + return this.default + } + + /** + * @returns {boolean} Whether or not the module is required. + */ + public get isRequired(): boolean { + return this.value + } + +} \ No newline at end of file diff --git a/src/models/Server.ts b/src/models/Server.ts new file mode 100644 index 00000000..ee1b948c --- /dev/null +++ b/src/models/Server.ts @@ -0,0 +1,73 @@ +import { Module } from "./Module" + +export interface IServer { + id: string, + name: string, + description: string, + icon: string, + version: string, + address: string, + minecraftVersion: string, + isMainServer: boolean, + autoconnect: boolean, + modules: Module[], +} + +/** + * Represents a server configuration. + */ +export class Server { + /** + * Parse a JSON object into a Server. + * + * @param {Object} json A JSON object representing a Server. + * + * @returns {Server} The parsed Server object. + */ + public static fromJSON(json: IServer) { + + const mdls = json.modules + json.modules = [] + + const serv = new Server( + json.id, + json.name, + json.description, + json.icon, + json.version, + json.address, + json.minecraftVersion, + json.isMainServer, + json.autoconnect, + json.modules + ) + serv.resolveModules(mdls) + + return serv + } + + + constructor( + public id: string, + public name: string, + public description: string, + public icon: string, + public version: string, + public address: string, + public minecraftVersion: string, + public isMainServer: boolean, + public autoconnect: boolean, + public modules: Module[] = [], + ) { } + + + private resolveModules(json) { + const modules: Module[] = [] + for (let m of json) { + modules.push(Module.fromJSON(m, this.id)) + } + this.modules = modules + } + + +} \ No newline at end of file diff --git a/src/services/JavaGuard.ts b/src/services/JavaGuard.ts new file mode 100644 index 00000000..c460cde8 --- /dev/null +++ b/src/services/JavaGuard.ts @@ -0,0 +1,5 @@ +import * as EventEmitter from "events"; + +export class JavaGuard extends EventEmitter { + +} \ No newline at end of file diff --git a/src/services/Logger.ts b/src/services/Logger.ts new file mode 100644 index 00000000..0ae76ea3 --- /dev/null +++ b/src/services/Logger.ts @@ -0,0 +1,2 @@ +import { LoggerUtil } from "helios-core/." +export const logger = LoggerUtil.getLogger('AssetExec') \ No newline at end of file diff --git a/src/services/ProcessBuilder.ts b/src/services/ProcessBuilder.ts new file mode 100644 index 00000000..31d72f19 --- /dev/null +++ b/src/services/ProcessBuilder.ts @@ -0,0 +1,874 @@ +import { ensureDirSync, remove, writeFile, writeFileSync } from "fs-extra"; +import { join, basename } from "path"; +import { pseudoRandomBytes } from "crypto"; +import os from "os"; +import { existsSync } from "fs-extra"; +import AdmZip from "adm-zip"; +import { spawn } from "child_process"; +import { LoggerUtil } from "helios-core/."; +import { ConfigManager } from "../manager/ConfigManager"; +import { MinecraftUtil } from "../util/MinecraftUtil"; +import { DistroTypes } from "../manager/DistroManager"; +import { Library } from "../models/Library"; +import { Module } from "../models/Module"; +import { Required } from "../models/Required"; + +const logger = LoggerUtil.getLogger('ProcessBuilder') +export default class ProcessBuilder { + + /** + * Get the platform specific classpath separator. On windows, this is a semicolon. + * On Unix, this is a colon. + * + * @returns {string} The classpath separator for the current operating system. + */ + public static get classpathSeparator() { + return process.platform === 'win32' ? ';' : ':' + } + + public static isModEnabled(modCfg, required?: Required) { + return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.isDefault() : true + } + + + public gameDir: string; + public commonDir: string; + public forgeModListFile: string; + public fmlDir: string; + public llDir: string; + public libPath: string; + + public usingLiteLoader = false; + public llPath?: string; + + constructor( + public server, + public versionData, + public forgeData, + public authUser, + public launcherVersion + ) { + + this.gameDir = join(ConfigManager.instanceDirectory, server.getID()) + this.commonDir = ConfigManager.commonDirectory + this.versionData = versionData + this.forgeData = forgeData + this.authUser = authUser + this.launcherVersion = launcherVersion + this.forgeModListFile = join(this.gameDir, 'forgeMods.list') // 1.13+ + this.fmlDir = join(this.gameDir, 'forgeModList.json') + this.llDir = join(this.gameDir, 'liteloaderModList.json') + this.libPath = join(this.commonDir, 'libraries') + + } + + + /** + * Convienence method to run the functions typically used to build a process. + */ + public build() { + ensureDirSync(this.gameDir) + const tempNativePath = join(os.tmpdir(), ConfigManager.tempNativeFolder, pseudoRandomBytes(16).toString('hex')) + process.throwDeprecation = true + this.setupLiteLoader() + logger.info('Using liteloader:', this.usingLiteLoader) + const modObj = this.resolveModConfiguration(ConfigManager.getModConfigurationForServer(this.server.getID()).mods, this.server.getModules()) + + // Mod list below 1.13 + if (!MinecraftUtil.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())) { + this.constructJSONModList('forge', modObj.forgeMods, true) + if (this.usingLiteLoader) { + this.constructJSONModList('liteloader', modObj.liteMods, true) + } + } + + const uberModArr = modObj.forgeMods.concat(modObj.liteMods) + let args = this.constructJVMArguments(uberModArr, tempNativePath) + + if (MinecraftUtil.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())) { + //args = args.concat(this.constructModArguments(modObj.forgeMods)) + args = args.concat(this.constructModList(modObj.forgeMods)) + } + + logger.info('Launch Arguments:', args) + + const child = spawn(ConfigManager.getJavaExecutable(this.server.getID()), args, { + cwd: this.gameDir, + detached: ConfigManager.getLaunchDetached() + }) + + if (ConfigManager.getLaunchDetached()) { + child.unref() + } + + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + + child.stdout.on('data', (data) => { + data.trim().split('\n').forEach(x => console.log(`\x1b[32m[Minecraft]\x1b[0m ${x}`)) + + }) + child.stderr.on('data', (data) => { + data.trim().split('\n').forEach(x => console.log(`\x1b[31m[Minecraft]\x1b[0m ${x}`)) + }) + child.on('close', (code, signal) => { + logger.info('Exited with code', code) + remove(tempNativePath, (err) => { + if (err) { + logger.warn('Error while deleting temp dir', err) + } else { + logger.info('Temp dir deleted successfully.') + } + }) + }) + + return child + } + + /** + * Function which performs a preliminary scan of the top level + * mods. If liteloader is present here, we setup the special liteloader + * launch options. Note that liteloader is only allowed as a top level + * mod. It must not be declared as a submodule. + */ + public setupLiteLoader() { + for (let ll of this.server.getModules()) { + if (ll.getType() === DistroTypes.LiteLoader) { + if (!ll.getRequired().isRequired()) { + const modCfg = ConfigManager.getModConfigurationForServer(this.server.getID()).mods + if (ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessID()], ll.getRequired())) { + if (existsSync(ll.getArtifact().getPath())) { + this.usingLiteLoader = true + this.llPath = ll.getArtifact().getPath() + } + } + } else { + if (existsSync(ll.getArtifact().getPath())) { + this.usingLiteLoader = true + this.llPath = ll.getArtifact().getPath() + } + } + } + } + } + + /** + * Resolve an array of all enabled mods. These mods will be constructed into + * a mod list format and enabled at launch. + * + * @param {Object} modConfig The mod configuration object. + * @param {Array.} modules An array of modules to parse. + * @returns {{forgeMods: Array., liteMods: Array.}} An object which contains + * a list of enabled forge mods and litemods. + */ + public resolveModConfiguration(modConfig, modules: Module[]) { + let forgeMods: Module[] = [] + let liteMods: Module[] = [] + + for (let module of modules) { + const type = module.type; + if (type === DistroTypes.ForgeMod || type === DistroTypes.LiteMod || type === DistroTypes.LiteLoader) { + const isRequired = !module.required.isRequired() + const isEnabled = ProcessBuilder.isModEnabled(modConfig[module.versionlessID], module.required) + if (!isRequired || (isRequired && isEnabled)) { + if (module.hasSubModules) { + const v = this.resolveModConfiguration(modConfig[module.versionlessID].mods, module.subModules) + forgeMods = forgeMods.concat(v.forgeMods) + liteMods = liteMods.concat(v.liteMods) + if (module.type === DistroTypes.LiteLoader) continue; + } + if (type === DistroTypes.ForgeMod) { + forgeMods.push(module) + } else { + liteMods.push(module) + } + } + } + } + + return { + forgeMods, + liteMods + } + } + + + /** + * Construct a mod list json object. + * + * @param {'forge' | 'liteloader'} type The mod list type to construct. + * @param {Array.} mods An array of mods to add to the mod list. + * @param {boolean} save Optional. Whether or not we should save the mod list file. + */ + public constructJSONModList(type: 'forge' | 'liteloader', mods: Module[], save = false) { + let modList: { + repositoryRoot: string, + modRef: string[] + } = { + repositoryRoot: ((type === 'forge' && this.requiresAbsolute()) ? 'absolute:' : '') + join(this.commonDir, 'modstore'), + modRef: [] + } + + const ids = [] + if (type === 'forge') { + for (let mod of mods) { + ids.push(mod.extensionlessID) + } + } else { + for (let mod of mods) { + ids.push(mod.extensionlessID + '@' + mod.artifactExt) + } + } + + modList.modRef = ids + + if (save) { + const json = JSON.stringify(modList, null, 4) + writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, { encoding: 'utf-8' }) + } + + return modList + } + + /** + * Construct the argument array that will be passed to the JVM process. + * + * @param {Array.} mods An array of enabled mods which will be launched with this process. + * @param {string} tempNativePath The path to store the native libraries. + * @returns {Array.} An array containing the full JVM arguments for this process. + */ + public constructJVMArguments(mods: Module, tempNativePath: string) { + if (MinecraftUtil.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())) { + return this.constructJVMArguments113(mods, tempNativePath) + } else { + return this.constructJVMArguments112(mods, tempNativePath) + } + } + + + /** + * Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared + * libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries, + * this method requires all enabled mods as an input + * + * @param {Array.} mods An array of enabled mods which will be launched with this process. + * @param {string} tempNativePath The path to store the native libraries. + * @returns {Array.} An array containing the paths of each library required by this process. + */ + public classpathArg(mods: Module[], tempNativePath: string) { + let cpArgs: string[] = [] + + if (!MinecraftUtil.mcVersionAtLeast('1.17', this.server.getMinecraftVersion())) { + // Add the version.jar to the classpath. + // Must not be added to the classpath for Forge 1.17+. + const version = this.versionData.id + cpArgs.push(join(this.commonDir, 'versions', version, version + '.jar')) + } + + + if (this.usingLiteLoader && this.llPath) { + cpArgs.push(this.llPath) + } + + // Resolve the Mojang declared libraries. + const mojangLibs = this.resolveMojangLibraries(tempNativePath) + + // Resolve the server declared libraries. + const servLibs = this.resolveServerLibraries(mods) + + // Merge libraries, server libs with the same + // maven identifier will override the mojang ones. + // Ex. 1.7.10 forge overrides mojang's guava with newer version. + const finalLibs = { ...mojangLibs, ...servLibs } + cpArgs = cpArgs.concat(Object.values(finalLibs)) + + this.processClassPathList(cpArgs) + + return cpArgs + } + + /** + * Construct the mod argument list for forge 1.13 + * + * @param {Array.} mods An array of mods to add to the mod list. + */ + public constructModList(mods: Module[]) { + const writeBuffer = mods.map(mod => { + return mod.extensionlessID + }).join('\n') + + if (writeBuffer) { + writeFileSync(this.forgeModListFile, writeBuffer, { encoding: 'utf-8' }) + return [ + '--fml.mavenRoots', + join('..', '..', 'common', 'modstore'), + '--fml.modLists', + this.forgeModListFile + ] + } else { + return [] + } + + } + + + + /** + * Ensure that the classpath entries all point to jar files. + * + * //TODO: WTF WHY MATE WHY ????? + * @param {Array.} classpathEntries Array of classpath entries. + */ + private processClassPathList(classpathEntries: string[]) { + const ext = '.jar' + const extLen = ext.length + for (let i = 0; i < classpathEntries.length; i++) { + const extIndex = classpathEntries[i].indexOf(ext) + if (extIndex > -1 && extIndex !== classpathEntries[i].length - extLen) { + classpathEntries[i] = classpathEntries[i].substring(0, extIndex + extLen) + } + } + } + + + /** + * Resolve the libraries declared by this server in order to add them to the classpath. + * This method will also check each enabled mod for libraries, as mods are permitted to + * declare libraries. + * + * @param {Array.} mods An array of enabled mods which will be launched with this process. + * @returns {{[id: string]: string}} An object containing the paths of each library this server requires. + */ + private resolveServerLibraries(mods: Module[]) { + const modules: Module[] = this.server.getModules(); + let libs: Record = {} + + // Locate Forge/Libraries + for (let module of modules) { + const type = module.type + if (type === DistroTypes.ForgeHosted || type === DistroTypes.Library) { + libs[module.versionlessID] = module.artifact.path; + if (!module.hasSubModules) continue; + const res = this.resolveModuleLibraries(module) + if (res.length > 0) { + libs = { ...libs, ...res } + } + } + } + + //Check for any libraries in our mod list. + for (let i = 0; i < mods.length; i++) { + const mod = mods[i]; + if (!mod.hasSubModules) continue; + const res = this.resolveModuleLibraries(mods[i]) + if (res.length > 0) { + libs = { ...libs, ...res } + } + } + + return libs + } + + /** + * Recursively resolve the path of each library required by this module. + * + * @param {Object} module A module object from the server distro index. + * @returns {Array.} An array containing the paths of each library this module requires. + */ + private resolveModuleLibraries(module: Module) { + if (!module.hasSubModules) return [] + + let libs: string[] = [] + + for (let subModule of module.subModules) { + if (subModule.type === DistroTypes.Library) { + + if (subModule.classpath) { + libs.push(subModule.artifact.path) + } + } + + // If this module has submodules, we need to resolve the libraries for those. + // To avoid unnecessary recursive calls, base case is checked here. + if (module.hasSubModules) { + const res = this.resolveModuleLibraries(subModule) + if (res.length > 0) { + libs = libs.concat(res) + } + } + } + + return libs + } + + + /** + * Resolve the libraries defined by Mojang's version data. This method will also extract + * native libraries and point to the correct location for its classpath. + * + * TODO - clean up function + * + * @param {string} tempNativePath The path to store the native libraries. + * @returns {{[id: string]: string}} An object containing the paths of each library mojang declares. + */ + private resolveMojangLibraries(tempNativePath: string) { + const nativesRegex = /.+:natives-([^-]+)(?:-(.+))?/ + let libs: Record = {} + + const libArr = this.versionData.libraries + ensureDirSync(tempNativePath) + for (let i = 0; i < libArr.length; i++) { + const lib = libArr[i] + if (Library.validateRules(lib.rules, lib.natives)) { + + // Pre-1.19 has a natives object. + if (lib.natives != null) { + // Extract the native library. + const exclusionArr: string[] = lib.extract != null ? lib.extract.exclude : ['META-INF/'] + const artifact = lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))] + + // Location of native zip. + const to = join(this.libPath, artifact.path) + + const zip = new AdmZip(to) + const zipEntries = zip.getEntries() + + // Unzip the native zip. + for (let i = 0; i < zipEntries.length; i++) { + const fileName = zipEntries[i].entryName + + let shouldExclude = false + + // Exclude noted files. + exclusionArr.forEach(exclusion => { + if (fileName.indexOf(exclusion) > -1) { + shouldExclude = true + } + }) + + // Extract the file. + if (shouldExclude) continue; + writeFile(join(tempNativePath, fileName), zipEntries[i].getData(), (err) => { + if (err) { + logger.error('Error while extracting native library:', err) + } + }) + + } + } + // 1.19+ logic + else if (lib.name.includes('natives-')) { + + const regexTest: RegExpExecArray | null = nativesRegex.exec(lib.name) + // const os = regexTest[1] + if (!regexTest) throw new Error("No RegexTest - Processor Builder"); + + const arch = regexTest[2] ?? 'x64' + + if (arch != process.arch) continue; + + // Extract the native library. + const exclusionArr: string[] = lib.extract != null ? lib.extract.exclude : ['META-INF/', '.git', '.sha1'] + const artifact = lib.downloads.artifact + + // Location of native zip. + const to = join(this.libPath, artifact.path) + + let zip = new AdmZip(to) + let zipEntries = zip.getEntries() + + // Unzip the native zip. + for (let i = 0; i < zipEntries.length; i++) { + if (zipEntries[i].isDirectory) continue; + + const fileName = zipEntries[i].entryName; + + let shouldExclude = false; + + // Exclude noted files. + exclusionArr.forEach(exclusion => { + if (fileName.indexOf(exclusion) > -1) { + shouldExclude = true + } + }) + + const extractName = fileName.includes('/') ? fileName.substring(fileName.lastIndexOf('/')) : fileName + + // Extract the file. + if (shouldExclude) continue; + + writeFile(join(tempNativePath, extractName), zipEntries[i].getData(), (err) => { + if (err) { + logger.error('Error while extracting native library:', err) + } + }) + } + } + // No natives + else { + const dlInfo = lib.downloads + const artifact = dlInfo.artifact + const to = join(this.libPath, artifact.path) + const versionIndependentId = lib.name.substring(0, lib.name.lastIndexOf(':')) + libs[versionIndependentId] = to + } + } + } + + return libs + } + + /** + * Construct the argument array that will be passed to the JVM process. + * This function is for 1.12 and below. + * + * @param {Array.} mods An array of enabled mods which will be launched with this process. + * @param {string} tempNativePath The path to store the native libraries. + * @returns {Array.} An array containing the full JVM arguments for this process. + */ + private constructJVMArguments112(mods: Module[], tempNativePath: string) { + + let args: string[] = [] + + // Classpath Argument + args.push('-cp') + args.push(this.classpathArg(mods, tempNativePath).join(ProcessBuilder.classpathSeparator)) + + // Java Arguments + if (process.platform === 'darwin') { + args.push(`-Xdock:name=${ConfigManager.launcherName.replace(" ", "")}`) + args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns')) + } + args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.getID())) + args.push('-Xms' + ConfigManager.getMinRAM(this.server.getID())) + args = args.concat(ConfigManager.getJVMOptions(this.server.getID())) + args.push('-Djava.library.path=' + tempNativePath) + + // Main Java Class + args.push(this.forgeData.mainClass) + + // Forge Arguments + args = args.concat(this.resolveForgeArgs()) + + return args + } + + /** + * Construct the argument array that will be passed to the JVM process. + * This function is for 1.13+ + * + * Note: Required Libs https://github.com/MinecraftForge/MinecraftForge/blob/af98088d04186452cb364280340124dfd4766a5c/src/fmllauncher/java/net/minecraftforge/fml/loading/LibraryFinder.java#L82 + * + * @param {Array.} mods An array of enabled mods which will be launched with this process. + * @param {string} tempNativePath The path to store the native libraries. + * @returns {Array.} An array containing the full JVM arguments for this process. + */ + private constructJVMArguments113(mods: Module, tempNativePath: string) { + + const argDiscovery = /\${*(.*)}/ + + // JVM Arguments First + let args: string[] = this.versionData.arguments.jvm + + // Debug securejarhandler + // args.push('-Dbsl.debug=true') + + if (this.forgeData.arguments.jvm != null) { + for (const argStr of this.forgeData.arguments.jvm) { + args.push(argStr + .replaceAll('${library_directory}', this.libPath) + .replaceAll('${classpath_separator}', ProcessBuilder.classpathSeparator) + .replaceAll('${version_name}', this.forgeData.id) + ) + } + } + + //args.push('-Dlog4j.configurationFile=D:\\WesterosCraft\\game\\common\\assets\\log_configs\\client-1.12.xml') + + // Java Arguments + if (process.platform === 'darwin') { + args.push(`-Xdock:name=${ConfigManager.launcherName.replace(" ", "")}`) + args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns')) + } + args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.getID())) + args.push('-Xms' + ConfigManager.getMinRAM(this.server.getID())) + args = args.concat(ConfigManager.getJVMOptions(this.server.getID())) + + // Main Java Class + args.push(this.forgeData.mainClass) + + // Vanilla Arguments + args = args.concat(this.versionData.arguments.game) + + for (let i = 0; i < args.length; i++) { + if (typeof args[i] === 'object' && args[i].rules != null) { + + let checksum = 0 + for (let rule of args[i].rules) { + if (rule.os != null) { + if (rule.os.name === Library.mojangFriendlyOS() + && (rule.os.version == null || new RegExp(rule.os.version).test(os.release()))) { + if (rule.action === 'allow') { + checksum++ + } + } else { + if (rule.action === 'disallow') { + checksum++ + } + } + } else if (rule.features != null) { + // We don't have many 'features' in the index at the moment. + // This should be fine for a while. + if (rule.features.has_custom_resolution != null && rule.features.has_custom_resolution === true) { + if (ConfigManager.getFullscreen()) { + args[i].value = [ + '--fullscreen', + 'true' + ] + } + checksum++ + } + } + } + + // TODO splice not push + if (checksum === args[i].rules.length) { + if (typeof args[i].value === 'string') { + args[i] = args[i].value + } else if (typeof args[i].value === 'object') { + //args = args.concat(args[i].value) + args.splice(i, 1, ...args[i].value) + } + + // Decrement i to reprocess the resolved value + i-- + } else { + args[i] = null + } + + } else if (typeof args[i] === 'string') { + if (argDiscovery.test(args[i])) { + const identifier = args[i].match(argDiscovery)[1] + let val = null + switch (identifier) { + case 'auth_player_name': + val = this.authUser.displayName.trim() + break + case 'version_name': + //val = versionData.id + val = this.server.getID() + break + case 'game_directory': + val = this.gameDir + break + case 'assets_root': + val = join(this.commonDir, 'assets') + break + case 'assets_index_name': + val = this.versionData.assets + break + case 'auth_uuid': + val = this.authUser.uuid.trim() + break + case 'auth_access_token': + val = this.authUser.accessToken + break + case 'user_type': + val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang' + break + case 'version_type': + val = this.versionData.type + break + case 'resolution_width': + val = ConfigManager.getGameWidth() + break + case 'resolution_height': + val = ConfigManager.getGameHeight() + break + case 'natives_directory': + val = args[i].replace(argDiscovery, tempNativePath) + break + case 'launcher_name': + val = args[i].replace(argDiscovery, ConfigManager.launcherName) + break + case 'launcher_version': + val = args[i].replace(argDiscovery, this.launcherVersion) + break + case 'classpath': + val = this.classpathArg(mods, tempNativePath).join(ProcessBuilder.classpathSeparator) + break + } + if (val != null) { + args[i] = val + } + } + } + } + + // Autoconnect + let isAutoconnectBroken + try { + isAutoconnectBroken = MinecraftUtil.isAutoconnectBroken(this.forgeData.id.split('-')[2]) + } catch (err) { + logger.error(err) + logger.error('Forge version format changed.. assuming autoconnect works.') + logger.debug('Forge version:', this.forgeData.id) + } + + if (isAutoconnectBroken) { + logger.error('Server autoconnect disabled on Forge 1.15.2 for builds earlier than 31.2.15 due to OpenGL Stack Overflow issue.') + logger.error('Please upgrade your Forge version to at least 31.2.15!') + } else { + this.processAutoConnectArg(args) + } + + + // Forge Specific Arguments + args = args.concat(this.forgeData.arguments.game) + + // Filter null values + args = args.filter(arg => { + return arg != null + }) + + return args + } + + private lteMinorVersion(version: number) { + return Number(this.forgeData.id.split('-')[0].split('.')[1]) <= version; + } + + + /** + * Test to see if this version of forge requires the absolute: prefix + * on the modListFile repository field. + */ + private requiresAbsolute() { + try { + if (this.lteMinorVersion(9)) { + return false + } + + const ver = this.forgeData.id.split('-')[2] + const pts = ver.split('.') + const min = [14, 23, 3, 2655] + for (let i = 0; i < pts.length; i++) { + const parsed = Number.parseInt(pts[i]) + if (parsed < min[i]) { + return false + } else if (parsed > min[i]) { + return true + } + } + } catch (err) { + // We know old forge versions follow this format. + // Error must be caused by newer version. + } + + // Equal or errored + return true + } + + /** + * Resolve the arguments required by forge. + * + * @returns {Array.} An array containing the arguments required by forge. + */ + private resolveForgeArgs() { + const mcArgs = this.forgeData.minecraftArguments.split(' ') + const argDiscovery = /\${*(.*)}/ + + // Replace the declared variables with their proper values. + for (let i = 0; i < mcArgs.length; ++i) { + if (argDiscovery.test(mcArgs[i])) { + const identifier = mcArgs[i].match(argDiscovery)[1] + let val = null + switch (identifier) { + case 'auth_player_name': + val = this.authUser.displayName.trim() + break + case 'version_name': + //val = versionData.id + val = this.server.getID() + break + case 'game_directory': + val = this.gameDir + break + case 'assets_root': + val = join(this.commonDir, 'assets') + break + case 'assets_index_name': + val = this.versionData.assets + break + case 'auth_uuid': + val = this.authUser.uuid.trim() + break + case 'auth_access_token': + val = this.authUser.accessToken + break + case 'user_type': + val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang' + break + case 'user_properties': // 1.8.9 and below. + val = '{}' + break + case 'version_type': + val = this.versionData.type + break + } + if (val != null) { + mcArgs[i] = val + } + } + } + + // Autoconnect to the selected server. + this.processAutoConnectArg(mcArgs) + + // Prepare game resolution + if (ConfigManager.getFullscreen()) { + mcArgs.push('--fullscreen') + mcArgs.push(true) + } else { + mcArgs.push('--width') + mcArgs.push(ConfigManager.getGameWidth()) + mcArgs.push('--height') + mcArgs.push(ConfigManager.getGameHeight()) + } + + // Mod List File Argument + mcArgs.push('--modListFile') + if (this.lteMinorVersion(9)) { + mcArgs.push(basename(this.fmlDir)) + } else { + mcArgs.push('absolute:' + this.fmlDir) + } + + + // LiteLoader + if (this.usingLiteLoader) { + mcArgs.push('--modRepo') + mcArgs.push(this.llDir) + + // Set first arg to liteloader tweak class + mcArgs.unshift('com.mumfrey.liteloader.launch.LiteLoaderTweaker') + mcArgs.unshift('--tweakClass') + } + + return mcArgs + } + + + private processAutoConnectArg(args: string[]) { + if (ConfigManager.getAutoConnect() && this.server.isAutoConnect()) { + const serverURL = new URL('my://' + this.server.getAddress()) + args.push('--server') + args.push(serverURL.hostname) + if (serverURL.port) { + args.push('--port') + args.push(serverURL.port) + } + } + } + +} \ No newline at end of file diff --git a/src/services/ServerStatus.ts b/src/services/ServerStatus.ts new file mode 100644 index 00000000..4bc6acc9 --- /dev/null +++ b/src/services/ServerStatus.ts @@ -0,0 +1,66 @@ +import * as net from "net"; + +export class ServerStatus { + + /** + * Retrieves the status of a minecraft server. + * + * @param {string} address The server address. + * @param {number} port Optional. The port of the server. Defaults to 25565. + * @returns {Promise.} A promise which resolves to an object containing + * status information. + */ + public static getStatus(address: string, port: number = 25565) { + + if (port === null || typeof port !== 'number') { + port = 25565 + } + + return new Promise((resolve, reject) => { + const socket = net.connect(port, address, () => { + let buff = Buffer.from([0xFE, 0x01]) + socket.write(buff) + }) + + socket.setTimeout(2500, () => { + socket.end() + reject({ + code: 'ETIMEDOUT', + errno: 'ETIMEDOUT', + address, + port + }) + }) + + socket.on('data', (data) => { + if (data != null && data.length > 0) { + let server_info = data.toString().split('\x00\x00\x00') + const NUM_FIELDS = 6 + if (server_info != null && server_info.length >= NUM_FIELDS) { + resolve({ + online: true, + version: server_info[2].replace(/\u0000/g, ''), + motd: server_info[3].replace(/\u0000/g, ''), + onlinePlayers: server_info[4].replace(/\u0000/g, ''), + maxPlayers: server_info[5].replace(/\u0000/g, '') + }) + } else { + resolve({ + online: false + }) + } + } + socket.end() + }) + + socket.on('error', (err) => { + socket.destroy() + reject(err) + // ENOTFOUND = Unable to resolve. + // ECONNREFUSED = Unable to connect to port. + }) + }) + + + } +} \ No newline at end of file diff --git a/src/util/DistroTypes.ts b/src/util/DistroTypes.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/util/MinecraftUtil.ts b/src/util/MinecraftUtil.ts new file mode 100644 index 00000000..2bc65899 --- /dev/null +++ b/src/util/MinecraftUtil.ts @@ -0,0 +1,68 @@ +export class MinecraftUtil { + + /** + * 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. + */ + public static mcVersionAtLeast(desired: string, actual: string) { + const des = desired.split("."); + const act = actual.split("."); + + for (let i = 0; i < des.length; i++) { + if (!(parseInt(act[i]) >= parseInt(des[i]))) { + return false; + } + } + return true; + } + + public static isForgeGradle3(mcVersion: string, forgeVersion: string) { + + if (this.mcVersionAtLeast('1.13', mcVersion)) { + return true + } + + try { + + const forgeVer = forgeVersion.split('-')[1] + + const maxFG2 = [14, 23, 5, 2847] + const verSplit = forgeVer.split('.').map(v => Number(v)) + + for (let i = 0; i < maxFG2.length; i++) { + if (verSplit[i] > maxFG2[i]) { + return true + } else if (verSplit[i] < maxFG2[i]) { + return false + } + } + + return false + + } catch (err) { + throw new Error('Forge version is complex (changed).. launcher requires a patch.') + } + } + + public static isAutoconnectBroken(forgeVersion: string) { + + const minWorking = [31, 2, 15] + const verSplit = forgeVersion.split('.').map(v => Number(v)) + + if (verSplit[0] === 31) { + for (let i = 0; i < minWorking.length; i++) { + if (verSplit[i] > minWorking[i]) { + return false + } else if (verSplit[i] < minWorking[i]) { + return true + } + } + } + + return false + } + +} diff --git a/src/util/System.ts b/src/util/System.ts new file mode 100644 index 00000000..3b244646 --- /dev/null +++ b/src/util/System.ts @@ -0,0 +1,10 @@ +import os from 'os'; + +export function resolveMaxRAM() { + const mem = os.totalmem(); + return mem >= 8000000000 ? "4G" : mem >= 6000000000 ? "3G" : "2G"; +} + +export function resolveMinRAM() { + return resolveMaxRAM(); +} \ No newline at end of file diff --git a/src/util/isDev.ts b/src/util/isDev.ts new file mode 100644 index 00000000..789b9be6 --- /dev/null +++ b/src/util/isDev.ts @@ -0,0 +1,15 @@ +const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV ?? "", 10) === 1 +const isEnvSet = 'ELECTRON_IS_DEV' in process.env + +export class DevUtil { + private static enforceDevMode = false; + + public static get IsDev() { + //@ts-ignore + return this.enforceDevMode ?? isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)) + } + + public static set IsDev(value) { + this.enforceDevMode = value; + } +} \ No newline at end of file diff --git a/src/views/app.ejs b/src/views/app.ejs new file mode 100644 index 00000000..e829fa14 --- /dev/null +++ b/src/views/app.ejs @@ -0,0 +1,55 @@ + + + + Helios Launcher + + + + + + + <%- include('frame') %> +
+ <%- include('welcome') %> + <%- include('login') %> + <%- include('waiting') %> + <%- include('loginOptions') %> + <%- include('settings') %> + <%- include('landing') %> +
+ <%- include('overlay') %> +
+
+
+ + +
+
+
+ + + \ No newline at end of file diff --git a/src/views/frame.ejs b/src/views/frame.ejs new file mode 100644 index 00000000..c2aaf337 --- /dev/null +++ b/src/views/frame.ejs @@ -0,0 +1,33 @@ +
+
+
+
+ <%if (process.platform === 'darwin') { %> +
+
+ + + +
+
+ <% } else{ %> +
+
+ Helios Launcher +
+
+ + + +
+
+ <% } %> +
+
+
\ No newline at end of file diff --git a/src/views/landing.ejs b/src/views/landing.ejs new file mode 100644 index 00000000..7e747818 --- /dev/null +++ b/src/views/landing.ejs @@ -0,0 +1,220 @@ + \ No newline at end of file diff --git a/src/views/login.ejs b/src/views/login.ejs new file mode 100644 index 00000000..7ecc4a6c --- /dev/null +++ b/src/views/login.ejs @@ -0,0 +1,65 @@ + \ No newline at end of file diff --git a/src/views/loginOptions.ejs b/src/views/loginOptions.ejs new file mode 100644 index 00000000..36af37e0 --- /dev/null +++ b/src/views/loginOptions.ejs @@ -0,0 +1,34 @@ + \ No newline at end of file diff --git a/src/views/overlay.ejs b/src/views/overlay.ejs new file mode 100644 index 00000000..0c18aef4 --- /dev/null +++ b/src/views/overlay.ejs @@ -0,0 +1,41 @@ + \ No newline at end of file diff --git a/src/views/settings.ejs b/src/views/settings.ejs new file mode 100644 index 00000000..aa1fa764 --- /dev/null +++ b/src/views/settings.ejs @@ -0,0 +1,393 @@ + \ No newline at end of file diff --git a/src/views/waiting.ejs b/src/views/waiting.ejs new file mode 100644 index 00000000..11c7e4d2 --- /dev/null +++ b/src/views/waiting.ejs @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/src/views/welcome.ejs b/src/views/welcome.ejs new file mode 100644 index 00000000..077bbaed --- /dev/null +++ b/src/views/welcome.ejs @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/src/views/welcome.ts b/src/views/welcome.ts new file mode 100644 index 00000000..d2bd7cc5 --- /dev/null +++ b/src/views/welcome.ts @@ -0,0 +1,9 @@ +/** + * Script for welcome.ejs + */ +document.getElementById('welcomeButton')?.addEventListener('click', e => { + loginOptionsCancelEnabled(false) // False by default, be explicit. + loginOptionsViewOnLoginSuccess = VIEWS.landing + loginOptionsViewOnLoginCancel = VIEWS.loginOptions + switchView(VIEWS.welcome, VIEWS.loginOptions) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..fc7c3f2f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "stripInternal": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "allowSyntheticDefaultImports": true, + "outDir": "./dist", + "lib": ["ES2022", "dom"], + "moduleResolution": "node" + } +}