Compare commits
No commits in common. "master" and "legacy" have entirely different histories.
@ -1 +0,0 @@
|
|||||||
dist
|
|
@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"es2022": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2022,
|
|
||||||
"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": [ "app/assets/js/scripts/*.js" ],
|
|
||||||
"rules": {
|
|
||||||
"no-unused-vars": [
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"no-undef": [
|
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
3
.github/FUNDING.yml
vendored
@ -1,3 +0,0 @@
|
|||||||
github: dscalzi
|
|
||||||
patreon: dscalzi
|
|
||||||
custom: ['https://www.paypal.me/dscalzi']
|
|
38
.github/workflows/build.yml
vendored
@ -1,38 +0,0 @@
|
|||||||
name: Build
|
|
||||||
|
|
||||||
on: push
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.x
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: npm run dist
|
|
||||||
shell: bash
|
|
5
.gitignore
vendored
@ -1,6 +1,5 @@
|
|||||||
/node_modules/
|
/node_modules/
|
||||||
/.vs/
|
/.vs/
|
||||||
/.vscode/
|
/.vscode/
|
||||||
/target/
|
/mcfiles/
|
||||||
/logs/
|
/logs/
|
||||||
/dist/
|
|
21
LICENSE.txt
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2017-2024 Daniel D. Scalzi
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
206
README.md
@ -1,138 +1,39 @@
|
|||||||
<p align="center"><img src="./app/assets/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
|
# Getting Started #
|
||||||
|
|
||||||
<h1 align="center">ONIMAI.RU MC Launcher</h1>
|
System Requirements:
|
||||||
|
* [Node.js](https://nodejs.org/en/) v7.9.0+
|
||||||
|
|
||||||
<em><h5 align="center">(formerly Electron Launcher)</h5></em>
|
This repository is dedicated to the development of the new custom launcher for the [WesterosCraft](http://www.westeroscraft.com/) server. This project is developed primarily with [Node.js](https://nodejs.org/en/) and the [Electron](https://electron.atom.io/) framework. For further reference you may view [the repository of the new launcher written in JavaFX/Java](https://gitlab.com/westeroscraft/WesteroscraftNewLauncher) which was discontinued. You may also view the repository of the [current launcher](https://gitlab.com/westeroscraft/westeroscraftlaunchercore), a modified fork of MCUpdater.
|
||||||
|
|
||||||
[<p align="center"><img src="https://img.shields.io/github/actions/workflow/status/dscalzi/HeliosLauncher/build.yml?branch=master&style=for-the-badge" alt="gh actions">](https://github.com/dscalzi/HeliosLauncher/actions) [<img src="https://img.shields.io/github/downloads/dscalzi/HeliosLauncher/total.svg?style=for-the-badge" alt="downloads">](https://github.com/dscalzi/HeliosLauncher/releases) <img src="https://forthebadge.com/images/badges/winter-is-coming.svg" height="28px" alt="winter-is-coming"></p>
|
For authentication with Mojang, we are currently planning on using [node-mojang](https://github.com/jamen/node-mojang). This will automatically be downloaded if you follow the simple installation instructions below.
|
||||||
|
|
||||||
<p align="center">Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.</p>
|
### Recommended IDE ###
|
||||||
|
|
||||||

|
The recommended IDE for this project is [VS Code](https://code.visualstudio.com/), an open source code editor by Microsoft. This editor is available on nearly every major platform (Windows, macOS, Linux). If you choose to use another editor, such as [Atom](https://atom.io/), please gitignore the IDE specific settings directory, if it hasn't been already.
|
||||||

|
|
||||||
|
|
||||||
## Features
|
### Installation ###
|
||||||
|
|
||||||
* 🔒 Full account management.
|
To begin working on this project clone the repository and open run the following command on the command line. This will download all of the required dependencies.
|
||||||
* Add multiple accounts and easily switch between them.
|
|
||||||
* Microsoft (OAuth 2.0) + Mojang (Yggdrasil) authentication fully supported.
|
|
||||||
* Credentials are never stored and transmitted directly to Mojang.
|
|
||||||
* 📂 Efficient asset management.
|
|
||||||
* Receive client updates as soon as we release them.
|
|
||||||
* Files are validated before launch. Corrupt or incorrect files will be redownloaded.
|
|
||||||
* ☕ **Automatic Java validation.**
|
|
||||||
* If you have an incompatible version of Java installed, we'll install the right one *for you*.
|
|
||||||
* You do not need to have Java installed to run the launcher.
|
|
||||||
* 📰 News feed natively built into the launcher.
|
|
||||||
* ⚙️ Intuitive settings management, including a Java control panel.
|
|
||||||
* Supports all of our servers.
|
|
||||||
* Switch between server configurations with ease.
|
|
||||||
* View the player count of the selected server.
|
|
||||||
* Automatic updates. That's right, the launcher updates itself.
|
|
||||||
* View the status of Mojang's services.
|
|
||||||
|
|
||||||
This is not an exhaustive list. Download and install the launcher to gauge all it can do!
|
```shell
|
||||||
|
npm install
|
||||||
#### Need Help? [Check the wiki.][wiki]
|
|
||||||
|
|
||||||
#### Like the project? Leave a ⭐ star on the repository!
|
|
||||||
|
|
||||||
## Downloads
|
|
||||||
|
|
||||||
You can download from [GitHub Releases](https://github.com/dscalzi/HeliosLauncher/releases)
|
|
||||||
|
|
||||||
#### Latest Release
|
|
||||||
|
|
||||||
[](https://github.com/dscalzi/HeliosLauncher/releases/latest)
|
|
||||||
|
|
||||||
#### Latest Pre-Release
|
|
||||||
[](https://github.com/dscalzi/HeliosLauncher/releases)
|
|
||||||
|
|
||||||
**Supported Platforms**
|
|
||||||
|
|
||||||
If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/releases) tab, select the installer for your system.
|
|
||||||
|
|
||||||
| Platform | File |
|
|
||||||
| -------- | ---- |
|
|
||||||
| Windows x64 | `Helios-Launcher-setup-VERSION.exe` |
|
|
||||||
| macOS x64 | `Helios-Launcher-setup-VERSION-x64.dmg` |
|
|
||||||
| macOS arm64 | `Helios-Launcher-setup-VERSION-arm64.dmg` |
|
|
||||||
| Linux x64 | `Helios-Launcher-setup-VERSION.AppImage` |
|
|
||||||
|
|
||||||
## Console
|
|
||||||
|
|
||||||
To open the console, use the following keybind.
|
|
||||||
|
|
||||||
```console
|
|
||||||
ctrl + shift + i
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Ensure that you have the console tab selected. Do not paste anything into the console unless you are 100% sure of what it will do. Pasting the wrong thing can expose sensitive information.
|
# Launching #
|
||||||
|
|
||||||
#### Export Output to a File
|
### Command Line ###
|
||||||
|
|
||||||
If you want to export the console output, simply right click anywhere on the console and click **Save as..**
|
There are several different ways to launch this project. One way is simply to run the following command on the command line.
|
||||||
|
|
||||||

|
```shell
|
||||||
|
npm start
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
This section details the setup of a basic developmentment environment.
|
|
||||||
|
|
||||||
### Getting Started
|
|
||||||
|
|
||||||
**System Requirements**
|
|
||||||
|
|
||||||
* [Node.js][nodejs] v20
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Clone and Install Dependencies**
|
|
||||||
|
|
||||||
```console
|
|
||||||
> git clone https://github.com/dscalzi/HeliosLauncher.git
|
|
||||||
> cd HeliosLauncher
|
|
||||||
> npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Visual Studio Code ###
|
||||||
|
|
||||||
**Launch Application**
|
If you use VS Code, you can run this directly from the IDE. Copy the following code into your launch.json file. This will require you to also install [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome).
|
||||||
|
|
||||||
```console
|
```json
|
||||||
> npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Build Installers**
|
|
||||||
|
|
||||||
To build for your current platform.
|
|
||||||
|
|
||||||
```console
|
|
||||||
> npm run dist
|
|
||||||
```
|
|
||||||
|
|
||||||
Build for a specific platform.
|
|
||||||
|
|
||||||
| Platform | Command |
|
|
||||||
| ----------- | -------------------- |
|
|
||||||
| Windows x64 | `npm run dist:win` |
|
|
||||||
| macOS | `npm run dist:mac` |
|
|
||||||
| Linux x64 | `npm run dist:linux` |
|
|
||||||
|
|
||||||
Builds for macOS may not work on Windows/Linux and vice-versa.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Visual Studio Code
|
|
||||||
|
|
||||||
All development of the launcher should be done using [Visual Studio Code][vscode].
|
|
||||||
|
|
||||||
Paste the following into `.vscode/launch.json`
|
|
||||||
|
|
||||||
```JSON
|
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
@ -140,72 +41,49 @@ Paste the following into `.vscode/launch.json`
|
|||||||
"name": "Debug Main Process",
|
"name": "Debug Main Process",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceRoot}",
|
||||||
"program": "${workspaceFolder}/node_modules/electron/cli.js",
|
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron",
|
||||||
"args" : ["."],
|
"windows": {
|
||||||
"outputCapture": "std"
|
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron.cmd"
|
||||||
|
},
|
||||||
|
"program": "${workspaceRoot}\\index.js",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"protocol": "legacy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Renderer Process",
|
"name": "Debug Renderer Process",
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron",
|
||||||
"windows": {
|
"windows": {
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
"runtimeExecutable": "${workspaceRoot}\\node_modules\\.bin\\electron.cmd"
|
||||||
},
|
},
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"${workspaceFolder}/.",
|
"${workspaceRoot}\\index.js",
|
||||||
"--remote-debugging-port=9222"
|
"--remote-debugging-port=9222"
|
||||||
],
|
],
|
||||||
"webRoot": "${workspaceFolder}"
|
"webRoot": "${workspaceRoot}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This adds two debug configurations.
|
This will create two launch configurations from which you can debug the launcher. The first configuration, **Debug Main Process**, will allow you to debug the main Electron process. The second configuration, **Debug Renderer Process**, will allow you to debug the rendering of web pages (ie the UI).
|
||||||
|
|
||||||
#### Debug Main Process
|
You can find more information [here](http://code.matsu.io/1).
|
||||||
|
|
||||||
This allows you to debug Electron's [main process][mainprocess]. You can debug scripts in the [renderer process][rendererprocess] by opening the DevTools Window.
|
### Notes on DevTools Window ###
|
||||||
|
|
||||||
#### Debug Renderer Process
|
Once you run the program, you can open the DevTools window by typing the following keys in sequence on the main window.
|
||||||
|
|
||||||
This allows you to debug Electron's [renderer process][rendererprocess]. This requires you to install the [Debugger for Chrome][chromedebugger] extension.
|
```shell
|
||||||
|
wcdev
|
||||||
|
```
|
||||||
|
|
||||||
Note that you **cannot** open the DevTools window while using this debug configuration. Chromium only allows one debugger, opening another will crash the program.
|
Please note that if you are debugging the application with VS Code and have launched the program using the **Debug Renderer Process** configuration you cannot open the DevTools window. If you attempt to do so, the program will crash. Remote debugging cannot be done with multiple DevTools clients.
|
||||||
|
|
||||||
---
|
# Issues / Further Support #
|
||||||
|
|
||||||
### Note on Third-Party Usage
|
If you run into any issue which cannot be resolved via a quick google search, create an issue using the tab above.
|
||||||
|
|
||||||
Please give credit to the original author and provide a link to the original source. This is free software, please do at least this much.
|
Much of the discussion regarding this launcher is done on Discord, feel free to join us there [](https://discord.gg/hqdjs3m)
|
||||||
|
|
||||||
For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
* [Wiki][wiki]
|
|
||||||
* [Nebula (Create Distribution.json)][nebula]
|
|
||||||
* [v2 Rewrite Branch (Inactive)][v2branch]
|
|
||||||
|
|
||||||
The best way to contact the developers is on Discord.
|
|
||||||
|
|
||||||
[][discord]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### See you ingame.
|
|
||||||
|
|
||||||
|
|
||||||
[nodejs]: https://nodejs.org/en/ 'Node.js'
|
|
||||||
[vscode]: https://code.visualstudio.com/ 'Visual Studio Code'
|
|
||||||
[mainprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Main Process'
|
|
||||||
[rendererprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Renderer Process'
|
|
||||||
[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'
|
|
||||||
[nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula'
|
|
||||||
[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch'
|
|
180
app/Inworks-index-ignore.html
Executable file
@ -0,0 +1,180 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="assets/css/global.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="assets/css/header.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="assets/css/nav.css">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
|
||||||
|
.inner_div{
|
||||||
|
width:100%;
|
||||||
|
background-color: black;
|
||||||
|
border: 5px solid #a02d2a;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="header_container">
|
||||||
|
<div id="header_seal_container">
|
||||||
|
<img id="header_seal" src="./assets/images/WesterosSealSquare.png"/>
|
||||||
|
</div>
|
||||||
|
<div id="header_img_container">
|
||||||
|
<img id="header_img" src="./assets/images/WCTextCrop.png" />
|
||||||
|
</div>
|
||||||
|
<div id="header_social_container">
|
||||||
|
<a href="http://facebook.com/westeroscraft" class="header_social_link">
|
||||||
|
<img src="./assets/images/facebook.png" class="header_social_img" />
|
||||||
|
</a>
|
||||||
|
<a href="http://reddit.com/r/westeroscraft" class="header_social_link">
|
||||||
|
<img src="./assets/images/reddit.png" class="header_social_img" />
|
||||||
|
</a>
|
||||||
|
<a href="http://twitter.com/westeroscraft" class="header_social_link">
|
||||||
|
<img src="./assets/images/twitter.png" class="header_social_img" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-md-2 col-sm-2">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-md-3 col-sm-3">
|
||||||
|
|
||||||
|
<img class="player_icon" src="https://minotar.net/helm/pufferboss.png">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-9 col-sm-9">
|
||||||
|
|
||||||
|
<p class="input_text">Email</p>
|
||||||
|
<input class="input" id="EMAIL">
|
||||||
|
|
||||||
|
<p class="input_text">Password</p>
|
||||||
|
<input type="password" id="PASSWORD" class="input" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-10 col-sm-10">
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="">NEWS</a></li>
|
||||||
|
<li><a href="">MAP</a></li>
|
||||||
|
<li><a href="">MODS</a></li>
|
||||||
|
<li><a href="">FAQ</a></li>
|
||||||
|
<li><a href="">LOG</a></li>
|
||||||
|
<li><a href="">SETTINGS</a></li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="inner_div">
|
||||||
|
|
||||||
|
<p class="title"> Post title </p>
|
||||||
|
|
||||||
|
<p class="subtite">Posted by: PERSON 23.4.17</p>
|
||||||
|
|
||||||
|
<pre style="text-align: left; word-break: break-all; overflow-wrap: break-word;"> Hello WesterosCraft Builders and Honored Guests! As you know,
|
||||||
|
progress on Kingslanding had stalled, but no longer -- we want to finish this big, beautiful, city in style and
|
||||||
|
are calling all builders to the capital!
|
||||||
|
There has been a lot of progress with Kingslanding planning and we are happy to...
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
||||||
|
=======
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Westeroscraft Launcher</title>
|
||||||
|
<script src="./assets/js/script.js"></script>
|
||||||
|
<link type="text/css" rel="stylesheet" href="./assets/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="header_container">
|
||||||
|
<div id="header_seal_container">
|
||||||
|
<img id="header_seal" src="./assets/images/WesterosSealSquare.png"/>
|
||||||
|
</div>
|
||||||
|
<div id="header_img_container">
|
||||||
|
<img id="header_img" src="./assets/images/WCTextCrop.png" />
|
||||||
|
</div>
|
||||||
|
<div id="header_social_container">
|
||||||
|
<a href="http://facebook.com/westeroscraft" class="header_social_link">
|
||||||
|
<img src="./assets/images/facebook.png" class="header_social_img" />
|
||||||
|
</a>
|
||||||
|
<a href="http://reddit.com/r/westeroscraft" class="header_social_link">
|
||||||
|
<img src="./assets/images/reddit.png" class="header_social_img" />
|
||||||
|
</a>
|
||||||
|
<a href="http://twitter.com/westeroscraft" class="header_social_link">
|
||||||
|
<img src="./assets/images/twitter.png" class="header_social_img" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="body_left_container">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="body_right_container">
|
||||||
|
<div id="main_toggle_group">
|
||||||
|
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>News</label></div>
|
||||||
|
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Map</label></div>
|
||||||
|
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Mods</label></div>
|
||||||
|
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>FAQ</label></div>
|
||||||
|
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Log</label></div>
|
||||||
|
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Settings</label></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
>>>>>>> refs/remotes/origin/master
|
49
app/app.ejs
@ -1,49 +0,0 @@
|
|||||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-In6B8teKZQll5heMl9bS7CESTbGvuAt3VVV86BUQBDk='"/>
|
|
||||||
<title><%= lang('app.title') %></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">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
/*background: url('assets/images/backgrounds/<%=bkid%>.jpg') no-repeat center center fixed;*/
|
|
||||||
transition: background-image 1s ease;
|
|
||||||
background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAPwBwAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8VooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9k=');
|
|
||||||
background-size: cover;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
#main {
|
|
||||||
display: none;
|
|
||||||
height: calc(100% - 22px);
|
|
||||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
#main[overlay] {
|
|
||||||
filter: blur(3px) contrast(0.9) brightness(1.0);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body bkid="<%=bkid%>">
|
|
||||||
<%- include('frame') %>
|
|
||||||
<div id="main">
|
|
||||||
<%- include('welcome') %>
|
|
||||||
<%- include('login') %>
|
|
||||||
<%- include('waiting') %>
|
|
||||||
<%- include('loginOptions') %>
|
|
||||||
<%- include('settings') %>
|
|
||||||
<%- include('landing') %>
|
|
||||||
</div>
|
|
||||||
<%- include('overlay') %>
|
|
||||||
<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">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
55
app/assets/css/global.css
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: ringbearer;
|
||||||
|
src: url('../fonts/ringbearer.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logger font, found on https://fonts.google.com/specimen/Inconsolata?selection.family=Inconsolata */
|
||||||
|
@font-face {
|
||||||
|
font-family: inconsolata;
|
||||||
|
src: url('../fonts/Inconsolata-Bold.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html , body{
|
||||||
|
background: url('../images/BrownWithWignette.jpg') no-repeat center center fixed;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a, a:hover
|
||||||
|
{
|
||||||
|
color: black;
|
||||||
|
text-decoration:none;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pre {border: 0; background-color: transparent;}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
input{
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 3px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid #ECECEC;
|
||||||
|
font-size: 20px;
|
||||||
|
outline:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
p, img, pre, span, label, h1 ,h2 ,h3 {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
-khtml-user-drag: none;
|
||||||
|
-moz-user-drag: none;
|
||||||
|
-o-user-drag: none;
|
||||||
|
user-drag: none;
|
||||||
|
}
|
46
app/assets/css/header.css
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#header_container {
|
||||||
|
background-color: black;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: thick solid #a02d2a;
|
||||||
|
border-bottom-width: 5px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Div container for the seal image. */
|
||||||
|
#header_seal_container {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Div container for the header image. */
|
||||||
|
#header_img_container {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seal and header images. */
|
||||||
|
#header_seal,
|
||||||
|
#header_img {
|
||||||
|
height: 75px;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Div container for the social buttons. */
|
||||||
|
#header_social_container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social buttons. */
|
||||||
|
.header_social_img {
|
||||||
|
height: 25px;
|
||||||
|
width: auto;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
351
app/assets/css/styles.css
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Fonts *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/* Primary font for the application, found on http://www.dafont.com/ringbearer.font */
|
||||||
|
@font-face {
|
||||||
|
font-family: ringbearer;
|
||||||
|
src: url('../fonts/ringbearer.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logger font, found on https://fonts.google.com/specimen/Inconsolata?selection.family=Inconsolata */
|
||||||
|
@font-face {
|
||||||
|
font-family: inconsolata;
|
||||||
|
src: url('../fonts/Inconsolata-Regular.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Body *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/* Reset body, html, and div presets. */
|
||||||
|
body, html, div {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: url('../images/BrownWithWignette.jpg') no-repeat center center fixed;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main_content {
|
||||||
|
height: calc(100% - 90px);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #a02d2a;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
padding: 7px 10px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: #a02d2a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Header *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
/* Main div header container. */
|
||||||
|
#header_container {
|
||||||
|
background-color: black;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 0px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: thick solid #a02d2a;
|
||||||
|
border-bottom-width: 5px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Div container for the seal image. */
|
||||||
|
#header_seal_container {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Div container for the header image. */
|
||||||
|
#header_img_container {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seal and header images. */
|
||||||
|
#header_seal,
|
||||||
|
#header_img {
|
||||||
|
height: 75px;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Div container for the social buttons. */
|
||||||
|
#header_social_container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social buttons. */
|
||||||
|
.header_social_img {
|
||||||
|
height: 25px;
|
||||||
|
width: auto;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Toggle Buttons *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: 'ringbearer';
|
||||||
|
user-select: none;
|
||||||
|
background-color: transparent;
|
||||||
|
display: inline-flex;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 90px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn-grp .toggle-btn {
|
||||||
|
border-radius: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:not(.success):hover {
|
||||||
|
box-shadow: inset 0 -4.5px 0 0 #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn.success {
|
||||||
|
background: #a02d2a;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visuallyhidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus {
|
||||||
|
clip: auto;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
overflow: visible;
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Left Body Container *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#left_pane {
|
||||||
|
float: left;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#welcome_text_container {
|
||||||
|
display: table;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#welcome_text {
|
||||||
|
font-family: 'ringbearer';
|
||||||
|
font-size: 16px;
|
||||||
|
display: table-cell;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_pane {
|
||||||
|
background: linear-gradient(141deg, #000000 -5%, #a02d2a 120%, #000000 150%);
|
||||||
|
padding-top: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.28);
|
||||||
|
width: 97%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_pane_header_container {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_pane_header {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'Segoe UI';
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_container {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_image {
|
||||||
|
height: 85px;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
border: 3px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_container_right input,
|
||||||
|
#login_container_right label {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_container_left {
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_container_right {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_field {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Segoe UI';
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
/* new */
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button_login {
|
||||||
|
float: right;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Right Body Container *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#right_pane {
|
||||||
|
float: right;
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content_container {
|
||||||
|
border: 3px solid #a02d2a;
|
||||||
|
height: calc(98% - 41.33px);
|
||||||
|
width: 98%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launcher-log {
|
||||||
|
font-family: 'inconsolata';
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #1a1b1c;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: scroll;
|
||||||
|
display: block;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
text-shadow: 0.01px 0.01px 0.01px #ffffff;
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launcher-log::selection {
|
||||||
|
background: rgba(160, 45, 42, .9);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launcher-log::-webkit-scrollbar {
|
||||||
|
background-color: #a02d2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#launcher-log::-webkit-scrollbar-thumb:window-inactive,
|
||||||
|
#launcher-log::-webkit-scrollbar-thumb {
|
||||||
|
background: black
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_debug {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Login View *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#login_view {
|
||||||
|
background: rgba(0, 0, 0, 0.76);
|
||||||
|
height: calc(100% - 90px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_content {
|
||||||
|
width: 50%;
|
||||||
|
height: 75%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_view #content_main {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_view #login_content_image {
|
||||||
|
height: 125px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login_view #content_main #right {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_input {
|
||||||
|
background-color: #a02d2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_input::-webkit-input-placeholder {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
BIN
app/assets/fonts/Inconsolata-Bold.ttf
Normal file
BIN
app/assets/fonts/Inconsolata-Regular.ttf
Normal file
BIN
app/assets/images/.BrownWithWignette.jpg.icloud
Normal file
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 20 KiB |
BIN
app/assets/images/WCTextCream.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
app/assets/images/WCTextCrop.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
app/assets/images/WesterosSealCircle.png
Normal file
After Width: | Height: | Size: 807 KiB |
3636
app/assets/images/WesterosSealSquare.hqx
Normal file
BIN
app/assets/images/WesterosSealSquare.icns
Normal file
BIN
app/assets/images/WesterosSealSquare.ico
Normal file
After Width: | Height: | Size: 361 KiB |
BIN
app/assets/images/WesterosSealSquare.png
Normal file
After Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 604 KiB |
Before Width: | Height: | Size: 781 KiB |
Before Width: | Height: | Size: 311 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 343 KiB |
Before Width: | Height: | Size: 500 KiB |
Before Width: | Height: | Size: 580 KiB |
Before Width: | Height: | Size: 312 KiB |
BIN
app/assets/images/facebook.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
@ -1,7 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.87 13.97">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:none;stroke:#FFF;stroke-width:2px;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>arrow</title>
|
|
||||||
<polyline class="cls-1" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 298 B |
@ -1,10 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
|
||||||
<clipPath id="clip-path"><rect class="cls-1" x="36.42" y="44.23" width="68.52" height="48.96"/></clipPath>
|
|
||||||
</defs>
|
|
||||||
<title>discord</title>
|
|
||||||
<g class="cls-2">
|
|
||||||
<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>
|
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,9 +0,0 @@
|
|||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="504px" height="504px" viewBox="0 0 5040 5040" preserveAspectRatio="xMidYMid meet">
|
|
||||||
<g id="layer101" fill="#000000" stroke="none">
|
|
||||||
<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>
|
|
Before Width: | Height: | Size: 1.8 KiB |
@ -1,11 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
|
||||||
<clipPath id="clip-path"><rect class="cls-1" x="29.3" y="52.62" width="82.77" height="34.15"/></clipPath>
|
|
||||||
</defs>
|
|
||||||
<title>link</title>
|
|
||||||
<g class="cls-2">
|
|
||||||
<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>
|
|
Before Width: | Height: | Size: 875 B |
@ -1,12 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#231f20;}</style>
|
|
||||||
<clipPath id="clip-path">
|
|
||||||
<rect class="cls-1" x="44.02" y="34.21" width="51.96" height="68.48"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<title>Lock</title>
|
|
||||||
<g class="cls-2">
|
|
||||||
<path class="cls-3" 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>
|
|
Before Width: | Height: | Size: 756 B |
@ -1,7 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
|
|
||||||
<path fill="#f3f3f3" d="M0 0h23v23H0z" />
|
|
||||||
<path fill="#f35325" d="M1 1h10v10H1z" />
|
|
||||||
<path fill="#81bc06" d="M12 1h10v10H12z" />
|
|
||||||
<path fill="#05a6f0" d="M1 12h10v10H1z" />
|
|
||||||
<path fill="#ffba08" d="M12 12h10v10H12z" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 303 B |
@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 9.677 9.667">
|
|
||||||
<path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
|
|
||||||
<path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
|
|
||||||
<path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 664 B |
@ -1,14 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.36 137.43">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#231f20;}.cls-2,.cls-3{fill:none;stroke-miterlimit:10;stroke-width:6px;}.cls-2{stroke:#231f20;}.cls-3{stroke:#000;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>News</title>
|
|
||||||
<rect class="cls-1" x="31.77" y="32.96" width="33.79" height="20.76"/>
|
|
||||||
<path class="cls-2" d="M115.36,113.8H27.18a6.67,6.67,0,0,1-6.67-6.67V19.27H108.2V107.1a6.71,6.71,0,0,0,6.71,6.7h0a6.71,6.71,0,0,0,6.71-6.71v-75H108.15"/>
|
|
||||||
<line class="cls-3" x1="73.75" y1="36.18" x2="97.14" y2="36.18"/>
|
|
||||||
<line class="cls-3" x1="73.75" y1="50.22" x2="97.14" y2="50.22"/>
|
|
||||||
<line class="cls-3" x1="31.66" y1="64.25" x2="97.14" y2="64.25"/>
|
|
||||||
<line class="cls-3" x1="31.66" y1="78.28" x2="97.14" y2="78.28"/>
|
|
||||||
<line class="cls-3" x1="31.66" y1="92.31" x2="97.14" y2="92.31"/>
|
|
||||||
<line class="cls-3" x1="31.66" y1="92.31" x2="97.14" y2="92.31"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 959 B |
@ -1,10 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
|
||||||
<clipPath id="clip-path"><rect class="cls-1" x="45.51" y="44.33" width="55.14" height="59.33"/></clipPath>
|
|
||||||
</defs>
|
|
||||||
<title>Profile</title>
|
|
||||||
<g class="cls-2">
|
|
||||||
<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>
|
|
Before Width: | Height: | Size: 602 B |
@ -1,10 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
|
||||||
<clipPath id="clip-path"><rect class="cls-1" x="45.65" y="42.62" width="49.58" height="52.43"/></clipPath>
|
|
||||||
</defs>
|
|
||||||
<title>settings</title>
|
|
||||||
<g class="cls-2">
|
|
||||||
<path d="M70.44,75a6.19,6.19,0,1,1,5.84-6.18A6,6,0,0,1,70.44,75M91.67,63.71h-5A18.4,18.4,0,0,0,85.19,60l3.48-3.68a3.93,3.93,0,0,0,0-5.32l-1.4-1.48a3.43,3.43,0,0,0-5,0l-3.48,3.68A16.34,16.34,0,0,0,75,51.59V46.38a3.68,3.68,0,0,0-3.56-3.76h-2a3.68,3.68,0,0,0-3.56,3.76v5.21a16.23,16.23,0,0,0-3.77,1.64l-3.48-3.68a3.43,3.43,0,0,0-5,0L52.21,51a3.93,3.93,0,0,0,0,5.32L55.69,60a18.21,18.21,0,0,0-1.48,3.67h-5a3.67,3.67,0,0,0-3.56,3.76v2.1a3.68,3.68,0,0,0,3.56,3.76h4.84a18.46,18.46,0,0,0,1.64,4.3l-3.48,3.68a3.93,3.93,0,0,0,0,5.32l1.4,1.48a3.43,3.43,0,0,0,5,0l3.48-3.68a16.36,16.36,0,0,0,3.77,1.64v5.21a3.67,3.67,0,0,0,3.56,3.76h2A3.67,3.67,0,0,0,75,91.29V86.08a16.48,16.48,0,0,0,3.77-1.64l3.48,3.68a3.43,3.43,0,0,0,5,0l1.4-1.48a3.93,3.93,0,0,0,0-5.32l-3.48-3.68a18.45,18.45,0,0,0,1.63-4.3h4.85a3.68,3.68,0,0,0,3.56-3.76v-2.1a3.67,3.67,0,0,0-3.56-3.76"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,13 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.45 104.74">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#1a171b;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>Seven Pointed Star</title>
|
|
||||||
<polygon class="cls-1" points="43.83 52.37 48.83 14.03 53.83 52.37 43.83 52.37"/>
|
|
||||||
<polygon class="cls-1" points="45.71 56.28 18.85 28.47 51.95 48.46 45.71 56.28"/>
|
|
||||||
<polygon class="cls-1" points="49.94 57.25 11.45 60.9 47.72 47.5 49.94 57.25"/>
|
|
||||||
<polygon class="cls-1" points="53.34 54.54 32.19 86.92 44.33 50.2 53.34 54.54"/>
|
|
||||||
<polygon class="cls-1" points="53.34 50.2 65.47 86.92 44.33 54.54 53.34 50.2"/>
|
|
||||||
<polygon class="cls-1" points="49.94 47.5 86.21 60.91 47.72 57.25 49.94 47.5"/>
|
|
||||||
<polygon class="cls-1" points="45.71 48.46 78.81 28.47 51.95 56.28 45.71 48.46"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 809 B |
@ -1,14 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.45 104.74">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#1a171b;}.cls-2{fill:none;stroke:#1a171b;stroke-miterlimit:10;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>Seven Pointed Star with Circle</title>
|
|
||||||
<polygon class="cls-1" points="43.83 52.37 48.83 14.03 53.83 52.37 43.83 52.37"/>
|
|
||||||
<polygon class="cls-1" points="45.71 56.28 18.85 28.47 51.95 48.46 45.71 56.28"/>
|
|
||||||
<polygon class="cls-1" points="49.94 57.25 11.45 60.9 47.72 47.5 49.94 57.25"/>
|
|
||||||
<polygon class="cls-1" points="53.34 54.54 32.19 86.92 44.33 50.2 53.34 54.54"/>
|
|
||||||
<polygon class="cls-1" points="53.34 50.2 65.47 86.92 44.33 54.54 53.34 50.2"/>
|
|
||||||
<polygon class="cls-1" points="49.94 47.5 86.21 60.91 47.72 57.25 49.94 47.5"/>
|
|
||||||
<polygon class="cls-1" points="45.71 48.46 78.81 28.47 51.95 56.28 45.71 48.46"/>
|
|
||||||
<circle class="cls-2" cx="48.83" cy="52.37" r="38"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 932 B |
@ -1,8 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.45 104.74">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#1a171b;}.cls-2{fill:none;stroke:#1a171b;stroke-miterlimit:10;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>Seven Pointed Star Extended with Circle</title>
|
|
||||||
<path class="cls-1" d="M100.93,65.54C89,62,68.18,55.65,63.54,52.13c2.7-5.23,18.8-19.2,28-27.55C81.36,31.74,63.74,43.87,58.09,45.3c-2.41-5.37-3.61-26.52-4.37-39-.77,12.46-2,33.64-4.36,39-5.7-1.46-23.3-13.57-33.49-20.72,9.26,8.37,25.39,22.36,28,27.55C39.21,55.68,18.47,62,6.52,65.55c12.32-2,33.63-6.06,39.34-4.9-.16,5.87-8.41,26.16-13.11,37.69,6.1-10.89,16.52-30.16,21-33.9,4.5,3.79,14.93,23.09,21,34C70,86.84,61.73,66.48,61.59,60.65,67.36,59.49,88.64,63.52,100.93,65.54Z"/>
|
|
||||||
<circle class="cls-2" cx="53.73" cy="53.9" r="38"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 822 B |
@ -1,15 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.45 104.74">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#1a171b;}.cls-2{fill:none;stroke:#1a171b;stroke-miterlimit:10;}.cls-3{fill:#fff;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>Seven Pointed Star with Circle and Hole</title>
|
|
||||||
<polygon class="cls-1" points="43.83 52.37 48.83 14.03 53.83 52.37 43.83 52.37"/>
|
|
||||||
<polygon class="cls-1" points="45.71 56.28 18.85 28.47 51.95 48.46 45.71 56.28"/>
|
|
||||||
<polygon class="cls-1" points="49.94 57.25 11.45 60.9 47.72 47.5 49.94 57.25"/>
|
|
||||||
<polygon class="cls-1" points="53.34 54.54 32.19 86.92 44.33 50.2 53.34 54.54"/>
|
|
||||||
<polygon class="cls-1" points="53.34 50.2 65.47 86.92 44.33 54.54 53.34 50.2"/>
|
|
||||||
<polygon class="cls-1" points="49.94 47.5 86.21 60.91 47.72 57.25 49.94 47.5"/>
|
|
||||||
<polygon class="cls-1" points="45.71 48.46 78.81 28.47 51.95 56.28 45.71 48.46"/>
|
|
||||||
<circle class="cls-2" cx="48.83" cy="52.37" r="38"/>
|
|
||||||
<circle class="cls-3" cx="48.83" cy="52.37" r="4.56"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1018 B |
@ -1,9 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.45 104.74">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#1a171b;}.cls-2{fill:none;stroke:#1a171b;stroke-miterlimit:10;}.cls-3{fill:#fff;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>Seven Pointed Star Extended with Circle and Hole</title>
|
|
||||||
<path class="cls-1" d="M100.93,65.54C89,62,68.18,55.65,63.54,52.13c2.7-5.23,18.8-19.2,28-27.55C81.36,31.74,63.74,43.87,58.09,45.3c-2.41-5.37-3.61-26.52-4.37-39-.77,12.46-2,33.64-4.36,39-5.7-1.46-23.3-13.57-33.49-20.72,9.26,8.37,25.39,22.36,28,27.55C39.21,55.68,18.47,62,6.52,65.55c12.32-2,33.63-6.06,39.34-4.9-.16,5.87-8.41,26.16-13.11,37.69,6.1-10.89,16.52-30.16,21-33.9,4.5,3.79,14.93,23.09,21,34C70,86.84,61.73,66.48,61.59,60.65,67.36,59.49,88.64,63.52,100.93,65.54Z"/>
|
|
||||||
<circle class="cls-2" cx="53.73" cy="53.9" r="38"/>
|
|
||||||
<circle class="cls-3" cx="53.73" cy="53.9" r="4.56"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 907 B |
@ -1,7 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.45 104.74">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#1a171b;}</style>
|
|
||||||
</defs>
|
|
||||||
<title>Seven Pointed Star Extended</title>
|
|
||||||
<path class="cls-1" d="M100.93,65.54C89,62,68.18,55.65,63.54,52.13c2.7-5.23,18.8-19.2,28-27.55C81.36,31.74,63.74,43.87,58.09,45.3c-2.41-5.37-3.61-26.52-4.37-39-.77,12.46-2,33.64-4.36,39-5.7-1.46-23.3-13.57-33.49-20.72,9.26,8.37,25.39,22.36,28,27.55C39.21,55.68,18.47,62,6.52,65.55c12.32-2,33.63-6.06,39.34-4.9-.16,5.87-8.41,26.16-13.11,37.69,6.1-10.89,16.52-30.16,21-33.9,4.5,3.79,14.93,23.09,21,34C70,86.84,61.73,66.48,61.59,60.65,67.36,59.49,88.64,63.52,100.93,65.54Z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 700 B |
Before Width: | Height: | Size: 9.8 KiB |
@ -1,10 +0,0 @@
|
|||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
|
||||||
<clipPath id="clip-path"><rect class="cls-1" x="38.29" y="45.86" width="70.16" height="48.48"/></clipPath>
|
|
||||||
</defs>
|
|
||||||
<title>youtube</title>
|
|
||||||
<g class="cls-2">
|
|
||||||
<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>
|
|
Before Width: | Height: | Size: 654 B |
BIN
app/assets/images/loginBackground.jpg
Normal file
After Width: | Height: | Size: 1012 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
app/assets/images/reddit.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
app/assets/images/twitter.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
app/assets/images/westeroscraftlogo1.png
Normal file
After Width: | Height: | Size: 606 KiB |
BIN
app/assets/images/westeroscraftlogo2.png
Normal file
After Width: | Height: | Size: 227 KiB |
239
app/assets/js/assetdownload.js
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const request = require('request')
|
||||||
|
const path = require('path')
|
||||||
|
const mkpath = require('mkdirp');
|
||||||
|
const async = require('async')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const Library = require('./library.js')
|
||||||
|
const {BrowserWindow} = require('electron')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHASING THIS OUT, WILL BE REMOVED WHEN ASSET GUARD MODULE IS COMPLETE!
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Asset(from, to, size, hash){
|
||||||
|
this.from = from
|
||||||
|
this.to = to
|
||||||
|
this.size = size
|
||||||
|
this.hash = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
function AssetIndex(id, sha1, size, url, totalSize){
|
||||||
|
this.id = id
|
||||||
|
this.sha1 = sha1
|
||||||
|
this.size = size
|
||||||
|
this.url = url
|
||||||
|
this.totalSize = totalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will download the version index data and read it into a Javascript
|
||||||
|
* Object. This object will then be returned.
|
||||||
|
*/
|
||||||
|
parseVersionData = function(version, basePath){
|
||||||
|
const name = version + '.json'
|
||||||
|
const baseURL = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
|
||||||
|
const versionPath = path.join(basePath, 'versions', version)
|
||||||
|
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
request.head(baseURL, function(err, res, body){
|
||||||
|
console.log('Preparing download of ' + version + ' assets.')
|
||||||
|
mkpath.sync(versionPath)
|
||||||
|
const stream = request(baseURL).pipe(fs.createWriteStream(path.join(versionPath, name)))
|
||||||
|
stream.on('finish', function(){
|
||||||
|
fulfill(JSON.parse(fs.readFileSync(path.join(versionPath, name))))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the client for version. This file is 'client.jar' although
|
||||||
|
* it must be renamed to '{version}'.jar.
|
||||||
|
*/
|
||||||
|
downloadClient = function(versionData, basePath){
|
||||||
|
const dls = versionData['downloads']
|
||||||
|
const clientData = dls['client']
|
||||||
|
const url = clientData['url']
|
||||||
|
const size = clientData['size']
|
||||||
|
const version = versionData['id']
|
||||||
|
const sha1 = clientData['sha1']
|
||||||
|
const targetPath = path.join(basePath, 'versions', version)
|
||||||
|
const targetFile = version + '.jar'
|
||||||
|
|
||||||
|
if(!validateLocalIntegrity(path.join(targetPath, targetFile), 'sha1', sha1)){
|
||||||
|
request.head(url, function(err, res, body){
|
||||||
|
console.log('Downloading ' + version + ' client..')
|
||||||
|
mkpath.sync(targetPath)
|
||||||
|
const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, targetFile)))
|
||||||
|
stream.on('finish', function(){
|
||||||
|
console.log('Finished downloading ' + version + ' client.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadLogConfig = function(versionData, basePath){
|
||||||
|
const logging = versionData['logging']
|
||||||
|
const client = logging['client']
|
||||||
|
const file = client['file']
|
||||||
|
const version = versionData['id']
|
||||||
|
const sha1 = file['sha1']
|
||||||
|
const targetPath = path.join(basePath, 'assets', 'log_configs')
|
||||||
|
const name = file['id']
|
||||||
|
const url = file['url']
|
||||||
|
|
||||||
|
if(!validateLocalIntegrity(path.join(targetPath, name), 'sha1', sha1)){
|
||||||
|
request.head(url, function(err, res, body){
|
||||||
|
console.log('Downloading ' + version + ' log config..')
|
||||||
|
mkpath.sync(targetPath)
|
||||||
|
const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, name)))
|
||||||
|
stream.on('finish', function(){
|
||||||
|
console.log('Finished downloading ' + version + ' log config..')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadLibraries = function(versionData, basePath){
|
||||||
|
const libArr = versionData['libraries']
|
||||||
|
const libPath = path.join(basePath, 'libraries')
|
||||||
|
|
||||||
|
let win = BrowserWindow.getFocusedWindow()
|
||||||
|
const libDlQueue = []
|
||||||
|
let dlSize = 0
|
||||||
|
|
||||||
|
//Check validity of each library. If the hashs don't match, download the library.
|
||||||
|
libArr.forEach(function(lib, index){
|
||||||
|
if(Library.validateRules(lib.rules)){
|
||||||
|
let artifact = null
|
||||||
|
if(lib.natives == null){
|
||||||
|
artifact = lib.downloads.artifact
|
||||||
|
} else {
|
||||||
|
artifact = lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
|
||||||
|
}
|
||||||
|
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
|
||||||
|
if(!validateLocalIntegrity(libItm.to, 'sha1', libItm.sha1)){
|
||||||
|
dlSize += libItm.size
|
||||||
|
libDlQueue.push(libItm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let acc = 0;
|
||||||
|
|
||||||
|
//Download all libraries that failed validation.
|
||||||
|
async.eachLimit(libDlQueue, 1, function(lib, cb){
|
||||||
|
mkpath.sync(path.join(lib.to, '..'))
|
||||||
|
let req = request(lib.from)
|
||||||
|
let writeStream = fs.createWriteStream(lib.to)
|
||||||
|
req.pipe(writeStream)
|
||||||
|
|
||||||
|
req.on('data', function(chunk){
|
||||||
|
acc += chunk.length
|
||||||
|
//console.log('Progress', acc/dlSize)
|
||||||
|
win.setProgressBar(acc/dlSize)
|
||||||
|
})
|
||||||
|
writeStream.on('close', cb)
|
||||||
|
}, function(err){
|
||||||
|
if(err){
|
||||||
|
console.log('A library failed to process');
|
||||||
|
} else {
|
||||||
|
console.log('All libraries have been processed successfully');
|
||||||
|
}
|
||||||
|
win.setProgressBar(-1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an index url, this function will asynchonously download the
|
||||||
|
* assets associated with that version.
|
||||||
|
*/
|
||||||
|
downloadAssets = function(versionData, basePath){
|
||||||
|
//Asset index constants.
|
||||||
|
const assetIndex = versionData.assetIndex
|
||||||
|
const indexURL = assetIndex.url
|
||||||
|
const gameVersion = versionData.id
|
||||||
|
const assetVersion = assetIndex.id
|
||||||
|
const name = assetVersion + '.json'
|
||||||
|
|
||||||
|
//Asset constants
|
||||||
|
const resourceURL = 'http://resources.download.minecraft.net/'
|
||||||
|
const localPath = path.join(basePath, 'assets')
|
||||||
|
const indexPath = path.join(localPath, 'indexes')
|
||||||
|
const objectPath = path.join(localPath, 'objects')
|
||||||
|
|
||||||
|
let win = BrowserWindow.getFocusedWindow()
|
||||||
|
|
||||||
|
const assetIndexLoc = path.join(indexPath, name)
|
||||||
|
/*if(!fs.existsSync(assetIndexLoc)){
|
||||||
|
|
||||||
|
}*/
|
||||||
|
console.log('Downloading ' + gameVersion + ' asset index.')
|
||||||
|
mkpath.sync(indexPath)
|
||||||
|
const stream = request(indexURL).pipe(fs.createWriteStream(assetIndexLoc))
|
||||||
|
stream.on('finish', function() {
|
||||||
|
const data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
|
||||||
|
const assetDlQueue = []
|
||||||
|
let dlSize = 0;
|
||||||
|
Object.keys(data.objects).forEach(function(key, index){
|
||||||
|
const ob = data.objects[key]
|
||||||
|
const hash = ob.hash
|
||||||
|
const assetName = path.join(hash.substring(0, 2), hash)
|
||||||
|
const urlName = hash.substring(0, 2) + "/" + hash
|
||||||
|
const ast = new Asset(resourceURL + urlName, path.join(objectPath, assetName), ob.size, String(ob.hash))
|
||||||
|
if(!validateLocalIntegrity(ast.to, 'sha1', ast.hash)){
|
||||||
|
dlSize += ast.size
|
||||||
|
assetDlQueue.push(ast)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let acc = 0;
|
||||||
|
async.eachLimit(assetDlQueue, 5, function(asset, cb){
|
||||||
|
mkpath.sync(path.join(asset.to, ".."))
|
||||||
|
let req = request(asset.from)
|
||||||
|
let writeStream = fs.createWriteStream(asset.to)
|
||||||
|
req.pipe(writeStream)
|
||||||
|
req.on('data', function(chunk){
|
||||||
|
acc += chunk.length
|
||||||
|
console.log('Progress', acc/dlSize)
|
||||||
|
win.setProgressBar(acc/dlSize)
|
||||||
|
})
|
||||||
|
writeStream.on('close', cb)
|
||||||
|
}, function(err){
|
||||||
|
if(err){
|
||||||
|
console.log('An asset failed to process');
|
||||||
|
} else {
|
||||||
|
console.log('All assets have been processed successfully');
|
||||||
|
}
|
||||||
|
win.setProgressBar(-1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
validateLocalIntegrity = function(filePath, algo, hash){
|
||||||
|
if(fs.existsSync(filePath)){
|
||||||
|
let fileName = path.basename(filePath)
|
||||||
|
console.log('Validating integrity of local file', fileName)
|
||||||
|
let shasum = crypto.createHash(algo)
|
||||||
|
let content = fs.readFileSync(filePath)
|
||||||
|
shasum.update(content)
|
||||||
|
let localhash = shasum.digest('hex')
|
||||||
|
if(localhash === hash){
|
||||||
|
console.log('Hash value of ' + fileName + ' matches the index hash, woo!')
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
console.log('Hash value of ' + fileName + ' (' + localhash + ')' + ' does not match the index hash. Redownloading..')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parseVersionData,
|
||||||
|
downloadClient,
|
||||||
|
downloadLogConfig,
|
||||||
|
downloadLibraries,
|
||||||
|
downloadAssets
|
||||||
|
}
|
856
app/assets/js/assetguard.js
Normal file
@ -0,0 +1,856 @@
|
|||||||
|
/**
|
||||||
|
* AssetGuard
|
||||||
|
*
|
||||||
|
* This module aims to provide a comprehensive and stable method for processing
|
||||||
|
* and downloading game assets for the WesterosCraft server. A central object
|
||||||
|
* stores download meta for several identifiers (categories). This meta data
|
||||||
|
* is initially empty until one of the module's processing functions are called.
|
||||||
|
* That function will process the corresponding asset index and validate any exisitng
|
||||||
|
* local files. If a file is missing or fails validation, it will be placed into an
|
||||||
|
* array which acts as a queue. This queue is wrapped in a download tracker object
|
||||||
|
* so that essential information can be cached. The download tracker object is then
|
||||||
|
* assigned as the value of the identifier in the central object. These download
|
||||||
|
* trackers will remain idle until an async process is started to process them.
|
||||||
|
*
|
||||||
|
* Once the async process is started, any enqueued assets will be downloaded. The central
|
||||||
|
* object will emit events throughout the download whose name correspond to the identifier
|
||||||
|
* being processed. For example, if the 'assets' identifier was being processed, whenever
|
||||||
|
* the download stream recieves data, the event 'assetsdlprogress' will be emitted off of
|
||||||
|
* the central object instance. This can be listened to by external modules allowing for
|
||||||
|
* categorical tracking of the downloading process.
|
||||||
|
*
|
||||||
|
* @module assetguard
|
||||||
|
*/
|
||||||
|
// Requirements
|
||||||
|
const fs = require('fs')
|
||||||
|
const request = require('request')
|
||||||
|
const path = require('path')
|
||||||
|
const mkpath = require('mkdirp');
|
||||||
|
const async = require('async')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const AdmZip = require('adm-zip')
|
||||||
|
const child_process = require('child_process')
|
||||||
|
const EventEmitter = require('events')
|
||||||
|
const {remote} = require('electron')
|
||||||
|
|
||||||
|
// Classes
|
||||||
|
|
||||||
|
/** Class representing a base asset. */
|
||||||
|
class Asset{
|
||||||
|
/**
|
||||||
|
* Create an asset.
|
||||||
|
*
|
||||||
|
* @param {any} id - id of the asset.
|
||||||
|
* @param {String} hash - hash value of the asset.
|
||||||
|
* @param {Number} size - size in bytes of the asset.
|
||||||
|
* @param {String} from - url where the asset can be found.
|
||||||
|
* @param {String} to - absolute local file path of the asset.
|
||||||
|
*/
|
||||||
|
constructor(id, hash, size, from, to){
|
||||||
|
this.id = id
|
||||||
|
this.hash = hash
|
||||||
|
this.size = size
|
||||||
|
this.from = from
|
||||||
|
this.to = to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Class representing a mojang library. */
|
||||||
|
class Library extends Asset{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the process.platform OS names to match mojang's OS names.
|
||||||
|
*/
|
||||||
|
static mojangFriendlyOS(){
|
||||||
|
const opSys = process.platform
|
||||||
|
if (opSys === 'darwin') {
|
||||||
|
return 'osx';
|
||||||
|
} else if (opSys === 'win32'){
|
||||||
|
return 'windows';
|
||||||
|
} else if (opSys === 'linux'){
|
||||||
|
return 'linux';
|
||||||
|
} else {
|
||||||
|
return 'unknown_os';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether or not a library is valid for download on a particular OS, following
|
||||||
|
* the rule format specified in the mojang version data index. If the allow property has
|
||||||
|
* an OS specified, then the library can ONLY be downloaded on that OS. If the disallow
|
||||||
|
* property has instead specified an OS, the library can be downloaded on any OS EXCLUDING
|
||||||
|
* the one specified.
|
||||||
|
*
|
||||||
|
* @param {Object} rules - the Library's download rules.
|
||||||
|
* @returns {Boolean} - true if the Library follows the specified rules, otherwise false.
|
||||||
|
*/
|
||||||
|
static validateRules(rules){
|
||||||
|
if(rules == null) return true
|
||||||
|
|
||||||
|
let result = true
|
||||||
|
rules.forEach(function(rule){
|
||||||
|
const action = rule['action']
|
||||||
|
const osProp = rule['os']
|
||||||
|
if(action != null){
|
||||||
|
if(osProp != null){
|
||||||
|
const osName = osProp['name']
|
||||||
|
const osMoj = Library.mojangFriendlyOS()
|
||||||
|
if(action === 'allow'){
|
||||||
|
result = osName === osMoj
|
||||||
|
return
|
||||||
|
} else if(action === 'disallow'){
|
||||||
|
result = osName !== osMoj
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DistroModule extends Asset {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a DistroModule. This is for processing,
|
||||||
|
* not equivalent to the module objects in the
|
||||||
|
* distro index.
|
||||||
|
*
|
||||||
|
* @param {any} id - id of the asset.
|
||||||
|
* @param {String} hash - hash value of the asset.
|
||||||
|
* @param {Number} size - size in bytes of the asset.
|
||||||
|
* @param {String} from - url where the asset can be found.
|
||||||
|
* @param {String} to - absolute local file path of the asset.
|
||||||
|
* @param {String} type - the module type.
|
||||||
|
*/
|
||||||
|
constructor(id, hash, size, from, to, type){
|
||||||
|
super(id, hash, size, from, to)
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a download tracker. This is used to store meta data
|
||||||
|
* about a download queue, including the queue itself.
|
||||||
|
*/
|
||||||
|
class DLTracker {
|
||||||
|
/**
|
||||||
|
* Create a DLTracker
|
||||||
|
*
|
||||||
|
* @param {Array.<Asset>} dlqueue - an array containing assets queued for download.
|
||||||
|
* @param {Number} dlsize - the combined size of each asset in the download queue array.
|
||||||
|
* @param {function(Asset)} callback - optional callback which is called when an asset finishes downloading.
|
||||||
|
*/
|
||||||
|
constructor(dlqueue, dlsize, callback = null){
|
||||||
|
this.dlqueue = dlqueue
|
||||||
|
this.dlsize = dlsize
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central object class used for control flow. This object stores data about
|
||||||
|
* categories of downloads. Each category is assigned an identifier with a
|
||||||
|
* DLTracker object as its value. Combined information is also stored, such as
|
||||||
|
* the total size of all the queued files in each category. This event is used
|
||||||
|
* to emit events so that external modules can listen into processing done in
|
||||||
|
* this module.
|
||||||
|
*/
|
||||||
|
class AssetGuard extends EventEmitter{
|
||||||
|
/**
|
||||||
|
* AssetGuard class should only ever have one instance which is defined in
|
||||||
|
* this module. On creation the object's properties are never-null default
|
||||||
|
* values. Each identifier is resolved to an empty DLTracker.
|
||||||
|
*/
|
||||||
|
constructor(){
|
||||||
|
super()
|
||||||
|
this.totaldlsize = 0;
|
||||||
|
this.progress = 0;
|
||||||
|
this.assets = new DLTracker([], 0)
|
||||||
|
this.libraries = new DLTracker([], 0)
|
||||||
|
this.files = new DLTracker([], 0)
|
||||||
|
this.forge = new DLTracker([], 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global static final instance of AssetGuard
|
||||||
|
*/
|
||||||
|
const instance = new AssetGuard()
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an artifact id into a path. For example, on windows
|
||||||
|
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
|
||||||
|
* net\minecraftforge\forge\1.11.2-13.20.0.2282\forge-1.11.2-13.20.0.2282.jar
|
||||||
|
*
|
||||||
|
* @param {String} artifactid - the artifact id string.
|
||||||
|
* @param {String} extension - the extension of the file at the resolved path.
|
||||||
|
* @returns {String} - the resolved relative path from the artifact id.
|
||||||
|
*/
|
||||||
|
function _resolvePath(artifactid, extension){
|
||||||
|
let ps = artifactid.split(':')
|
||||||
|
let cs = ps[0].split('.')
|
||||||
|
|
||||||
|
cs.push(ps[1])
|
||||||
|
cs.push(ps[2])
|
||||||
|
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
|
||||||
|
|
||||||
|
return path.join.apply(path, cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an artifact id into a URL. For example,
|
||||||
|
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
|
||||||
|
* net/minecraftforge/forge/1.11.2-13.20.0.2282/forge-1.11.2-13.20.0.2282.jar
|
||||||
|
*
|
||||||
|
* @param {String} artifactid - the artifact id string.
|
||||||
|
* @param {String} extension - the extension of the file at the resolved url.
|
||||||
|
* @returns {String} - the resolved relative URL from the artifact id.
|
||||||
|
*/
|
||||||
|
function _resolveURL(artifactid, extension){
|
||||||
|
let ps = artifactid.split(':')
|
||||||
|
let cs = ps[0].split('.')
|
||||||
|
|
||||||
|
cs.push(ps[1])
|
||||||
|
cs.push(ps[2])
|
||||||
|
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
|
||||||
|
|
||||||
|
return cs.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the hash for a file using the specified algorithm.
|
||||||
|
*
|
||||||
|
* @param {Buffer} buf - the buffer containing file data.
|
||||||
|
* @param {String} algo - the hash algorithm.
|
||||||
|
* @returns {String} - the calculated hash in hex.
|
||||||
|
*/
|
||||||
|
function _calculateHash(buf, algo){
|
||||||
|
return crypto.createHash(algo).update(buf).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to parse a checksums file. This is specifically designed for
|
||||||
|
* the checksums.sha1 files found inside the forge scala dependencies.
|
||||||
|
*
|
||||||
|
* @param {String} content - the string content of the checksums file.
|
||||||
|
* @returns {Object} - an object with keys being the file names, and values being the hashes.
|
||||||
|
*/
|
||||||
|
function _parseChecksumsFile(content){
|
||||||
|
let finalContent = {}
|
||||||
|
let lines = content.split('\n')
|
||||||
|
for(let i=0; i<lines.length; i++){
|
||||||
|
let bits = lines[i].split(' ')
|
||||||
|
if(bits[1] == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
finalContent[bits[1]] = bits[0]
|
||||||
|
}
|
||||||
|
return finalContent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a file exists and matches a given hash value.
|
||||||
|
*
|
||||||
|
* @param {String} filePath - the path of the file to validate.
|
||||||
|
* @param {String} algo - the hash algorithm to check against.
|
||||||
|
* @param {String} hash - the existing hash to check against.
|
||||||
|
* @returns {Boolean} - true if the file exists and calculated hash matches the given hash, otherwise false.
|
||||||
|
*/
|
||||||
|
function _validateLocal(filePath, algo, hash){
|
||||||
|
//No hash provided, have to assume it's good.
|
||||||
|
if(hash == null){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(fs.existsSync(filePath)){
|
||||||
|
let fileName = path.basename(filePath)
|
||||||
|
let buf = fs.readFileSync(filePath)
|
||||||
|
let calcdhash = _calculateHash(buf, algo)
|
||||||
|
return calcdhash === hash
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a file in the style used by forge's version index.
|
||||||
|
*
|
||||||
|
* @param {String} filePath - the path of the file to validate.
|
||||||
|
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
|
||||||
|
* @returns {Boolean} - true if the file exists and the hashes match, otherwise false.
|
||||||
|
*/
|
||||||
|
function _validateForgeChecksum(filePath, checksums){
|
||||||
|
if(fs.existsSync(filePath)){
|
||||||
|
if(checksums == null || checksums.length === 0){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let buf = fs.readFileSync(filePath)
|
||||||
|
let calcdhash = _calculateHash(buf, 'sha1')
|
||||||
|
let valid = checksums.includes(calcdhash)
|
||||||
|
if(!valid && filePath.endsWith('.jar')){
|
||||||
|
valid = _validateForgeJar(filePath, checksums)
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a forge jar file dependency who declares a checksums.sha1 file.
|
||||||
|
* This can be an expensive task as it usually requires that we calculate thousands
|
||||||
|
* of hashes.
|
||||||
|
*
|
||||||
|
* @param {Buffer} buf - the buffer of the jar file.
|
||||||
|
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
|
||||||
|
* @returns {Boolean} - true if all hashes declared in the checksums.sha1 file match the actual hashes.
|
||||||
|
*/
|
||||||
|
function _validateForgeJar(buf, checksums){
|
||||||
|
|
||||||
|
const hashes = {}
|
||||||
|
let expected = {}
|
||||||
|
|
||||||
|
const zip = new AdmZip(buf)
|
||||||
|
const zipEntries = zip.getEntries()
|
||||||
|
|
||||||
|
//First pass
|
||||||
|
for(let i=0; i<zipEntries.length; i++){
|
||||||
|
let entry = zipEntries[i]
|
||||||
|
if(entry.entryName === 'checksums.sha1'){
|
||||||
|
expected = _parseChecksumsFile(zip.readAsText(entry))
|
||||||
|
}
|
||||||
|
hashes[entry.entryName] = _calculateHash(entry.getData(), 'sha1')
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!checksums.includes(hashes['checksums.sha1'])){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check against expected
|
||||||
|
const expectedEntries = Object.keys(expected)
|
||||||
|
for(let i=0; i<expectedEntries.length; i++){
|
||||||
|
if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and unpacks a file from .pack.xz format.
|
||||||
|
*
|
||||||
|
* @param {Array.<String>} filePaths - The paths of the files to be extracted and unpacked.
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the extraction has completed.
|
||||||
|
*/
|
||||||
|
function _extractPackXZ(filePaths){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
const libPath = path.join(__dirname, '..', 'libraries', 'java', 'PackXZExtract.jar')
|
||||||
|
const filePath = filePaths.join(',')
|
||||||
|
const child = child_process.spawn('C:\\Program Files\\Java\\jre1.8.0_131\\bin\\javaw.exe', ['-jar', libPath, '-packxz', filePath])
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
//console.log('PackXZExtract:', data.toString('utf8'))
|
||||||
|
})
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
//console.log('PackXZExtract:', data.toString('utf8'))
|
||||||
|
})
|
||||||
|
child.on('close', (code, signal) => {
|
||||||
|
//console.log('PackXZExtract: Exited with code', code)
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function _finalizeForgeAsset(asset, basePath){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
fs.readFile(asset.to, (err, data) => {
|
||||||
|
const zip = new AdmZip(data)
|
||||||
|
const zipEntries = zip.getEntries()
|
||||||
|
|
||||||
|
for(let i=0; i<zipEntries.length; i++){
|
||||||
|
if(zipEntries[i].entryName === 'version.json'){
|
||||||
|
const forgeVersion = JSON.parse(zip.readAsText(zipEntries[i]))
|
||||||
|
const versionPath = path.join(basePath, 'versions', forgeVersion.id)
|
||||||
|
const versionFile = path.join(versionPath, forgeVersion.id + '.json')
|
||||||
|
if(!fs.existsSync(versionFile)){
|
||||||
|
mkpath.sync(versionPath)
|
||||||
|
fs.writeFileSync(path.join(versionPath, forgeVersion.id + '.json'), zipEntries[i].getData())
|
||||||
|
fulfill(forgeVersion)
|
||||||
|
} else {
|
||||||
|
fulfill(JSON.parse(fs.readFileSync(versionFile, 'utf-8')))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate an async download process for an AssetGuard DLTracker.
|
||||||
|
*
|
||||||
|
* @param {String} identifier - the identifier of the AssetGuard DLTracker.
|
||||||
|
* @param {Number} limit - optional. The number of async processes to run in parallel.
|
||||||
|
* @returns {Boolean} - true if the process began, otherwise false.
|
||||||
|
*/
|
||||||
|
function startAsyncProcess(identifier, limit = 5){
|
||||||
|
let win = remote.getCurrentWindow()
|
||||||
|
|
||||||
|
let acc = 0
|
||||||
|
const concurrentDlTracker = instance[identifier]
|
||||||
|
const concurrentDlQueue = concurrentDlTracker.dlqueue.slice(0)
|
||||||
|
if(concurrentDlQueue.length === 0){
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
console.log(instance.progress)
|
||||||
|
async.eachLimit(concurrentDlQueue, limit, function(asset, cb){
|
||||||
|
let count = 0;
|
||||||
|
mkpath.sync(path.join(asset.to, ".."))
|
||||||
|
let req = request(asset.from)
|
||||||
|
req.pause()
|
||||||
|
req.on('response', (resp) => {
|
||||||
|
if(resp.statusCode === 200){
|
||||||
|
let writeStream = fs.createWriteStream(asset.to)
|
||||||
|
writeStream.on('close', () => {
|
||||||
|
//console.log('DLResults ' + asset.size + ' ' + count + ' ', asset.size === count)
|
||||||
|
if(concurrentDlTracker.callback != null){
|
||||||
|
concurrentDlTracker.callback.apply(concurrentDlTracker, [asset])
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
})
|
||||||
|
req.pipe(writeStream)
|
||||||
|
req.resume()
|
||||||
|
} else {
|
||||||
|
req.abort()
|
||||||
|
console.log('Failed to download ' + asset.from + '. Response code', resp.statusCode)
|
||||||
|
instance.progress += asset.size*1
|
||||||
|
win.setProgressBar(instance.progress/instance.totaldlsize)
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
req.on('data', function(chunk){
|
||||||
|
count += chunk.length
|
||||||
|
instance.progress += chunk.length
|
||||||
|
acc += chunk.length
|
||||||
|
instance.emit(identifier + 'dlprogress', acc)
|
||||||
|
//console.log(identifier + ' Progress', acc/instance[identifier].dlsize)
|
||||||
|
win.setProgressBar(instance.progress/instance.totaldlsize)
|
||||||
|
})
|
||||||
|
}, function(err){
|
||||||
|
if(err){
|
||||||
|
instance.emit(identifier + 'dlerror')
|
||||||
|
console.log('An item in ' + identifier + ' failed to process');
|
||||||
|
} else {
|
||||||
|
instance.emit(identifier + 'dlcomplete')
|
||||||
|
console.log('All ' + identifier + ' have been processed successfully')
|
||||||
|
}
|
||||||
|
instance.totaldlsize -= instance[identifier].dlsize
|
||||||
|
instance.progress -= instance[identifier].dlsize
|
||||||
|
instance[identifier] = new DLTracker([], 0)
|
||||||
|
if(instance.totaldlsize === 0) {
|
||||||
|
win.setProgressBar(-1)
|
||||||
|
instance.emit('dlcomplete')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation Functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the version data for a given minecraft version.
|
||||||
|
*
|
||||||
|
* @param {String} version - the game version for which to load the index data.
|
||||||
|
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||||
|
* @param {Boolean} force - optional. If true, the version index will be downloaded even if it exists locally. Defaults to false.
|
||||||
|
* @returns {Promise.<Object>} - Promise which resolves to the version data object.
|
||||||
|
*/
|
||||||
|
function loadVersionData(version, basePath, force = false){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
const name = version + '.json'
|
||||||
|
const url = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
|
||||||
|
const versionPath = path.join(basePath, 'versions', version)
|
||||||
|
const versionFile = path.join(versionPath, name)
|
||||||
|
if(!fs.existsSync(versionFile) || force){
|
||||||
|
//This download will never be tracked as it's essential and trivial.
|
||||||
|
request.head(url, function(err, res, body){
|
||||||
|
console.log('Preparing download of ' + version + ' assets.')
|
||||||
|
mkpath.sync(versionPath)
|
||||||
|
const stream = request(url).pipe(fs.createWriteStream(versionFile))
|
||||||
|
stream.on('finish', function(){
|
||||||
|
fulfill(JSON.parse(fs.readFileSync(versionFile)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fulfill(JSON.parse(fs.readFileSync(versionFile)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public asset validation function. This function will handle the validation of assets.
|
||||||
|
* It will parse the asset index specified in the version data, analyzing each
|
||||||
|
* asset entry. In this analysis it will check to see if the local file exists and is valid.
|
||||||
|
* If not, it will be added to the download queue for the 'assets' identifier.
|
||||||
|
*
|
||||||
|
* @param {Object} versionData - the version data for the assets.
|
||||||
|
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||||
|
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||||
|
*/
|
||||||
|
function validateAssets(versionData, basePath, force = false){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
_assetChainIndexData(versionData, basePath, force).then(() => {
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Chain the asset tasks to provide full async. The below functions are private.
|
||||||
|
/**
|
||||||
|
* Private function used to chain the asset validation process. This function retrieves
|
||||||
|
* the index data.
|
||||||
|
* @param {Object} versionData
|
||||||
|
* @param {String} basePath
|
||||||
|
* @param {Boolean} force
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||||
|
*/
|
||||||
|
function _assetChainIndexData(versionData, basePath, force = false){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
//Asset index constants.
|
||||||
|
const assetIndex = versionData.assetIndex
|
||||||
|
const name = assetIndex.id + '.json'
|
||||||
|
const indexPath = path.join(basePath, 'assets', 'indexes')
|
||||||
|
const assetIndexLoc = path.join(indexPath, name)
|
||||||
|
|
||||||
|
let data = null
|
||||||
|
if(!fs.existsSync(assetIndexLoc) || force){
|
||||||
|
console.log('Downloading ' + versionData.id + ' asset index.')
|
||||||
|
mkpath.sync(indexPath)
|
||||||
|
const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc))
|
||||||
|
stream.on('finish', function() {
|
||||||
|
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
|
||||||
|
_assetChainValidateAssets(versionData, basePath, data).then(() => {
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
|
||||||
|
_assetChainValidateAssets(versionData, basePath, data).then(() => {
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private function used to chain the asset validation process. This function processes
|
||||||
|
* the assets and enqueues missing or invalid files.
|
||||||
|
* @param {Object} versionData
|
||||||
|
* @param {String} basePath
|
||||||
|
* @param {Boolean} force
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||||
|
*/
|
||||||
|
function _assetChainValidateAssets(versionData, basePath, indexData){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
|
||||||
|
//Asset constants
|
||||||
|
const resourceURL = 'http://resources.download.minecraft.net/'
|
||||||
|
const localPath = path.join(basePath, 'assets')
|
||||||
|
const indexPath = path.join(localPath, 'indexes')
|
||||||
|
const objectPath = path.join(localPath, 'objects')
|
||||||
|
|
||||||
|
const assetDlQueue = []
|
||||||
|
let dlSize = 0;
|
||||||
|
//const objKeys = Object.keys(data.objects)
|
||||||
|
async.forEachOfLimit(indexData.objects, 10, function(value, key, cb){
|
||||||
|
const hash = value.hash
|
||||||
|
const assetName = path.join(hash.substring(0, 2), hash)
|
||||||
|
const urlName = hash.substring(0, 2) + "/" + hash
|
||||||
|
const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName))
|
||||||
|
if(!_validateLocal(ast.to, 'sha1', ast.hash)){
|
||||||
|
dlSize += (ast.size*1)
|
||||||
|
assetDlQueue.push(ast)
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
}, function(err){
|
||||||
|
instance.assets = new DLTracker(assetDlQueue, dlSize)
|
||||||
|
instance.totaldlsize += dlSize*1
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public library validation function. This function will handle the validation of libraries.
|
||||||
|
* It will parse the version data, analyzing each library entry. In this analysis, it will
|
||||||
|
* check to see if the local file exists and is valid. If not, it will be added to the download
|
||||||
|
* queue for the 'libraries' identifier.
|
||||||
|
*
|
||||||
|
* @param {Object} versionData - the version data for the assets.
|
||||||
|
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||||
|
*/
|
||||||
|
function validateLibraries(versionData, basePath){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
|
||||||
|
const libArr = versionData.libraries
|
||||||
|
const libPath = path.join(basePath, 'libraries')
|
||||||
|
|
||||||
|
const libDlQueue = []
|
||||||
|
let dlSize = 0
|
||||||
|
|
||||||
|
//Check validity of each library. If the hashs don't match, download the library.
|
||||||
|
async.eachLimit(libArr, 5, function(lib, cb){
|
||||||
|
if(Library.validateRules(lib.rules)){
|
||||||
|
let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
|
||||||
|
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
|
||||||
|
if(!_validateLocal(libItm.to, 'sha1', libItm.hash)){
|
||||||
|
dlSize += (libItm.size*1)
|
||||||
|
libDlQueue.push(libItm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
}, function(err){
|
||||||
|
instance.libraries = new DLTracker(libDlQueue, dlSize)
|
||||||
|
instance.totaldlsize += dlSize*1
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public miscellaneous mojang file validation function. These files will be enqueued under
|
||||||
|
* the 'files' identifier.
|
||||||
|
*
|
||||||
|
* @param {Object} versionData - the version data for the assets.
|
||||||
|
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||||
|
*/
|
||||||
|
function validateMiscellaneous(versionData, basePath){
|
||||||
|
return new Promise(async function(fulfill, reject){
|
||||||
|
await validateClient(versionData, basePath)
|
||||||
|
await validateLogConfig(versionData, basePath)
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate client file - artifact renamed from client.jar to '{version}'.jar.
|
||||||
|
*
|
||||||
|
* @param {Object} versionData - the version data for the assets.
|
||||||
|
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||||
|
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||||
|
*/
|
||||||
|
function validateClient(versionData, basePath, force = false){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
const clientData = versionData.downloads.client
|
||||||
|
const version = versionData.id
|
||||||
|
const targetPath = path.join(basePath, 'versions', version)
|
||||||
|
const targetFile = version + '.jar'
|
||||||
|
|
||||||
|
let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile))
|
||||||
|
|
||||||
|
if(!_validateLocal(client.to, 'sha1', client.hash) || force){
|
||||||
|
instance.files.dlqueue.push(client)
|
||||||
|
instance.files.dlsize += client.size*1
|
||||||
|
fulfill()
|
||||||
|
} else {
|
||||||
|
fulfill()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate log config.
|
||||||
|
*
|
||||||
|
* @param {Object} versionData - the version data for the assets.
|
||||||
|
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||||
|
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
|
||||||
|
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||||
|
*/
|
||||||
|
function validateLogConfig(versionData, basePath){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
const client = versionData.logging.client
|
||||||
|
const file = client.file
|
||||||
|
const targetPath = path.join(basePath, 'assets', 'log_configs')
|
||||||
|
|
||||||
|
let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id))
|
||||||
|
|
||||||
|
if(!_validateLocal(logConfig.to, 'sha1', logConfig.hash)){
|
||||||
|
instance.files.dlqueue.push(logConfig)
|
||||||
|
instance.files.dlsize += client.size*1
|
||||||
|
fulfill()
|
||||||
|
} else {
|
||||||
|
fulfill()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDistribution(serverpackid, basePath){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
_chainValidateDistributionIndex(basePath).then((value) => {
|
||||||
|
let servers = value.servers
|
||||||
|
let serv = null
|
||||||
|
for(let i=0; i<servers.length; i++){
|
||||||
|
if(servers[i].id === serverpackid){
|
||||||
|
serv = servers[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.forge = _parseDistroModules(serv.modules, basePath, serv.mc_version)
|
||||||
|
//Correct our workaround here.
|
||||||
|
let decompressqueue = instance.forge.callback
|
||||||
|
instance.forge.callback = function(asset){
|
||||||
|
if(asset.to.toLowerCase().endsWith('.pack.xz')){
|
||||||
|
_extractPackXZ([asset.to])
|
||||||
|
}
|
||||||
|
if(asset.type === 'forge-hosted' || asset.type === 'forge'){
|
||||||
|
_finalizeForgeAsset(asset, basePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instance.totaldlsize += instance.forge.dlsize*1
|
||||||
|
fulfill()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO The distro index should be downloaded in the 'pre-loader'. This is because
|
||||||
|
//we will eventually NEED the index to generate the server list on the ui.
|
||||||
|
function _chainValidateDistributionIndex(basePath){
|
||||||
|
return new Promise(function(fulfill, reject){
|
||||||
|
//const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
|
||||||
|
const targetFile = path.join(basePath, 'westeroscraft.json')
|
||||||
|
|
||||||
|
//TEMP WORKAROUND TO TEST WHILE THIS IS NOT HOSTED
|
||||||
|
fs.readFile(path.join(basePath, '..', 'app', 'assets', 'westeroscraft.json'), 'utf-8', (err, data) => {
|
||||||
|
fulfill(JSON.parse(data))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function _parseDistroModules(modules, basePath, version){
|
||||||
|
let alist = []
|
||||||
|
let asize = 0;
|
||||||
|
//This may be removed soon, considering the most efficient way to extract.
|
||||||
|
let decompressqueue = []
|
||||||
|
for(let i=0; i<modules.length; i++){
|
||||||
|
let ob = modules[i]
|
||||||
|
let obType = ob.type
|
||||||
|
let obArtifact = ob.artifact
|
||||||
|
let obPath = obArtifact.path == null ? _resolvePath(ob.id, obArtifact.extension) : obArtifact.path
|
||||||
|
switch(obType){
|
||||||
|
case 'forge-hosted':
|
||||||
|
case 'forge':
|
||||||
|
obPath = path.join(basePath, 'libraries', obPath)
|
||||||
|
break
|
||||||
|
case 'library':
|
||||||
|
obPath = path.join(basePath, 'libraries', obPath)
|
||||||
|
break
|
||||||
|
case 'forgemod':
|
||||||
|
obPath = path.join(basePath, 'mods', obPath)
|
||||||
|
break
|
||||||
|
case 'litemod':
|
||||||
|
obPath = path.join(basePath, 'mods', version, obPath)
|
||||||
|
break
|
||||||
|
case 'file':
|
||||||
|
default:
|
||||||
|
obPath = path.join(basePath, obPath)
|
||||||
|
}
|
||||||
|
let artifact = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, obType)
|
||||||
|
if(obPath.toLowerCase().endsWith('.pack.xz')){
|
||||||
|
if(!_validateLocal(obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')), 'MD5', artifact.hash)){
|
||||||
|
asize += artifact.size*1
|
||||||
|
alist.push(artifact)
|
||||||
|
decompressqueue.push(obPath)
|
||||||
|
}
|
||||||
|
} else if(!_validateLocal(obPath, 'MD5', artifact.hash)){
|
||||||
|
asize += artifact.size*1
|
||||||
|
alist.push(artifact)
|
||||||
|
}
|
||||||
|
if(ob.sub_modules != null){
|
||||||
|
let dltrack = _parseDistroModules(ob.sub_modules, basePath, version)
|
||||||
|
asize += dltrack.dlsize*1
|
||||||
|
alist = alist.concat(dltrack.dlqueue)
|
||||||
|
decompressqueue = decompressqueue.concat(dltrack.callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new DLTracker(alist, asize, decompressqueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadForgeData(serverpack, basePath){
|
||||||
|
return new Promise(async function(fulfill, reject){
|
||||||
|
let distro = await _chainValidateDistributionIndex(basePath)
|
||||||
|
|
||||||
|
const servers = distro.servers
|
||||||
|
let serv = null
|
||||||
|
for(let i=0; i<servers.length; i++){
|
||||||
|
if(servers[i].id === serverpack){
|
||||||
|
serv = servers[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modules = serv.modules
|
||||||
|
for(let i=0; i<modules.length; i++){
|
||||||
|
const ob = modules[i]
|
||||||
|
if(ob.type === 'forge-hosted' || ob.type === 'forge'){
|
||||||
|
let obArtifact = ob.artifact
|
||||||
|
let obPath = obArtifact.path == null ? path.join(basePath, 'libraries', _resolvePath(ob.id, obArtifact.extension)) : obArtifact.path
|
||||||
|
let asset = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, ob.type)
|
||||||
|
let forgeData = await _finalizeForgeAsset(asset, basePath)
|
||||||
|
fulfill(forgeData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reject('No forge module found!')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function _parseForgeLibraries(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will initiate the download processed for the specified identifiers. If no argument is
|
||||||
|
* given, all identifiers will be initiated. Note that in order for files to be processed you need to run
|
||||||
|
* the processing function corresponding to that identifier. If you run this function without processing
|
||||||
|
* the files, it is likely nothing will be enqueued in the global object and processing will complete
|
||||||
|
* immediately. Once all downloads are complete, this function will fire the 'dlcomplete' event on the
|
||||||
|
* global object instance.
|
||||||
|
*
|
||||||
|
* @param {Array.<{id: string, limit: number}>} identifiers - optional. The identifiers to process and corresponding parallel async task limit.
|
||||||
|
*/
|
||||||
|
function processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){
|
||||||
|
this.progress = 0;
|
||||||
|
let win = remote.getCurrentWindow()
|
||||||
|
|
||||||
|
let shouldFire = true
|
||||||
|
|
||||||
|
for(let i=0; i<identifiers.length; i++){
|
||||||
|
let iden = identifiers[i]
|
||||||
|
let r = startAsyncProcess(iden.id, iden.limit)
|
||||||
|
if(r) shouldFire = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if(shouldFire){
|
||||||
|
instance.emit('dlcomplete')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadVersionData,
|
||||||
|
loadForgeData,
|
||||||
|
validateAssets,
|
||||||
|
validateLibraries,
|
||||||
|
validateMiscellaneous,
|
||||||
|
validateDistribution,
|
||||||
|
processDlQueues,
|
||||||
|
instance,
|
||||||
|
Asset,
|
||||||
|
Library,
|
||||||
|
_resolvePath
|
||||||
|
}
|
@ -1,447 +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('helios-core')
|
|
||||||
const { RestResponseStatus } = require('helios-core/common')
|
|
||||||
const { MojangRestAPI, MojangErrorCode } = require('helios-core/mojang')
|
|
||||||
const { MicrosoftAuth, MicrosoftErrorCode } = require('helios-core/microsoft')
|
|
||||||
const { AZURE_CLIENT_ID } = require('./ipcconstants')
|
|
||||||
const Lang = require('./langloader')
|
|
||||||
|
|
||||||
const log = LoggerUtil.getLogger('AuthManager')
|
|
||||||
|
|
||||||
// Error messages
|
|
||||||
|
|
||||||
function microsoftErrorDisplayable(errorCode) {
|
|
||||||
switch (errorCode) {
|
|
||||||
case MicrosoftErrorCode.NO_PROFILE:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.microsoft.error.noProfileTitle'),
|
|
||||||
desc: Lang.queryJS('auth.microsoft.error.noProfileDesc')
|
|
||||||
}
|
|
||||||
case MicrosoftErrorCode.NO_XBOX_ACCOUNT:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.microsoft.error.noXboxAccountTitle'),
|
|
||||||
desc: Lang.queryJS('auth.microsoft.error.noXboxAccountDesc')
|
|
||||||
}
|
|
||||||
case MicrosoftErrorCode.XBL_BANNED:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.microsoft.error.xblBannedTitle'),
|
|
||||||
desc: Lang.queryJS('auth.microsoft.error.xblBannedDesc')
|
|
||||||
}
|
|
||||||
case MicrosoftErrorCode.UNDER_18:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.microsoft.error.under18Title'),
|
|
||||||
desc: Lang.queryJS('auth.microsoft.error.under18Desc')
|
|
||||||
}
|
|
||||||
case MicrosoftErrorCode.UNKNOWN:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.microsoft.error.unknownTitle'),
|
|
||||||
desc: Lang.queryJS('auth.microsoft.error.unknownDesc')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mojangErrorDisplayable(errorCode) {
|
|
||||||
switch(errorCode) {
|
|
||||||
case MojangErrorCode.ERROR_METHOD_NOT_ALLOWED:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.methodNotAllowedTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.methodNotAllowedDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_NOT_FOUND:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.notFoundTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.notFoundDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_USER_MIGRATED:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.accountMigratedTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.accountMigratedDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_INVALID_CREDENTIALS:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.invalidCredentialsTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.invalidCredentialsDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_RATELIMIT:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.tooManyAttemptsTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.tooManyAttemptsDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_INVALID_TOKEN:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.invalidTokenTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.invalidTokenDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.tokenHasProfileTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.tokenHasProfileDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_CREDENTIALS_MISSING:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.credentialsMissingTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.credentialsMissingDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_INVALID_SALT_VERSION:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.invalidSaltVersionTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.invalidSaltVersionDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_GONE:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.accountGoneTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.accountGoneDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_UNREACHABLE:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.unreachableTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.unreachableDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.ERROR_NOT_PAID:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.gameNotPurchasedTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.gameNotPurchasedDesc')
|
|
||||||
}
|
|
||||||
case MojangErrorCode.UNKNOWN:
|
|
||||||
return {
|
|
||||||
title: Lang.queryJS('auth.mojang.error.unknownErrorTitle'),
|
|
||||||
desc: Lang.queryJS('auth.mojang.error.unknownErrorDesc')
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown error code: ${errorCode}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a Mojang 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.addMojangAccount = async function(username, password) {
|
|
||||||
try {
|
|
||||||
const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
|
|
||||||
console.log(response)
|
|
||||||
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
|
||||||
|
|
||||||
const session = response.data
|
|
||||||
if(session.selectedProfile != null){
|
|
||||||
const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
|
|
||||||
if(ConfigManager.getClientToken() == null){
|
|
||||||
ConfigManager.setClientToken(session.clientToken)
|
|
||||||
}
|
|
||||||
ConfigManager.save()
|
|
||||||
return ret
|
|
||||||
} else {
|
|
||||||
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID))
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err){
|
|
||||||
log.error(err)
|
|
||||||
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.addOfflineAccount = async function(username) {
|
|
||||||
try {
|
|
||||||
const ret = ConfigManager.addOfflineAccount(username)
|
|
||||||
ConfigManager.save()
|
|
||||||
return ret
|
|
||||||
} catch (err){
|
|
||||||
log.error(err)
|
|
||||||
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform the full MS Auth flow in a given mode.
|
|
||||||
*
|
|
||||||
* AUTH_MODE.FULL = Full authorization for a new account.
|
|
||||||
* AUTH_MODE.MS_REFRESH = Full refresh authorization.
|
|
||||||
* AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token.
|
|
||||||
*
|
|
||||||
* @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken
|
|
||||||
* @param {*} authMode The auth mode.
|
|
||||||
* @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH.
|
|
||||||
*/
|
|
||||||
async function fullMicrosoftAuthFlow(entryCode, authMode) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
let accessTokenRaw
|
|
||||||
let accessToken
|
|
||||||
if(authMode !== AUTH_MODE.MC_REFRESH) {
|
|
||||||
const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID)
|
|
||||||
if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) {
|
|
||||||
return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode))
|
|
||||||
}
|
|
||||||
accessToken = accessTokenResponse.data
|
|
||||||
accessTokenRaw = accessToken.access_token
|
|
||||||
} else {
|
|
||||||
accessTokenRaw = entryCode
|
|
||||||
}
|
|
||||||
|
|
||||||
const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw)
|
|
||||||
if(xblResponse.responseStatus === RestResponseStatus.ERROR) {
|
|
||||||
return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode))
|
|
||||||
}
|
|
||||||
const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data)
|
|
||||||
if(xstsResonse.responseStatus === RestResponseStatus.ERROR) {
|
|
||||||
return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode))
|
|
||||||
}
|
|
||||||
const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data)
|
|
||||||
if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) {
|
|
||||||
return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode))
|
|
||||||
}
|
|
||||||
const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token)
|
|
||||||
if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) {
|
|
||||||
return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode))
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
accessToken,
|
|
||||||
accessTokenRaw,
|
|
||||||
xbl: xblResponse.data,
|
|
||||||
xsts: xstsResonse.data,
|
|
||||||
mcToken: mcTokenResponse.data,
|
|
||||||
mcProfile: mcProfileResponse.data
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
log.error(err)
|
|
||||||
return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the expiry date. Advance the expiry time by 10 seconds
|
|
||||||
* to reduce the liklihood of working with an expired token.
|
|
||||||
*
|
|
||||||
* @param {number} nowMs Current time milliseconds.
|
|
||||||
* @param {number} epiresInS Expires in (seconds)
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function calculateExpiryDate(nowMs, epiresInS) {
|
|
||||||
return nowMs + ((epiresInS-10)*1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow.
|
|
||||||
* The resultant data will be stored as an auth account in the configuration database.
|
|
||||||
*
|
|
||||||
* @param {string} authCode The authCode obtained from microsoft.
|
|
||||||
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
|
||||||
*/
|
|
||||||
exports.addMicrosoftAccount = async function(authCode) {
|
|
||||||
|
|
||||||
const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL)
|
|
||||||
|
|
||||||
// Advance expiry by 10 seconds to avoid close calls.
|
|
||||||
const now = new Date().getTime()
|
|
||||||
|
|
||||||
const ret = ConfigManager.addMicrosoftAuthAccount(
|
|
||||||
fullAuth.mcProfile.id,
|
|
||||||
fullAuth.mcToken.access_token,
|
|
||||||
fullAuth.mcProfile.name,
|
|
||||||
calculateExpiryDate(now, fullAuth.mcToken.expires_in),
|
|
||||||
fullAuth.accessToken.access_token,
|
|
||||||
fullAuth.accessToken.refresh_token,
|
|
||||||
calculateExpiryDate(now, fullAuth.accessToken.expires_in)
|
|
||||||
)
|
|
||||||
ConfigManager.save()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a Mojang 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.removeMojangAccount = async function(uuid){
|
|
||||||
try {
|
|
||||||
const authAcc = ConfigManager.getAuthAccount(uuid)
|
|
||||||
const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
|
|
||||||
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
|
||||||
ConfigManager.removeAuthAccount(uuid)
|
|
||||||
ConfigManager.save()
|
|
||||||
return Promise.resolve()
|
|
||||||
} else {
|
|
||||||
log.error('Error while removing account', response.error)
|
|
||||||
return Promise.reject(response.error)
|
|
||||||
}
|
|
||||||
} catch (err){
|
|
||||||
log.error('Error while removing account', err)
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout
|
|
||||||
* through the ipc renderer.
|
|
||||||
*
|
|
||||||
* @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.removeMicrosoftAccount = async function(uuid){
|
|
||||||
try {
|
|
||||||
ConfigManager.removeAuthAccount(uuid)
|
|
||||||
ConfigManager.save()
|
|
||||||
return Promise.resolve()
|
|
||||||
} catch (err){
|
|
||||||
log.error('Error while removing account', err)
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.removeOfflineAccount = async function(uuid){
|
|
||||||
try {
|
|
||||||
ConfigManager.removeAuthAccount(uuid)
|
|
||||||
ConfigManager.save()
|
|
||||||
return Promise.resolve()
|
|
||||||
} catch (err){
|
|
||||||
log.error('Error while removing account', 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.
|
|
||||||
*
|
|
||||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
|
||||||
* otherwise false.
|
|
||||||
*/
|
|
||||||
async function validateSelectedMojangAccount(){
|
|
||||||
const current = ConfigManager.getSelectedAccount()
|
|
||||||
const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
|
|
||||||
|
|
||||||
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
|
||||||
const isValid = response.data
|
|
||||||
if(!isValid){
|
|
||||||
const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
|
|
||||||
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
|
|
||||||
const session = refreshResponse.data
|
|
||||||
ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
|
|
||||||
ConfigManager.save()
|
|
||||||
} else {
|
|
||||||
log.error('Error while validating selected profile:', refreshResponse.error)
|
|
||||||
log.info('Account access token is invalid.')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log.info('Account access token validated.')
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
log.info('Account access token validated.')
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the selected account with Microsoft'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.
|
|
||||||
*
|
|
||||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
|
||||||
* otherwise false.
|
|
||||||
*/
|
|
||||||
async function validateSelectedMicrosoftAccount(){
|
|
||||||
const current = ConfigManager.getSelectedAccount()
|
|
||||||
const now = new Date().getTime()
|
|
||||||
const mcExpiresAt = current.expiresAt
|
|
||||||
const mcExpired = now >= mcExpiresAt
|
|
||||||
|
|
||||||
if(!mcExpired) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MC token expired. Check MS token.
|
|
||||||
|
|
||||||
const msExpiresAt = current.microsoft.expires_at
|
|
||||||
const msExpired = now >= msExpiresAt
|
|
||||||
|
|
||||||
if(msExpired) {
|
|
||||||
// MS expired, do full refresh.
|
|
||||||
try {
|
|
||||||
const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH)
|
|
||||||
|
|
||||||
ConfigManager.updateMicrosoftAuthAccount(
|
|
||||||
current.uuid,
|
|
||||||
res.mcToken.access_token,
|
|
||||||
res.accessToken.access_token,
|
|
||||||
res.accessToken.refresh_token,
|
|
||||||
calculateExpiryDate(now, res.accessToken.expires_in),
|
|
||||||
calculateExpiryDate(now, res.mcToken.expires_in)
|
|
||||||
)
|
|
||||||
ConfigManager.save()
|
|
||||||
return true
|
|
||||||
} catch(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Only MC expired, use existing MS token.
|
|
||||||
try {
|
|
||||||
const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH)
|
|
||||||
|
|
||||||
ConfigManager.updateMicrosoftAuthAccount(
|
|
||||||
current.uuid,
|
|
||||||
res.mcToken.access_token,
|
|
||||||
current.microsoft.access_token,
|
|
||||||
current.microsoft.refresh_token,
|
|
||||||
current.microsoft.expires_at,
|
|
||||||
calculateExpiryDate(now, res.mcToken.expires_in)
|
|
||||||
)
|
|
||||||
ConfigManager.save()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the selected auth account.
|
|
||||||
*
|
|
||||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
|
||||||
* otherwise false.
|
|
||||||
*/
|
|
||||||
exports.validateSelected = async function(){
|
|
||||||
const current = ConfigManager.getSelectedAccount()
|
|
||||||
|
|
||||||
if(current.type === 'microsoft') {
|
|
||||||
return await validateSelectedMicrosoftAccount()
|
|
||||||
} else {
|
|
||||||
return await validateSelectedMojangAccount()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,808 +0,0 @@
|
|||||||
const fs = require('fs-extra')
|
|
||||||
const { LoggerUtil } = require('helios-core')
|
|
||||||
const { randomUUID } = require('crypto')
|
|
||||||
const os = require('os')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const logger = LoggerUtil.getLogger('ConfigManager')
|
|
||||||
|
|
||||||
const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME)
|
|
||||||
|
|
||||||
const dataPath = path.join(sysRoot, '.helioslauncher')
|
|
||||||
|
|
||||||
const launcherDir = 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(ram){
|
|
||||||
if(ram?.minimum != null) {
|
|
||||||
return ram.minimum/1024
|
|
||||||
} else {
|
|
||||||
// Legacy behavior
|
|
||||||
const mem = os.totalmem()
|
|
||||||
return mem >= (6*1073741824) ? 3 : 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getAbsoluteMaxRAM = function(ram){
|
|
||||||
const mem = os.totalmem()
|
|
||||||
const gT16 = mem-(16*1073741824)
|
|
||||||
return Math.floor((mem-(gT16 > 0 ? (Number.parseInt(gT16/8) + (16*1073741824)/4) : mem/4))/1073741824)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSelectedRAM(ram) {
|
|
||||||
if(ram?.recommended != null) {
|
|
||||||
return `${ram.recommended}M`
|
|
||||||
} else {
|
|
||||||
// Legacy behavior
|
|
||||||
const mem = os.totalmem()
|
|
||||||
return mem >= (8*1073741824) ? '4G' : (mem >= (6*1073741824) ? '3G' : '2G')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Three types of values:
|
|
||||||
* Static = Explicitly declared.
|
|
||||||
* Dynamic = Calculated by a private function.
|
|
||||||
* Resolved = Resolved externally, defaults to null.
|
|
||||||
*/
|
|
||||||
const DEFAULT_CONFIG = {
|
|
||||||
settings: {
|
|
||||||
game: {
|
|
||||||
resWidth: 1280,
|
|
||||||
resHeight: 720,
|
|
||||||
fullscreen: false,
|
|
||||||
autoConnect: true,
|
|
||||||
launchDetached: true
|
|
||||||
},
|
|
||||||
launcher: {
|
|
||||||
allowPrerelease: false,
|
|
||||||
dataDirectory: dataPath
|
|
||||||
}
|
|
||||||
},
|
|
||||||
newsCache: {
|
|
||||||
date: null,
|
|
||||||
content: null,
|
|
||||||
dismissed: false
|
|
||||||
},
|
|
||||||
clientToken: null,
|
|
||||||
selectedServer: null, // Resolved
|
|
||||||
selectedAccount: null,
|
|
||||||
authenticationDatabase: {},
|
|
||||||
modConfigurations: [],
|
|
||||||
javaConfig: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = null
|
|
||||||
|
|
||||||
// Persistance Utility Functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the current configuration to a file.
|
|
||||||
*/
|
|
||||||
exports.save = function(){
|
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the configuration into memory. If a configuration file exists,
|
|
||||||
* that will be read and saved. Otherwise, a default configuration will
|
|
||||||
* be generated. Note that "resolved" values default to null and will
|
|
||||||
* need to be externally assigned.
|
|
||||||
*/
|
|
||||||
exports.load = function(){
|
|
||||||
let doLoad = true
|
|
||||||
|
|
||||||
if(!fs.existsSync(configPath)){
|
|
||||||
// Create all parent directories.
|
|
||||||
fs.ensureDirSync(path.join(configPath, '..'))
|
|
||||||
if(fs.existsSync(configPathLEGACY)){
|
|
||||||
fs.moveSync(configPathLEGACY, configPath)
|
|
||||||
} else {
|
|
||||||
doLoad = false
|
|
||||||
config = DEFAULT_CONFIG
|
|
||||||
exports.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(doLoad){
|
|
||||||
let doValidate = false
|
|
||||||
try {
|
|
||||||
config = JSON.parse(fs.readFileSync(configPath, 'UTF-8'))
|
|
||||||
doValidate = true
|
|
||||||
} catch (err){
|
|
||||||
logger.error(err)
|
|
||||||
logger.info('Configuration file contains malformed JSON or is corrupt.')
|
|
||||||
logger.info('Generating a new configuration file.')
|
|
||||||
fs.ensureDirSync(path.join(configPath, '..'))
|
|
||||||
config = DEFAULT_CONFIG
|
|
||||||
exports.save()
|
|
||||||
}
|
|
||||||
if(doValidate){
|
|
||||||
config = validateKeySet(DEFAULT_CONFIG, config)
|
|
||||||
exports.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info('Successfully Loaded')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean} Whether or not the manager has been loaded.
|
|
||||||
*/
|
|
||||||
exports.isLoaded = function(){
|
|
||||||
return config != null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that the destination object has at least every field
|
|
||||||
* present in the source object. Assign a default value otherwise.
|
|
||||||
*
|
|
||||||
* @param {Object} srcObj The source object to reference against.
|
|
||||||
* @param {Object} destObj The destination object.
|
|
||||||
* @returns {Object} A validated destination object.
|
|
||||||
*/
|
|
||||||
function validateKeySet(srcObj, destObj){
|
|
||||||
if(srcObj == null){
|
|
||||||
srcObj = {}
|
|
||||||
}
|
|
||||||
const validationBlacklist = ['authenticationDatabase', 'javaConfig']
|
|
||||||
const keys = Object.keys(srcObj)
|
|
||||||
for(let i=0; i<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 mojang account.
|
|
||||||
*
|
|
||||||
* @param {string} uuid The uuid of the authenticated account.
|
|
||||||
* @param {string} accessToken The new Access Token.
|
|
||||||
*
|
|
||||||
* @returns {Object} The authenticated account object created by this action.
|
|
||||||
*/
|
|
||||||
exports.updateMojangAuthAccount = function(uuid, accessToken){
|
|
||||||
config.authenticationDatabase[uuid].accessToken = accessToken
|
|
||||||
config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion.
|
|
||||||
return config.authenticationDatabase[uuid]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an authenticated mojang account to the database to be stored.
|
|
||||||
*
|
|
||||||
* @param {string} uuid The uuid of the authenticated account.
|
|
||||||
* @param {string} accessToken The accessToken of the authenticated account.
|
|
||||||
* @param {string} username The username (usually email) of the authenticated account.
|
|
||||||
* @param {string} displayName The in game name of the authenticated account.
|
|
||||||
*
|
|
||||||
* @returns {Object} The authenticated account object created by this action.
|
|
||||||
*/
|
|
||||||
exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
|
|
||||||
config.selectedAccount = uuid
|
|
||||||
config.authenticationDatabase[uuid] = {
|
|
||||||
type: 'mojang',
|
|
||||||
accessToken,
|
|
||||||
username: username.trim(),
|
|
||||||
uuid: uuid.trim(),
|
|
||||||
displayName: displayName.trim()
|
|
||||||
}
|
|
||||||
return config.authenticationDatabase[uuid]
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.addOfflineAccount = function(username){
|
|
||||||
console.log("Yeah I try to create new offline account")
|
|
||||||
uuid = randomUUID()
|
|
||||||
config.selectedAccount = uuid
|
|
||||||
config.authenticationDatabase[uuid] = {
|
|
||||||
type: 'offline',
|
|
||||||
password: '',
|
|
||||||
username: username.trim(),
|
|
||||||
uuid: uuid.trim(),
|
|
||||||
displayName: username.trim()
|
|
||||||
}
|
|
||||||
return config.authenticationDatabase[uuid]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the tokens of an authenticated microsoft account.
|
|
||||||
*
|
|
||||||
* @param {string} uuid The uuid of the authenticated account.
|
|
||||||
* @param {string} accessToken The new Access Token.
|
|
||||||
* @param {string} msAccessToken The new Microsoft Access Token
|
|
||||||
* @param {string} msRefreshToken The new Microsoft Refresh Token
|
|
||||||
* @param {date} msExpires The date when the microsoft access token expires
|
|
||||||
* @param {date} mcExpires The date when the mojang access token expires
|
|
||||||
*
|
|
||||||
* @returns {Object} The authenticated account object created by this action.
|
|
||||||
*/
|
|
||||||
exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) {
|
|
||||||
config.authenticationDatabase[uuid].accessToken = accessToken
|
|
||||||
config.authenticationDatabase[uuid].expiresAt = mcExpires
|
|
||||||
config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken
|
|
||||||
config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken
|
|
||||||
config.authenticationDatabase[uuid].microsoft.expires_at = msExpires
|
|
||||||
return config.authenticationDatabase[uuid]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an authenticated microsoft account to the database to be stored.
|
|
||||||
*
|
|
||||||
* @param {string} uuid The uuid of the authenticated account.
|
|
||||||
* @param {string} accessToken The accessToken of the authenticated account.
|
|
||||||
* @param {string} name The in game name of the authenticated account.
|
|
||||||
* @param {date} mcExpires The date when the mojang access token expires
|
|
||||||
* @param {string} msAccessToken The microsoft access token
|
|
||||||
* @param {string} msRefreshToken The microsoft refresh token
|
|
||||||
* @param {date} msExpires The date when the microsoft access token expires
|
|
||||||
*
|
|
||||||
* @returns {Object} The authenticated account object created by this action.
|
|
||||||
*/
|
|
||||||
exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) {
|
|
||||||
config.selectedAccount = uuid
|
|
||||||
config.authenticationDatabase[uuid] = {
|
|
||||||
type: 'microsoft',
|
|
||||||
accessToken,
|
|
||||||
username: name.trim(),
|
|
||||||
uuid: uuid.trim(),
|
|
||||||
displayName: name.trim(),
|
|
||||||
expiresAt: mcExpires,
|
|
||||||
microsoft: {
|
|
||||||
access_token: msAccessToken,
|
|
||||||
refresh_token: msRefreshToken,
|
|
||||||
expires_at: msExpires
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config.authenticationDatabase[uuid]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an authenticated account from the database. If the account
|
|
||||||
* was also the selected account, a new one will be selected. If there
|
|
||||||
* are no accounts, the selected account will be null.
|
|
||||||
*
|
|
||||||
* @param {string} uuid The uuid of the authenticated account.
|
|
||||||
*
|
|
||||||
* @returns {boolean} True if the account was removed, false if it never existed.
|
|
||||||
*/
|
|
||||||
exports.removeAuthAccount = function(uuid){
|
|
||||||
if(config.authenticationDatabase[uuid] != null){
|
|
||||||
delete config.authenticationDatabase[uuid]
|
|
||||||
if(config.selectedAccount === uuid){
|
|
||||||
const keys = Object.keys(config.authenticationDatabase)
|
|
||||||
if(keys.length > 0){
|
|
||||||
config.selectedAccount = keys[0]
|
|
||||||
} else {
|
|
||||||
config.selectedAccount = null
|
|
||||||
config.clientToken = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the currently selected authenticated account.
|
|
||||||
*
|
|
||||||
* @returns {Object} The selected authenticated account.
|
|
||||||
*/
|
|
||||||
exports.getSelectedAccount = function(){
|
|
||||||
return config.authenticationDatabase[config.selectedAccount]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the selected authenticated account.
|
|
||||||
*
|
|
||||||
* @param {string} uuid The UUID of the account which is to be set
|
|
||||||
* as the selected account.
|
|
||||||
*
|
|
||||||
* @returns {Object} The selected authenticated account.
|
|
||||||
*/
|
|
||||||
exports.setSelectedAccount = function(uuid){
|
|
||||||
const authAcc = config.authenticationDatabase[uuid]
|
|
||||||
if(authAcc != null) {
|
|
||||||
config.selectedAccount = uuid
|
|
||||||
}
|
|
||||||
return authAcc
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an array of each mod configuration currently stored.
|
|
||||||
*
|
|
||||||
* @returns {Array.<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
|
|
||||||
|
|
||||||
function defaultJavaConfig(effectiveJavaOptions, ram) {
|
|
||||||
if(effectiveJavaOptions.suggestedMajor > 8) {
|
|
||||||
return defaultJavaConfig17(ram)
|
|
||||||
} else {
|
|
||||||
return defaultJavaConfig8(ram)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultJavaConfig8(ram) {
|
|
||||||
return {
|
|
||||||
minRAM: resolveSelectedRAM(ram),
|
|
||||||
maxRAM: resolveSelectedRAM(ram),
|
|
||||||
executable: null,
|
|
||||||
jvmOptions: [
|
|
||||||
'-XX:+UseConcMarkSweepGC',
|
|
||||||
'-XX:+CMSIncrementalMode',
|
|
||||||
'-XX:-UseAdaptiveSizePolicy',
|
|
||||||
'-Xmn128M'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultJavaConfig17(ram) {
|
|
||||||
return {
|
|
||||||
minRAM: resolveSelectedRAM(ram),
|
|
||||||
maxRAM: resolveSelectedRAM(ram),
|
|
||||||
executable: null,
|
|
||||||
jvmOptions: [
|
|
||||||
'-XX:+UnlockExperimentalVMOptions',
|
|
||||||
'-XX:+UseG1GC',
|
|
||||||
'-XX:G1NewSizePercent=20',
|
|
||||||
'-XX:G1ReservePercent=20',
|
|
||||||
'-XX:MaxGCPauseMillis=50',
|
|
||||||
'-XX:G1HeapRegionSize=32M'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure a java config property is set for the given server.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @param {*} mcVersion The minecraft version of the server.
|
|
||||||
*/
|
|
||||||
exports.ensureJavaConfig = function(serverid, effectiveJavaOptions, ram) {
|
|
||||||
if(!Object.prototype.hasOwnProperty.call(config.javaConfig, serverid)) {
|
|
||||||
config.javaConfig[serverid] = defaultJavaConfig(effectiveJavaOptions, ram)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the minimum amount of memory for JVM initialization. This value
|
|
||||||
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
|
||||||
* 1024 MegaBytes, etc.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @returns {string} The minimum amount of memory for JVM initialization.
|
|
||||||
*/
|
|
||||||
exports.getMinRAM = function(serverid){
|
|
||||||
return config.javaConfig[serverid].minRAM
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the minimum amount of memory for JVM initialization. This value should
|
|
||||||
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
|
||||||
* 1024 MegaBytes, etc.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @param {string} minRAM The new minimum amount of memory for JVM initialization.
|
|
||||||
*/
|
|
||||||
exports.setMinRAM = function(serverid, minRAM){
|
|
||||||
config.javaConfig[serverid].minRAM = minRAM
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the maximum amount of memory for JVM initialization. This value
|
|
||||||
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
|
||||||
* 1024 MegaBytes, etc.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @returns {string} The maximum amount of memory for JVM initialization.
|
|
||||||
*/
|
|
||||||
exports.getMaxRAM = function(serverid){
|
|
||||||
return config.javaConfig[serverid].maxRAM
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the maximum amount of memory for JVM initialization. This value should
|
|
||||||
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
|
||||||
* 1024 MegaBytes, etc.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @param {string} maxRAM The new maximum amount of memory for JVM initialization.
|
|
||||||
*/
|
|
||||||
exports.setMaxRAM = function(serverid, maxRAM){
|
|
||||||
config.javaConfig[serverid].maxRAM = maxRAM
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the path of the Java Executable.
|
|
||||||
*
|
|
||||||
* This is a resolved configuration value and defaults to null until externally assigned.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @returns {string} The path of the Java Executable.
|
|
||||||
*/
|
|
||||||
exports.getJavaExecutable = function(serverid){
|
|
||||||
return config.javaConfig[serverid].executable
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the path of the Java Executable.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @param {string} executable The new path of the Java Executable.
|
|
||||||
*/
|
|
||||||
exports.setJavaExecutable = function(serverid, executable){
|
|
||||||
config.javaConfig[serverid].executable = executable
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the additional arguments for JVM initialization. Required arguments,
|
|
||||||
* such as memory allocation, will be dynamically resolved and will not be included
|
|
||||||
* in this value.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @returns {Array.<string>} An array of the additional arguments for JVM initialization.
|
|
||||||
*/
|
|
||||||
exports.getJVMOptions = function(serverid){
|
|
||||||
return config.javaConfig[serverid].jvmOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the additional arguments for JVM initialization. Required arguments,
|
|
||||||
* such as memory allocation, will be dynamically resolved and should not be
|
|
||||||
* included in this value.
|
|
||||||
*
|
|
||||||
* @param {string} serverid The server id.
|
|
||||||
* @param {Array.<string>} jvmOptions An array of the new additional arguments for JVM
|
|
||||||
* initialization.
|
|
||||||
*/
|
|
||||||
exports.setJVMOptions = function(serverid, jvmOptions){
|
|
||||||
config.javaConfig[serverid].jvmOptions = jvmOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game Settings
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the width of the game window.
|
|
||||||
*
|
|
||||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
|
||||||
* @returns {number} The width of the game window.
|
|
||||||
*/
|
|
||||||
exports.getGameWidth = function(def = false){
|
|
||||||
return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the width of the game window.
|
|
||||||
*
|
|
||||||
* @param {number} resWidth The new width of the game window.
|
|
||||||
*/
|
|
||||||
exports.setGameWidth = function(resWidth){
|
|
||||||
config.settings.game.resWidth = Number.parseInt(resWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a potential new width value.
|
|
||||||
*
|
|
||||||
* @param {number} resWidth The width value to validate.
|
|
||||||
* @returns {boolean} Whether or not the value is valid.
|
|
||||||
*/
|
|
||||||
exports.validateGameWidth = function(resWidth){
|
|
||||||
const nVal = Number.parseInt(resWidth)
|
|
||||||
return Number.isInteger(nVal) && nVal >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the height of the game window.
|
|
||||||
*
|
|
||||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
|
||||||
* @returns {number} The height of the game window.
|
|
||||||
*/
|
|
||||||
exports.getGameHeight = function(def = false){
|
|
||||||
return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the height of the game window.
|
|
||||||
*
|
|
||||||
* @param {number} resHeight The new height of the game window.
|
|
||||||
*/
|
|
||||||
exports.setGameHeight = function(resHeight){
|
|
||||||
config.settings.game.resHeight = Number.parseInt(resHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a potential new height value.
|
|
||||||
*
|
|
||||||
* @param {number} resHeight The height value to validate.
|
|
||||||
* @returns {boolean} Whether or not the value is valid.
|
|
||||||
*/
|
|
||||||
exports.validateGameHeight = function(resHeight){
|
|
||||||
const nVal = Number.parseInt(resHeight)
|
|
||||||
return Number.isInteger(nVal) && nVal >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the game should be launched in fullscreen mode.
|
|
||||||
*
|
|
||||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
|
||||||
* @returns {boolean} Whether or not the game is set to launch in fullscreen mode.
|
|
||||||
*/
|
|
||||||
exports.getFullscreen = function(def = false){
|
|
||||||
return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the status of if the game should be launched in fullscreen mode.
|
|
||||||
*
|
|
||||||
* @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode.
|
|
||||||
*/
|
|
||||||
exports.setFullscreen = function(fullscreen){
|
|
||||||
config.settings.game.fullscreen = fullscreen
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the game should auto connect to servers.
|
|
||||||
*
|
|
||||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
|
||||||
* @returns {boolean} Whether or not the game should auto connect to servers.
|
|
||||||
*/
|
|
||||||
exports.getAutoConnect = function(def = false){
|
|
||||||
return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the status of whether or not the game should auto connect to servers.
|
|
||||||
*
|
|
||||||
* @param {boolean} autoConnect Whether or not the game should auto connect to servers.
|
|
||||||
*/
|
|
||||||
exports.setAutoConnect = function(autoConnect){
|
|
||||||
config.settings.game.autoConnect = autoConnect
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the game should launch as a detached process.
|
|
||||||
*
|
|
||||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
|
||||||
* @returns {boolean} Whether or not the game will launch as a detached process.
|
|
||||||
*/
|
|
||||||
exports.getLaunchDetached = function(def = false){
|
|
||||||
return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the status of whether or not the game should launch as a detached process.
|
|
||||||
*
|
|
||||||
* @param {boolean} launchDetached Whether or not the game should launch as a detached process.
|
|
||||||
*/
|
|
||||||
exports.setLaunchDetached = function(launchDetached){
|
|
||||||
config.settings.game.launchDetached = launchDetached
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launcher Settings
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the launcher should download prerelease versions.
|
|
||||||
*
|
|
||||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
|
||||||
* @returns {boolean} Whether or not the launcher should download prerelease versions.
|
|
||||||
*/
|
|
||||||
exports.getAllowPrerelease = function(def = false){
|
|
||||||
return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the status of Whether or not the launcher should download prerelease versions.
|
|
||||||
*
|
|
||||||
* @param {boolean} launchDetached Whether or not the launcher should download prerelease versions.
|
|
||||||
*/
|
|
||||||
exports.setAllowPrerelease = function(allowPrerelease){
|
|
||||||
config.settings.launcher.allowPrerelease = allowPrerelease
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
// Work in progress
|
|
||||||
const { LoggerUtil } = require('helios-core')
|
|
||||||
|
|
||||||
const logger = LoggerUtil.getLogger('DiscordWrapper')
|
|
||||||
|
|
||||||
const { Client } = require('discord-rpc-patch')
|
|
||||||
|
|
||||||
const Lang = require('./langloader')
|
|
||||||
|
|
||||||
let client
|
|
||||||
let activity
|
|
||||||
|
|
||||||
exports.initRPC = function(genSettings, servSettings, initialDetails = Lang.queryJS('discord.waiting')){
|
|
||||||
client = new Client({ transport: 'ipc' })
|
|
||||||
|
|
||||||
activity = {
|
|
||||||
details: initialDetails,
|
|
||||||
state: Lang.queryJS('discord.state', {shortId: servSettings.shortId}),
|
|
||||||
largeImageKey: servSettings.largeImageKey,
|
|
||||||
largeImageText: servSettings.largeImageText,
|
|
||||||
smallImageKey: genSettings.smallImageKey,
|
|
||||||
smallImageText: genSettings.smallImageText,
|
|
||||||
startTimestamp: new Date().getTime(),
|
|
||||||
instance: false
|
|
||||||
}
|
|
||||||
|
|
||||||
client.on('ready', () => {
|
|
||||||
logger.info('Discord RPC Connected')
|
|
||||||
client.setActivity(activity)
|
|
||||||
})
|
|
||||||
|
|
||||||
client.login({clientId: genSettings.clientId}).catch(error => {
|
|
||||||
if(error.message.includes('ENOENT')) {
|
|
||||||
logger.info('Unable to initialize Discord Rich Presence, no client detected.')
|
|
||||||
} else {
|
|
||||||
logger.info('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,15 +0,0 @@
|
|||||||
const { DistributionAPI } = require('helios-core/common')
|
|
||||||
|
|
||||||
const ConfigManager = require('./configmanager')
|
|
||||||
|
|
||||||
exports.REMOTE_DISTRO_URL = 'https://git.onimai.ru/ONIMAI-SMP/distribution/raw/branch/main/distribution.json'
|
|
||||||
|
|
||||||
const api = new DistributionAPI(
|
|
||||||
ConfigManager.getLauncherDirectory(),
|
|
||||||
null, // Injected forcefully by the preloader.
|
|
||||||
null, // Injected forcefully by the preloader.
|
|
||||||
exports.REMOTE_DISTRO_URL,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
exports.DistroAPI = api
|
|
@ -1,238 +0,0 @@
|
|||||||
const fs = require('fs-extra')
|
|
||||||
const path = require('path')
|
|
||||||
const { ipcRenderer, shell } = require('electron')
|
|
||||||
const { SHELL_OPCODE } = require('./ipcconstants')
|
|
||||||
|
|
||||||
// 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 {Promise.<boolean>} True if the mod was deleted, otherwise false.
|
|
||||||
*/
|
|
||||||
exports.deleteDropinMod = async function(modsDir, fullName){
|
|
||||||
|
|
||||||
const res = await ipcRenderer.invoke(SHELL_OPCODE.TRASH_ITEM, path.join(modsDir, fullName))
|
|
||||||
|
|
||||||
if(!res.result) {
|
|
||||||
shell.beep()
|
|
||||||
console.error('Error deleting drop-in mod.', res.error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,28 +0,0 @@
|
|||||||
// NOTE FOR THIRD-PARTY
|
|
||||||
// REPLACE THIS CLIENT ID WITH YOUR APPLICATION ID.
|
|
||||||
// SEE https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md
|
|
||||||
exports.AZURE_CLIENT_ID = '58f2320b-3644-4194-9174-796e17617dd0'
|
|
||||||
// SEE NOTE ABOVE.
|
|
||||||
|
|
||||||
|
|
||||||
// Opcodes
|
|
||||||
exports.MSFT_OPCODE = {
|
|
||||||
OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN',
|
|
||||||
OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT',
|
|
||||||
REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN',
|
|
||||||
REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT'
|
|
||||||
}
|
|
||||||
// Reply types for REPLY opcode.
|
|
||||||
exports.MSFT_REPLY_TYPE = {
|
|
||||||
SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS',
|
|
||||||
ERROR: 'MSFT_AUTH_REPLY_ERROR'
|
|
||||||
}
|
|
||||||
// Error types for ERROR reply.
|
|
||||||
exports.MSFT_ERROR = {
|
|
||||||
ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN',
|
|
||||||
NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED'
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.SHELL_OPCODE = {
|
|
||||||
TRASH_ITEM: 'TRASH_ITEM'
|
|
||||||
}
|
|
@ -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,43 +0,0 @@
|
|||||||
const fs = require('fs-extra')
|
|
||||||
const path = require('path')
|
|
||||||
const toml = require('toml')
|
|
||||||
const merge = require('lodash.merge')
|
|
||||||
|
|
||||||
let lang
|
|
||||||
|
|
||||||
exports.loadLanguage = function(id){
|
|
||||||
lang = merge(lang || {}, toml.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.toml`))) || {})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.query = function(id, placeHolders){
|
|
||||||
let query = id.split('.')
|
|
||||||
let res = lang
|
|
||||||
for(let q of query){
|
|
||||||
res = res[q]
|
|
||||||
}
|
|
||||||
let text = res === lang ? '' : res
|
|
||||||
if (placeHolders) {
|
|
||||||
Object.entries(placeHolders).forEach(([key, value]) => {
|
|
||||||
text = text.replace(`{${key}}`, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.queryJS = function(id, placeHolders){
|
|
||||||
return exports.query(`js.${id}`, placeHolders)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.queryEJS = function(id, placeHolders){
|
|
||||||
return exports.query(`ejs.${id}`, placeHolders)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.setupLanguage = function(){
|
|
||||||
// Load Language Files
|
|
||||||
exports.loadLanguage('en_US')
|
|
||||||
// Uncomment this when translations are ready
|
|
||||||
//exports.loadLanguage('xx_XX')
|
|
||||||
|
|
||||||
// Load Custom Language File for Launcher Customizer
|
|
||||||
exports.loadLanguage('_custom')
|
|
||||||
}
|
|
84
app/assets/js/launchindex.json
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"id": "WesterosCraft-1.11.2",
|
||||||
|
"name": "WesterosCraft Production Client",
|
||||||
|
"news-feed": "http://www.westeroscraft.com/api/rss.php?preset_id=12700544",
|
||||||
|
"icon-url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/server-prod.png",
|
||||||
|
"revision": "0.0.1",
|
||||||
|
"server-ip": "mc.westeroscraft.com",
|
||||||
|
"mc-version": "1.11.2",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"id": "MODNAME",
|
||||||
|
"name": "Mod Name version 1.11.2",
|
||||||
|
"type": "forgemod",
|
||||||
|
"_comment": "If no required is given, it will default to true. If a def(ault) is not give, it will default to true. If required is present it always expects a value.",
|
||||||
|
"required": {
|
||||||
|
"value": false,
|
||||||
|
"def": false
|
||||||
|
},
|
||||||
|
"artifact": {
|
||||||
|
"size": 1234,
|
||||||
|
"MD5": "e71e88c744588fdad48d3b3beb4935fc",
|
||||||
|
"path": "forgemod path is appended to {basepath}/mods",
|
||||||
|
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/somemod.jar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_comment": "Forge is a special instance of library.",
|
||||||
|
"id": "net.minecraftforge.forge.forge-universal:1.11.2-13.20.0.2228",
|
||||||
|
"name": "Minecraft Forge 1.11.2-13.20.0.2228",
|
||||||
|
"type": "forge",
|
||||||
|
"artifact": {
|
||||||
|
"size": 4123353,
|
||||||
|
"MD5": "5b9105f1a8552beac0c8228203d994ae",
|
||||||
|
"path": "net/minecraftforge/forge/1.11.2-13.20.0.2228/forge-1.11.2-13.20.0.2228-universal.jar",
|
||||||
|
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.0.2228/forge-1.11.2-13.20.0.2228-universal.jar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_comment": "library path is appended to {basepath}/libraries",
|
||||||
|
"id": "net.optifine.optifine:1.11.2_HD_U_B8",
|
||||||
|
"name": "Optifine 1.11.2 HD U B8",
|
||||||
|
"type": "library",
|
||||||
|
"artifact": {
|
||||||
|
"size": 2050307,
|
||||||
|
"MD5": "c18c80f8bfa2a440cc5af4ab8816bc4b",
|
||||||
|
"path": "optifine/OptiFine/1.11.2_HD_U_B8/OptiFine-1.11.2_HD_U_B8.jar",
|
||||||
|
"url": "http://optifine.net/download.php?f=OptiFine_1.11.2_HD_U_B8.jar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chatbubbles",
|
||||||
|
"name": "Chat Bubbles 1.11.2",
|
||||||
|
"type": "litemod",
|
||||||
|
"required": {
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
"artifact": {
|
||||||
|
"size": 37838,
|
||||||
|
"MD5": "0497a93e5429b43082282e9d9119fcba",
|
||||||
|
"path": "litemod path is appended to {basepath}/mods/{mc-version}",
|
||||||
|
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/mod_chatBubbles-1.0.1_for_1.11.2.litemod"
|
||||||
|
},
|
||||||
|
"_comment": "Any module can declare submodules, even submodules.",
|
||||||
|
"sub-modules": [
|
||||||
|
{
|
||||||
|
"id": "customRegexes",
|
||||||
|
"name": "Custom Regexes for Chat Bubbles",
|
||||||
|
"type": "file",
|
||||||
|
"artifact": {
|
||||||
|
"size": 331,
|
||||||
|
"MD5": "f21b4b325f09238a3d6b2103d54351ef",
|
||||||
|
"path": "file path is appended to {basepath}",
|
||||||
|
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/customRegexes.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
208
app/assets/js/launchprocess.js
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
const mojang = require('mojang')
|
||||||
|
const uuidV4 = require('uuid/v4')
|
||||||
|
const path = require('path')
|
||||||
|
const child_process = require('child_process')
|
||||||
|
const ag = require('./assetguard.js')
|
||||||
|
const AdmZip = require('adm-zip')
|
||||||
|
const fs = require('fs')
|
||||||
|
const mkpath = require('mkdirp');
|
||||||
|
|
||||||
|
function launchMinecraft(versionData, forgeData, basePath){
|
||||||
|
const authPromise = mojang.auth('EMAIL', 'PASS', uuidV4(), {
|
||||||
|
name: 'Minecraft',
|
||||||
|
version: 1
|
||||||
|
})
|
||||||
|
authPromise.then(function(data){
|
||||||
|
const args = finalizeArgumentsForge(versionData, forgeData, data, basePath)
|
||||||
|
//TODO make this dynamic
|
||||||
|
const child = child_process.spawn('C:\\Program Files\\Java\\jre1.8.0_131\\bin\\javaw.exe', args)
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
console.log('Minecraft:', data.toString('utf8'))
|
||||||
|
})
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
console.log('Minecraft:', data.toString('utf8'))
|
||||||
|
})
|
||||||
|
child.on('close', (code, signal) => {
|
||||||
|
console.log('Exited with code', code)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeArgumentsForge(versionData, forgeData, authData, basePath){
|
||||||
|
const mcArgs = forgeData['minecraftArguments']
|
||||||
|
const gameProfile = authData['selectedProfile']
|
||||||
|
const regex = new RegExp('\\${*(.*)}')
|
||||||
|
const argArr = mcArgs.split(' ')
|
||||||
|
const staticArgs = ['-Xmx4G',
|
||||||
|
'-XX:+UseConcMarkSweepGC',
|
||||||
|
'-XX:+CMSIncrementalMode',
|
||||||
|
'-XX:-UseAdaptiveSizePolicy',
|
||||||
|
'-Xmn128M',
|
||||||
|
'-Djava.library.path=' + path.join(basePath, 'natives'),
|
||||||
|
'-cp',
|
||||||
|
classpathArg(versionData, basePath).concat(forgeClasspathArg(forgeData, basePath)).join(';'),
|
||||||
|
forgeData.mainClass]
|
||||||
|
for(let i=0; i<argArr.length; i++){
|
||||||
|
if(regex.test(argArr[i])){
|
||||||
|
const identifier = argArr[i].match(regex)[1]
|
||||||
|
let newVal = argArr[i]
|
||||||
|
switch(identifier){
|
||||||
|
case 'auth_player_name':
|
||||||
|
newVal = gameProfile['name']
|
||||||
|
break
|
||||||
|
case 'version_name':
|
||||||
|
newVal = versionData['id']
|
||||||
|
break
|
||||||
|
case 'game_directory':
|
||||||
|
newVal = basePath
|
||||||
|
break
|
||||||
|
case 'assets_root':
|
||||||
|
newVal = path.join(basePath, 'assets')
|
||||||
|
break
|
||||||
|
case 'assets_index_name':
|
||||||
|
newVal = versionData['assets']
|
||||||
|
break
|
||||||
|
case 'auth_uuid':
|
||||||
|
newVal = gameProfile['id']
|
||||||
|
break
|
||||||
|
case 'auth_access_token':
|
||||||
|
newVal = authData['accessToken']
|
||||||
|
break
|
||||||
|
case 'user_type':
|
||||||
|
newVal = 'MOJANG'
|
||||||
|
break
|
||||||
|
case 'version_type':
|
||||||
|
newVal = versionData['type']
|
||||||
|
break
|
||||||
|
}
|
||||||
|
argArr[i] = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return staticArgs.concat(argArr)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeArguments(versionData, authData, basePath){
|
||||||
|
const mcArgs = versionData['minecraftArguments']
|
||||||
|
const gameProfile = authData['selectedProfile']
|
||||||
|
const regex = new RegExp('\\${*(.*)}')
|
||||||
|
const argArr = mcArgs.split(' ')
|
||||||
|
const staticArgs = ['-Xmx1G',
|
||||||
|
'-XX:+UseConcMarkSweepGC',
|
||||||
|
'-XX:+CMSIncrementalMode',
|
||||||
|
'-XX:-UseAdaptiveSizePolicy',
|
||||||
|
'-Xmn128M',
|
||||||
|
'-Djava.library.path=' + path.join(basePath, 'natives'),
|
||||||
|
'-cp',
|
||||||
|
classpathArg(versionData, basePath).join(';'),
|
||||||
|
versionData.mainClass]
|
||||||
|
for(let i=0; i<argArr.length; i++){
|
||||||
|
if(regex.test(argArr[i])){
|
||||||
|
const identifier = argArr[i].match(regex)[1]
|
||||||
|
let newVal = argArr[i]
|
||||||
|
switch(identifier){
|
||||||
|
case 'auth_player_name':
|
||||||
|
newVal = gameProfile['name']
|
||||||
|
break
|
||||||
|
case 'version_name':
|
||||||
|
newVal = versionData['id']
|
||||||
|
break
|
||||||
|
case 'game_directory':
|
||||||
|
newVal = basePath
|
||||||
|
break
|
||||||
|
case 'assets_root':
|
||||||
|
newVal = path.join(basePath, 'assets')
|
||||||
|
break
|
||||||
|
case 'assets_index_name':
|
||||||
|
newVal = versionData['assets']
|
||||||
|
break
|
||||||
|
case 'auth_uuid':
|
||||||
|
newVal = gameProfile['id']
|
||||||
|
break
|
||||||
|
case 'auth_access_token':
|
||||||
|
newVal = authData['accessToken']
|
||||||
|
break
|
||||||
|
case 'user_type':
|
||||||
|
newVal = 'MOJANG'
|
||||||
|
break
|
||||||
|
case 'version_type':
|
||||||
|
newVal = versionData['type']
|
||||||
|
break
|
||||||
|
}
|
||||||
|
argArr[i] = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return staticArgs.concat(argArr)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forgeClasspathArg(forgeData, basePath){
|
||||||
|
const libArr = forgeData['libraries']
|
||||||
|
const libPath = path.join(basePath, 'libraries')
|
||||||
|
const cpArgs = []
|
||||||
|
for(let i=0; i<libArr.length; i++){
|
||||||
|
const lib = libArr[i]
|
||||||
|
const to = path.join(libPath, ag._resolvePath(lib.name, '.jar'))
|
||||||
|
cpArgs.push(to)
|
||||||
|
}
|
||||||
|
return cpArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
function classpathArg(versionData, basePath){
|
||||||
|
const libArr = versionData['libraries']
|
||||||
|
const libPath = path.join(basePath, 'libraries')
|
||||||
|
const nativePath = path.join(basePath, 'natives')
|
||||||
|
const version = versionData['id']
|
||||||
|
const cpArgs = [path.join(basePath, 'versions', version, version + '.jar')]
|
||||||
|
libArr.forEach(function(lib){
|
||||||
|
if(ag.Library.validateRules(lib['rules'])){
|
||||||
|
if(lib['natives'] == null){
|
||||||
|
const dlInfo = lib['downloads']
|
||||||
|
const artifact = dlInfo['artifact']
|
||||||
|
const to = path.join(libPath, artifact['path'])
|
||||||
|
cpArgs.push(to)
|
||||||
|
} else {
|
||||||
|
//Now we need to extract natives.
|
||||||
|
const natives = lib['natives']
|
||||||
|
const extractInst = lib['extract']
|
||||||
|
const exclusionArr = extractInst['exclude']
|
||||||
|
const opSys = ag.Library.mojangFriendlyOS()
|
||||||
|
const indexId = natives[opSys]
|
||||||
|
const dlInfo = lib['downloads']
|
||||||
|
const classifiers = dlInfo['classifiers']
|
||||||
|
const artifact = classifiers[indexId]
|
||||||
|
|
||||||
|
const to = path.join(libPath, artifact['path'])
|
||||||
|
|
||||||
|
let zip = new AdmZip(to)
|
||||||
|
let zipEntries = zip.getEntries()
|
||||||
|
|
||||||
|
for(let i=0; i<zipEntries.length; i++){
|
||||||
|
const fileName = zipEntries[i].entryName
|
||||||
|
|
||||||
|
let shouldExclude = false
|
||||||
|
|
||||||
|
exclusionArr.forEach(function(exclusion){
|
||||||
|
if(exclusion.indexOf(fileName) > -1){
|
||||||
|
shouldExclude = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(!shouldExclude){
|
||||||
|
mkpath.sync(path.join(nativePath, fileName, '..'))
|
||||||
|
fs.writeFile(path.join(nativePath, fileName), zipEntries[i].getData())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
cpArgs.push(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return cpArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
launchMinecraft
|
||||||
|
}
|
51
app/assets/js/modlist.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to configure mod launch args.
|
||||||
|
*/
|
||||||
|
export class ModList {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a ModList.
|
||||||
|
*
|
||||||
|
* @param {String} repositoryRoot - the base path of the mod locations.
|
||||||
|
* @param {Array.<String>} modRef - array containing the mod artifact ids.
|
||||||
|
* @param {String} parentList - parent ModList file path, null if none.
|
||||||
|
*/
|
||||||
|
constructor(repositoryRoot, modRef, parentList){
|
||||||
|
if(!arguments.length){
|
||||||
|
this.repositoryRoot = ''
|
||||||
|
this.modRef = []
|
||||||
|
}
|
||||||
|
this.repositoryRoot
|
||||||
|
this.modRef = modRef
|
||||||
|
if(parentList != null) this.parentList = parentList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports a ModList object to the specified file path.
|
||||||
|
*
|
||||||
|
* @param {ModList} modList - the ModList object to export.
|
||||||
|
* @param {String} filePath - desired filepath.
|
||||||
|
* @returns {Promise.<String>} - a promise which resolves FML modList argument.
|
||||||
|
*/
|
||||||
|
static exportModList(modList, filePath){
|
||||||
|
return new Promise(function(resolve, reject){
|
||||||
|
fs.writeFile(filePath, JSON.stringify(modList), (err) => {
|
||||||
|
if(err){
|
||||||
|
reject(err.message)
|
||||||
|
}
|
||||||
|
resolve('--modListFile ' + filePath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} distro - the distribution index.
|
||||||
|
*/
|
||||||
|
static generateModList(distro){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
const {ipcRenderer} = require('electron')
|
|
||||||
const fs = require('fs-extra')
|
|
||||||
const os = require('os')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const ConfigManager = require('./configmanager')
|
|
||||||
const { DistroAPI } = require('./distromanager')
|
|
||||||
const LangLoader = require('./langloader')
|
|
||||||
const { LoggerUtil } = require('helios-core')
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { HeliosDistribution } = require('helios-core/common')
|
|
||||||
|
|
||||||
const logger = LoggerUtil.getLogger('Preloader')
|
|
||||||
|
|
||||||
logger.info('Loading..')
|
|
||||||
|
|
||||||
// Load ConfigManager
|
|
||||||
ConfigManager.load()
|
|
||||||
|
|
||||||
// Yuck!
|
|
||||||
// TODO Fix this
|
|
||||||
DistroAPI['commonDir'] = ConfigManager.getCommonDirectory()
|
|
||||||
DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory()
|
|
||||||
|
|
||||||
// Load Strings
|
|
||||||
LangLoader.setupLanguage()
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {HeliosDistribution} data
|
|
||||||
*/
|
|
||||||
function onDistroLoad(data){
|
|
||||||
if(data != null){
|
|
||||||
|
|
||||||
// Resolve the selected server if its value has yet to be set.
|
|
||||||
if(ConfigManager.getSelectedServer() == null || data.getServerById(ConfigManager.getSelectedServer()) == null){
|
|
||||||
logger.info('Determining default selected server..')
|
|
||||||
ConfigManager.setSelectedServer(data.getMainServer().rawServer.id)
|
|
||||||
ConfigManager.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipcRenderer.send('distributionIndexDone', data != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Distribution is downloaded and cached.
|
|
||||||
DistroAPI.getDistribution()
|
|
||||||
.then(heliosDistro => {
|
|
||||||
logger.info('Loaded distribution index.')
|
|
||||||
|
|
||||||
onDistroLoad(heliosDistro)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.info('Failed to load an older version of the distribution index.')
|
|
||||||
logger.info('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.info('Cleaned natives directory.')
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,888 +0,0 @@
|
|||||||
const AdmZip = require('adm-zip')
|
|
||||||
const child_process = require('child_process')
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const fs = require('fs-extra')
|
|
||||||
const { LoggerUtil } = require('helios-core')
|
|
||||||
const { getMojangOS, isLibraryCompatible, mcVersionAtLeast } = require('helios-core/common')
|
|
||||||
const { Type } = require('helios-distribution-types')
|
|
||||||
const os = require('os')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const ConfigManager = require('./configmanager')
|
|
||||||
|
|
||||||
const logger = LoggerUtil.getLogger('ProcessBuilder')
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only forge and fabric are top level mod loaders.
|
|
||||||
*
|
|
||||||
* Forge 1.13+ launch logic is similar to fabrics, for now using usingFabricLoader flag to
|
|
||||||
* change minor details when needed.
|
|
||||||
*
|
|
||||||
* Rewrite of this module may be needed in the future.
|
|
||||||
*/
|
|
||||||
class ProcessBuilder {
|
|
||||||
|
|
||||||
constructor(distroServer, vanillaManifest, modManifest, authUser, launcherVersion){
|
|
||||||
this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.rawServer.id)
|
|
||||||
this.commonDir = ConfigManager.getCommonDirectory()
|
|
||||||
this.server = distroServer
|
|
||||||
this.vanillaManifest = vanillaManifest
|
|
||||||
this.modManifest = modManifest
|
|
||||||
this.authUser = authUser
|
|
||||||
this.launcherVersion = launcherVersion
|
|
||||||
this.forgeModListFile = path.join(this.gameDir, 'forgeMods.list') // 1.13+
|
|
||||||
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.usingFabricLoader = 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.info('Using liteloader:', this.usingLiteLoader)
|
|
||||||
this.usingFabricLoader = this.server.modules.some(mdl => mdl.rawModule.type === Type.Fabric)
|
|
||||||
logger.info('Using fabric loader:', this.usingFabricLoader)
|
|
||||||
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.rawServer.id).mods, this.server.modules)
|
|
||||||
|
|
||||||
// Mod list below 1.13
|
|
||||||
// Fabric only supports 1.14+
|
|
||||||
if(!mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
|
|
||||||
this.constructJSONModList('forge', modObj.fMods, true)
|
|
||||||
if(this.usingLiteLoader){
|
|
||||||
this.constructJSONModList('liteloader', modObj.lMods, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uberModArr = modObj.fMods.concat(modObj.lMods)
|
|
||||||
let args = this.constructJVMArguments(uberModArr, tempNativePath)
|
|
||||||
|
|
||||||
if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
|
|
||||||
//args = args.concat(this.constructModArguments(modObj.fMods))
|
|
||||||
args = args.concat(this.constructModList(modObj.fMods))
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Launch Arguments:', args)
|
|
||||||
|
|
||||||
const child = child_process.spawn(ConfigManager.getJavaExecutable(this.server.rawServer.id), args, {
|
|
||||||
cwd: this.gameDir,
|
|
||||||
detached: ConfigManager.getLaunchDetached()
|
|
||||||
})
|
|
||||||
|
|
||||||
if(ConfigManager.getLaunchDetached()){
|
|
||||||
child.unref()
|
|
||||||
}
|
|
||||||
|
|
||||||
child.stdout.setEncoding('utf8')
|
|
||||||
child.stderr.setEncoding('utf8')
|
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
|
||||||
data.trim().split('\n').forEach(x => console.log(`\x1b[32m[Minecraft]\x1b[0m ${x}`))
|
|
||||||
|
|
||||||
})
|
|
||||||
child.stderr.on('data', (data) => {
|
|
||||||
data.trim().split('\n').forEach(x => console.log(`\x1b[31m[Minecraft]\x1b[0m ${x}`))
|
|
||||||
})
|
|
||||||
child.on('close', (code, signal) => {
|
|
||||||
logger.info('Exited with code', code)
|
|
||||||
fs.remove(tempNativePath, (err) => {
|
|
||||||
if(err){
|
|
||||||
logger.warn('Error while deleting temp dir', err)
|
|
||||||
} else {
|
|
||||||
logger.info('Temp dir deleted successfully.')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return child
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the platform specific classpath separator. On windows, this is a semicolon.
|
|
||||||
* On Unix, this is a colon.
|
|
||||||
*
|
|
||||||
* @returns {string} The classpath separator for the current operating system.
|
|
||||||
*/
|
|
||||||
static getClasspathSeparator() {
|
|
||||||
return process.platform === 'win32' ? ';' : ':'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.def : 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.modules){
|
|
||||||
if(ll.rawModule.type === Type.LiteLoader){
|
|
||||||
if(!ll.getRequired().value){
|
|
||||||
const modCfg = ConfigManager.getModConfiguration(this.server.rawServer.id).mods
|
|
||||||
if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessMavenIdentifier()], ll.getRequired())){
|
|
||||||
if(fs.existsSync(ll.getPath())){
|
|
||||||
this.usingLiteLoader = true
|
|
||||||
this.llPath = ll.getPath()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(fs.existsSync(ll.getPath())){
|
|
||||||
this.usingLiteLoader = true
|
|
||||||
this.llPath = ll.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.rawModule.type
|
|
||||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
|
||||||
const o = !mdl.getRequired().value
|
|
||||||
const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessMavenIdentifier()], mdl.getRequired())
|
|
||||||
if(!o || (o && e)){
|
|
||||||
if(mdl.subModules.length > 0){
|
|
||||||
const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessMavenIdentifier()].mods, mdl.subModules)
|
|
||||||
fMods = fMods.concat(v.fMods)
|
|
||||||
lMods = lMods.concat(v.lMods)
|
|
||||||
if(type === Type.LiteLoader){
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(type === Type.ForgeMod || type === Type.FabricMod){
|
|
||||||
fMods.push(mdl)
|
|
||||||
} else {
|
|
||||||
lMods.push(mdl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fMods,
|
|
||||||
lMods
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_lteMinorVersion(version) {
|
|
||||||
return Number(this.modManifest.id.split('-')[0].split('.')[1]) <= Number(version)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test to see if this version of forge requires the absolute: prefix
|
|
||||||
* on the modListFile repository field.
|
|
||||||
*/
|
|
||||||
_requiresAbsolute(){
|
|
||||||
try {
|
|
||||||
if(this._lteMinorVersion(9)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const ver = this.modManifest.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.
|
|
||||||
*/
|
|
||||||
constructJSONModList(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.getExtensionlessMavenIdentifier())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for(let mod of mods){
|
|
||||||
ids.push(mod.getMavenIdentifier())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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.getExtensionlessMavenIdentifier()
|
|
||||||
// }).join(',')
|
|
||||||
|
|
||||||
// if(argStr){
|
|
||||||
// return [
|
|
||||||
// '--fml.mavenRoots',
|
|
||||||
// path.join('..', '..', 'common', 'modstore'),
|
|
||||||
// '--fml.mods',
|
|
||||||
// argStr
|
|
||||||
// ]
|
|
||||||
// } else {
|
|
||||||
// return []
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct the mod argument list for forge 1.13 and Fabric
|
|
||||||
*
|
|
||||||
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
|
||||||
*/
|
|
||||||
constructModList(mods) {
|
|
||||||
const writeBuffer = mods.map(mod => {
|
|
||||||
return this.usingFabricLoader ? mod.getPath() : mod.getExtensionlessMavenIdentifier()
|
|
||||||
}).join('\n')
|
|
||||||
|
|
||||||
if(writeBuffer) {
|
|
||||||
fs.writeFileSync(this.forgeModListFile, writeBuffer, 'UTF-8')
|
|
||||||
return this.usingFabricLoader ? [
|
|
||||||
'--fabric.addMods',
|
|
||||||
`@${this.forgeModListFile}`
|
|
||||||
] : [
|
|
||||||
'--fml.mavenRoots',
|
|
||||||
path.join('..', '..', 'common', 'modstore'),
|
|
||||||
'--fml.modLists',
|
|
||||||
this.forgeModListFile
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_processAutoConnectArg(args){
|
|
||||||
if(ConfigManager.getAutoConnect() && this.server.rawServer.autoconnect){
|
|
||||||
if(mcVersionAtLeast('1.20', this.server.rawServer.minecraftVersion)){
|
|
||||||
args.push('--quickPlayMultiplayer')
|
|
||||||
args.push(`${this.server.hostname}:${this.server.port}`)
|
|
||||||
} else {
|
|
||||||
args.push('--server')
|
|
||||||
args.push(this.server.hostname)
|
|
||||||
args.push('--port')
|
|
||||||
args.push(this.server.port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
|
|
||||||
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(ProcessBuilder.getClasspathSeparator()))
|
|
||||||
|
|
||||||
// Java Arguments
|
|
||||||
if(process.platform === 'darwin'){
|
|
||||||
args.push('-Xdock:name=onimairu-mc_launcher')
|
|
||||||
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
|
|
||||||
}
|
|
||||||
args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id))
|
|
||||||
args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id))
|
|
||||||
args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id))
|
|
||||||
args.push('-Djava.library.path=' + tempNativePath)
|
|
||||||
|
|
||||||
// Main Java Class
|
|
||||||
args.push(this.modManifest.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.vanillaManifest.arguments.jvm
|
|
||||||
|
|
||||||
// Debug securejarhandler
|
|
||||||
// args.push('-Dbsl.debug=true')
|
|
||||||
|
|
||||||
if(this.modManifest.arguments.jvm != null) {
|
|
||||||
for(const argStr of this.modManifest.arguments.jvm) {
|
|
||||||
args.push(argStr
|
|
||||||
.replaceAll('${library_directory}', this.libPath)
|
|
||||||
.replaceAll('${classpath_separator}', ProcessBuilder.getClasspathSeparator())
|
|
||||||
.replaceAll('${version_name}', this.modManifest.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//args.push('-Dlog4j.configurationFile=D:\\WesterosCraft\\game\\common\\assets\\log_configs\\client-1.12.xml')
|
|
||||||
|
|
||||||
// Java Arguments
|
|
||||||
if(process.platform === 'darwin'){
|
|
||||||
args.push('-Xdock:name=onimairu-mc_launcher')
|
|
||||||
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
|
|
||||||
}
|
|
||||||
args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id))
|
|
||||||
args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id))
|
|
||||||
args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id))
|
|
||||||
|
|
||||||
// Main Java Class
|
|
||||||
args.push(this.modManifest.mainClass)
|
|
||||||
|
|
||||||
// Vanilla Arguments
|
|
||||||
args = args.concat(this.vanillaManifest.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 === getMojangOS()
|
|
||||||
&& (rule.os.version == null || new RegExp(rule.os.version).test(os.release))){
|
|
||||||
if(rule.action === 'allow'){
|
|
||||||
checksum++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(rule.action === 'disallow'){
|
|
||||||
checksum++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if(rule.features != null){
|
|
||||||
// We don't have many 'features' in the index at the moment.
|
|
||||||
// This should be fine for a while.
|
|
||||||
if(rule.features.has_custom_resolution != null && rule.features.has_custom_resolution === true){
|
|
||||||
if(ConfigManager.getFullscreen()){
|
|
||||||
args[i].value = [
|
|
||||||
'--fullscreen',
|
|
||||||
'true'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
checksum++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO splice not push
|
|
||||||
if(checksum === args[i].rules.length){
|
|
||||||
if(typeof args[i].value === 'string'){
|
|
||||||
args[i] = args[i].value
|
|
||||||
} else if(typeof args[i].value === 'object'){
|
|
||||||
//args = args.concat(args[i].value)
|
|
||||||
args.splice(i, 1, ...args[i].value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrement i to reprocess the resolved value
|
|
||||||
i--
|
|
||||||
} else {
|
|
||||||
args[i] = null
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if(typeof args[i] === 'string'){
|
|
||||||
if(argDiscovery.test(args[i])){
|
|
||||||
const identifier = args[i].match(argDiscovery)[1]
|
|
||||||
let val = null
|
|
||||||
switch(identifier){
|
|
||||||
case 'auth_player_name':
|
|
||||||
val = this.authUser.displayName.trim()
|
|
||||||
break
|
|
||||||
case 'version_name':
|
|
||||||
//val = vanillaManifest.id
|
|
||||||
val = this.server.rawServer.id
|
|
||||||
break
|
|
||||||
case 'game_directory':
|
|
||||||
val = this.gameDir
|
|
||||||
break
|
|
||||||
case 'assets_root':
|
|
||||||
val = path.join(this.commonDir, 'assets')
|
|
||||||
break
|
|
||||||
case 'assets_index_name':
|
|
||||||
val = this.vanillaManifest.assets
|
|
||||||
break
|
|
||||||
case 'auth_uuid':
|
|
||||||
val = this.authUser.uuid.trim()
|
|
||||||
break
|
|
||||||
case 'auth_access_token':
|
|
||||||
val = this.authUser.accessToken
|
|
||||||
break
|
|
||||||
case 'user_type':
|
|
||||||
val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
|
|
||||||
break
|
|
||||||
case 'version_type':
|
|
||||||
val = this.vanillaManifest.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(ProcessBuilder.getClasspathSeparator())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if(val != null){
|
|
||||||
args[i] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Autoconnect
|
|
||||||
this._processAutoConnectArg(args)
|
|
||||||
|
|
||||||
|
|
||||||
// Forge Specific Arguments
|
|
||||||
args = args.concat(this.modManifest.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.modManifest.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 = vanillaManifest.id
|
|
||||||
val = this.server.rawServer.id
|
|
||||||
break
|
|
||||||
case 'game_directory':
|
|
||||||
val = this.gameDir
|
|
||||||
break
|
|
||||||
case 'assets_root':
|
|
||||||
val = path.join(this.commonDir, 'assets')
|
|
||||||
break
|
|
||||||
case 'assets_index_name':
|
|
||||||
val = this.vanillaManifest.assets
|
|
||||||
break
|
|
||||||
case 'auth_uuid':
|
|
||||||
val = this.authUser.uuid.trim()
|
|
||||||
break
|
|
||||||
case 'auth_access_token':
|
|
||||||
val = this.authUser.accessToken
|
|
||||||
break
|
|
||||||
case 'user_type':
|
|
||||||
val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
|
|
||||||
break
|
|
||||||
case 'user_properties': // 1.8.9 and below.
|
|
||||||
val = '{}'
|
|
||||||
break
|
|
||||||
case 'version_type':
|
|
||||||
val = this.vanillaManifest.type
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if(val != null){
|
|
||||||
mcArgs[i] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Autoconnect to the selected server.
|
|
||||||
this._processAutoConnectArg(mcArgs)
|
|
||||||
|
|
||||||
// Prepare game resolution
|
|
||||||
if(ConfigManager.getFullscreen()){
|
|
||||||
mcArgs.push('--fullscreen')
|
|
||||||
mcArgs.push(true)
|
|
||||||
} else {
|
|
||||||
mcArgs.push('--width')
|
|
||||||
mcArgs.push(ConfigManager.getGameWidth())
|
|
||||||
mcArgs.push('--height')
|
|
||||||
mcArgs.push(ConfigManager.getGameHeight())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mod List File Argument
|
|
||||||
mcArgs.push('--modListFile')
|
|
||||||
if(this._lteMinorVersion(9)) {
|
|
||||||
mcArgs.push(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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that the classpath entries all point to jar files.
|
|
||||||
*
|
|
||||||
* @param {Array.<String>} list Array of classpath entries.
|
|
||||||
*/
|
|
||||||
_processClassPathList(list) {
|
|
||||||
|
|
||||||
const ext = '.jar'
|
|
||||||
const extLen = ext.length
|
|
||||||
for(let i=0; i<list.length; i++) {
|
|
||||||
const extIndex = list[i].indexOf(ext)
|
|
||||||
if(extIndex > -1 && extIndex !== list[i].length - extLen) {
|
|
||||||
list[i] = list[i].substring(0, extIndex + extLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = []
|
|
||||||
|
|
||||||
if(!mcVersionAtLeast('1.17', this.server.rawServer.minecraftVersion) || this.usingFabricLoader) {
|
|
||||||
// Add the version.jar to the classpath.
|
|
||||||
// Must not be added to the classpath for Forge 1.17+.
|
|
||||||
const version = this.vanillaManifest.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))
|
|
||||||
|
|
||||||
this._processClassPathList(cpArgs)
|
|
||||||
|
|
||||||
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 nativesRegex = /.+:natives-([^-]+)(?:-(.+))?/
|
|
||||||
const libs = {}
|
|
||||||
|
|
||||||
const libArr = this.vanillaManifest.libraries
|
|
||||||
fs.ensureDirSync(tempNativePath)
|
|
||||||
for(let i=0; i<libArr.length; i++){
|
|
||||||
const lib = libArr[i]
|
|
||||||
if(isLibraryCompatible(lib.rules, lib.natives)){
|
|
||||||
|
|
||||||
// Pre-1.19 has a natives object.
|
|
||||||
if(lib.natives != null) {
|
|
||||||
// Extract the native library.
|
|
||||||
const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/']
|
|
||||||
const artifact = lib.downloads.classifiers[lib.natives[getMojangOS()].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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 1.19+ logic
|
|
||||||
else if(lib.name.includes('natives-')) {
|
|
||||||
|
|
||||||
const regexTest = nativesRegex.exec(lib.name)
|
|
||||||
// const os = regexTest[1]
|
|
||||||
const arch = regexTest[2] ?? 'x64'
|
|
||||||
|
|
||||||
if(arch != process.arch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the native library.
|
|
||||||
const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/', '.git', '.sha1']
|
|
||||||
const artifact = lib.downloads.artifact
|
|
||||||
|
|
||||||
// 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++){
|
|
||||||
if(zipEntries[i].isDirectory) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = zipEntries[i].entryName
|
|
||||||
|
|
||||||
let shouldExclude = false
|
|
||||||
|
|
||||||
// Exclude noted files.
|
|
||||||
exclusionArr.forEach(function(exclusion){
|
|
||||||
if(fileName.indexOf(exclusion) > -1){
|
|
||||||
shouldExclude = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const extractName = fileName.includes('/') ? fileName.substring(fileName.lastIndexOf('/')) : fileName
|
|
||||||
|
|
||||||
// Extract the file.
|
|
||||||
if(!shouldExclude){
|
|
||||||
fs.writeFile(path.join(tempNativePath, extractName), zipEntries[i].getData(), (err) => {
|
|
||||||
if(err){
|
|
||||||
logger.error('Error while extracting native library:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No natives
|
|
||||||
else {
|
|
||||||
const dlInfo = lib.downloads
|
|
||||||
const artifact = dlInfo.artifact
|
|
||||||
const to = path.join(this.libPath, artifact.path)
|
|
||||||
const versionIndependentId = lib.name.substring(0, lib.name.lastIndexOf(':'))
|
|
||||||
libs[versionIndependentId] = to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.modules
|
|
||||||
let libs = {}
|
|
||||||
|
|
||||||
// Locate Forge/Fabric/Libraries
|
|
||||||
for(let mdl of mdls){
|
|
||||||
const type = mdl.rawModule.type
|
|
||||||
if(type === Type.ForgeHosted || type === Type.Fabric || type === Type.Library){
|
|
||||||
libs[mdl.getVersionlessMavenIdentifier()] = mdl.getPath()
|
|
||||||
if(mdl.subModules.length > 0){
|
|
||||||
const res = this._resolveModuleLibraries(mdl)
|
|
||||||
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])
|
|
||||||
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 {{[id: string]: string}} An object containing the paths of each library this module requires.
|
|
||||||
*/
|
|
||||||
_resolveModuleLibraries(mdl){
|
|
||||||
if(!mdl.subModules.length > 0){
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
let libs = {}
|
|
||||||
for(let sm of mdl.subModules){
|
|
||||||
if(sm.rawModule.type === Type.Library){
|
|
||||||
|
|
||||||
if(sm.rawModule.classpath ?? true) {
|
|
||||||
libs[sm.getVersionlessMavenIdentifier()] = sm.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.subModules.length > 0){
|
|
||||||
const res = this._resolveModuleLibraries(sm)
|
|
||||||
libs = {...libs, ...res}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return libs
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ProcessBuilder
|
|
103
app/assets/js/script.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
var $ = require('jQuery');
|
||||||
|
const remote = require('electron').remote
|
||||||
|
const shell = require('electron').shell
|
||||||
|
const path = require('path')
|
||||||
|
const os = require('os');
|
||||||
|
const ag = require(path.join(__dirname, 'assets', 'js', 'assetguard.js'))
|
||||||
|
|
||||||
|
function timestamp(){
|
||||||
|
let date = new Date();
|
||||||
|
const month = date.getMonth() < 9 ? '0'.concat((date.getMonth()+1)) : date.getMonth()
|
||||||
|
const day = date.getDate() < 10 ? '0'.concat(date.getDate()) : date.getDate();
|
||||||
|
let hour = date.getHours() > 12 ? date.getHours() - 12 : date.getHours();
|
||||||
|
hour = hour < 10 ? '0'.concat(hour) : hour
|
||||||
|
const min = date.getMinutes() < 10 ? '0'.concat(date.getMinutes()) : date.getMinutes();
|
||||||
|
const sec = date.getSeconds() < 10 ? '0'.concat(date.getSeconds()) : date.getSeconds();
|
||||||
|
|
||||||
|
return '[' + month + '/' + day + '/' + date.getFullYear() + ' ' + hour + ':' + min + ':' + sec + ']'
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('ready', function(){
|
||||||
|
$(".toggle-btn input[type=radio]").addClass("visuallyhidden");
|
||||||
|
$(".toggle-btn input[type=radio]").change(function() {
|
||||||
|
if($(this).attr("name")) {
|
||||||
|
$(this).parent().addClass("success").siblings().removeClass("success")
|
||||||
|
} else {
|
||||||
|
$(this).parent().toggleClass("success")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log = function(){
|
||||||
|
$('#launcher-log').append(timestamp() + ' [Log] - ' + Array.prototype.slice.call(arguments).join(' ') + os.EOL)
|
||||||
|
}
|
||||||
|
console.error = function(){
|
||||||
|
$('#launcher-log').append('<span class="log_debug">' + timestamp() + ' [Debug] - ' + Array.prototype.slice.call(arguments).join(' ') + "</span>" + os.EOL)
|
||||||
|
}
|
||||||
|
console.debug = function(){
|
||||||
|
$('#launcher-log').append('<span class="log_debug">' + timestamp() + ' [Error] - ' + Array.prototype.slice.call(arguments).join(' ') + "</span>" + os.EOL)
|
||||||
|
}
|
||||||
|
console.log('test')
|
||||||
|
console.debug('test')
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Open web links in the user's default browser. */
|
||||||
|
$(document).on('click', 'a[href^="http"]', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
testdownloads()
|
||||||
|
//console.log(os.homedir())
|
||||||
|
//shell.openExternal(this.href)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
testdownloads = async function(){
|
||||||
|
const lp = require(path.join(__dirname, 'assets', 'js', 'launchprocess.js'))
|
||||||
|
const basePath = path.join(__dirname, '..', 'mcfiles')
|
||||||
|
let versionData = await ag.loadVersionData('1.11.2', basePath)
|
||||||
|
await ag.validateAssets(versionData, basePath)
|
||||||
|
console.log('assets done')
|
||||||
|
await ag.validateLibraries(versionData, basePath)
|
||||||
|
console.log('libs done')
|
||||||
|
await ag.validateMiscellaneous(versionData, basePath)
|
||||||
|
console.log('files done')
|
||||||
|
await ag.validateDistribution('WesterosCraft-1.11.2', basePath)
|
||||||
|
console.log('forge stuff done')
|
||||||
|
ag.instance.on('dlcomplete', async function(){
|
||||||
|
let forgeData = await ag.loadForgeData('WesterosCraft-1.11.2', basePath)
|
||||||
|
lp.launchMinecraft(versionData, forgeData, basePath)
|
||||||
|
//lp.launchMinecraft(versionData, basePath)
|
||||||
|
})
|
||||||
|
ag.processDlQueues()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Opens DevTools window if you type "wcdev" in sequence.
|
||||||
|
This will crash the program if you are using multiple
|
||||||
|
DevTools, for example the chrome debugger in VS Code. */
|
||||||
|
const match = [87, 67, 68, 69, 86]
|
||||||
|
let at = 0;
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
switch(e.keyCode){
|
||||||
|
case match[0]:
|
||||||
|
if(at === 0) ++at
|
||||||
|
break
|
||||||
|
case match[1]:
|
||||||
|
if(at === 1) ++at
|
||||||
|
break
|
||||||
|
case match[2]:
|
||||||
|
if(at === 2) ++at
|
||||||
|
break
|
||||||
|
case match[3]:
|
||||||
|
if(at === 3) ++at
|
||||||
|
break
|
||||||
|
case match[4]:
|
||||||
|
if(at === 4) ++at
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
at = 0
|
||||||
|
}
|
||||||
|
if(at === 5) {
|
||||||
|
var window = remote.getCurrentWindow()
|
||||||
|
window.toggleDevTools()
|
||||||
|
at = 0
|
||||||
|
}
|
||||||
|
})
|
@ -1,213 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script for login.ejs
|
|
||||||
*/
|
|
||||||
// Validation Regexes.
|
|
||||||
const validUsername = /^[a-zA-Z0-9_]{1,16}$/
|
|
||||||
const basicEmail = /^\S+@\S+\.\S+$/
|
|
||||||
//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
|
||||||
|
|
||||||
// Login Elements
|
|
||||||
const loginCancelContainer = document.getElementById('loginCancelContainer')
|
|
||||||
const loginCancelButton = document.getElementById('loginCancelButton')
|
|
||||||
const loginEmailError = document.getElementById('loginEmailError')
|
|
||||||
const loginUsername = document.getElementById('loginUsername')
|
|
||||||
const checkmarkContainer = document.getElementById('checkmarkContainer')
|
|
||||||
const loginRememberOption = document.getElementById('loginRememberOption')
|
|
||||||
const loginButton = document.getElementById('loginButton')
|
|
||||||
const loginForm = document.getElementById('loginForm')
|
|
||||||
|
|
||||||
// Control variables.
|
|
||||||
let lu = false, lp = false
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a login error.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element The element on which to display the error.
|
|
||||||
* @param {string} value The error text.
|
|
||||||
*/
|
|
||||||
function showError(element, value){
|
|
||||||
element.innerHTML = value
|
|
||||||
element.style.opacity = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shake a login error to add emphasis.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element The element to shake.
|
|
||||||
*/
|
|
||||||
function shakeError(element){
|
|
||||||
if(element.style.opacity == 1){
|
|
||||||
element.classList.remove('shake')
|
|
||||||
void element.offsetWidth
|
|
||||||
element.classList.add('shake')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that an email field is neither empty nor invalid.
|
|
||||||
*
|
|
||||||
* @param {string} value The email value.
|
|
||||||
*/
|
|
||||||
function validateEmail(value){
|
|
||||||
if(value){
|
|
||||||
if(!basicEmail.test(value) && !validUsername.test(value)){
|
|
||||||
showError(loginEmailError, Lang.queryJS('login.error.invalidValue'))
|
|
||||||
loginDisabled(true)
|
|
||||||
lu = false
|
|
||||||
} else {
|
|
||||||
loginEmailError.style.opacity = 0
|
|
||||||
lu = true
|
|
||||||
if(lp){
|
|
||||||
loginDisabled(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lu = false
|
|
||||||
showError(loginEmailError, Lang.queryJS('login.error.requiredValue'))
|
|
||||||
loginDisabled(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that the password field is not empty.
|
|
||||||
*
|
|
||||||
* @param {string} value The password value.
|
|
||||||
*/
|
|
||||||
function validatePassword(value){
|
|
||||||
lp = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emphasize errors with shake when focus is lost.
|
|
||||||
loginUsername.addEventListener('focusout', (e) => {
|
|
||||||
validateEmail(e.target.value)
|
|
||||||
shakeError(loginEmailError)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Validate input for each field.
|
|
||||||
loginUsername.addEventListener('input', (e) => {
|
|
||||||
loginDisabled(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable the login button.
|
|
||||||
*
|
|
||||||
* @param {boolean} v True to enable, false to disable.
|
|
||||||
*/
|
|
||||||
function loginDisabled(v){
|
|
||||||
if(loginButton.disabled !== v){
|
|
||||||
loginButton.disabled = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable loading elements.
|
|
||||||
*
|
|
||||||
* @param {boolean} v True to enable, false to disable.
|
|
||||||
*/
|
|
||||||
function loginLoading(v){
|
|
||||||
if(v){
|
|
||||||
loginButton.setAttribute('loading', v)
|
|
||||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn'))
|
|
||||||
} else {
|
|
||||||
loginButton.removeAttribute('loading')
|
|
||||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable login form.
|
|
||||||
*
|
|
||||||
* @param {boolean} v True to enable, false to disable.
|
|
||||||
*/
|
|
||||||
function formDisabled(v){
|
|
||||||
loginDisabled(v)
|
|
||||||
loginCancelButton.disabled = v
|
|
||||||
loginUsername.disabled = v
|
|
||||||
if(v){
|
|
||||||
checkmarkContainer.setAttribute('disabled', v)
|
|
||||||
} else {
|
|
||||||
checkmarkContainer.removeAttribute('disabled')
|
|
||||||
}
|
|
||||||
loginRememberOption.disabled = v
|
|
||||||
}
|
|
||||||
|
|
||||||
let loginViewOnSuccess = VIEWS.landing
|
|
||||||
let loginViewOnCancel = VIEWS.settings
|
|
||||||
let loginViewCancelHandler
|
|
||||||
|
|
||||||
function loginCancelEnabled(val){
|
|
||||||
if(val){
|
|
||||||
$(loginCancelContainer).show()
|
|
||||||
} else {
|
|
||||||
$(loginCancelContainer).hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loginCancelButton.onclick = (e) => {
|
|
||||||
switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => {
|
|
||||||
loginUsername.value = ''
|
|
||||||
loginPassword.value = ''
|
|
||||||
loginCancelEnabled(false)
|
|
||||||
if(loginViewCancelHandler != null){
|
|
||||||
loginViewCancelHandler()
|
|
||||||
loginViewCancelHandler = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable default form behavior.
|
|
||||||
loginForm.onsubmit = () => { return false }
|
|
||||||
|
|
||||||
// Bind login button behavior.
|
|
||||||
loginButton.addEventListener('click', () => {
|
|
||||||
// Disable form.
|
|
||||||
formDisabled(true)
|
|
||||||
|
|
||||||
// Show loading stuff.
|
|
||||||
loginLoading(true)
|
|
||||||
|
|
||||||
AuthManager.addOfflineAccount(loginUsername.value).then((value) => {
|
|
||||||
updateSelectedAccount(value)
|
|
||||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
|
|
||||||
$('.circle-loader').toggleClass('load-complete')
|
|
||||||
$('.checkmark').toggle()
|
|
||||||
setTimeout(() => {
|
|
||||||
switchView(VIEWS.login, loginViewOnSuccess, 500, 500, async () => {
|
|
||||||
// Temporary workaround
|
|
||||||
if(loginViewOnSuccess === VIEWS.settings){
|
|
||||||
await prepareSettings()
|
|
||||||
}
|
|
||||||
loginViewOnSuccess = VIEWS.landing // Reset this for good measure.
|
|
||||||
loginCancelEnabled(false) // Reset this for good measure.
|
|
||||||
loginViewCancelHandler = null // Reset this for good measure.
|
|
||||||
loginUsername.value = ''
|
|
||||||
$('.circle-loader').toggleClass('load-complete')
|
|
||||||
$('.checkmark').toggle()
|
|
||||||
loginLoading(false)
|
|
||||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login'))
|
|
||||||
formDisabled(false)
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
}).catch((displayableError) => {
|
|
||||||
loginLoading(false)
|
|
||||||
|
|
||||||
let actualDisplayableError
|
|
||||||
if(isDisplayableError(displayableError)) {
|
|
||||||
msftLoginLogger.error('Error while logging in.', displayableError)
|
|
||||||
actualDisplayableError = displayableError
|
|
||||||
} else {
|
|
||||||
// Uh oh.
|
|
||||||
msftLoginLogger.error('Unhandled error during login.', displayableError)
|
|
||||||
actualDisplayableError = Lang.queryJS('login.error.unknown')
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
|
|
||||||
setOverlayHandler(() => {
|
|
||||||
formDisabled(false)
|
|
||||||
toggleOverlay(false)
|
|
||||||
})
|
|
||||||
toggleOverlay(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
@ -1,51 +0,0 @@
|
|||||||
const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer')
|
|
||||||
const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft')
|
|
||||||
//const loginOptionMojang = document.getElementById('loginOptionMojang')
|
|
||||||
const loginOptionOffline = document.getElementById('loginOptionOffline')
|
|
||||||
const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton')
|
|
||||||
|
|
||||||
let loginOptionsCancellable = false
|
|
||||||
|
|
||||||
let loginOptionsViewOnLoginSuccess
|
|
||||||
let loginOptionsViewOnLoginCancel
|
|
||||||
let loginOptionsViewOnCancel
|
|
||||||
let loginOptionsViewCancelHandler
|
|
||||||
|
|
||||||
function loginOptionsCancelEnabled(val){
|
|
||||||
if(val){
|
|
||||||
$(loginOptionsCancelContainer).show()
|
|
||||||
} else {
|
|
||||||
$(loginOptionsCancelContainer).hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loginOptionMicrosoft.onclick = (e) => {
|
|
||||||
switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
|
|
||||||
ipcRenderer.send(
|
|
||||||
MSFT_OPCODE.OPEN_LOGIN,
|
|
||||||
loginOptionsViewOnLoginSuccess,
|
|
||||||
loginOptionsViewOnLoginCancel
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loginOptionOffline.onclick = (e) => {
|
|
||||||
switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
|
|
||||||
loginViewOnSuccess = loginOptionsViewOnLoginSuccess
|
|
||||||
loginViewOnCancel = loginOptionsViewOnLoginCancel
|
|
||||||
loginCancelEnabled(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loginOptionsCancelButton.onclick = (e) => {
|
|
||||||
switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => {
|
|
||||||
// Clear login values (Mojang login)
|
|
||||||
// No cleanup needed for Microsoft.
|
|
||||||
loginUsername.value = ''
|
|
||||||
loginPassword.value = ''
|
|
||||||
if(loginOptionsViewCancelHandler != null){
|
|
||||||
loginOptionsViewCancelHandler()
|
|
||||||
loginOptionsViewCancelHandler = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,324 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script for overlay.ejs
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Overlay Wrapper Functions */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check to see if the overlay is visible.
|
|
||||||
*
|
|
||||||
* @returns {boolean} Whether or not the overlay is visible.
|
|
||||||
*/
|
|
||||||
function isOverlayVisible(){
|
|
||||||
return document.getElementById('main').hasAttribute('overlay')
|
|
||||||
}
|
|
||||||
|
|
||||||
let overlayHandlerContent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overlay keydown handler for a non-dismissable overlay.
|
|
||||||
*
|
|
||||||
* @param {KeyboardEvent} e The keydown event.
|
|
||||||
*/
|
|
||||||
function overlayKeyHandler (e){
|
|
||||||
if(e.key === 'Enter' || e.key === 'Escape'){
|
|
||||||
document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Overlay keydown handler for a dismissable overlay.
|
|
||||||
*
|
|
||||||
* @param {KeyboardEvent} e The keydown event.
|
|
||||||
*/
|
|
||||||
function overlayKeyDismissableHandler (e){
|
|
||||||
if(e.key === 'Enter'){
|
|
||||||
document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click()
|
|
||||||
} else if(e.key === 'Escape'){
|
|
||||||
document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEsc')[0].click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind overlay keydown listeners for escape and exit.
|
|
||||||
*
|
|
||||||
* @param {boolean} state Whether or not to add new event listeners.
|
|
||||||
* @param {string} content The overlay content which will be shown.
|
|
||||||
* @param {boolean} dismissable Whether or not the overlay is dismissable
|
|
||||||
*/
|
|
||||||
function bindOverlayKeys(state, content, dismissable){
|
|
||||||
overlayHandlerContent = content
|
|
||||||
document.removeEventListener('keydown', overlayKeyHandler)
|
|
||||||
document.removeEventListener('keydown', overlayKeyDismissableHandler)
|
|
||||||
if(state){
|
|
||||||
if(dismissable){
|
|
||||||
document.addEventListener('keydown', overlayKeyDismissableHandler)
|
|
||||||
} else {
|
|
||||||
document.addEventListener('keydown', overlayKeyHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle the visibility of the overlay.
|
|
||||||
*
|
|
||||||
* @param {boolean} toggleState True to display, false to hide.
|
|
||||||
* @param {boolean} dismissable Optional. True to show the dismiss option, otherwise false.
|
|
||||||
* @param {string} content Optional. The content div to be shown.
|
|
||||||
*/
|
|
||||||
function toggleOverlay(toggleState, dismissable = false, content = 'overlayContent'){
|
|
||||||
if(toggleState == null){
|
|
||||||
toggleState = !document.getElementById('main').hasAttribute('overlay')
|
|
||||||
}
|
|
||||||
if(typeof dismissable === 'string'){
|
|
||||||
content = dismissable
|
|
||||||
dismissable = false
|
|
||||||
}
|
|
||||||
bindOverlayKeys(toggleState, content, dismissable)
|
|
||||||
if(toggleState){
|
|
||||||
document.getElementById('main').setAttribute('overlay', true)
|
|
||||||
// Make things untabbable.
|
|
||||||
$('#main *').attr('tabindex', '-1')
|
|
||||||
$('#' + content).parent().children().hide()
|
|
||||||
$('#' + content).show()
|
|
||||||
if(dismissable){
|
|
||||||
$('#overlayDismiss').show()
|
|
||||||
} else {
|
|
||||||
$('#overlayDismiss').hide()
|
|
||||||
}
|
|
||||||
$('#overlayContainer').fadeIn({
|
|
||||||
duration: 250,
|
|
||||||
start: () => {
|
|
||||||
if(getCurrentView() === VIEWS.settings){
|
|
||||||
document.getElementById('settingsContainer').style.backgroundColor = 'transparent'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
document.getElementById('main').removeAttribute('overlay')
|
|
||||||
// Make things tabbable.
|
|
||||||
$('#main *').removeAttr('tabindex')
|
|
||||||
$('#overlayContainer').fadeOut({
|
|
||||||
duration: 250,
|
|
||||||
start: () => {
|
|
||||||
if(getCurrentView() === VIEWS.settings){
|
|
||||||
document.getElementById('settingsContainer').style.backgroundColor = 'rgba(0, 0, 0, 0.50)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
$('#' + content).parent().children().hide()
|
|
||||||
$('#' + content).show()
|
|
||||||
if(dismissable){
|
|
||||||
$('#overlayDismiss').show()
|
|
||||||
} else {
|
|
||||||
$('#overlayDismiss').hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleServerSelection(toggleState){
|
|
||||||
await prepareServerSelectionList()
|
|
||||||
toggleOverlay(toggleState, true, 'serverSelectContent')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the content of the overlay.
|
|
||||||
*
|
|
||||||
* @param {string} title Overlay title text.
|
|
||||||
* @param {string} description Overlay description text.
|
|
||||||
* @param {string} acknowledge Acknowledge button text.
|
|
||||||
* @param {string} dismiss Dismiss button text.
|
|
||||||
*/
|
|
||||||
function setOverlayContent(title, description, acknowledge, dismiss = Lang.queryJS('overlay.dismiss')){
|
|
||||||
document.getElementById('overlayTitle').innerHTML = title
|
|
||||||
document.getElementById('overlayDesc').innerHTML = description
|
|
||||||
document.getElementById('overlayAcknowledge').innerHTML = acknowledge
|
|
||||||
document.getElementById('overlayDismiss').innerHTML = dismiss
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the onclick handler of the overlay acknowledge button.
|
|
||||||
* If the handler is null, a default handler will be added.
|
|
||||||
*
|
|
||||||
* @param {function} handler
|
|
||||||
*/
|
|
||||||
function setOverlayHandler(handler){
|
|
||||||
if(handler == null){
|
|
||||||
document.getElementById('overlayAcknowledge').onclick = () => {
|
|
||||||
toggleOverlay(false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.getElementById('overlayAcknowledge').onclick = handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the onclick handler of the overlay dismiss button.
|
|
||||||
* If the handler is null, a default handler will be added.
|
|
||||||
*
|
|
||||||
* @param {function} handler
|
|
||||||
*/
|
|
||||||
function setDismissHandler(handler){
|
|
||||||
if(handler == null){
|
|
||||||
document.getElementById('overlayDismiss').onclick = () => {
|
|
||||||
toggleOverlay(false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.getElementById('overlayDismiss').onclick = handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Server Select View */
|
|
||||||
|
|
||||||
document.getElementById('serverSelectConfirm').addEventListener('click', async () => {
|
|
||||||
const listings = document.getElementsByClassName('serverListing')
|
|
||||||
for(let i=0; i<listings.length; i++){
|
|
||||||
if(listings[i].hasAttribute('selected')){
|
|
||||||
const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid'))
|
|
||||||
updateSelectedServer(serv)
|
|
||||||
refreshServerStatus(true)
|
|
||||||
toggleOverlay(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// None are selected? Not possible right? Meh, handle it.
|
|
||||||
if(listings.length > 0){
|
|
||||||
const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid'))
|
|
||||||
updateSelectedServer(serv)
|
|
||||||
toggleOverlay(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
document.getElementById('accountSelectConfirm').addEventListener('click', async () => {
|
|
||||||
const listings = document.getElementsByClassName('accountListing')
|
|
||||||
for(let i=0; i<listings.length; i++){
|
|
||||||
if(listings[i].hasAttribute('selected')){
|
|
||||||
const authAcc = ConfigManager.setSelectedAccount(listings[i].getAttribute('uuid'))
|
|
||||||
ConfigManager.save()
|
|
||||||
updateSelectedAccount(authAcc)
|
|
||||||
if(getCurrentView() === VIEWS.settings) {
|
|
||||||
await prepareSettings()
|
|
||||||
}
|
|
||||||
toggleOverlay(false)
|
|
||||||
validateSelectedAccount()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// None are selected? Not possible right? Meh, handle it.
|
|
||||||
if(listings.length > 0){
|
|
||||||
const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid'))
|
|
||||||
ConfigManager.save()
|
|
||||||
updateSelectedAccount(authAcc)
|
|
||||||
if(getCurrentView() === VIEWS.settings) {
|
|
||||||
await prepareSettings()
|
|
||||||
}
|
|
||||||
toggleOverlay(false)
|
|
||||||
validateSelectedAccount()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Bind server select cancel button.
|
|
||||||
document.getElementById('serverSelectCancel').addEventListener('click', () => {
|
|
||||||
toggleOverlay(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
document.getElementById('accountSelectCancel').addEventListener('click', () => {
|
|
||||||
$('#accountSelectContent').fadeOut(250, () => {
|
|
||||||
$('#overlayContent').fadeIn(250)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function setServerListingHandlers(){
|
|
||||||
const listings = Array.from(document.getElementsByClassName('serverListing'))
|
|
||||||
listings.map((val) => {
|
|
||||||
val.onclick = e => {
|
|
||||||
if(val.hasAttribute('selected')){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const cListings = document.getElementsByClassName('serverListing')
|
|
||||||
for(let i=0; i<cListings.length; i++){
|
|
||||||
if(cListings[i].hasAttribute('selected')){
|
|
||||||
cListings[i].removeAttribute('selected')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val.setAttribute('selected', '')
|
|
||||||
document.activeElement.blur()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAccountListingHandlers(){
|
|
||||||
const listings = Array.from(document.getElementsByClassName('accountListing'))
|
|
||||||
listings.map((val) => {
|
|
||||||
val.onclick = e => {
|
|
||||||
if(val.hasAttribute('selected')){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const cListings = document.getElementsByClassName('accountListing')
|
|
||||||
for(let i=0; i<cListings.length; i++){
|
|
||||||
if(cListings[i].hasAttribute('selected')){
|
|
||||||
cListings[i].removeAttribute('selected')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val.setAttribute('selected', '')
|
|
||||||
document.activeElement.blur()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function populateServerListings(){
|
|
||||||
const distro = await DistroAPI.getDistribution()
|
|
||||||
const giaSel = ConfigManager.getSelectedServer()
|
|
||||||
const servers = distro.servers
|
|
||||||
let htmlString = ''
|
|
||||||
for(const serv of servers){
|
|
||||||
htmlString += `<button class="serverListing" servid="${serv.rawServer.id}" ${serv.rawServer.id === giaSel ? 'selected' : ''}>
|
|
||||||
<img class="serverListingImg" src="${serv.rawServer.icon}"/>
|
|
||||||
<div class="serverListingDetails">
|
|
||||||
<span class="serverListingName">${serv.rawServer.name}</span>
|
|
||||||
<span class="serverListingDescription">${serv.rawServer.description}</span>
|
|
||||||
<div class="serverListingInfo">
|
|
||||||
<div class="serverListingVersion">${serv.rawServer.minecraftVersion}</div>
|
|
||||||
<div class="serverListingRevision">${serv.rawServer.version}</div>
|
|
||||||
${serv.rawServer.mainServer ? `<div class="serverListingStarWrapper">
|
|
||||||
<svg id="Layer_1" viewBox="0 0 107.45 104.74" width="20px" height="20px">
|
|
||||||
<defs>
|
|
||||||
<style>.cls-1{fill:#fff;}.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;}</style>
|
|
||||||
</defs>
|
|
||||||
<path class="cls-1" d="M100.93,65.54C89,62,68.18,55.65,63.54,52.13c2.7-5.23,18.8-19.2,28-27.55C81.36,31.74,63.74,43.87,58.09,45.3c-2.41-5.37-3.61-26.52-4.37-39-.77,12.46-2,33.64-4.36,39-5.7-1.46-23.3-13.57-33.49-20.72,9.26,8.37,25.39,22.36,28,27.55C39.21,55.68,18.47,62,6.52,65.55c12.32-2,33.63-6.06,39.34-4.9-.16,5.87-8.41,26.16-13.11,37.69,6.1-10.89,16.52-30.16,21-33.9,4.5,3.79,14.93,23.09,21,34C70,86.84,61.73,66.48,61.59,60.65,67.36,59.49,88.64,63.52,100.93,65.54Z"/>
|
|
||||||
<circle class="cls-2" cx="53.73" cy="53.9" r="38"/>
|
|
||||||
</svg>
|
|
||||||
<span class="serverListingStarTooltip">${Lang.queryJS('settings.serverListing.mainServer')}</span>
|
|
||||||
</div>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>`
|
|
||||||
}
|
|
||||||
document.getElementById('serverSelectListScrollable').innerHTML = htmlString
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateAccountListings(){
|
|
||||||
const accountsObj = ConfigManager.getAuthAccounts()
|
|
||||||
const accounts = Array.from(Object.keys(accountsObj), v=>accountsObj[v])
|
|
||||||
let htmlString = ''
|
|
||||||
for(let i=0; i<accounts.length; i++){
|
|
||||||
htmlString += `<button class="accountListing" uuid="${accounts[i].uuid}" ${i===0 ? 'selected' : ''}>
|
|
||||||
<img src="https://mc-heads.net/head/${accounts[i].uuid}/40">
|
|
||||||
<div class="accountListingName">${accounts[i].displayName}</div>
|
|
||||||
</button>`
|
|
||||||
}
|
|
||||||
document.getElementById('accountSelectListScrollable').innerHTML = htmlString
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prepareServerSelectionList(){
|
|
||||||
await populateServerListings()
|
|
||||||
setServerListingHandlers()
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareAccountSelectionList(){
|
|
||||||
populateAccountListings()
|
|
||||||
setAccountListingHandlers()
|
|
||||||
}
|
|
@ -1,467 +0,0 @@
|
|||||||
/**
|
|
||||||
* Initialize UI functions which depend on internal modules.
|
|
||||||
* Loaded after core UI functions are initialized in uicore.js.
|
|
||||||
*/
|
|
||||||
// Requirements
|
|
||||||
const path = require('path')
|
|
||||||
const { Type } = require('helios-distribution-types')
|
|
||||||
|
|
||||||
const AuthManager = require('./assets/js/authmanager')
|
|
||||||
const ConfigManager = require('./assets/js/configmanager')
|
|
||||||
const { DistroAPI } = require('./assets/js/distromanager')
|
|
||||||
|
|
||||||
let rscShouldLoad = false
|
|
||||||
let fatalStartupError = false
|
|
||||||
|
|
||||||
// Mapping of each view to their container IDs.
|
|
||||||
const VIEWS = {
|
|
||||||
landing: '#landingContainer',
|
|
||||||
loginOptions: '#loginOptionsContainer',
|
|
||||||
loginOffline: '#loginOfflineContainer',
|
|
||||||
login: '#loginContainer',
|
|
||||||
settings: '#settingsContainer',
|
|
||||||
welcome: '#welcomeContainer',
|
|
||||||
waiting: '#waitingContainer'
|
|
||||||
}
|
|
||||||
|
|
||||||
// The currently shown view container.
|
|
||||||
let currentView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch launcher views.
|
|
||||||
*
|
|
||||||
* @param {string} current The ID of the current view container.
|
|
||||||
* @param {*} next The ID of the next view container.
|
|
||||||
* @param {*} currentFadeTime Optional. The fade out time for the current view.
|
|
||||||
* @param {*} nextFadeTime Optional. The fade in time for the next view.
|
|
||||||
* @param {*} onCurrentFade Optional. Callback function to execute when the current
|
|
||||||
* view fades out.
|
|
||||||
* @param {*} onNextFade Optional. Callback function to execute when the next view
|
|
||||||
* fades in.
|
|
||||||
*/
|
|
||||||
function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){
|
|
||||||
currentView = next
|
|
||||||
$(`${current}`).fadeOut(currentFadeTime, async () => {
|
|
||||||
await onCurrentFade()
|
|
||||||
$(`${next}`).fadeIn(nextFadeTime, async () => {
|
|
||||||
await onNextFade()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the currently shown view container.
|
|
||||||
*
|
|
||||||
* @returns {string} The currently shown view container.
|
|
||||||
*/
|
|
||||||
function getCurrentView(){
|
|
||||||
return currentView
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showMainUI(data){
|
|
||||||
|
|
||||||
if(!isDev){
|
|
||||||
loggerAutoUpdater.info('Initializing..')
|
|
||||||
ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease())
|
|
||||||
}
|
|
||||||
|
|
||||||
await prepareSettings(true)
|
|
||||||
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
|
|
||||||
refreshServerStatus()
|
|
||||||
setTimeout(() => {
|
|
||||||
document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
|
||||||
document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')`
|
|
||||||
$('#main').show()
|
|
||||||
|
|
||||||
const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0
|
|
||||||
|
|
||||||
// If this is enabled in a development environment we'll get ratelimited.
|
|
||||||
// The relaunch frequency is usually far too high.
|
|
||||||
if(!isDev && isLoggedIn){
|
|
||||||
validateSelectedAccount()
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ConfigManager.isFirstLaunch()){
|
|
||||||
currentView = VIEWS.welcome
|
|
||||||
$(VIEWS.welcome).fadeIn(1000)
|
|
||||||
} else {
|
|
||||||
if(isLoggedIn){
|
|
||||||
currentView = VIEWS.landing
|
|
||||||
$(VIEWS.landing).fadeIn(1000)
|
|
||||||
} else {
|
|
||||||
loginOptionsCancelEnabled(false)
|
|
||||||
loginOptionsViewOnLoginSuccess = VIEWS.landing
|
|
||||||
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
|
||||||
currentView = VIEWS.loginOptions
|
|
||||||
$(VIEWS.loginOptions).fadeIn(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
$('#loadingContainer').fadeOut(500, () => {
|
|
||||||
$('#loadSpinnerImage').removeClass('rotating')
|
|
||||||
})
|
|
||||||
}, 250)
|
|
||||||
|
|
||||||
}, 750)
|
|
||||||
// Disable tabbing to the news container.
|
|
||||||
initNews().then(() => {
|
|
||||||
$('#newsContainer *').attr('tabindex', '-1')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFatalStartupError(){
|
|
||||||
setTimeout(() => {
|
|
||||||
$('#loadingContainer').fadeOut(250, () => {
|
|
||||||
document.getElementById('overlayContainer').style.background = 'none'
|
|
||||||
setOverlayContent(
|
|
||||||
Lang.queryJS('uibinder.startup.fatalErrorTitle'),
|
|
||||||
Lang.queryJS('uibinder.startup.fatalErrorMessage'),
|
|
||||||
Lang.queryJS('uibinder.startup.closeButton')
|
|
||||||
)
|
|
||||||
setOverlayHandler(() => {
|
|
||||||
const window = remote.getCurrentWindow()
|
|
||||||
window.close()
|
|
||||||
})
|
|
||||||
toggleOverlay(true)
|
|
||||||
})
|
|
||||||
}, 750)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common functions to perform after refreshing the distro index.
|
|
||||||
*
|
|
||||||
* @param {Object} data The distro index object.
|
|
||||||
*/
|
|
||||||
function onDistroRefresh(data){
|
|
||||||
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
|
|
||||||
refreshServerStatus()
|
|
||||||
initNews()
|
|
||||||
syncModConfigurations(data)
|
|
||||||
ensureJavaSettings(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync the mod configurations with the distro index.
|
|
||||||
*
|
|
||||||
* @param {Object} data The distro index object.
|
|
||||||
*/
|
|
||||||
function syncModConfigurations(data){
|
|
||||||
|
|
||||||
const syncedCfgs = []
|
|
||||||
|
|
||||||
for(let serv of data.servers){
|
|
||||||
|
|
||||||
const id = serv.rawServer.id
|
|
||||||
const mdls = serv.modules
|
|
||||||
const cfg = ConfigManager.getModConfiguration(id)
|
|
||||||
|
|
||||||
if(cfg != null){
|
|
||||||
|
|
||||||
const modsOld = cfg.mods
|
|
||||||
const mods = {}
|
|
||||||
|
|
||||||
for(let mdl of mdls){
|
|
||||||
const type = mdl.rawModule.type
|
|
||||||
|
|
||||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
|
||||||
if(!mdl.getRequired().value){
|
|
||||||
const mdlID = mdl.getVersionlessMavenIdentifier()
|
|
||||||
if(modsOld[mdlID] == null){
|
|
||||||
mods[mdlID] = scanOptionalSubModules(mdl.subModules, mdl)
|
|
||||||
} else {
|
|
||||||
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.subModules, mdl), false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(mdl.subModules.length > 0){
|
|
||||||
const mdlID = mdl.getVersionlessMavenIdentifier()
|
|
||||||
const v = scanOptionalSubModules(mdl.subModules, mdl)
|
|
||||||
if(typeof v === 'object'){
|
|
||||||
if(modsOld[mdlID] == null){
|
|
||||||
mods[mdlID] = v
|
|
||||||
} else {
|
|
||||||
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
syncedCfgs.push({
|
|
||||||
id,
|
|
||||||
mods
|
|
||||||
})
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const mods = {}
|
|
||||||
|
|
||||||
for(let mdl of mdls){
|
|
||||||
const type = mdl.rawModule.type
|
|
||||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
|
||||||
if(!mdl.getRequired().value){
|
|
||||||
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
|
|
||||||
} else {
|
|
||||||
if(mdl.subModules.length > 0){
|
|
||||||
const v = scanOptionalSubModules(mdl.subModules, mdl)
|
|
||||||
if(typeof v === 'object'){
|
|
||||||
mods[mdl.getVersionlessMavenIdentifier()] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
syncedCfgs.push({
|
|
||||||
id,
|
|
||||||
mods
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigManager.setModConfigurations(syncedCfgs)
|
|
||||||
ConfigManager.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure java configurations are present for the available servers.
|
|
||||||
*
|
|
||||||
* @param {Object} data The distro index object.
|
|
||||||
*/
|
|
||||||
function ensureJavaSettings(data) {
|
|
||||||
|
|
||||||
// Nothing too fancy for now.
|
|
||||||
for(const serv of data.servers){
|
|
||||||
ConfigManager.ensureJavaConfig(serv.rawServer.id, serv.effectiveJavaOptions, serv.rawServer.javaOptions?.ram)
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigManager.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively scan for optional sub modules. If none are found,
|
|
||||||
* this function returns a boolean. If optional sub modules do exist,
|
|
||||||
* a recursive configuration object is returned.
|
|
||||||
*
|
|
||||||
* @returns {boolean | Object} The resolved mod configuration.
|
|
||||||
*/
|
|
||||||
function scanOptionalSubModules(mdls, origin){
|
|
||||||
if(mdls != null){
|
|
||||||
const mods = {}
|
|
||||||
|
|
||||||
for(let mdl of mdls){
|
|
||||||
const type = mdl.rawModule.type
|
|
||||||
// Optional types.
|
|
||||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
|
||||||
// It is optional.
|
|
||||||
if(!mdl.getRequired().value){
|
|
||||||
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
|
|
||||||
} else {
|
|
||||||
if(mdl.hasSubModules()){
|
|
||||||
const v = scanOptionalSubModules(mdl.subModules, mdl)
|
|
||||||
if(typeof v === 'object'){
|
|
||||||
mods[mdl.getVersionlessMavenIdentifier()] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Object.keys(mods).length > 0){
|
|
||||||
const ret = {
|
|
||||||
mods
|
|
||||||
}
|
|
||||||
if(!origin.getRequired().value){
|
|
||||||
ret.value = origin.getRequired().def
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return origin.getRequired().def
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively merge an old configuration into a new configuration.
|
|
||||||
*
|
|
||||||
* @param {boolean | Object} o The old configuration value.
|
|
||||||
* @param {boolean | Object} n The new configuration value.
|
|
||||||
* @param {boolean} nReq If the new value is a required mod.
|
|
||||||
*
|
|
||||||
* @returns {boolean | Object} The merged configuration.
|
|
||||||
*/
|
|
||||||
function mergeModConfiguration(o, n, nReq = false){
|
|
||||||
if(typeof o === 'boolean'){
|
|
||||||
if(typeof n === 'boolean') return o
|
|
||||||
else if(typeof n === 'object'){
|
|
||||||
if(!nReq){
|
|
||||||
n.value = o
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
} else if(typeof o === 'object'){
|
|
||||||
if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true
|
|
||||||
else if(typeof n === 'object'){
|
|
||||||
if(!nReq){
|
|
||||||
n.value = typeof o.value !== 'undefined' ? o.value : true
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMods = Object.keys(n.mods)
|
|
||||||
for(let i=0; i<newMods.length; i++){
|
|
||||||
|
|
||||||
const mod = newMods[i]
|
|
||||||
if(o.mods[mod] != null){
|
|
||||||
n.mods[mod] = mergeModConfiguration(o.mods[mod], n.mods[mod])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If for some reason we haven't been able to merge,
|
|
||||||
// wipe the old value and use the new one. Just to be safe
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateSelectedAccount(){
|
|
||||||
const selectedAcc = ConfigManager.getSelectedAccount()
|
|
||||||
if(selectedAcc != null){
|
|
||||||
const val = await AuthManager.validateSelected()
|
|
||||||
if(!val){
|
|
||||||
ConfigManager.removeAuthAccount(selectedAcc.uuid)
|
|
||||||
ConfigManager.save()
|
|
||||||
const accLen = Object.keys(ConfigManager.getAuthAccounts()).length
|
|
||||||
setOverlayContent(
|
|
||||||
Lang.queryJS('uibinder.validateAccount.failedMessageTitle'),
|
|
||||||
accLen > 0
|
|
||||||
? Lang.queryJS('uibinder.validateAccount.failedMessage', { 'account': selectedAcc.displayName })
|
|
||||||
: Lang.queryJS('uibinder.validateAccount.failedMessageSelectAnotherAccount', { 'account': selectedAcc.displayName }),
|
|
||||||
Lang.queryJS('uibinder.validateAccount.loginButton'),
|
|
||||||
Lang.queryJS('uibinder.validateAccount.selectAnotherAccountButton')
|
|
||||||
)
|
|
||||||
setOverlayHandler(() => {
|
|
||||||
|
|
||||||
const isMicrosoft = selectedAcc.type === 'microsoft'
|
|
||||||
|
|
||||||
if(isMicrosoft) {
|
|
||||||
// Empty for now
|
|
||||||
} else {
|
|
||||||
// Mojang
|
|
||||||
// For convenience, pre-populate the username of the account.
|
|
||||||
document.getElementById('loginUsername').value = selectedAcc.username
|
|
||||||
validateEmail(selectedAcc.username)
|
|
||||||
}
|
|
||||||
|
|
||||||
loginOptionsViewOnLoginSuccess = getCurrentView()
|
|
||||||
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
|
||||||
|
|
||||||
if(accLen > 0) {
|
|
||||||
loginOptionsViewOnCancel = getCurrentView()
|
|
||||||
loginOptionsViewCancelHandler = () => {
|
|
||||||
if(isMicrosoft) {
|
|
||||||
ConfigManager.addMicrosoftAuthAccount(
|
|
||||||
selectedAcc.uuid,
|
|
||||||
selectedAcc.accessToken,
|
|
||||||
selectedAcc.username,
|
|
||||||
selectedAcc.expiresAt,
|
|
||||||
selectedAcc.microsoft.access_token,
|
|
||||||
selectedAcc.microsoft.refresh_token,
|
|
||||||
selectedAcc.microsoft.expires_at
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
|
|
||||||
}
|
|
||||||
ConfigManager.save()
|
|
||||||
validateSelectedAccount()
|
|
||||||
}
|
|
||||||
loginOptionsCancelEnabled(true)
|
|
||||||
} else {
|
|
||||||
loginOptionsCancelEnabled(false)
|
|
||||||
}
|
|
||||||
toggleOverlay(false)
|
|
||||||
switchView(getCurrentView(), VIEWS.loginOptions)
|
|
||||||
})
|
|
||||||
setDismissHandler(() => {
|
|
||||||
if(accLen > 1){
|
|
||||||
prepareAccountSelectionList()
|
|
||||||
$('#overlayContent').fadeOut(250, () => {
|
|
||||||
bindOverlayKeys(true, 'accountSelectContent', true)
|
|
||||||
$('#accountSelectContent').fadeIn(250)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const accountsObj = ConfigManager.getAuthAccounts()
|
|
||||||
const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v])
|
|
||||||
// This function validates the account switch.
|
|
||||||
setSelectedAccount(accounts[0].uuid)
|
|
||||||
toggleOverlay(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
toggleOverlay(true, accLen > 0)
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporary function to update the selected account along
|
|
||||||
* with the relevent UI elements.
|
|
||||||
*
|
|
||||||
* @param {string} uuid The UUID of the account.
|
|
||||||
*/
|
|
||||||
function setSelectedAccount(uuid){
|
|
||||||
const authAcc = ConfigManager.setSelectedAccount(uuid)
|
|
||||||
ConfigManager.save()
|
|
||||||
updateSelectedAccount(authAcc)
|
|
||||||
validateSelectedAccount()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Synchronous Listener
|
|
||||||
document.addEventListener('readystatechange', async () => {
|
|
||||||
|
|
||||||
if (document.readyState === 'interactive' || document.readyState === 'complete'){
|
|
||||||
if(rscShouldLoad){
|
|
||||||
rscShouldLoad = false
|
|
||||||
if(!fatalStartupError){
|
|
||||||
const data = await DistroAPI.getDistribution()
|
|
||||||
await showMainUI(data)
|
|
||||||
} else {
|
|
||||||
showFatalStartupError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
// Actions that must be performed after the distribution index is downloaded.
|
|
||||||
ipcRenderer.on('distributionIndexDone', async (event, res) => {
|
|
||||||
if(res) {
|
|
||||||
const data = await DistroAPI.getDistribution()
|
|
||||||
syncModConfigurations(data)
|
|
||||||
ensureJavaSettings(data)
|
|
||||||
if(document.readyState === 'interactive' || document.readyState === 'complete'){
|
|
||||||
await showMainUI(data)
|
|
||||||
} else {
|
|
||||||
rscShouldLoad = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fatalStartupError = true
|
|
||||||
if(document.readyState === 'interactive' || document.readyState === 'complete'){
|
|
||||||
showFatalStartupError()
|
|
||||||
} else {
|
|
||||||
rscShouldLoad = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Util for development
|
|
||||||
async function devModeToggle() {
|
|
||||||
DistroAPI.toggleDevMode(true)
|
|
||||||
const data = await DistroAPI.refreshDistributionOrFallback()
|
|
||||||
ensureJavaSettings(data)
|
|
||||||
updateSelectedServer(data.servers[0])
|
|
||||||
syncModConfigurations(data)
|
|
||||||
}
|
|
@ -1,214 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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, shell, webFrame} = require('electron')
|
|
||||||
const remote = require('@electron/remote')
|
|
||||||
const isDev = require('./assets/js/isdev')
|
|
||||||
const { LoggerUtil } = require('helios-core')
|
|
||||||
const Lang = require('./assets/js/langloader')
|
|
||||||
|
|
||||||
const loggerUICore = LoggerUtil.getLogger('UICore')
|
|
||||||
const loggerAutoUpdater = LoggerUtil.getLogger('AutoUpdater')
|
|
||||||
|
|
||||||
// Log deprecation and process warnings.
|
|
||||||
process.traceProcessWarnings = true
|
|
||||||
process.traceDeprecation = true
|
|
||||||
|
|
||||||
// Disable eval function.
|
|
||||||
// eslint-disable-next-line
|
|
||||||
window.eval = global.eval = function () {
|
|
||||||
throw new Error('Sorry, this app does not support window.eval().')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display warning when devtools window is opened.
|
|
||||||
remote.getCurrentWebContents().on('devtools-opened', () => {
|
|
||||||
console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold')
|
|
||||||
console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px')
|
|
||||||
console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Disable zoom, needed for darwin.
|
|
||||||
webFrame.setZoomLevel(0)
|
|
||||||
webFrame.setVisualZoomLevelLimits(1, 1)
|
|
||||||
|
|
||||||
// Initialize auto updates in production environments.
|
|
||||||
let updateCheckListener
|
|
||||||
if(!isDev){
|
|
||||||
ipcRenderer.on('autoUpdateNotification', (event, arg, info) => {
|
|
||||||
switch(arg){
|
|
||||||
case 'checking-for-update':
|
|
||||||
loggerAutoUpdater.info('Checking for update..')
|
|
||||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkingForUpdateButton'), true)
|
|
||||||
break
|
|
||||||
case 'update-available':
|
|
||||||
loggerAutoUpdater.info('New update available', info.version)
|
|
||||||
|
|
||||||
if(process.platform === 'darwin'){
|
|
||||||
info.darwindownload = `https://git.onimai.ru/ONIMAI-SMP/Launcher/releases/download/v${info.version}/ONIMAI.RU-MC-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg`
|
|
||||||
showUpdateUI(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
populateSettingsUpdateInformation(info)
|
|
||||||
break
|
|
||||||
case 'update-downloaded':
|
|
||||||
loggerAutoUpdater.info('Update ' + info.version + ' ready to be installed.')
|
|
||||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.installNowButton'), false, () => {
|
|
||||||
if(!isDev){
|
|
||||||
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
showUpdateUI(info)
|
|
||||||
break
|
|
||||||
case 'update-not-available':
|
|
||||||
loggerAutoUpdater.info('No new update found.')
|
|
||||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkForUpdatesButton'))
|
|
||||||
break
|
|
||||||
case 'ready':
|
|
||||||
updateCheckListener = setInterval(() => {
|
|
||||||
ipcRenderer.send('autoUpdateAction', 'checkForUpdate')
|
|
||||||
}, 1800000)
|
|
||||||
ipcRenderer.send('autoUpdateAction', 'checkForUpdate')
|
|
||||||
break
|
|
||||||
case 'realerror':
|
|
||||||
if(info != null && info.code != null){
|
|
||||||
if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){
|
|
||||||
loggerAutoUpdater.info('No suitable releases found.')
|
|
||||||
} else if(info.code === 'ERR_XML_MISSED_ELEMENT'){
|
|
||||||
loggerAutoUpdater.info('No releases found.')
|
|
||||||
} else {
|
|
||||||
loggerAutoUpdater.error('Error during update check..', info)
|
|
||||||
loggerAutoUpdater.debug('Error Code:', info.code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
loggerAutoUpdater.info('Unknown argument', arg)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a notification to the main process changing the value of
|
|
||||||
* allowPrerelease. If we are running a prerelease version, then
|
|
||||||
* this will always be set to true, regardless of the current value
|
|
||||||
* of val.
|
|
||||||
*
|
|
||||||
* @param {boolean} val The new allow prerelease value.
|
|
||||||
*/
|
|
||||||
function changeAllowPrerelease(val){
|
|
||||||
ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', 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').onclick = () => {
|
|
||||||
/*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later')
|
|
||||||
setOverlayHandler(() => {
|
|
||||||
if(!isDev){
|
|
||||||
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
|
|
||||||
} else {
|
|
||||||
console.error('Cannot install updates in development environment.')
|
|
||||||
toggleOverlay(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setDismissHandler(() => {
|
|
||||||
toggleOverlay(false)
|
|
||||||
})
|
|
||||||
toggleOverlay(true, true)*/
|
|
||||||
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
|
|
||||||
settingsNavItemListener(document.getElementById('settingsNavUpdate'), false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* jQuery Example
|
|
||||||
$(function(){
|
|
||||||
loggerUICore.info('UICore Initialized');
|
|
||||||
})*/
|
|
||||||
|
|
||||||
document.addEventListener('readystatechange', function () {
|
|
||||||
if (document.readyState === 'interactive'){
|
|
||||||
loggerUICore.info('UICore Initializing..')
|
|
||||||
|
|
||||||
// Bind close button.
|
|
||||||
Array.from(document.getElementsByClassName('fCb')).map((val) => {
|
|
||||||
val.addEventListener('click', e => {
|
|
||||||
const window = remote.getCurrentWindow()
|
|
||||||
window.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Bind restore down button.
|
|
||||||
Array.from(document.getElementsByClassName('fRb')).map((val) => {
|
|
||||||
val.addEventListener('click', e => {
|
|
||||||
const window = remote.getCurrentWindow()
|
|
||||||
if(window.isMaximized()){
|
|
||||||
window.unmaximize()
|
|
||||||
} else {
|
|
||||||
window.maximize()
|
|
||||||
}
|
|
||||||
document.activeElement.blur()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Bind minimize button.
|
|
||||||
Array.from(document.getElementsByClassName('fMb')).map((val) => {
|
|
||||||
val.addEventListener('click', e => {
|
|
||||||
const window = remote.getCurrentWindow()
|
|
||||||
window.minimize()
|
|
||||||
document.activeElement.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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
} else if(document.readyState === 'complete'){
|
|
||||||
|
|
||||||
//266.01
|
|
||||||
//170.8
|
|
||||||
//53.21
|
|
||||||
// Bind progress bar length to length of bot wrapper
|
|
||||||
//const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width
|
|
||||||
//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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open web links in the user's default browser.
|
|
||||||
*/
|
|
||||||
$(document).on('click', 'a[href^="http"]', function(event) {
|
|
||||||
event.preventDefault()
|
|
||||||
shell.openExternal(this.href)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens DevTools window if you hold (ctrl + shift + i).
|
|
||||||
* This will crash the program if you are using multiple
|
|
||||||
* DevTools, for example the chrome debugger in VS Code.
|
|
||||||
*/
|
|
||||||
document.addEventListener('keydown', function (e) {
|
|
||||||
if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){
|
|
||||||
let window = remote.getCurrentWindow()
|
|
||||||
window.toggleDevTools()
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script for welcome.ejs
|
|
||||||
*/
|
|
||||||
document.getElementById('welcomeButton').addEventListener('click', e => {
|
|
||||||
loginOptionsCancelEnabled(false) // False by default, be explicit.
|
|
||||||
loginOptionsViewOnLoginSuccess = VIEWS.landing
|
|
||||||
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
|
||||||
switchView(VIEWS.welcome, VIEWS.loginOptions)
|
|
||||||
})
|
|
@ -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.
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|