Compare commits
46 Commits
master
...
ts-refacto
Author | SHA1 | Date | |
---|---|---|---|
|
f139992e06 | ||
|
df74b0e67c | ||
|
6a04ef0f62 | ||
|
e86992a7ee | ||
|
9793c4072d | ||
|
043f85c0dc | ||
|
0cbd39b79c | ||
|
67e42ead78 | ||
|
cb68c34abe | ||
|
574b362d12 | ||
|
dc00e6104b | ||
|
dbc49f51dd | ||
|
e76eb91ac9 | ||
|
c7b95f3719 | ||
|
571b788e25 | ||
|
27f95235f8 | ||
|
3838729da7 | ||
|
feb9b98b2a | ||
|
bc43d842e3 | ||
|
15fd2c842a | ||
|
53d5599545 | ||
|
10c88aa7d0 | ||
|
c670b7d88d | ||
|
691cf937fc | ||
|
33e454e529 | ||
|
ab51b84bea | ||
|
d9394432d2 | ||
|
18fbfe4289 | ||
|
7ef375db7a | ||
|
dc7386f19d | ||
|
9a67087766 | ||
|
5944f70a2a | ||
|
c718cc741a | ||
|
f1a7e39e13 | ||
|
a9d6f2021d | ||
|
af6066115c | ||
|
3fcfa492af | ||
|
28f78f719e | ||
|
c9147d86a8 | ||
|
8764c52fcc | ||
|
75a7e0f713 | ||
|
9097bafb5d | ||
|
761a46060b | ||
|
3cde9ef75e | ||
|
b9536ed014 | ||
|
9cb10b70af |
@ -1,66 +1,48 @@
|
||||
{
|
||||
"env": {
|
||||
"es2017": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module"
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"windows"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-var": [
|
||||
"error"
|
||||
"quotes": "off",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"no-console": [
|
||||
0
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"no-control-regex": [
|
||||
0
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"ignoreRestSiblings": false,
|
||||
"argsIgnorePattern": "reject"
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": false
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"no-async-promise-executor": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [ "app/assets/js/scripts/*.js" ],
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
0
|
||||
],
|
||||
"no-undef": [
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
66
.eslintrc.json.old
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"env": {
|
||||
"es2017": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"windows"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-var": [
|
||||
"error"
|
||||
],
|
||||
"no-console": [
|
||||
0
|
||||
],
|
||||
"no-control-regex": [
|
||||
0
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"ignoreRestSiblings": false,
|
||||
"argsIgnorePattern": "reject"
|
||||
}
|
||||
],
|
||||
"no-async-promise-executor": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [ "src/scripts/*.js" ],
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
0
|
||||
],
|
||||
"no-undef": [
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
2
.gitignore
vendored
@ -4,3 +4,5 @@
|
||||
/target/
|
||||
/logs/
|
||||
/dist/
|
||||
/out/
|
||||
/old/
|
17
README.md
@ -1,4 +1,4 @@
|
||||
<p align="center"><img src="./app/assets/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
|
||||
<p align="center"><img src="./static/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
|
||||
|
||||
<h1 align="center">Helios Launcher</h1>
|
||||
|
||||
@ -177,19 +177,6 @@ Note that you **cannot** open the DevTools window while using this debug configu
|
||||
|
||||
---
|
||||
|
||||
### Note on Third-Party Usage
|
||||
|
||||
You may use this software in your own project so long as the following conditions are met.
|
||||
|
||||
* Credit is expressly given to the original authors (Daniel Scalzi).
|
||||
* Include a link to the original source on the launcher's About page.
|
||||
* Credit the authors and provide a link to the original source in any publications or download pages.
|
||||
* The source code remain **public** as a fork of this repository.
|
||||
|
||||
We reserve the right to update these conditions at any time, please check back periodically.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
* [Wiki][wiki]
|
||||
@ -210,3 +197,5 @@ The best way to contact the developers is on Discord.
|
||||
[chromedebugger]: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Debugger for Chrome'
|
||||
[discord]: https://discord.gg/zNWUXdt 'Discord'
|
||||
[wiki]: https://github.com/dscalzi/HeliosLauncher/wiki 'wiki'
|
||||
|
||||
© 2020 Daniel Scalzi All Rights Reserved
|
@ -1,68 +0,0 @@
|
||||
let target = require('./assetguard')[process.argv[2]]
|
||||
if(target == null){
|
||||
process.send({context: 'error', data: null, error: 'Invalid class name'})
|
||||
console.error('Invalid class name passed to argv[2], cannot continue.')
|
||||
process.exit(1)
|
||||
}
|
||||
let tracker = new target(...(process.argv.splice(3)))
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
//const tracker = new AssetGuard(process.argv[2], process.argv[3])
|
||||
console.log('AssetExec Started')
|
||||
|
||||
// Temporary for debug purposes.
|
||||
process.on('unhandledRejection', r => console.log(r))
|
||||
|
||||
function assignListeners(){
|
||||
tracker.on('validate', (data) => {
|
||||
process.send({context: 'validate', data})
|
||||
})
|
||||
tracker.on('progress', (data, acc, total) => {
|
||||
process.send({context: 'progress', data, value: acc, total, percent: parseInt((acc/total)*100)})
|
||||
})
|
||||
tracker.on('complete', (data, ...args) => {
|
||||
process.send({context: 'complete', data, args})
|
||||
})
|
||||
tracker.on('error', (data, error) => {
|
||||
process.send({context: 'error', data, error})
|
||||
})
|
||||
}
|
||||
|
||||
assignListeners()
|
||||
|
||||
process.on('message', (msg) => {
|
||||
if(msg.task === 'execute'){
|
||||
const func = msg.function
|
||||
let nS = tracker[func] // Nonstatic context
|
||||
let iS = target[func] // Static context
|
||||
if(typeof nS === 'function' || typeof iS === 'function'){
|
||||
const f = typeof nS === 'function' ? nS : iS
|
||||
const res = f.apply(f === nS ? tracker : null, msg.argsArr)
|
||||
if(res instanceof Promise){
|
||||
res.then((v) => {
|
||||
process.send({result: v, context: func})
|
||||
}).catch((err) => {
|
||||
process.send({result: err.message || err, context: func})
|
||||
})
|
||||
} else {
|
||||
process.send({result: res, context: func})
|
||||
}
|
||||
} else {
|
||||
process.send({context: 'error', data: null, error: `Function ${func} not found on ${process.argv[2]}`})
|
||||
}
|
||||
} else if(msg.task === 'changeContext'){
|
||||
target = require('./assetguard')[msg.class]
|
||||
if(target == null){
|
||||
process.send({context: 'error', data: null, error: `Invalid class ${msg.class}`})
|
||||
} else {
|
||||
tracker = new target(...(msg.args))
|
||||
assignListeners()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
process.on('disconnect', () => {
|
||||
console.log('AssetExec Disconnected')
|
||||
process.exit(0)
|
||||
})
|
@ -1,99 +0,0 @@
|
||||
/**
|
||||
* AuthManager
|
||||
*
|
||||
* This module aims to abstract login procedures. Results from Mojang's REST api
|
||||
* are retrieved through our Mojang module. These results are processed and stored,
|
||||
* if applicable, in the config using the ConfigManager. All login procedures should
|
||||
* be made through this module.
|
||||
*
|
||||
* @module authmanager
|
||||
*/
|
||||
// Requirements
|
||||
const ConfigManager = require('./configmanager')
|
||||
const LoggerUtil = require('./loggerutil')
|
||||
const Mojang = require('./mojang')
|
||||
const logger = LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold')
|
||||
const loggerSuccess = LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold')
|
||||
|
||||
// Functions
|
||||
|
||||
/**
|
||||
* Add an account. This will authenticate the given credentials with Mojang's
|
||||
* authserver. The resultant data will be stored as an auth account in the
|
||||
* configuration database.
|
||||
*
|
||||
* @param {string} username The account username (email if migrated).
|
||||
* @param {string} password The account password.
|
||||
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
||||
*/
|
||||
exports.addAccount = async function(username, password){
|
||||
try {
|
||||
const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken())
|
||||
if(session.selectedProfile != null){
|
||||
const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
|
||||
if(ConfigManager.getClientToken() == null){
|
||||
ConfigManager.setClientToken(session.clientToken)
|
||||
}
|
||||
ConfigManager.save()
|
||||
return ret
|
||||
} else {
|
||||
throw new Error('NotPaidAccount')
|
||||
}
|
||||
|
||||
} catch (err){
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an account. This will invalidate the access token associated
|
||||
* with the account and then remove it from the database.
|
||||
*
|
||||
* @param {string} uuid The UUID of the account to be removed.
|
||||
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
|
||||
*/
|
||||
exports.removeAccount = async function(uuid){
|
||||
try {
|
||||
const authAcc = ConfigManager.getAuthAccount(uuid)
|
||||
await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
|
||||
ConfigManager.removeAuthAccount(uuid)
|
||||
ConfigManager.save()
|
||||
return Promise.resolve()
|
||||
} catch (err){
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the selected account with Mojang's authserver. If the account is not valid,
|
||||
* we will attempt to refresh the access token and update that value. If that fails, a
|
||||
* new login will be required.
|
||||
*
|
||||
* **Function is WIP**
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
exports.validateSelected = async function(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken())
|
||||
if(!isValid){
|
||||
try {
|
||||
const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken())
|
||||
ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
|
||||
ConfigManager.save()
|
||||
} catch(err) {
|
||||
logger.debug('Error while validating selected profile:', err)
|
||||
if(err && err.error === 'ForbiddenOperationException'){
|
||||
// What do we do?
|
||||
}
|
||||
logger.log('Account access token is invalid.')
|
||||
return false
|
||||
}
|
||||
loggerSuccess.log('Account access token validated.')
|
||||
return true
|
||||
} else {
|
||||
loggerSuccess.log('Account access token validated.')
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,688 +0,0 @@
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
const logger = require('./loggerutil')('%c[ConfigManager]', 'color: #a02d2a; font-weight: bold')
|
||||
|
||||
const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME)
|
||||
// TODO change
|
||||
const dataPath = path.join(sysRoot, '.westeroscraft')
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Three types of values:
|
||||
* Static = Explicitly declared.
|
||||
* Dynamic = Calculated by a private function.
|
||||
* Resolved = Resolved externally, defaults to null.
|
||||
*/
|
||||
const DEFAULT_CONFIG = {
|
||||
settings: {
|
||||
java: {
|
||||
minRAM: resolveMinRAM(),
|
||||
maxRAM: 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: dataPath
|
||||
}
|
||||
},
|
||||
newsCache: {
|
||||
date: null,
|
||||
content: null,
|
||||
dismissed: false
|
||||
},
|
||||
clientToken: null,
|
||||
selectedServer: null, // Resolved
|
||||
selectedAccount: null,
|
||||
authenticationDatabase: {},
|
||||
modConfigurations: []
|
||||
}
|
||||
|
||||
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.log('Configuration file contains malformed JSON or is corrupt.')
|
||||
logger.log('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.log('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']
|
||||
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]] = validateKeySet(srcObj[keys[i]], destObj[keys[i]])
|
||||
}
|
||||
}
|
||||
return destObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this is the first time the user has launched the
|
||||
* application. This is determined by the existance of the data path.
|
||||
*
|
||||
* @returns {boolean} True if this is the first launch, otherwise false.
|
||||
*/
|
||||
exports.isFirstLaunch = function(){
|
||||
return 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.
|
||||
*/
|
||||
exports.getTempNativeFolder = function(){
|
||||
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.
|
||||
*/
|
||||
exports.getNewsCache = function(){
|
||||
return config.newsCache
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the new news cache object.
|
||||
*
|
||||
* @param {Object} newsCache The new news cache object.
|
||||
*/
|
||||
exports.setNewsCache = function(newsCache){
|
||||
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).
|
||||
*/
|
||||
exports.setNewsCacheDismissed = function(dismissed){
|
||||
config.newsCache.dismissed = dismissed
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the common directory for shared
|
||||
* game files (assets, libraries, etc).
|
||||
*
|
||||
* @returns {string} The launcher's common directory.
|
||||
*/
|
||||
exports.getCommonDirectory = function(){
|
||||
return path.join(exports.getDataDirectory(), 'common')
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the instance directory for the per
|
||||
* server game directories.
|
||||
*
|
||||
* @returns {string} The launcher's instance directory.
|
||||
*/
|
||||
exports.getInstanceDirectory = function(){
|
||||
return path.join(exports.getDataDirectory(), 'instances')
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the launcher's Client Token.
|
||||
* There is no default client token.
|
||||
*
|
||||
* @returns {string} The launcher's Client Token.
|
||||
*/
|
||||
exports.getClientToken = function(){
|
||||
return config.clientToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the launcher's Client Token.
|
||||
*
|
||||
* @param {string} clientToken The launcher's new Client Token.
|
||||
*/
|
||||
exports.setClientToken = function(clientToken){
|
||||
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.
|
||||
*/
|
||||
exports.getSelectedServer = function(def = false){
|
||||
return !def ? config.selectedServer : DEFAULT_CONFIG.clientToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ID of the selected serverpack.
|
||||
*
|
||||
* @param {string} serverID The ID of the new selected serverpack.
|
||||
*/
|
||||
exports.setSelectedServer = function(serverID){
|
||||
config.selectedServer = serverID
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of each account currently authenticated by the launcher.
|
||||
*
|
||||
* @returns {Array.<Object>} 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 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.updateAuthAccount = function(uuid, accessToken){
|
||||
config.authenticationDatabase[uuid].accessToken = accessToken
|
||||
return 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 {Object} The authenticated account object created by this action.
|
||||
*/
|
||||
exports.addAuthAccount = function(uuid, accessToken, username, displayName){
|
||||
config.selectedAccount = uuid
|
||||
config.authenticationDatabase[uuid] = {
|
||||
accessToken,
|
||||
username: username.trim(),
|
||||
uuid: uuid.trim(),
|
||||
displayName: displayName.trim()
|
||||
}
|
||||
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.<Object>} An array of each stored mod configuration.
|
||||
*/
|
||||
exports.getModConfigurations = function(){
|
||||
return config.modConfigurations
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the array of stored mod configurations.
|
||||
*
|
||||
* @param {Array.<Object>} 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<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.
|
||||
*/
|
||||
exports.setModConfiguration = function(serverid, configuration){
|
||||
const cfgs = config.modConfigurations
|
||||
for(let i=0; i<cfgs.length; i++){
|
||||
if(cfgs[i].id === serverid){
|
||||
cfgs[i] = configuration
|
||||
return
|
||||
}
|
||||
}
|
||||
cfgs.push(configuration)
|
||||
}
|
||||
|
||||
// User Configurable Settings
|
||||
|
||||
// Java Settings
|
||||
|
||||
/**
|
||||
* 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 {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {string} The minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.getMinRAM = function(def = false){
|
||||
return !def ? config.settings.java.minRAM : DEFAULT_CONFIG.settings.java.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} minRAM The new minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.setMinRAM = function(minRAM){
|
||||
config.settings.java.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 {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {string} The maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.getMaxRAM = function(def = false){
|
||||
return !def ? config.settings.java.maxRAM : resolveMaxRAM()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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} maxRAM The new maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.setMaxRAM = function(maxRAM){
|
||||
config.settings.java.maxRAM = maxRAM
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the path of the Java Executable.
|
||||
*
|
||||
* This is a resolved configuration value and defaults to null until externally assigned.
|
||||
*
|
||||
* @returns {string} The path of the Java Executable.
|
||||
*/
|
||||
exports.getJavaExecutable = function(){
|
||||
return config.settings.java.executable
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path of the Java Executable.
|
||||
*
|
||||
* @param {string} executable The new path of the Java Executable.
|
||||
*/
|
||||
exports.setJavaExecutable = function(executable){
|
||||
config.settings.java.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 {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {Array.<string>} An array of the additional arguments for JVM initialization.
|
||||
*/
|
||||
exports.getJVMOptions = function(def = false){
|
||||
return !def ? config.settings.java.jvmOptions : 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.<string>} jvmOptions An array of the new additional arguments for JVM
|
||||
* initialization.
|
||||
*/
|
||||
exports.setJVMOptions = function(jvmOptions){
|
||||
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.
|
||||
*/
|
||||
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
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
// Work in progress
|
||||
const logger = require('./loggerutil')('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold')
|
||||
|
||||
const {Client} = require('discord-rpc')
|
||||
|
||||
let client
|
||||
let activity
|
||||
|
||||
exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){
|
||||
client = new Client({ transport: 'ipc' })
|
||||
|
||||
activity = {
|
||||
details: initialDetails,
|
||||
state: 'Server: ' + servSettings.shortId,
|
||||
largeImageKey: servSettings.largeImageKey,
|
||||
largeImageText: servSettings.largeImageText,
|
||||
smallImageKey: genSettings.smallImageKey,
|
||||
smallImageText: genSettings.smallImageText,
|
||||
startTimestamp: new Date().getTime(),
|
||||
instance: false
|
||||
}
|
||||
|
||||
client.on('ready', () => {
|
||||
logger.log('Discord RPC Connected')
|
||||
client.setActivity(activity)
|
||||
})
|
||||
|
||||
client.login({clientId: genSettings.clientId}).catch(error => {
|
||||
if(error.message.includes('ENOENT')) {
|
||||
logger.log('Unable to initialize Discord Rich Presence, no client detected.')
|
||||
} else {
|
||||
logger.log('Unable to initialize Discord Rich Presence: ' + error.message, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
exports.updateDetails = function(details){
|
||||
activity.details = details
|
||||
client.setActivity(activity)
|
||||
}
|
||||
|
||||
exports.shutdownRPC = function(){
|
||||
if(!client) return
|
||||
client.clearActivity()
|
||||
client.destroy()
|
||||
client = null
|
||||
activity = null
|
||||
}
|
@ -1,605 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const request = require('request')
|
||||
|
||||
const ConfigManager = require('./configmanager')
|
||||
const logger = require('./loggerutil')('%c[DistroManager]', 'color: #a02d2a; font-weight: bold')
|
||||
|
||||
/**
|
||||
* Represents the download information
|
||||
* for a specific module.
|
||||
*/
|
||||
class Artifact {
|
||||
|
||||
/**
|
||||
* Parse a JSON object into an Artifact.
|
||||
*
|
||||
* @param {Object} json A JSON object representing an Artifact.
|
||||
*
|
||||
* @returns {Artifact} The parsed Artifact.
|
||||
*/
|
||||
static fromJSON(json){
|
||||
return Object.assign(new Artifact(), json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MD5 hash of the artifact. This value may
|
||||
* be undefined for artifacts which are not to be
|
||||
* validated and updated.
|
||||
*
|
||||
* @returns {string} The MD5 hash of the Artifact or undefined.
|
||||
*/
|
||||
getHash(){
|
||||
return this.MD5
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} The download size of the artifact.
|
||||
*/
|
||||
getSize(){
|
||||
return this.size
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The download url of the artifact.
|
||||
*/
|
||||
getURL(){
|
||||
return this.url
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The artifact's destination path.
|
||||
*/
|
||||
getPath(){
|
||||
return this.path
|
||||
}
|
||||
|
||||
}
|
||||
exports.Artifact
|
||||
|
||||
/**
|
||||
* Represents a the requirement status
|
||||
* of a module.
|
||||
*/
|
||||
class Required {
|
||||
|
||||
/**
|
||||
* Parse a JSON object into a Required object.
|
||||
*
|
||||
* @param {Object} json A JSON object representing a Required object.
|
||||
*
|
||||
* @returns {Required} The parsed Required object.
|
||||
*/
|
||||
static fromJSON(json){
|
||||
if(json == null){
|
||||
return new Required(true, true)
|
||||
} else {
|
||||
return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(value, def){
|
||||
this.value = value
|
||||
this.default = def
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value for a required object. If a module
|
||||
* is not required, this value determines whether or not
|
||||
* it is enabled by default.
|
||||
*
|
||||
* @returns {boolean} The default enabled value.
|
||||
*/
|
||||
isDefault(){
|
||||
return this.default
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether or not the module is required.
|
||||
*/
|
||||
isRequired(){
|
||||
return this.value
|
||||
}
|
||||
|
||||
}
|
||||
exports.Required
|
||||
|
||||
/**
|
||||
* Represents a module.
|
||||
*/
|
||||
class Module {
|
||||
|
||||
/**
|
||||
* Parse a JSON object into a Module.
|
||||
*
|
||||
* @param {Object} json A JSON object representing a Module.
|
||||
* @param {string} serverid The ID of the server to which this module belongs.
|
||||
*
|
||||
* @returns {Module} The parsed Module.
|
||||
*/
|
||||
static fromJSON(json, serverid){
|
||||
return new Module(json.id, json.name, json.type, json.required, json.artifact, json.subModules, serverid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default extension for a specific module type.
|
||||
*
|
||||
* @param {string} type The type of the module.
|
||||
*
|
||||
* @return {string} The default extension for the given type.
|
||||
*/
|
||||
static _resolveDefaultExtension(type){
|
||||
switch (type) {
|
||||
case exports.Types.Library:
|
||||
case exports.Types.ForgeHosted:
|
||||
case exports.Types.LiteLoader:
|
||||
case exports.Types.ForgeMod:
|
||||
return 'jar'
|
||||
case exports.Types.LiteMod:
|
||||
return 'litemod'
|
||||
case exports.Types.File:
|
||||
default:
|
||||
return 'jar' // There is no default extension really.
|
||||
}
|
||||
}
|
||||
|
||||
constructor(id, name, type, required, artifact, subModules, serverid) {
|
||||
this.identifier = id
|
||||
this.type = type
|
||||
this._resolveMetaData()
|
||||
this.name = name
|
||||
this.required = Required.fromJSON(required)
|
||||
this.artifact = Artifact.fromJSON(artifact)
|
||||
this._resolveArtifactPath(artifact.path, serverid)
|
||||
this._resolveSubModules(subModules, serverid)
|
||||
}
|
||||
|
||||
_resolveMetaData(){
|
||||
try {
|
||||
|
||||
const m0 = this.identifier.split('@')
|
||||
|
||||
this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type)
|
||||
|
||||
const m1 = m0[0].split(':')
|
||||
|
||||
this.artifactClassifier = m1[3] || undefined
|
||||
this.artifactVersion = m1[2] || '???'
|
||||
this.artifactID = m1[1] || '???'
|
||||
this.artifactGroup = m1[0] || '???'
|
||||
|
||||
} catch (err) {
|
||||
// Improper identifier
|
||||
logger.error('Improper ID for module', this.identifier, err)
|
||||
}
|
||||
}
|
||||
|
||||
_resolveArtifactPath(artifactPath, serverid){
|
||||
const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.getExtension()}`) : artifactPath
|
||||
|
||||
switch (this.type){
|
||||
case exports.Types.Library:
|
||||
case exports.Types.ForgeHosted:
|
||||
case exports.Types.LiteLoader:
|
||||
this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth)
|
||||
break
|
||||
case exports.Types.ForgeMod:
|
||||
case exports.Types.LiteMod:
|
||||
this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth)
|
||||
break
|
||||
case exports.Types.VersionManifest:
|
||||
this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`)
|
||||
break
|
||||
case exports.Types.File:
|
||||
default:
|
||||
this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_resolveSubModules(json, serverid){
|
||||
const arr = []
|
||||
if(json != null){
|
||||
for(let sm of json){
|
||||
arr.push(Module.fromJSON(sm, serverid))
|
||||
}
|
||||
}
|
||||
this.subModules = arr.length > 0 ? arr : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The full, unparsed module identifier.
|
||||
*/
|
||||
getIdentifier(){
|
||||
return this.identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The name of the module.
|
||||
*/
|
||||
getName(){
|
||||
return this.name
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Required} The required object declared by this module.
|
||||
*/
|
||||
getRequired(){
|
||||
return this.required
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Artifact} The artifact declared by this module.
|
||||
*/
|
||||
getArtifact(){
|
||||
return this.artifact
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The maven identifier of this module's artifact.
|
||||
*/
|
||||
getID(){
|
||||
return this.artifactID
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The maven group of this module's artifact.
|
||||
*/
|
||||
getGroup(){
|
||||
return this.artifactGroup
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The identifier without he version or extension.
|
||||
*/
|
||||
getVersionlessID(){
|
||||
return this.getGroup() + ':' + this.getID()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The identifier without the extension.
|
||||
*/
|
||||
getExtensionlessID(){
|
||||
return this.getIdentifier().split('@')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The version of this module's artifact.
|
||||
*/
|
||||
getVersion(){
|
||||
return this.artifactVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The classifier of this module's artifact
|
||||
*/
|
||||
getClassifier(){
|
||||
return this.artifactClassifier
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The extension of this module's artifact.
|
||||
*/
|
||||
getExtension(){
|
||||
return this.artifactExt
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether or not this module has sub modules.
|
||||
*/
|
||||
hasSubModules(){
|
||||
return this.subModules != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array.<Module>} An array of sub modules.
|
||||
*/
|
||||
getSubModules(){
|
||||
return this.subModules
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The type of the module.
|
||||
*/
|
||||
getType(){
|
||||
return this.type
|
||||
}
|
||||
|
||||
}
|
||||
exports.Module
|
||||
|
||||
/**
|
||||
* Represents a server configuration.
|
||||
*/
|
||||
class Server {
|
||||
|
||||
/**
|
||||
* Parse a JSON object into a Server.
|
||||
*
|
||||
* @param {Object} json A JSON object representing a Server.
|
||||
*
|
||||
* @returns {Server} The parsed Server object.
|
||||
*/
|
||||
static fromJSON(json){
|
||||
|
||||
const mdls = json.modules
|
||||
json.modules = []
|
||||
|
||||
const serv = Object.assign(new Server(), json)
|
||||
serv._resolveModules(mdls)
|
||||
|
||||
return serv
|
||||
}
|
||||
|
||||
_resolveModules(json){
|
||||
const arr = []
|
||||
for(let m of json){
|
||||
arr.push(Module.fromJSON(m, this.getID()))
|
||||
}
|
||||
this.modules = arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The ID of the server.
|
||||
*/
|
||||
getID(){
|
||||
return this.id
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The name of the server.
|
||||
*/
|
||||
getName(){
|
||||
return this.name
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The description of the server.
|
||||
*/
|
||||
getDescription(){
|
||||
return this.description
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The URL of the server's icon.
|
||||
*/
|
||||
getIcon(){
|
||||
return this.icon
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The version of the server configuration.
|
||||
*/
|
||||
getVersion(){
|
||||
return this.version
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The IP address of the server.
|
||||
*/
|
||||
getAddress(){
|
||||
return this.address
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The minecraft version of the server.
|
||||
*/
|
||||
getMinecraftVersion(){
|
||||
return this.minecraftVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether or not this server is the main
|
||||
* server. The main server is selected by the launcher when
|
||||
* no valid server is selected.
|
||||
*/
|
||||
isMainServer(){
|
||||
return this.mainServer
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether or not the server is autoconnect.
|
||||
* by default.
|
||||
*/
|
||||
isAutoConnect(){
|
||||
return this.autoconnect
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array.<Module>} An array of modules for this server.
|
||||
*/
|
||||
getModules(){
|
||||
return this.modules
|
||||
}
|
||||
|
||||
}
|
||||
exports.Server
|
||||
|
||||
/**
|
||||
* Represents the Distribution Index.
|
||||
*/
|
||||
class DistroIndex {
|
||||
|
||||
/**
|
||||
* Parse a JSON object into a DistroIndex.
|
||||
*
|
||||
* @param {Object} json A JSON object representing a DistroIndex.
|
||||
*
|
||||
* @returns {DistroIndex} The parsed Server object.
|
||||
*/
|
||||
static fromJSON(json){
|
||||
|
||||
const servers = json.servers
|
||||
json.servers = []
|
||||
|
||||
const distro = Object.assign(new DistroIndex(), json)
|
||||
distro._resolveServers(servers)
|
||||
distro._resolveMainServer()
|
||||
|
||||
return distro
|
||||
}
|
||||
|
||||
_resolveServers(json){
|
||||
const arr = []
|
||||
for(let s of json){
|
||||
arr.push(Server.fromJSON(s))
|
||||
}
|
||||
this.servers = arr
|
||||
}
|
||||
|
||||
_resolveMainServer(){
|
||||
|
||||
for(let serv of this.servers){
|
||||
if(serv.mainServer){
|
||||
this.mainServer = serv.id
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If no server declares default_selected, default to the first one declared.
|
||||
this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The version of the distribution index.
|
||||
*/
|
||||
getVersion(){
|
||||
return this.version
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The URL to the news RSS feed.
|
||||
*/
|
||||
getRSS(){
|
||||
return this.rss
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array.<Server>} An array of declared server configurations.
|
||||
*/
|
||||
getServers(){
|
||||
return this.servers
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a server configuration by its ID. If it does not
|
||||
* exist, null will be returned.
|
||||
*
|
||||
* @param {string} id The ID of the server.
|
||||
*
|
||||
* @returns {Server} The server configuration with the given ID or null.
|
||||
*/
|
||||
getServer(id){
|
||||
for(let serv of this.servers){
|
||||
if(serv.id === id){
|
||||
return serv
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main server.
|
||||
*
|
||||
* @returns {Server} The main server.
|
||||
*/
|
||||
getMainServer(){
|
||||
return this.mainServer != null ? this.getServer(this.mainServer) : null
|
||||
}
|
||||
|
||||
}
|
||||
exports.DistroIndex
|
||||
|
||||
exports.Types = {
|
||||
Library: 'Library',
|
||||
ForgeHosted: 'ForgeHosted',
|
||||
Forge: 'Forge', // Unimplemented
|
||||
LiteLoader: 'LiteLoader',
|
||||
ForgeMod: 'ForgeMod',
|
||||
LiteMod: 'LiteMod',
|
||||
File: 'File',
|
||||
VersionManifest: 'VersionManifest'
|
||||
}
|
||||
|
||||
let DEV_MODE = false
|
||||
|
||||
const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json')
|
||||
const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json')
|
||||
|
||||
let data = null
|
||||
|
||||
/**
|
||||
* @returns {Promise.<DistroIndex>}
|
||||
*/
|
||||
exports.pullRemote = function(){
|
||||
if(DEV_MODE){
|
||||
return exports.pullLocal()
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
|
||||
//const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/'
|
||||
const opts = {
|
||||
url: distroURL,
|
||||
timeout: 2500
|
||||
}
|
||||
const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json')
|
||||
request(opts, (error, resp, body) => {
|
||||
if(!error){
|
||||
|
||||
try {
|
||||
data = DistroIndex.fromJSON(JSON.parse(body))
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
|
||||
fs.writeFile(distroDest, body, 'utf-8', (err) => {
|
||||
if(!err){
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise.<DistroIndex>}
|
||||
*/
|
||||
exports.pullLocal = function(){
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => {
|
||||
if(!err){
|
||||
data = DistroIndex.fromJSON(JSON.parse(d))
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.setDevMode = function(value){
|
||||
if(value){
|
||||
logger.log('Developer mode enabled.')
|
||||
logger.log('If you don\'t know what that means, revert immediately.')
|
||||
} else {
|
||||
logger.log('Developer mode disabled.')
|
||||
}
|
||||
DEV_MODE = value
|
||||
}
|
||||
|
||||
exports.isDevMode = function(){
|
||||
return DEV_MODE
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {DistroIndex}
|
||||
*/
|
||||
exports.getDistribution = function(){
|
||||
return data
|
||||
}
|
@ -1,232 +0,0 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { shell } = require('electron')
|
||||
|
||||
// Group #1: File Name (without .disabled, if any)
|
||||
// Group #2: File Extension (jar, zip, or litemod)
|
||||
// Group #3: If it is disabled (if string 'disabled' is present)
|
||||
const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/
|
||||
const DISABLED_EXT = '.disabled'
|
||||
|
||||
const SHADER_REGEX = /^(.+)\.zip$/
|
||||
const SHADER_OPTION = /shaderPack=(.+)/
|
||||
const SHADER_DIR = 'shaderpacks'
|
||||
const SHADER_CONFIG = 'optionsshaders.txt'
|
||||
|
||||
/**
|
||||
* Validate that the given directory exists. If not, it is
|
||||
* created.
|
||||
*
|
||||
* @param {string} modsDir The path to the mods directory.
|
||||
*/
|
||||
exports.validateDir = function(dir) {
|
||||
fs.ensureDirSync(dir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for drop-in mods in both the mods folder and version
|
||||
* safe mods folder.
|
||||
*
|
||||
* @param {string} modsDir The path to the mods directory.
|
||||
* @param {string} version The minecraft version of the server configuration.
|
||||
*
|
||||
* @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]}
|
||||
* An array of objects storing metadata about each discovered mod.
|
||||
*/
|
||||
exports.scanForDropinMods = function(modsDir, version) {
|
||||
const modsDiscovered = []
|
||||
if(fs.existsSync(modsDir)){
|
||||
let modCandidates = fs.readdirSync(modsDir)
|
||||
let verCandidates = []
|
||||
const versionDir = path.join(modsDir, version)
|
||||
if(fs.existsSync(versionDir)){
|
||||
verCandidates = fs.readdirSync(versionDir)
|
||||
}
|
||||
for(let file of modCandidates){
|
||||
const match = MOD_REGEX.exec(file)
|
||||
if(match != null){
|
||||
modsDiscovered.push({
|
||||
fullName: match[0],
|
||||
name: match[1],
|
||||
ext: match[2],
|
||||
disabled: match[3] != null
|
||||
})
|
||||
}
|
||||
}
|
||||
for(let file of verCandidates){
|
||||
const match = MOD_REGEX.exec(file)
|
||||
if(match != null){
|
||||
modsDiscovered.push({
|
||||
fullName: path.join(version, match[0]),
|
||||
name: match[1],
|
||||
ext: match[2],
|
||||
disabled: match[3] != null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return modsDiscovered
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dropin mods.
|
||||
*
|
||||
* @param {FileList} files The files to add.
|
||||
* @param {string} modsDir The path to the mods directory.
|
||||
*/
|
||||
exports.addDropinMods = function(files, modsdir) {
|
||||
|
||||
exports.validateDir(modsdir)
|
||||
|
||||
for(let f of files) {
|
||||
if(MOD_REGEX.exec(f.name) != null) {
|
||||
fs.moveSync(f.path, path.join(modsdir, f.name))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a drop-in mod from the file system.
|
||||
*
|
||||
* @param {string} modsDir The path to the mods directory.
|
||||
* @param {string} fullName The fullName of the discovered mod to delete.
|
||||
*
|
||||
* @returns {boolean} True if the mod was deleted, otherwise false.
|
||||
*/
|
||||
exports.deleteDropinMod = function(modsDir, fullName){
|
||||
const res = shell.moveItemToTrash(path.join(modsDir, fullName))
|
||||
if(!res){
|
||||
shell.beep()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a discovered mod on or off. This is achieved by either
|
||||
* adding or disabling the .disabled extension to the local file.
|
||||
*
|
||||
* @param {string} modsDir The path to the mods directory.
|
||||
* @param {string} fullName The fullName of the discovered mod to toggle.
|
||||
* @param {boolean} enable Whether to toggle on or off the mod.
|
||||
*
|
||||
* @returns {Promise.<void>} A promise which resolves when the mod has
|
||||
* been toggled. If an IO error occurs the promise will be rejected.
|
||||
*/
|
||||
exports.toggleDropinMod = function(modsDir, fullName, enable){
|
||||
return new Promise((resolve, reject) => {
|
||||
const oldPath = path.join(modsDir, fullName)
|
||||
const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT)
|
||||
|
||||
fs.rename(oldPath, newPath, (err) => {
|
||||
if(err){
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a drop-in mod is enabled.
|
||||
*
|
||||
* @param {string} fullName The fullName of the discovered mod to toggle.
|
||||
* @returns {boolean} True if the mod is enabled, otherwise false.
|
||||
*/
|
||||
exports.isDropinModEnabled = function(fullName){
|
||||
return !fullName.endsWith(DISABLED_EXT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for shaderpacks inside the shaderpacks folder.
|
||||
*
|
||||
* @param {string} instanceDir The path to the server instance directory.
|
||||
*
|
||||
* @returns {{fullName: string, name: string}[]}
|
||||
* An array of objects storing metadata about each discovered shaderpack.
|
||||
*/
|
||||
exports.scanForShaderpacks = function(instanceDir){
|
||||
const shaderDir = path.join(instanceDir, SHADER_DIR)
|
||||
const packsDiscovered = [{
|
||||
fullName: 'OFF',
|
||||
name: 'Off (Default)'
|
||||
}]
|
||||
if(fs.existsSync(shaderDir)){
|
||||
let modCandidates = fs.readdirSync(shaderDir)
|
||||
for(let file of modCandidates){
|
||||
const match = SHADER_REGEX.exec(file)
|
||||
if(match != null){
|
||||
packsDiscovered.push({
|
||||
fullName: match[0],
|
||||
name: match[1]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return packsDiscovered
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the optionsshaders.txt file to locate the current
|
||||
* enabled pack. If the file does not exist, OFF is returned.
|
||||
*
|
||||
* @param {string} instanceDir The path to the server instance directory.
|
||||
*
|
||||
* @returns {string} The file name of the enabled shaderpack.
|
||||
*/
|
||||
exports.getEnabledShaderpack = function(instanceDir){
|
||||
exports.validateDir(instanceDir)
|
||||
|
||||
const optionsShaders = path.join(instanceDir, SHADER_CONFIG)
|
||||
if(fs.existsSync(optionsShaders)){
|
||||
const buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'})
|
||||
const match = SHADER_OPTION.exec(buf)
|
||||
if(match != null){
|
||||
return match[1]
|
||||
} else {
|
||||
console.warn('WARNING: Shaderpack regex failed.')
|
||||
}
|
||||
}
|
||||
return 'OFF'
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the enabled shaderpack.
|
||||
*
|
||||
* @param {string} instanceDir The path to the server instance directory.
|
||||
* @param {string} pack the file name of the shaderpack.
|
||||
*/
|
||||
exports.setEnabledShaderpack = function(instanceDir, pack){
|
||||
exports.validateDir(instanceDir)
|
||||
|
||||
const optionsShaders = path.join(instanceDir, SHADER_CONFIG)
|
||||
let buf
|
||||
if(fs.existsSync(optionsShaders)){
|
||||
buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'})
|
||||
buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`)
|
||||
} else {
|
||||
buf = `shaderPack=${pack}`
|
||||
}
|
||||
fs.writeFileSync(optionsShaders, buf, {encoding: 'utf-8'})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add shaderpacks.
|
||||
*
|
||||
* @param {FileList} files The files to add.
|
||||
* @param {string} instanceDir The path to the server instance directory.
|
||||
*/
|
||||
exports.addShaderpacks = function(files, instanceDir) {
|
||||
|
||||
const p = path.join(instanceDir, SHADER_DIR)
|
||||
|
||||
exports.validateDir(p)
|
||||
|
||||
for(let f of files) {
|
||||
if(SHADER_REGEX.exec(f.name) != null) {
|
||||
fs.moveSync(f.path, path.join(p, f.name))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
'use strict'
|
||||
const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1
|
||||
const isEnvSet = 'ELECTRON_IS_DEV' in process.env
|
||||
|
||||
module.exports = isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath))
|
@ -1,21 +0,0 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
|
||||
let lang
|
||||
|
||||
exports.loadLanguage = function(id){
|
||||
lang = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.json`))) || {}
|
||||
}
|
||||
|
||||
exports.query = function(id){
|
||||
let query = id.split('.')
|
||||
let res = lang
|
||||
for(let q of query){
|
||||
res = res[q]
|
||||
}
|
||||
return res === lang ? {} : res
|
||||
}
|
||||
|
||||
exports.queryJS = function(id){
|
||||
return exports.query(`js.${id}`)
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
class LoggerUtil {
|
||||
|
||||
constructor(prefix, style){
|
||||
this.prefix = prefix
|
||||
this.style = style
|
||||
}
|
||||
|
||||
log(){
|
||||
console.log.apply(null, [this.prefix, this.style, ...arguments])
|
||||
}
|
||||
|
||||
info(){
|
||||
console.info.apply(null, [this.prefix, this.style, ...arguments])
|
||||
}
|
||||
|
||||
warn(){
|
||||
console.warn.apply(null, [this.prefix, this.style, ...arguments])
|
||||
}
|
||||
|
||||
debug(){
|
||||
console.debug.apply(null, [this.prefix, this.style, ...arguments])
|
||||
}
|
||||
|
||||
error(){
|
||||
console.error.apply(null, [this.prefix, this.style, ...arguments])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = function (prefix, style){
|
||||
return new LoggerUtil(prefix, style)
|
||||
}
|
@ -1,271 +0,0 @@
|
||||
/**
|
||||
* Mojang
|
||||
*
|
||||
* This module serves as a minimal wrapper for Mojang's REST api.
|
||||
*
|
||||
* @module mojang
|
||||
*/
|
||||
// Requirements
|
||||
const request = require('request')
|
||||
const logger = require('./loggerutil')('%c[Mojang]', 'color: #a02d2a; font-weight: bold')
|
||||
|
||||
// Constants
|
||||
const minecraftAgent = {
|
||||
name: 'Minecraft',
|
||||
version: 1
|
||||
}
|
||||
const authpath = 'https://authserver.mojang.com'
|
||||
const statuses = [
|
||||
{
|
||||
service: 'sessionserver.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Multiplayer Session Service',
|
||||
essential: true
|
||||
},
|
||||
{
|
||||
service: 'authserver.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Authentication Service',
|
||||
essential: true
|
||||
},
|
||||
{
|
||||
service: 'textures.minecraft.net',
|
||||
status: 'grey',
|
||||
name: 'Minecraft Skins',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'api.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Public API',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'minecraft.net',
|
||||
status: 'grey',
|
||||
name: 'Minecraft.net',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'account.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Mojang Accounts Website',
|
||||
essential: false
|
||||
}
|
||||
]
|
||||
|
||||
// Functions
|
||||
|
||||
/**
|
||||
* Converts a Mojang status color to a hex value. Valid statuses
|
||||
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
|
||||
* to our project which represents an unknown status.
|
||||
*
|
||||
* @param {string} status A valid status code.
|
||||
* @returns {string} The hex color of the status code.
|
||||
*/
|
||||
exports.statusToHex = function(status){
|
||||
switch(status.toLowerCase()){
|
||||
case 'green':
|
||||
return '#a5c325'
|
||||
case 'yellow':
|
||||
return '#eac918'
|
||||
case 'red':
|
||||
return '#c32625'
|
||||
case 'grey':
|
||||
default:
|
||||
return '#848484'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of Mojang's services.
|
||||
* The response is condensed into a single object. Each service is
|
||||
* a key, where the value is an object containing a status and name
|
||||
* property.
|
||||
*
|
||||
* @see http://wiki.vg/Mojang_API#API_Status
|
||||
*/
|
||||
exports.status = function(){
|
||||
return new Promise((resolve, reject) => {
|
||||
request.get('https://status.mojang.com/check',
|
||||
{
|
||||
json: true,
|
||||
timeout: 2500
|
||||
},
|
||||
function(error, response, body){
|
||||
|
||||
if(error || response.statusCode !== 200){
|
||||
logger.warn('Unable to retrieve Mojang status.')
|
||||
logger.debug('Error while retrieving Mojang statuses:', error)
|
||||
//reject(error || response.statusCode)
|
||||
for(let i=0; i<statuses.length; i++){
|
||||
statuses[i].status = 'grey'
|
||||
}
|
||||
resolve(statuses)
|
||||
} else {
|
||||
for(let i=0; i<body.length; i++){
|
||||
const key = Object.keys(body[i])[0]
|
||||
inner:
|
||||
for(let j=0; j<statuses.length; j++){
|
||||
if(statuses[j].service === key) {
|
||||
statuses[j].status = body[i][key]
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve(statuses)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user with their Mojang credentials.
|
||||
*
|
||||
* @param {string} username The user's username, this is often an email.
|
||||
* @param {string} password The user's password.
|
||||
* @param {string} clientToken The launcher's Client Token.
|
||||
* @param {boolean} requestUser Optional. Adds user object to the reponse.
|
||||
* @param {Object} agent Optional. Provided by default. Adds user info to the response.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Authenticate
|
||||
*/
|
||||
exports.authenticate = function(username, password, clientToken, requestUser = true, agent = minecraftAgent){
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const body = {
|
||||
agent,
|
||||
username,
|
||||
password,
|
||||
requestUser
|
||||
}
|
||||
if(clientToken != null){
|
||||
body.clientToken = clientToken
|
||||
}
|
||||
|
||||
request.post(authpath + '/authenticate',
|
||||
{
|
||||
json: true,
|
||||
body
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
logger.error('Error during authentication.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 200){
|
||||
resolve(body)
|
||||
} else {
|
||||
reject(body || {code: 'ENOTFOUND'})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an access token. This should always be done before launching.
|
||||
* The client token should match the one used to create the access token.
|
||||
*
|
||||
* @param {string} accessToken The access token to validate.
|
||||
* @param {string} clientToken The launcher's client token.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Validate
|
||||
*/
|
||||
exports.validate = function(accessToken, clientToken){
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(authpath + '/validate',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
logger.error('Error during validation.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 403){
|
||||
resolve(false)
|
||||
} else {
|
||||
// 204 if valid
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates an access token. The clientToken must match the
|
||||
* token used to create the provided accessToken.
|
||||
*
|
||||
* @param {string} accessToken The access token to invalidate.
|
||||
* @param {string} clientToken The launcher's client token.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Invalidate
|
||||
*/
|
||||
exports.invalidate = function(accessToken, clientToken){
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(authpath + '/invalidate',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
logger.error('Error during invalidation.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 204){
|
||||
resolve()
|
||||
} else {
|
||||
reject(body)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh a user's authentication. This should be used to keep a user logged
|
||||
* in without asking them for their credentials again. A new access token will
|
||||
* be generated using a recent invalid access token. See Wiki for more info.
|
||||
*
|
||||
* @param {string} accessToken The old access token.
|
||||
* @param {string} clientToken The launcher's client token.
|
||||
* @param {boolean} requestUser Optional. Adds user object to the reponse.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Refresh
|
||||
*/
|
||||
exports.refresh = function(accessToken, clientToken, requestUser = true){
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(authpath + '/refresh',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken,
|
||||
requestUser
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(error){
|
||||
logger.error('Error during refresh.', error)
|
||||
reject(error)
|
||||
} else {
|
||||
if(response.statusCode === 200){
|
||||
resolve(body)
|
||||
} else {
|
||||
reject(body)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
const {ipcRenderer} = require('electron')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
const ConfigManager = require('./configmanager')
|
||||
const DistroManager = require('./distromanager')
|
||||
const LangLoader = require('./langloader')
|
||||
const logger = require('./loggerutil')('%c[Preloader]', 'color: #a02d2a; font-weight: bold')
|
||||
|
||||
logger.log('Loading..')
|
||||
|
||||
// Load ConfigManager
|
||||
ConfigManager.load()
|
||||
|
||||
// Load Strings
|
||||
LangLoader.loadLanguage('en_US')
|
||||
|
||||
function onDistroLoad(data){
|
||||
if(data != null){
|
||||
|
||||
// Resolve the selected server if its value has yet to be set.
|
||||
if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null){
|
||||
logger.log('Determining default selected server..')
|
||||
ConfigManager.setSelectedServer(data.getMainServer().getID())
|
||||
ConfigManager.save()
|
||||
}
|
||||
}
|
||||
ipcRenderer.send('distributionIndexDone', data != null)
|
||||
}
|
||||
|
||||
// Ensure Distribution is downloaded and cached.
|
||||
DistroManager.pullRemote().then((data) => {
|
||||
logger.log('Loaded distribution index.')
|
||||
|
||||
onDistroLoad(data)
|
||||
|
||||
}).catch((err) => {
|
||||
logger.log('Failed to load distribution index.')
|
||||
logger.error(err)
|
||||
|
||||
logger.log('Attempting to load an older version of the distribution index.')
|
||||
// Try getting a local copy, better than nothing.
|
||||
DistroManager.pullLocal().then((data) => {
|
||||
logger.log('Successfully loaded an older version of the distribution index.')
|
||||
|
||||
onDistroLoad(data)
|
||||
|
||||
|
||||
}).catch((err) => {
|
||||
|
||||
logger.log('Failed to load an older version of the distribution index.')
|
||||
logger.log('Application cannot run.')
|
||||
logger.error(err)
|
||||
|
||||
onDistroLoad(null)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
// Clean up temp dir incase previous launches ended unexpectedly.
|
||||
fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => {
|
||||
if(err){
|
||||
logger.warn('Error while cleaning natives directory', err)
|
||||
} else {
|
||||
logger.log('Cleaned natives directory.')
|
||||
}
|
||||
})
|
@ -1,738 +0,0 @@
|
||||
const AdmZip = require('adm-zip')
|
||||
const child_process = require('child_process')
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const { URL } = require('url')
|
||||
|
||||
const { Util, Library } = require('./assetguard')
|
||||
const ConfigManager = require('./configmanager')
|
||||
const DistroManager = require('./distromanager')
|
||||
const LoggerUtil = require('./loggerutil')
|
||||
|
||||
const logger = LoggerUtil('%c[ProcessBuilder]', 'color: #003996; font-weight: bold')
|
||||
|
||||
class ProcessBuilder {
|
||||
|
||||
constructor(distroServer, versionData, forgeData, authUser, launcherVersion){
|
||||
this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.getID())
|
||||
this.commonDir = ConfigManager.getCommonDirectory()
|
||||
this.server = distroServer
|
||||
this.versionData = versionData
|
||||
this.forgeData = forgeData
|
||||
this.authUser = authUser
|
||||
this.launcherVersion = launcherVersion
|
||||
this.fmlDir = path.join(this.gameDir, 'forgeModList.json')
|
||||
this.llDir = path.join(this.gameDir, 'liteloaderModList.json')
|
||||
this.libPath = path.join(this.commonDir, 'libraries')
|
||||
|
||||
this.usingLiteLoader = false
|
||||
this.llPath = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convienence method to run the functions typically used to build a process.
|
||||
*/
|
||||
build(){
|
||||
fs.ensureDirSync(this.gameDir)
|
||||
const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex'))
|
||||
process.throwDeprecation = true
|
||||
this.setupLiteLoader()
|
||||
logger.log('Using liteloader:', this.usingLiteLoader)
|
||||
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.getID()).mods, this.server.getModules())
|
||||
|
||||
// Mod list below 1.13
|
||||
if(!Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
|
||||
this.constructModList('forge', modObj.fMods, true)
|
||||
if(this.usingLiteLoader){
|
||||
this.constructModList('liteloader', modObj.lMods, true)
|
||||
}
|
||||
}
|
||||
|
||||
const uberModArr = modObj.fMods.concat(modObj.lMods)
|
||||
let args = this.constructJVMArguments(uberModArr, tempNativePath)
|
||||
|
||||
if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
|
||||
args = args.concat(this.constructModArguments(modObj.fMods))
|
||||
}
|
||||
|
||||
logger.log('Launch Arguments:', args)
|
||||
|
||||
const child = child_process.spawn(ConfigManager.getJavaExecutable(), args, {
|
||||
cwd: this.gameDir,
|
||||
detached: ConfigManager.getLaunchDetached()
|
||||
})
|
||||
|
||||
if(ConfigManager.getLaunchDetached()){
|
||||
child.unref()
|
||||
}
|
||||
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stderr.setEncoding('utf8')
|
||||
|
||||
const loggerMCstdout = LoggerUtil('%c[Minecraft]', 'color: #36b030; font-weight: bold')
|
||||
const loggerMCstderr = LoggerUtil('%c[Minecraft]', 'color: #b03030; font-weight: bold')
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
loggerMCstdout.log(data)
|
||||
})
|
||||
child.stderr.on('data', (data) => {
|
||||
loggerMCstderr.log(data)
|
||||
})
|
||||
child.on('close', (code, signal) => {
|
||||
logger.log('Exited with code', code)
|
||||
fs.remove(tempNativePath, (err) => {
|
||||
if(err){
|
||||
logger.warn('Error while deleting temp dir', err)
|
||||
} else {
|
||||
logger.log('Temp dir deleted successfully.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an optional mod is enabled from its configuration value. If the
|
||||
* configuration value is null, the required object will be used to
|
||||
* determine if it is enabled.
|
||||
*
|
||||
* A mod is enabled if:
|
||||
* * The configuration is not null and one of the following:
|
||||
* * The configuration is a boolean and true.
|
||||
* * The configuration is an object and its 'value' property is true.
|
||||
* * The configuration is null and one of the following:
|
||||
* * The required object is null.
|
||||
* * The required object's 'def' property is null or true.
|
||||
*
|
||||
* @param {Object | boolean} modCfg The mod configuration object.
|
||||
* @param {Object} required Optional. The required object from the mod's distro declaration.
|
||||
* @returns {boolean} True if the mod is enabled, false otherwise.
|
||||
*/
|
||||
static isModEnabled(modCfg, required = null){
|
||||
return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.isDefault() : true
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
setupLiteLoader(){
|
||||
for(let ll of this.server.getModules()){
|
||||
if(ll.getType() === DistroManager.Types.LiteLoader){
|
||||
if(!ll.getRequired().isRequired()){
|
||||
const modCfg = ConfigManager.getModConfiguration(this.server.getID()).mods
|
||||
if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessID()], ll.getRequired())){
|
||||
if(fs.existsSync(ll.getArtifact().getPath())){
|
||||
this.usingLiteLoader = true
|
||||
this.llPath = ll.getArtifact().getPath()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(fs.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} modCfg The mod configuration object.
|
||||
* @param {Array.<Object>} mdls An array of modules to parse.
|
||||
* @returns {{fMods: Array.<Object>, lMods: Array.<Object>}} An object which contains
|
||||
* a list of enabled forge mods and litemods.
|
||||
*/
|
||||
resolveModConfiguration(modCfg, mdls){
|
||||
let fMods = []
|
||||
let lMods = []
|
||||
|
||||
for(let mdl of mdls){
|
||||
const type = mdl.getType()
|
||||
if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
|
||||
const o = !mdl.getRequired().isRequired()
|
||||
const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessID()], mdl.getRequired())
|
||||
if(!o || (o && e)){
|
||||
if(mdl.hasSubModules()){
|
||||
const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessID()].mods, mdl.getSubModules())
|
||||
fMods = fMods.concat(v.fMods)
|
||||
lMods = lMods.concat(v.lMods)
|
||||
if(mdl.type === DistroManager.Types.LiteLoader){
|
||||
continue
|
||||
}
|
||||
}
|
||||
if(mdl.type === DistroManager.Types.ForgeMod){
|
||||
fMods.push(mdl)
|
||||
} else {
|
||||
lMods.push(mdl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fMods,
|
||||
lMods
|
||||
}
|
||||
}
|
||||
|
||||
_isBelowOneDotSeven() {
|
||||
return Number(this.forgeData.id.split('-')[0].split('.')[1]) <= 7
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if this version of forge requires the absolute: prefix
|
||||
* on the modListFile repository field.
|
||||
*/
|
||||
_requiresAbsolute(){
|
||||
try {
|
||||
if(this._isBelowOneDotSeven()) {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a mod list json object.
|
||||
*
|
||||
* @param {'forge' | 'liteloader'} type The mod list type to construct.
|
||||
* @param {Array.<Object>} 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.
|
||||
*/
|
||||
constructModList(type, mods, save = false){
|
||||
const modList = {
|
||||
repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + path.join(this.commonDir, 'modstore')
|
||||
}
|
||||
|
||||
const ids = []
|
||||
if(type === 'forge'){
|
||||
for(let mod of mods){
|
||||
ids.push(mod.getExtensionlessID())
|
||||
}
|
||||
} else {
|
||||
for(let mod of mods){
|
||||
ids.push(mod.getExtensionlessID() + '@' + mod.getExtension())
|
||||
}
|
||||
}
|
||||
modList.modRef = ids
|
||||
|
||||
if(save){
|
||||
const json = JSON.stringify(modList, null, 4)
|
||||
fs.writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8')
|
||||
}
|
||||
|
||||
return modList
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the mod argument list for forge 1.13
|
||||
*
|
||||
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
||||
*/
|
||||
constructModArguments(mods){
|
||||
const argStr = mods.map(mod => {
|
||||
return mod.getExtensionlessID()
|
||||
}).join(',')
|
||||
|
||||
if(argStr){
|
||||
return [
|
||||
'--fml.mavenRoots',
|
||||
path.join('..', '..', 'common', 'modstore'),
|
||||
'--fml.mods',
|
||||
argStr
|
||||
]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the argument array that will be passed to the JVM process.
|
||||
*
|
||||
* @param {Array.<Object>} 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.<string>} An array containing the full JVM arguments for this process.
|
||||
*/
|
||||
constructJVMArguments(mods, tempNativePath){
|
||||
if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
|
||||
return this._constructJVMArguments113(mods, tempNativePath)
|
||||
} else {
|
||||
return this._constructJVMArguments112(mods, tempNativePath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the argument array that will be passed to the JVM process.
|
||||
* This function is for 1.12 and below.
|
||||
*
|
||||
* @param {Array.<Object>} 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.<string>} An array containing the full JVM arguments for this process.
|
||||
*/
|
||||
_constructJVMArguments112(mods, tempNativePath){
|
||||
|
||||
let args = []
|
||||
|
||||
// Classpath Argument
|
||||
args.push('-cp')
|
||||
args.push(this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':'))
|
||||
|
||||
// Java Arguments
|
||||
if(process.platform === 'darwin'){
|
||||
args.push('-Xdock:name=HeliosLauncher')
|
||||
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
|
||||
}
|
||||
args.push('-Xmx' + ConfigManager.getMaxRAM())
|
||||
args.push('-Xms' + ConfigManager.getMinRAM())
|
||||
args = args.concat(ConfigManager.getJVMOptions())
|
||||
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.<Object>} 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.<string>} An array containing the full JVM arguments for this process.
|
||||
*/
|
||||
_constructJVMArguments113(mods, tempNativePath){
|
||||
|
||||
const argDiscovery = /\${*(.*)}/
|
||||
|
||||
// JVM Arguments First
|
||||
let args = this.versionData.arguments.jvm
|
||||
|
||||
//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=HeliosLauncher')
|
||||
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
|
||||
}
|
||||
args.push('-Xmx' + ConfigManager.getMaxRAM())
|
||||
args.push('-Xms' + ConfigManager.getMinRAM())
|
||||
args = args.concat(ConfigManager.getJVMOptions())
|
||||
|
||||
// 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()){
|
||||
rule.values = [
|
||||
'--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 = path.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 = '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, 'Helios-Launcher')
|
||||
break
|
||||
case 'launcher_version':
|
||||
val = args[i].replace(argDiscovery, this.launcherVersion)
|
||||
break
|
||||
case 'classpath':
|
||||
val = this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':')
|
||||
break
|
||||
}
|
||||
if(val != null){
|
||||
args[i] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forge Specific Arguments
|
||||
args = args.concat(this.forgeData.arguments.game)
|
||||
|
||||
// Filter null values
|
||||
args = args.filter(arg => {
|
||||
return arg != null
|
||||
})
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the arguments required by forge.
|
||||
*
|
||||
* @returns {Array.<string>} An array containing the arguments required by forge.
|
||||
*/
|
||||
_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 = path.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 = '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.
|
||||
if(ConfigManager.getAutoConnect() && this.server.isAutoConnect()){
|
||||
const serverURL = new URL('my://' + this.server.getAddress())
|
||||
mcArgs.push('--server')
|
||||
mcArgs.push(serverURL.hostname)
|
||||
if(serverURL.port){
|
||||
mcArgs.push('--port')
|
||||
mcArgs.push(serverURL.port)
|
||||
}
|
||||
}
|
||||
|
||||
// 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._isBelowOneDotSeven()) {
|
||||
mcArgs.push(path.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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.<Object>} 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.<string>} An array containing the paths of each library required by this process.
|
||||
*/
|
||||
classpathArg(mods, tempNativePath){
|
||||
let cpArgs = []
|
||||
|
||||
// Add the version.jar to the classpath.
|
||||
const version = this.versionData.id
|
||||
cpArgs.push(path.join(this.commonDir, 'versions', version, version + '.jar'))
|
||||
|
||||
if(this.usingLiteLoader){
|
||||
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))
|
||||
|
||||
return cpArgs
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
_resolveMojangLibraries(tempNativePath){
|
||||
const libs = {}
|
||||
|
||||
const libArr = this.versionData.libraries
|
||||
fs.ensureDirSync(tempNativePath)
|
||||
for(let i=0; i<libArr.length; i++){
|
||||
const lib = libArr[i]
|
||||
if(Library.validateRules(lib.rules, lib.natives)){
|
||||
if(lib.natives == null){
|
||||
const dlInfo = lib.downloads
|
||||
const artifact = dlInfo.artifact
|
||||
const to = path.join(this.libPath, artifact.path)
|
||||
const versionIndependentId = lib.name.substring(0, lib.name.lastIndexOf(':'))
|
||||
libs[versionIndependentId] = to
|
||||
} else {
|
||||
// Extract the native library.
|
||||
const exclusionArr = 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 = path.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++){
|
||||
const fileName = zipEntries[i].entryName
|
||||
|
||||
let shouldExclude = false
|
||||
|
||||
// Exclude noted files.
|
||||
exclusionArr.forEach(function(exclusion){
|
||||
if(fileName.indexOf(exclusion) > -1){
|
||||
shouldExclude = true
|
||||
}
|
||||
})
|
||||
|
||||
// Extract the file.
|
||||
if(!shouldExclude){
|
||||
fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
|
||||
if(err){
|
||||
logger.error('Error while extracting native library:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return libs
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.<Object>} 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.
|
||||
*/
|
||||
_resolveServerLibraries(mods){
|
||||
const mdls = this.server.getModules()
|
||||
let libs = {}
|
||||
|
||||
// Locate Forge/Libraries
|
||||
for(let mdl of mdls){
|
||||
const type = mdl.getType()
|
||||
if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Library){
|
||||
libs[mdl.getVersionlessID()] = mdl.getArtifact().getPath()
|
||||
if(mdl.hasSubModules()){
|
||||
const res = this._resolveModuleLibraries(mdl)
|
||||
if(res.length > 0){
|
||||
libs = {...libs, ...res}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Check for any libraries in our mod list.
|
||||
for(let i=0; i<mods.length; i++){
|
||||
if(mods.sub_modules != null){
|
||||
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} mdl A module object from the server distro index.
|
||||
* @returns {Array.<string>} An array containing the paths of each library this module requires.
|
||||
*/
|
||||
_resolveModuleLibraries(mdl){
|
||||
if(!mdl.hasSubModules()){
|
||||
return []
|
||||
}
|
||||
let libs = []
|
||||
for(let sm of mdl.getSubModules()){
|
||||
if(sm.getType() === DistroManager.Types.Library){
|
||||
libs.push(sm.getArtifact().getPath())
|
||||
}
|
||||
// If this module has submodules, we need to resolve the libraries for those.
|
||||
// To avoid unnecessary recursive calls, base case is checked here.
|
||||
if(mdl.hasSubModules()){
|
||||
const res = this._resolveModuleLibraries(sm)
|
||||
if(res.length > 0){
|
||||
libs = libs.concat(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
return libs
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProcessBuilder
|
@ -1,65 +0,0 @@
|
||||
const net = require('net')
|
||||
|
||||
/**
|
||||
* 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.<Object>} A promise which resolves to an object containing
|
||||
* status information.
|
||||
*/
|
||||
exports.getStatus = function(address, port = 25565){
|
||||
|
||||
if(port == null || port == ''){
|
||||
port = 25565
|
||||
}
|
||||
if(typeof port === 'string'){
|
||||
port = parseInt(port)
|
||||
}
|
||||
|
||||
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 != ''){
|
||||
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.
|
||||
})
|
||||
})
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/* Github Code Highlighting. */
|
||||
@import "../../../node_modules/github-syntax-dark/lib/github-dark.css";
|
||||
@import "../../node_modules/github-syntax-dark/lib/github-dark.css";
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 298 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 875 B After Width: | Height: | Size: 875 B |
Before Width: | Height: | Size: 756 B After Width: | Height: | Size: 756 B |
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 959 B |
Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 602 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 809 B After Width: | Height: | Size: 809 B |
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 932 B |
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 822 B |
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1018 B |
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 907 B |
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 700 B |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 654 B After Width: | Height: | Size: 654 B |
@ -7,10 +7,10 @@ const crypto = require('crypto')
|
||||
const {URL} = require('url')
|
||||
|
||||
// Internal Requirements
|
||||
const DiscordWrapper = require('./assets/js/discordwrapper')
|
||||
const Mojang = require('./assets/js/mojang')
|
||||
const ProcessBuilder = require('./assets/js/processbuilder')
|
||||
const ServerStatus = require('./assets/js/serverstatus')
|
||||
const DiscordWrapper = require('./../discordwrapper')
|
||||
const Mojang = require('./../mojang/mojang')
|
||||
const ProcessBuilder = require('./../processbuilder')
|
||||
const ServerStatus = require('./../serverstatus')
|
||||
|
||||
// Launch Elements
|
||||
const launch_content = document.getElementById('launch_content')
|
@ -21,7 +21,7 @@ const loginForm = document.getElementById('loginForm')
|
||||
// Control variables.
|
||||
let lu = false, lp = false
|
||||
|
||||
const loggerLogin = LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold')
|
||||
const loggerLogin = new LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold')
|
||||
|
||||
|
||||
/**
|
@ -2,8 +2,8 @@
|
||||
const os = require('os')
|
||||
const semver = require('semver')
|
||||
|
||||
const { JavaGuard } = require('./assets/js/assetguard')
|
||||
const DropinModUtil = require('./assets/js/dropinmodutil')
|
||||
const { JavaGuard } = require('./../assetguard')
|
||||
const DropinModUtil = require('./../dropinmodutil')
|
||||
|
||||
const settingsState = {
|
||||
invalid: new Set()
|
||||
@ -694,7 +694,7 @@ function bindDropinModFileSystemButton(){
|
||||
const fsBtn = document.getElementById('settingsDropinFileSystemButton')
|
||||
fsBtn.onclick = () => {
|
||||
DropinModUtil.validateDir(CACHE_SETTINGS_MODS_DIR)
|
||||
shell.openItem(CACHE_SETTINGS_MODS_DIR)
|
||||
shell.openPath(CACHE_SETTINGS_MODS_DIR)
|
||||
}
|
||||
fsBtn.ondragenter = e => {
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
@ -818,7 +818,7 @@ function bindShaderpackButton() {
|
||||
spBtn.onclick = () => {
|
||||
const p = path.join(CACHE_SETTINGS_INSTANCE_DIR, 'shaderpacks')
|
||||
DropinModUtil.validateDir(p)
|
||||
shell.openItem(p)
|
||||
shell.openPath(p)
|
||||
}
|
||||
spBtn.ondragenter = e => {
|
||||
e.dataTransfer.dropEffect = 'move'
|
@ -5,10 +5,10 @@
|
||||
// Requirements
|
||||
const path = require('path')
|
||||
|
||||
const AuthManager = require('./assets/js/authmanager')
|
||||
const ConfigManager = require('./assets/js/configmanager')
|
||||
const DistroManager = require('./assets/js/distromanager')
|
||||
const Lang = require('./assets/js/langloader')
|
||||
const { AuthManager } = require('./../authmanager')
|
||||
const ConfigManager = require('./../configmanager')
|
||||
const DistroManager = require('./../distromanager')
|
||||
const Lang = require('./../langloader')
|
||||
|
||||
let rscShouldLoad = false
|
||||
let fatalStartupError = false
|
@ -1,18 +1,19 @@
|
||||
// @ts-nocheck
|
||||
import $ from 'jquery'
|
||||
import { ipcRenderer, remote, shell, webFrame } from 'electron'
|
||||
import { LoggerUtil } from '../loggerutil'
|
||||
import isDev from '../isdev'
|
||||
|
||||
/**
|
||||
* Core UI functions are initialized in this file. This prevents
|
||||
* unexpected errors from breaking the core features. Specifically,
|
||||
* actions in this file should not require the usage of any internal
|
||||
* modules, excluding dependencies.
|
||||
*/
|
||||
// Requirements
|
||||
const $ = require('jquery')
|
||||
const {ipcRenderer, remote, shell, webFrame} = require('electron')
|
||||
const isDev = require('./assets/js/isdev')
|
||||
const LoggerUtil = require('./assets/js/loggerutil')
|
||||
|
||||
const loggerUICore = LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold')
|
||||
const loggerAutoUpdater = LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
|
||||
const loggerAutoUpdaterSuccess = LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
|
||||
const loggerUICore = new LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold')
|
||||
const loggerAutoUpdater = new LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
|
||||
const loggerAutoUpdaterSuccess = new LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
|
||||
|
||||
// Log deprecation and process warnings.
|
||||
process.traceProcessWarnings = true
|
||||
@ -49,7 +50,7 @@ if(!isDev){
|
||||
loggerAutoUpdaterSuccess.log('New update available', info.version)
|
||||
|
||||
if(process.platform === 'darwin'){
|
||||
info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/helioslauncher-${info.version}.dmg`
|
||||
info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/helioslauncher-setup-${info.version}.dmg`
|
||||
showUpdateUI(info)
|
||||
}
|
||||
|
||||
@ -107,7 +108,7 @@ function changeAllowPrerelease(val){
|
||||
|
||||
function showUpdateUI(info){
|
||||
//TODO Make this message a bit more informative `${info.version}`
|
||||
document.getElementById('image_seal_container').setAttribute('update', true)
|
||||
document.getElementById('image_seal_container').setAttribute('update', 'true')
|
||||
document.getElementById('image_seal_container').onclick = () => {
|
||||
/*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later')
|
||||
setOverlayHandler(() => {
|
||||
@ -138,6 +139,7 @@ document.addEventListener('readystatechange', function () {
|
||||
loggerUICore.log('UICore Initializing..')
|
||||
|
||||
// Bind close button.
|
||||
// DONE
|
||||
Array.from(document.getElementsByClassName('fCb')).map((val) => {
|
||||
val.addEventListener('click', e => {
|
||||
const window = remote.getCurrentWindow()
|
||||
@ -146,6 +148,7 @@ document.addEventListener('readystatechange', function () {
|
||||
})
|
||||
|
||||
// Bind restore down button.
|
||||
// DONE
|
||||
Array.from(document.getElementsByClassName('fRb')).map((val) => {
|
||||
val.addEventListener('click', e => {
|
||||
const window = remote.getCurrentWindow()
|
||||
@ -154,23 +157,24 @@ document.addEventListener('readystatechange', function () {
|
||||
} else {
|
||||
window.maximize()
|
||||
}
|
||||
document.activeElement.blur()
|
||||
(document.activeElement as HTMLElement).blur()
|
||||
})
|
||||
})
|
||||
|
||||
// Bind minimize button.
|
||||
// DONE
|
||||
Array.from(document.getElementsByClassName('fMb')).map((val) => {
|
||||
val.addEventListener('click', e => {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.minimize()
|
||||
document.activeElement.blur()
|
||||
window.minimize();
|
||||
(document.activeElement as HTMLElement).blur()
|
||||
})
|
||||
})
|
||||
|
||||
// Remove focus from social media buttons once they're clicked.
|
||||
Array.from(document.getElementsByClassName('mediaURL')).map(val => {
|
||||
val.addEventListener('click', e => {
|
||||
document.activeElement.blur()
|
||||
(document.activeElement as HTMLElement).blur()
|
||||
})
|
||||
})
|
||||
|
||||
@ -184,10 +188,10 @@ document.addEventListener('readystatechange', function () {
|
||||
//const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width
|
||||
//const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width
|
||||
|
||||
document.getElementById('launch_details').style.maxWidth = 266.01
|
||||
document.getElementById('launch_progress').style.width = 170.8
|
||||
document.getElementById('launch_details_right').style.maxWidth = 170.8
|
||||
document.getElementById('launch_progress_label').style.width = 53.21
|
||||
document.getElementById('launch_details').style.maxWidth = '266.01'
|
||||
document.getElementById('launch_progress').style.width = '170.8'
|
||||
document.getElementById('launch_details_right').style.maxWidth = '170.8'
|
||||
document.getElementById('launch_progress_label').style.width = '53.21'
|
||||
|
||||
}
|
||||
|
||||
@ -209,6 +213,6 @@ $(document).on('click', 'a[href^="http"]', function(event) {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){
|
||||
let window = remote.getCurrentWindow()
|
||||
window.toggleDevTools()
|
||||
window.webContents.toggleDevTools()
|
||||
}
|
||||
})
|
@ -2,9 +2,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8" http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-In6B8teKZQll5heMl9bS7CESTbGvuAt3VVV86BUQBDk='"/>
|
||||
<title>Westeroscraft Launcher</title>
|
||||
<script src="./assets/js/scripts/uicore.js"></script>
|
||||
<script src="./assets/js/scripts/uibinder.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="./assets/css/launcher.css">
|
||||
<script src="../../out/scripts/uicore.js"></script>
|
||||
<script src="../../out/scripts/uibinder.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="../css/launcher.css">
|
||||
<style>
|
||||
body {
|
||||
/*background: url('assets/images/backgrounds/<%=bkid%>.jpg') no-repeat center center fixed;*/
|
||||
@ -38,8 +38,8 @@
|
||||
<div id="loadingContainer">
|
||||
<div id="loadingContent">
|
||||
<div id="loadSpinnerContainer">
|
||||
<img id="loadCenterImage" src="assets/images/LoadingSeal.png">
|
||||
<img id="loadSpinnerImage" class="rotating" src="assets/images/LoadingText.png">
|
||||
<img id="loadCenterImage" src="../images/LoadingSeal.png">
|
||||
<img id="loadSpinnerImage" class="rotating" src="../images/LoadingText.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -2,7 +2,7 @@
|
||||
<div id="upper">
|
||||
<div id="left">
|
||||
<div id="image_seal_container">
|
||||
<img id="image_seal" src="assets/images/SealCircle.png"/>
|
||||
<img id="image_seal" src="../images/SealCircle.png"/>
|
||||
<div id="updateAvailableTooltip">Update Available</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -216,5 +216,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/landing.js"></script>
|
||||
<script src="../../out/scripts/landing.js"></script>
|
||||
</div>
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div id="loginContent">
|
||||
<form id="loginForm">
|
||||
<img id="loginImageSeal" src="assets/images/SealCircle.png"/>
|
||||
<img id="loginImageSeal" src="../images/SealCircle.png"/>
|
||||
<span id="loginSubheader">MINECRAFT LOGIN</span>
|
||||
<div class="loginFieldContainer">
|
||||
<svg id="profileSVG" class="loginSVG" viewBox="40 37 65.36 61.43">
|
||||
@ -61,5 +61,5 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/login.js"></script>
|
||||
<script src="../../out/scripts/login.js"></script>
|
||||
</div>
|
@ -37,5 +37,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/overlay.js"></script>
|
||||
<script src="../../out/scripts/overlay.js"></script>
|
||||
</div>
|
@ -277,7 +277,7 @@
|
||||
<div id="settingsAboutCurrentContainer">
|
||||
<div id="settingsAboutCurrentContent">
|
||||
<div id="settingsAboutCurrentHeadline">
|
||||
<img id="settingsAboutLogo" src="./assets/images/SealCircle.png">
|
||||
<img id="settingsAboutLogo" src="../images/SealCircle.png">
|
||||
<span id="settingsAboutTitle">Helios Launcher</span>
|
||||
</div>
|
||||
<div id="settingsAboutCurrentVersion">
|
||||
@ -352,5 +352,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/settings.js"></script>
|
||||
<script src="../../out/scripts/settings.js"></script>
|
||||
</div>
|
@ -4,7 +4,7 @@
|
||||
<div class="cloudBottom"></div>
|
||||
</div>-->
|
||||
<div id="welcomeContent">
|
||||
<img id="welcomeImageSeal" src="assets/images/SealCircle.png"/>
|
||||
<img id="welcomeImageSeal" src="../images/SealCircle.png"/>
|
||||
<span id="welcomeHeader">WELCOME TO WESTEROSCRAFT</span>
|
||||
<span id="welcomeDescription">Our mission is to recreate the universe imagined by author George RR Martin in his fantasy series, A Song of Ice and Fire. Through the collaborative effort of thousands of community members, we have sought to create Westeros as accurately and precisely as possible within Minecraft. The world we are creating is yours to explore. Journey from Dorne to Castle Black, and if you aren’t afraid, beyond the Wall itself, but best not delay. As the words of House Stark ominously warn: Winter is Coming.</span>
|
||||
<br>
|
||||
@ -21,5 +21,5 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/welcome.js"></script>
|
||||
<script src="../../out/scripts/welcome.js"></script>
|
||||
</div>
|
5
build.js
@ -20,7 +20,7 @@ builder.build({
|
||||
config: {
|
||||
appId: 'helioslauncher',
|
||||
productName: 'Helios Launcher',
|
||||
artifactName: '${productName}.${ext}',
|
||||
artifactName: '${productName}-setup-${version}.${ext}',
|
||||
copyright: 'Copyright © 2018-2020 Daniel Scalzi',
|
||||
directories: {
|
||||
buildResources: 'build',
|
||||
@ -57,7 +57,8 @@ builder.build({
|
||||
'!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.travis.yml,.nvmrc,.eslintrc.json,build.js}'
|
||||
],
|
||||
extraResources: [
|
||||
'libraries'
|
||||
'libraries',
|
||||
'static'
|
||||
],
|
||||
asar: true
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
{
|
||||
"__comment__": [
|
||||
"This is an example only.",
|
||||
"The file is hosted on the URL in DistroManager.js"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"discord": {
|
||||
"clientId": "385581240906022916",
|
13681
package-lock.json
generated
90
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "helioslauncher",
|
||||
"version": "1.7.0",
|
||||
"version": "2.0.0-alpha.0",
|
||||
"productName": "Helios Launcher",
|
||||
"description": "Modded Minecraft Launcher",
|
||||
"author": "Daniel Scalzi (https://github.com/dscalzi/)",
|
||||
@ -10,40 +10,90 @@
|
||||
"url": "https://github.com/dscalzi/HeliosLauncher/issues"
|
||||
},
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"main": "./dist/main.js",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"tsc": "tsc",
|
||||
"start": "electron .",
|
||||
"cilinux": "node build.js WINDOWS && node build.js LINUX",
|
||||
"cidarwin": "node build.js MAC",
|
||||
"dist": "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true node build.js",
|
||||
"dist:win": "npm run dist -- WINDOWS",
|
||||
"dist:mac": "npm run dist -- MAC",
|
||||
"dist:linux": "npm run dist -- LINUX",
|
||||
"lint": "eslint --config .eslintrc.json ."
|
||||
"lint": "eslint --ext=jsx,js,tsx,ts src",
|
||||
"dev": "electron-webpack dev",
|
||||
"compile": "electron-webpack",
|
||||
"test": "cross-env TS_NODE_PROJECT='./tsconfig.test.json' NODE_ENV=test mocha -r ts-node/register -r tsconfig-paths/register test/**/*.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": "12.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.4.13",
|
||||
"async": "^3.1.0",
|
||||
"discord-rpc": "3.1.0",
|
||||
"ejs": "^3.0.1",
|
||||
"ejs-electron": "^2.0.3",
|
||||
"electron-updater": "^4.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"adm-zip": "^0.4.16",
|
||||
"async": "^3.2.0",
|
||||
"discord-rpc": "^3.1.4",
|
||||
"electron-updater": "^4.3.5",
|
||||
"fs-extra": "^9.0.1",
|
||||
"github-syntax-dark": "^0.5.0",
|
||||
"jquery": "^3.4.1",
|
||||
"request": "^2.88.0",
|
||||
"semver": "^7.1.1",
|
||||
"tar-fs": "^2.0.0",
|
||||
"winreg": "^1.2.4"
|
||||
"got": "^11.7.0",
|
||||
"jquery": "^3.5.1",
|
||||
"lodash": "^4.17.20",
|
||||
"luxon": "^1.25.0",
|
||||
"request": "^2.88.2",
|
||||
"semver": "^7.3.2",
|
||||
"tar-fs": "^2.1.0",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winreg": "^1.2.4",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "^7.1.9",
|
||||
"electron-builder": "^22.2.0",
|
||||
"eslint": "^6.8.0"
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@types/adm-zip": "^0.4.33",
|
||||
"@types/async": "^3.2.3",
|
||||
"@types/chai": "^4.2.13",
|
||||
"@types/chai-as-promised": "^7.1.3",
|
||||
"@types/discord-rpc": "^3.0.4",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/jquery": "^3.5.2",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/luxon": "^1.25.0",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"@types/node": "^12.12.64",
|
||||
"@types/react": "^16.9.51",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/request": "^2.48.5",
|
||||
"@types/tar-fs": "^2.0.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"@types/winreg": "^1.2.30",
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||
"@typescript-eslint/parser": "^4.4.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"electron": "^9.3.2",
|
||||
"electron-builder": "^22.8.1",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"electron-webpack-ts": "^4.0.1",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint-plugin-react": "^7.21.3",
|
||||
"helios-distribution-types": "1.0.0-rc.1",
|
||||
"mocha": "^8.1.3",
|
||||
"nock": "^13.0.4",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"redux": "^4.0.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^9.0.0",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
0
src/common/asset/assetguardnew.ts
Normal file
7
src/common/asset/model/engine/Asset.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface Asset {
|
||||
id: string
|
||||
hash: string
|
||||
size: number
|
||||
url: string
|
||||
path: string
|
||||
}
|
33
src/common/asset/model/engine/AssetGuardError.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export class AssetGuardError extends Error {
|
||||
|
||||
code?: string
|
||||
stack!: string
|
||||
error?: Partial<Error & {code?: string}>
|
||||
|
||||
constructor(message: string, error?: Partial<Error & {code?: string}>) {
|
||||
super(message)
|
||||
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
|
||||
// Reference: https://github.com/sindresorhus/got/blob/master/source/core/index.ts#L340
|
||||
if(error) {
|
||||
|
||||
this.error = error
|
||||
this.code = error?.code
|
||||
|
||||
if (error.stack != null) {
|
||||
const indexOfMessage = this.stack.indexOf(this.message) + this.message.length
|
||||
const thisStackTrace = this.stack.slice(indexOfMessage).split('\n').reverse()
|
||||
const errorStackTrace = error.stack.slice(error.stack.indexOf(error.message!) + error.message!.length).split('\n').reverse()
|
||||
|
||||
// Remove duplicated traces
|
||||
while (errorStackTrace.length !== 0 && errorStackTrace[0] === thisStackTrace[0]) {
|
||||
thisStackTrace.shift()
|
||||
}
|
||||
|
||||
this.stack = `${this.stack.slice(0, indexOfMessage)}${thisStackTrace.reverse().join('\n')}${errorStackTrace.reverse().join('\n')}`
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
12
src/common/asset/model/engine/IndexProcessor.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Asset } from './Asset'
|
||||
|
||||
export abstract class IndexProcessor {
|
||||
|
||||
constructor(
|
||||
protected commonDir: string
|
||||
) {}
|
||||
|
||||
abstract async init(): Promise<void>
|
||||
abstract async validate(): Promise<{[category: string]: Asset[]}>
|
||||
|
||||
}
|
54
src/common/asset/model/mojang/LauncherJson.ts
Normal file
@ -0,0 +1,54 @@
|
||||
export interface LauncherJava {
|
||||
sha1: string
|
||||
url: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface LauncherVersions {
|
||||
launcher: {
|
||||
commit: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface LauncherJson {
|
||||
|
||||
java: {
|
||||
lzma: {
|
||||
sha1: string
|
||||
url: string
|
||||
}
|
||||
sha1: string
|
||||
}
|
||||
linux: {
|
||||
applink: string
|
||||
downloadhash: string
|
||||
versions: LauncherVersions
|
||||
}
|
||||
osx: {
|
||||
'64': {
|
||||
jdk: LauncherJava
|
||||
jre: LauncherJava
|
||||
}
|
||||
apphash: string
|
||||
applink: string
|
||||
downloadhash: string
|
||||
versions: LauncherVersions
|
||||
}
|
||||
windows: {
|
||||
'32': {
|
||||
jdk: LauncherJava
|
||||
jre: LauncherJava
|
||||
}
|
||||
'64': {
|
||||
jdk: LauncherJava
|
||||
jre: LauncherJava
|
||||
}
|
||||
apphash: string
|
||||
applink: string
|
||||
downloadhash: string
|
||||
rolloutPercent: number
|
||||
versions: LauncherVersions
|
||||
}
|
||||
|
||||
}
|
103
src/common/asset/model/mojang/VersionJson.ts
Normal file
@ -0,0 +1,103 @@
|
||||
export interface Rule {
|
||||
action: string
|
||||
os?: {
|
||||
name: string
|
||||
version?: string
|
||||
}
|
||||
features?: {
|
||||
[key: string]: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface Natives {
|
||||
linux?: string
|
||||
osx?: string
|
||||
windows?: string
|
||||
}
|
||||
|
||||
export interface BaseArtifact {
|
||||
|
||||
sha1: string
|
||||
size: number
|
||||
url: string
|
||||
|
||||
}
|
||||
|
||||
export interface LibraryArtifact extends BaseArtifact {
|
||||
|
||||
path: string
|
||||
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
downloads: {
|
||||
artifact: LibraryArtifact
|
||||
classifiers?: {
|
||||
javadoc?: LibraryArtifact
|
||||
'natives-linux'?: LibraryArtifact
|
||||
'natives-macos'?: LibraryArtifact
|
||||
'natives-windows'?: LibraryArtifact
|
||||
sources?: LibraryArtifact
|
||||
}
|
||||
}
|
||||
extract?: {
|
||||
exclude: string[]
|
||||
}
|
||||
name: string
|
||||
natives?: Natives
|
||||
rules?: Rule[]
|
||||
}
|
||||
|
||||
export interface VersionJson {
|
||||
|
||||
arguments: {
|
||||
game: string[]
|
||||
jvm: {
|
||||
rules: Rule[]
|
||||
value: string[]
|
||||
}[]
|
||||
}
|
||||
assetIndex: {
|
||||
id: string
|
||||
sha1: string
|
||||
size: number
|
||||
totalSize: number
|
||||
url: string
|
||||
}
|
||||
assets: string
|
||||
downloads: {
|
||||
client: BaseArtifact
|
||||
server: BaseArtifact
|
||||
}
|
||||
id: string
|
||||
libraries: Library[]
|
||||
logging: {
|
||||
client: {
|
||||
argument: string
|
||||
file: {
|
||||
id: string
|
||||
sha1: string
|
||||
size: number
|
||||
url: string
|
||||
}
|
||||
type: string
|
||||
}
|
||||
}
|
||||
mainClass: string
|
||||
minimumLauncherVersion: number
|
||||
releaseTime: string
|
||||
time: string
|
||||
type: string
|
||||
|
||||
}
|
||||
|
||||
export interface AssetIndex {
|
||||
|
||||
objects: {
|
||||
[file: string]: {
|
||||
hash: string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
|
||||
}
|
15
src/common/asset/model/mojang/VersionManifest.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface MojangVersionManifest {
|
||||
|
||||
latest: {
|
||||
release: string
|
||||
snapshot: string
|
||||
}
|
||||
versions: {
|
||||
id: string
|
||||
type: string
|
||||
url: string
|
||||
time: string
|
||||
releaseTime: string
|
||||
}[]
|
||||
|
||||
}
|
290
src/common/asset/processor/MojangIndexProcessor.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import got from 'got'
|
||||
import { dirname, join } from 'path'
|
||||
import { ensureDir, pathExists, readFile, readJson, writeFile } from 'fs-extra'
|
||||
|
||||
import { Asset } from 'common/asset/model/engine/Asset'
|
||||
import { AssetGuardError } from 'common/asset/model/engine/AssetGuardError'
|
||||
import { IndexProcessor } from 'common/asset/model/engine/IndexProcessor'
|
||||
import { MojangVersionManifest } from 'common/asset/model/mojang/VersionManifest'
|
||||
import { handleGotError } from 'common/got/RestResponse'
|
||||
import { AssetIndex, LibraryArtifact, VersionJson } from 'common/asset/model/mojang/VersionJson'
|
||||
import { calculateHash, getLibraryDir, getVersionJarPath, getVersionJsonPath, validateLocalFile } from 'common/util/FileUtils'
|
||||
import { getMojangOS, isLibraryCompatible } from 'common/util/MojangUtils'
|
||||
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||
|
||||
export class MojangIndexProcessor extends IndexProcessor {
|
||||
|
||||
public static readonly LAUNCHER_JSON_ENDPOINT = 'https://launchermeta.mojang.com/mc/launcher.json'
|
||||
public static readonly VERSION_MANIFEST_ENDPOINT = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'
|
||||
public static readonly ASSET_RESOURCE_ENDPOINT = 'http://resources.download.minecraft.net'
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('MojangIndexProcessor')
|
||||
|
||||
private versionJson!: VersionJson
|
||||
private assetIndex!: AssetIndex
|
||||
private client = got.extend({
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
private assetPath: string
|
||||
|
||||
constructor(commonDir: string, protected version: string) {
|
||||
super(commonDir)
|
||||
this.assetPath = join(commonDir, 'assets')
|
||||
}
|
||||
|
||||
/**
|
||||
* Download https://launchermeta.mojang.com/mc/game/version_manifest.json
|
||||
* Unable to download:
|
||||
* Proceed, check versions directory for target version
|
||||
* If version.json not present, fatal error.
|
||||
* If version.json present, load and use.
|
||||
* Able to download:
|
||||
* Download, use in memory only.
|
||||
* Locate target version entry.
|
||||
* Extract hash
|
||||
* Validate local exists and matches hash
|
||||
* Condition fails: download
|
||||
* Download fails: fatal error
|
||||
* Download succeeds: Save to disk, continue
|
||||
* Passes: load from file
|
||||
*
|
||||
* Version JSON in memory
|
||||
* Extract assetIndex
|
||||
* Check that local exists and hash matches
|
||||
* if false, download
|
||||
* download fails: fatal error
|
||||
* if true: load from disk and use
|
||||
*
|
||||
* complete init when 3 files are validated and loaded.
|
||||
*
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
|
||||
const versionManifest = await this.loadVersionManifest()
|
||||
this.versionJson = await this.loadVersionJson(this.version, versionManifest)
|
||||
this.assetIndex = await this.loadAssetIndex(this.versionJson)
|
||||
|
||||
}
|
||||
|
||||
private async loadAssetIndex(versionJson: VersionJson): Promise<AssetIndex> {
|
||||
const assetIndexPath = this.getAssetIndexPath(versionJson.assetIndex.id)
|
||||
const assetIndex = await this.loadContentWithRemoteFallback<AssetIndex>(versionJson.assetIndex.url, assetIndexPath, { algo: 'sha1', value: versionJson.assetIndex.sha1 })
|
||||
if(assetIndex == null) {
|
||||
throw new AssetGuardError(`Failed to download ${versionJson.assetIndex.id} asset index.`)
|
||||
}
|
||||
return assetIndex
|
||||
}
|
||||
|
||||
private async loadVersionJson(version: string, versionManifest: MojangVersionManifest | null): Promise<VersionJson> {
|
||||
const versionJsonPath = getVersionJsonPath(this.commonDir, version)
|
||||
if(versionManifest != null) {
|
||||
const versionJsonUrl = this.getVersionJsonUrl(version, versionManifest)
|
||||
if(versionJsonUrl == null) {
|
||||
throw new AssetGuardError(`Invalid version: ${version}.`)
|
||||
}
|
||||
const hash = this.getVersionJsonHash(versionJsonUrl)
|
||||
if(hash == null) {
|
||||
throw new AssetGuardError('Format of Mojang\'s version manifest has changed. Unable to proceed.')
|
||||
}
|
||||
const versionJson = await this.loadContentWithRemoteFallback<VersionJson>(versionJsonUrl, versionJsonPath, { algo: 'sha1', value: hash })
|
||||
if(versionJson == null) {
|
||||
throw new AssetGuardError(`Failed to download ${version} json index.`)
|
||||
}
|
||||
|
||||
return versionJson
|
||||
|
||||
} else {
|
||||
// Attempt to find local index.
|
||||
if(await pathExists(versionJsonPath)) {
|
||||
return await readJson(versionJsonPath)
|
||||
} else {
|
||||
throw new AssetGuardError(`Unable to load version manifest and ${version} json index does not exist locally.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadContentWithRemoteFallback<T>(url: string, path: string, hash?: {algo: string, value: string}): Promise<T | null> {
|
||||
|
||||
try {
|
||||
if(await pathExists(path)) {
|
||||
const buf = await readFile(path)
|
||||
if(hash) {
|
||||
const bufHash = calculateHash(buf, hash.algo)
|
||||
if(bufHash === hash.value) {
|
||||
return JSON.parse(buf.toString())
|
||||
}
|
||||
} else {
|
||||
return JSON.parse(buf.toString())
|
||||
}
|
||||
}
|
||||
} catch(error) {
|
||||
throw new AssetGuardError(`Failure while loading ${path}.`, error)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.client.get<T>(url)
|
||||
|
||||
await ensureDir(dirname(path))
|
||||
await writeFile(path, res.body)
|
||||
|
||||
return res.body
|
||||
} catch(error) {
|
||||
return handleGotError(url, error, MojangIndexProcessor.logger, () => null).data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async loadVersionManifest(): Promise<MojangVersionManifest | null> {
|
||||
try {
|
||||
const res = await this.client.get<MojangVersionManifest>(MojangIndexProcessor.VERSION_MANIFEST_ENDPOINT)
|
||||
return res.body
|
||||
} catch(error) {
|
||||
return handleGotError('Load Mojang Version Manifest', error, MojangIndexProcessor.logger, () => null).data
|
||||
}
|
||||
}
|
||||
|
||||
private getVersionJsonUrl(id: string, manifest: MojangVersionManifest): string | null {
|
||||
for(const version of manifest.versions) {
|
||||
if(version.id == id){
|
||||
return version.url
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private getVersionJsonHash(url: string): string | null {
|
||||
const regex = /^https:\/\/launchermeta.mojang.com\/v1\/packages\/(.+)\/.+.json$/
|
||||
const match = regex.exec(url)
|
||||
if(match != null && match[1]) {
|
||||
return match[1]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private getAssetIndexPath(id: string): string {
|
||||
return join(this.assetPath, 'indexes', `${id}.json`)
|
||||
}
|
||||
|
||||
// TODO progress tracker
|
||||
// TODO type return object
|
||||
public async validate(): Promise<{[category: string]: Asset[]}> {
|
||||
|
||||
const assets = await this.validateAssets(this.assetIndex)
|
||||
const libraries = await this.validateLibraries(this.versionJson)
|
||||
const client = await this.validateClient(this.versionJson)
|
||||
const logConfig = await this.validateLogConfig(this.versionJson)
|
||||
|
||||
return {
|
||||
assets,
|
||||
libraries,
|
||||
client,
|
||||
misc: [
|
||||
...logConfig
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private async validateAssets(assetIndex: AssetIndex): Promise<Asset[]> {
|
||||
|
||||
const objectDir = join(this.assetPath, 'objects')
|
||||
const notValid: Asset[] = []
|
||||
|
||||
for(const assetEntry of Object.entries(assetIndex.objects)) {
|
||||
const hash = assetEntry[1].hash
|
||||
const path = join(objectDir, hash.substring(0, 2), hash)
|
||||
const url = `${MojangIndexProcessor.ASSET_RESOURCE_ENDPOINT}/${hash.substring(0, 2)}/${hash}`
|
||||
|
||||
if(!await validateLocalFile(path, 'sha1', hash)) {
|
||||
notValid.push({
|
||||
id: assetEntry[0],
|
||||
hash,
|
||||
size: assetEntry[1].size,
|
||||
url,
|
||||
path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return notValid
|
||||
|
||||
}
|
||||
|
||||
private async validateLibraries(versionJson: VersionJson): Promise<Asset[]> {
|
||||
|
||||
const libDir = getLibraryDir(this.commonDir)
|
||||
const notValid: Asset[] = []
|
||||
|
||||
for(const libEntry of versionJson.libraries) {
|
||||
if(isLibraryCompatible(libEntry.rules, libEntry.natives)) {
|
||||
let artifact: LibraryArtifact
|
||||
if(libEntry.natives == null) {
|
||||
artifact = libEntry.downloads.artifact
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const classifier = libEntry.natives[getMojangOS()].replace('${arch}', process.arch.replace('x', ''))
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
artifact = libEntry.downloads.classifiers[classifier]
|
||||
}
|
||||
|
||||
const path = join(libDir, artifact.path)
|
||||
const hash = artifact.sha1
|
||||
if(!await validateLocalFile(path, 'sha1', hash)) {
|
||||
notValid.push({
|
||||
id: libEntry.name,
|
||||
hash,
|
||||
size: artifact.size,
|
||||
url: artifact.url,
|
||||
path
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notValid
|
||||
}
|
||||
|
||||
private async validateClient(versionJson: VersionJson): Promise<Asset[]> {
|
||||
|
||||
const version = versionJson.id
|
||||
const versionJarPath = getVersionJarPath(this.commonDir, version)
|
||||
const hash = versionJson.downloads.client.sha1
|
||||
|
||||
if(!await validateLocalFile(versionJarPath, 'sha1', hash)) {
|
||||
return [{
|
||||
id: `${version} client`,
|
||||
hash,
|
||||
size: versionJson.downloads.client.size,
|
||||
url: versionJson.downloads.client.url,
|
||||
path: versionJarPath
|
||||
}]
|
||||
}
|
||||
|
||||
return []
|
||||
|
||||
}
|
||||
|
||||
private async validateLogConfig(versionJson: VersionJson): Promise<Asset[]> {
|
||||
|
||||
const logFile = versionJson.logging.client.file
|
||||
const path = join(this.assetPath, 'log_configs', logFile.id)
|
||||
const hash = logFile.sha1
|
||||
|
||||
if(!await validateLocalFile(path, 'sha1', hash)) {
|
||||
return [{
|
||||
id: logFile.id,
|
||||
hash,
|
||||
size: logFile.size,
|
||||
url: logFile.url,
|
||||
path
|
||||
}]
|
||||
}
|
||||
|
||||
return []
|
||||
|
||||
}
|
||||
|
||||
}
|
700
src/common/config/configmanager.ts
Normal file
@ -0,0 +1,700 @@
|
||||
import { join } from 'path'
|
||||
import { pathExistsSync, writeFileSync, ensureDirSync, readFileSync } from 'fs-extra'
|
||||
import { totalmem } from 'os'
|
||||
import { SavedAccount } from './model/SavedAccount'
|
||||
import { LauncherConfig } from './model/LauncherConfig'
|
||||
import { ModConfig } from './model/ModConfig'
|
||||
import { NewsCache } from './model/NewsCache'
|
||||
import { LoggerUtil } from '../logging/loggerutil'
|
||||
|
||||
// TODO final review upon usage in implementation.
|
||||
|
||||
export class ConfigManager {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('ConfigManager')
|
||||
private static readonly sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME)
|
||||
private static readonly dataPath = join(ConfigManager.sysRoot as string, '.helioslauncher')
|
||||
|
||||
// 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(): string {
|
||||
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): string {
|
||||
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): void {
|
||||
ConfigManager.config.settings.launcher.dataDirectory = dataDirectory
|
||||
}
|
||||
|
||||
private static readonly configPath = join(ConfigManager.getLauncherDirectory(), 'config.json')
|
||||
private static readonly firstLaunch = !pathExistsSync(ConfigManager.configPath)
|
||||
|
||||
/**
|
||||
* 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(): number {
|
||||
const mem = totalmem()
|
||||
return mem >= 6000000000 ? 3 : 2
|
||||
}
|
||||
|
||||
public static getAbsoluteMaxRAM(): number {
|
||||
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(): void {
|
||||
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(): void {
|
||||
let doLoad = true
|
||||
|
||||
if(!pathExistsSync(ConfigManager.configPath)){
|
||||
// Create all parent directories.
|
||||
ensureDirSync(join(ConfigManager.configPath, '..'))
|
||||
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.info('Configuration file contains malformed JSON or is corrupt.')
|
||||
ConfigManager.logger.info('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.info('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.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static validateKeySet(srcObj: any, destObj: any){
|
||||
if(srcObj == null){
|
||||
srcObj = {}
|
||||
}
|
||||
const validationBlacklist = ['authenticationDatabase']
|
||||
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]] = ConfigManager.validateKeySet(srcObj[keys[i]], destObj[keys[i]])
|
||||
}
|
||||
}
|
||||
return destObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this is the first time the user has launched the
|
||||
* application. This is determined by the existance of the data path.
|
||||
*
|
||||
* @returns {boolean} True if this is the first launch, otherwise false.
|
||||
*/
|
||||
public static isFirstLaunch(): boolean {
|
||||
return ConfigManager.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 getTempNativeFolder(): string {
|
||||
return 'HeliosLauncherNatives'
|
||||
}
|
||||
|
||||
// System Settings (Unconfigurable on UI)
|
||||
|
||||
/**
|
||||
* Retrieve the news cache to determine
|
||||
* whether or not there is newer news.
|
||||
*
|
||||
* @returns {NewsCache} The news cache object.
|
||||
*/
|
||||
public static getNewsCache(): NewsCache {
|
||||
return ConfigManager.config.newsCache
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the new news cache object.
|
||||
*
|
||||
* @param {Object} newsCache The new news cache object.
|
||||
*/
|
||||
public static setNewsCache(newsCache: NewsCache): void {
|
||||
ConfigManager.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): void {
|
||||
ConfigManager.config.newsCache.dismissed = dismissed
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the common directory for shared
|
||||
* game files (assets, libraries, etc).
|
||||
*
|
||||
* @returns {string} The launcher's common directory.
|
||||
*/
|
||||
public static getCommonDirectory(): string {
|
||||
return join(ConfigManager.getDataDirectory(), 'common')
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the instance directory for the per
|
||||
* server game directories.
|
||||
*
|
||||
* @returns {string} The launcher's instance directory.
|
||||
*/
|
||||
public static getInstanceDirectory(): string {
|
||||
return join(ConfigManager.getDataDirectory(), 'instances')
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the launcher's Client Token.
|
||||
* There is no default client token.
|
||||
*
|
||||
* @returns {string | null} The launcher's Client Token.
|
||||
*/
|
||||
public static getClientToken(): string | null {
|
||||
return ConfigManager.config.clientToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the launcher's Client Token.
|
||||
*
|
||||
* @param {string} clientToken The launcher's new Client Token.
|
||||
*/
|
||||
public static setClientToken(clientToken: string): void {
|
||||
ConfigManager.config.clientToken = clientToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ID of the selected serverpack.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {string | null} The ID of the selected serverpack.
|
||||
*/
|
||||
public static getSelectedServer(def = false): string | null {
|
||||
return !def ? ConfigManager.config.selectedServer : ConfigManager.DEFAULT_CONFIG.selectedServer
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ID of the selected serverpack.
|
||||
*
|
||||
* @param {string} serverID The ID of the new selected serverpack.
|
||||
*/
|
||||
public static setSelectedServer(serverID: string): void {
|
||||
ConfigManager.config.selectedServer = serverID
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of each account currently authenticated by the launcher.
|
||||
*
|
||||
* @returns {Array.<SavedAccount>} 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.<ModConfig>} An array of each stored mod configuration.
|
||||
*/
|
||||
public static getModConfigurations(): ModConfig[] {
|
||||
return ConfigManager.config.modConfigurations
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the array of stored mod configurations.
|
||||
*
|
||||
* @param {Array.<ModConfig>} 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<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 {ModConfig} configuration The mod configuration for the given server.
|
||||
*/
|
||||
public static setModConfiguration(serverid: string, configuration: ModConfig): void {
|
||||
const cfgs = ConfigManager.config.modConfigurations
|
||||
for(let i=0; i<cfgs.length; i++){
|
||||
if(cfgs[i].id === serverid){
|
||||
cfgs[i] = configuration
|
||||
return
|
||||
}
|
||||
}
|
||||
cfgs.push(configuration)
|
||||
}
|
||||
|
||||
// User Configurable Settings
|
||||
|
||||
// Java Settings
|
||||
|
||||
/**
|
||||
* 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 {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {string} The minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
public static getMinRAM(def = false): string {
|
||||
return !def ? ConfigManager.config.settings.java.minRAM : ConfigManager.DEFAULT_CONFIG.settings.java.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} minRAM The new minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
public static setMinRAM(minRAM: string): void {
|
||||
ConfigManager.config.settings.java.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 {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {string} The maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
public static getMaxRAM(def = false): string {
|
||||
return !def ? ConfigManager.config.settings.java.maxRAM : ConfigManager.resolveMaxRAM()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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} maxRAM The new maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
public static setMaxRAM(maxRAM: string): void {
|
||||
ConfigManager.config.settings.java.maxRAM = maxRAM
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the path of the Java Executable.
|
||||
*
|
||||
* This is a resolved configuration value and defaults to null until externally assigned.
|
||||
*
|
||||
* @returns {string | null} The path of the Java Executable.
|
||||
*/
|
||||
public static getJavaExecutable(): string | null {
|
||||
return ConfigManager.config.settings.java.executable
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path of the Java Executable.
|
||||
*
|
||||
* @param {string} executable The new path of the Java Executable.
|
||||
*/
|
||||
public static setJavaExecutable(executable: string): void {
|
||||
ConfigManager.config.settings.java.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 {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {Array.<string>} 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.<string>} 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
|
||||
}
|
||||
|
||||
}
|
33
src/common/config/model/LauncherConfig.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { SavedAccount } from './SavedAccount'
|
||||
import { NewsCache } from './NewsCache'
|
||||
import { ModConfig } from './ModConfig'
|
||||
|
||||
export interface LauncherConfig {
|
||||
|
||||
settings: {
|
||||
java: {
|
||||
minRAM: string
|
||||
maxRAM: string
|
||||
executable: string | null
|
||||
jvmOptions: string[]
|
||||
}
|
||||
game: {
|
||||
resWidth: number
|
||||
resHeight: number
|
||||
fullscreen: boolean
|
||||
autoConnect: boolean
|
||||
launchDetached: boolean
|
||||
}
|
||||
launcher: {
|
||||
allowPrerelease: boolean
|
||||
dataDirectory: string
|
||||
}
|
||||
}
|
||||
newsCache: NewsCache
|
||||
clientToken: string | null
|
||||
selectedServer: string | null
|
||||
selectedAccount: string | null
|
||||
authenticationDatabase: {[uuid: string]: SavedAccount}
|
||||
modConfigurations: ModConfig[]
|
||||
|
||||
}
|
17
src/common/config/model/ModConfig.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface SubModConfig {
|
||||
|
||||
mods: {
|
||||
[id: string]: boolean | SubModConfig
|
||||
}
|
||||
value: boolean
|
||||
|
||||
}
|
||||
|
||||
export interface ModConfig {
|
||||
|
||||
id: string
|
||||
mods: {
|
||||
[id: string]: boolean | SubModConfig
|
||||
}
|
||||
|
||||
}
|
7
src/common/config/model/NewsCache.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface NewsCache {
|
||||
|
||||
date: string | null
|
||||
content: string | null
|
||||
dismissed: boolean
|
||||
|
||||
}
|
7
src/common/config/model/SavedAccount.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface SavedAccount {
|
||||
|
||||
accessToken: string
|
||||
username: string
|
||||
uuid: string
|
||||
displayName: string
|
||||
}
|
109
src/common/distribution/DistributionAPI.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { resolve } from 'path'
|
||||
import { Distribution } from 'helios-distribution-types'
|
||||
import got from 'got'
|
||||
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||
import { RestResponse, handleGotError, RestResponseStatus } from 'common/got/RestResponse'
|
||||
import { pathExists, readFile, writeFile } from 'fs-extra'
|
||||
|
||||
// TODO Option to check endpoint for hash of distro for local compare
|
||||
// Useful if distro is large (MBs)
|
||||
|
||||
export class DistributionAPI {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('DistributionAPI')
|
||||
|
||||
private readonly REMOTE_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
|
||||
|
||||
private readonly DISTRO_FILE = 'distribution.json'
|
||||
private readonly DISTRO_FILE_DEV = 'distribution_dev.json'
|
||||
|
||||
private readonly DEV_MODE = false // placeholder
|
||||
|
||||
private distroPath: string
|
||||
private distroDevPath: string
|
||||
|
||||
private rawDistribution!: Distribution
|
||||
|
||||
constructor(
|
||||
private launcherDirectory: string
|
||||
) {
|
||||
this.distroPath = resolve(launcherDirectory, this.DISTRO_FILE)
|
||||
this.distroDevPath = resolve(launcherDirectory, this.DISTRO_FILE_DEV)
|
||||
}
|
||||
|
||||
public async testLoad(): Promise<Distribution> {
|
||||
await this.loadDistribution()
|
||||
return this.rawDistribution
|
||||
}
|
||||
|
||||
protected async loadDistribution(): Promise<void> {
|
||||
|
||||
let distro
|
||||
|
||||
if(!this.DEV_MODE) {
|
||||
|
||||
distro = (await this.pullRemote()).data
|
||||
if(distro == null) {
|
||||
distro = await this.pullLocal(false)
|
||||
} else {
|
||||
this.writeDistributionToDisk(distro)
|
||||
}
|
||||
|
||||
} else {
|
||||
distro = await this.pullLocal(true)
|
||||
}
|
||||
|
||||
if(distro == null) {
|
||||
// TODO Bubble this up nicer
|
||||
throw new Error('FATAL: Unable to load distribution from remote server or local disk.')
|
||||
}
|
||||
|
||||
this.rawDistribution = distro
|
||||
}
|
||||
|
||||
protected async pullRemote(): Promise<RestResponse<Distribution | null>> {
|
||||
|
||||
try {
|
||||
|
||||
const res = await got.get<Distribution>(this.REMOTE_URL, { responseType: 'json' })
|
||||
|
||||
return {
|
||||
data: res.body,
|
||||
responseStatus: RestResponseStatus.SUCCESS
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
|
||||
return handleGotError('Pull Remote', error, DistributionAPI.logger, () => null)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected async writeDistributionToDisk(distribution: Distribution): Promise<void> {
|
||||
await writeFile(this.distroPath, distribution)
|
||||
}
|
||||
|
||||
protected async pullLocal(dev: boolean): Promise<Distribution | null> {
|
||||
return await this.readDistributionFromFile(!dev ? this.distroPath : this.distroDevPath)
|
||||
}
|
||||
|
||||
|
||||
protected async readDistributionFromFile(path: string): Promise<Distribution | null> {
|
||||
|
||||
if(await pathExists(path)) {
|
||||
const raw = await readFile(path, 'utf-8')
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch(error) {
|
||||
DistributionAPI.logger.error(`Malformed distribution file at ${path}`)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
DistributionAPI.logger.error(`No distribution file found at ${path}!`)
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
231
src/common/distribution/DistributionFactory.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import { Distribution, Server, Module, Type, Required as HeliosRequired } from 'helios-distribution-types'
|
||||
import { MavenComponents, MavenUtil } from 'common/util/MavenUtil'
|
||||
import { join } from 'path'
|
||||
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||
|
||||
const logger = LoggerUtil.getLogger('DistributionFactory')
|
||||
|
||||
export class HeliosDistribution {
|
||||
|
||||
private mainServerIndex!: number
|
||||
|
||||
public readonly servers: HeliosServer[]
|
||||
|
||||
constructor(
|
||||
public readonly rawDistribution: Distribution
|
||||
) {
|
||||
this.resolveMainServerIndex()
|
||||
this.servers = this.rawDistribution.servers.map(s => new HeliosServer(s))
|
||||
}
|
||||
|
||||
private resolveMainServerIndex(): void {
|
||||
|
||||
if(this.rawDistribution.servers.length > 0) {
|
||||
for(let i=0; i<this.rawDistribution.servers.length; i++) {
|
||||
if(this.mainServerIndex == null) {
|
||||
if(this.rawDistribution.servers[i].mainServer) {
|
||||
this.mainServerIndex = i
|
||||
}
|
||||
} else {
|
||||
this.rawDistribution.servers[i].mainServer = false
|
||||
}
|
||||
}
|
||||
if(this.mainServerIndex == null) {
|
||||
this.mainServerIndex = 0
|
||||
this.rawDistribution.servers[this.mainServerIndex].mainServer = true
|
||||
}
|
||||
} else {
|
||||
logger.warn('Distribution has 0 configured servers. This doesnt seem right..')
|
||||
this.mainServerIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
public getMainServer(): HeliosServer | null {
|
||||
return this.mainServerIndex < this.servers.length ? this.servers[this.mainServerIndex] : null
|
||||
}
|
||||
|
||||
public getServerById(id: string): HeliosServer | null {
|
||||
return this.servers.find(s => s.rawServer.id === id) || null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class HeliosServer {
|
||||
|
||||
public readonly modules: HeliosModule[]
|
||||
public readonly hostname: string
|
||||
public readonly port: number
|
||||
|
||||
constructor(
|
||||
public readonly rawServer: Server
|
||||
) {
|
||||
const { hostname, port } = this.parseAddress()
|
||||
this.hostname = hostname
|
||||
this.port = port
|
||||
this.modules = rawServer.modules.map(m => new HeliosModule(m, rawServer.id))
|
||||
}
|
||||
|
||||
private parseAddress(): { hostname: string, port: number } {
|
||||
// Srv record lookup here if needed.
|
||||
if(this.rawServer.address.includes(':')) {
|
||||
const pieces = this.rawServer.address.split(':')
|
||||
const port = Number(pieces[1])
|
||||
|
||||
if(!Number.isInteger(port)) {
|
||||
throw new Error(`Malformed server address for ${this.rawServer.id}. Port must be an integer!`)
|
||||
}
|
||||
|
||||
return { hostname: pieces[0], port }
|
||||
} else {
|
||||
return { hostname: this.rawServer.address, port: 25565 }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class HeliosModule {
|
||||
|
||||
public readonly subModules: HeliosModule[]
|
||||
|
||||
private readonly mavenComponents: Readonly<MavenComponents>
|
||||
private readonly required: Readonly<Required<HeliosRequired>>
|
||||
private readonly localPath: string
|
||||
|
||||
constructor(
|
||||
public readonly rawModule: Module,
|
||||
private readonly serverId: string
|
||||
) {
|
||||
|
||||
this.mavenComponents = this.resolveMavenComponents()
|
||||
this.required = this.resolveRequired()
|
||||
this.localPath = this.resolveLocalPath()
|
||||
|
||||
if(this.rawModule.subModules != null) {
|
||||
this.subModules = this.rawModule.subModules.map(m => new HeliosModule(m, serverId))
|
||||
} else {
|
||||
this.subModules = []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private resolveMavenComponents(): MavenComponents {
|
||||
|
||||
// Files need not have a maven identifier if they provide a path.
|
||||
if(this.rawModule.type === Type.File && this.rawModule.artifact.path != null) {
|
||||
return null! as MavenComponents
|
||||
}
|
||||
// Version Manifests never provide a maven identifier.
|
||||
if(this.rawModule.type === Type.VersionManifest) {
|
||||
return null! as MavenComponents
|
||||
}
|
||||
|
||||
const isMavenId = MavenUtil.isMavenIdentifier(this.rawModule.id)
|
||||
|
||||
if(!isMavenId) {
|
||||
if(this.rawModule.type !== Type.File) {
|
||||
throw new Error(`Module ${this.rawModule.name} (${this.rawModule.id}) of type ${this.rawModule.type} must have a valid maven identifier!`)
|
||||
} else {
|
||||
throw new Error(`Module ${this.rawModule.name} (${this.rawModule.id}) of type ${this.rawModule.type} must either declare an artifact path or have a valid maven identifier!`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return MavenUtil.getMavenComponents(this.rawModule.id)
|
||||
} catch(err) {
|
||||
throw new Error(`Failed to resolve maven components for module ${this.rawModule.name} (${this.rawModule.id}) of type ${this.rawModule.type}. Reason: ${err.message}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private resolveRequired(): Required<HeliosRequired> {
|
||||
if(this.rawModule.required == null) {
|
||||
return {
|
||||
value: true,
|
||||
def: true
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
value: this.rawModule.required.value ?? true,
|
||||
def: this.rawModule.required.def ?? true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private resolveLocalPath(): string {
|
||||
|
||||
// Version Manifests have a pre-determined path.
|
||||
if(this.rawModule.type === Type.VersionManifest) {
|
||||
return join('TODO_COMMON_DIR', 'versions', this.rawModule.id, `${this.rawModule.id}.json`)
|
||||
}
|
||||
|
||||
const relativePath = this.rawModule.artifact.path ?? MavenUtil.mavenComponentsAsNormalizedPath(
|
||||
this.mavenComponents.group,
|
||||
this.mavenComponents.artifact,
|
||||
this.mavenComponents.version,
|
||||
this.mavenComponents.classifier,
|
||||
this.mavenComponents.extension
|
||||
)
|
||||
|
||||
switch (this.rawModule.type) {
|
||||
case Type.Library:
|
||||
case Type.Forge:
|
||||
case Type.ForgeHosted:
|
||||
case Type.LiteLoader:
|
||||
return join('TODO_COMMON_DIR', 'libraries', relativePath)
|
||||
case Type.ForgeMod:
|
||||
case Type.LiteMod:
|
||||
return join('TODO_COMMON_DIR', 'modstore', relativePath)
|
||||
case Type.File:
|
||||
default:
|
||||
return join('TODO_INSTANCE_DIR', this.serverId, relativePath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public hasMavenComponents(): boolean {
|
||||
return this.mavenComponents != null
|
||||
}
|
||||
|
||||
public getMavenComponents(): Readonly<MavenComponents> {
|
||||
return this.mavenComponents
|
||||
}
|
||||
|
||||
public getRequired(): Readonly<Required<HeliosRequired>> {
|
||||
return this.required
|
||||
}
|
||||
|
||||
public getPath(): string {
|
||||
return this.localPath
|
||||
}
|
||||
|
||||
public getMavenIdentifier(): string {
|
||||
return MavenUtil.mavenComponentsToIdentifier(
|
||||
this.mavenComponents.group,
|
||||
this.mavenComponents.artifact,
|
||||
this.mavenComponents.version,
|
||||
this.mavenComponents.classifier,
|
||||
this.mavenComponents.extension
|
||||
)
|
||||
}
|
||||
|
||||
public getExtensionlessMavenIdentifier(): string {
|
||||
return MavenUtil.mavenComponentsToExtensionlessIdentifier(
|
||||
this.mavenComponents.group,
|
||||
this.mavenComponents.artifact,
|
||||
this.mavenComponents.version,
|
||||
this.mavenComponents.classifier
|
||||
)
|
||||
}
|
||||
|
||||
public getVersionlessMavenIdentifier(): string {
|
||||
return MavenUtil.mavenComponentsToVersionlessIdentifier(
|
||||
this.mavenComponents.group,
|
||||
this.mavenComponents.artifact
|
||||
)
|
||||
}
|
||||
|
||||
public hasSubModules(): boolean {
|
||||
return this.subModules.length > 0
|
||||
}
|
||||
|
||||
}
|
43
src/common/got/RestResponse.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { RequestError, HTTPError, TimeoutError, ParseError } from 'got'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export enum RestResponseStatus {
|
||||
|
||||
SUCCESS,
|
||||
ERROR
|
||||
|
||||
}
|
||||
|
||||
export interface RestResponse<T> {
|
||||
|
||||
data: T
|
||||
responseStatus: RestResponseStatus
|
||||
error?: RequestError
|
||||
|
||||
}
|
||||
|
||||
export function handleGotError<T>(operation: string, error: RequestError, logger: Logger, dataProvider: () => T): RestResponse<T> {
|
||||
const response: RestResponse<T> = {
|
||||
data: dataProvider(),
|
||||
responseStatus: RestResponseStatus.ERROR,
|
||||
error
|
||||
}
|
||||
|
||||
if(error instanceof HTTPError) {
|
||||
logger.error(`Error during ${operation} request (HTTP Response ${error.response.statusCode})`, error)
|
||||
logger.debug('Response Details:')
|
||||
logger.debug('Body:', error.response.body)
|
||||
logger.debug('Headers:', error.response.headers)
|
||||
} else if(Object.getPrototypeOf(error) instanceof RequestError) {
|
||||
logger.error(`${operation} request recieved no response (${error.code}).`, error)
|
||||
} else if(error instanceof TimeoutError) {
|
||||
logger.error(`${operation} request timed out (${error.timings.phases.total}ms).`)
|
||||
} else if(error instanceof ParseError) {
|
||||
logger.error(`${operation} request recieved unexepected body (Parse Error).`)
|
||||
} else {
|
||||
// CacheError, ReadError, MaxRedirectsError, UnsupportedProtocolError, CancelError
|
||||
logger.error(`Error during ${operation} request.`, error)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
41
src/common/logging/loggerutil.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { createLogger, format, transports, Logger } from 'winston'
|
||||
import { SPLAT } from 'triple-beam'
|
||||
import { DateTime } from 'luxon'
|
||||
import { inspect } from 'util'
|
||||
|
||||
export class LoggerUtil {
|
||||
|
||||
public static getLogger(label: string): Logger {
|
||||
return createLogger({
|
||||
format: format.combine(
|
||||
format.label(),
|
||||
format.colorize(),
|
||||
format.label({ label }),
|
||||
format.printf(info => {
|
||||
if(info[SPLAT]) {
|
||||
if(info[SPLAT].length === 1 && info[SPLAT][0] instanceof Error) {
|
||||
const err = info[SPLAT][0] as Error
|
||||
if(info.message.length > err.message.length && info.message.endsWith(err.message)) {
|
||||
info.message = info.message.substring(0, info.message.length-err.message.length)
|
||||
}
|
||||
} else if(info[SPLAT].length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
info.message += ' ' + info[SPLAT].map((it: any) => {
|
||||
if(typeof it === 'object' && it != null) {
|
||||
return inspect(it, false, null, true)
|
||||
}
|
||||
return it
|
||||
}).join(' ')
|
||||
}
|
||||
}
|
||||
return `[${DateTime.local().toFormat('yyyy-MM-dd TT').trim()}] [${info.level}] [${info.label}]: ${info.message}${info.stack ? `\n${info.stack}` : ''}`
|
||||
})
|
||||
),
|
||||
level: process.env.NODE_ENV === 'test' ? 'emerg' : 'debug',
|
||||
transports: [
|
||||
new transports.Console()
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
}
|
157
src/common/mojang/net/Protocol.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Utility Class to construct a packet conforming to Minecraft's
|
||||
* protocol. All data types are BE except VarInt and VarLong.
|
||||
*
|
||||
* @see https://wiki.vg/Protocol
|
||||
*/
|
||||
export class ServerBoundPacket {
|
||||
|
||||
private buffer: number[]
|
||||
|
||||
protected constructor() {
|
||||
this.buffer = []
|
||||
}
|
||||
|
||||
public static build(): ServerBoundPacket {
|
||||
return new ServerBoundPacket()
|
||||
}
|
||||
|
||||
/**
|
||||
* Packet is prefixed with its data length as a VarInt.
|
||||
*
|
||||
* @see https://wiki.vg/Protocol#Packet_format
|
||||
*/
|
||||
public toBuffer(): Buffer {
|
||||
const finalizedPacket = new ServerBoundPacket()
|
||||
finalizedPacket.writeVarInt(this.buffer.length)
|
||||
finalizedPacket.writeBytes(...this.buffer)
|
||||
|
||||
return Buffer.from(finalizedPacket.buffer)
|
||||
}
|
||||
|
||||
public writeBytes(...bytes: number[]): ServerBoundPacket {
|
||||
this.buffer.push(...bytes)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://wiki.vg/Protocol#VarInt_and_VarLong
|
||||
*/
|
||||
public writeVarInt(value: number): ServerBoundPacket {
|
||||
do {
|
||||
let temp = value & 0b01111111
|
||||
|
||||
value >>>= 7
|
||||
|
||||
if (value != 0) {
|
||||
temp |= 0b10000000
|
||||
}
|
||||
|
||||
this.writeBytes(temp)
|
||||
} while (value != 0)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Strings are prefixed with their length as a VarInt.
|
||||
*
|
||||
* @see https://wiki.vg/Protocol#Data_types
|
||||
*/
|
||||
public writeString(string: string): ServerBoundPacket {
|
||||
this.writeVarInt(string.length)
|
||||
for (let i=0; i<string.length; i++) {
|
||||
this.writeBytes(string.codePointAt(i)!)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public writeUnsignedShort(short: number): ServerBoundPacket {
|
||||
const buf = Buffer.alloc(2)
|
||||
buf.writeUInt16BE(short, 0)
|
||||
this.writeBytes(...buf)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility Class to read a client-bound packet conforming to
|
||||
* Minecraft's protocol. All data types are BE except VarInt
|
||||
* and VarLong.
|
||||
*
|
||||
* @see https://wiki.vg/Protocol
|
||||
*/
|
||||
export class ClientBoundPacket {
|
||||
|
||||
private buffer: number[]
|
||||
|
||||
constructor(buffer: Buffer) {
|
||||
this.buffer = [...buffer]
|
||||
}
|
||||
|
||||
public append(buffer: Buffer): void {
|
||||
this.buffer.push(...buffer)
|
||||
}
|
||||
|
||||
public readByte(): number {
|
||||
return this.buffer.shift()!
|
||||
}
|
||||
|
||||
public readBytes(length: number): number[] {
|
||||
const value = this.buffer.slice(0, length)
|
||||
this.buffer.splice(0, length)
|
||||
return value
|
||||
}
|
||||
|
||||
public readVarInt(): number {
|
||||
|
||||
let numRead = 0
|
||||
let result = 0
|
||||
let read
|
||||
|
||||
do {
|
||||
read = this.readByte()
|
||||
const value = (read & 0b01111111)
|
||||
result |= (value << (7 * numRead))
|
||||
|
||||
numRead++
|
||||
if (numRead > 5) {
|
||||
throw new Error('VarInt is too big')
|
||||
}
|
||||
} while ((read & 0b10000000) != 0)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public readString(): string {
|
||||
const length = this.readVarInt()
|
||||
const data = this.readBytes(length)
|
||||
|
||||
let value = ''
|
||||
|
||||
for (let i=0; i<data.length; i++) {
|
||||
value += String.fromCharCode(data[i])
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ProtocolUtils {
|
||||
|
||||
public static getVarIntSize(value: number): number {
|
||||
let size = 0
|
||||
|
||||
do {
|
||||
value >>>= 7
|
||||
size++
|
||||
} while (value != 0)
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
}
|
186
src/common/mojang/net/ServerStatusAPI.ts
Normal file
@ -0,0 +1,186 @@
|
||||
/* eslint-disable no-control-regex */
|
||||
import { connect } from 'net'
|
||||
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||
import { ServerBoundPacket, ClientBoundPacket, ProtocolUtils } from './Protocol'
|
||||
|
||||
const logger = LoggerUtil.getLogger('ServerStatusUtil')
|
||||
|
||||
export interface ServerStatus {
|
||||
version: {
|
||||
name: string
|
||||
protocol: number
|
||||
}
|
||||
players: {
|
||||
max: number
|
||||
online: number
|
||||
sample: {
|
||||
name: string
|
||||
id: string
|
||||
}[]
|
||||
}
|
||||
description: {
|
||||
text: string
|
||||
}
|
||||
favicon: string
|
||||
modinfo?: { // Only for modded servers
|
||||
type: string // Ex. FML
|
||||
modList: {
|
||||
modid: string
|
||||
version: string
|
||||
}[]
|
||||
}
|
||||
retrievedAt: number // Internal tracking
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the handshake packet.
|
||||
*
|
||||
* @param protocol The client's protocol version.
|
||||
* @param hostname The server hostname.
|
||||
* @param port The server port.
|
||||
*
|
||||
* @see https://wiki.vg/Server_List_Ping#Handshake
|
||||
*/
|
||||
function getHandshakePacket(protocol: number, hostname: string, port: number): Buffer {
|
||||
|
||||
return ServerBoundPacket.build()
|
||||
.writeVarInt(0x00) // Packet Id
|
||||
.writeVarInt(protocol)
|
||||
.writeString(hostname)
|
||||
.writeUnsignedShort(port)
|
||||
.writeVarInt(1) // State, 1 = status
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request packet.
|
||||
*
|
||||
* @see https://wiki.vg/Server_List_Ping#Request
|
||||
*/
|
||||
function getRequestPacket(): Buffer {
|
||||
|
||||
return ServerBoundPacket.build()
|
||||
.writeVarInt(0x00)
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Some servers do not return the same status object. Unify
|
||||
* the response so that the caller need only worry about
|
||||
* handling a single format.
|
||||
*
|
||||
* @param resp The servevr status response.
|
||||
*/
|
||||
function unifyStatusResponse(resp: ServerStatus): ServerStatus {
|
||||
// Some servers don't wrap their description in a text object.
|
||||
if(typeof resp.description === 'string') {
|
||||
resp.description = {
|
||||
text: resp.description
|
||||
}
|
||||
}
|
||||
resp.retrievedAt = (new Date()).getTime()
|
||||
return resp
|
||||
}
|
||||
|
||||
export function getServerStatus(protocol: number, hostname: string, port = 25565): Promise<ServerStatus | undefined> {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const socket = connect(port, hostname, () => {
|
||||
socket.write(getHandshakePacket(protocol, hostname, port))
|
||||
socket.write(getRequestPacket())
|
||||
})
|
||||
|
||||
socket.setTimeout(5000, () => {
|
||||
socket.destroy()
|
||||
logger.error(`Server Status Socket timed out (${hostname}:${port})`)
|
||||
reject(new Error(`Server Status Socket timed out (${hostname}:${port})`))
|
||||
})
|
||||
|
||||
const maxTries = 2
|
||||
let iterations = 0
|
||||
let bytesLeft = -1
|
||||
|
||||
socket.once('data', (data) => {
|
||||
|
||||
const inboundPacket = new ClientBoundPacket(data)
|
||||
|
||||
// Length of Packet ID + Data
|
||||
const packetLength = inboundPacket.readVarInt() // First VarInt is packet length.
|
||||
const packetType = inboundPacket.readVarInt() // Second VarInt is packet type.
|
||||
|
||||
if(packetType !== 0x00) {
|
||||
// TODO
|
||||
socket.destroy()
|
||||
reject(new Error(`Invalid response. Expected packet type ${0x00}, received ${packetType}!`))
|
||||
return
|
||||
}
|
||||
|
||||
// Size of packetLength VarInt is not included in the packetLength.
|
||||
bytesLeft = packetLength + ProtocolUtils.getVarIntSize(packetLength)
|
||||
|
||||
// Listener to keep reading until we have read all the bytes into the buffer.
|
||||
const packetReadListener = (nextData: Buffer, doAppend: boolean) => {
|
||||
|
||||
if(iterations > maxTries) {
|
||||
socket.destroy()
|
||||
reject(new Error(`Data read from ${hostname}:${port} exceeded ${maxTries} iterations, closing connection.`))
|
||||
return
|
||||
}
|
||||
++iterations
|
||||
|
||||
if(bytesLeft > 0) {
|
||||
bytesLeft -= nextData.length
|
||||
if(doAppend) {
|
||||
inboundPacket.append(nextData)
|
||||
}
|
||||
}
|
||||
|
||||
// All bytes read, attempt conversion.
|
||||
if(bytesLeft === 0) {
|
||||
|
||||
// Remainder of Buffer is the server status json.
|
||||
const result = inboundPacket.readString()
|
||||
|
||||
try {
|
||||
const parsed: ServerStatus = JSON.parse(result)
|
||||
socket.end()
|
||||
resolve(unifyStatusResponse(parsed))
|
||||
} catch(err) {
|
||||
socket.destroy()
|
||||
logger.error('Failed to parse server status JSON', err)
|
||||
reject(new Error('Failed to parse server status JSON'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read the data we just received.
|
||||
packetReadListener(data, false)
|
||||
// Add a listener to keep reading if the data is too long.
|
||||
socket.on('data', (data) => packetReadListener(data, true))
|
||||
|
||||
})
|
||||
|
||||
socket.on('error', (err: NodeJS.ErrnoException) => {
|
||||
socket.destroy()
|
||||
|
||||
if(err.code === 'ENOTFOUND') {
|
||||
// ENOTFOUND = Unable to resolve.
|
||||
logger.error(`Server ${hostname}:${port} not found!`)
|
||||
resolve(undefined)
|
||||
return
|
||||
} else if(err.code === 'ECONNREFUSED') {
|
||||
// ECONNREFUSED = Unable to connect to port.
|
||||
logger.error(`Server ${hostname}:${port} refused to connect, is the port correct?`)
|
||||
resolve(undefined)
|
||||
return
|
||||
} else {
|
||||
logger.error(`Error trying to pull server status (${hostname}:${port})`, err)
|
||||
resolve(undefined)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
34
src/common/mojang/rest/Auth.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export interface Agent {
|
||||
|
||||
name: 'Minecraft'
|
||||
version: number
|
||||
|
||||
}
|
||||
|
||||
export interface AuthPayload {
|
||||
|
||||
agent: Agent
|
||||
username: string
|
||||
password: string
|
||||
clientToken?: string
|
||||
requestUser?: boolean
|
||||
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
|
||||
accessToken: string
|
||||
clientToken: string
|
||||
selectedProfile: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
user?: {
|
||||
id: string
|
||||
properties: Array<{
|
||||
name: string
|
||||
value: string
|
||||
}>
|
||||
}
|
||||
|
||||
}
|
306
src/common/mojang/rest/MojangRestAPI.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { LoggerUtil } from '../../logging/loggerutil'
|
||||
import { MojangStatus, MojangStatusColor } from './internal/MojangStatus'
|
||||
import got, { RequestError, HTTPError } from 'got'
|
||||
import { MojangResponse, MojangErrorCode, decipherErrorCode, isInternalError, MojangErrorBody } from './internal/MojangResponse'
|
||||
import { RestResponseStatus, handleGotError } from 'common/got/RestResponse'
|
||||
import { Agent, AuthPayload, Session } from './Auth'
|
||||
|
||||
export class MojangRestAPI {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('Mojang')
|
||||
|
||||
private static readonly TIMEOUT = 2500
|
||||
|
||||
public static readonly AUTH_ENDPOINT = 'https://authserver.mojang.com'
|
||||
public static readonly STATUS_ENDPOINT = 'https://status.mojang.com'
|
||||
|
||||
private static authClient = got.extend({
|
||||
prefixUrl: MojangRestAPI.AUTH_ENDPOINT,
|
||||
responseType: 'json',
|
||||
retry: 0
|
||||
})
|
||||
private static statusClient = got.extend({
|
||||
prefixUrl: MojangRestAPI.STATUS_ENDPOINT,
|
||||
responseType: 'json',
|
||||
retry: 0
|
||||
})
|
||||
|
||||
public static readonly MINECRAFT_AGENT: Agent = {
|
||||
name: 'Minecraft',
|
||||
version: 1
|
||||
}
|
||||
|
||||
protected static statuses: MojangStatus[] = MojangRestAPI.getDefaultStatuses()
|
||||
|
||||
public static getDefaultStatuses(): MojangStatus[] {
|
||||
return [
|
||||
{
|
||||
service: 'sessionserver.mojang.com',
|
||||
status: MojangStatusColor.GREY,
|
||||
name: 'Multiplayer Session Service',
|
||||
essential: true
|
||||
},
|
||||
{
|
||||
service: 'authserver.mojang.com',
|
||||
status: MojangStatusColor.GREY,
|
||||
name: 'Authentication Service',
|
||||
essential: true
|
||||
},
|
||||
{
|
||||
service: 'textures.minecraft.net',
|
||||
status: MojangStatusColor.GREY,
|
||||
name: 'Minecraft Skins',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'api.mojang.com',
|
||||
status: MojangStatusColor.GREY,
|
||||
name: 'Public API',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'minecraft.net',
|
||||
status: MojangStatusColor.GREY,
|
||||
name: 'Minecraft.net',
|
||||
essential: false
|
||||
},
|
||||
{
|
||||
service: 'account.mojang.com',
|
||||
status: MojangStatusColor.GREY,
|
||||
name: 'Mojang Accounts Website',
|
||||
essential: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Mojang status color to a hex value. Valid statuses
|
||||
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
|
||||
* to our project which represents an unknown status.
|
||||
*/
|
||||
public static statusToHex(status: string): string {
|
||||
switch(status.toLowerCase()){
|
||||
case MojangStatusColor.GREEN:
|
||||
return '#a5c325'
|
||||
case MojangStatusColor.YELLOW:
|
||||
return '#eac918'
|
||||
case MojangStatusColor.RED:
|
||||
return '#c32625'
|
||||
case MojangStatusColor.GREY:
|
||||
default:
|
||||
return '#848484'
|
||||
}
|
||||
}
|
||||
|
||||
private static handleGotError<T>(operation: string, error: RequestError, dataProvider: () => T): MojangResponse<T> {
|
||||
|
||||
const response: MojangResponse<T> = handleGotError(operation, error, MojangRestAPI.logger, dataProvider)
|
||||
|
||||
if(error instanceof HTTPError) {
|
||||
response.mojangErrorCode = decipherErrorCode(error.response.body as MojangErrorBody)
|
||||
} else {
|
||||
response.mojangErrorCode = MojangErrorCode.UNKNOWN
|
||||
}
|
||||
response.isInternalError = isInternalError(response.mojangErrorCode)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private static expectSpecificSuccess(operation: string, expected: number, actual: number) {
|
||||
if(actual !== expected) {
|
||||
MojangRestAPI.logger.warn(`${operation} expected ${expected} response, recieved ${actual}.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of Mojang's services.
|
||||
* The response is condensed into a single object. Each service is
|
||||
* a key, where the value is an object containing a status and name
|
||||
* property.
|
||||
*
|
||||
* @see http://wiki.vg/Mojang_API#API_Status
|
||||
*/
|
||||
public static async status(): Promise<MojangResponse<MojangStatus[]>>{
|
||||
try {
|
||||
|
||||
const res = await MojangRestAPI.statusClient.get<{[service: string]: MojangStatusColor}[]>('check')
|
||||
|
||||
MojangRestAPI.expectSpecificSuccess('Mojang Status', 200, res.statusCode)
|
||||
|
||||
res.body.forEach(status => {
|
||||
const entry = Object.entries(status)[0]
|
||||
for(let i=0; i<MojangRestAPI.statuses.length; i++) {
|
||||
if(MojangRestAPI.statuses[i].service === entry[0]) {
|
||||
MojangRestAPI.statuses[i].status = entry[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
data: MojangRestAPI.statuses,
|
||||
responseStatus: RestResponseStatus.SUCCESS
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
|
||||
return MojangRestAPI.handleGotError('Mojang Status', error, () => {
|
||||
for(let i=0; i<MojangRestAPI.statuses.length; i++){
|
||||
MojangRestAPI.statuses[i].status = MojangStatusColor.GREY
|
||||
}
|
||||
return MojangRestAPI.statuses
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user with their Mojang credentials.
|
||||
*
|
||||
* @param {string} username The user's username, this is often an email.
|
||||
* @param {string} password The user's password.
|
||||
* @param {string} clientToken The launcher's Client Token.
|
||||
* @param {boolean} requestUser Optional. Adds user object to the reponse.
|
||||
* @param {Object} agent Optional. Provided by default. Adds user info to the response.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Authenticate
|
||||
*/
|
||||
public static async authenticate(
|
||||
username: string,
|
||||
password: string,
|
||||
clientToken: string | null,
|
||||
requestUser = true,
|
||||
agent: Agent = MojangRestAPI.MINECRAFT_AGENT
|
||||
): Promise<MojangResponse<Session | null>> {
|
||||
|
||||
try {
|
||||
|
||||
const json: AuthPayload = {
|
||||
agent,
|
||||
username,
|
||||
password,
|
||||
requestUser
|
||||
}
|
||||
if(clientToken != null){
|
||||
json.clientToken = clientToken
|
||||
}
|
||||
|
||||
const res = await MojangRestAPI.authClient.post<Session>('authenticate', { json, responseType: 'json' })
|
||||
MojangRestAPI.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode)
|
||||
return {
|
||||
data: res.body,
|
||||
responseStatus: RestResponseStatus.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
return MojangRestAPI.handleGotError('Mojang Authenticate', err, () => null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an access token. This should always be done before launching.
|
||||
* The client token should match the one used to create the access token.
|
||||
*
|
||||
* @param {string} accessToken The access token to validate.
|
||||
* @param {string} clientToken The launcher's client token.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Validate
|
||||
*/
|
||||
public static async validate(accessToken: string, clientToken: string): Promise<MojangResponse<boolean>> {
|
||||
|
||||
try {
|
||||
|
||||
const json = {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
|
||||
const res = await MojangRestAPI.authClient.post('validate', { json })
|
||||
MojangRestAPI.expectSpecificSuccess('Mojang Validate', 204, res.statusCode)
|
||||
|
||||
return {
|
||||
data: res.statusCode === 204,
|
||||
responseStatus: RestResponseStatus.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
if(err instanceof HTTPError && err.response.statusCode === 403) {
|
||||
return {
|
||||
data: false,
|
||||
responseStatus: RestResponseStatus.SUCCESS
|
||||
}
|
||||
}
|
||||
return MojangRestAPI.handleGotError('Mojang Validate', err, () => false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates an access token. The clientToken must match the
|
||||
* token used to create the provided accessToken.
|
||||
*
|
||||
* @param {string} accessToken The access token to invalidate.
|
||||
* @param {string} clientToken The launcher's client token.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Invalidate
|
||||
*/
|
||||
public static async invalidate(accessToken: string, clientToken: string): Promise<MojangResponse<undefined>> {
|
||||
|
||||
try {
|
||||
|
||||
const json = {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
|
||||
const res = await MojangRestAPI.authClient.post('invalidate', { json })
|
||||
MojangRestAPI.expectSpecificSuccess('Mojang Invalidate', 204, res.statusCode)
|
||||
|
||||
return {
|
||||
data: undefined,
|
||||
responseStatus: RestResponseStatus.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
return MojangRestAPI.handleGotError('Mojang Invalidate', err, () => undefined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh a user's authentication. This should be used to keep a user logged
|
||||
* in without asking them for their credentials again. A new access token will
|
||||
* be generated using a recent invalid access token. See Wiki for more info.
|
||||
*
|
||||
* @param {string} accessToken The old access token.
|
||||
* @param {string} clientToken The launcher's client token.
|
||||
* @param {boolean} requestUser Optional. Adds user object to the reponse.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Refresh
|
||||
*/
|
||||
public static async refresh(accessToken: string, clientToken: string, requestUser = true): Promise<MojangResponse<Session | null>> {
|
||||
|
||||
try {
|
||||
|
||||
const json = {
|
||||
accessToken,
|
||||
clientToken,
|
||||
requestUser
|
||||
}
|
||||
|
||||
const res = await MojangRestAPI.authClient.post<Session>('refresh', { json, responseType: 'json' })
|
||||
MojangRestAPI.expectSpecificSuccess('Mojang Refresh', 200, res.statusCode)
|
||||
|
||||
return {
|
||||
data: res.body,
|
||||
responseStatus: RestResponseStatus.SUCCESS
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
return MojangRestAPI.handleGotError('Mojang Refresh', err, () => null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
87
src/common/mojang/rest/internal/MojangResponse.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { RestResponse } from 'common/got/RestResponse'
|
||||
|
||||
/**
|
||||
* @see https://wiki.vg/Authentication#Errors
|
||||
*/
|
||||
export enum MojangErrorCode {
|
||||
ERROR_METHOD_NOT_ALLOWED, // INTERNAL
|
||||
ERROR_NOT_FOUND, // INTERNAL
|
||||
ERROR_USER_MIGRATED,
|
||||
ERROR_INVALID_CREDENTIALS,
|
||||
ERROR_RATELIMIT,
|
||||
ERROR_INVALID_TOKEN,
|
||||
ERROR_ACCESS_TOKEN_HAS_PROFILE, // ??
|
||||
ERROR_CREDENTIALS_ARE_NULL, // INTERNAL
|
||||
ERROR_INVALID_SALT_VERSION, // ??
|
||||
ERROR_UNSUPPORTED_MEDIA_TYPE, // INTERNAL
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
export interface MojangResponse<T> extends RestResponse<T> {
|
||||
mojangErrorCode?: MojangErrorCode
|
||||
isInternalError?: boolean
|
||||
}
|
||||
|
||||
export interface MojangErrorBody {
|
||||
error: string
|
||||
errorMessage: string
|
||||
cause?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the error response code from the response body.
|
||||
*
|
||||
* @param body The mojang error body response.
|
||||
*/
|
||||
export function decipherErrorCode(body: MojangErrorBody): MojangErrorCode {
|
||||
|
||||
if(body.error === 'Method Not Allowed') {
|
||||
return MojangErrorCode.ERROR_METHOD_NOT_ALLOWED
|
||||
} else if(body.error === 'Not Found') {
|
||||
return MojangErrorCode.ERROR_NOT_FOUND
|
||||
} else if(body.error === 'Unsupported Media Type') {
|
||||
return MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE
|
||||
} else if(body.error === 'ForbiddenOperationException') {
|
||||
|
||||
if(body.cause && body.cause === 'UserMigratedException') {
|
||||
return MojangErrorCode.ERROR_USER_MIGRATED
|
||||
}
|
||||
|
||||
if(body.errorMessage === 'Invalid credentials. Invalid username or password.') {
|
||||
return MojangErrorCode.ERROR_INVALID_CREDENTIALS
|
||||
} else if(body.errorMessage === 'Invalid credentials.') {
|
||||
return MojangErrorCode.ERROR_RATELIMIT
|
||||
} else if(body.errorMessage === 'Invalid token.') {
|
||||
return MojangErrorCode.ERROR_INVALID_TOKEN
|
||||
}
|
||||
|
||||
} else if(body.error === 'IllegalArgumentException') {
|
||||
|
||||
if(body.errorMessage === 'Access token already has a profile assigned.') {
|
||||
return MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE
|
||||
} else if(body.errorMessage === 'credentials is null') {
|
||||
return MojangErrorCode.ERROR_CREDENTIALS_ARE_NULL
|
||||
} else if(body.errorMessage === 'Invalid salt version') {
|
||||
return MojangErrorCode.ERROR_INVALID_SALT_VERSION
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return MojangErrorCode.UNKNOWN
|
||||
|
||||
}
|
||||
|
||||
// These indicate problems with the code and not the data.
|
||||
export function isInternalError(errorCode: MojangErrorCode): boolean {
|
||||
switch(errorCode) {
|
||||
case MojangErrorCode.ERROR_METHOD_NOT_ALLOWED: // We've sent the wrong method to an endpoint. (ex. GET to POST)
|
||||
case MojangErrorCode.ERROR_NOT_FOUND: // Indicates endpoint has changed. (404)
|
||||
case MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE: // Selecting profiles isn't implemented yet. (Shouldnt happen)
|
||||
case MojangErrorCode.ERROR_CREDENTIALS_ARE_NULL: // Username/password was not submitted. (UI should forbid this)
|
||||
case MojangErrorCode.ERROR_INVALID_SALT_VERSION: // ??? (Shouldnt happen)
|
||||
case MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE: // Data was not submitted as application/json
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
15
src/common/mojang/rest/internal/MojangStatus.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export enum MojangStatusColor {
|
||||
RED = 'red',
|
||||
YELLOW = 'yellow',
|
||||
GREEN = 'green',
|
||||
GREY = 'grey'
|
||||
}
|
||||
|
||||
export interface MojangStatus {
|
||||
|
||||
service: string
|
||||
status: MojangStatusColor
|
||||
name: string
|
||||
essential: boolean
|
||||
|
||||
}
|
34
src/common/util/FileUtils.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { join } from 'path'
|
||||
import { pathExists, readFile } from 'fs-extra'
|
||||
|
||||
export function calculateHash(buf: Buffer, algo: string): string {
|
||||
return createHash(algo).update(buf).digest('hex')
|
||||
}
|
||||
|
||||
export async function validateLocalFile(path: string, algo: string, hash?: string): Promise<boolean> {
|
||||
if(await pathExists(path)) {
|
||||
if(hash == null) {
|
||||
return true
|
||||
}
|
||||
const buf = await readFile(path)
|
||||
return calculateHash(buf, algo) === hash
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getVersionExtPath(commonDir: string, version: string, ext: string) {
|
||||
return join(commonDir, 'versions', version, `${version}.${ext}`)
|
||||
}
|
||||
|
||||
export function getVersionJsonPath(commonDir: string, version: string): string {
|
||||
return getVersionExtPath(commonDir, version, 'json')
|
||||
}
|
||||
|
||||
export function getVersionJarPath(commonDir: string, version: string): string {
|
||||
return getVersionExtPath(commonDir, version, 'jar')
|
||||
}
|
||||
|
||||
export function getLibraryDir(commonDir: string): string {
|
||||
return join(commonDir, 'libraries')
|
||||
}
|
107
src/common/util/MavenUtil.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { normalize } from 'path'
|
||||
import { URL } from 'url'
|
||||
|
||||
export interface MavenComponents {
|
||||
group: string
|
||||
artifact: string
|
||||
version: string
|
||||
classifier?: string
|
||||
extension: string
|
||||
}
|
||||
|
||||
export class MavenUtil {
|
||||
|
||||
public static readonly ID_REGEX = /(.+):(.+):([^@]+)()(?:@{1}(.+)$)?/
|
||||
public static readonly ID_REGEX_WITH_CLASSIFIER = /(.+):(.+):(?:([^@]+)(?:-([a-zA-Z]+)))(?:@{1}(.+)$)?/
|
||||
|
||||
public static mavenComponentsToIdentifier(
|
||||
group: string,
|
||||
artifact: string,
|
||||
version: string,
|
||||
classifier?: string,
|
||||
extension?: string
|
||||
): string {
|
||||
return `${group}:${artifact}:${version}${classifier != null ? `:${classifier}` : ''}${extension != null ? `@${extension}` : ''}`
|
||||
}
|
||||
|
||||
public static mavenComponentsToExtensionlessIdentifier(
|
||||
group: string,
|
||||
artifact: string,
|
||||
version: string,
|
||||
classifier?: string
|
||||
): string {
|
||||
return MavenUtil.mavenComponentsToIdentifier(group, artifact, version, classifier)
|
||||
}
|
||||
|
||||
public static mavenComponentsToVersionlessIdentifier(
|
||||
group: string,
|
||||
artifact: string
|
||||
): string {
|
||||
return `${group}:${artifact}`
|
||||
}
|
||||
|
||||
public static isMavenIdentifier(id: string): boolean {
|
||||
return MavenUtil.ID_REGEX.test(id) || MavenUtil.ID_REGEX_WITH_CLASSIFIER.test(id)
|
||||
}
|
||||
|
||||
public static getMavenComponents(id: string, extension = 'jar'): MavenComponents {
|
||||
if (!MavenUtil.isMavenIdentifier(id)) {
|
||||
throw new Error('Id is not a maven identifier.')
|
||||
}
|
||||
|
||||
let result
|
||||
|
||||
if (MavenUtil.ID_REGEX_WITH_CLASSIFIER.test(id)) {
|
||||
result = MavenUtil.ID_REGEX_WITH_CLASSIFIER.exec(id)
|
||||
} else {
|
||||
result = MavenUtil.ID_REGEX.exec(id)
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
return {
|
||||
group: result[1],
|
||||
artifact: result[2],
|
||||
version: result[3],
|
||||
classifier: result[4] || undefined,
|
||||
extension: result[5] || extension
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to process maven data.')
|
||||
}
|
||||
|
||||
public static mavenIdentifierAsPath(id: string, extension = 'jar'): string {
|
||||
const tmp = MavenUtil.getMavenComponents(id, extension)
|
||||
|
||||
return MavenUtil.mavenComponentsAsPath(
|
||||
tmp.group, tmp.artifact, tmp.version, tmp.classifier, tmp.extension
|
||||
)
|
||||
}
|
||||
|
||||
public static mavenComponentsAsPath(
|
||||
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): string {
|
||||
return `${group.replace(/\./g, '/')}/${artifact}/${version}/${artifact}-${version}${classifier != null ? `-${classifier}` : ''}.${extension}`
|
||||
}
|
||||
|
||||
public static mavenIdentifierToUrl(id: string, extension = 'jar'): URL {
|
||||
return new URL(MavenUtil.mavenIdentifierAsPath(id, extension))
|
||||
}
|
||||
|
||||
public static mavenComponentsToUrl(
|
||||
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): URL {
|
||||
return new URL(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension))
|
||||
}
|
||||
|
||||
public static mavenIdentifierToPath(id: string, extension = 'jar'): string {
|
||||
return normalize(MavenUtil.mavenIdentifierAsPath(id, extension))
|
||||
}
|
||||
|
||||
public static mavenComponentsAsNormalizedPath(
|
||||
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): string {
|
||||
return normalize(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension))
|
||||
}
|
||||
|
||||
}
|
60
src/common/util/MojangUtils.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Rule, Natives } from '../asset/model/mojang/VersionJson'
|
||||
|
||||
export function getMojangOS(): string {
|
||||
const opSys = process.platform
|
||||
switch(opSys) {
|
||||
case 'darwin':
|
||||
return 'osx'
|
||||
case 'win32':
|
||||
return 'windows'
|
||||
case 'linux':
|
||||
return 'linux'
|
||||
default:
|
||||
return opSys
|
||||
}
|
||||
}
|
||||
|
||||
export function validateLibraryRules(rules?: Rule[]): boolean {
|
||||
if(rules == null) {
|
||||
return false
|
||||
}
|
||||
for(const rule of rules){
|
||||
if(rule.action != null && rule.os != null){
|
||||
const osName = rule.os.name
|
||||
const osMoj = getMojangOS()
|
||||
if(rule.action === 'allow'){
|
||||
return osName === osMoj
|
||||
} else if(rule.action === 'disallow'){
|
||||
return osName !== osMoj
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function validateLibraryNatives(natives?: Natives): boolean {
|
||||
return natives == null ? true : Object.hasOwnProperty.call(natives, getMojangOS())
|
||||
}
|
||||
|
||||
export function isLibraryCompatible(rules?: Rule[], natives?: Natives): boolean {
|
||||
return rules == null ? validateLibraryNatives(natives) : validateLibraryRules(rules)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function mcVersionAtLeast(desired: string, actual: string): boolean {
|
||||
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
|
||||
}
|
5
src/common/util/isdev.ts
Normal file
@ -0,0 +1,5 @@
|
||||
'use strict'
|
||||
const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV as string, 10) === 1
|
||||
const isEnvSet = 'ELECTRON_IS_DEV' in process.env
|
||||
|
||||
export default (isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath))) as boolean
|
@ -1,16 +1,23 @@
|
||||
// Requirements
|
||||
const {app, BrowserWindow, ipcMain} = require('electron')
|
||||
const Menu = require('electron').Menu
|
||||
const autoUpdater = require('electron-updater').autoUpdater
|
||||
const ejse = require('ejs-electron')
|
||||
const fs = require('fs')
|
||||
const isDev = require('./app/assets/js/isdev')
|
||||
const path = require('path')
|
||||
const semver = require('semver')
|
||||
const url = require('url')
|
||||
import { ipcMain, app, BrowserWindow, Menu, MenuItem } from 'electron'
|
||||
import { prerelease } from 'semver'
|
||||
import { join } from 'path'
|
||||
import { format } from 'url'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import isdev from '../common/util/isdev'
|
||||
|
||||
declare const __static: string
|
||||
|
||||
const installExtensions = async () => {
|
||||
|
||||
const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } = await import('electron-devtools-installer')
|
||||
const forceDownload = !!process.env.UPGRADE_EXTENSIONS
|
||||
const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS]
|
||||
|
||||
return installExtension(extensions, forceDownload).catch(console.log) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
// Setup auto updater.
|
||||
function initAutoUpdater(event, data) {
|
||||
function initAutoUpdater(event: any, data: any) {
|
||||
|
||||
if(data){
|
||||
autoUpdater.allowPrerelease = true
|
||||
@ -19,9 +26,9 @@ function initAutoUpdater(event, data) {
|
||||
// autoUpdater.allowPrerelease = true
|
||||
}
|
||||
|
||||
if(isDev){
|
||||
if(isdev){
|
||||
autoUpdater.autoInstallOnAppQuit = false
|
||||
autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml')
|
||||
autoUpdater.updateConfigPath = join(__dirname, '..', 'dev-app-update.yml')
|
||||
}
|
||||
if(process.platform === 'darwin'){
|
||||
autoUpdater.autoDownload = false
|
||||
@ -59,7 +66,7 @@ ipcMain.on('autoUpdateAction', (event, arg, data) => {
|
||||
break
|
||||
case 'allowPrereleaseChange':
|
||||
if(!data){
|
||||
const preRelComp = semver.prerelease(app.getVersion())
|
||||
const preRelComp = prerelease(app.getVersion())
|
||||
if(preRelComp != null && preRelComp.length > 0){
|
||||
autoUpdater.allowPrerelease = true
|
||||
} else {
|
||||
@ -88,9 +95,13 @@ app.disableHardwareAcceleration()
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let win
|
||||
let win: BrowserWindow | null
|
||||
|
||||
function createWindow() {
|
||||
async function createWindow() {
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
await installExtensions()
|
||||
}
|
||||
|
||||
win = new BrowserWindow({
|
||||
width: 980,
|
||||
@ -98,20 +109,32 @@ function createWindow() {
|
||||
icon: getPlatformIcon('SealCircle'),
|
||||
frame: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'),
|
||||
preload: join(__dirname, '..', 'out', 'preloader.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
contextIsolation: false,
|
||||
enableRemoteModule: true,
|
||||
worldSafeExecuteJavaScript: true
|
||||
},
|
||||
backgroundColor: '#171614'
|
||||
})
|
||||
|
||||
ejse.data('bkid', Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)))
|
||||
if (isdev) {
|
||||
win.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`)
|
||||
}
|
||||
else {
|
||||
win.loadURL(format({
|
||||
pathname: join(__dirname, 'index.html'),
|
||||
protocol: 'file',
|
||||
slashes: true
|
||||
}))
|
||||
}
|
||||
|
||||
win.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'app', 'app.ejs'),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
}))
|
||||
// console.log(__dirname)
|
||||
// win.loadURL(format({
|
||||
// pathname: join(__dirname, 'index.html'),
|
||||
// protocol: 'file',
|
||||
// slashes: true
|
||||
// }))
|
||||
|
||||
/*win.once('ready-to-show', () => {
|
||||
win.show()
|
||||
@ -120,6 +143,19 @@ function createWindow() {
|
||||
win.removeMenu()
|
||||
|
||||
win.resizable = true
|
||||
// win.webContents.on('new-window', (e, url) => {
|
||||
// if(url != win!.webContents.getURL()) {
|
||||
// e.preventDefault()
|
||||
// shell.openExternal(url)
|
||||
// }
|
||||
// })
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Open DevTools, see https://github.com/electron/electron/issues/12438 for why we wait for dom-ready
|
||||
win.webContents.once('dom-ready', () => {
|
||||
win!.webContents.openDevTools()
|
||||
})
|
||||
}
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null
|
||||
@ -131,57 +167,66 @@ function createMenu() {
|
||||
if(process.platform === 'darwin') {
|
||||
|
||||
// Extend default included application menu to continue support for quit keyboard shortcut
|
||||
let applicationSubMenu = {
|
||||
const applicationSubMenu = new MenuItem({
|
||||
label: 'Application',
|
||||
submenu: [{
|
||||
label: 'About Application',
|
||||
selector: 'orderFrontStandardAboutPanel:'
|
||||
role: 'about'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
role: 'quit',
|
||||
click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
// New edit menu adds support for text-editing keyboard shortcuts
|
||||
let editSubMenu = {
|
||||
const editSubMenu = new MenuItem({
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
label: 'Undo',
|
||||
accelerator: 'CmdOrCtrl+Z',
|
||||
selector: 'undo:'
|
||||
}, {
|
||||
label: 'Redo',
|
||||
accelerator: 'Shift+CmdOrCtrl+Z',
|
||||
selector: 'redo:'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Cut',
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
selector: 'cut:'
|
||||
}, {
|
||||
label: 'Copy',
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
selector: 'copy:'
|
||||
}, {
|
||||
label: 'Paste',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
selector: 'paste:'
|
||||
}, {
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
selector: 'selectAll:'
|
||||
}]
|
||||
}
|
||||
submenu: [
|
||||
{
|
||||
label: 'Undo',
|
||||
accelerator: 'CmdOrCtrl+Z',
|
||||
role: 'undo'
|
||||
},
|
||||
{
|
||||
label: 'Redo',
|
||||
accelerator: 'Shift+CmdOrCtrl+Z',
|
||||
role: 'redo'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Cut',
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
role: 'cut'
|
||||
},
|
||||
{
|
||||
label: 'Copy',
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
role: 'copy'
|
||||
},
|
||||
{
|
||||
label: 'Paste',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
role: 'paste'
|
||||
},
|
||||
{
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
role: 'selectAll'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Bundle submenus into a single template and build a menu object with it
|
||||
let menuTemplate = [applicationSubMenu, editSubMenu]
|
||||
let menuObject = Menu.buildFromTemplate(menuTemplate)
|
||||
const menuTemplate: MenuItem[] = [applicationSubMenu, editSubMenu]
|
||||
const menuObject = Menu.buildFromTemplate(menuTemplate)
|
||||
|
||||
// Assign it to the application
|
||||
Menu.setApplicationMenu(menuObject)
|
||||
@ -190,17 +235,20 @@ function createMenu() {
|
||||
|
||||
}
|
||||
|
||||
function getPlatformIcon(filename){
|
||||
const opSys = process.platform
|
||||
if (opSys === 'darwin') {
|
||||
filename = filename + '.icns'
|
||||
} else if (opSys === 'win32') {
|
||||
filename = filename + '.ico'
|
||||
} else {
|
||||
filename = filename + '.png'
|
||||
function getPlatformIcon(filename: string){
|
||||
let ext
|
||||
switch(process.platform) {
|
||||
case 'win32':
|
||||
ext = 'ico'
|
||||
break
|
||||
case 'darwin':
|
||||
case 'linux':
|
||||
default:
|
||||
ext = 'png'
|
||||
break
|
||||
}
|
||||
|
||||
return path.join(__dirname, 'app', 'assets', 'images', filename)
|
||||
return join(__static, 'images', `${filename}.${ext}`)
|
||||
}
|
||||
|
||||
app.on('ready', createWindow)
|
41
src/renderer/components/Application.css
Normal file
@ -0,0 +1,41 @@
|
||||
.appWrapper {
|
||||
height: calc(100vh - 22px);
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
.appWrapper[overlay] {
|
||||
filter: blur(3px) contrast(0.9) brightness(1.0);
|
||||
}
|
||||
|
||||
.loader-enter {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.loader-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
.loader-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
.loader-exit-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
|
||||
.appWrapper-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
.appWrapper-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 500ms, transform 500ms;
|
||||
}
|
||||
.appWrapper-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
.appWrapper-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms, transform 500ms;
|
||||
}
|
386
src/renderer/components/Application.tsx
Normal file
@ -0,0 +1,386 @@
|
||||
import { hot } from 'react-hot-loader/root'
|
||||
import * as React from 'react'
|
||||
import Frame from './frame/Frame'
|
||||
import Welcome from './welcome/Welcome'
|
||||
import { connect } from 'react-redux'
|
||||
import { View } from '../meta/Views'
|
||||
import Landing from './landing/Landing'
|
||||
import Login from './login/Login'
|
||||
import Loader from './loader/Loader'
|
||||
import Settings from './settings/Settings'
|
||||
import Overlay from './overlay/Overlay'
|
||||
import Fatal from './fatal/Fatal'
|
||||
import { StoreType } from '../redux/store'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
import { ViewActionDispatch } from '../redux/actions/viewActions'
|
||||
import { throttle } from 'lodash'
|
||||
import { readdir } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { AppActionDispatch } from '../redux/actions/appActions'
|
||||
import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions'
|
||||
|
||||
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||
import { DistributionAPI } from 'common/distribution/DistributionAPI'
|
||||
import { getServerStatus, ServerStatus } from 'common/mojang/net/ServerStatusAPI'
|
||||
import { Distribution } from 'helios-distribution-types'
|
||||
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
|
||||
import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
|
||||
import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
|
||||
import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
|
||||
import { RestResponseStatus } from 'common/got/RestResponse'
|
||||
|
||||
import './Application.css'
|
||||
|
||||
declare const __static: string
|
||||
|
||||
function setBackground(id: number) {
|
||||
import(`../../../static/images/backgrounds/${id}.jpg`).then(mdl => {
|
||||
document.body.style.backgroundImage = `url('${mdl.default}')`
|
||||
})
|
||||
}
|
||||
|
||||
interface ApplicationProps {
|
||||
currentView: View
|
||||
overlayQueue: OverlayPushAction<unknown>[]
|
||||
distribution: HeliosDistribution
|
||||
selectedServer?: HeliosServer
|
||||
selectedServerStatus?: ServerStatus
|
||||
mojangStatuses: MojangStatus[]
|
||||
}
|
||||
|
||||
interface ApplicationState {
|
||||
loading: boolean
|
||||
showMain: boolean
|
||||
renderMain: boolean
|
||||
workingView: View
|
||||
}
|
||||
|
||||
const mapState = (state: StoreType): Partial<ApplicationProps> => {
|
||||
return {
|
||||
currentView: state.currentView,
|
||||
overlayQueue: state.overlayQueue,
|
||||
distribution: state.app.distribution,
|
||||
selectedServer: state.app.selectedServer,
|
||||
mojangStatuses: state.app.mojangStatuses
|
||||
}
|
||||
}
|
||||
const mapDispatch = {
|
||||
...AppActionDispatch,
|
||||
...ViewActionDispatch,
|
||||
...OverlayActionDispatch
|
||||
}
|
||||
|
||||
type InternalApplicationProps = ApplicationProps & typeof mapDispatch
|
||||
|
||||
class Application extends React.Component<InternalApplicationProps, ApplicationState> {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('Application')
|
||||
|
||||
private mojangStatusInterval!: NodeJS.Timeout
|
||||
private serverStatusInterval!: NodeJS.Timeout
|
||||
|
||||
private bkid!: number
|
||||
|
||||
constructor(props: InternalApplicationProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
loading: true,
|
||||
showMain: false,
|
||||
renderMain: false,
|
||||
workingView: props.currentView
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount(): Promise<void> {
|
||||
|
||||
this.mojangStatusInterval = setInterval(async () => {
|
||||
Application.logger.info('Refreshing Mojang Statuses..')
|
||||
await this.loadMojangStatuses()
|
||||
}, 300000)
|
||||
|
||||
this.serverStatusInterval = setInterval(async () => {
|
||||
Application.logger.info('Refreshing selected server status..')
|
||||
await this.syncServerStatus()
|
||||
}, 300000)
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
|
||||
// Clean up intervals.
|
||||
clearInterval(this.mojangStatusInterval)
|
||||
clearInterval(this.serverStatusInterval)
|
||||
|
||||
}
|
||||
|
||||
async componentDidUpdate(prevProps: InternalApplicationProps): Promise<void> {
|
||||
|
||||
if(this.props.selectedServer?.rawServer.id !== prevProps.selectedServer?.rawServer.id) {
|
||||
await this.syncServerStatus()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the mojang statuses and add them to the global store.
|
||||
*/
|
||||
private loadMojangStatuses = async (): Promise<void> => {
|
||||
const response: MojangResponse<MojangStatus[]> = await MojangRestAPI.status()
|
||||
|
||||
if(response.responseStatus !== RestResponseStatus.SUCCESS) {
|
||||
Application.logger.warn('Failed to retrieve Mojang Statuses.')
|
||||
}
|
||||
|
||||
// TODO Temp workaround because their status checker always shows
|
||||
// this as red. https://bugs.mojang.com/browse/WEB-2303
|
||||
const statuses = response.data
|
||||
for(const status of statuses) {
|
||||
if(status.service === 'sessionserver.mojang.com' || status.service === 'minecraft.net') {
|
||||
status.status = MojangStatusColor.GREEN
|
||||
}
|
||||
}
|
||||
|
||||
this.props.setMojangStatuses(response.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the status of the selected server and store it in the global store.
|
||||
*/
|
||||
private syncServerStatus = async (): Promise<void> => {
|
||||
let serverStatus: ServerStatus | undefined
|
||||
|
||||
if(this.props.selectedServer != null) {
|
||||
const { hostname, port } = this.props.selectedServer
|
||||
try {
|
||||
serverStatus = await getServerStatus(
|
||||
47,
|
||||
hostname,
|
||||
port
|
||||
)
|
||||
} catch(err) {
|
||||
Application.logger.error('Error while refreshing server status', err)
|
||||
}
|
||||
|
||||
} else {
|
||||
serverStatus = undefined
|
||||
}
|
||||
|
||||
this.props.setSelectedServerStatus(serverStatus)
|
||||
}
|
||||
|
||||
private getViewElement = (): JSX.Element => {
|
||||
// TODO debug remove
|
||||
console.log('loading', this.props.currentView, this.state.workingView)
|
||||
switch(this.state.workingView) {
|
||||
case View.WELCOME:
|
||||
return <>
|
||||
<Welcome />
|
||||
</>
|
||||
case View.LANDING:
|
||||
return <>
|
||||
<Landing
|
||||
distribution={this.props.distribution}
|
||||
selectedServer={this.props.selectedServer}
|
||||
selectedServerStatus={this.props.selectedServerStatus}
|
||||
mojangStatuses={this.props.mojangStatuses}
|
||||
/>
|
||||
</>
|
||||
case View.LOGIN:
|
||||
return <>
|
||||
<Login cancelable={false} />
|
||||
</>
|
||||
case View.SETTINGS:
|
||||
return <>
|
||||
<Settings />
|
||||
</>
|
||||
case View.FATAL:
|
||||
return <>
|
||||
<Fatal />
|
||||
</>
|
||||
case View.NONE:
|
||||
return <></>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private hasOverlay = (): boolean => {
|
||||
return this.props.overlayQueue.length > 0
|
||||
}
|
||||
|
||||
private updateWorkingView = throttle(() => {
|
||||
// TODO debug remove
|
||||
console.log('Setting to', this.props.currentView)
|
||||
this.setState({
|
||||
...this.state,
|
||||
workingView: this.props.currentView
|
||||
})
|
||||
|
||||
}, 200)
|
||||
|
||||
private finishLoad = (): void => {
|
||||
if(this.props.currentView !== View.FATAL) {
|
||||
setBackground(this.bkid)
|
||||
}
|
||||
this.showMain()
|
||||
}
|
||||
|
||||
private showMain = (): void => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
showMain: true
|
||||
})
|
||||
}
|
||||
|
||||
private initLoad = async (): Promise<void> => {
|
||||
if(this.state.loading) {
|
||||
const MIN_LOAD = 800
|
||||
const start = Date.now()
|
||||
|
||||
// Initial distribution load.
|
||||
const distroAPI = new DistributionAPI('C:\\Users\\user\\AppData\\Roaming\\Helios Launcher')
|
||||
let rawDisto: Distribution
|
||||
try {
|
||||
rawDisto = await distroAPI.testLoad()
|
||||
console.log('distro', distroAPI)
|
||||
} catch(err) {
|
||||
console.log('EXCEPTION IN DISTRO LOAD TODO TODO TODO', err)
|
||||
rawDisto = null!
|
||||
}
|
||||
|
||||
// Fatal error
|
||||
if(rawDisto == null) {
|
||||
this.props.setView(View.FATAL)
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: false,
|
||||
workingView: View.FATAL
|
||||
})
|
||||
return
|
||||
} else {
|
||||
|
||||
// For debugging display.
|
||||
// for(let i=0; i<10; i++) {
|
||||
// rawDisto.servers.push(rawDisto.servers[1])
|
||||
// }
|
||||
|
||||
|
||||
const distro = new HeliosDistribution(rawDisto)
|
||||
// TODO TEMP USE CONFIG
|
||||
// TODO TODO TODO TODO
|
||||
const selectedServer: HeliosServer = distro.servers[0]
|
||||
const { hostname, port } = selectedServer
|
||||
let selectedServerStatus
|
||||
try {
|
||||
selectedServerStatus = await getServerStatus(47, hostname, port)
|
||||
} catch(err) {
|
||||
Application.logger.error('Failed to refresh server status', selectedServerStatus)
|
||||
}
|
||||
this.props.setDistribution(distro)
|
||||
this.props.setSelectedServer(selectedServer)
|
||||
this.props.setSelectedServerStatus(selectedServerStatus)
|
||||
}
|
||||
|
||||
// Load initial mojang statuses.
|
||||
Application.logger.info('Loading mojang statuses..')
|
||||
await this.loadMojangStatuses()
|
||||
|
||||
// TODO Setup hook for distro refresh every ~ 5 mins.
|
||||
|
||||
// Pick a background id.
|
||||
this.bkid = Math.floor((Math.random() * (await readdir(join(__static, 'images', 'backgrounds'))).length))
|
||||
this.bkid = 3 // TEMP
|
||||
|
||||
const endLoad = () => {
|
||||
// TODO determine correct view
|
||||
// either welcome, landing, or login
|
||||
this.props.setView(View.LANDING)
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: false,
|
||||
workingView: View.LANDING
|
||||
})
|
||||
// TODO temp
|
||||
setTimeout(() => {
|
||||
// this.props.setView(View.WELCOME)
|
||||
// this.props.pushGenericOverlay({
|
||||
// title: 'Load Distribution',
|
||||
// description: 'This is a test.',
|
||||
// dismissible: false,
|
||||
// acknowledgeCallback: async () => {
|
||||
// const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565)
|
||||
// console.log(serverStatus)
|
||||
// }
|
||||
// })
|
||||
// this.props.pushGenericOverlay({
|
||||
// title: 'Test Title 2',
|
||||
// description: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.',
|
||||
// dismissible: true
|
||||
// })
|
||||
// this.props.pushGenericOverlay({
|
||||
// title: 'Test Title IMPORTANT',
|
||||
// description: 'Test Description',
|
||||
// dismissible: true
|
||||
// }, true)
|
||||
}, 5000)
|
||||
}
|
||||
const diff = Date.now() - start
|
||||
if(diff < MIN_LOAD) {
|
||||
setTimeout(endLoad, MIN_LOAD-diff)
|
||||
} else {
|
||||
endLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Frame />
|
||||
<CSSTransition
|
||||
in={this.state.showMain}
|
||||
appear={true}
|
||||
timeout={500}
|
||||
classNames="appWrapper"
|
||||
unmountOnExit
|
||||
>
|
||||
<div className="appWrapper" {...(this.hasOverlay() ? {overlay: 'true'} : {})}>
|
||||
<CSSTransition
|
||||
in={this.props.currentView == this.state.workingView}
|
||||
appear={true}
|
||||
timeout={500}
|
||||
classNames="appWrapper"
|
||||
unmountOnExit
|
||||
onExited={this.updateWorkingView}
|
||||
>
|
||||
{this.getViewElement()}
|
||||
</CSSTransition>
|
||||
|
||||
</div>
|
||||
</CSSTransition>
|
||||
<CSSTransition
|
||||
in={this.hasOverlay()}
|
||||
appear={true}
|
||||
timeout={500}
|
||||
classNames="appWrapper"
|
||||
unmountOnExit
|
||||
>
|
||||
<Overlay overlayQueue={this.props.overlayQueue} />
|
||||
</CSSTransition>
|
||||
<CSSTransition
|
||||
in={this.state.loading}
|
||||
appear={true}
|
||||
timeout={300}
|
||||
classNames="loader"
|
||||
unmountOnExit
|
||||
onEnter={this.initLoad}
|
||||
onExited={this.finishLoad}
|
||||
>
|
||||
<Loader />
|
||||
</CSSTransition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default hot(connect<unknown, typeof mapDispatch>(mapState, mapDispatch)(Application))
|
124
src/renderer/components/fatal/Fatal.css
Normal file
@ -0,0 +1,124 @@
|
||||
#fatalContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#fatalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
padding-top: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#fatalHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0 1rem 0;
|
||||
width: 70%;
|
||||
margin-bottom: .5rem;
|
||||
border-bottom: 0.0625rem solid rgba(126, 126, 126, 0.57);
|
||||
}
|
||||
|
||||
#fatalLeft {
|
||||
display: flex;
|
||||
}
|
||||
#fatalErrorImg {
|
||||
width: 3.125rem;
|
||||
}
|
||||
|
||||
#fatalRight {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 0.625rem;
|
||||
}
|
||||
|
||||
#fatalErrorLabel {
|
||||
font-size: .75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
#fatalErrorText {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
#fatalBody {
|
||||
width: 65%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#fatalDescription {
|
||||
text-align: justify;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#fatalChecklistContainer {
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Div which contains action buttons. */
|
||||
#fatalActionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
/* Fatal acknowledge button styles. */
|
||||
#fatalAcknowledge {
|
||||
background: none;
|
||||
border: 0.0625rem solid #ffffff;
|
||||
color: white;
|
||||
font-family: 'Avenir Medium';
|
||||
font-weight: bold;
|
||||
border-radius: .125rem;
|
||||
padding: 0 .6rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
#fatalAcknowledge:hover,
|
||||
#fatalAcknowledge:focus {
|
||||
box-shadow: 0 0 .625rem 0 #fff;
|
||||
outline: none;
|
||||
}
|
||||
#fatalAcknowledge:active {
|
||||
border-color: rgba(255, 255, 255, 0.75);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
#fatalDismissWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Fatal dismiss option styles. */
|
||||
#fatalDismiss {
|
||||
font-weight: bold;
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none;
|
||||
padding-top: 0.375rem;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
color: rgba(202, 202, 202, 0.75);
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
#fatalDismiss:hover,
|
||||
#fatalDismiss:focus {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
#fatalDismiss:active {
|
||||
color: rgba(165, 165, 165, 0.75);
|
||||
}
|
70
src/renderer/components/fatal/Fatal.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import * as React from 'react'
|
||||
import { remote, shell } from 'electron'
|
||||
|
||||
import './Fatal.css'
|
||||
|
||||
function closeHandler() {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.close()
|
||||
}
|
||||
|
||||
function openLatest() {
|
||||
// TODO don't hardcode
|
||||
shell.openExternal('https://github.com/dscalzi/HeliosLauncher/releases')
|
||||
}
|
||||
|
||||
export default class Fatal extends React.Component {
|
||||
|
||||
render(): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="fatalContainer">
|
||||
<div id="fatalContent">
|
||||
|
||||
<div id="fatalHeader">
|
||||
<div id="fatalLeft">
|
||||
<img id="fatalErrorImg" src="../images/SealCircleError.png"/>
|
||||
</div>
|
||||
<div id="fatalRight">
|
||||
<span id="fatalErrorLabel">FATAL ERROR</span>
|
||||
<span id="fatalErrorText">Failed to load Distribution Index</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fatalBody">
|
||||
<h4>What Happened?</h4>
|
||||
<p id="fatalDescription">
|
||||
A connection could not be established to our servers to download the distribution index. No local copies were available to load.
|
||||
The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it.
|
||||
</p>
|
||||
|
||||
{/* TODO When auto update is done, do a version check and auto/update here. */}
|
||||
|
||||
<div id="fatalChecklistContainer">
|
||||
<ul>
|
||||
<li>Ensure you are running the latest version of Helios Launcher.</li>
|
||||
<li>Ensure you are connected to the internet.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4>Relaunch the application to try again.</h4>
|
||||
|
||||
<div id="fatalActionContainer">
|
||||
<button onClick={openLatest} id="fatalAcknowledge">Latest Releaes</button>
|
||||
<div id="fatalDismissWrapper">
|
||||
<button onClick={closeHandler} id="fatalDismiss">Close Launcher</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
154
src/renderer/components/frame/Frame.css
Normal file
@ -0,0 +1,154 @@
|
||||
/* Frame Bar */
|
||||
#frameBar {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: background-color 1s ease;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* Undraggable region on the top of the frame. */
|
||||
#frameResizableTop {
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Flexbox to wrap the main frame content. */
|
||||
#frameMain {
|
||||
display: flex;
|
||||
height: 20px
|
||||
}
|
||||
|
||||
/* Undraggable region on the left and right of the frame. */
|
||||
.frameResizableVert {
|
||||
width: 2px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Main frame content for windows. */
|
||||
#frameContentWin {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
/* Main frame content for darwin. */
|
||||
#frameContentDarwin {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
/* Frame logo (windows only). */
|
||||
#frameTitleDock {
|
||||
padding: 0px 10px;
|
||||
display: flex;
|
||||
}
|
||||
#frameTitleText {
|
||||
font-size: 14px;
|
||||
font-family: 'Avenir Medium';
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Windows frame button dock. */
|
||||
#frameButtonDockWin {
|
||||
display: flex;
|
||||
-webkit-app-region: no-drag !important;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
height: 22px;
|
||||
}
|
||||
/* #frameButtonDockWin > .frameButton:not(:first-child) {
|
||||
margin-left: -4px;
|
||||
} */
|
||||
|
||||
/* Darwin frame button dock: NaN; */
|
||||
#frameButtonDockDarwin {
|
||||
-webkit-app-region: no-drag !important;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
/* Windows Frame Button Styles. */
|
||||
.frameButton {
|
||||
background: none;
|
||||
border: none;
|
||||
height: 22px;
|
||||
width: 39px;
|
||||
cursor: pointer;
|
||||
line-height: 22px;
|
||||
}
|
||||
.frameButton:hover,
|
||||
.frameButton:focus {
|
||||
background: rgba(189, 189, 189, 0.43);
|
||||
}
|
||||
.frameButton:active {
|
||||
background: rgba(156, 156, 156, 0.43);
|
||||
}
|
||||
.frameButton:focus {
|
||||
outline: 0px;
|
||||
}
|
||||
|
||||
/* Close button is red. */
|
||||
#frameButton_close:hover,
|
||||
#frameButton_close:focus {
|
||||
background: rgba(255, 53, 53, 0.61) !important;
|
||||
}
|
||||
#frameButton_close:active {
|
||||
background: rgba(235, 0, 0, 0.61) !important;
|
||||
}
|
||||
|
||||
/* Darwin Frame Button Styles. */
|
||||
.frameButtonDarwin {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
border: 0px;
|
||||
margin-left: 5px;
|
||||
-webkit-app-region: no-drag !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.frameButtonDarwin:focus {
|
||||
outline: 0px;
|
||||
}
|
||||
|
||||
#frameButtonDarwin_close {
|
||||
background-color: #e74c32;
|
||||
}
|
||||
#frameButtonDarwin_close:hover,
|
||||
#frameButtonDarwin_close:focus {
|
||||
background-color: #FF9A8A;
|
||||
}
|
||||
#frameButtonDarwin_close:active {
|
||||
background-color: #ff8d7b;
|
||||
}
|
||||
|
||||
#frameButtonDarwin_minimize {
|
||||
background-color: #fed045;
|
||||
}
|
||||
#frameButtonDarwin_minimize:hover,
|
||||
#frameButtonDarwin_minimize:focus {
|
||||
background-color: #FFE9A9;
|
||||
}
|
||||
#frameButtonDarwin_minimize:active {
|
||||
background-color: #ffde7b;
|
||||
}
|
||||
|
||||
#frameButtonDarwin_restoredown {
|
||||
background-color: #96e734;
|
||||
}
|
||||
#frameButtonDarwin_restoredown:hover,
|
||||
#frameButtonDarwin_restoredown:focus {
|
||||
background-color: #D6FFA6;
|
||||
}
|
||||
#frameButtonDarwin_restoredown:active {
|
||||
background-color: #bfff76;
|
||||
}
|
62
src/renderer/components/frame/Frame.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import * as React from 'react'
|
||||
import { remote } from 'electron'
|
||||
import './Frame.css'
|
||||
|
||||
function closeHandler() {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.close()
|
||||
}
|
||||
|
||||
function restoreDownHandler() {
|
||||
const window = remote.getCurrentWindow()
|
||||
if(window.isMaximized()){
|
||||
window.unmaximize()
|
||||
} else {
|
||||
window.maximize()
|
||||
}
|
||||
(document.activeElement as HTMLElement).blur()
|
||||
}
|
||||
|
||||
function minimizeHandler() {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.minimize();
|
||||
(document.activeElement as HTMLElement).blur()
|
||||
}
|
||||
|
||||
const Frame = (): JSX.Element => (
|
||||
<div id="frameBar">
|
||||
<div id="frameResizableTop" className="frameDragPadder"></div>
|
||||
<div id="frameMain">
|
||||
<div className="frameResizableVert frameDragPadder"></div>
|
||||
{ process.platform === 'darwin' ?
|
||||
<div id="frameContentDarwin">
|
||||
<div id="frameButtonDockDarwin">
|
||||
<button className="frameButtonDarwin" onClick={closeHandler} id="frameButtonDarwin_close" tabIndex={-1}></button>
|
||||
<button className="frameButtonDarwin" onClick={minimizeHandler} id="frameButtonDarwin_minimize" tabIndex={-1}></button>
|
||||
<button className="frameButtonDarwin" onClick={restoreDownHandler} id="frameButtonDarwin_restoredown" tabIndex={-1}></button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div id="frameContentWin">
|
||||
<div id="frameTitleDock">
|
||||
<span id="frameTitleText">Helios Launcher</span>
|
||||
</div>
|
||||
<div id="frameButtonDockWin">
|
||||
<button className="frameButton" onClick={minimizeHandler} id="frameButton_minimize" tabIndex={-1}>
|
||||
<svg name="TitleBarMinimize" width="10" height="10" viewBox="0 0 12 12"><rect stroke="#ffffff" fill="#ffffff" width="10" height="1" x="1" y="6"></rect></svg>
|
||||
</button>
|
||||
<button className="frameButton" onClick={restoreDownHandler} id="frameButton_restoredown" tabIndex={-1}>
|
||||
<svg name="TitleBarMaximize" width="10" height="10" viewBox="0 0 12 12"><rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="#ffffff" strokeWidth="1.4px"></rect></svg>
|
||||
</button>
|
||||
<button className="frameButton" onClick={closeHandler} id="frameButton_close" tabIndex={-1}>
|
||||
<svg name="TitleBarClose" width="10" height="10" viewBox="0 0 12 12"><polygon stroke="#ffffff" fill="#ffffff" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"></polygon></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="frameResizableVert frameDragPadder"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Frame
|
624
src/renderer/components/landing/Landing.css
Normal file
@ -0,0 +1,624 @@
|
||||
.serverStatusWrapper-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
.serverStatusWrapper-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 500ms, transform 500ms;
|
||||
}
|
||||
.serverStatusWrapper-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
.serverStatusWrapper-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms, transform 500ms;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Landing View (Structural Styles) *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Main content container. */
|
||||
#landingContainer {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: background 2s ease;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* Upper content container. */
|
||||
#landingContainer > #upper {
|
||||
position: relative;
|
||||
transition: top 2s ease;
|
||||
top: 0;
|
||||
height: 77%;
|
||||
display: flex;
|
||||
}
|
||||
#landingContainer > #upper > #left {
|
||||
display: inline-flex;
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
#landingContainer > #upper > #content {
|
||||
display: inline-flex;
|
||||
width: 70%;
|
||||
height: 100%;
|
||||
}
|
||||
#landingContainer > #upper > #right {
|
||||
display: inline-flex;
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Lower content container. */
|
||||
#landingContainer > #lower {
|
||||
height: 23%;
|
||||
display: flex;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
|
||||
}
|
||||
#landingContainer > #lower > #left {
|
||||
position: relative;
|
||||
transition: top 2s ease;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 33%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#landingContainer > #lower > #left #content {
|
||||
position: relative;
|
||||
top: 25px;
|
||||
display: inline-flex;
|
||||
line-height: 24px;
|
||||
left: 50px;
|
||||
}
|
||||
#landingContainer > #lower > #center {
|
||||
position: relative;
|
||||
transition: top 2s ease;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 34%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#landingContainer > #lower > #center #content {
|
||||
position: relative;
|
||||
z-index: 500;
|
||||
transition: top 2s ease;
|
||||
top: 10px;
|
||||
}
|
||||
#landingContainer > #lower > #right {
|
||||
position: relative;
|
||||
transition: top 2s ease;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 33%;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Landing View (Top Styles) *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* * *
|
||||
* Landing View (Top Styles) | Left Content
|
||||
* * */
|
||||
|
||||
/* Logo image. */
|
||||
#image_seal {
|
||||
height: 70px;
|
||||
width: auto;
|
||||
position: relative;
|
||||
border: 2px solid white;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Logo container styles. */
|
||||
#image_seal_container {
|
||||
position: relative;
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
border-radius: 50%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
/* Logo container styles w/ update. */
|
||||
#image_seal_container[update]{
|
||||
cursor: pointer
|
||||
}
|
||||
#image_seal_container[update]:before,
|
||||
#image_seal_container[update]:after {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 15px #43c628;
|
||||
animation: glow-grow 4s ease-out infinite;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
#image_seal_container[update]:before {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
/* Update available tooltip styles. */
|
||||
#updateAvailableTooltip {
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
width: 100px;
|
||||
height: 15px;
|
||||
background-color: rgb(0, 0, 0);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 115%;
|
||||
left: -17.5px;
|
||||
font-family: 'Avenir Medium';
|
||||
font-size: 12px;
|
||||
transition: visibility 0s linear 0.25s, opacity 0.25s ease;
|
||||
}
|
||||
#updateAvailableTooltip::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 100%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent rgb(0, 0, 0) transparent;
|
||||
}
|
||||
#image_seal_container[update]:hover #updateAvailableTooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
/* Update available animation. */
|
||||
@keyframes glow-grow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* * *
|
||||
* Landing View (Bottom Styles) | Right Content
|
||||
* * */
|
||||
|
||||
/* Wrapper container for top, right content. */
|
||||
#rightContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
top: 50px;
|
||||
align-items: flex-start;
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
/* Right hand user content container. */
|
||||
#user_content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* User profile avatar container. */
|
||||
#avatarContainer {
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cad7e1;
|
||||
box-sizing: border-box;
|
||||
background: rgba(1, 2, 1, 0.5);
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
box-shadow: 0 0 10px 0 rgb(0, 0, 0);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
/* Avatar edit overlay. */
|
||||
#avatarOverlay {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: 0.25s ease;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
-webkit-user-select: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
#avatarOverlay:hover,
|
||||
#avatarOverlay:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
#avatarOverlay:active {
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
/* User profile name text. */
|
||||
#user_text {
|
||||
font-size: 12px;
|
||||
min-width: 135px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 0 20px black;
|
||||
position: absolute;
|
||||
right: 95px;
|
||||
text-align: right;
|
||||
-webkit-user-select: initial;
|
||||
}
|
||||
|
||||
/* Social media icon content container. */
|
||||
#mediaContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1.5625rem;
|
||||
width: 70px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Social Media Icon division containers. */
|
||||
#internalMedia, #externalMedia {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Divider bar between the external and internal icons. */
|
||||
.mediaDivider {
|
||||
height: 0.0625rem;
|
||||
width: 0.875rem;
|
||||
background: rgb(255, 255, 255);
|
||||
margin: 0.625rem 0;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Landing View (Bottom Styles) *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Style for a general label on the bottom of the landing view. */
|
||||
.bot_label {
|
||||
font-size: 9px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 0 #bebcbb;
|
||||
}
|
||||
|
||||
/* Divider used on the bottom of the landing view. */
|
||||
.bot_divider {
|
||||
height: 25px;
|
||||
width: 2px;
|
||||
background: rgba(107, 105, 105, 0.7);
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* * *
|
||||
* Landing View (Bottom Styles) | Left Content
|
||||
* * */
|
||||
|
||||
/* Maintains maximum width on the status bar. */
|
||||
#server_status_wrapper {
|
||||
display: inline-flex;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
/* Span which displays the player count of the selected server. */
|
||||
#player_count {
|
||||
color: #949494;
|
||||
font-size: 8px;
|
||||
font-weight: 900;
|
||||
text-shadow: 0 0 20px #949494;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Wrapper container for the mojang status bar. */
|
||||
#mojangStatusWrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Icon which displays the status of the mojang services. */
|
||||
#mojang_status_icon {
|
||||
font-size: 30px;
|
||||
color: #848484;
|
||||
margin-left: 15px;
|
||||
font-family: 'sans-serif';
|
||||
}
|
||||
|
||||
/* Tooltip which displays more details about the mojang statuses. */
|
||||
#mojangStatusTooltip {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
width: 145px;
|
||||
min-height: 150px;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
z-index: 1;
|
||||
font-family: 'Avenir Medium';
|
||||
font-size: 12px;
|
||||
transition: visibility 0s linear 0.25s, opacity 0.25s ease;
|
||||
bottom: calc(100% + 15px);
|
||||
transform: translateX(-50%);
|
||||
margin-left: 50%;
|
||||
box-shadow: 0 0 20px rgb(0, 0, 0);
|
||||
cursor: default;
|
||||
}
|
||||
#mojangStatusTooltip:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: rgba(0, 0, 0, 0.75) transparent transparent transparent;
|
||||
}
|
||||
#mojangStatusWrapper:hover #mojangStatusTooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
/* Tooltip title for the mojang statuses. */
|
||||
#mojangStatusTooltipTitle {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Wrapper container for the non essential services title. */
|
||||
#mojangStatusNEContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* White bar which surrounds the non essential service title. */
|
||||
.mojangStatusNEBar {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Non essential service title text. */
|
||||
#mojangStatusNETitle {
|
||||
font-size: 10px;
|
||||
padding: 0 3px;
|
||||
text-align: center;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Wrapper container for mojang service information. */
|
||||
.mojangStatusContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Displays the name of the mojang service. */
|
||||
.mojangStatusName {
|
||||
width: 100%;
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 12px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
/* Displays the status of the mojang service. */
|
||||
.mojangStatusIcon {
|
||||
margin-right: 10px;
|
||||
font-size: 18.5px;
|
||||
color: #848484;
|
||||
}
|
||||
|
||||
/* * *
|
||||
* Landing View (Bottom Styles) | Center Content
|
||||
* * */
|
||||
|
||||
/* Button which opens the news view. */
|
||||
#newsButton {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
#newsButton:hover #newsButtonText,
|
||||
#newsButton:focus #newsButtonText {
|
||||
text-shadow: 0 0 20px #fff, 0 0 20px #fff;
|
||||
}
|
||||
#newsButton:active {
|
||||
color: #c7c7c7;
|
||||
text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
|
||||
}
|
||||
|
||||
#newsButton:hover #newsButtonSVG,
|
||||
#newsButton:focus #newsButtonSVG {
|
||||
filter: drop-shadow(0px 0 2px #fff);
|
||||
-webkit-filter: drop-shadow(0px 0 2px #fff);
|
||||
}
|
||||
#newsButton:active #newsButtonSVG .arrowLine {
|
||||
stroke: #c7c7c7;
|
||||
}
|
||||
#newsButton:active #newsButtonSVG {
|
||||
filter: drop-shadow(0px 0 2px #c7c7c7);
|
||||
-webkit-filter: drop-shadow(0px 0 2px #c7c7c7);
|
||||
}
|
||||
#newsButton:disabled #newsButtonSVG .arrowLine {
|
||||
stroke: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
/* Icon which indicates there is new news. */
|
||||
#newsButtonAlert {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: red;
|
||||
right: -1px;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
/* Arrow image which floats above the news button. */
|
||||
#newsButtonSVG {
|
||||
height: 11px;
|
||||
margin-left: -2px;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
|
||||
/* Span which contains the news button text. */
|
||||
#newsButtonText {
|
||||
color: white;
|
||||
font-weight: 900;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 0 #bebcbb;
|
||||
font-size: 11px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
|
||||
/* * *
|
||||
* Landing View (Bottom Styles) | Right Content
|
||||
* * */
|
||||
|
||||
/* Main launch content container. */
|
||||
#landingContainer > #lower > #right #launch_content {
|
||||
position: relative;
|
||||
top: 25px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* The launch button. */
|
||||
#launch_button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 0 #bebcbb;
|
||||
font-size: 20px;
|
||||
padding: 0;
|
||||
transition: 0.25s ease;
|
||||
outline: none;
|
||||
}
|
||||
#launch_button:hover,
|
||||
#launch_button:focus {
|
||||
text-shadow: 0 0 20px #fff, 0 0 20px #fff;
|
||||
}
|
||||
#launch_button:active {
|
||||
color: #c7c7c7;
|
||||
text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
|
||||
}
|
||||
#launch_button:disabled {
|
||||
color: #c7c7c7;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Launch details main container, hidden until launch processing begins. */
|
||||
#launch_details {
|
||||
position: relative;
|
||||
top: 25px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Left side of launch details container, displays percentage and a divider. */
|
||||
#launch_details_left {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Span which displays percentage complete. */
|
||||
#launch_progress_label {
|
||||
font-weight: 900;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 0 0 #bebcbb;
|
||||
font-size: 20px;
|
||||
min-width: 53.21px;
|
||||
max-width: 53.21px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Right side of launch details container, displays progress bar and details. */
|
||||
#launch_details_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Button which opens the server selection view. */
|
||||
#server_selection_button {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
padding: 0;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
#server_selection_button:hover,
|
||||
#server_selection_button:focus {
|
||||
text-shadow: 0 0 20px #fff, 0 0 20px #fff, 0 0 20px #fff;
|
||||
}
|
||||
#server_selection_button:active {
|
||||
color: #c7c7c7;
|
||||
text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
|
||||
}
|
||||
|
||||
/* Progress bar styles. */
|
||||
#launch_progress[value] {
|
||||
height: 3px;
|
||||
width: 265px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
#launch_progress[value]::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
#launch_progress[value]::-webkit-progress-value {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Span which displays information about the status of the launch process. */
|
||||
#launch_details_text {
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
312
src/renderer/components/landing/Landing.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
|
||||
import { StoreType } from '../../redux/store'
|
||||
import { AppActionDispatch } from '../..//redux/actions/appActions'
|
||||
import { OverlayActionDispatch } from '../../redux/actions/overlayActions'
|
||||
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
|
||||
import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
|
||||
import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
|
||||
import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
|
||||
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||
|
||||
import { MediaButton, MediaButtonType } from './mediabutton/MediaButton'
|
||||
import News from '../news/News'
|
||||
|
||||
import './Landing.css'
|
||||
|
||||
interface LandingProps {
|
||||
distribution: HeliosDistribution
|
||||
selectedServer?: HeliosServer
|
||||
selectedServerStatus?: ServerStatus
|
||||
mojangStatuses: MojangStatus[]
|
||||
}
|
||||
|
||||
interface LandingState {
|
||||
workingServerStatus?: ServerStatus
|
||||
}
|
||||
|
||||
const mapState = (state: StoreType): Partial<LandingProps> => {
|
||||
return {
|
||||
distribution: state.app.distribution!,
|
||||
selectedServer: state.app.selectedServer,
|
||||
selectedServerStatus: state.app.selectedServerStatus,
|
||||
mojangStatuses: state.app.mojangStatuses
|
||||
}
|
||||
}
|
||||
const mapDispatch = {
|
||||
...AppActionDispatch,
|
||||
...OverlayActionDispatch
|
||||
}
|
||||
|
||||
type InternalLandingProps = LandingProps & typeof mapDispatch
|
||||
|
||||
class Landing extends React.Component<InternalLandingProps, LandingState> {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('Landing')
|
||||
|
||||
constructor(props: InternalLandingProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
workingServerStatus: props.selectedServerStatus
|
||||
}
|
||||
}
|
||||
|
||||
/* Mojang Status Methods */
|
||||
|
||||
private getMainMojangStatusColor = (): string => {
|
||||
const essential = this.props.mojangStatuses.filter(s => s.essential)
|
||||
|
||||
if(this.props.mojangStatuses.length === 0) {
|
||||
return MojangRestAPI.statusToHex(MojangStatusColor.GREY)
|
||||
}
|
||||
|
||||
// If any essential are red, it's red.
|
||||
if(essential.filter(s => s.status === MojangStatusColor.RED).length > 0) {
|
||||
return MojangRestAPI.statusToHex(MojangStatusColor.RED)
|
||||
}
|
||||
// If any essential are yellow, it's yellow.
|
||||
if(essential.filter(s => s.status === MojangStatusColor.YELLOW).length > 0) {
|
||||
return MojangRestAPI.statusToHex(MojangStatusColor.YELLOW)
|
||||
}
|
||||
// If any non-essential are not green, return yellow.
|
||||
if(this.props.mojangStatuses.filter(s => s.status !== MojangStatusColor.GREEN && s.status !== MojangStatusColor.GREY).length > 0) {
|
||||
return MojangRestAPI.statusToHex(MojangStatusColor.YELLOW)
|
||||
}
|
||||
// if all are grey, return grey.
|
||||
if(this.props.mojangStatuses.filter(s => s.status === MojangStatusColor.GREY).length === this.props.mojangStatuses.length) {
|
||||
return MojangRestAPI.statusToHex(MojangStatusColor.GREY)
|
||||
}
|
||||
|
||||
return MojangRestAPI.statusToHex(MojangStatusColor.GREEN)
|
||||
}
|
||||
|
||||
private getMojangStatusesAsJSX = (essential: boolean): JSX.Element[] => {
|
||||
|
||||
const statuses: JSX.Element[] = []
|
||||
for(const status of this.props.mojangStatuses.filter(s => s.essential === essential)) {
|
||||
statuses.push(
|
||||
<div className="mojangStatusContainer" key={status.service}>
|
||||
<span className="mojangStatusIcon" style={{color: MojangRestAPI.statusToHex(status.status)}}>•</span>
|
||||
<span className="mojangStatusName">{status.name}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
/* Selected Server Methods */
|
||||
|
||||
private updateWorkingServerStatus = (): void => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
workingServerStatus: this.props.selectedServerStatus
|
||||
})
|
||||
}
|
||||
|
||||
private openServerSelect = (): void => {
|
||||
this.props.pushServerSelectOverlay({
|
||||
servers: this.props.distribution.servers,
|
||||
selectedId: this.props.selectedServer?.rawServer.id,
|
||||
onSelection: async (serverId: string) => {
|
||||
Landing.logger.info('Server Selection Change:', serverId)
|
||||
const next: HeliosServer = this.props.distribution.getServerById(serverId)!
|
||||
this.props.setSelectedServer(next)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getSelectedServerText = (): string => {
|
||||
if(this.props.selectedServer != null) {
|
||||
return `• ${this.props.selectedServer.rawServer.id}`
|
||||
} else {
|
||||
return '• No Server Selected'
|
||||
}
|
||||
}
|
||||
|
||||
private getSelectedServerStatusText = (): string => {
|
||||
return this.state.workingServerStatus != null ? 'PLAYERS' : 'SERVER'
|
||||
}
|
||||
|
||||
private getSelectedServerCount = (): string => {
|
||||
if(this.state.workingServerStatus != null) {
|
||||
const { online, max } = this.state.workingServerStatus.players
|
||||
return `${online}/${max}`
|
||||
} else {
|
||||
return 'OFFLINE'
|
||||
}
|
||||
}
|
||||
|
||||
private readonly mediaButtons = [
|
||||
{
|
||||
href: 'https://github.com/dscalzi/HeliosLauncher',
|
||||
type: MediaButtonType.LINK,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
href: '#',
|
||||
type: MediaButtonType.TWITTER,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
href: '#',
|
||||
type: MediaButtonType.INSTAGRAM,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
href: '#',
|
||||
type: MediaButtonType.YOUTUBE,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
href: 'https://discord.gg/zNWUXdt',
|
||||
type: MediaButtonType.DISCORD,
|
||||
disabled: false,
|
||||
tooltip: 'Discord'
|
||||
}
|
||||
]
|
||||
|
||||
private getExternalMediaButtons = (): JSX.Element[] => {
|
||||
const ret: JSX.Element[] = []
|
||||
for(const { href, type, disabled, tooltip } of this.mediaButtons) {
|
||||
ret.push(
|
||||
<MediaButton
|
||||
key={`${type.toLowerCase()}LandingButton`}
|
||||
href={href}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
private onSettingsClick = async (): Promise<void> => {
|
||||
console.log('Settings clicked')
|
||||
}
|
||||
|
||||
/* Render */
|
||||
|
||||
render(): JSX.Element {
|
||||
return <>
|
||||
|
||||
<div id="landingContainer">
|
||||
<div id="upper">
|
||||
<div id="left">
|
||||
<div id="image_seal_container">
|
||||
<img id="image_seal" src="../images/SealCircle.png"/>
|
||||
<div id="updateAvailableTooltip">Update Available</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
</div>
|
||||
<div id="right">
|
||||
<div id="rightContainer">
|
||||
<div id="user_content">
|
||||
<span id="user_text">Username</span>
|
||||
<div id="avatarContainer">
|
||||
<button id="avatarOverlay">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mediaContent">
|
||||
<div id="internalMedia">
|
||||
<MediaButton
|
||||
type={MediaButtonType.SETTINGS}
|
||||
action={this.onSettingsClick}
|
||||
tooltip="Settings"
|
||||
/>
|
||||
</div>
|
||||
<div className="mediaDivider"></div>
|
||||
<div id="externalMedia">
|
||||
{this.getExternalMediaButtons()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lower">
|
||||
<div id="left">
|
||||
<div className="bot_wrapper">
|
||||
<div id="content">
|
||||
|
||||
<CSSTransition
|
||||
in={this.props.selectedServerStatus?.retrievedAt === this.state.workingServerStatus?.retrievedAt}
|
||||
timeout={500}
|
||||
classNames="serverStatusWrapper"
|
||||
unmountOnExit
|
||||
onExited={this.updateWorkingServerStatus}
|
||||
>
|
||||
<div id="server_status_wrapper">
|
||||
<span className="bot_label" id="landingPlayerLabel">{this.getSelectedServerStatusText()}</span>
|
||||
<span id="player_count">{this.getSelectedServerCount()}</span>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
|
||||
<div className="bot_divider"></div>
|
||||
<div id="mojangStatusWrapper">
|
||||
<span className="bot_label">MOJANG STATUS</span>
|
||||
<span id="mojang_status_icon" style={{color: this.getMainMojangStatusColor()}}>•</span>
|
||||
<div id="mojangStatusTooltip">
|
||||
<div id="mojangStatusTooltipTitle">Services</div>
|
||||
<div id="mojangStatusEssentialContainer">
|
||||
{this.getMojangStatusesAsJSX(true)}
|
||||
</div>
|
||||
<div id="mojangStatusNEContainer">
|
||||
<div className="mojangStatusNEBar"></div>
|
||||
<div id="mojangStatusNETitle">Non Essential</div>
|
||||
<div className="mojangStatusNEBar"></div>
|
||||
</div>
|
||||
<div id="mojangStatusNonEssentialContainer">
|
||||
{this.getMojangStatusesAsJSX(false)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="center">
|
||||
<div className="bot_wrapper">
|
||||
<div id="content">
|
||||
<button id="newsButton">
|
||||
{/* <img src="assets/images/icons/arrow.svg" id="newsButtonSVG"/> */}
|
||||
<div id="newsButtonAlert" style={{display: 'none'}}></div>
|
||||
<svg id="newsButtonSVG" viewBox="0 0 24.87 13.97">
|
||||
<polyline fill="none" stroke="#FFF" strokeWidth="2px" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
||||
</svg>
|
||||
<span id="newsButtonText">NEWS</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="right">
|
||||
<div className="bot_wrapper">
|
||||
<div id="launch_content">
|
||||
<button id="launch_button">PLAY</button>
|
||||
<div className="bot_divider"></div>
|
||||
<button onClick={this.openServerSelect} id="server_selection_button" className="bot_label">{this.getSelectedServerText()}</button>
|
||||
</div>
|
||||
<div id="launch_details">
|
||||
<div id="launch_details_left">
|
||||
<span id="launch_progress_label">0%</span>
|
||||
<div className="bot_divider"></div>
|
||||
</div>
|
||||
<div id="launch_details_right">
|
||||
<progress id="launch_progress" value="22" max="100"></progress>
|
||||
<span id="launch_details_text" className="bot_label">Please wait..</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<News />
|
||||
</div>
|
||||
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect<unknown, typeof mapDispatch>(mapState, mapDispatch)(Landing)
|
130
src/renderer/components/landing/mediabutton/MediaButton.css
Normal file
@ -0,0 +1,130 @@
|
||||
/* Container object which wraps an icon to ensure fluid transitions. */
|
||||
.mediaContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 1.6875rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Social media icon shared styles. */
|
||||
.mediaSVG {
|
||||
fill: #ffffff;
|
||||
transition: 0.25s ease;
|
||||
cursor: pointer;
|
||||
height: 0.75rem;
|
||||
width: 1.5625rem;
|
||||
}
|
||||
.mediaSVG:hover,
|
||||
.mediaURL:focus .mediaSVG,
|
||||
.mediaSVG:active {
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Social media URL shared styles. */
|
||||
.mediaURL {
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Internal media button shared styles. */
|
||||
.mediaButton {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Twitter icon colors. */
|
||||
.twitterSVG:hover,
|
||||
.twitterURL:focus .twitterSVG {
|
||||
fill: #1da1f2;
|
||||
}
|
||||
.twitterSVG:active {
|
||||
fill: #1b8dd4;
|
||||
}
|
||||
|
||||
/* Instagram icon colors. */
|
||||
.instagramSVG:hover,
|
||||
.instagramURL:focus .instagramSVG {
|
||||
fill: url('#instaFill')
|
||||
/*fill: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); */
|
||||
}
|
||||
.instagramSVG:active {
|
||||
fill: url('#instaFill')
|
||||
}
|
||||
|
||||
/* Youtube icon colors. */
|
||||
.youtubeSVG:hover,
|
||||
.youtubeURL:focus .youtubeSVG {
|
||||
fill: #f00;
|
||||
}
|
||||
.youtubeSVG:active {
|
||||
fill: #ea0202;
|
||||
}
|
||||
|
||||
/* Discord icon colors. */
|
||||
.discordSVG:hover,
|
||||
.discordURL:focus .discordSVG {
|
||||
fill: #7288d9;
|
||||
}
|
||||
.discordSVG:active {
|
||||
fill: #657ac4;
|
||||
}
|
||||
|
||||
/* Settings icon colors. */
|
||||
.settingsSVG {
|
||||
stroke: #ffffff;
|
||||
height: 0.9375rem;
|
||||
}
|
||||
.mediaButton:hover .settingsSVG,
|
||||
.mediaButton:focus .settingsSVG,
|
||||
.mediaButton:active .settingsSVG {
|
||||
height: 1.4375rem;
|
||||
}
|
||||
|
||||
.mediaTooltip {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
width: 4.6875rem;
|
||||
height: 1.25rem;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 130%;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
transition: visibility 0s linear 0.25s, opacity 0.25s ease;
|
||||
color: #fff;
|
||||
}
|
||||
.mediaTooltip::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
margin-top: -0.3125rem;
|
||||
border-width: 0.3125rem;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent transparent rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.mediaURL:hover .mediaTooltip,
|
||||
.mediaURL:focus .mediaTooltip,
|
||||
.mediaURL:active .mediaTooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition-delay:0s;
|
||||
}
|
||||
|
||||
.mediaButton:hover .mediaTooltip,
|
||||
.mediaButton:focus .mediaTooltip,
|
||||
.mediaButton:active .mediaTooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition-delay:0s;
|
||||
}
|
169
src/renderer/components/landing/mediabutton/MediaButton.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||
import * as React from 'react'
|
||||
|
||||
import './MediaButton.css'
|
||||
|
||||
// Pass href to render with an anchor.
|
||||
// Pass action to render with a button.
|
||||
|
||||
export interface MediaButtonProps {
|
||||
type: MediaButtonType
|
||||
href?: string
|
||||
action?: () => Promise<void>
|
||||
disabled?: boolean
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export enum MediaButtonType {
|
||||
LINK = 'LINK',
|
||||
TWITTER = 'TWITTER',
|
||||
INSTAGRAM = 'INSTAGRAM',
|
||||
YOUTUBE = 'YOUTUBE',
|
||||
DISCORD = 'DISCORD',
|
||||
SETTINGS = 'SETTINGS'
|
||||
}
|
||||
|
||||
export class MediaButton extends React.Component<MediaButtonProps> {
|
||||
|
||||
private readonly logger = LoggerUtil.getLogger('MediaButton')
|
||||
|
||||
private getSVGClassName = (type: MediaButtonType): string => {
|
||||
return `${type.toLowerCase()}SVG`
|
||||
}
|
||||
|
||||
private getAnchorClassName = (type: MediaButtonType): string => {
|
||||
return `${type.toLowerCase()}URL`
|
||||
}
|
||||
|
||||
private getButtonClassName = (type: MediaButtonType): string => {
|
||||
return `${type.toLowerCase()}MediaButton`
|
||||
}
|
||||
|
||||
private readonly mediaContentMap: {[key in MediaButtonType]: JSX.Element} = {
|
||||
[MediaButtonType.LINK]: (
|
||||
<svg className={`mediaSVG ${this.getSVGClassName(MediaButtonType.LINK)}`} viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
|
||||
<path d="M66,73.88a3.85,3.85,0,0,0,1.73-.42,8.22,8.22,0,0,1-.94-3.76,8.36,8.36,0,0,1,8.35-8.35H95A8.35,8.35,0,1,1,95,78H85.8a21.51,21.51,0,0,1-6.65,8.72H95a17.07,17.07,0,0,0,0-34.15H75.13A17,17,0,0,0,58.59,73.88Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[MediaButtonType.TWITTER]: (
|
||||
<svg className={`mediaSVG ${this.getSVGClassName(MediaButtonType.TWITTER)}`} viewBox="0 0 5000 4060" preserveAspectRatio="xMidYMid meet">
|
||||
<g>
|
||||
<path d="M1210 4048 c-350 -30 -780 -175 -1124 -378 -56 -33 -86 -57 -86 -68 0 -16 7 -17 83 -9 114 12 349 1 493 -22 295 -49 620 -180 843 -341 l54 -38 -49 -7 c-367 -49 -660 -256 -821 -582 -30 -61 -53 -120 -51 -130 3 -16 12 -17 73 -13 97 7 199 5 270 -4 l60 -9 -65 -22 c-341 -117 -609 -419 -681 -769 -18 -88 -26 -226 -13 -239 4 -3 32 7 63 22 68 35 198 77 266 86 28 4 58 9 68 12 10 2 -22 -34 -72 -82 -240 -232 -353 -532 -321 -852 15 -149 79 -347 133 -418 16 -20 17 -19 49 20 377 455 913 795 1491 945 160 41 346 74 485 86 l82 7 -7 -59 c-5 -33 -7 -117 -6 -189 2 -163 31 -286 103 -430 141 -285 422 -504 708 -550 112 -19 333 -19 442 0 180 30 335 108 477 239 l58 54 95 -24 c143 -36 286 -89 427 -160 70 -35 131 -60 135 -56 19 19 -74 209 -151 312 -50 66 -161 178 -216 217 l-30 22 73 -14 c111 -21 257 -63 353 -101 99 -39 99 -39 99 -19 0 57 -237 326 -412 468 l-88 71 6 51 c4 28 1 130 -5 226 -30 440 -131 806 -333 1202 -380 745 -1036 1277 -1823 1477 -243 62 -430 81 -786 78 -134 0 -291 -5 -349 -10z"/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[MediaButtonType.INSTAGRAM]: (
|
||||
<svg className={`mediaSVG ${this.getSVGClassName(MediaButtonType.INSTAGRAM)}`} viewBox="0 0 5040 5040">
|
||||
<defs>
|
||||
<radialGradient id="instaFill" cx="30%" cy="107%" r="150%">
|
||||
<stop offset="0%" stopColor="#fdf497"/>
|
||||
<stop offset="5%" stopColor="#fdf497"/>
|
||||
<stop offset="45%" stopColor="#fd5949"/>
|
||||
<stop offset="60%" stopColor="#d6249f"/>
|
||||
<stop offset="90%" stopColor="#285AEB"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M1390 5024 c-163 -9 -239 -19 -315 -38 -281 -70 -477 -177 -660 -361 -184 -184 -292 -380 -361 -660 -43 -171 -53 -456 -53 -1445 0 -989 10 -1274 53 -1445 69 -280 177 -476 361 -660 184 -184 380 -292 660 -361 171 -43 456 -53 1445 -53 989 0 1274 10 1445 53 280 69 476 177 660 361 184 184 292 380 361 660 43 171 53 456 53 1445 0 989 -10 1274 -53 1445 -69 280 -177 476 -361 660 -184 184 -380 292 -660 361 -174 44 -454 53 -1470 52 -599 0 -960 -5 -1105 -14z m2230 -473 c58 -6 141 -18 185 -27 397 -78 638 -318 719 -714 37 -183 41 -309 41 -1290 0 -981 -4 -1107 -41 -1290 -81 -395 -319 -633 -714 -714 -183 -37 -309 -41 -1290 -41 -981 0 -1107 4 -1290 41 -397 81 -636 322 -714 719 -33 166 -38 296 -43 1100 -5 796 3 1203 27 1380 67 489 338 758 830 825 47 7 162 15 255 20 250 12 1907 4 2035 -9z"/>
|
||||
<path d="M2355 3819 c-307 -42 -561 -172 -780 -400 -244 -253 -359 -543 -359 -899 0 -361 116 -648 367 -907 262 -269 563 -397 937 -397 374 0 675 128 937 397 251 259 367 546 367 907 0 361 -116 648 -367 907 -197 203 -422 326 -690 378 -101 20 -317 27 -412 14z m400 -509 c275 -88 470 -284 557 -560 20 -65 23 -95 23 -230 0 -135 -3 -165 -23 -230 -88 -278 -284 -474 -562 -562 -65 -20 -95 -23 -230 -23 -135 0 -165 3 -230 23 -278 88 -474 284 -562 562 -20 65 -23 95 -23 230 0 135 3 165 23 230 73 230 219 403 427 507 134 67 212 83 390 79 111 -3 155 -8 210 -26z"/>
|
||||
<path d="M3750 1473 c-29 -11 -66 -38 -106 -77 -70 -71 -94 -126 -94 -221 0 -95 24 -150 94 -221 72 -71 126 -94 225 -94 168 0 311 143 311 311 0 99 -23 154 -94 225 -43 42 -76 66 -110 77 -61 21 -166 21 -226 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[MediaButtonType.YOUTUBE]: (
|
||||
<svg className={`mediaSVG ${this.getSVGClassName(MediaButtonType.YOUTUBE)}`} viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M84.8,69.52,65.88,79.76V59.27Zm23.65.59c0-5.14-.79-17.63-3.94-20.57S99,45.86,73.37,45.86s-28,.73-31.14,3.68S38.29,65,38.29,70.11s.79,17.63,3.94,20.57,5.52,3.68,31.14,3.68,28-.74,31.14-3.68,3.94-15.42,3.94-20.57"/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[MediaButtonType.DISCORD]: (
|
||||
<svg className={`mediaSVG ${this.getSVGClassName(MediaButtonType.DISCORD)}`} viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M81.23,78.48a6.14,6.14,0,1,1,6.14-6.14,6.14,6.14,0,0,1-6.14,6.14M60,78.48a6.14,6.14,0,1,1,6.14-6.14A6.14,6.14,0,0,1,60,78.48M104.41,73c-.92-7.7-8.24-22.9-8.24-22.9A43,43,0,0,0,88,45.59a17.88,17.88,0,0,0-8.38-1.27l-.13,1.06a23.52,23.52,0,0,1,5.8,1.95,87.59,87.59,0,0,1,8.17,4.87s-10.32-5.63-22.27-5.63a51.32,51.32,0,0,0-23.2,5.63,87.84,87.84,0,0,1,8.17-4.87,23.57,23.57,0,0,1,5.8-1.95l-.13-1.06a17.88,17.88,0,0,0-8.38,1.27,42.84,42.84,0,0,0-8.21,4.56S37.87,65.35,37,73s-.37,11.54-.37,11.54,4.22,5.68,9.9,7.14,7.7,1.47,7.7,1.47l3.75-4.68a21.22,21.22,0,0,1-4.65-2A24.47,24.47,0,0,1,47.93,82S61.16,88.4,70.68,88.4c10,0,22.75-6.44,22.75-6.44a24.56,24.56,0,0,1-5.35,4.56,21.22,21.22,0,0,1-4.65,2l3.75,4.68s2,0,7.7-1.47,9.89-7.14,9.89-7.14.55-3.85-.37-11.54"/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[MediaButtonType.SETTINGS]: (
|
||||
<svg className={`mediaSVG ${this.getSVGClassName(MediaButtonType.SETTINGS)}`} viewBox="0 0 141.36 137.43">
|
||||
<path d="M70.70475616319865,83.36934004916053 a15.320781354859122,15.320781354859122 0 1 1 14.454501310561755,-15.296030496450625 A14.850515045097694,14.850515045097694 0 0 1 70.70475616319865,83.36934004916053 M123.25082856443602,55.425620905968366 h-12.375429204248078 A45.54157947163293,45.54157947163293 0 0 0 107.21227231573047,46.243052436416285 l8.613298726156664,-9.108315894326587 a9.727087354538993,9.727087354538993 0 0 0 0,-13.167456673319956 l-3.465120177189462,-3.6631270444574313 a8.489544434114185,8.489544434114185 0 0 0 -12.375429204248078,0 l-8.613298726156664,9.108315894326587 A40.442902639482725,40.442902639482725 0 0 0 81.99114759747292,25.427580514871032 V12.532383284044531 a9.108315894326587,9.108315894326587 0 0 0 -8.811305593424633,-9.306322761594556 h-4.950171681699231 a9.108315894326587,9.108315894326587 0 0 0 -8.811305593424633,9.306322761594556 v12.895197230826497 a40.17064319698927,40.17064319698927 0 0 0 -9.331073620003052,4.0591407789933704 l-8.613298726156664,-9.108315894326587 a8.489544434114185,8.489544434114185 0 0 0 -12.375429204248078,0 L25.58394128451018,23.967279868769744 a9.727087354538993,9.727087354538993 0 0 0 0,13.167456673319956 L34.19724001066683,46.243052436416285 a45.07131316187151,45.07131316187151 0 0 0 -3.6631270444574313,9.083565035918088 h-12.375429204248078 a9.083565035918088,9.083565035918088 0 0 0 -8.811305593424633,9.306322761594556 v5.197680265784193 a9.108315894326587,9.108315894326587 0 0 0 8.811305593424633,9.306322761594556 h11.979415469712139 a45.69008462208391,45.69008462208391 0 0 0 4.0591407789933704,10.642869115653347 l-8.613298726156664,9.108315894326587 a9.727087354538993,9.727087354538993 0 0 0 0,13.167456673319956 l3.465120177189462,3.6631270444574313 a8.489544434114185,8.489544434114185 0 0 0 12.375429204248078,0 l8.613298726156664,-9.108315894326587 a40.49240435629971,40.49240435629971 0 0 0 9.331073620003052,4.0591407789933704 v12.895197230826497 a9.083565035918088,9.083565035918088 0 0 0 8.811305593424633,9.306322761594556 h4.950171681699231 A9.083565035918088,9.083565035918088 0 0 0 81.99114759747292,123.68848839660077 V110.79329116577425 a40.78941465720167,40.78941465720167 0 0 0 9.331073620003052,-4.0591407789933704 l8.613298726156664,9.108315894326587 a8.489544434114185,8.489544434114185 0 0 0 12.375429204248078,0 l3.465120177189462,-3.6631270444574313 a9.727087354538993,9.727087354538993 0 0 0 0,-13.167456673319956 l-8.613298726156664,-9.108315894326587 a45.665333763675406,45.665333763675406 0 0 0 4.034389920584874,-10.642869115653347 h12.004166328120636 a9.108315894326587,9.108315894326587 0 0 0 8.811305593424633,-9.306322761594556 v-5.197680265784193 a9.083565035918088,9.083565035918088 0 0 0 -8.811305593424633,-9.306322761594556 " id="svg_3" className=""/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
private blurOnClick = (): void => {
|
||||
if(document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
}
|
||||
|
||||
private handleButtonClick = (action: () => Promise<void>): () => Promise<void> => {
|
||||
return async () => {
|
||||
this.blurOnClick()
|
||||
try {
|
||||
await action()
|
||||
} catch(err) {
|
||||
this.logger.error('Uncaught error in media button action.', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderTooltip = (): JSX.Element => {
|
||||
if(this.props.tooltip) {
|
||||
return <div className="mediaTooltip">{this.props.tooltip}</div>
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
||||
private renderContent = (): JSX.Element => {
|
||||
|
||||
const internalContent: JSX.Element = (
|
||||
<>
|
||||
{this.mediaContentMap[this.props.type]}
|
||||
{this.renderTooltip()}
|
||||
</>
|
||||
)
|
||||
|
||||
if(this.props.href) {
|
||||
// Render anchor
|
||||
return (
|
||||
<a
|
||||
onClick={this.blurOnClick}
|
||||
href={this.props.href}
|
||||
className={`mediaURL ${this.getAnchorClassName(this.props.type)}`}
|
||||
{...(this.props.disabled ? {disabled: true} : {})}
|
||||
>
|
||||
{internalContent}
|
||||
</a>
|
||||
)
|
||||
} else if(this.props.action) {
|
||||
// Render button
|
||||
return (
|
||||
<button
|
||||
className={`mediaButton ${this.getButtonClassName(this.props.type)}`}
|
||||
onClick={this.handleButtonClick(this.props.action)}
|
||||
{...(this.props.disabled ? {disabled: true} : {})}
|
||||
>
|
||||
{internalContent}
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
return <>
|
||||
No Content
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return <>
|
||||
<div className="mediaContainer">
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
63
src/renderer/components/loader/Loader.css
Normal file
@ -0,0 +1,63 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Loading Element (app.ejs) *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Loading container, placed above everything. */
|
||||
#loadingContainer {
|
||||
position: absolute;
|
||||
z-index: 400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: calc(100% - 22px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Loading content container. */
|
||||
#loadingContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Spinner container. */
|
||||
#loadSpinnerContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Stationary image for the spinner. */
|
||||
#loadCenterImage {
|
||||
position: absolute;
|
||||
width: 17.3125rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Rotating image for the spinner. */
|
||||
#loadSpinnerImage {
|
||||
width: 17.5rem;
|
||||
height: auto;
|
||||
z-index: 400;
|
||||
}
|
||||
|
||||
/* Rotating animation for the spinner. */
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Class which is applied when the spinner image is spinning. */
|
||||
.rotating {
|
||||
animation: rotating 10s linear infinite;
|
||||
}
|
23
src/renderer/components/loader/Loader.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import './Loader.css'
|
||||
|
||||
import LoadingSeal from '../../../../static/images/LoadingSeal.png'
|
||||
import LoadingText from '../../../../static/images/LoadingText.png'
|
||||
|
||||
export default class Loader extends React.Component {
|
||||
|
||||
render(): JSX.Element {
|
||||
return <>
|
||||
<div id="loadingContainer">
|
||||
<div id="loadingContent">
|
||||
<div id="loadSpinnerContainer">
|
||||
<img id="loadCenterImage" src={LoadingSeal} />
|
||||
<img id="loadSpinnerImage" className="rotating" src={LoadingText} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
425
src/renderer/components/login/Login.css
Normal file
@ -0,0 +1,425 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Login View (login.ejs) *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Styles for dimmer login span. */
|
||||
.loginSpanDim {
|
||||
font-size: 0.75rem;
|
||||
color: #848484;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Main login container. */
|
||||
#loginContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.50);
|
||||
}
|
||||
|
||||
/* Login cancel button styles. */
|
||||
#loginCancelContainer {
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
right: 5%;
|
||||
}
|
||||
|
||||
/* Login cancel button styles. */
|
||||
#loginCancelButton {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
#loginCancelButton:hover #loginCancelIcon,
|
||||
#loginCancelButton:hover #loginCancelText,
|
||||
#loginCancelButton:focus #loginCancelIcon,
|
||||
#loginCancelButton:focus #loginCancelText {
|
||||
text-shadow: 0 0 1.25rem white;
|
||||
}
|
||||
#loginCancelButton:hover #loginCancelIcon,
|
||||
#loginCancelButton:focus #loginCancelIcon {
|
||||
box-shadow: 0 0 1.25rem white;
|
||||
}
|
||||
#loginCancelButton:active #loginCancelIcon,
|
||||
#loginCancelButton:active #loginCancelText {
|
||||
text-shadow: 0 0 1.25rem rgba(255, 255, 255, 0.75);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
border-color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
#loginCancelButton:active #loginCancelIcon {
|
||||
box-shadow: 0 0 1.25rem rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
#loginCancelButton:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
#loginCancelButton:disabled #loginCancelIcon,
|
||||
#loginCancelButton:disabled #loginCancelText {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
border-color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
/* The X in a circle icon for the cancel button. */
|
||||
#loginCancelIcon {
|
||||
border-radius: 50%;
|
||||
border: 0.0625rem solid white;
|
||||
box-sizing: border-box;
|
||||
height: 1.875rem;
|
||||
width: 1.875rem;
|
||||
font-size: 1.1875rem;
|
||||
line-height: 1.875rem;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 0.3125rem;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
/* Text for the login cancel button. */
|
||||
#loginCancelText {
|
||||
font-size: 0.9375rem;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
|
||||
/* Login content wrapper. */
|
||||
#loginContent {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 1.5625rem;
|
||||
}
|
||||
|
||||
/* Login form. */
|
||||
#loginForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Login form anchor styles. */
|
||||
#loginForm a {
|
||||
font-size: 0.75rem;
|
||||
color: #848484;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
#loginForm a:hover,
|
||||
#loginForm a:focus {
|
||||
color: #a2a2a2;
|
||||
outline: none;
|
||||
}
|
||||
#loginForm a:active {
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
/* Logo on login form. */
|
||||
#loginImageSeal {
|
||||
border-radius: 50%;
|
||||
border: 0.125rem solid #cad7e1;
|
||||
background: rgba(1, 2, 1, 0.5);
|
||||
height: 7.8125rem;
|
||||
width: 7.8125rem;
|
||||
box-shadow: 0 0 0.625rem 0 rgb(0, 0, 0);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
/* Header on login view. */
|
||||
#loginSubheader {
|
||||
font-family: 'Avenir Medium';
|
||||
margin-bottom: 1.5625rem;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.0625rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Add spacing between password field and options bar. */
|
||||
#labelPassword {
|
||||
margin-bottom: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Container which contains the forgot and remember options. */
|
||||
#loginOptions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Remember option text. */
|
||||
#loginRememberText {
|
||||
padding-right: 0.625rem;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
|
||||
/* Login button styles. */
|
||||
#loginButton {
|
||||
background: none;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.125rem;
|
||||
border: none;
|
||||
padding: 0.9375rem 0.3125rem;
|
||||
margin: 0.625rem 0;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
right: -1.25rem;
|
||||
transition: 0.5s ease;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
#loginButton:disabled {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
pointer-events: none;
|
||||
}
|
||||
#loginButton[loading] {
|
||||
color: #fff;
|
||||
}
|
||||
#loginButton:hover,
|
||||
#loginButton:focus {
|
||||
text-shadow: 0 0 1.25rem #fff;
|
||||
outline: none;
|
||||
}
|
||||
#loginButton:active {
|
||||
color: #c7c7c7;
|
||||
text-shadow: 0 0 1.25rem #c7c7c7;
|
||||
}
|
||||
#loginSVG {
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
overflow: visible;
|
||||
transform: rotate(90deg);
|
||||
margin-left: 1.25rem;
|
||||
transition: 0.25s ease;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
#loginButton:hover #loginSVG,
|
||||
#loginButton:focus #loginSVG {
|
||||
filter: drop-shadow(0 0 0.125rem #fff);
|
||||
-webkit-filter: drop-shadow(0 0 0.125rem #fff);
|
||||
}
|
||||
#loginButton:active #loginSVG .arrowLine {
|
||||
stroke: #c7c7c7;
|
||||
}
|
||||
#loginButton:active #loginSVG {
|
||||
filter: drop-shadow(0 0 0.125rem #c7c7c7);
|
||||
-webkit-filter: drop-shadow(0 0 0.125rem #c7c7c7);
|
||||
}
|
||||
#loginButton:disabled #loginSVG .arrowLine {
|
||||
stroke: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
#loginButtonContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#loginButton .circle-loader,
|
||||
#loginButton[loading] #loginSVG {
|
||||
display: none;
|
||||
}
|
||||
#loginButton[loading] .circle-loader,
|
||||
#loginButton #loginSVG {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
|
||||
.circle-loader {
|
||||
margin-left: 1.25rem;
|
||||
border: 0.125rem solid rgba(255, 255, 255, 0.5);
|
||||
border-left-color: #ffffff;
|
||||
animation-name: loader-spin;
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
border-radius: 50%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
.load-complete {
|
||||
animation: none;
|
||||
border-color: #ffffff;
|
||||
transition: border 500ms ease-out;
|
||||
}
|
||||
.checkmark {
|
||||
display: none;
|
||||
}
|
||||
.checkmark.draw:after {
|
||||
animation-duration: 800ms;
|
||||
animation-timing-function: ease;
|
||||
animation-name: checkmark;
|
||||
transform: scaleX(-1) rotate(135deg);
|
||||
}
|
||||
.checkmark:after {
|
||||
opacity: 1;
|
||||
height: 0.5rem;
|
||||
width: 0.25rem;
|
||||
transform-origin: left top;
|
||||
border-right: 0.125rem solid #ffffff;
|
||||
border-top: 0.125rem solid #ffffff;
|
||||
content: '';
|
||||
left: 0.125rem;
|
||||
top: 0.5rem;
|
||||
position: absolute;
|
||||
}
|
||||
@keyframes loader-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes checkmark {
|
||||
0% {
|
||||
height: 0;
|
||||
width: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
20% {
|
||||
height: 0;
|
||||
width: 0.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
40% {
|
||||
height: 0.5rem;
|
||||
width: 0.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
height: 0.5rem;
|
||||
width: 0.25rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*.spinningCircle {
|
||||
margin-left: 1.25rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
border: 0.125rem solid rgba(255,255,255,0);
|
||||
border-top-color: #ffffff;
|
||||
border-right-color: #ffffff;
|
||||
border-left-color: rgba(255, 255, 255, 0.50);
|
||||
border-bottom-color: rgba(255, 255, 255, 0.50);
|
||||
animation: single2 4s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes single2 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(720deg);
|
||||
}
|
||||
}*/
|
||||
|
||||
/* Disclaimer container. */
|
||||
#loginDisclaimer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Add spacing between register anchor and disclaimer. */
|
||||
#loginRegisterSpan {
|
||||
margin-bottom: 0.3125rem;
|
||||
}
|
||||
|
||||
/* Disclaimer text styles. */
|
||||
.loginDisclaimerText {
|
||||
font-size: 0.4375rem;
|
||||
color: #848484;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* * *
|
||||
* Login View | Custom Checkbox
|
||||
* * */
|
||||
|
||||
/* Checkbox container. */
|
||||
#checkmarkContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: 1.375rem;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* Hide the default checkbox. */
|
||||
#checkmarkContainer input {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Create a custom checkbox. */
|
||||
.loginCheckmark {
|
||||
position: relative;
|
||||
height: 0.625rem;
|
||||
width: 0.625rem;
|
||||
border: 0.0625rem solid #848484;
|
||||
border-radius: 0.0625rem;
|
||||
background: none;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
/* On hover and focus, add a grey border color. */
|
||||
#checkmarkContainer:hover input ~ *,
|
||||
#checkmarkContainer input:focus ~ * {
|
||||
color: #a2a2a2;
|
||||
border-color: #a2a2a2;
|
||||
}
|
||||
/* On keydown, darken the checkbox a bit. */
|
||||
#checkmarkContainer input:active ~ *:not(#loginRememberText) {
|
||||
color: #8d8d8d;
|
||||
border-color: #8d8d8d;
|
||||
}
|
||||
#checkmarkContainer[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* For checked -> #checkmarkContainer input:checked ~ * */
|
||||
/* Create the checkmark/indicator (hidden when not checked). */
|
||||
.loginCheckmark:after {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
/* Show the checkmark when checked. */
|
||||
#checkmarkContainer input:checked ~ .loginCheckmark:after {
|
||||
display: block;
|
||||
}
|
||||
/* Style the checkmark/indicator. */
|
||||
#checkmarkContainer .loginCheckmark:after {
|
||||
position: absolute;
|
||||
left: 0.21875rem;
|
||||
top: 0.03125rem;
|
||||
width: 0.125rem;
|
||||
height: 0.375rem;
|
||||
border: solid #a2a2a2;
|
||||
border-width: 0 0.125rem 0.125rem 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/*
|
||||
#login_filter {
|
||||
height: calc(100% - 22px);
|
||||
width: 100%;
|
||||
z-index: 9000;
|
||||
position: absolute;
|
||||
filter: blur(0.5rem) contrast(0.9) brightness(1.0);
|
||||
background: url('./../images/backgrounds/0.jpg') no-repeat center center fixed;
|
||||
transform: scale(1.2);
|
||||
background-size: cover;
|
||||
}
|
||||
*/
|
187
src/renderer/components/login/Login.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import * as React from 'react'
|
||||
import LoginField from './login-field/LoginField'
|
||||
|
||||
import './Login.css'
|
||||
|
||||
enum LoginStatus {
|
||||
IDLE,
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
ERROR
|
||||
}
|
||||
|
||||
type LoginProperties = {
|
||||
cancelable: boolean
|
||||
}
|
||||
|
||||
type LoginState = {
|
||||
rememberMe: boolean
|
||||
userValid: boolean
|
||||
passValid: boolean
|
||||
status: LoginStatus
|
||||
}
|
||||
|
||||
export default class Login extends React.Component<LoginProperties, LoginState> {
|
||||
|
||||
private userRef: React.RefObject<LoginField>
|
||||
private passRef: React.RefObject<LoginField>
|
||||
|
||||
constructor(props: LoginProperties) {
|
||||
super(props)
|
||||
this.state = {
|
||||
rememberMe: true,
|
||||
userValid: false,
|
||||
passValid: false,
|
||||
status: LoginStatus.IDLE
|
||||
}
|
||||
this.userRef = React.createRef()
|
||||
this.passRef = React.createRef()
|
||||
}
|
||||
|
||||
getCancelButton(): JSX.Element {
|
||||
if(this.props.cancelable) {
|
||||
return (
|
||||
<>
|
||||
<div id="loginCancelContainer">
|
||||
<button id="loginCancelButton" disabled={this.isFormDisabled()}>
|
||||
<div id="loginCancelIcon">X</div>
|
||||
<span id="loginCancelText">Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (<></>)
|
||||
}
|
||||
}
|
||||
|
||||
isFormDisabled = (): boolean => {
|
||||
return this.state.status !== LoginStatus.IDLE
|
||||
}
|
||||
|
||||
isLoading = (): boolean => {
|
||||
return this.state.status === LoginStatus.LOADING
|
||||
}
|
||||
|
||||
canSave = (): boolean => {
|
||||
return this.state.passValid && this.state.userValid && !this.isFormDisabled()
|
||||
}
|
||||
|
||||
getButtonText = (): string => {
|
||||
switch(this.state.status) {
|
||||
case LoginStatus.LOADING:
|
||||
return 'LOGGING IN'
|
||||
case LoginStatus.SUCCESS:
|
||||
return 'SUCCESS'
|
||||
case LoginStatus.ERROR:
|
||||
case LoginStatus.IDLE:
|
||||
return 'LOGIN'
|
||||
}
|
||||
}
|
||||
|
||||
handleUserValidityChange = (valid: boolean): void => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
userValid: valid
|
||||
})
|
||||
}
|
||||
|
||||
handlePassValidityChange = (valid: boolean): void => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
passValid: valid
|
||||
})
|
||||
}
|
||||
|
||||
handleCheckBoxChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
rememberMe: event.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
handleFormSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
handleLoginButtonClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
console.log(this.userRef.current!.getValue())
|
||||
console.log(this.passRef.current!.getValue())
|
||||
this.setState({
|
||||
...this.state,
|
||||
status: LoginStatus.LOADING
|
||||
})
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div id="loginContainer">
|
||||
{this.getCancelButton()}
|
||||
<div id="loginContent">
|
||||
<form id="loginForm" onSubmit={this.handleFormSubmit}>
|
||||
<img id="loginImageSeal" src="../images/SealCircle.png"/>
|
||||
<span id="loginSubheader">MINECRAFT LOGIN</span>
|
||||
|
||||
<LoginField
|
||||
ref={this.userRef}
|
||||
password={false}
|
||||
disabled={this.isFormDisabled()}
|
||||
onValidityChange={this.handleUserValidityChange} />
|
||||
<LoginField
|
||||
ref={this.passRef}
|
||||
password={true}
|
||||
disabled={this.isFormDisabled()}
|
||||
onValidityChange={this.handlePassValidityChange} />
|
||||
|
||||
<div id="loginOptions">
|
||||
<span className="loginSpanDim">
|
||||
<a href="https://my.minecraft.net/en-us/password/forgot/">forgot password?</a>
|
||||
</span>
|
||||
<label id="checkmarkContainer" {...(this.isFormDisabled() ? {disabled: true} : {})} >
|
||||
<input
|
||||
id="loginRememberOption"
|
||||
type="checkbox"
|
||||
checked={this.state.rememberMe}
|
||||
onChange={this.handleCheckBoxChange}
|
||||
disabled={this.isFormDisabled()}
|
||||
></input>
|
||||
<span id="loginRememberText" className="loginSpanDim">remember me?</span>
|
||||
<span className="loginCheckmark"></span>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
id="loginButton"
|
||||
disabled={!this.canSave()}
|
||||
onClick={this.handleLoginButtonClick}
|
||||
{...(this.isLoading() ? {loading: 'true'} : {})}>
|
||||
<div id="loginButtonContent">
|
||||
{this.getButtonText()}
|
||||
<svg id="loginSVG" viewBox="0 0 24.87 13.97">
|
||||
<defs>
|
||||
<style>{'.arrowLine{transition: 0.25s ease;}'}</style> {/** TODO */}
|
||||
</defs>
|
||||
<polyline className="arrowLine" fill="none" stroke="#FFF" strokeWidth="2px" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
||||
</svg>
|
||||
<div className="circle-loader">
|
||||
<div className="checkmark draw"></div>
|
||||
</div>
|
||||
{/*<div className="spinningCircle" id="loginSpinner"></div>-*/}
|
||||
</div>
|
||||
</button>
|
||||
<div id="loginDisclaimer">
|
||||
<span className="loginSpanDim" id="loginRegisterSpan">
|
||||
<a href="https://minecraft.net/en-us/store/minecraft/">Need an Account?</a>
|
||||
</span>
|
||||
<p className="loginDisclaimerText">Your password is sent directly to mojang and never stored.</p>
|
||||
<p className="loginDisclaimerText">Helios Launcher is not affiliated with Mojang AB.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
85
src/renderer/components/login/login-field/LoginField.css
Normal file
@ -0,0 +1,85 @@
|
||||
/* Container to organize login field elements. */
|
||||
.loginFieldContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* SVG icons on the login view. */
|
||||
.loginSVG {
|
||||
fill: #fff;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
/* Span which displays errors related to login field content. */
|
||||
.loginErrorSpan {
|
||||
font-family: 'Avenir Medium';
|
||||
font-weight: bold;
|
||||
font-size: 0.5rem;
|
||||
color: #ff1b0c;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
top: 0.4375rem;
|
||||
opacity: 0;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
|
||||
.shake {
|
||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% {
|
||||
transform: translate3d(-0.0625rem, 0, 0);
|
||||
}
|
||||
|
||||
20%, 80% {
|
||||
transform: translate3d(0.125rem, 0, 0);
|
||||
}
|
||||
|
||||
30%, 50%, 70% {
|
||||
transform: translate3d(-0.25rem, 0, 0);
|
||||
}
|
||||
|
||||
40%, 60% {
|
||||
transform: translate3d(0.25rem, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Login text input styles. */
|
||||
.loginField {
|
||||
font-family: 'Avenir Book';
|
||||
background: none;
|
||||
border-width: 0.09375rem 0 0 0;
|
||||
border-style: solid;
|
||||
width: 15.625rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-color: #fff;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0.46875rem;
|
||||
font-size: 0.625rem;
|
||||
letter-spacing: 0.0625rem;
|
||||
}
|
||||
.loginField:focus {
|
||||
outline: none;
|
||||
}
|
||||
.loginField:disabled {
|
||||
color: rgba(255, 255, 255, 0.50);
|
||||
}
|
||||
.loginField::-webkit-input-placeholder {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 0.625rem;
|
||||
letter-spacing: 0.0625rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.loginField:focus::-webkit-input-placeholder {
|
||||
color: transparent;
|
||||
}
|
186
src/renderer/components/login/login-field/LoginField.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import './LoginField.css'
|
||||
|
||||
enum FieldError {
|
||||
REQUIRED = 'Required',
|
||||
INVALID = 'Invalid Value'
|
||||
}
|
||||
|
||||
type LoginFieldProps = {
|
||||
password: boolean
|
||||
disabled: boolean
|
||||
onValidityChange: (valid: boolean) => void
|
||||
}
|
||||
|
||||
type LoginFieldState = {
|
||||
errorText: FieldError
|
||||
hasError: boolean
|
||||
shake: boolean
|
||||
value: string
|
||||
}
|
||||
|
||||
export default class LoginField extends React.Component<LoginFieldProps, LoginFieldState> {
|
||||
|
||||
private readonly USERNAME_REGEX = /^[a-zA-Z0-9_]{1,16}$/
|
||||
private readonly BASIC_EMAIL_REGEX = /^\S+@\S+\.\S+$/
|
||||
// private readonly VALID_EMAIL_REGEX = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||
|
||||
private readonly SHAKE_CLASS = 'shake'
|
||||
|
||||
private errorSpanRef: React.RefObject<HTMLSpanElement>
|
||||
private internalTrigger = false // Indicates that the component updated from an internal trigger.
|
||||
|
||||
constructor(props: LoginFieldProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
errorText: FieldError.REQUIRED,
|
||||
hasError: true,
|
||||
shake: false,
|
||||
value: ''
|
||||
}
|
||||
this.errorSpanRef = React.createRef()
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if(this.internalTrigger) {
|
||||
if(this.state.hasError) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Opacity is a number, not a string..
|
||||
this.errorSpanRef.current!.style.opacity = 1
|
||||
if(this.state.shake) {
|
||||
this.errorSpanRef.current!.classList.remove(this.SHAKE_CLASS)
|
||||
void this.errorSpanRef.current!.offsetWidth
|
||||
this.errorSpanRef.current!.classList.add(this.SHAKE_CLASS)
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Opacity is a number, not a string..
|
||||
this.errorSpanRef.current!.style.opacity = 0
|
||||
}
|
||||
}
|
||||
this.internalTrigger = false
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
private getFieldSvg(): JSX.Element {
|
||||
if(this.props.password) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg className="loginSVG" viewBox="40 32 60.36 70.43">
|
||||
<g>
|
||||
<path d="M86.16,54a16.38,16.38,0,1,0-32,0H44V102.7H96V54Zm-25.9-3.39a9.89,9.89,0,1,1,19.77,0A9.78,9.78,0,0,1,79.39,54H60.89A9.78,9.78,0,0,1,60.26,50.59ZM70,96.2a6.5,6.5,0,0,1-6.5-6.5,6.39,6.39,0,0,1,3.1-5.4V67h6.5V84.11a6.42,6.42,0,0,1,3.39,5.6A6.5,6.5,0,0,1,70,96.2Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
|
||||
} else {
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg className="loginSVG" viewBox="40 37 65.36 61.43">
|
||||
<g>
|
||||
<path d="M86.77,58.12A13.79,13.79,0,1,0,73,71.91,13.79,13.79,0,0,0,86.77,58.12M97,103.67a3.41,3.41,0,0,0,3.39-3.84,27.57,27.57,0,0,0-54.61,0,3.41,3.41,0,0,0,3.39,3.84Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private formatError(error: FieldError): string {
|
||||
return `* ${error}`
|
||||
}
|
||||
|
||||
private getErrorState(shake: boolean, errorText: FieldError): Partial<LoginFieldState> & Required<{hasError: boolean}> {
|
||||
return {
|
||||
shake,
|
||||
errorText,
|
||||
hasError: true,
|
||||
}
|
||||
}
|
||||
|
||||
private getValidState(): Partial<LoginFieldState> & Required<{hasError: boolean}> {
|
||||
return {
|
||||
hasError: false
|
||||
}
|
||||
}
|
||||
|
||||
private validateEmail = (value: string, shakeOnError: boolean): void => {
|
||||
let newState
|
||||
if(value) {
|
||||
if(!this.BASIC_EMAIL_REGEX.test(value) && !this.USERNAME_REGEX.test(value)) {
|
||||
newState = this.getErrorState(shakeOnError, FieldError.INVALID)
|
||||
} else {
|
||||
newState = this.getValidState()
|
||||
}
|
||||
} else {
|
||||
newState = this.getErrorState(shakeOnError, FieldError.REQUIRED)
|
||||
}
|
||||
this.internalTrigger = true
|
||||
this.setState({
|
||||
...this.state,
|
||||
...newState,
|
||||
value
|
||||
})
|
||||
this.props.onValidityChange(!newState.hasError)
|
||||
}
|
||||
|
||||
private validatePassword = (value: string, shakeOnError: boolean): void => {
|
||||
let newState
|
||||
if(value) {
|
||||
newState = this.getValidState()
|
||||
} else {
|
||||
newState = this.getErrorState(shakeOnError, FieldError.REQUIRED)
|
||||
}
|
||||
this.internalTrigger = true
|
||||
this.setState({
|
||||
...this.state,
|
||||
...newState,
|
||||
value
|
||||
})
|
||||
this.props.onValidityChange(!newState.hasError)
|
||||
}
|
||||
|
||||
private getValidateFunction(): (value: string, shakeOnError: boolean) => void {
|
||||
return this.props.password ? this.validatePassword : this.validateEmail
|
||||
}
|
||||
|
||||
private handleBlur = (event: React.FocusEvent<HTMLInputElement>): void => {
|
||||
this.getValidateFunction()(event.target.value, true)
|
||||
}
|
||||
|
||||
private handleInput = (event: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.getValidateFunction()((event.target as HTMLInputElement).value, false)
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="loginFieldContainer">
|
||||
{this.getFieldSvg()}
|
||||
<span
|
||||
className="loginErrorSpan"
|
||||
ref={this.errorSpanRef}>
|
||||
{this.formatError(this.state.errorText)}
|
||||
</span>
|
||||
<input
|
||||
className="loginField"
|
||||
disabled={this.props.disabled}
|
||||
type={this.props.password ? 'password' : 'text'}
|
||||
defaultValue={this.state.value}
|
||||
placeholder={this.props.password ? 'PASSWORD' : 'EMAIL OR USERNAME'}
|
||||
onBlur={this.handleBlur}
|
||||
onInput={this.handleInput} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
320
src/renderer/components/news/News.css
Normal file
@ -0,0 +1,320 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Landing View (News Styles) *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Main container. */
|
||||
#newsContainer {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: top 2s ease;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* News content container. */
|
||||
#newsContent {
|
||||
height: 82vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
-webkit-user-select: initial;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Drop shadow displayed when content is scrolled out of view. */
|
||||
#newsContent:before {
|
||||
content: '';
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.25), transparent);
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
#newsContent[scrolled]:before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* News article status container (left). */
|
||||
#newsStatusContainer {
|
||||
width: calc(30% - 60px);
|
||||
height: calc(100% - 30px);
|
||||
padding: 15px 15px 15px 45px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* News status content. */
|
||||
#newsStatusContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* News title wrapper. */
|
||||
#newsTitleContainer {
|
||||
display: flex;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
/* News article title styles. */
|
||||
#newsArticleTitle {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: 'Avenir Medium';
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
transition: 0.25s ease;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
}
|
||||
#newsArticleTitle:hover,
|
||||
#newsArticleTitle:focus {
|
||||
text-shadow: 0 0 20px white;
|
||||
}
|
||||
#newsArticleTitle:active {
|
||||
color: #c7c7c7;
|
||||
text-shadow: 0 0 20px #c7c7c7;
|
||||
}
|
||||
|
||||
/* News meta container. */
|
||||
#newsMetaContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Date and author wrappers. */
|
||||
#newsArticleDateWrapper,
|
||||
#newsArticleAuthorWrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Date and author shared styles. */
|
||||
#newsArticleDate,
|
||||
#newsArticleAuthor {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 0 5px;
|
||||
font-weight: bold;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Date styles. */
|
||||
#newsArticleDate {
|
||||
background: white;
|
||||
color: black;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Author styles. */
|
||||
#newsArticleAuthor {
|
||||
background: #a02d2a;
|
||||
}
|
||||
|
||||
/* News article comments styles. */
|
||||
#newsArticleComments {
|
||||
margin-top: 5px;
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
transition: 0.25s ease;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
}
|
||||
#newsArticleComments:focus,
|
||||
#newsArticleComments:hover {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
#newsArticleComments:active {
|
||||
color: #c7c7c7;
|
||||
}
|
||||
|
||||
/* Article content container (right). */
|
||||
#newsArticleContainer {
|
||||
width: calc(100% - 25px);
|
||||
height: 100%;
|
||||
margin: 0 0 0 25px;
|
||||
}
|
||||
|
||||
/* Article content styles. */
|
||||
#newsArticleContentScrollable {
|
||||
font-size: 12px;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
padding: 0 15px 0 15px;
|
||||
}
|
||||
#newsArticleContentScrollable img,
|
||||
#newsArticleContentScrollable iframe {
|
||||
max-width: 95%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#newsArticleContentScrollable a {
|
||||
color: rgba(202, 202, 202, 0.75);
|
||||
transition: 0.25s ease;
|
||||
outline: none;
|
||||
}
|
||||
#newsArticleContentScrollable a:hover,
|
||||
#newsArticleContentScrollable a:focus {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
#newsArticleContentScrollable a:active {
|
||||
color: rgba(165, 165, 165, 0.75);
|
||||
}
|
||||
#newsArticleContentScrollable::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
#newsArticleContentScrollable::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
#newsArticleContentScrollable::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50);
|
||||
}
|
||||
.bbCodeSpoilerButton {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: 0.25s ease;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid white;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.bbCodeSpoilerButton:hover,
|
||||
.bbCodeSpoilerButton:focus {
|
||||
text-shadow: 0 0 20px #ffffff, 0 0 20px #ffffff, 0 0 20px #ffffff;
|
||||
}
|
||||
.bbCodeSpoilerButton:active {
|
||||
color: #c7c7c7;
|
||||
text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
|
||||
}
|
||||
.bbCodeSpoilerText {
|
||||
display: none;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
|
||||
#newsArticleContentWrapper {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.newsArticleSpacerTop {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
/* Div to add spacing at the end of a news article. */
|
||||
.newsArticleSpacerBot {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
/* News navigation container. */
|
||||
#newsNavigationContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
-webkit-user-select: none;
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Navigation status span. */
|
||||
#newsNavigationStatus {
|
||||
font-size: 12px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
/* Left and right navigation button styles. */
|
||||
#newsNavigateLeft,
|
||||
#newsNavigateRight {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#newsNavigateLeft:hover #newsNavigationLeftSVG,
|
||||
#newsNavigateLeft:focus #newsNavigationLeftSVG,
|
||||
#newsNavigateRight:hover #newsNavigationRightSVG,
|
||||
#newsNavigateRight:focus #newsNavigationRightSVG {
|
||||
filter: drop-shadow(0px 0 2px #fff);
|
||||
-webkit-filter: drop-shadow(0px 0 2px #fff);
|
||||
}
|
||||
#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine,
|
||||
#newsNavigateRight:active #newsNavigationRightSVG .arrowLine {
|
||||
stroke: #c7c7c7;
|
||||
}
|
||||
#newsNavigateLeft:active #newsNavigationLeftSVG,
|
||||
#newsNavigateRight:active #newsNavigationRightSVG {
|
||||
filter: drop-shadow(0px 0 2px #c7c7c7);
|
||||
-webkit-filter: drop-shadow(0px 0 2px #c7c7c7);
|
||||
}
|
||||
#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine,
|
||||
#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine {
|
||||
stroke: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
#newsNavigationLeftSVG {
|
||||
transform: rotate(-90deg);
|
||||
width: 15px;
|
||||
}
|
||||
#newsNavigationRightSVG {
|
||||
transform: rotate(90deg);
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
/* News error (message) container. */
|
||||
#newsErrorContainer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
#newsErrorFailed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* News error content (message). */
|
||||
.newsErrorContent {
|
||||
font-size: 20px;
|
||||
}
|
||||
#newsErrorLoading {
|
||||
display: flex;
|
||||
width: 168.92px;
|
||||
}
|
||||
#nELoadSpan {
|
||||
white-space: pre;
|
||||
}
|
||||
/* News error retry button styles. */
|
||||
#newsErrorRetry {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
#newsErrorRetry:focus,
|
||||
#newsErrorRetry:hover {
|
||||
text-shadow: 0 0 20px white;
|
||||
}
|
||||
#newsErrorRetry:active {
|
||||
color: #c7c7c7;
|
||||
text-shadow: 0 0 20px #c7c7c7;
|
||||
}
|