Compare commits

..

No commits in common. "master" and "legacy" have entirely different histories.

127 changed files with 6671 additions and 20766 deletions

View File

@ -1 +0,0 @@
dist

View File

@ -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
View File

@ -1,3 +0,0 @@
github: dscalzi
patreon: dscalzi
custom: ['https://www.paypal.me/dscalzi']

View File

@ -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
View File

@ -1,6 +1,5 @@
/node_modules/ /node_modules/
/.vs/ /.vs/
/.vscode/ /.vscode/
/target/ /mcfiles/
/logs/ /logs/
/dist/

1
.nvmrc
View File

@ -1 +0,0 @@
20

View File

@ -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
View File

@ -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 ###
![Screenshot 1](https://i.imgur.com/6o7SmH6.png) 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.
![Screenshot 2](https://i.imgur.com/x3B34n1.png)
## 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://img.shields.io/github/release/dscalzi/HeliosLauncher.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases/latest)
#### Latest Pre-Release
[![](https://img.shields.io/github/release/dscalzi/HeliosLauncher/all.svg?style=flat-square)](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.
![console example](https://i.imgur.com/T5e73jP.png) ```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 [![Discord](https://discordapp.com/api/guilds/98469309352775680/widget.png)](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](https://discordapp.com/api/guilds/211524927831015424/embed.png?style=banner3)][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
View 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

View File

@ -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
View 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
View 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;
}

File diff suppressed because it is too large Load Diff

351
app/assets/css/styles.css Normal file
View 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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View 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
View 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
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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))
}
}
}

View File

@ -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'
}

View File

@ -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))

View File

@ -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')
}

View 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"
}
}
]
}
]
}
]
}

View 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
View 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){
}
}

View File

@ -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.')
}
})

View File

@ -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
View 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
}
})

File diff suppressed because it is too large Load Diff

View File

@ -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)
})
})

View File

@ -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
}
})
}

View File

@ -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()
}

File diff suppressed because it is too large Load Diff

View File

@ -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)
}

View File

@ -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()
}
})

View File

@ -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)
})

View File

@ -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.
})
})
}

Some files were not shown because too many files have changed in this diff Show More