Compare commits
397 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
258cd0d421 | ||
|
f65eb2f2d6 | ||
|
fb1cb7b415 | ||
|
9b898cc033 | ||
|
5a6217467a | ||
|
3d470d9a32 | ||
|
cf7fd2f411 | ||
|
16790ca416 | ||
|
7e95771957 | ||
|
ab7e3c301c | ||
|
b019f40802 | ||
|
9d80d3b1d5 | ||
|
92f2aab2a1 | ||
|
06ba2ebe69 | ||
|
e6cf76b436 | ||
|
5de3acb96a | ||
|
d89b270c20 | ||
|
9e26b288fc | ||
|
22d2e064f3 | ||
|
ab3d4e9499 | ||
|
3295430c54 | ||
|
e4ddf898f9 | ||
|
22233831dc | ||
|
9c9f70a7ca | ||
|
11f5501a25 | ||
|
e96231579e | ||
|
776c46d7e1 | ||
|
47378d63d2 | ||
|
ac8724c868 | ||
|
ba265af4e9 | ||
|
aa9a03c7b1 | ||
|
ee96980dee | ||
|
2f27cdbaf4 | ||
|
c9aeb0e0aa | ||
|
2abe214005 | ||
|
f3c1e42a03 | ||
|
e639061fa8 | ||
|
e3af7669d8 | ||
|
a731fa90ea | ||
|
3ef5fabb35 | ||
|
12a84c1cce | ||
|
a1837aa20e | ||
|
16ad59685e | ||
|
15f7560916 | ||
|
e314599d99 | ||
|
28c9c65220 | ||
|
43b26ef1b9 | ||
|
e9a5f80a36 | ||
|
9a4129c11a | ||
|
b32857e7de | ||
|
a22bd32cb1 | ||
|
45630c068c | ||
|
fb586cca69 | ||
|
ecfe0de0fb | ||
|
1c598e7980 | ||
|
248937c22d | ||
|
5d44cc3408 | ||
|
9224531b62 | ||
|
e3ee03ef73 | ||
|
190bb4cf85 | ||
|
fa47c9c42c | ||
|
e7dd171cea | ||
|
2e1ab3c266 | ||
|
0a80a3b073 | ||
|
ee61ea4979 | ||
|
4e2c9ce3ec | ||
|
0bc74d9c66 | ||
|
d7d0803661 | ||
|
15fc12b625 | ||
|
ccf099a5cf | ||
|
b092722488 | ||
|
fc289262a8 | ||
|
a8c92ab272 | ||
|
f6b787c526 | ||
|
78a4f7bbf3 | ||
|
58e68c116c | ||
|
ad47617cd0 | ||
|
2fdb217e64 | ||
|
c1d36d2b03 | ||
|
9c6d75f812 | ||
|
a2168da999 | ||
|
430e840514 | ||
|
f9e4fd8561 | ||
|
54e6572754 | ||
|
79135f310d | ||
|
31a51b8e7f | ||
|
84c13e6972 | ||
|
1b48ad58e6 | ||
|
2c487f71ad | ||
|
61dbabdba3 | ||
|
cd1ca7edf5 | ||
|
be4a42b50a | ||
|
48d0c9e549 | ||
|
a9fa7c4ff9 | ||
|
cb8d1bb00f | ||
|
2743585b12 | ||
|
a6ca2e800c | ||
|
779a9a54ec | ||
|
8723a192b4 | ||
|
c93d4922a6 | ||
|
d48abf444c | ||
|
3ea41b42e5 | ||
|
faa5f5267b | ||
|
c0776dcf61 | ||
|
25e7e5aa55 | ||
|
17e36fa5a2 | ||
|
2156099ea8 | ||
|
9a2c1fd9b9 | ||
|
cc86f2a257 | ||
|
e6897dad2c | ||
|
bd19b16530 | ||
|
46853157ec | ||
|
0203295e7c | ||
|
6b755fef15 | ||
|
bace2e12e1 | ||
|
eeaa2e98d0 | ||
|
bec1385c55 | ||
|
f795b28d23 | ||
|
ef8f36b519 | ||
|
b09cd2ef28 | ||
|
1bdb413ab5 | ||
|
71b25d3e5c | ||
|
7f821f36d7 | ||
|
1430d0faa2 | ||
|
8726638a23 | ||
|
b1abe07aeb | ||
|
b3f8ff9595 | ||
|
64dfc541dc | ||
|
c151744ff9 | ||
|
9d1aa497a7 | ||
|
141a753893 | ||
|
8b5a7bb02e | ||
|
1a2e9f3be4 | ||
|
5fc6be444a | ||
|
96db607912 | ||
|
1110119df0 | ||
|
4f504cd470 | ||
|
19ee187f10 | ||
|
9761c1d9d0 | ||
|
8b04f476ee | ||
|
90cd2c4614 | ||
|
da706813ba | ||
|
d087ca5a60 | ||
|
28631610ab | ||
|
cbc8b07467 | ||
|
590cefc5d4 | ||
|
0194c2b6f1 | ||
|
d94365f535 | ||
|
555cb0125c | ||
|
68362021f5 | ||
|
b54b206b9a | ||
|
ef112cef3a | ||
|
887bb67ab0 | ||
|
c141475404 | ||
|
a0520a9458 | ||
|
448440a604 | ||
|
90818245f3 | ||
|
644a32de37 | ||
|
5c0a293390 | ||
|
1fc118ee8c | ||
|
e08c3a903a | ||
|
81367bc619 | ||
|
e8e7f85c64 | ||
|
c834ca971a | ||
|
6b96770c98 | ||
|
46f8b195d6 | ||
|
6d1ecd73ee | ||
|
c74e3f0676 | ||
|
8bd040aece | ||
|
90651d4fbe | ||
|
3e20b1f63a | ||
|
8c0bf8faac | ||
|
cb12c0786e | ||
|
de15440e6d | ||
|
e12197526d | ||
|
d2982ca387 | ||
|
d7fe519923 | ||
|
be533af38b | ||
|
b98a4ec21f | ||
|
a4d4b69791 | ||
|
f5b1418d18 | ||
|
f65793c0fb | ||
|
001a2cbe81 | ||
|
f613bdba4d | ||
|
f5ce7734b8 | ||
|
d779eacf61 | ||
|
d9c9b32446 | ||
|
d9858958c9 | ||
|
9d5f708dfc | ||
|
684e884d9c | ||
|
79d1d59451 | ||
|
c6051f9942 | ||
|
acd6143d30 | ||
|
11cb9e6c28 | ||
|
bcd8082afe | ||
|
03273232c6 | ||
|
37d35a6938 | ||
|
affacbf56e | ||
|
e6b9728fe5 | ||
|
ee2a4a9319 | ||
|
a9f12ee329 | ||
|
eb946d6a2a | ||
|
b7f03fa778 | ||
|
ce86840a87 | ||
|
96d08955a4 | ||
|
2b10133f88 | ||
|
92a8b431d6 | ||
|
1a3acec994 | ||
|
8a7b947500 | ||
|
f089993ea4 | ||
|
86c7245c41 | ||
|
23cb2b8164 | ||
|
d54a20a002 | ||
|
e6874b50bb | ||
|
b93ecf29e3 | ||
|
2da80c2047 | ||
|
5c9b5d9398 | ||
|
2090021b30 | ||
|
3acc213544 | ||
|
b2e9223b10 | ||
|
72f822d2b2 | ||
|
53fcf6773a | ||
|
514b952cf4 | ||
|
8fc2ebf831 | ||
|
2a9db6c646 | ||
|
7851fdbefb | ||
|
e53b92c38b | ||
|
cf7e9adb89 | ||
|
db5653a7b7 | ||
|
70b83a6397 | ||
|
29a8f2a345 | ||
|
0346597afe | ||
|
37606dc8d2 | ||
|
afe69a21c6 | ||
|
58a8215b05 | ||
|
845721a830 | ||
|
50317c3fc2 | ||
|
d581a2896a | ||
|
e602848f9c | ||
|
4d2f50b136 | ||
|
f1d89c0e6b | ||
|
4f416220c2 | ||
|
1566ff4e4c | ||
|
96d393696d | ||
|
9eef48b110 | ||
|
2ec91a633b | ||
|
b46ac97493 | ||
|
556199aa55 | ||
|
ff3f2bfb8d | ||
|
7301dfc71a | ||
|
0c98cc2447 | ||
|
60ba7b4f8a | ||
|
34b806c3a8 | ||
|
40506c4283 | ||
|
e583133c8b | ||
|
6e71cd6b46 | ||
|
5bceaa92d0 | ||
|
372a76b0f2 | ||
|
04cf2ee718 | ||
|
cb28bce992 | ||
|
810e81521c | ||
|
ededf85892 | ||
|
7dcce68455 | ||
|
5b0b1924cf | ||
|
97979df431 | ||
|
6759b143c4 | ||
|
41aae0c418 | ||
|
3131002d02 | ||
|
651c2d6f35 | ||
|
85331a7088 | ||
|
52d90276bf | ||
|
13a6dcea63 | ||
|
8f172a41e6 | ||
|
dab195a996 | ||
|
aa48a756c5 | ||
|
8ab5b2db1d | ||
|
932443fcb8 | ||
|
aa0e1a20ca | ||
|
145a2fe77b | ||
|
4196856d31 | ||
|
9769458499 | ||
|
c0714ecbc6 | ||
|
a68abaf89c | ||
|
f44d3b69d3 | ||
|
2d0c4c76eb | ||
|
e2e48f6444 | ||
|
5a16416db5 | ||
|
e7752b4374 | ||
|
109c24bc79 | ||
|
7cf0e1f049 | ||
|
b61a9a2c55 | ||
|
6ac48a63b5 | ||
|
34e8da2aa2 | ||
|
1fd69207c0 | ||
|
db7ba0d450 | ||
|
08eb04775e | ||
|
05fe516249 | ||
|
50d85d30cc | ||
|
790a3e0e8b | ||
|
0cc861f614 | ||
|
97e9c15baf | ||
|
74a60a61c2 | ||
|
91c842dd40 | ||
|
2dcbb45bdb | ||
|
30c258da2d | ||
|
ee55446cd6 | ||
|
daa6faac86 | ||
|
5a692d9088 | ||
|
e9e2ec162a | ||
|
a67dac23cf | ||
|
ba916aa953 | ||
|
f5f5b72bed | ||
|
f4abbef58c | ||
|
71cbd109c4 | ||
|
d2c435ce51 | ||
|
54e3861ba8 | ||
|
49bad485f6 | ||
|
0d11749ad4 | ||
|
e3890b2057 | ||
|
f0a66e7a02 | ||
|
848440ed1c | ||
|
f0b21330a0 | ||
|
d33476bcf9 | ||
|
f1a98f2d45 | ||
|
15a83a7736 | ||
|
f161e196be | ||
|
cd4f7918c8 | ||
|
0c1ebd0ce0 | ||
|
40de1e3cd3 | ||
|
252b82a944 | ||
|
39fd7e19ef | ||
|
4106b2b069 | ||
|
ea758aee1f | ||
|
d08cfbf248 | ||
|
0216582827 | ||
|
f1cf433ca8 | ||
|
95afe5c63a | ||
|
5b74ecef21 | ||
|
c6637d18e1 | ||
|
1a7c8fd70f | ||
|
009a1b58af | ||
|
ae3c8854f4 | ||
|
5fe43ac8e9 | ||
|
4b708f59fe | ||
|
22f5eabe49 | ||
|
6e55442b25 | ||
|
4b8133474d | ||
|
2f66d44824 | ||
|
92d8a5e254 | ||
|
1b38629084 | ||
|
4d26298b98 | ||
|
8d5cd2b00b | ||
|
f7e24fd092 | ||
|
61538fdde5 | ||
|
9d04eb2227 | ||
|
e994f4c474 | ||
|
714daace18 | ||
|
8446af4669 | ||
|
6e8d4fe9bc | ||
|
9448b9b5a3 | ||
|
2f899822b5 | ||
|
be39d60705 | ||
|
5475ac0c69 | ||
|
631c3cd6d4 | ||
|
66a3854a24 | ||
|
b5386c0257 | ||
|
28cd147ca0 | ||
|
12aa9f9c5b | ||
|
fc81016dc6 | ||
|
2a551f18ba | ||
|
f257208e2f | ||
|
05ebb80f19 | ||
|
4eb9d267eb | ||
|
40a02726ad | ||
|
8d682b15b3 | ||
|
9b4a2d4ef9 | ||
|
07946714cc | ||
|
a16a22e2e1 | ||
|
5a8ae0485a | ||
|
0a79634b8a | ||
|
9b63d9bb58 | ||
|
5335e0124b | ||
|
92cb88a23a | ||
|
c6f9121806 | ||
|
5c7e0c3c8a | ||
|
ec9e95c130 | ||
|
7fb33c6813 | ||
|
ae387757bc | ||
|
13cc555afd | ||
|
f8131d9322 | ||
|
aacf15efc5 | ||
|
2062865e7f | ||
|
4b2cac1eff | ||
|
4fd202d180 | ||
|
d3c5997baa | ||
|
5e8342aec9 | ||
|
454964ce0b |
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
dist
|
66
.eslintrc.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"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
Normal file
@ -0,0 +1,3 @@
|
||||
github: dscalzi
|
||||
patreon: dscalzi
|
||||
custom: ['https://www.paypal.me/dscalzi']
|
38
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
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: 18
|
||||
|
||||
- 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
|
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2022 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.
|
212
README.md
@ -1,39 +1,138 @@
|
||||
# Getting Started #
|
||||
<p align="center"><img src="./app/assets/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
|
||||
|
||||
System Requirements:
|
||||
* [Node.js](https://nodejs.org/en/) v8.9.0+
|
||||
<h1 align="center">Helios Launcher</h1>
|
||||
|
||||
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.
|
||||
<em><h5 align="center">(formerly Electron Launcher)</h5></em>
|
||||
|
||||
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"><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>
|
||||
|
||||
### Recommended IDE ###
|
||||
<p align="center">Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.</p>
|
||||
|
||||
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 1](https://i.imgur.com/6o7SmH6.png)
|
||||
![Screenshot 2](https://i.imgur.com/x3B34n1.png)
|
||||
|
||||
### Installation ###
|
||||
## Features
|
||||
|
||||
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.
|
||||
* 🔒 Full account management.
|
||||
* 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.
|
||||
|
||||
```shell
|
||||
npm install
|
||||
This is not an exhaustive list. Download and install the launcher to gauge all it can do!
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
# Launching #
|
||||
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.
|
||||
|
||||
### Command Line ###
|
||||
#### Export Output to a File
|
||||
|
||||
There are several different ways to launch this project. One way is simply to run the following command on the command line.
|
||||
If you want to export the console output, simply right click anywhere on the console and click **Save as..**
|
||||
|
||||
```shell
|
||||
npm start
|
||||
![console example](https://i.imgur.com/T5e73jP.png)
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
This section details the setup of a basic developmentment environment.
|
||||
|
||||
### Getting Started
|
||||
|
||||
**System Requirements**
|
||||
|
||||
* [Node.js][nodejs] v18
|
||||
|
||||
---
|
||||
|
||||
**Clone and Install Dependencies**
|
||||
|
||||
```console
|
||||
> git clone https://github.com/dscalzi/HeliosLauncher.git
|
||||
> cd HeliosLauncher
|
||||
> npm install
|
||||
```
|
||||
|
||||
### Visual Studio Code ###
|
||||
---
|
||||
|
||||
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).
|
||||
**Launch Application**
|
||||
|
||||
```json
|
||||
```console
|
||||
> 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",
|
||||
"configurations": [
|
||||
@ -41,67 +140,72 @@ If you use VS Code, you can run this directly from the IDE. Copy the following c
|
||||
"name": "Debug Main Process",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"program": "${workspaceRoot}/index.js",
|
||||
"console": "integratedTerminal",
|
||||
"protocol": "inspector",
|
||||
"timeout": 100000000
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/node_modules/electron/cli.js",
|
||||
"args" : ["."],
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"name": "Debug Renderer Process",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"${workspaceRoot}/index.js",
|
||||
"${workspaceFolder}/.",
|
||||
"--remote-debugging-port=9222"
|
||||
],
|
||||
"webRoot": "${workspaceRoot}"
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
||||
This adds two debug configurations.
|
||||
|
||||
You can find more information [here](http://code.matsu.io/1).
|
||||
#### Debug Main Process
|
||||
|
||||
### Notes on DevTools Window ###
|
||||
This allows you to debug Electron's [main process][mainprocess]. You can debug scripts in the [renderer process][rendererprocess] by opening the DevTools Window.
|
||||
|
||||
Once you run the program, you can open the DevTools window by using the following keybind.
|
||||
#### Debug Renderer Process
|
||||
|
||||
```shell
|
||||
ctrl + shift + i
|
||||
```
|
||||
This allows you to debug Electron's [renderer process][rendererprocess]. This requires you to install the [Debugger for Chrome][chromedebugger] extension.
|
||||
|
||||
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.
|
||||
Note that you **cannot** open the DevTools window while using this debug configuration. Chromium only allows one debugger, opening another will crash the program.
|
||||
|
||||
# Building
|
||||
---
|
||||
|
||||
Run either of the build scrips noted below. Note that each platform can only be build when running on the specific operating system. Currently investigating ways to run multi-platform builds efficiently.
|
||||
### Note on Third-Party Usage
|
||||
|
||||
### Build for Current Platform
|
||||
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.
|
||||
|
||||
* `npm run dist`
|
||||
For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md.
|
||||
|
||||
### Platforms Specifc Builds
|
||||
---
|
||||
|
||||
* Windows x64 (win32 x64)
|
||||
* `npm run dist:win`
|
||||
* MacOS (darwin x64)
|
||||
* `npm run dist:mac`
|
||||
* Linux (linux x64)
|
||||
* `npm run dist:linux`
|
||||
## Resources
|
||||
|
||||
# Issues / Further Support #
|
||||
* [Wiki][wiki]
|
||||
* [Nebula (Create Distribution.json)][nebula]
|
||||
* [v2 Rewrite Branch (Inactive)][v2branch]
|
||||
|
||||
If you run into any issue which cannot be resolved via a quick google search, create an issue using the tab above.
|
||||
The best way to contact the developers is on Discord.
|
||||
|
||||
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)
|
||||
[![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'
|
||||
|
41
app/app.ejs
@ -1,24 +1,49 @@
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Westeroscraft Launcher</title>
|
||||
<script src="./assets/js/uicore.js"></script>
|
||||
<script src="./assets/js/actionbinder.js"></script>
|
||||
<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/<%=0%>.jpg') no-repeat center center fixed;
|
||||
/*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>
|
||||
<% include frame.ejs %>
|
||||
<body bkid="<%=bkid%>">
|
||||
<%- include('frame') %>
|
||||
<div id="main">
|
||||
<% include login.ejs %>
|
||||
<%- 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>
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"repositoryRoot": "/modstore",
|
||||
"modRef": [
|
||||
"my.custom:mod:1.11.2",
|
||||
"another.custom:mod:1.11.2"
|
||||
],
|
||||
"parentList": "/mods/WesterosCraft-1.11.2.json"
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"repositoryRoot": "C:\\Users\\Asus\\Desktop\\LauncherElectron\\mcfiles\\modstore",
|
||||
"modRef": [
|
||||
"com.westeroscraft:westerosblocks:3.0.0-beta-71",
|
||||
"mezz:jei:1.11.2-4.3.5.277",
|
||||
"net.optifine:optifine:1.11.2_HD_U_B9",
|
||||
"chatbubbles:chatbubbles:1.0.1_for_1.11.2"
|
||||
]
|
||||
}
|
BIN
app/assets/images/LoadingSeal.png
Normal file
After Width: | Height: | Size: 244 KiB |
BIN
app/assets/images/LoadingText.png
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
app/assets/images/SealCircle.ico
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
app/assets/images/SealCircle.png
Normal file
After Width: | Height: | Size: 142 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 807 KiB |
Before Width: | Height: | Size: 361 KiB |
Before Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 969 KiB After Width: | Height: | Size: 502 KiB |
BIN
app/assets/images/backgrounds/5.jpg
Normal file
After Width: | Height: | Size: 456 KiB |
BIN
app/assets/images/backgrounds/6.jpg
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
app/assets/images/backgrounds/7.jpg
Normal file
After Width: | Height: | Size: 5.0 MiB |
7
app/assets/images/icons/microsoft.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<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>
|
After Width: | Height: | Size: 303 B |
5
app/assets/images/icons/mojang.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<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>
|
After Width: | Height: | Size: 664 B |
13
app/assets/images/icons/sevenstar.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<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>
|
After Width: | Height: | Size: 809 B |
14
app/assets/images/icons/sevenstar_circle.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<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>
|
After Width: | Height: | Size: 932 B |
8
app/assets/images/icons/sevenstar_circle_extended.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<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>
|
After Width: | Height: | Size: 822 B |
15
app/assets/images/icons/sevenstar_circle_hole.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<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>
|
After Width: | Height: | Size: 1018 B |
@ -0,0 +1,9 @@
|
||||
<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>
|
After Width: | Height: | Size: 907 B |
7
app/assets/images/icons/sevenstar_extended.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<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>
|
After Width: | Height: | Size: 700 B |
BIN
app/assets/images/minecraft.icns
Normal file
Before Width: | Height: | Size: 606 KiB |
Before Width: | Height: | Size: 227 KiB |
@ -1,249 +0,0 @@
|
||||
const cp = require('child_process')
|
||||
const path = require('path')
|
||||
const {AssetGuard} = require(path.join(__dirname, 'assets', 'js', 'assetguard.js'))
|
||||
const ProcessBuilder = require(path.join(__dirname, 'assets', 'js', 'processbuilder.js'))
|
||||
const ConfigManager = require(path.join(__dirname, 'assets', 'js', 'configmanager.js'))
|
||||
const DiscordWrapper = require(path.join(__dirname, 'assets', 'js', 'discordwrapper.js'))
|
||||
const Mojang = require(path.join(__dirname, 'assets', 'js', 'mojang.js'))
|
||||
const AuthManager = require(path.join(__dirname, 'assets', 'js', 'authmanager.js'))
|
||||
|
||||
let mojangStatusListener
|
||||
|
||||
// Launch Elements
|
||||
let launch_content, launch_details, launch_progress, launch_progress_label, launch_details_text
|
||||
|
||||
// Synchronous Listener
|
||||
document.addEventListener('readystatechange', function(){
|
||||
if (document.readyState === 'interactive'){
|
||||
|
||||
// Save a reference to the launch elements.
|
||||
launch_content = document.getElementById('launch_content')
|
||||
launch_details = document.getElementById('launch_details')
|
||||
launch_progress = document.getElementById('launch_progress')
|
||||
launch_progress_label = document.getElementById('launch_progress_label')
|
||||
launch_details_text = document.getElementById('launch_details_text')
|
||||
|
||||
// Bind launch button
|
||||
document.getElementById('launch_button').addEventListener('click', function(e){
|
||||
console.log('Launching game..')
|
||||
//testdownloads()
|
||||
dlAsync()
|
||||
})
|
||||
|
||||
// TODO convert this to dropdown menu.
|
||||
// Bind selected server
|
||||
document.getElementById('server_selection').innerHTML = '\u2022 ' + AssetGuard.getServerById(ConfigManager.getGameDirectory(), ConfigManager.getSelectedServer()).name
|
||||
|
||||
|
||||
// Update Mojang Status Color
|
||||
const refreshMojangStatuses = async function(){
|
||||
console.log('Refreshing Mojang Statuses..')
|
||||
try {
|
||||
let status = 'grey'
|
||||
const statuses = await Mojang.status()
|
||||
greenCount = 0
|
||||
for(let i=0; i<statuses.length; i++){
|
||||
if(statuses[i].status === 'yellow' && status !== 'red'){
|
||||
status = 'yellow'
|
||||
continue
|
||||
} else if(statuses[i].status === 'red'){
|
||||
status = 'red'
|
||||
break
|
||||
}
|
||||
++greenCount
|
||||
}
|
||||
if(greenCount == statuses.length){
|
||||
status = 'green'
|
||||
}
|
||||
|
||||
document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status)
|
||||
|
||||
} catch (err) {
|
||||
console.error('Unable to refresh Mojang service status..', err)
|
||||
}
|
||||
}
|
||||
|
||||
refreshMojangStatuses()
|
||||
// Set refresh rate to once every 5 minutes.
|
||||
mojangStatusListener = setInterval(refreshMojangStatuses, 300000)
|
||||
|
||||
}
|
||||
}, false)
|
||||
|
||||
// Keep reference to Minecraft Process
|
||||
let proc
|
||||
// Is DiscordRPC enabled
|
||||
let hasRPC = false
|
||||
// Joined server regex
|
||||
const servJoined = /[[0-2][0-9]:[0-6][0-9]:[0-6][0-9]\] \[Client thread\/INFO\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/g
|
||||
const gameJoined = /\[[0-2][0-9]:[0-6][0-9]:[0-6][0-9]\] \[Client thread\/WARN\]: Skipping bad option: lastServer:/g
|
||||
const gameJoined2 = /\[[0-2][0-9]:[0-6][0-9]:[0-6][0-9]\] \[Client thread\/INFO\]: Created: \d+x\d+ textures-atlas/g
|
||||
|
||||
let aEx
|
||||
let currentProc
|
||||
let serv
|
||||
let versionData
|
||||
let forgeData
|
||||
|
||||
function dlAsync(login = true){
|
||||
|
||||
// Login parameter is temporary for debug purposes. Allows testing the validation/downloads without
|
||||
// launching the game.
|
||||
|
||||
if(login) {
|
||||
if(ConfigManager.getSelectedAccount() == null){
|
||||
console.error('login first.')
|
||||
//in devtools AuthManager.addAccount(username, pass)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
launch_details_text.innerHTML = 'Please wait..'
|
||||
launch_progress.setAttribute('max', '100')
|
||||
launch_details.style.display = 'flex'
|
||||
launch_content.style.display = 'none'
|
||||
|
||||
aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
|
||||
ConfigManager.getGameDirectory(),
|
||||
ConfigManager.getJavaExecutable()
|
||||
])
|
||||
|
||||
aEx.on('message', (m) => {
|
||||
if(currentProc === 'validateDistribution'){
|
||||
|
||||
launch_progress.setAttribute('value', 20)
|
||||
launch_progress_label.innerHTML = '20%'
|
||||
serv = m.result
|
||||
console.log('forge stuff done')
|
||||
|
||||
// Begin version load.
|
||||
launch_details_text.innerHTML = 'Loading version information..'
|
||||
currentProc = 'loadVersionData'
|
||||
aEx.send({task: 0, content: currentProc, argsArr: [serv.mc_version]})
|
||||
|
||||
} else if(currentProc === 'loadVersionData'){
|
||||
|
||||
launch_progress.setAttribute('value', 40)
|
||||
launch_progress_label.innerHTML = '40%'
|
||||
versionData = m.result
|
||||
|
||||
// Begin asset validation.
|
||||
launch_details_text.innerHTML = 'Validating asset integrity..'
|
||||
currentProc = 'validateAssets'
|
||||
aEx.send({task: 0, content: currentProc, argsArr: [versionData]})
|
||||
|
||||
} else if(currentProc === 'validateAssets'){
|
||||
|
||||
launch_progress.setAttribute('value', 60)
|
||||
launch_progress_label.innerHTML = '60%'
|
||||
console.log('assets done')
|
||||
|
||||
// Begin library validation.
|
||||
launch_details_text.innerHTML = 'Validating library integrity..'
|
||||
currentProc = 'validateLibraries'
|
||||
aEx.send({task: 0, content: currentProc, argsArr: [versionData]})
|
||||
|
||||
} else if(currentProc === 'validateLibraries'){
|
||||
|
||||
launch_progress.setAttribute('value', 80)
|
||||
launch_progress_label.innerHTML = '80%'
|
||||
console.log('libs done')
|
||||
|
||||
// Begin miscellaneous validation.
|
||||
launch_details_text.innerHTML = 'Validating miscellaneous file integrity..'
|
||||
currentProc = 'validateMiscellaneous'
|
||||
aEx.send({task: 0, content: currentProc, argsArr: [versionData]})
|
||||
|
||||
} else if(currentProc === 'validateMiscellaneous'){
|
||||
|
||||
launch_progress.setAttribute('value', 100)
|
||||
launch_progress_label.innerHTML = '100%'
|
||||
console.log('files done')
|
||||
|
||||
launch_details_text.innerHTML = 'Downloading files..'
|
||||
currentProc = 'processDlQueues'
|
||||
aEx.send({task: 0, content: currentProc})
|
||||
|
||||
} else if(currentProc === 'processDlQueues'){
|
||||
if(m.task === 0){
|
||||
remote.getCurrentWindow().setProgressBar(m.value/m.total)
|
||||
launch_progress.setAttribute('max', m.total)
|
||||
launch_progress.setAttribute('value', m.value)
|
||||
launch_progress_label.innerHTML = m.percent + '%'
|
||||
} else if(m.task === 1){
|
||||
remote.getCurrentWindow().setProgressBar(-1)
|
||||
|
||||
launch_details_text.innerHTML = 'Preparing to launch..'
|
||||
currentProc = 'loadForgeData'
|
||||
aEx.send({task: 0, content: currentProc, argsArr: [serv.id]})
|
||||
|
||||
} else {
|
||||
console.error('Unknown download data type.', m)
|
||||
}
|
||||
} else if(currentProc === 'loadForgeData'){
|
||||
|
||||
forgeData = m.result
|
||||
|
||||
if(login) {
|
||||
//if(!(await AuthManager.validateSelected())){
|
||||
//
|
||||
//}
|
||||
const authUser = ConfigManager.getSelectedAccount();
|
||||
console.log('authu', authUser)
|
||||
let pb = new ProcessBuilder(ConfigManager.getGameDirectory(), serv, versionData, forgeData, authUser)
|
||||
launch_details_text.innerHTML = 'Launching game..'
|
||||
try{
|
||||
proc = pb.build()
|
||||
launch_details_text.innerHTML = 'Done. Enjoy the server!'
|
||||
const tempListener = function(data){
|
||||
if(data.indexOf('[Client thread/INFO]: -- System Details --') > -1){
|
||||
launch_details.style.display = 'none'
|
||||
launch_content.style.display = 'inline-flex'
|
||||
if(hasRPC){
|
||||
DiscordWrapper.updateDetails('Loading game..')
|
||||
}
|
||||
proc.stdout.removeListener('data', tempListener)
|
||||
}
|
||||
}
|
||||
const gameStateChange = function(data){
|
||||
if(servJoined.test(data)){
|
||||
DiscordWrapper.updateDetails('Exploring the Realm!')
|
||||
} else if(gameJoined.test(data)){
|
||||
DiscordWrapper.updateDetails('Idling on Main Menu')
|
||||
}
|
||||
}
|
||||
proc.stdout.on('data', tempListener)
|
||||
proc.stdout.on('data', gameStateChange)
|
||||
// Init Discord Hook (Untested)
|
||||
const distro = AssetGuard.retrieveDistributionDataSync(ConfigManager.getGameDirectory)
|
||||
if(distro.discord != null && serv.discord != null){
|
||||
DiscordWrapper.initRPC(distro.discord, serv.discord)
|
||||
hasRPC = true
|
||||
proc.on('close', (code, signal) => {
|
||||
console.log('Shutting down Discord Rich Presence..')
|
||||
DiscordWrapper.shutdownRPC()
|
||||
hasRPC = false
|
||||
proc = null
|
||||
})
|
||||
}
|
||||
} catch(err) {
|
||||
//launch_details_text.innerHTML = 'Error: ' + err.message;
|
||||
launch_details_text.innerHTML = 'Error: See log for details..';
|
||||
console.log(err)
|
||||
setTimeout(function(){
|
||||
launch_details.style.display = 'none'
|
||||
launch_content.style.display = 'inline-flex'
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from AssetExec
|
||||
aEx.disconnect()
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
launch_details_text.innerHTML = 'Loading server information..'
|
||||
currentProc = 'validateDistribution'
|
||||
aEx.send({task: 0, content: currentProc, argsArr: [ConfigManager.getSelectedServer()]})
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
const {AssetGuard} = require('./assetguard.js')
|
||||
|
||||
const tracker = new AssetGuard(process.argv[2], process.argv[3])
|
||||
console.log('AssetExec Started')
|
||||
|
||||
// Temporary for debug purposes.
|
||||
process.on('unhandledRejection', r => console.log(r))
|
||||
|
||||
tracker.on('totaldlprogress', (data) => {
|
||||
process.send({task: 0, total: data.total, value: data.acc, percent: parseInt((data.acc/data.total)*100)})
|
||||
})
|
||||
|
||||
tracker.on('dlcomplete', () => {
|
||||
process.send({task: 1})
|
||||
})
|
||||
|
||||
process.on('message', (msg) => {
|
||||
if(msg.task === 0){
|
||||
const func = msg.content
|
||||
if(typeof tracker[func] === 'function'){
|
||||
const f = tracker[func]
|
||||
const res = f.apply(tracker, msg.argsArr)
|
||||
if(res instanceof Promise){
|
||||
res.then((v) => {
|
||||
process.send({result: v})
|
||||
})
|
||||
} else {
|
||||
process.send({result: res})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
process.on('disconnect', () => {
|
||||
console.log('AssetExec Disconnected')
|
||||
process.exit(0)
|
||||
})
|
@ -1,972 +0,0 @@
|
||||
/**
|
||||
* AssetGuard
|
||||
*
|
||||
* This module aims to provide a comprehensive and stable method for processing
|
||||
* and downloading game assets for the WesterosCraft server. Download meta is
|
||||
* for several identifiers (categories) is stored inside of an AssetGuard object.
|
||||
* 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 a download queue (array). 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 AssetGuard 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 AssetGuard
|
||||
* 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 AssetGuard instance. This can be listened to by external modules allowing for
|
||||
* categorical tracking of the downloading process.
|
||||
*
|
||||
* @module assetguard
|
||||
*/
|
||||
// Requirements
|
||||
const AdmZip = require('adm-zip')
|
||||
const async = require('async')
|
||||
const child_process = require('child_process')
|
||||
const crypto = require('crypto')
|
||||
const EventEmitter = require('events')
|
||||
const fs = require('fs')
|
||||
const mkpath = require('mkdirp');
|
||||
const path = require('path')
|
||||
const request = require('request')
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let distributionData = null
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* Create an instance of AssetGuard.
|
||||
* On creation the object's properties are never-null default
|
||||
* values. Each identifier is resolved to an empty DLTracker.
|
||||
*
|
||||
* @param {String} basePath - base path for asset validation (game root).
|
||||
* @param {String} javaexec - path to a java executable which will be used
|
||||
* to finalize installation.
|
||||
*/
|
||||
constructor(basePath, javaexec){
|
||||
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)
|
||||
this.basePath = basePath
|
||||
this.javaexec = javaexec
|
||||
}
|
||||
|
||||
// Static 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.
|
||||
*/
|
||||
static _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.
|
||||
*/
|
||||
static _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.
|
||||
*/
|
||||
static _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.
|
||||
*/
|
||||
static _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.
|
||||
*/
|
||||
static _validateLocal(filePath, algo, hash){
|
||||
if(fs.existsSync(filePath)){
|
||||
//No hash provided, have to assume it's good.
|
||||
if(hash == null){
|
||||
return true
|
||||
}
|
||||
let fileName = path.basename(filePath)
|
||||
let buf = fs.readFileSync(filePath)
|
||||
let calcdhash = AssetGuard._calculateHash(buf, algo)
|
||||
return calcdhash === hash
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statically retrieve the distribution data.
|
||||
*
|
||||
* @param {String} basePath - base path for asset validation (game root).
|
||||
* @param {Boolean} cached - optional. False if the distro should be freshly downloaded, else
|
||||
* a cached copy will be returned.
|
||||
* @returns {Promise.<Object>} - A promise which resolves to the distribution data object.
|
||||
*/
|
||||
static retrieveDistributionData(basePath, cached = true){
|
||||
return new Promise(function(fulfill, reject){
|
||||
if(!cached || distributionData == null){
|
||||
// TODO Download file from upstream.
|
||||
//const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
|
||||
// TODO Save file to path.join(basePath, 'westeroscraft.json')
|
||||
// TODO Fulfill with JSON.parse()
|
||||
|
||||
// Workaround while file is not hosted.
|
||||
fs.readFile(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8', (err, data) => {
|
||||
distributionData = JSON.parse(data)
|
||||
fulfill(distributionData)
|
||||
})
|
||||
} else {
|
||||
fulfill(distributionData)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Statically retrieve the distribution data.
|
||||
*
|
||||
* @param {String} basePath - base path for asset validation (game root).
|
||||
* @param {Boolean} cached - optional. False if the distro should be freshly downloaded, else
|
||||
* a cached copy will be returned.
|
||||
* @returns {Object} - The distribution data object.
|
||||
*/
|
||||
static retrieveDistributionDataSync(basePath, cached = true){
|
||||
if(!cached || distributionData == null){
|
||||
distributionData = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8'))
|
||||
}
|
||||
return distributionData
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default selected server from the distribution index.
|
||||
*
|
||||
* @param {String} basePath - base path for asset validation (game root).
|
||||
* @returns {Object} - An object resolving to the default selected server.
|
||||
*/
|
||||
static resolveSelectedServer(basePath){
|
||||
const distro = AssetGuard.retrieveDistributionDataSync(basePath)
|
||||
const servers = distro.servers
|
||||
for(let i=0; i<servers.length; i++){
|
||||
if(servers[i].default_selected){
|
||||
return servers[i].id
|
||||
}
|
||||
}
|
||||
// If no server declares default_selected, default to the first one declared.
|
||||
return (servers.length > 0) ? servers[0].id : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a server from the distro index which maches the provided ID.
|
||||
* Returns null if the ID could not be found or the distro index has
|
||||
* not yet been loaded.
|
||||
*
|
||||
* @param {String} basePath - base path for asset validation (game root).
|
||||
* @param {String} serverID - The id of the server to retrieve.
|
||||
* @returns {Object} - The server object whose id matches the parameter.
|
||||
*/
|
||||
static getServerById(basePath, serverID){
|
||||
if(distributionData == null){
|
||||
AssetGuard.retrieveDistributionDataSync(basePath, false)
|
||||
}
|
||||
const servers = distributionData.servers
|
||||
let serv = null
|
||||
for(let i=0; i<servers.length; i++){
|
||||
if(servers[i].id === serverID){
|
||||
serv = servers[i]
|
||||
}
|
||||
}
|
||||
return serv
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
static _validateForgeChecksum(filePath, checksums){
|
||||
if(fs.existsSync(filePath)){
|
||||
if(checksums == null || checksums.length === 0){
|
||||
return true
|
||||
}
|
||||
let buf = fs.readFileSync(filePath)
|
||||
let calcdhash = AssetGuard._calculateHash(buf, 'sha1')
|
||||
let valid = checksums.includes(calcdhash)
|
||||
if(!valid && filePath.endsWith('.jar')){
|
||||
valid = AssetGuard._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.
|
||||
*/
|
||||
static _validateForgeJar(buf, checksums){
|
||||
// Double pass method was the quickest I found. I tried a version where we store data
|
||||
// to only require a single pass, plus some quick cleanup but that seemed to take slightly more time.
|
||||
|
||||
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 = AssetGuard._parseChecksumsFile(zip.readAsText(entry))
|
||||
}
|
||||
hashes[entry.entryName] = AssetGuard._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.
|
||||
*/
|
||||
static _extractPackXZ(filePaths, javaExecutable){
|
||||
return new Promise(function(fulfill, reject){
|
||||
const libPath = path.join(__dirname, '..', 'libraries', 'java', 'PackXZExtract.jar')
|
||||
const filePath = filePaths.join(',')
|
||||
const child = child_process.spawn(javaExecutable, ['-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 which finalizes the forge installation process. This creates a 'version'
|
||||
* instance for forge and saves its version.json file into that instance. If that
|
||||
* instance already exists, the contents of the version.json file are read and returned
|
||||
* in a promise.
|
||||
*
|
||||
* @param {Asset} asset - The Asset object representing Forge.
|
||||
* @param {String} basePath - Base path for asset validation (game root).
|
||||
* @returns {Promise.<Object>} - A promise which resolves to the contents of forge's version.json.
|
||||
*/
|
||||
static _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 {
|
||||
//Read the saved file to allow for user modifications.
|
||||
fulfill(JSON.parse(fs.readFileSync(versionFile, 'utf-8')))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
//We didn't find forge's version.json.
|
||||
reject('Unable to finalize Forge processing, version.json not found! Has forge changed their format?')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
startAsyncProcess(identifier, limit = 5){
|
||||
const self = this
|
||||
let acc = 0
|
||||
const concurrentDlTracker = this[identifier]
|
||||
const concurrentDlQueue = concurrentDlTracker.dlqueue.slice(0)
|
||||
if(concurrentDlQueue.length === 0){
|
||||
return false
|
||||
} else {
|
||||
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)
|
||||
self.progress += asset.size*1
|
||||
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
|
||||
cb()
|
||||
}
|
||||
})
|
||||
req.on('data', function(chunk){
|
||||
count += chunk.length
|
||||
self.progress += chunk.length
|
||||
acc += chunk.length
|
||||
self.emit(identifier + 'dlprogress', acc)
|
||||
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
|
||||
})
|
||||
}, function(err){
|
||||
if(err){
|
||||
self.emit(identifier + 'dlerror')
|
||||
console.log('An item in ' + identifier + ' failed to process');
|
||||
} else {
|
||||
self.emit(identifier + 'dlcomplete')
|
||||
console.log('All ' + identifier + ' have been processed successfully')
|
||||
}
|
||||
self.totaldlsize -= self[identifier].dlsize
|
||||
self.progress -= self[identifier].dlsize
|
||||
self[identifier] = new DLTracker([], 0)
|
||||
if(self.totaldlsize === 0) {
|
||||
self.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 {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.
|
||||
*/
|
||||
loadVersionData(version, force = false){
|
||||
const self = this
|
||||
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(self.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 {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.
|
||||
*/
|
||||
validateAssets(versionData, force = false){
|
||||
const self = this
|
||||
return new Promise(function(fulfill, reject){
|
||||
self._assetChainIndexData(versionData, 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 {Boolean} force
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
_assetChainIndexData(versionData, force = false){
|
||||
const self = this
|
||||
return new Promise(function(fulfill, reject){
|
||||
//Asset index constants.
|
||||
const assetIndex = versionData.assetIndex
|
||||
const name = assetIndex.id + '.json'
|
||||
const indexPath = path.join(self.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'))
|
||||
self._assetChainValidateAssets(versionData, data).then(() => {
|
||||
fulfill()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
|
||||
self._assetChainValidateAssets(versionData, 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 {Boolean} force
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
_assetChainValidateAssets(versionData, indexData){
|
||||
const self = this
|
||||
return new Promise(function(fulfill, reject){
|
||||
|
||||
//Asset constants
|
||||
const resourceURL = 'http://resources.download.minecraft.net/'
|
||||
const localPath = path.join(self.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(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){
|
||||
dlSize += (ast.size*1)
|
||||
assetDlQueue.push(ast)
|
||||
}
|
||||
cb()
|
||||
}, function(err){
|
||||
self.assets = new DLTracker(assetDlQueue, dlSize)
|
||||
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.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
validateLibraries(versionData){
|
||||
const self = this
|
||||
return new Promise(function(fulfill, reject){
|
||||
|
||||
const libArr = versionData.libraries
|
||||
const libPath = path.join(self.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(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){
|
||||
dlSize += (libItm.size*1)
|
||||
libDlQueue.push(libItm)
|
||||
}
|
||||
}
|
||||
cb()
|
||||
}, function(err){
|
||||
self.libraries = new DLTracker(libDlQueue, dlSize)
|
||||
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.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
validateMiscellaneous(versionData){
|
||||
const self = this
|
||||
return new Promise(async function(fulfill, reject){
|
||||
await self.validateClient(versionData)
|
||||
await self.validateLogConfig(versionData)
|
||||
fulfill()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate client file - artifact renamed from client.jar to '{version}'.jar.
|
||||
*
|
||||
* @param {Object} versionData - the version data for the assets.
|
||||
* @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.
|
||||
*/
|
||||
validateClient(versionData, force = false){
|
||||
const self = this
|
||||
return new Promise(function(fulfill, reject){
|
||||
const clientData = versionData.downloads.client
|
||||
const version = versionData.id
|
||||
const targetPath = path.join(self.basePath, 'versions', version)
|
||||
const targetFile = version + '.jar'
|
||||
|
||||
let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile))
|
||||
|
||||
if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){
|
||||
self.files.dlqueue.push(client)
|
||||
self.files.dlsize += client.size*1
|
||||
fulfill()
|
||||
} else {
|
||||
fulfill()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate log config.
|
||||
*
|
||||
* @param {Object} versionData - the version data for the assets.
|
||||
* @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.
|
||||
*/
|
||||
validateLogConfig(versionData){
|
||||
const self = this
|
||||
return new Promise(function(fulfill, reject){
|
||||
const client = versionData.logging.client
|
||||
const file = client.file
|
||||
const targetPath = path.join(self.basePath, 'assets', 'log_configs')
|
||||
|
||||
let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id))
|
||||
|
||||
if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){
|
||||
self.files.dlqueue.push(logConfig)
|
||||
self.files.dlsize += logConfig.size*1
|
||||
fulfill()
|
||||
} else {
|
||||
fulfill()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the distribution.
|
||||
*
|
||||
* @param {String} serverpackid - The id of the server to validate.
|
||||
* @returns {Promise.<Object>} - A promise which resolves to the server distribution object.
|
||||
*/
|
||||
validateDistribution(serverpackid){
|
||||
const self = this
|
||||
return new Promise(function(fulfill, reject){
|
||||
AssetGuard.retrieveDistributionData(self.basePath, false).then((value) => {
|
||||
/*const servers = value.servers
|
||||
let serv = null
|
||||
for(let i=0; i<servers.length; i++){
|
||||
if(servers[i].id === serverpackid){
|
||||
serv = servers[i]
|
||||
break
|
||||
}
|
||||
}*/
|
||||
const serv = AssetGuard.getServerById(self.basePath, serverpackid)
|
||||
|
||||
if(serv == null) {
|
||||
console.error('Invalid server pack id:', serverpackid)
|
||||
}
|
||||
|
||||
self.forge = self._parseDistroModules(serv.modules, serv.mc_version)
|
||||
//Correct our workaround here.
|
||||
let decompressqueue = self.forge.callback
|
||||
self.forge.callback = function(asset){
|
||||
if(asset.to.toLowerCase().endsWith('.pack.xz')){
|
||||
AssetGuard._extractPackXZ([asset.to], self.javaexec)
|
||||
}
|
||||
if(asset.type === 'forge-hosted' || asset.type === 'forge'){
|
||||
AssetGuard._finalizeForgeAsset(asset, self.basePath)
|
||||
}
|
||||
}
|
||||
fulfill(serv)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*//TODO The file should be hosted, the following code is for local testing.
|
||||
_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(__dirname, '..', 'westeroscraft.json'), 'utf-8', (err, data) => {
|
||||
fulfill(JSON.parse(data))
|
||||
})
|
||||
})
|
||||
}*/
|
||||
|
||||
_parseDistroModules(modules, 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 ? AssetGuard._resolvePath(ob.id, obArtifact.extension) : obArtifact.path
|
||||
switch(obType){
|
||||
case 'forge-hosted':
|
||||
case 'forge':
|
||||
case 'library':
|
||||
obPath = path.join(this.basePath, 'libraries', obPath)
|
||||
break
|
||||
case 'forgemod':
|
||||
//obPath = path.join(this.basePath, 'mods', obPath)
|
||||
obPath = path.join(this.basePath, 'modstore', obPath)
|
||||
break
|
||||
case 'litemod':
|
||||
//obPath = path.join(this.basePath, 'mods', version, obPath)
|
||||
obPath = path.join(this.basePath, 'modstore', obPath)
|
||||
break
|
||||
case 'file':
|
||||
default:
|
||||
obPath = path.join(this.basePath, obPath)
|
||||
}
|
||||
let artifact = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, obType)
|
||||
const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath
|
||||
if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){
|
||||
asize += artifact.size*1
|
||||
alist.push(artifact)
|
||||
if(validationPath !== obPath) decompressqueue.push(obPath)
|
||||
}
|
||||
//Recursively process the submodules then combine the results.
|
||||
if(ob.sub_modules != null){
|
||||
let dltrack = this._parseDistroModules(ob.sub_modules, version)
|
||||
asize += dltrack.dlsize*1
|
||||
alist = alist.concat(dltrack.dlqueue)
|
||||
decompressqueue = decompressqueue.concat(dltrack.callback)
|
||||
}
|
||||
}
|
||||
|
||||
//Since we have no callback at this point, we use this value to store the decompressqueue.
|
||||
return new DLTracker(alist, asize, decompressqueue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Forge's version.json data into memory for the specified server id.
|
||||
*
|
||||
* @param {String} serverpack - The id of the server to load Forge data for.
|
||||
* @returns {Promise.<Object>} - A promise which resolves to Forge's version.json data.
|
||||
*/
|
||||
loadForgeData(serverpack){
|
||||
const self = this
|
||||
return new Promise(async function(fulfill, reject){
|
||||
let distro = AssetGuard.retrieveDistributionDataSync(self.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(self.basePath, 'libraries', AssetGuard._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 AssetGuard._finalizeForgeAsset(asset, self.basePath)
|
||||
fulfill(forgeData)
|
||||
return
|
||||
}
|
||||
}
|
||||
reject('No forge module found!')
|
||||
})
|
||||
}
|
||||
|
||||
_parseForgeLibraries(){
|
||||
/* TODO
|
||||
* Forge asset validations are already implemented. When there's nothing much
|
||||
* to work on, implement forge downloads using forge's version.json. This is to
|
||||
* have the code on standby if we ever need it (since it's half implemented already).
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
*/
|
||||
processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){
|
||||
this.progress = 0;
|
||||
|
||||
let shouldFire = true
|
||||
|
||||
// Assign dltracking variables.
|
||||
this.totaldlsize = 0
|
||||
this.progress = 0
|
||||
for(let i=0; i<identifiers.length; i++){
|
||||
this.totaldlsize += this[identifiers[i].id].dlsize
|
||||
}
|
||||
|
||||
for(let i=0; i<identifiers.length; i++){
|
||||
let iden = identifiers[i]
|
||||
let r = this.startAsyncProcess(iden.id, iden.limit)
|
||||
if(r) shouldFire = false
|
||||
}
|
||||
|
||||
if(shouldFire){
|
||||
this.emit('dlcomplete')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AssetGuard,
|
||||
Asset,
|
||||
Library
|
||||
}
|
@ -1,34 +1,315 @@
|
||||
const ConfigManager = require('./configmanager.js')
|
||||
const Mojang = require('./mojang.js')
|
||||
/**
|
||||
* 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, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
|
||||
const { MicrosoftAuth, microsoftErrorDisplayable, MicrosoftErrorCode } = require('helios-core/microsoft')
|
||||
const { AZURE_CLIENT_ID } = require('./ipcconstants')
|
||||
|
||||
const log = LoggerUtil.getLogger('AuthManager')
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
exports.addAccount = async function(username, password){
|
||||
try{
|
||||
const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken)
|
||||
} catch (err){
|
||||
return Promise.reject(err)
|
||||
log.error(err)
|
||||
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
|
||||
}
|
||||
const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
exports.validateSelected = async function(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken())
|
||||
console.log(isValid)
|
||||
if(!isValid){
|
||||
try {
|
||||
const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken())
|
||||
console.log('ses', session)
|
||||
ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
|
||||
/**
|
||||
* 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()
|
||||
} catch(err) {
|
||||
if(err && err.message === 'ForbiddenOperationException'){
|
||||
return false
|
||||
}
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
log.error('Error while removing account', response.error)
|
||||
return Promise.reject(response.error)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return true
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the selected account with Mojang's authserver. If the account is not valid,
|
||||
* we will attempt to refresh the access token and update that value. If that fails, a
|
||||
* new login will be required.
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
async function validateSelectedMojangAccount(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
|
||||
|
||||
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
||||
const isValid = response.data
|
||||
if(!isValid){
|
||||
const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
|
||||
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
|
||||
const session = refreshResponse.data
|
||||
ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
|
||||
ConfigManager.save()
|
||||
} else {
|
||||
log.error('Error while validating selected profile:', refreshResponse.error)
|
||||
log.info('Account access token is invalid.')
|
||||
return false
|
||||
}
|
||||
log.info('Account access token validated.')
|
||||
return true
|
||||
} else {
|
||||
log.info('Account access token validated.')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the selected account with Microsoft's authserver. If the account is not valid,
|
||||
* we will attempt to refresh the access token and update that value. If that fails, a
|
||||
* new login will be required.
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
async function validateSelectedMicrosoftAccount(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
const now = new Date().getTime()
|
||||
const mcExpiresAt = current.expiresAt
|
||||
const mcExpired = now >= mcExpiresAt
|
||||
|
||||
if(!mcExpired) {
|
||||
return true
|
||||
}
|
||||
|
||||
// MC token expired. Check MS token.
|
||||
|
||||
const msExpiresAt = current.microsoft.expires_at
|
||||
const msExpired = now >= msExpiresAt
|
||||
|
||||
if(msExpired) {
|
||||
// MS expired, do full refresh.
|
||||
try {
|
||||
const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH)
|
||||
|
||||
ConfigManager.updateMicrosoftAuthAccount(
|
||||
current.uuid,
|
||||
res.mcToken.access_token,
|
||||
res.accessToken.access_token,
|
||||
res.accessToken.refresh_token,
|
||||
calculateExpiryDate(now, res.accessToken.expires_in),
|
||||
calculateExpiryDate(now, res.mcToken.expires_in)
|
||||
)
|
||||
ConfigManager.save()
|
||||
return true
|
||||
} catch(err) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// Only MC expired, use existing MS token.
|
||||
try {
|
||||
const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH)
|
||||
|
||||
ConfigManager.updateMicrosoftAuthAccount(
|
||||
current.uuid,
|
||||
res.mcToken.access_token,
|
||||
current.microsoft.access_token,
|
||||
current.microsoft.refresh_token,
|
||||
current.microsoft.expires_at,
|
||||
calculateExpiryDate(now, res.mcToken.expires_in)
|
||||
)
|
||||
ConfigManager.save()
|
||||
return true
|
||||
}
|
||||
catch(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the selected auth account.
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
exports.validateSelected = async function(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
|
||||
if(current.type === 'microsoft') {
|
||||
return await validateSelectedMicrosoftAccount()
|
||||
} else {
|
||||
return await validateSelectedMojangAccount()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,72 @@
|
||||
const fs = require('fs')
|
||||
const mkpath = require('mkdirp')
|
||||
const os = require('os')
|
||||
const fs = require('fs-extra')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
|
||||
function resolveMaxRAM(){
|
||||
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()
|
||||
return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G')
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -17,35 +77,32 @@ function resolveMaxRAM(){
|
||||
*/
|
||||
const DEFAULT_CONFIG = {
|
||||
settings: {
|
||||
java: {
|
||||
minRAM: '2G',
|
||||
maxRAM: resolveMaxRAM(), // Dynamic
|
||||
executable: 'C:\\Program Files\\Java\\jdk1.8.0_152\\bin\\javaw.exe', // TODO Resolve
|
||||
jvmOptions: [
|
||||
'-XX:+UseConcMarkSweepGC',
|
||||
'-XX:+CMSIncrementalMode',
|
||||
'-XX:-UseAdaptiveSizePolicy',
|
||||
'-Xmn128M'
|
||||
],
|
||||
},
|
||||
game: {
|
||||
directory: path.join(__dirname, '..', '..', '..', 'target', 'test', 'mcfiles'),
|
||||
resWidth: 1280,
|
||||
resHeight: 720,
|
||||
fullscreen: false,
|
||||
autoConnect: true
|
||||
autoConnect: true,
|
||||
launchDetached: true
|
||||
},
|
||||
launcher: {
|
||||
|
||||
allowPrerelease: false,
|
||||
dataDirectory: dataPath
|
||||
}
|
||||
},
|
||||
clientToken: uuidV4().replace(/-/g, ''),
|
||||
newsCache: {
|
||||
date: null,
|
||||
content: null,
|
||||
dismissed: false
|
||||
},
|
||||
clientToken: null,
|
||||
selectedServer: null, // Resolved
|
||||
selectedAccount: null,
|
||||
authenticationDatabase: {}
|
||||
authenticationDatabase: {},
|
||||
modConfigurations: [],
|
||||
javaConfig: {}
|
||||
}
|
||||
|
||||
let config = null;
|
||||
let config = null
|
||||
|
||||
// Persistance Utility Functions
|
||||
|
||||
@ -53,8 +110,7 @@ let config = null;
|
||||
* Save the current configuration to a file.
|
||||
*/
|
||||
exports.save = function(){
|
||||
const filePath = path.join(config.settings.game.directory, 'config.json')
|
||||
fs.writeFileSync(filePath, JSON.stringify(config, null, 4), 'UTF-8')
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,27 +120,146 @@ exports.save = function(){
|
||||
* need to be externally assigned.
|
||||
*/
|
||||
exports.load = function(){
|
||||
// Determine the effective configuration.
|
||||
const EFFECTIVE_CONFIG = config == null ? DEFAULT_CONFIG : config
|
||||
const filePath = path.join(EFFECTIVE_CONFIG.settings.game.directory, 'config.json')
|
||||
let doLoad = true
|
||||
|
||||
if(!fs.existsSync(filePath)){
|
||||
if(!fs.existsSync(configPath)){
|
||||
// Create all parent directories.
|
||||
mkpath.sync(path.join(filePath, '..'))
|
||||
config = DEFAULT_CONFIG
|
||||
exports.save()
|
||||
} else {
|
||||
config = JSON.parse(fs.readFileSync(filePath, 'UTF-8'))
|
||||
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.
|
||||
* @returns {string} The launcher's Client Token.
|
||||
*/
|
||||
exports.getClientToken = function(){
|
||||
return config.clientToken
|
||||
@ -93,7 +268,7 @@ exports.getClientToken = function(){
|
||||
/**
|
||||
* Set the launcher's Client Token.
|
||||
*
|
||||
* @param {String} clientToken - the launcher's new Client Token.
|
||||
* @param {string} clientToken The launcher's new Client Token.
|
||||
*/
|
||||
exports.setClientToken = function(clientToken){
|
||||
config.clientToken = clientToken
|
||||
@ -102,8 +277,8 @@ exports.setClientToken = function(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.
|
||||
* @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
|
||||
@ -112,7 +287,7 @@ exports.getSelectedServer = function(def = false){
|
||||
/**
|
||||
* Set the ID of the selected serverpack.
|
||||
*
|
||||
* @param {String} serverID - the ID of the new selected serverpack.
|
||||
* @param {string} serverID The ID of the new selected serverpack.
|
||||
*/
|
||||
exports.setSelectedServer = function(serverID){
|
||||
config.selectedServer = serverID
|
||||
@ -121,7 +296,7 @@ exports.setSelectedServer = function(serverID){
|
||||
/**
|
||||
* Get an array of each account currently authenticated by the launcher.
|
||||
*
|
||||
* @returns {Array.<Object>} - an array of each stored authenticated account.
|
||||
* @returns {Array.<Object>} An array of each stored authenticated account.
|
||||
*/
|
||||
exports.getAuthAccounts = function(){
|
||||
return config.authenticationDatabase
|
||||
@ -131,70 +306,267 @@ exports.getAuthAccounts = function(){
|
||||
* 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.
|
||||
* @param {string} uuid The uuid of the authenticated account.
|
||||
* @returns {Object} The authenticated account with the given uuid.
|
||||
*/
|
||||
exports.getAuthAccount = function(uuid){
|
||||
return config.authenticationDatabase[uuid]
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the access token of an authenticated account.
|
||||
* Update the access token of an authenticated mojang account.
|
||||
*
|
||||
* @param {String} uuid - uuid of the authenticated account.
|
||||
* @param {String} accessToken - the new Access Token.
|
||||
* @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.
|
||||
* @returns {Object} The authenticated account object created by this action.
|
||||
*/
|
||||
exports.updateAuthAccount = function(uuid, accessToken){
|
||||
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 account to the database to be stored.
|
||||
* Adds an authenticated mojang account to the database to be stored.
|
||||
*
|
||||
* @param {String} uuid - uuid of the authenticated account.
|
||||
* @param {String} accessToken - accessToken of the authenticated account.
|
||||
* @param {String} username - username (usually email) of the authenticated account.
|
||||
* @param {String} displayName - in game name of the authenticated account.
|
||||
* @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.
|
||||
* @returns {Object} The authenticated account object created by this action.
|
||||
*/
|
||||
exports.addAuthAccount = function(uuid, accessToken, username, displayName){
|
||||
exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
|
||||
config.selectedAccount = uuid
|
||||
config.authenticationDatabase[uuid] = {
|
||||
type: 'mojang',
|
||||
accessToken,
|
||||
username,
|
||||
uuid,
|
||||
displayName
|
||||
username: username.trim(),
|
||||
uuid: uuid.trim(),
|
||||
displayName: displayName.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.
|
||||
* @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 {Boolean} def - optional. If true, the default value will be returned.
|
||||
* @returns {String} - the minimum amount of memory for JVM initialization.
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {string} The minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.getMinRAM = function(def = false){
|
||||
return !def ? config.settings.java.minRAM : DEFAULT_CONFIG.settings.java.minRAM
|
||||
exports.getMinRAM = function(serverid){
|
||||
return config.javaConfig[serverid].minRAM
|
||||
}
|
||||
|
||||
/**
|
||||
@ -202,10 +574,11 @@ exports.getMinRAM = function(def = false){
|
||||
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
||||
* 1024 MegaBytes, etc.
|
||||
*
|
||||
* @param {String} minRAM - the new minimum amount of memory for JVM initialization.
|
||||
* @param {string} serverid The server id.
|
||||
* @param {string} minRAM The new minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.setMinRAM = function(minRAM){
|
||||
config.settings.java.minRAM = minRAM
|
||||
exports.setMinRAM = function(serverid, minRAM){
|
||||
config.javaConfig[serverid].minRAM = minRAM
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,11 +586,11 @@ exports.setMinRAM = function(minRAM){
|
||||
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
||||
* 1024 MegaBytes, etc.
|
||||
*
|
||||
* @param {Boolean} def - optional. If true, the default value will be returned.
|
||||
* @returns {String} - the maximum amount of memory for JVM initialization.
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {string} The maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.getMaxRAM = function(def = false){
|
||||
return !def ? config.settings.java.maxRAM : resolveMaxRAM()
|
||||
exports.getMaxRAM = function(serverid){
|
||||
return config.javaConfig[serverid].maxRAM
|
||||
}
|
||||
|
||||
/**
|
||||
@ -225,10 +598,11 @@ exports.getMaxRAM = function(def = false){
|
||||
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
||||
* 1024 MegaBytes, etc.
|
||||
*
|
||||
* @param {String} maxRAM - the new maximum amount of memory for JVM initialization.
|
||||
* @param {string} serverid The server id.
|
||||
* @param {string} maxRAM The new maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.setMaxRAM = function(maxRAM){
|
||||
config.settings.java.maxRAM = maxRAM
|
||||
exports.setMaxRAM = function(serverid, maxRAM){
|
||||
config.javaConfig[serverid].maxRAM = maxRAM
|
||||
}
|
||||
|
||||
/**
|
||||
@ -236,19 +610,21 @@ exports.setMaxRAM = function(maxRAM){
|
||||
*
|
||||
* This is a resolved configuration value and defaults to null until externally assigned.
|
||||
*
|
||||
* @returns {String} - the path of the Java Executable.
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {string} The path of the Java Executable.
|
||||
*/
|
||||
exports.getJavaExecutable = function(){
|
||||
return config.settings.java.executable
|
||||
exports.getJavaExecutable = function(serverid){
|
||||
return config.javaConfig[serverid].executable
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path of the Java Executable.
|
||||
*
|
||||
* @param {String} executable - the new path of the Java Executable.
|
||||
* @param {string} serverid The server id.
|
||||
* @param {string} executable The new path of the Java Executable.
|
||||
*/
|
||||
exports.setJavaExecutable = function(executable){
|
||||
config.settings.java.executable = executable
|
||||
exports.setJavaExecutable = function(serverid, executable){
|
||||
config.javaConfig[serverid].executable = executable
|
||||
}
|
||||
|
||||
/**
|
||||
@ -256,11 +632,11 @@ exports.setJavaExecutable = function(executable){
|
||||
* such as memory allocation, will be dynamically resolved and will not be included
|
||||
* in this value.
|
||||
*
|
||||
* @param {Boolean} def - optional. If true, the default value will be returned.
|
||||
* @returns {Array.<String>} - an array of the additional arguments for JVM initialization.
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {Array.<string>} An array of the additional arguments for JVM initialization.
|
||||
*/
|
||||
exports.getJVMOptions = function(def = false){
|
||||
return !def ? config.settings.java.jvmOptions : DEFAULT_CONFIG.settings.java.jvmOptions
|
||||
exports.getJVMOptions = function(serverid){
|
||||
return config.javaConfig[serverid].jvmOptions
|
||||
}
|
||||
|
||||
/**
|
||||
@ -268,39 +644,21 @@ exports.getJVMOptions = function(def = false){
|
||||
* such as memory allocation, will be dynamically resolved and should not be
|
||||
* included in this value.
|
||||
*
|
||||
* @param {Array.<String>} jvmOptions - an array of the new additional arguments for JVM
|
||||
* @param {string} serverid The server id.
|
||||
* @param {Array.<string>} jvmOptions An array of the new additional arguments for JVM
|
||||
* initialization.
|
||||
*/
|
||||
exports.setJVMOptions = function(jvmOptions){
|
||||
config.settings.java.jvmOptions = jvmOptions
|
||||
exports.setJVMOptions = function(serverid, jvmOptions){
|
||||
config.javaConfig[serverid].jvmOptions = jvmOptions
|
||||
}
|
||||
|
||||
// Game Settings
|
||||
|
||||
/**
|
||||
* Retrieve the absolute path of the game directory.
|
||||
*
|
||||
* @param {Boolean} def - optional. If true, the default value will be returned.
|
||||
* @returns {String} - the absolute path of the game directory.
|
||||
*/
|
||||
exports.getGameDirectory = function(def = false){
|
||||
return !def ? config.settings.game.directory : DEFAULT_CONFIG.settings.game.directory
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the absolute path of the game directory.
|
||||
*
|
||||
* @param {String} directory - the absolute path of the new game directory.
|
||||
*/
|
||||
exports.setGameDirectory = function(directory){
|
||||
config.settings.game.directory = directory
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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
|
||||
@ -309,17 +667,28 @@ exports.getGameWidth = function(def = false){
|
||||
/**
|
||||
* Set the width of the game window.
|
||||
*
|
||||
* @param {Number} resWidth - the new width of the game window.
|
||||
* @param {number} resWidth The new width of the game window.
|
||||
*/
|
||||
exports.setGameWidth = function(resWidth){
|
||||
config.settings.game.resWidth = 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.
|
||||
* @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
|
||||
@ -328,26 +697,37 @@ exports.getGameHeight = function(def = false){
|
||||
/**
|
||||
* Set the height of the game window.
|
||||
*
|
||||
* @param {Number} resHeight - the new height of the game window.
|
||||
* @param {number} resHeight The new height of the game window.
|
||||
*/
|
||||
exports.setGameHeight = function(resHeight){
|
||||
config.settings.game.resHeight = 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.
|
||||
* @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.isFullscreen = function(def = false){
|
||||
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.
|
||||
* @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode.
|
||||
*/
|
||||
exports.setFullscreen = function(fullscreen){
|
||||
config.settings.game.fullscreen = fullscreen
|
||||
@ -356,18 +736,58 @@ exports.setFullscreen = function(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.
|
||||
* @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.isAutoConnect = function(def = false){
|
||||
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.
|
||||
* @param {boolean} autoConnect Whether or not the game should auto connect to servers.
|
||||
*/
|
||||
exports.setAutoConnect = function(autoConnect){
|
||||
config.settings.game.autoConnect = autoConnect
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should launch as a detached process.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {boolean} Whether or not the game will launch as a detached process.
|
||||
*/
|
||||
exports.getLaunchDetached = function(def = false){
|
||||
return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of whether or not the game should launch as a detached process.
|
||||
*
|
||||
* @param {boolean} launchDetached Whether or not the game should launch as a detached process.
|
||||
*/
|
||||
exports.setLaunchDetached = function(launchDetached){
|
||||
config.settings.game.launchDetached = launchDetached
|
||||
}
|
||||
|
||||
// Launcher Settings
|
||||
|
||||
/**
|
||||
* Check if the launcher should download prerelease versions.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {boolean} Whether or not the launcher should download prerelease versions.
|
||||
*/
|
||||
exports.getAllowPrerelease = function(def = false){
|
||||
return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of Whether or not the launcher should download prerelease versions.
|
||||
*
|
||||
* @param {boolean} launchDetached Whether or not the launcher should download prerelease versions.
|
||||
*/
|
||||
exports.setAllowPrerelease = function(allowPrerelease){
|
||||
config.settings.launcher.allowPrerelease = allowPrerelease
|
||||
}
|
@ -1,49 +1,52 @@
|
||||
// Work in progress
|
||||
const {Client} = require('discord-rpc')
|
||||
const ConfigManager = require('./configmanager.js')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
|
||||
let rpc
|
||||
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 = 'Waiting for Client..'){
|
||||
rpc = new Client({ transport: 'ipc' })
|
||||
exports.initRPC = function(genSettings, servSettings, initialDetails = Lang.queryJS('discord.waiting')){
|
||||
client = new Client({ transport: 'ipc' })
|
||||
|
||||
rpc.on('ready', () => {
|
||||
activity = {
|
||||
details: initialDetails,
|
||||
state: 'Server: ' + servSettings.shortId,
|
||||
largeImageKey: servSettings.largeImageKey,
|
||||
largeImageText: servSettings.largeImageText,
|
||||
smallImageKey: genSettings.smallImageKey,
|
||||
smallImageText: genSettings.smallImageText,
|
||||
startTimestamp: new Date().getTime() / 1000,
|
||||
instance: false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
rpc.setActivity(activity)
|
||||
client.on('ready', () => {
|
||||
logger.info('Discord RPC Connected')
|
||||
client.setActivity(activity)
|
||||
})
|
||||
|
||||
rpc.login(genSettings.clientID).catch(error => {
|
||||
client.login({clientId: genSettings.clientId}).catch(error => {
|
||||
if(error.message.includes('ENOENT')) {
|
||||
console.log('Unable to initialize Discord Rich Presence, no client detected.')
|
||||
logger.info('Unable to initialize Discord Rich Presence, no client detected.')
|
||||
} else {
|
||||
console.log('Unable to initialize Discord Rich Presence: ' + error.message, error)
|
||||
logger.info('Unable to initialize Discord Rich Presence: ' + error.message, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
exports.updateDetails = function(details){
|
||||
if(activity == null){
|
||||
console.error('Discord RPC is not initialized and therefore cannot be updated.')
|
||||
}
|
||||
activity.details = details
|
||||
rpc.setActivity(activity)
|
||||
client.setActivity(activity)
|
||||
}
|
||||
|
||||
exports.shutdownRPC = function(){
|
||||
if(!rpc) return
|
||||
rpc.setActivity({})
|
||||
rpc.destroy()
|
||||
rpc = null
|
||||
if(!client) return
|
||||
client.clearActivity()
|
||||
client.destroy()
|
||||
client = null
|
||||
activity = null
|
||||
}
|
17
app/assets/js/distromanager.js
Normal file
@ -0,0 +1,17 @@
|
||||
const { DistributionAPI } = require('helios-core/common')
|
||||
|
||||
const ConfigManager = require('./configmanager')
|
||||
|
||||
// Old WesterosCraft url.
|
||||
// exports.REMOTE_DISTRO_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
|
||||
exports.REMOTE_DISTRO_URL = 'https://helios-files.geekcorner.eu.org/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
|
238
app/assets/js/dropinmodutil.js
Normal file
@ -0,0 +1,238 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
28
app/assets/js/ipcconstants.js
Normal file
@ -0,0 +1,28 @@
|
||||
// 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 = '1ce6e35a-126f-48fd-97fb-54d143ac6d45'
|
||||
// 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'
|
||||
}
|
5
app/assets/js/isdev.js
Normal file
@ -0,0 +1,5 @@
|
||||
'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))
|
43
app/assets/js/langloader.js
Normal file
@ -0,0 +1,43 @@
|
||||
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')
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,222 +0,0 @@
|
||||
/**
|
||||
* File is officially deprecated in favor of processbuilder.js,
|
||||
* will be removed once the new module is 100% complete and this
|
||||
* is no longer needed for reference.
|
||||
*/
|
||||
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){
|
||||
console.log(data)
|
||||
const args = finalizeArgumentsForge(versionData, forgeData, data, basePath)
|
||||
//BRUTEFORCE for testing
|
||||
//args.push('-mods modstore\\chatbubbles\\chatbubbles\\1.0.1_for_1.11.2\\mod_chatBubbles-1.0.1_for_1.11.2.litemod,modstore\\com\\westeroscraft\\westerosblocks\\3.0.0-beta-71\\westerosblocks-3.0.0-beta-71.jar,modstore\\mezz\\jei\\1.11.2-4.3.5.277\\jei-1.11.2-4.3.5.277.jar,modstore\\net\\optifine\\optifine\\1.11.2_HD_U_B9\\optifine-1.11.2_HD_U_B9.jar')
|
||||
//args.push('--modListFile absolute:C:\\Users\\Asus\\Desktop\\LauncherElectron\\app\\assets\\WesterosCraft-1.11.2.json')
|
||||
//TODO make this dynamic
|
||||
const child = child_process.spawn('C:\\Program Files\\Java\\jdk1.8.0_152\\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']
|
||||
newVal = 'WesterosCraft-1.11.2'
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//BRUTEFORCE LIB INJECTION
|
||||
//FOR TESTING ONLY
|
||||
cpArgs.push(path.join(libPath, 'com', 'mumfrey', 'liteloader', '1.11.2-SNAPSHOT', 'liteloader-1.11.2-SNAPSHOT.jar'))
|
||||
|
||||
return cpArgs
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
launchMinecraft
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
const request = require('request')
|
||||
|
||||
const minecraftAgent = {
|
||||
name: 'Minecraft',
|
||||
version: 1
|
||||
}
|
||||
const authpath = 'https://authserver.mojang.com'
|
||||
const statuses = [
|
||||
{
|
||||
service: 'minecraft.net',
|
||||
status: 'grey',
|
||||
name: 'Minecraft.net'
|
||||
},
|
||||
{
|
||||
service: 'api.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Public API'
|
||||
},
|
||||
{
|
||||
service: 'textures.minecraft.net',
|
||||
status: 'grey',
|
||||
name: 'Minecraft Skins'
|
||||
},
|
||||
{
|
||||
service: 'authserver.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Authentication Service'
|
||||
},
|
||||
{
|
||||
service: 'sessionserver.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Multiplayer Session Service'
|
||||
},
|
||||
{
|
||||
service: 'account.mojang.com',
|
||||
status: 'grey',
|
||||
name: 'Mojang accounts website'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Converts a Mojang status color to a hex value. Valid statuses
|
||||
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
|
||||
* to our project which represends an unknown status.
|
||||
*
|
||||
* @param {String} status - a valid status code.
|
||||
* @returns {String} - the hex color of the status code.
|
||||
*/
|
||||
exports.statusToHex = function(status){
|
||||
switch(status.toLowerCase()){
|
||||
case 'green':
|
||||
return '#a5c325'
|
||||
case 'yellow':
|
||||
return '#eac918'
|
||||
case 'red':
|
||||
return '#c32625'
|
||||
case 'grey':
|
||||
default:
|
||||
return '#848484'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of Mojang's services.
|
||||
* The response is condensed into a single object. Each service is
|
||||
* a key, where the value is an object containing a status and name
|
||||
* property.
|
||||
*
|
||||
* @see http://wiki.vg/Mojang_API#API_Status
|
||||
*/
|
||||
exports.status = function(){
|
||||
return new Promise(function(fulfill, reject) {
|
||||
request.get('https://status.mojang.com/check',
|
||||
{
|
||||
json: true
|
||||
},
|
||||
function(error, response, body){
|
||||
if(response.statusCode === 200){
|
||||
for(let i=0; i<body.length; i++){
|
||||
const key = Object.keys(body[i])[0]
|
||||
inner:
|
||||
for(let j=0; j<statuses.length; j++){
|
||||
if(statuses[j].service === key) {
|
||||
statuses[j].status = body[i][key]
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
fulfill(statuses)
|
||||
} else {
|
||||
reject()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user with their Mojang credentials.
|
||||
*
|
||||
* @param {String} username - user's username, this is often an email.
|
||||
* @param {String} password - user's password.
|
||||
* @param {String} clientToken - launcher's Client Token.
|
||||
* @param {Boolean} requestUser - optional. Adds user object to the reponse.
|
||||
* @param {Object} agent - optional. Provided by default. Adds user info to the response.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Authenticate
|
||||
*/
|
||||
exports.authenticate = function(username, password, clientToken, requestUser = true, agent = minecraftAgent){
|
||||
return new Promise(function(fulfill, reject){
|
||||
request.post(authpath + '/authenticate',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
agent,
|
||||
username,
|
||||
password,
|
||||
clientToken,
|
||||
requestUser
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(response.statusCode === 200){
|
||||
fulfill(body)
|
||||
} else {
|
||||
reject(body)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an access token. This should always be done before launching.
|
||||
* The client token should match the one used to create the access token.
|
||||
*
|
||||
* @param {String} accessToken - the access token to validate.
|
||||
* @param {String} clientToken - the launcher's client token.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Validate
|
||||
*/
|
||||
exports.validate = function(accessToken, clientToken){
|
||||
return new Promise(function(fulfill, reject){
|
||||
request.post(authpath + '/validate',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(response.statusCode === 403){
|
||||
fulfill(false)
|
||||
} else {
|
||||
// 204 if valid
|
||||
fulfill(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates an access token. The clientToken must match the
|
||||
* token used to create the provided accessToken.
|
||||
*
|
||||
* @param {String} accessToken - the access token to invalidate.
|
||||
* @param {String} clientToken - the launcher's client token.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Invalidate
|
||||
*/
|
||||
exports.invalidate = function(accessToken, clientToken){
|
||||
return new Promise(function(fulfill, reject){
|
||||
request.post(authpath + '/invalidate',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(response.statusCode === 200){
|
||||
fulfill()
|
||||
} else {
|
||||
reject()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh a user's authentication. This should be used to keep a user logged
|
||||
* in without asking them for their credentials again. A new access token will
|
||||
* be generated using a recent invalid access token. See Wiki for more info.
|
||||
*
|
||||
* @param {String} accessToken - the old access token.
|
||||
* @param {String} clientToken - the launcher's client token.
|
||||
* @param {Boolean} requestUser - optional. Adds user object to the reponse.
|
||||
*
|
||||
* @see http://wiki.vg/Authentication#Refresh
|
||||
*/
|
||||
exports.refresh = function(accessToken, clientToken, requestUser = true){
|
||||
return new Promise(function(fulfill, reject){
|
||||
request.post(authpath + '/refresh',
|
||||
{
|
||||
json: true,
|
||||
body: {
|
||||
accessToken,
|
||||
clientToken,
|
||||
requestUser
|
||||
}
|
||||
},
|
||||
function(error, response, body){
|
||||
if(response.statusCode === 200){
|
||||
fulfill(body)
|
||||
} else {
|
||||
reject(response.body)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
@ -1,18 +1,67 @@
|
||||
const {AssetGuard} = require('./assetguard.js')
|
||||
const ConfigManager = require('./configmanager.js')
|
||||
const path = require('path')
|
||||
const {ipcRenderer} = require('electron')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
console.log('Preloading')
|
||||
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()
|
||||
|
||||
// Ensure Distribution is downloaded and cached.
|
||||
AssetGuard.retrieveDistributionDataSync(ConfigManager.getGameDirectory(), false)
|
||||
// Yuck!
|
||||
// TODO Fix this
|
||||
DistroAPI['commonDir'] = ConfigManager.getCommonDirectory()
|
||||
DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory()
|
||||
|
||||
// Resolve the selected server if its value has yet to be set.
|
||||
if(ConfigManager.getSelectedServer() == null){
|
||||
console.log('Determining default selected server..')
|
||||
ConfigManager.setSelectedServer(AssetGuard.resolveSelectedServer(ConfigManager.getGameDirectory()))
|
||||
ConfigManager.save()
|
||||
// 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.')
|
||||
}
|
||||
})
|
1026
app/assets/js/scripts/landing.js
Normal file
234
app/assets/js/scripts/login.js
Normal file
@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 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 loginPasswordError = document.getElementById('loginPasswordError')
|
||||
const loginPassword = document.getElementById('loginPassword')
|
||||
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){
|
||||
if(value){
|
||||
loginPasswordError.style.opacity = 0
|
||||
lp = true
|
||||
if(lu){
|
||||
loginDisabled(false)
|
||||
}
|
||||
} else {
|
||||
lp = false
|
||||
showError(loginPasswordError, Lang.queryJS('login.error.invalidValue'))
|
||||
loginDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Emphasize errors with shake when focus is lost.
|
||||
loginUsername.addEventListener('focusout', (e) => {
|
||||
validateEmail(e.target.value)
|
||||
shakeError(loginEmailError)
|
||||
})
|
||||
loginPassword.addEventListener('focusout', (e) => {
|
||||
validatePassword(e.target.value)
|
||||
shakeError(loginPasswordError)
|
||||
})
|
||||
|
||||
// Validate input for each field.
|
||||
loginUsername.addEventListener('input', (e) => {
|
||||
validateEmail(e.target.value)
|
||||
})
|
||||
loginPassword.addEventListener('input', (e) => {
|
||||
validatePassword(e.target.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 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
|
||||
loginPassword.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.addMojangAccount(loginUsername.value, loginPassword.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 = ''
|
||||
loginPassword.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)
|
||||
})
|
||||
|
||||
})
|
50
app/assets/js/scripts/loginOptions.js
Normal file
@ -0,0 +1,50 @@
|
||||
const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer')
|
||||
const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft')
|
||||
const loginOptionMojang = document.getElementById('loginOptionMojang')
|
||||
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
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
loginOptionMojang.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
|
||||
}
|
||||
})
|
||||
}
|
324
app/assets/js/scripts/overlay.js
Normal file
@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 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()
|
||||
}
|
1583
app/assets/js/scripts/settings.js
Normal file
466
app/assets/js/scripts/uibinder.js
Normal file
@ -0,0 +1,466 @@
|
||||
/**
|
||||
* 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',
|
||||
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)
|
||||
}
|
214
app/assets/js/scripts/uicore.js
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/Helios-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()
|
||||
}
|
||||
})
|
9
app/assets/js/scripts/welcome.js
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Script for welcome.ejs
|
||||
*/
|
||||
document.getElementById('welcomeButton').addEventListener('click', e => {
|
||||
loginOptionsCancelEnabled(false) // False by default, be explicit.
|
||||
loginOptionsViewOnLoginSuccess = VIEWS.landing
|
||||
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
||||
switchView(VIEWS.welcome, VIEWS.loginOptions)
|
||||
})
|
65
app/assets/js/serverstatus.js
Normal file
@ -0,0 +1,65 @@
|
||||
const net = require('net')
|
||||
|
||||
/**
|
||||
* Retrieves the status of a minecraft server.
|
||||
*
|
||||
* @param {string} address The server address.
|
||||
* @param {number} port Optional. The port of the server. Defaults to 25565.
|
||||
* @returns {Promise.<Object>} A promise which resolves to an object containing
|
||||
* status information.
|
||||
*/
|
||||
exports.getStatus = function(address, port = 25565){
|
||||
|
||||
if(port == null || port == ''){
|
||||
port = 25565
|
||||
}
|
||||
if(typeof port === 'string'){
|
||||
port = parseInt(port)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(port, address, () => {
|
||||
let buff = Buffer.from([0xFE, 0x01])
|
||||
socket.write(buff)
|
||||
})
|
||||
|
||||
socket.setTimeout(2500, () => {
|
||||
socket.end()
|
||||
reject({
|
||||
code: 'ETIMEDOUT',
|
||||
errno: 'ETIMEDOUT',
|
||||
address,
|
||||
port
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if(data != null && data != ''){
|
||||
let server_info = data.toString().split('\x00\x00\x00')
|
||||
const NUM_FIELDS = 6
|
||||
if(server_info != null && server_info.length >= NUM_FIELDS){
|
||||
resolve({
|
||||
online: true,
|
||||
version: server_info[2].replace(/\u0000/g, ''),
|
||||
motd: server_info[3].replace(/\u0000/g, ''),
|
||||
onlinePlayers: server_info[4].replace(/\u0000/g, ''),
|
||||
maxPlayers: server_info[5].replace(/\u0000/g,'')
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
online: false
|
||||
})
|
||||
}
|
||||
}
|
||||
socket.end()
|
||||
})
|
||||
|
||||
socket.on('error', (err) => {
|
||||
socket.destroy()
|
||||
reject(err)
|
||||
// ENOTFOUND = Unable to resolve.
|
||||
// ECONNREFUSED = Unable to connect to port.
|
||||
})
|
||||
})
|
||||
|
||||
}
|
@ -1,75 +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.
|
||||
*/
|
||||
const $ = require('jquery');
|
||||
const {remote, shell} = require('electron')
|
||||
|
||||
/* jQuery Example
|
||||
$(function(){
|
||||
console.log('UICore Initialized');
|
||||
})*/
|
||||
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (document.readyState === 'interactive'){
|
||||
console.log('UICore Initializing..');
|
||||
|
||||
// Bind close button.
|
||||
document.getElementById("frame_btn_close").addEventListener("click", function (e) {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.close()
|
||||
})
|
||||
|
||||
// Bind restore down button.
|
||||
document.getElementById("frame_btn_restoredown").addEventListener("click", function (e) {
|
||||
const window = remote.getCurrentWindow()
|
||||
if(window.isMaximized()){
|
||||
window.unmaximize();
|
||||
} else {
|
||||
window.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
// Bind minimize button.
|
||||
document.getElementById("frame_btn_minimize").addEventListener("click", function (e) {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.minimize()
|
||||
})
|
||||
|
||||
} else if(document.readyState === 'complete'){
|
||||
|
||||
// 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 = targetWidth
|
||||
document.getElementById("launch_progress").style.width = targetWidth2
|
||||
document.getElementById("launch_details_right").style.maxWidth = targetWidth2
|
||||
document.getElementById("launch_progress_label").style.width = targetWidth3
|
||||
|
||||
}
|
||||
|
||||
}, false)
|
||||
|
||||
/**
|
||||
* Open web links in the user's default browser.
|
||||
*/
|
||||
$(document).on('click', 'a[href^="http"]', function(event) {
|
||||
event.preventDefault();
|
||||
//console.log(os.homedir())
|
||||
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.keyCode == 73 && e.ctrlKey && e.shiftKey){
|
||||
let window = remote.getCurrentWindow()
|
||||
window.toggleDevTools()
|
||||
}
|
||||
})
|
@ -1,44 +0,0 @@
|
||||
const app = require('electron')
|
||||
const remote = require('electron').BrowserWindow
|
||||
|
||||
/**
|
||||
* Doesn't work yet.
|
||||
*/
|
||||
exports.setIconBadge = function(text){
|
||||
if(process.platform === 'darwin'){
|
||||
app.dock.setBadge('' + text)
|
||||
} else if (process.platform === 'win32'){
|
||||
const win = remote.getFocusedWindow()
|
||||
if(text === ''){
|
||||
win.setOverlayIcon(null, '')
|
||||
return;
|
||||
}
|
||||
|
||||
//Create badge
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.height = 140;
|
||||
canvas.width = 140;
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.fillStyle = '#a02d2a'
|
||||
ctx.beginPath()
|
||||
ctx.ellipse(70, 70, 70, 70, 0, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillStyle = 'white'
|
||||
|
||||
if(text.length > 2 ){
|
||||
ctx.font = '75px sans-serif'
|
||||
ctx.fillText('' + text, 70, 98)
|
||||
} else if (text.length > 1){
|
||||
ctx.font = '100px sans-serif'
|
||||
ctx.fillText('' + text, 70, 105)
|
||||
} else {
|
||||
ctx.font = '125px sans-serif'
|
||||
ctx.fillText('' + text, 70, 112)
|
||||
}
|
||||
|
||||
const badgeDataURL = canvas.toDataURL()
|
||||
const img = NativeImage.createFromDataURL(badgeDataURL)
|
||||
win.setOverlayIcon(img, '' + text)
|
||||
}
|
||||
}
|
20
app/assets/lang/_custom.toml
Normal file
@ -0,0 +1,20 @@
|
||||
# Custom Language File for Launcher Customizer
|
||||
|
||||
[ejs.app]
|
||||
title = "Helios Launcher"
|
||||
|
||||
[ejs.landing]
|
||||
mediaGitHubURL = "https://github.com/dscalzi/HeliosLauncher"
|
||||
mediaTwitterURL = "#"
|
||||
mediaInstagramURL = "#"
|
||||
mediaYouTubeURL = "#"
|
||||
mediaDiscordURL = "https://discord.gg/zNWUXdt"
|
||||
|
||||
[ejs.settings]
|
||||
sourceGithubLink = "https://github.com/dscalZi/HeliosLauncher"
|
||||
supportLink = "https://github.com/dscalZi/HeliosLauncher/issues"
|
||||
|
||||
[ejs.welcome]
|
||||
welcomeHeader = "WELCOME TO WESTEROSCRAFT"
|
||||
welcomeDescription = "Our mission is to recreate the universe imagined by author George RR Martin in his fantasy series, A Song of Ice and Fire. Through the collaborative effort of thousands of community members, we have sought to create Westeros as accurately and precisely as possible within Minecraft. The world we are creating is yours to explore. Journey from Dorne to Castle Black, and if you aren’t afraid, beyond the Wall itself, but best not delay. As the words of House Stark ominously warn: Winter is Coming."
|
||||
welcomeDescCTA = "You are just a few clicks away from Westeros."
|
297
app/assets/lang/en_US.toml
Normal file
@ -0,0 +1,297 @@
|
||||
[ejs.landing]
|
||||
updateAvailableTooltip = "Update Available"
|
||||
usernamePlaceholder = "Username"
|
||||
usernameEditButton = "Edit"
|
||||
settingsTooltip = "Settings"
|
||||
serverStatus = "SERVER"
|
||||
serverStatusPlaceholder = "OFFLINE"
|
||||
mojangStatus = "MOJANG STATUS"
|
||||
mojangStatusTooltipTitle = "Services"
|
||||
mojangStatusNETitle = "Non Essential"
|
||||
newsButton = "NEWS"
|
||||
launchButton = "PLAY"
|
||||
launchButtonPlaceholder = "• No Server Selected"
|
||||
launchDetails = "Please wait.."
|
||||
newsNavigationStatus = "{currentPage} of {totalPages}"
|
||||
newsErrorLoadSpan = "Checking for News.."
|
||||
newsErrorFailedSpan = "Failed to Load News"
|
||||
newsErrorRetryButton = "Try Again"
|
||||
newsErrorNoneSpan = "No News"
|
||||
|
||||
[ejs.login]
|
||||
loginCancelText = "Cancel"
|
||||
loginSubheader = "MINECRAFT LOGIN"
|
||||
loginEmailError = "* Invalid Value"
|
||||
loginEmailPlaceholder = "EMAIL OR USERNAME"
|
||||
loginPasswordError = "* Required"
|
||||
loginPasswordPlaceholder = "PASSWORD"
|
||||
loginForgotPasswordLink = "https://minecraft.net/password/forgot/"
|
||||
loginForgotPasswordText = "forgot password?"
|
||||
loginRememberMeText = "remember me?"
|
||||
loginButtonText = "LOGIN"
|
||||
loginNeedAccountLink = "https://minecraft.net/store/minecraft-java-edition/"
|
||||
loginNeedAccountText = "Need an Account?"
|
||||
loginPasswordDisclaimer1 = "Your password is sent directly to mojang and never stored."
|
||||
loginPasswordDisclaimer2 = "{appName} is not affiliated with Mojang AB."
|
||||
|
||||
[ejs.loginOptions]
|
||||
loginOptionsTitle = "Login Options"
|
||||
loginWithMicrosoft = "Login with Microsoft"
|
||||
loginWithMojang = "Login with Mojang"
|
||||
cancelButton = "Cancel"
|
||||
|
||||
[ejs.overlay]
|
||||
serverSelectHeader = "Available Servers"
|
||||
serverSelectConfirm = "Select"
|
||||
serverSelectCancel = "Cancel"
|
||||
accountSelectHeader = "Select an Account"
|
||||
accountSelectConfirm = "Select"
|
||||
accountSelectCancel = "Cancel"
|
||||
|
||||
[ejs.settings]
|
||||
navHeaderText = "Settings"
|
||||
navAccount = "Account"
|
||||
navMinecraft = "Minecraft"
|
||||
navMods = "Mods"
|
||||
navJava = "Java"
|
||||
navLauncher = "Launcher"
|
||||
navAbout = "About"
|
||||
navUpdates = "Updates"
|
||||
navDone = "Done"
|
||||
tabAccountHeaderText = "Account Settings"
|
||||
tabAccountHeaderDesc = "Add new accounts or manage existing ones."
|
||||
microsoftAccount = "Microsoft"
|
||||
addMicrosoftAccount = "+ Add Microsoft Account"
|
||||
mojangAccount = "Mojang"
|
||||
addMojangAccount = "+ Add Mojang Account"
|
||||
minecraftTabHeaderText = "Minecraft Settings"
|
||||
minecraftTabHeaderDesc = "Options related to game launch."
|
||||
gameResolutionTitle = "Game Resolution"
|
||||
launchFullscreenTitle = "Launch in fullscreen."
|
||||
autoConnectTitle = "Automatically connect to the server on launch."
|
||||
launchDetachedTitle = "Launch game process detached from launcher."
|
||||
launchDetachedDesc = "If the game is not detached, closing the launcher will also close the game."
|
||||
tabModsHeaderText = "Mod Settings"
|
||||
tabModsHeaderDesc = "Enable or disable mods."
|
||||
switchServerButton = "Switch"
|
||||
requiredMods = "Required Mods"
|
||||
optionalMods = "Optional Mods"
|
||||
dropinMods = "Drop-in Mods"
|
||||
addMods = "Add Mods"
|
||||
dropinRefreshNote = "(F5 to Refresh)"
|
||||
shaderpacks = "Shaderpacks"
|
||||
shaderpackDesc = "Enable or disable shaders. Please note, shaders will only run smoothly on powerful setups. You may add custom packs here."
|
||||
selectShaderpack = "Select Shaderpack"
|
||||
tabJavaHeaderText = "Java Settings"
|
||||
tabJavaHeaderDesc = "Manage the Java configuration (advanced)."
|
||||
memoryTitle = "Memory"
|
||||
maxRAM = "Maximum RAM"
|
||||
minRAM = "Minimum RAM"
|
||||
memoryDesc = "The recommended minimum RAM is 3 gigabytes. Setting the minimum and maximum values to the same value may reduce lag."
|
||||
memoryTotalTitle = "Total"
|
||||
memoryAvailableTitle = "Available"
|
||||
javaExecutableTitle = "Java Executable"
|
||||
javaExecSelDialogTitle = "Select Java Executable"
|
||||
javaExecSelButtonText = "Choose File"
|
||||
javaExecDesc = "The Java executable is validated before game launch."
|
||||
javaPathDesc = "The path should end with <strong>{pathSuffix}</strong>."
|
||||
jvmOptsTitle = "Additional JVM Options"
|
||||
jvmOptsDesc = "Options to be provided to the JVM at runtime. <em>-Xms</em> and <em>-Xmx</em> should not be included."
|
||||
launcherTabHeaderText = "Launcher Settings"
|
||||
launcherTabHeaderDesc = "Options related to the launcher itself."
|
||||
allowPrereleaseTitle = "Allow Pre-Release Updates."
|
||||
allowPrereleaseDesc = "Pre-Releases include new features which may have not been fully tested or integrated.<br>This will always be true if you are using a pre-release version."
|
||||
dataDirectoryTitle = "Data Directory"
|
||||
selectDataDirectory = "Select Data Directory"
|
||||
chooseFolder = "Choose Folder"
|
||||
dataDirectoryDesc = "All game files and local Java installations will be stored in the data directory.<br>Screenshots and world saves are stored in the instance folder for the corresponding server configuration."
|
||||
aboutTabHeaderText = "About"
|
||||
aboutTabHeaderDesc = "View information and release notes for the current version."
|
||||
aboutTitle = "{appName}"
|
||||
stableRelease = "Stable Release"
|
||||
versionText = "Version "
|
||||
sourceGithub = "Source (GitHub)"
|
||||
support = "Support"
|
||||
devToolsConsole = "DevTools Console"
|
||||
releaseNotes = "Release Notes"
|
||||
changelog = "Changelog"
|
||||
noReleaseNotes = "No Release Notes"
|
||||
viewReleaseNotes = "View Release Notes on GitHub"
|
||||
launcherUpdatesHeaderText = "Launcher Updates"
|
||||
launcherUpdatesHeaderDesc = "Download, install, and review updates for the launcher."
|
||||
checkForUpdates = "Check for Updates"
|
||||
whatsNew = "What's New"
|
||||
updateReleaseNotes = "Update Release Notes"
|
||||
|
||||
[ejs.waiting]
|
||||
waitingText = "Waiting for Microsoft.."
|
||||
|
||||
[ejs.welcome]
|
||||
continueButton = "CONTINUE"
|
||||
|
||||
|
||||
[js.discord]
|
||||
waiting = "Waiting for Client.."
|
||||
state = "Server: {shortId}"
|
||||
|
||||
[js.index]
|
||||
microsoftLoginTitle = "Microsoft Login"
|
||||
microsoftLogoutTitle = "Microsoft Logout"
|
||||
|
||||
[js.login]
|
||||
login = "LOGIN"
|
||||
loggingIn = "LOGGING IN"
|
||||
success = "SUCCESS"
|
||||
tryAgain = "Try Again"
|
||||
|
||||
[js.login.error]
|
||||
invalidValue = "* Invalid Value"
|
||||
requiredValue = "* Required"
|
||||
|
||||
[js.login.error.unknown]
|
||||
title = "Unknown Error During Login"
|
||||
desc = "An unknown error has occurred. Please see the console for details."
|
||||
|
||||
[js.landing.launch]
|
||||
pleaseWait = "Please wait.."
|
||||
failureTitle = "Error During Launch"
|
||||
failureText = "See console (CTRL + Shift + i) for more details."
|
||||
okay = "Okay"
|
||||
|
||||
[js.landing.selectedAccount]
|
||||
noAccountSelected = "No Account Selected"
|
||||
|
||||
[js.landing.selectedServer]
|
||||
noSelection = "No Server Selected"
|
||||
loading = "Loading.."
|
||||
|
||||
[js.landing.serverStatus]
|
||||
server = "SERVER"
|
||||
offline = "OFFLINE"
|
||||
players = "PLAYERS"
|
||||
|
||||
[js.landing.systemScan]
|
||||
checking = "Checking system info.."
|
||||
noCompatibleJava = "No Compatible<br>Java Installation Found"
|
||||
installJavaMessage = "In order to launch Minecraft, you need a 64-bit installation of Java {major}. Would you like us to install a copy?"
|
||||
installJava = "Install Java"
|
||||
installJavaManually = "Install Manually"
|
||||
javaDownloadPrepare = "Preparing Java Download.."
|
||||
javaDownloadFailureTitle = "Error During Java Download"
|
||||
javaDownloadFailureText = "See console (CTRL + Shift + i) for more details."
|
||||
javaRequired = "Java is Required<br>to Launch"
|
||||
javaRequiredMessage = 'A valid x64 installation of Java {major} is required to launch.<br><br>Please refer to our <a href="https://github.com/dscalzi/HeliosLauncher/wiki/Java-Management#manually-installing-a-valid-version-of-java">Java Management Guide</a> for instructions on how to manually install Java.'
|
||||
javaRequiredDismiss = "I Understand"
|
||||
javaRequiredCancel = "Go Back"
|
||||
|
||||
[js.landing.downloadJava]
|
||||
findJdkFailure = "Failed to find OpenJDK distribution."
|
||||
javaDownloadCorruptedError = "Downloaded JDK has a bad hash, the file may be corrupted."
|
||||
extractingJava = "Extracting Java"
|
||||
javaInstalled = "Java Installed!"
|
||||
|
||||
[js.landing.dlAsync]
|
||||
loadingServerInfo = "Loading server information.."
|
||||
fatalError = "Fatal Error"
|
||||
unableToLoadDistributionIndex = "Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details."
|
||||
pleaseWait = "Please wait.."
|
||||
errorDuringLaunchTitle = "Error During Launch"
|
||||
seeConsoleForDetails = "See console (CTRL + Shift + i) for more details."
|
||||
validatingFileIntegrity = "Validating file integrity.."
|
||||
errorDuringFileVerificationTitle = "Error During File Verification"
|
||||
downloadingFiles = "Downloading files.."
|
||||
errorDuringFileDownloadTitle = "Error During File Download"
|
||||
preparingToLaunch = "Preparing to launch.."
|
||||
launchingGame = "Launching game.."
|
||||
launchWrapperNotDownloaded = "The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.<br><br>To fix this issue, temporarily turn off your antivirus software and launch the game again.<br><br>If you have time, please <a href=\"https://github.com/dscalzi/HeliosLauncher/issues\">submit an issue</a> and let us know what antivirus software you use. We'll contact them and try to straighten things out."
|
||||
doneEnjoyServer = "Done. Enjoy the server!"
|
||||
checkConsoleForDetails = "Please check the console (CTRL + Shift + i) for more details."
|
||||
|
||||
[js.landing.news]
|
||||
checking = "Checking for News"
|
||||
|
||||
[js.landing.discord]
|
||||
loading = "Loading game.."
|
||||
joining = "Sailing to Westeros!"
|
||||
joined = "Exploring the Realm!"
|
||||
|
||||
[js.overlay]
|
||||
dismiss = "Dismiss"
|
||||
|
||||
[js.settings.fileSelectors]
|
||||
executables = "Executables"
|
||||
allFiles = "All Files"
|
||||
|
||||
[js.settings.mstfLogin]
|
||||
errorTitle = "Something Went Wrong"
|
||||
errorMessage = "Microsoft authentication failed. Please try again."
|
||||
okButton = "OK"
|
||||
|
||||
[js.settings.mstfLogout]
|
||||
errorTitle = "Something Went Wrong"
|
||||
errorMessage = "Microsoft logout failed. Please try again."
|
||||
okButton = "OK"
|
||||
|
||||
[js.settings.authAccountSelect]
|
||||
selectButton = "Select Account"
|
||||
selectedButton = "Selected Account ✔"
|
||||
|
||||
[js.settings.authAccountLogout]
|
||||
lastAccountWarningTitle = "Warning<br>This is Your Last Account"
|
||||
lastAccountWarningMessage = "In order to use the launcher you must be logged into at least one account. You will need to login again after.<br><br>Are you sure you want to log out?"
|
||||
confirmButton = "I'm Sure"
|
||||
cancelButton = "Cancel"
|
||||
|
||||
[js.settings.authAccountPopulate]
|
||||
username = "Username"
|
||||
uuid = "UUID"
|
||||
selectAccount = "Select Account"
|
||||
selectedAccount = "Selected Account ✓"
|
||||
logout = "Log Out"
|
||||
|
||||
[js.settings.dropinMods]
|
||||
removeButton = "Remove"
|
||||
deleteFailedTitle = "Failed to Delete<br>Drop-in Mod {fullName}"
|
||||
deleteFailedMessage = "Make sure the file is not in use and try again."
|
||||
failedToggleTitle = "Failed to Toggle<br>One or More Drop-in Mods"
|
||||
okButton = "Okay"
|
||||
|
||||
[js.settings.serverListing]
|
||||
mainServer = "Main Server"
|
||||
|
||||
[js.settings.java]
|
||||
selectedJava = "Selected: Java {version} ({vendor})"
|
||||
invalidSelection = "Invalid Selection"
|
||||
requiresJava = "Requires Java {major} x64."
|
||||
availableOptions = "Available Options for Java {major} (HotSpot VM)"
|
||||
|
||||
[js.settings.about]
|
||||
preReleaseTitle = "Pre-release"
|
||||
stableReleaseTitle = "Stable Release"
|
||||
releaseNotesFailed = "Failed to load release notes."
|
||||
|
||||
[js.settings.updates]
|
||||
newReleaseTitle = "New Release Available"
|
||||
newPreReleaseTitle = "New Pre-release Available"
|
||||
downloadingButton = "Downloading.."
|
||||
downloadButton = 'Download from GitHub<span style="font-size: 10px;color: gray;text-shadow: none !important;">Close the launcher and run the dmg to update.</span>'
|
||||
latestVersionTitle = "You Are Running the Latest Version"
|
||||
checkForUpdatesButton = "Check for Updates"
|
||||
checkingForUpdatesButton = "Checking for Updates.."
|
||||
|
||||
[js.uibinder.startup]
|
||||
fatalErrorTitle = "Fatal Error: Unable to Load Distribution Index"
|
||||
fatalErrorMessage = "A connection could not be established to our servers to download the distribution index. No local copies were available to load. <br><br>The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. Ensure you are connected to the internet and relaunch the application."
|
||||
closeButton = "Close"
|
||||
|
||||
[js.uibinder.validateAccount]
|
||||
failedMessageTitle = "Failed to Refresh Login"
|
||||
failedMessage = "We were unable to refresh the login for <strong>{account}</strong>. Please select another account or login again."
|
||||
failedMessageSelectAnotherAccount = "We were unable to refresh the login for <strong>{account}</strong>. Please login again."
|
||||
loginButton = "Login"
|
||||
selectAnotherAccountButton = "Select Another Account"
|
||||
|
||||
[js.uicore.autoUpdate]
|
||||
checkingForUpdateButton = "Checking for Updates..."
|
||||
installNowButton = "Install Now"
|
||||
checkForUpdatesButton = "Check for Updates"
|
@ -1,411 +0,0 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"discord": {
|
||||
"clientID": "385581240906022916",
|
||||
"smallImageText": "WesterosCraft",
|
||||
"smallImageKey": "seal-circle"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"id": "WesterosCraft-1.11.2",
|
||||
"name": "WesterosCraft Production Server",
|
||||
"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",
|
||||
"discord": {
|
||||
"shortId": "Production",
|
||||
"largeImageText": "WesterosCraft Production Server",
|
||||
"largeImageKey": "server-prod"
|
||||
},
|
||||
"default_selected": true,
|
||||
"autoconnect": true,
|
||||
"modules": [
|
||||
{
|
||||
"id": "net.minecraftforge:forge:1.11.2-13.20.1.2429",
|
||||
"name": "Minecraft Forge 1.11.2-13.20.1.2429",
|
||||
"type": "forge-hosted",
|
||||
"artifact": {
|
||||
"size": 4450992,
|
||||
"MD5": "3fcc9b0104f0261397d3cc897e55a1c5",
|
||||
"extension": ".jar",
|
||||
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar"
|
||||
},
|
||||
"sub_modules": [
|
||||
{
|
||||
"id": "net.minecraft:launchwrapper:1.12",
|
||||
"name": "Mojang (LaunchWrapper)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 32999,
|
||||
"MD5": "934b2d91c7c5be4a49577c9e6b40e8da",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.ow2.asm:asm-all:5.0.3",
|
||||
"name": "Mojang (ASM)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 241639,
|
||||
"MD5": "c5cc4613bbdfba3ccf5f0ab85390d0b8",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/asm-all-5.0.3.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang:scala-library:2.11.1",
|
||||
"name": "Minecraft Forge (scala-library)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 1474672,
|
||||
"MD5": "379c15c4f724421c6d5d7aecedaf87a6",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-library-2.11.1.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang:scala-compiler:2.11.1",
|
||||
"name": "Minecraft Forge (scala-compiler)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 3076920,
|
||||
"MD5": "7d89e952f2d5c74577310cd2c28e3f20",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-compiler-2.11.1.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang:scala-actors-migration_2.11:1.1.0",
|
||||
"name": "Minecraft Forge (scala-actors-migration)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 21324,
|
||||
"MD5": "04e3428b2600ace33c7ae2bf1f6c0a4c",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-actors-migration_2.11-1.1.0.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang.plugins:scala-continuations-library_2.11:1.0.2",
|
||||
"name": "Minecraft Forge (scala-continuations-library)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 7956,
|
||||
"MD5": "ed9b1d27aba8ac4090a3749c4dfc895a",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-library_2.11-1.0.2.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang.plugins:scala-continuations-plugin_2.11.1:1.0.2",
|
||||
"name": "Minecraft Forge (scala-continuations-plugin)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 46140,
|
||||
"MD5": "a8232db22a72a981de6b1399eb86dff7",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-plugin_2.11.1-1.0.2.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang:scala-parser-combinators_2.11:1.0.1",
|
||||
"name": "Minecraft Forge (scala-parser-combinators)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 85568,
|
||||
"MD5": "2e50a7df17680daadacca69f07f8a16d",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-parser-combinators_2.11-1.0.1.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang:scala-reflect:2.11.1",
|
||||
"name": "Minecraft Forge (scala-reflect)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 1070312,
|
||||
"MD5": "84e5dc81c10e2bd74c579c9d0332fdd9",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-reflect-2.11.1.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang:scala-swing_2.11:1.0.1",
|
||||
"name": "Minecraft Forge (scala-swing)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 736795,
|
||||
"MD5": "1d360289e697022a3f57abaad344b28f",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-swing_2.11-1.0.1.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "org.scala-lang:scala-xml_2.11:1.0.2",
|
||||
"name": "Minecraft Forge (scala-xml)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 217812,
|
||||
"MD5": "cc891b094a4c32dedc56bfefe9b072ff",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-xml_2.11-1.0.2.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "com.typesafe.akka:akka-actor_2.11:2.3.3",
|
||||
"name": "Minecraft Forge (akka-actor)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 746612,
|
||||
"MD5": "25cb22c3078e9fb3f7a861c912924862",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/akka-actor_2.11-2.3.3.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "com.typesafe:config:1.2.1",
|
||||
"name": "Minecraft Forge (typesafe-config)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 56636,
|
||||
"MD5": "10ec4ccabc4e68aac9cf87165ead5d7d",
|
||||
"extension": ".jar.pack.xz",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/config-1.2.1.jar.pack.xz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lzma:lzma:0.0.1",
|
||||
"name": "Mojang (LZMA)",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 5762,
|
||||
"MD5": "a3e3c3186e41c4a1a3027ba2bb23cdc6",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/lzma-0.0.1.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "net.sf.trove4j:trove4j:3.0.3",
|
||||
"name": "Trove4J 3.0.3",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 2523218,
|
||||
"MD5": "8fc4d4e0129244f9fd39650c5f30feb2",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/trove4j-3.0.3.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "java3d:vecmath:1.5.2",
|
||||
"name": "Vecmath 1.5.2",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 318956,
|
||||
"MD5": "e5d2b7f46c4800a32f62ce75676a5710",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/vecmath-1.5.2.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "net.sf.jopt-simple:jopt-simple:4.6",
|
||||
"name": "Jopt-simple 4.6",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 62477,
|
||||
"MD5": "13560a58a79b46b82057686543e8d727",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "net.minecraftforge:MercuriusUpdater:1.11.2",
|
||||
"name": "MercuriusUpdater 1.11.2",
|
||||
"type": "library",
|
||||
"artifact": {
|
||||
"size": 15146,
|
||||
"MD5": "7556d06064ebbfa3b334a15092d725d0",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/MercuriusUpdater-1.11.2.jar"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "net.optifine:optifine:1.11.2_HD_U_C3",
|
||||
"name": "Optifine (1.11.2_HD_U_C3)",
|
||||
"type": "forgemod",
|
||||
"artifact": {
|
||||
"size": 2106193,
|
||||
"MD5": "82f495594cd50e1fda7a8aa0246239fc",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/OptiFine.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mezz:jei:1.11.2-4.5.0.290",
|
||||
"name": "JustEnoughItems (1.11.2-4.5.0.290)",
|
||||
"type": "forgemod",
|
||||
"artifact": {
|
||||
"size": 538740,
|
||||
"MD5": "f4d931f6db6210621a86fa1e7eae8016",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/jei.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcp.mobius:waila:1.7.1_1.11.2",
|
||||
"name": "Waila (1.7.1_1.11.2)",
|
||||
"type": "forgemod",
|
||||
"required": {
|
||||
"value": false
|
||||
},
|
||||
"artifact": {
|
||||
"size": 542744,
|
||||
"MD5": "26258a3557bf333e8f4ce8b1e9481031",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/Waila.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "com.github.hexomod:worldeditcuife2:2.1.1-mf-1.11.2-13.20.0.2228",
|
||||
"name": "WorldEditCUI (v2.1.1-mf-1.11.2-13.20.0.2228)",
|
||||
"type": "forgemod",
|
||||
"required": {
|
||||
"value": false
|
||||
},
|
||||
"artifact": {
|
||||
"size": 461691,
|
||||
"MD5": "439f82b69f3464969163c188818c677b",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/worldeditcuife.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "techbrew:journeymap:1.11.2-5.4.7",
|
||||
"name": "JourneyMap (1.11.2-5.4.7)",
|
||||
"type": "forgemod",
|
||||
"required": {
|
||||
"value": false
|
||||
},
|
||||
"artifact": {
|
||||
"size": 1735525,
|
||||
"MD5": "1c3380502eb7b9a495581b2402d144df",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/journeymap.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "octarine-noise:betterfoliage:1.11.2-2.1.8",
|
||||
"name": "BetterFoliage (1.11.2-2.1.8)",
|
||||
"type": "forgemod",
|
||||
"required": {
|
||||
"value": false
|
||||
},
|
||||
"artifact": {
|
||||
"size": 4676029,
|
||||
"MD5": "b2dd47e42da56fb49a07a0d38df91bc4",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/BetterFoliage.jar"
|
||||
},
|
||||
"sub_modules": [
|
||||
{
|
||||
"id": "betterfoliage.cfg",
|
||||
"name": "BetterFoliage Configuration File",
|
||||
"type": "file",
|
||||
"artifact": {
|
||||
"size": 7878,
|
||||
"MD5": "6dd38f873c4129af05a2d6c500cbe954",
|
||||
"path": "/config/betterfoliage.cfg",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/betterfoliage.cfg"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "org.blockartistry:dynsurround:1.11.2-3.4.6.2",
|
||||
"name": "DynamicSurroundings (1.11.2-3.4.6.2)",
|
||||
"type": "forgemod",
|
||||
"required": {
|
||||
"value": false
|
||||
},
|
||||
"artifact": {
|
||||
"size": 21853035,
|
||||
"MD5": "82a6aac5420151960b8dd709deee5423",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/DynamicSurroundings.jar"
|
||||
},
|
||||
"sub_modules": [
|
||||
{
|
||||
"id": "dsurround.cfg",
|
||||
"name": "DynamicSurroundings General Configuration File",
|
||||
"type": "file",
|
||||
"artifact": {
|
||||
"size": 19736,
|
||||
"MD5": "4c64fc6cbbb83b18012ed4820b0b496e",
|
||||
"path": "/config/dsurround/dsurround.cfg",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/dsurround.cfg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "westeros.json",
|
||||
"name": "DynamicSurroundings WesterosCraft Configuration File",
|
||||
"type": "file",
|
||||
"artifact": {
|
||||
"size": 608,
|
||||
"MD5": "44eab112ff24d0bd29974c270de868ba",
|
||||
"path": "/config/dsurround/westeros.json",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/westeros.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133",
|
||||
"name": "WesterosBlocks (3.0.0-beta-6-133)",
|
||||
"type": "forgemod",
|
||||
"artifact": {
|
||||
"size": 16321712,
|
||||
"MD5": "5a89e2ab18916c18965fc93a0766cc6e",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "com.westeroscraft:westeroscraftrp:2017-08-16",
|
||||
"name": "WesterosCraft Resource Pack (2017-08-16)",
|
||||
"type": "file",
|
||||
"artifact": {
|
||||
"size": 45241339,
|
||||
"MD5": "ec2d9fdb14d5c2eafe5975a240202f1a",
|
||||
"path": "resourcepacks/WesterosCraft.zip",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/resourcepacks/WesterosCraft.zip"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "options.txt",
|
||||
"name": "Default Client Options",
|
||||
"type": "file",
|
||||
"artifact": {
|
||||
"size": 1973,
|
||||
"path": "options.txt",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/options-1.11.2.txt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "servers.dat",
|
||||
"name": "Saved Client Servers",
|
||||
"type": "file",
|
||||
"artifact": {
|
||||
"size": 84,
|
||||
"MD5": "71d99e229d7d2b8d2a6423e46832a4b8",
|
||||
"path": "servers.dat",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/servers.dat"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,403 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ServerPack version="3.0">
|
||||
<Server id="WesterosCraft-1.11.2" name="WesterosCraft Production Client" newsUrl="http://www.westeroscraft.com/home/" iconUrl="http://mc.westeroscraft.com/WesterosCraftLauncher/files/server-prod.png"
|
||||
revision="3.4.17" serverAddress="mc.westeroscraft.com" generateList="true" version="1.11.2" autoConnect="true" mainClass="net.minecraft.launchwrapper.Launch"
|
||||
libOverrides="com.google.guava:guava:17.0 org.apache.commons:commons-lang3:3.3.2">
|
||||
<Module id="forge" name="Minecraft Forge 13.20.1.2429">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/forge-1.11.2-13.20.1.2429-universal.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType order="1" jreArgs="-Dfml.ignorePatchDiscrepancies=true -Dfml.ignoreInvalidMinecraftCertificates=true" launchArgs="--tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker">Library</ModType>
|
||||
<MD5>3fcc9b0104f0261397d3cc897e55a1c5</MD5>
|
||||
|
||||
<Submodule id="launchwrapper" name="Mojang (LaunchWrapper)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar</URL>
|
||||
<MD5>934b2d91c7c5be4a49577c9e6b40e8da</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="2">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="asm" name="Mojang (ASM)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/asm-all-5.0.3.jar</URL>
|
||||
<MD5>c5cc4613bbdfba3ccf5f0ab85390d0b8</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="3">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-lib" name="Minecraft Forge (scala-library)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-library-2.11.1.jar.pack.xz</URL>
|
||||
<MD5>379c15c4f724421c6d5d7aecedaf87a6</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="4">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-compiler" name="Minecraft Forge (scala-compiler)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-compiler-2.11.1.jar.pack.xz</URL>
|
||||
<MD5>7d89e952f2d5c74577310cd2c28e3f20</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-actors-migration" name="Minecraft Forge (scala-actors-migration)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-actors-migration_2.11-1.1.0.jar.pack.xz</URL>
|
||||
<MD5>55e0dc48a2a122353628a463b9499cf0</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-continuations-library" name="Minecraft Forge (scala-continuations-library)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-library_2.11-1.0.2.jar.pack.xz</URL>
|
||||
<MD5>820008ee6df308dafa0a6c478270aa19</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-continuations-plugin" name="Minecraft Forge (scala-continuations-plugin)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-plugin_2.11.1-1.0.2.jar.pack.xz</URL>
|
||||
<MD5>657a7ad23928bf4bee68202c7d7e1d56</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-parser-combinators" name="Minecraft Forge (scala-parser-combinators)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-parser-combinators_2.11-1.0.1.jar.pack.xz</URL>
|
||||
<MD5>de496467c91610e55e6e711b6bdbf10c</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-reflect" name="Minecraft Forge (scala-reflect)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-reflect-2.11.1.jar.pack.xz</URL>
|
||||
<MD5>abbdaf710e856f6e65376706c9505952</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-swing" name="Minecraft Forge (scala-swing)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-swing_2.11-1.0.1.jar</URL>
|
||||
<MD5>1d360289e697022a3f57abaad344b28f</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-xml" name="Minecraft Forge (scala-xml)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-xml_2.11-1.0.2.jar.pack.xz</URL>
|
||||
<MD5>cc891b094a4c32dedc56bfefe9b072ff</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="akka-actor" name="Minecraft Forge (akka-actor)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/akka-actor_2.11-2.3.3.jar.pack.xz</URL>
|
||||
<MD5>25cb22c3078e9fb3f7a861c912924862</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="typesafe-config" name="Minecraft Forge (typesafe-config)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/config-1.2.1.jar.pack.xz</URL>
|
||||
<MD5>f24b281c40daef1280d5ef26b11caa63</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="lzma" name="Mojang (LZMA)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/lzma-0.0.1.jar</URL>
|
||||
<MD5>a3e3c3186e41c4a1a3027ba2bb23cdc6</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="6">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="trove4j" name="Trove4J 3.0.3">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/trove4j-3.0.3.jar</URL>
|
||||
<MD5>8fc4d4e0129244f9fd39650c5f30feb2</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="7">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="vecmath" name="Vecmath 1.5.2">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/vecmath-1.5.2.jar</URL>
|
||||
<MD5>e5d2b7f46c4800a32f62ce75676a5710</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="8">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="jopt-simple" name="Jopt-simple 4.6">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar</URL>
|
||||
<MD5>13560a58a79b46b82057686543e8d727</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="9">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="MercuriusUpdater" name="MercuriusUpdater 1.11.2">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/MercuriusUpdater-1.11.2.jar</URL>
|
||||
<MD5>13560a58a79b46b82057686543e8d727</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="10">Library</ModType>
|
||||
</Submodule>
|
||||
</Module>
|
||||
<Module name="WesterosBlocks (3.0.0-beta-6-133)" id="WesterosBlocks">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>5a89e2ab18916c18965fc93a0766cc6e</MD5>
|
||||
</Module>
|
||||
<Module name="OptiFine (1.11.2_HD_U_C3)" id="OptiFine">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/OptiFine.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>82f495594cd50e1fda7a8aa0246239fc</MD5>
|
||||
</Module>
|
||||
<Module name="JustEnoughItems (1.11.2-4.5.0.290)" id="JustEnoughItems">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/jei.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>f4d931f6db6210621a86fa1e7eae8016</MD5>
|
||||
</Module>
|
||||
<Module name="Waila (1.7.1_1.11.2)" id="Waila">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/Waila.jar</URL>
|
||||
<Required isDefault="false">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>26258a3557bf333e8f4ce8b1e9481031</MD5>
|
||||
</Module>
|
||||
<Module name="WorldEditCUI (v2.1.1-mf-1.11.2-13.20.0.2228)" id="worldeditcuife">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/worldeditcuife.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>439f82b69f3464969163c188818c677b</MD5>
|
||||
</Module>
|
||||
<Module id="journeymap" name="JourneyMap (1.11.2-5.4.7)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/journeymap.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>1c3380502eb7b9a495581b2402d144df</MD5>
|
||||
</Module>
|
||||
<Module id="BetterFoliage" name="BetterFoliage (1.11.2-2.1.8)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/BetterFoliage.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>b2dd47e42da56fb49a07a0d38df91bc4</MD5>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/betterfoliage.cfg</URL>
|
||||
<Path>/config/betterfoliage.cfg</Path>
|
||||
<MD5>6dd38f873c4129af05a2d6c500cbe954</MD5>
|
||||
</ConfigFile>
|
||||
</Module>
|
||||
<Module id="DynamicSurroundings" name="DynamicSurroundings (1.11.2-3.4.6.2)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/DynamicSurroundings.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>82a6aac5420151960b8dd709deee5423</MD5>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/dsurround.cfg</URL>
|
||||
<Path>/config/dsurround/dsurround.cfg</Path>
|
||||
<MD5>4c64fc6cbbb83b18012ed4820b0b496e</MD5>
|
||||
</ConfigFile>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/westeros.json</URL>
|
||||
<Path>/config/dsurround/westeros.json</Path>
|
||||
<MD5>44eab112ff24d0bd29974c270de868ba</MD5>
|
||||
</ConfigFile>
|
||||
</Module>
|
||||
<Module id="WesterosCraftRP" name="WesterosCraft Resource Pack (2017-08-16)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/resourcepacks/WesterosCraft.zip</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<ModPath>/resourcepacks/WesterosCraft.zip</ModPath>
|
||||
<MD5>ec2d9fdb14d5c2eafe5975a240202f1a</MD5>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/files/options-1.11.2.txt</URL>
|
||||
<Path>/options.txt</Path>
|
||||
<NoOverwrite>true</NoOverwrite>
|
||||
</ConfigFile>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/servers.dat</URL>
|
||||
<Path>/servers.dat</Path>
|
||||
<MD5>71d99e229d7d2b8d2a6423e46832a4b8</MD5>
|
||||
</ConfigFile>
|
||||
</Module>
|
||||
</Server>
|
||||
|
||||
<Server id="WesterosCraftTest-1.11.2" name="WesterosCraft Test Client" newsUrl="http://www.westeroscraft.com/home/" iconUrl="http://mc.westeroscraft.com/WesterosCraftLauncher/files/server-test.png"
|
||||
revision="3.4.18" serverAddress="mc.westeroscraft.com:4444" generateList="false" version="1.11.2" autoConnect="true" mainClass="net.minecraft.launchwrapper.Launch"
|
||||
libOverrides="com.google.guava:guava:17.0 org.apache.commons:commons-lang3:3.3.2">
|
||||
<Module id="forge" name="Minecraft Forge 13.20.1.2476">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/forge-1.11.2-13.20.1.2476-universal.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType order="1" jreArgs="-Dfml.ignorePatchDiscrepancies=true -Dfml.ignoreInvalidMinecraftCertificates=true" launchArgs="--tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker">Library</ModType>
|
||||
<MD5>7cef816cc01d53a04a180f0214d2982a</MD5>
|
||||
|
||||
<Submodule id="launchwrapper" name="Mojang (LaunchWrapper)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar</URL>
|
||||
<MD5>934b2d91c7c5be4a49577c9e6b40e8da</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="2">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="asm" name="Mojang (ASM)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/asm-all-5.0.3.jar</URL>
|
||||
<MD5>c5cc4613bbdfba3ccf5f0ab85390d0b8</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="3">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-lib" name="Minecraft Forge (scala-library)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-library-2.11.1.jar.pack.xz</URL>
|
||||
<MD5>379c15c4f724421c6d5d7aecedaf87a6</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="4">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-compiler" name="Minecraft Forge (scala-compiler)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-compiler-2.11.1.jar.pack.xz</URL>
|
||||
<MD5>7d89e952f2d5c74577310cd2c28e3f20</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-actors-migration" name="Minecraft Forge (scala-actors-migration)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-actors-migration_2.11-1.1.0.jar.pack.xz</URL>
|
||||
<MD5>55e0dc48a2a122353628a463b9499cf0</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-continuations-library" name="Minecraft Forge (scala-continuations-library)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-library_2.11-1.0.2.jar.pack.xz</URL>
|
||||
<MD5>820008ee6df308dafa0a6c478270aa19</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-continuations-plugin" name="Minecraft Forge (scala-continuations-plugin)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-plugin_2.11.1-1.0.2.jar.pack.xz</URL>
|
||||
<MD5>657a7ad23928bf4bee68202c7d7e1d56</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-parser-combinators" name="Minecraft Forge (scala-parser-combinators)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-parser-combinators_2.11-1.0.1.jar.pack.xz</URL>
|
||||
<MD5>de496467c91610e55e6e711b6bdbf10c</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-reflect" name="Minecraft Forge (scala-reflect)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-reflect-2.11.1.jar.pack.xz</URL>
|
||||
<MD5>abbdaf710e856f6e65376706c9505952</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-swing" name="Minecraft Forge (scala-swing)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-swing_2.11-1.0.1.jar</URL>
|
||||
<MD5>1d360289e697022a3f57abaad344b28f</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="scala-xml" name="Minecraft Forge (scala-xml)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-xml_2.11-1.0.2.jar.pack.xz</URL>
|
||||
<MD5>cc891b094a4c32dedc56bfefe9b072ff</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="akka-actor" name="Minecraft Forge (akka-actor)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/akka-actor_2.11-2.3.3.jar.pack.xz</URL>
|
||||
<MD5>25cb22c3078e9fb3f7a861c912924862</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="typesafe-config" name="Minecraft Forge (typesafe-config)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/config-1.2.1.jar.pack.xz</URL>
|
||||
<MD5>f24b281c40daef1280d5ef26b11caa63</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="5">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="lzma" name="Mojang (LZMA)">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/lzma-0.0.1.jar</URL>
|
||||
<MD5>a3e3c3186e41c4a1a3027ba2bb23cdc6</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="6">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="trove4j" name="Trove4J 3.0.3">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/trove4j-3.0.3.jar</URL>
|
||||
<MD5>8fc4d4e0129244f9fd39650c5f30feb2</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="7">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="vecmath" name="Vecmath 1.5.2">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/vecmath-1.5.2.jar</URL>
|
||||
<MD5>e5d2b7f46c4800a32f62ce75676a5710</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="8">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="jopt-simple" name="Jopt-simple 4.6">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar</URL>
|
||||
<MD5>13560a58a79b46b82057686543e8d727</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="9">Library</ModType>
|
||||
</Submodule>
|
||||
<Submodule id="MercuriusUpdater" name="MercuriusUpdater 1.11.2">
|
||||
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/MercuriusUpdater-1.11.2.jar</URL>
|
||||
<MD5>13560a58a79b46b82057686543e8d727</MD5>
|
||||
<Required>true</Required>
|
||||
<ModType order="10">Library</ModType>
|
||||
</Submodule>
|
||||
</Module>
|
||||
<Module name="WesterosBlocks (3.0.0-beta-6-133)" id="WesterosBlocks">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/WesterosBlocks.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>5a89e2ab18916c18965fc93a0766cc6e</MD5>
|
||||
</Module>
|
||||
<Module name="OptiFine (1.11.2_HD_U_C3)" id="OptiFine">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/OptiFine.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>82f495594cd50e1fda7a8aa0246239fc</MD5>
|
||||
</Module>
|
||||
<Module name="JustEnoughItems (1.11.2-4.5.0.290)" id="JustEnoughItems">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/jei.jar</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>f4d931f6db6210621a86fa1e7eae8016</MD5>
|
||||
</Module>
|
||||
<Module name="Waila (1.7.1_1.11.2)" id="Waila">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/Waila.jar</URL>
|
||||
<Required isDefault="false">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>26258a3557bf333e8f4ce8b1e9481031</MD5>
|
||||
</Module>
|
||||
<Module name="WorldEditCUI (v2.1.1-mf-1.11.2-13.20.0.2228)" id="worldeditcuife">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/worldeditcuife.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>439f82b69f3464969163c188818c677b</MD5>
|
||||
</Module>
|
||||
<Module id="journeymap" name="JourneyMap (1.11.2-5.4.7)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/journeymap.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>1c3380502eb7b9a495581b2402d144df</MD5>
|
||||
</Module>
|
||||
<Module id="BetterFoliage" name="BetterFoliage (1.11.2-2.1.8)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/BetterFoliage.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>b2dd47e42da56fb49a07a0d38df91bc4</MD5>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/config/betterfoliage.cfg</URL>
|
||||
<Path>/config/betterfoliage.cfg</Path>
|
||||
<MD5>6dd38f873c4129af05a2d6c500cbe954</MD5>
|
||||
</ConfigFile>
|
||||
</Module>
|
||||
<Module id="DynamicSurroundings" name="DynamicSurroundings (1.11.2-3.4.6.2)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/DynamicSurroundings.jar</URL>
|
||||
<Required isDefault="true">false</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<MD5>82a6aac5420151960b8dd709deee5423</MD5>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/config/dsurround/dsurround.cfg</URL>
|
||||
<Path>/config/dsurround/dsurround.cfg</Path>
|
||||
<MD5>4c64fc6cbbb83b18012ed4820b0b496e</MD5>
|
||||
</ConfigFile>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/config/dsurround/westeros.json</URL>
|
||||
<Path>/config/dsurround/westeros.json</Path>
|
||||
<MD5>44eab112ff24d0bd29974c270de868ba</MD5>
|
||||
</ConfigFile>
|
||||
</Module>
|
||||
<Module id="WesterosCraftRP" name="WesterosCraft Resource Pack (2017-08-16)">
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/resourcepacks/WesterosCraft.zip</URL>
|
||||
<Required>true</Required>
|
||||
<ModType>Regular</ModType>
|
||||
<ModPath>/resourcepacks/WesterosCraft.zip</ModPath>
|
||||
<MD5>ec2d9fdb14d5c2eafe5975a240202f1a</MD5>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/files/options-1.11.2.txt</URL>
|
||||
<Path>/options.txt</Path>
|
||||
<NoOverwrite>true</NoOverwrite>
|
||||
</ConfigFile>
|
||||
<ConfigFile>
|
||||
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/servers.dat</URL>
|
||||
<Path>/servers.dat</Path>
|
||||
<MD5>594de6063df993b5fde31c7290226ee4</MD5>
|
||||
</ConfigFile>
|
||||
</Module>
|
||||
</Server>
|
||||
|
||||
</ServerPack>
|
@ -1,7 +1,33 @@
|
||||
<div id="frame_bar">
|
||||
<div id="frame_btn_dock">
|
||||
<button class="frame_btn" id="frame_btn_close" tabIndex="-1"></button>
|
||||
<button class="frame_btn" id="frame_btn_restoredown" tabIndex="-1"></button>
|
||||
<button class="frame_btn" id="frame_btn_minimize" tabIndex="-1"></button>
|
||||
<div id="frameBar">
|
||||
<div id="frameResizableTop" class="frameDragPadder"></div>
|
||||
<div id="frameMain">
|
||||
<div class="frameResizableVert frameDragPadder"></div>
|
||||
<%if (process.platform === 'darwin') { %>
|
||||
<div id="frameContentDarwin">
|
||||
<div id="frameButtonDockDarwin">
|
||||
<button class="frameButtonDarwin fCb" id="frameButtonDarwin_close" tabIndex="-1"></button>
|
||||
<button class="frameButtonDarwin fMb" id="frameButtonDarwin_minimize" tabIndex="-1"></button>
|
||||
<button class="frameButtonDarwin fRb" id="frameButtonDarwin_restoredown" tabIndex="-1"></button>
|
||||
</div>
|
||||
</div>
|
||||
<% } else{ %>
|
||||
<div id="frameContentWin">
|
||||
<div id="frameTitleDock">
|
||||
<span id="frameTitleText"><%= lang('app.title') %></span>
|
||||
</div>
|
||||
<div id="frameButtonDockWin">
|
||||
<button class="frameButton fMb" id="frameButton_minimize" tabIndex="-1">
|
||||
<svg name="TitleBarMinimize" width="10" height="10" viewBox="0 0 12 12"><rect stroke="#ffffff" fill="#ffffff" width="10" height="1" x="1" y="6"></rect></svg>
|
||||
</button>
|
||||
<button class="frameButton fRb" id="frameButton_restoredown" tabIndex="-1">
|
||||
<svg name="TitleBarMaximize" width="10" height="10" viewBox="0 0 12 12"><rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="#ffffff" stroke-width="1.4px"></rect></svg>
|
||||
</button>
|
||||
<button class="frameButton fCb" id="frameButton_close" tabIndex="-1">
|
||||
<svg name="TitleBarClose" width="10" height="10" viewBox="0 0 12 12"><polygon stroke="#ffffff" fill="#ffffff" fill-rule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"></polygon></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="frameResizableVert frameDragPadder"></div>
|
||||
</div>
|
||||
</div>
|
162
app/index.ejs
@ -1,162 +0,0 @@
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Westeroscraft Launcher</title>
|
||||
<script src="./assets/js/uicore.js"></script>
|
||||
<script src="./assets/js/actionbinder.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="./assets/css/launcher.css">
|
||||
<style>
|
||||
body {
|
||||
background: url('assets/images/backgrounds/<%=0%>.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<% include frame.ejs %>
|
||||
<div id="main">
|
||||
<div id="upper">
|
||||
<div id="left">
|
||||
<img id="image_seal" src="assets/images/WesterosSealCircle.png"/>
|
||||
</div>
|
||||
<div id="content">
|
||||
</div>
|
||||
<div id="right">
|
||||
<div id="rightContainer">
|
||||
<div id="user_content">
|
||||
<span id="user_text">Username</span>
|
||||
<div id="avatarContainer">
|
||||
<div id="avatarOverlay">Edit</div>
|
||||
<img id="avatarImage" src="https://cdn.discordapp.com/avatars/169197209630277642/6650b5a50e1cb3d00a79b9b88b9a0cd4.png"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mediaContent">
|
||||
<div id="internalMedia">
|
||||
<div class="mediaContainer">
|
||||
<svg id="newsSVG" class="mediaSVG" viewBox="18 14 106.36 104.43">
|
||||
<defs>
|
||||
<style>.cls-2,.cls-3{fill:none;stroke-miterlimit:10;stroke-width:6px;}</style>
|
||||
</defs>
|
||||
<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"/>
|
||||
<g>
|
||||
<rect class="cls-1" x="31.77" y="32.96" width="33.79" height="20.76"/>
|
||||
<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"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mediaDivider"></div>
|
||||
<div id="externalMedia">
|
||||
<div class="mediaContainer">
|
||||
<a href="http://www.westeroscraft.com">
|
||||
<svg id="linkSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
|
||||
<path d="M66,73.88a3.85,3.85,0,0,0,1.73-.42,8.22,8.22,0,0,1-.94-3.76,8.36,8.36,0,0,1,8.35-8.35H95A8.35,8.35,0,1,1,95,78H85.8a21.51,21.51,0,0,1-6.65,8.72H95a17.07,17.07,0,0,0,0-34.15H75.13A17,17,0,0,0,58.59,73.88Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="https://twitter.com/westeroscraft">
|
||||
<svg id="twitterSVG" class="mediaSVG" viewBox="0 0 5000 4060" preserveAspectRatio="xMidYMid meet">
|
||||
<g>
|
||||
<path d="M1210 4048 c-350 -30 -780 -175 -1124 -378 -56 -33 -86 -57 -86 -68 0 -16 7 -17 83 -9 114 12 349 1 493 -22 295 -49 620 -180 843 -341 l54 -38 -49 -7 c-367 -49 -660 -256 -821 -582 -30 -61 -53 -120 -51 -130 3 -16 12 -17 73 -13 97 7 199 5 270 -4 l60 -9 -65 -22 c-341 -117 -609 -419 -681 -769 -18 -88 -26 -226 -13 -239 4 -3 32 7 63 22 68 35 198 77 266 86 28 4 58 9 68 12 10 2 -22 -34 -72 -82 -240 -232 -353 -532 -321 -852 15 -149 79 -347 133 -418 16 -20 17 -19 49 20 377 455 913 795 1491 945 160 41 346 74 485 86 l82 7 -7 -59 c-5 -33 -7 -117 -6 -189 2 -163 31 -286 103 -430 141 -285 422 -504 708 -550 112 -19 333 -19 442 0 180 30 335 108 477 239 l58 54 95 -24 c143 -36 286 -89 427 -160 70 -35 131 -60 135 -56 19 19 -74 209 -151 312 -50 66 -161 178 -216 217 l-30 22 73 -14 c111 -21 257 -63 353 -101 99 -39 99 -39 99 -19 0 57 -237 326 -412 468 l-88 71 6 51 c4 28 1 130 -5 226 -30 440 -131 806 -333 1202 -380 745 -1036 1277 -1823 1477 -243 62 -430 81 -786 78 -134 0 -291 -5 -349 -10z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="https://www.instagram.com/westeroscraft/">
|
||||
<svg id="instagramSVG" class="mediaSVG" viewBox="0 0 5040 5040">
|
||||
<defs>
|
||||
<radialGradient id="instaFill" cx="30%" cy="107%" r="150%">
|
||||
<stop offset="0%" stop-color="#fdf497"/>
|
||||
<stop offset="5%" stop-color="#fdf497"/>
|
||||
<stop offset="45%" stop-color="#fd5949"/>
|
||||
<stop offset="60%" stop-color="#d6249f"/>
|
||||
<stop offset="90%" stop-color="#285AEB"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M1390 5024 c-163 -9 -239 -19 -315 -38 -281 -70 -477 -177 -660 -361 -184 -184 -292 -380 -361 -660 -43 -171 -53 -456 -53 -1445 0 -989 10 -1274 53 -1445 69 -280 177 -476 361 -660 184 -184 380 -292 660 -361 171 -43 456 -53 1445 -53 989 0 1274 10 1445 53 280 69 476 177 660 361 184 184 292 380 361 660 43 171 53 456 53 1445 0 989 -10 1274 -53 1445 -69 280 -177 476 -361 660 -184 184 -380 292 -660 361 -174 44 -454 53 -1470 52 -599 0 -960 -5 -1105 -14z m2230 -473 c58 -6 141 -18 185 -27 397 -78 638 -318 719 -714 37 -183 41 -309 41 -1290 0 -981 -4 -1107 -41 -1290 -81 -395 -319 -633 -714 -714 -183 -37 -309 -41 -1290 -41 -981 0 -1107 4 -1290 41 -397 81 -636 322 -714 719 -33 166 -38 296 -43 1100 -5 796 3 1203 27 1380 67 489 338 758 830 825 47 7 162 15 255 20 250 12 1907 4 2035 -9z"/>
|
||||
<path d="M2355 3819 c-307 -42 -561 -172 -780 -400 -244 -253 -359 -543 -359 -899 0 -361 116 -648 367 -907 262 -269 563 -397 937 -397 374 0 675 128 937 397 251 259 367 546 367 907 0 361 -116 648 -367 907 -197 203 -422 326 -690 378 -101 20 -317 27 -412 14z m400 -509 c275 -88 470 -284 557 -560 20 -65 23 -95 23 -230 0 -135 -3 -165 -23 -230 -88 -278 -284 -474 -562 -562 -65 -20 -95 -23 -230 -23 -135 0 -165 3 -230 23 -278 88 -474 284 -562 562 -20 65 -23 95 -23 230 0 135 3 165 23 230 73 230 219 403 427 507 134 67 212 83 390 79 111 -3 155 -8 210 -26z"/>
|
||||
<path d="M3750 1473 c-29 -11 -66 -38 -106 -77 -70 -71 -94 -126 -94 -221 0 -95 24 -150 94 -221 72 -71 126 -94 225 -94 168 0 311 143 311 311 0 99 -23 154 -94 225 -43 42 -76 66 -110 77 -61 21 -166 21 -226 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="https://www.youtube.com/user/WesterosCraft">
|
||||
<svg id="youtubeSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M84.8,69.52,65.88,79.76V59.27Zm23.65.59c0-5.14-.79-17.63-3.94-20.57S99,45.86,73.37,45.86s-28,.73-31.14,3.68S38.29,65,38.29,70.11s.79,17.63,3.94,20.57,5.52,3.68,31.14,3.68,28-.74,31.14-3.68,3.94-15.42,3.94-20.57"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="https://discord.gg/hqdjs3m">
|
||||
<svg id="discordSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M81.23,78.48a6.14,6.14,0,1,1,6.14-6.14,6.14,6.14,0,0,1-6.14,6.14M60,78.48a6.14,6.14,0,1,1,6.14-6.14A6.14,6.14,0,0,1,60,78.48M104.41,73c-.92-7.7-8.24-22.9-8.24-22.9A43,43,0,0,0,88,45.59a17.88,17.88,0,0,0-8.38-1.27l-.13,1.06a23.52,23.52,0,0,1,5.8,1.95,87.59,87.59,0,0,1,8.17,4.87s-10.32-5.63-22.27-5.63a51.32,51.32,0,0,0-23.2,5.63,87.84,87.84,0,0,1,8.17-4.87,23.57,23.57,0,0,1,5.8-1.95l-.13-1.06a17.88,17.88,0,0,0-8.38,1.27,42.84,42.84,0,0,0-8.21,4.56S37.87,65.35,37,73s-.37,11.54-.37,11.54,4.22,5.68,9.9,7.14,7.7,1.47,7.7,1.47l3.75-4.68a21.22,21.22,0,0,1-4.65-2A24.47,24.47,0,0,1,47.93,82S61.16,88.4,70.68,88.4c10,0,22.75-6.44,22.75-6.44a24.56,24.56,0,0,1-5.35,4.56,21.22,21.22,0,0,1-4.65,2l3.75,4.68s2,0,7.7-1.47,9.89-7.14,9.89-7.14.55-3.85-.37-11.54"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lower">
|
||||
<div id="left">
|
||||
<div class="bot_wrapper">
|
||||
<div id="content">
|
||||
<span class="bot_label">PLAYERS</span>
|
||||
<span id="player_count">18/100</span>
|
||||
<div class="bot_divider"></div>
|
||||
<span class="bot_label">MOJANG STATUS</span>
|
||||
<span id="mojang_status_icon">•</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="center">
|
||||
<div class="bot_wrapper">
|
||||
<div id="content">
|
||||
<button id="menu_button">
|
||||
<img src="assets/images/icons/arrow.svg" id="menu_img"/>
|
||||
<span id="menu_text">MENU</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="right">
|
||||
<div class="bot_wrapper">
|
||||
<div id="launch_content">
|
||||
<button id="launch_button">PLAY</button>
|
||||
<div class="bot_divider"></div>
|
||||
<!-- Span until we implement the real selection -->
|
||||
<span class="bot_label" id="server_selection">• No Server Selected</span>
|
||||
</div>
|
||||
<div id="launch_details">
|
||||
<div id="launch_details_left">
|
||||
<span id="launch_progress_label">0%</span>
|
||||
<div class="bot_divider"></div>
|
||||
</div>
|
||||
<div id="launch_details_right">
|
||||
<progress id="launch_progress" value="22" max="100"></progress>
|
||||
<span id="launch_details_text" class="bot_label">Please wait..</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
220
app/landing.ejs
Normal file
@ -0,0 +1,220 @@
|
||||
<div id="landingContainer" style="display: none;">
|
||||
<div id="upper">
|
||||
<div id="left">
|
||||
<div id="image_seal_container">
|
||||
<img id="image_seal" src="assets/images/SealCircle.png"/>
|
||||
<div id="updateAvailableTooltip"><%- lang('landing.updateAvailableTooltip') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
</div>
|
||||
<div id="right">
|
||||
<div id="rightContainer">
|
||||
<div id="user_content">
|
||||
<span id="user_text"><%- lang('landing.usernamePlaceholder') %></span>
|
||||
<div id="avatarContainer">
|
||||
<button id="avatarOverlay"><%- lang('landing.usernameEditButton') %></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mediaContent">
|
||||
<div id="internalMedia">
|
||||
<div class="mediaContainer" id="settingsMediaContainer">
|
||||
<button class="mediaButton" id="settingsMediaButton">
|
||||
<svg id="settingsSVG" class="mediaSVG" viewBox="0 0 141.36 137.43">
|
||||
<path d="M70.70475616319865,83.36934004916053 a15.320781354859122,15.320781354859122 0 1 1 14.454501310561755,-15.296030496450625 A14.850515045097694,14.850515045097694 0 0 1 70.70475616319865,83.36934004916053 M123.25082856443602,55.425620905968366 h-12.375429204248078 A45.54157947163293,45.54157947163293 0 0 0 107.21227231573047,46.243052436416285 l8.613298726156664,-9.108315894326587 a9.727087354538993,9.727087354538993 0 0 0 0,-13.167456673319956 l-3.465120177189462,-3.6631270444574313 a8.489544434114185,8.489544434114185 0 0 0 -12.375429204248078,0 l-8.613298726156664,9.108315894326587 A40.442902639482725,40.442902639482725 0 0 0 81.99114759747292,25.427580514871032 V12.532383284044531 a9.108315894326587,9.108315894326587 0 0 0 -8.811305593424633,-9.306322761594556 h-4.950171681699231 a9.108315894326587,9.108315894326587 0 0 0 -8.811305593424633,9.306322761594556 v12.895197230826497 a40.17064319698927,40.17064319698927 0 0 0 -9.331073620003052,4.0591407789933704 l-8.613298726156664,-9.108315894326587 a8.489544434114185,8.489544434114185 0 0 0 -12.375429204248078,0 L25.58394128451018,23.967279868769744 a9.727087354538993,9.727087354538993 0 0 0 0,13.167456673319956 L34.19724001066683,46.243052436416285 a45.07131316187151,45.07131316187151 0 0 0 -3.6631270444574313,9.083565035918088 h-12.375429204248078 a9.083565035918088,9.083565035918088 0 0 0 -8.811305593424633,9.306322761594556 v5.197680265784193 a9.108315894326587,9.108315894326587 0 0 0 8.811305593424633,9.306322761594556 h11.979415469712139 a45.69008462208391,45.69008462208391 0 0 0 4.0591407789933704,10.642869115653347 l-8.613298726156664,9.108315894326587 a9.727087354538993,9.727087354538993 0 0 0 0,13.167456673319956 l3.465120177189462,3.6631270444574313 a8.489544434114185,8.489544434114185 0 0 0 12.375429204248078,0 l8.613298726156664,-9.108315894326587 a40.49240435629971,40.49240435629971 0 0 0 9.331073620003052,4.0591407789933704 v12.895197230826497 a9.083565035918088,9.083565035918088 0 0 0 8.811305593424633,9.306322761594556 h4.950171681699231 A9.083565035918088,9.083565035918088 0 0 0 81.99114759747292,123.68848839660077 V110.79329116577425 a40.78941465720167,40.78941465720167 0 0 0 9.331073620003052,-4.0591407789933704 l8.613298726156664,9.108315894326587 a8.489544434114185,8.489544434114185 0 0 0 12.375429204248078,0 l3.465120177189462,-3.6631270444574313 a9.727087354538993,9.727087354538993 0 0 0 0,-13.167456673319956 l-8.613298726156664,-9.108315894326587 a45.665333763675406,45.665333763675406 0 0 0 4.034389920584874,-10.642869115653347 h12.004166328120636 a9.108315894326587,9.108315894326587 0 0 0 8.811305593424633,-9.306322761594556 v-5.197680265784193 a9.083565035918088,9.083565035918088 0 0 0 -8.811305593424633,-9.306322761594556 " id="svg_3" class=""/>
|
||||
</svg>
|
||||
<div id="settingsTooltip"><%- lang('landing.settingsTooltip') %></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mediaDivider"></div>
|
||||
<div id="externalMedia">
|
||||
<div class="mediaContainer">
|
||||
<a href="<%- lang('landing.mediaGitHubURL') %>" class="mediaURL" id="linkURL">
|
||||
<svg id="linkSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
|
||||
<path d="M66,73.88a3.85,3.85,0,0,0,1.73-.42,8.22,8.22,0,0,1-.94-3.76,8.36,8.36,0,0,1,8.35-8.35H95A8.35,8.35,0,1,1,95,78H85.8a21.51,21.51,0,0,1-6.65,8.72H95a17.07,17.07,0,0,0,0-34.15H75.13A17,17,0,0,0,58.59,73.88Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="<%- lang('landing.mediaTwitterURL') %>" class="mediaURL" id="twitterURL">
|
||||
<svg id="twitterSVG" class="mediaSVG" viewBox="0 0 5000 4060" preserveAspectRatio="xMidYMid meet">
|
||||
<g>
|
||||
<path d="M1210 4048 c-350 -30 -780 -175 -1124 -378 -56 -33 -86 -57 -86 -68 0 -16 7 -17 83 -9 114 12 349 1 493 -22 295 -49 620 -180 843 -341 l54 -38 -49 -7 c-367 -49 -660 -256 -821 -582 -30 -61 -53 -120 -51 -130 3 -16 12 -17 73 -13 97 7 199 5 270 -4 l60 -9 -65 -22 c-341 -117 -609 -419 -681 -769 -18 -88 -26 -226 -13 -239 4 -3 32 7 63 22 68 35 198 77 266 86 28 4 58 9 68 12 10 2 -22 -34 -72 -82 -240 -232 -353 -532 -321 -852 15 -149 79 -347 133 -418 16 -20 17 -19 49 20 377 455 913 795 1491 945 160 41 346 74 485 86 l82 7 -7 -59 c-5 -33 -7 -117 -6 -189 2 -163 31 -286 103 -430 141 -285 422 -504 708 -550 112 -19 333 -19 442 0 180 30 335 108 477 239 l58 54 95 -24 c143 -36 286 -89 427 -160 70 -35 131 -60 135 -56 19 19 -74 209 -151 312 -50 66 -161 178 -216 217 l-30 22 73 -14 c111 -21 257 -63 353 -101 99 -39 99 -39 99 -19 0 57 -237 326 -412 468 l-88 71 6 51 c4 28 1 130 -5 226 -30 440 -131 806 -333 1202 -380 745 -1036 1277 -1823 1477 -243 62 -430 81 -786 78 -134 0 -291 -5 -349 -10z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="<%- lang('landing.mediaInstagramURL') %>" class="mediaURL" id="instagramURL">
|
||||
<svg id="instagramSVG" class="mediaSVG" viewBox="0 0 5040 5040">
|
||||
<defs>
|
||||
<radialGradient id="instaFill" cx="30%" cy="107%" r="150%">
|
||||
<stop offset="0%" stop-color="#fdf497"/>
|
||||
<stop offset="5%" stop-color="#fdf497"/>
|
||||
<stop offset="45%" stop-color="#fd5949"/>
|
||||
<stop offset="60%" stop-color="#d6249f"/>
|
||||
<stop offset="90%" stop-color="#285AEB"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M1390 5024 c-163 -9 -239 -19 -315 -38 -281 -70 -477 -177 -660 -361 -184 -184 -292 -380 -361 -660 -43 -171 -53 -456 -53 -1445 0 -989 10 -1274 53 -1445 69 -280 177 -476 361 -660 184 -184 380 -292 660 -361 171 -43 456 -53 1445 -53 989 0 1274 10 1445 53 280 69 476 177 660 361 184 184 292 380 361 660 43 171 53 456 53 1445 0 989 -10 1274 -53 1445 -69 280 -177 476 -361 660 -184 184 -380 292 -660 361 -174 44 -454 53 -1470 52 -599 0 -960 -5 -1105 -14z m2230 -473 c58 -6 141 -18 185 -27 397 -78 638 -318 719 -714 37 -183 41 -309 41 -1290 0 -981 -4 -1107 -41 -1290 -81 -395 -319 -633 -714 -714 -183 -37 -309 -41 -1290 -41 -981 0 -1107 4 -1290 41 -397 81 -636 322 -714 719 -33 166 -38 296 -43 1100 -5 796 3 1203 27 1380 67 489 338 758 830 825 47 7 162 15 255 20 250 12 1907 4 2035 -9z"/>
|
||||
<path d="M2355 3819 c-307 -42 -561 -172 -780 -400 -244 -253 -359 -543 -359 -899 0 -361 116 -648 367 -907 262 -269 563 -397 937 -397 374 0 675 128 937 397 251 259 367 546 367 907 0 361 -116 648 -367 907 -197 203 -422 326 -690 378 -101 20 -317 27 -412 14z m400 -509 c275 -88 470 -284 557 -560 20 -65 23 -95 23 -230 0 -135 -3 -165 -23 -230 -88 -278 -284 -474 -562 -562 -65 -20 -95 -23 -230 -23 -135 0 -165 3 -230 23 -278 88 -474 284 -562 562 -20 65 -23 95 -23 230 0 135 3 165 23 230 73 230 219 403 427 507 134 67 212 83 390 79 111 -3 155 -8 210 -26z"/>
|
||||
<path d="M3750 1473 c-29 -11 -66 -38 -106 -77 -70 -71 -94 -126 -94 -221 0 -95 24 -150 94 -221 72 -71 126 -94 225 -94 168 0 311 143 311 311 0 99 -23 154 -94 225 -43 42 -76 66 -110 77 -61 21 -166 21 -226 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="<%- lang('landing.mediaYouTubeURL') %>" class="mediaURL" id="youtubeURL">
|
||||
<svg id="youtubeSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M84.8,69.52,65.88,79.76V59.27Zm23.65.59c0-5.14-.79-17.63-3.94-20.57S99,45.86,73.37,45.86s-28,.73-31.14,3.68S38.29,65,38.29,70.11s.79,17.63,3.94,20.57,5.52,3.68,31.14,3.68,28-.74,31.14-3.68,3.94-15.42,3.94-20.57"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mediaContainer">
|
||||
<a href="<%- lang('landing.mediaDiscordURL') %>" class="mediaURL" id="discordURL">
|
||||
<svg id="discordSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
||||
<g>
|
||||
<path d="M81.23,78.48a6.14,6.14,0,1,1,6.14-6.14,6.14,6.14,0,0,1-6.14,6.14M60,78.48a6.14,6.14,0,1,1,6.14-6.14A6.14,6.14,0,0,1,60,78.48M104.41,73c-.92-7.7-8.24-22.9-8.24-22.9A43,43,0,0,0,88,45.59a17.88,17.88,0,0,0-8.38-1.27l-.13,1.06a23.52,23.52,0,0,1,5.8,1.95,87.59,87.59,0,0,1,8.17,4.87s-10.32-5.63-22.27-5.63a51.32,51.32,0,0,0-23.2,5.63,87.84,87.84,0,0,1,8.17-4.87,23.57,23.57,0,0,1,5.8-1.95l-.13-1.06a17.88,17.88,0,0,0-8.38,1.27,42.84,42.84,0,0,0-8.21,4.56S37.87,65.35,37,73s-.37,11.54-.37,11.54,4.22,5.68,9.9,7.14,7.7,1.47,7.7,1.47l3.75-4.68a21.22,21.22,0,0,1-4.65-2A24.47,24.47,0,0,1,47.93,82S61.16,88.4,70.68,88.4c10,0,22.75-6.44,22.75-6.44a24.56,24.56,0,0,1-5.35,4.56,21.22,21.22,0,0,1-4.65,2l3.75,4.68s2,0,7.7-1.47,9.89-7.14,9.89-7.14.55-3.85-.37-11.54"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lower">
|
||||
<div id="left">
|
||||
<div class="bot_wrapper">
|
||||
<div id="content">
|
||||
<div id="server_status_wrapper">
|
||||
<span class="bot_label" id="landingPlayerLabel"><%- lang('landing.serverStatus') %></span>
|
||||
<span id="player_count"><%- lang('landing.serverStatusPlaceholder') %></span>
|
||||
</div>
|
||||
<div class="bot_divider"></div>
|
||||
<div id="mojangStatusWrapper">
|
||||
<span class="bot_label"><%- lang('landing.mojangStatus') %></span>
|
||||
<span id="mojang_status_icon">•</span>
|
||||
<div id="mojangStatusTooltip">
|
||||
<div id="mojangStatusTooltipTitle"><%- lang('landing.mojangStatusTooltipTitle') %></div>
|
||||
<div id="mojangStatusEssentialContainer">
|
||||
<!-- Essential Mojang services are populated here. -->
|
||||
</div>
|
||||
<div id="mojangStatusNEContainer">
|
||||
<div class="mojangStatusNEBar"></div>
|
||||
<div id="mojangStatusNETitle"><%- lang('landing.mojangStatusNETitle') %></div>
|
||||
<div class="mojangStatusNEBar"></div>
|
||||
</div>
|
||||
<div id="mojangStatusNonEssentialContainer">
|
||||
<!-- Non Essential Mojang services are populated here. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="center">
|
||||
<div class="bot_wrapper">
|
||||
<div id="content">
|
||||
<button id="newsButton">
|
||||
<!--<img src="assets/images/icons/arrow.svg" id="newsButtonSVG"/>-->
|
||||
<div id="newsButtonAlert" style="display: none;"></div>
|
||||
<svg id="newsButtonSVG" viewBox="0 0 24.87 13.97">
|
||||
<defs>
|
||||
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;}</style>
|
||||
</defs>
|
||||
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
||||
</svg>
|
||||
<span id="newsButtonText"><%- lang('landing.newsButton') %></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="right">
|
||||
<div class="bot_wrapper">
|
||||
<div id="launch_content">
|
||||
<button id="launch_button"><%- lang('landing.launchButton') %></button>
|
||||
<div class="bot_divider"></div>
|
||||
<button id="server_selection_button" class="bot_label"><%- lang('landing.launchButtonPlaceholder') %></button>
|
||||
</div>
|
||||
<div id="launch_details">
|
||||
<div id="launch_details_left">
|
||||
<span id="launch_progress_label">0%</span>
|
||||
<div class="bot_divider"></div>
|
||||
</div>
|
||||
<div id="launch_details_right">
|
||||
<progress id="launch_progress" value="22" max="100"></progress>
|
||||
<span id="launch_details_text" class="bot_label"><%- lang('landing.launchDetails') %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newsContainer">
|
||||
<div id="newsContent" article="-1" style="display: none;">
|
||||
<div id="newsStatusContainer">
|
||||
<div id="newsStatusContent">
|
||||
<div id="newsTitleContainer">
|
||||
<a id="newsArticleTitle" href="#">Lorem Ipsum</a>
|
||||
</div>
|
||||
<div id="newsMetaContainer">
|
||||
<div id="newsArticleDateWrapper">
|
||||
<span id="newsArticleDate">Mar 15, 44 BC, 9:14 AM</span>
|
||||
</div>
|
||||
<div id="newsArticleAuthorWrapper">
|
||||
<span id="newsArticleAuthor">by Cicero</span>
|
||||
</div>
|
||||
<a href="#" id="newsArticleComments">0 Comments</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newsNavigationContainer">
|
||||
<button id="newsNavigateLeft">
|
||||
<svg id="newsNavigationLeftSVG" viewBox="0 0 24.87 13.97">
|
||||
<defs>
|
||||
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
|
||||
</defs>
|
||||
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="newsNavigationStatus"><%- lang('landing.newsNavigationStatus', { currentPage: 1, totalPages: 1 }) %></span>
|
||||
<button id="newsNavigateRight">
|
||||
<svg id="newsNavigationRightSVG" viewBox="0 0 24.87 13.97">
|
||||
<defs>
|
||||
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
|
||||
</defs>
|
||||
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newsArticleContainer">
|
||||
<div id="newsArticleContent">
|
||||
<div id="newsArticleContentScrollable">
|
||||
<!-- Article Content -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newsErrorContainer">
|
||||
<div id="newsErrorLoading">
|
||||
<span id="nELoadSpan" class="newsErrorContent"><%- lang('landing.newsErrorLoadSpan') %></span>
|
||||
</div>
|
||||
<div id="newsErrorFailed" style="display: none;">
|
||||
<span id="nEFailedSpan" class="newsErrorContent"><%- lang('landing.newsErrorFailedSpan') %></span>
|
||||
<button id="newsErrorRetry"><%- lang('landing.newsErrorRetryButton') %></button>
|
||||
</div>
|
||||
<div id="newsErrorNone" style="display: none;">
|
||||
<span id="nENoneSpan" class="newsErrorContent"><%- lang('landing.newsErrorNoneSpan') %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/landing.js"></script>
|
||||
</div>
|
246
app/login.ejs
@ -1,16 +1,22 @@
|
||||
<div id="loginContainer">
|
||||
<div id="loginContainer" style="display: none;">
|
||||
<div id="loginCancelContainer" style="display: none;">
|
||||
<button id="loginCancelButton">
|
||||
<div id="loginCancelIcon">X</div>
|
||||
<span id="loginCancelText"><%- lang('login.loginCancelText') %></span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="loginContent">
|
||||
<div id='loginForm'>
|
||||
<img id="loginImageSeal" src="assets/images/WesterosSealCircle.png"/>
|
||||
<span class="loginSpan" id="loginSubheader">MEMBER LOGIN</span>
|
||||
<form id="loginForm">
|
||||
<img id="loginImageSeal" src="assets/images/SealCircle.png"/>
|
||||
<span id="loginSubheader"><%- lang('login.loginSubheader') %></span>
|
||||
<div class="loginFieldContainer">
|
||||
<svg id="profileSVG" class="loginSVG" viewBox="40 37 65.36 61.43">
|
||||
<g>
|
||||
<path d="M86.77,58.12A13.79,13.79,0,1,0,73,71.91,13.79,13.79,0,0,0,86.77,58.12M97,103.67a3.41,3.41,0,0,0,3.39-3.84,27.57,27.57,0,0,0-54.61,0,3.41,3.41,0,0,0,3.39,3.84Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="loginErrorSpan" id="loginEmailError">* Invalid Value</span>
|
||||
<input id="loginUsername" class="loginField" type="text" placeholder="EMAIL"/>
|
||||
<span class="loginErrorSpan" id="loginEmailError"><%- lang('login.loginEmailError') %></span>
|
||||
<input id="loginUsername" class="loginField" type="text" placeholder="<%- lang('login.loginEmailPlaceholder') %>"/>
|
||||
</div>
|
||||
<div class="loginFieldContainer">
|
||||
<svg id="lockSVG" class="loginSVG" viewBox="40 32 60.36 70.43">
|
||||
@ -18,22 +24,22 @@
|
||||
<path d="M86.16,54a16.38,16.38,0,1,0-32,0H44V102.7H96V54Zm-25.9-3.39a9.89,9.89,0,1,1,19.77,0A9.78,9.78,0,0,1,79.39,54H60.89A9.78,9.78,0,0,1,60.26,50.59ZM70,96.2a6.5,6.5,0,0,1-6.5-6.5,6.39,6.39,0,0,1,3.1-5.4V67h6.5V84.11a6.42,6.42,0,0,1,3.39,5.6A6.5,6.5,0,0,1,70,96.2Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="loginErrorSpan" id="loginPasswordError">* Required</span>
|
||||
<input id="loginPassword" class="loginField" type="password" placeholder="PASSWORD"/>
|
||||
<span class="loginErrorSpan" id="loginPasswordError"><%- lang('login.loginPasswordError') %></span>
|
||||
<input id="loginPassword" class="loginField" type="password" placeholder="<%- lang('login.loginPasswordPlaceholder') %>"/>
|
||||
</div>
|
||||
<div id="loginOptions">
|
||||
<span class="loginSpanDim">
|
||||
<a href="https://help.mojang.com/customer/en/portal/articles/329524-change-or-forgot-password">forgot password?</a>
|
||||
<a href="<%- lang('login.loginForgotPasswordLink') %>"><%- lang('login.loginForgotPasswordText') %></a>
|
||||
</span>
|
||||
<label id="checkmarkContainer">
|
||||
<input id="loginRememberOption" type="checkbox" checked>
|
||||
<span id="loginRememberText" class="loginSpanDim">remember me?</span>
|
||||
<span id="loginRememberText" class="loginSpanDim"><%- lang('login.loginRememberMeText') %></span>
|
||||
<span class="loginCheckmark"></span>
|
||||
</label>
|
||||
</div>
|
||||
<button id="loginButton" disabled>
|
||||
<div id="loginButtonContent">
|
||||
LOGIN
|
||||
<%- lang('login.loginButtonText') %>
|
||||
<svg id="loginSVG" viewBox="0 0 24.87 13.97">
|
||||
<defs>
|
||||
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
|
||||
@ -48,220 +54,12 @@
|
||||
</button>
|
||||
<div id="loginDisclaimer">
|
||||
<span class="loginSpanDim" id="loginRegisterSpan">
|
||||
<a href="https://minecraft.net/en-us/store/minecraft/">Need an Account?</a>
|
||||
<a href="<%- lang('login.loginNeedAccountLink') %>"><%- lang('login.loginNeedAccountText') %></a>
|
||||
</span>
|
||||
<p class="loginDisclaimerText">Your password is sent directly to mojang and never stored.</p>
|
||||
<p class="loginDisclaimerText">WesterosCraft is not affiliated with Mojang AB.</p>
|
||||
<p class="loginDisclaimerText"><%- lang('login.loginPasswordDisclaimer1') %></p>
|
||||
<p class="loginDisclaimerText"><%- lang('login.loginPasswordDisclaimer2', { appName: lang('app.title') }) %></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="loginErrorContainer">
|
||||
<div id="loginErrorContent">
|
||||
<span id="loginErrorTitle">LOGIN FAILED:<br>INVALID CREDENTIALS</span>
|
||||
<span id="loginErrorDesc">Either the email or password you supplied is invalid. Please ensure everything is correct and try again.</span>
|
||||
<button id="loginErrorAcknowledge">Try Again</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||
|
||||
// Validation Regexes.
|
||||
const validUsername = /^[a-zA-Z0-9_]{1,16}$/
|
||||
const basicEmail = /^\S+@\S+\.\S+$/
|
||||
|
||||
// DOM cache.
|
||||
const loginContainer = document.getElementById('loginContainer')
|
||||
const loginErrorTitle = document.getElementById('loginErrorTitle')
|
||||
const loginErrorDesc = document.getElementById('loginErrorDesc')
|
||||
const loginErrorAcknowledge = document.getElementById('loginErrorAcknowledge')
|
||||
|
||||
const loginEmailError = document.getElementById('loginEmailError')
|
||||
const loginUsername = document.getElementById('loginUsername')
|
||||
const loginPasswordError = document.getElementById('loginPasswordError')
|
||||
const loginPassword = document.getElementById('loginPassword')
|
||||
const checkmarkContainer = document.getElementById('checkmarkContainer')
|
||||
const loginRememberOption = document.getElementById('loginRememberOption')
|
||||
const loginButton = document.getElementById('loginButton')
|
||||
|
||||
// Control variables.
|
||||
let lu = false, lp = false
|
||||
|
||||
// Show error element.
|
||||
function showError(element, value){
|
||||
element.innerHTML = value
|
||||
element.style.opacity = 1
|
||||
}
|
||||
|
||||
// Shake error element.
|
||||
function shakeError(element){
|
||||
if(element.style.opacity == 1){
|
||||
element.classList.remove('shake')
|
||||
void element.offsetWidth
|
||||
element.classList.add('shake')
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email field is neither empty nor invalid.
|
||||
function validateEmail(value){
|
||||
if(value){
|
||||
if(!basicEmail.test(value) && !validUsername.test(value)){
|
||||
showError(loginEmailError, '* Invalid Value')
|
||||
loginDisabled(true)
|
||||
lu = false
|
||||
} else {
|
||||
loginEmailError.style.opacity = 0
|
||||
lu = true
|
||||
if(lp){
|
||||
loginDisabled(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lu = false
|
||||
showError(loginEmailError, '* Required')
|
||||
loginDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate password field is not empty.
|
||||
function validatePassword(value){
|
||||
if(value){
|
||||
loginPasswordError.style.opacity = 0
|
||||
lp = true
|
||||
if(lu){
|
||||
loginDisabled(false)
|
||||
}
|
||||
} else {
|
||||
lp = false
|
||||
showError(loginPasswordError, '* Required')
|
||||
loginDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Emphasize errors with shake when focus is lost.
|
||||
loginUsername.addEventListener('focusout', (e) => {
|
||||
validateEmail(e.target.value)
|
||||
shakeError(loginEmailError)
|
||||
})
|
||||
loginPassword.addEventListener('focusout', (e) => {
|
||||
validatePassword(e.target.value)
|
||||
shakeError(loginPasswordError)
|
||||
})
|
||||
|
||||
// Validate input for each field.
|
||||
loginUsername.addEventListener('input', (e) => {
|
||||
validateEmail(e.target.value)
|
||||
})
|
||||
loginPassword.addEventListener('input', (e) => {
|
||||
validatePassword(e.target.value)
|
||||
})
|
||||
|
||||
// Enable or disable login button.
|
||||
function loginDisabled(v){
|
||||
if(loginButton.disabled !== v){
|
||||
loginButton.disabled = v
|
||||
}
|
||||
}
|
||||
|
||||
// Enable or disable loading elements.
|
||||
function loginLoading(v){
|
||||
if(v){
|
||||
loginButton.setAttribute('loading', v)
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace('LOGIN', 'LOGGING IN')
|
||||
} else {
|
||||
loginButton.removeAttribute('loading')
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'LOGIN')
|
||||
}
|
||||
}
|
||||
|
||||
// Disable or enable login form.
|
||||
function formDisabled(v){
|
||||
loginDisabled(v)
|
||||
loginUsername.disabled = v
|
||||
loginPassword.disabled = v
|
||||
if(v){
|
||||
checkmarkContainer.setAttribute('disabled', v)
|
||||
} else {
|
||||
checkmarkContainer.removeAttribute('disabled')
|
||||
}
|
||||
loginRememberOption.disabled = v
|
||||
}
|
||||
|
||||
function resolveError(err){
|
||||
if(err.cause != null && err.cause === 'UserMigratedException') {
|
||||
return {
|
||||
title: 'Error During Login:<br>Invalid Credentials',
|
||||
desc: 'You\'ve attempted to login with a migrated account. Try again using the account email as the username.'
|
||||
}
|
||||
} else {
|
||||
if(err.error != null){
|
||||
if(err.error === 'ForbiddenOperationException'){
|
||||
if(err.errorMessage != null){
|
||||
if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){
|
||||
return {
|
||||
title: 'Error During Login:<br>Invalid Credentials',
|
||||
desc: 'The email or password you\'ve entered is incorrect. Please try again.'
|
||||
}
|
||||
} else if(err.errorMessage === 'Invalid credentials.'){
|
||||
return {
|
||||
title: 'Error During Login:<br>Too Many Attempts',
|
||||
desc: 'There have been too many login attempts with this account recently. Please try again later.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: err.error,
|
||||
desc: err.errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
loginButton.addEventListener('click', () => {
|
||||
// Disable form.
|
||||
formDisabled(true)
|
||||
|
||||
// Show loading stuff.
|
||||
loginLoading(true)
|
||||
|
||||
AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => {
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'SUCCESS')
|
||||
$('.circle-loader').toggleClass('load-complete')
|
||||
$('.checkmark').toggle()
|
||||
console.log(value)
|
||||
}).catch((err) => {
|
||||
loginLoading(false)
|
||||
$('#loginErrorContainer').css('display', 'flex').hide().fadeIn(250)
|
||||
loginContainer.setAttribute('error', true)
|
||||
const errF = resolveError(err)
|
||||
loginErrorTitle.innerHTML = errF.title
|
||||
loginErrorDesc.innerHTML = errF.desc
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
// Temp for debugging, use procedure with real code.
|
||||
setTimeout(() => {
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'SUCCESS')
|
||||
$('.circle-loader').toggleClass('load-complete')
|
||||
$('.checkmark').toggle()
|
||||
}, 2500)
|
||||
|
||||
})
|
||||
|
||||
loginErrorAcknowledge.addEventListener('click', () => {
|
||||
formDisabled(false)
|
||||
loginContainer.removeAttribute('error', false)
|
||||
$('#loginErrorContainer').fadeOut(250)
|
||||
})
|
||||
</script>
|
||||
<!-- Will reuse this down the line, then it will be removed from this file. -->
|
||||
<!--<div id="loginLoading">
|
||||
<div id="loginLoadingContent">
|
||||
<div id="loadSpinnerContainer">
|
||||
<img id="loadCenterImage" src="assets/images/westeroscraftlogo1.png">
|
||||
<img id="loadSpinnerImage" class="rotating" src="assets/images/westeroscraftlogo2.png">
|
||||
</div>
|
||||
<span id="loadDescText">LOGGING IN</span>
|
||||
</div>
|
||||
</div>-->
|
||||
<script src="./assets/js/scripts/login.js"></script>
|
||||
</div>
|
34
app/loginOptions.ejs
Normal file
@ -0,0 +1,34 @@
|
||||
<div id="loginOptionsContainer" style="display: none;">
|
||||
<div id="loginOptionsContent">
|
||||
<div class="loginOptionsMainContent">
|
||||
<h2><%- lang('loginOptions.loginOptionsTitle') %></h2>
|
||||
<div class="loginOptionActions">
|
||||
<div class="loginOptionButtonContainer">
|
||||
<button id="loginOptionMicrosoft" class="loginOptionButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
|
||||
<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>
|
||||
<span><%- lang('loginOptions.loginWithMicrosoft') %></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="loginOptionButtonContainer">
|
||||
<button id="loginOptionMojang" class="loginOptionButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" 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>
|
||||
<span><%- lang('loginOptions.loginWithMojang') %></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loginOptionCancelContainer" style="display: none;">
|
||||
<button id="loginOptionCancelButton"><%- lang('loginOptions.cancelButton') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/loginOptions.js"></script>
|
||||
</div>
|
41
app/overlay.ejs
Normal file
@ -0,0 +1,41 @@
|
||||
<div id="overlayContainer" style="display: none;">
|
||||
<div id="serverSelectContent" style="display: none;">
|
||||
<span id="serverSelectHeader"><%- lang('overlay.serverSelectHeader') %></span>
|
||||
<div id="serverSelectList">
|
||||
<div id="serverSelectListScrollable">
|
||||
<!-- Server listings populated here. -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="serverSelectActions">
|
||||
<button id="serverSelectConfirm" class="overlayKeybindEnter" type="submit"><%- lang('overlay.serverSelectConfirm') %></button>
|
||||
<div id="serverSelectCancelWrapper">
|
||||
<button id="serverSelectCancel" class="overlayKeybindEsc"><%- lang('overlay.serverSelectCancel') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountSelectContent" style="display: none;">
|
||||
<span id="accountSelectHeader"><%- lang('overlay.accountSelectHeader') %></span>
|
||||
<div id="accountSelectList">
|
||||
<div id="accountSelectListScrollable">
|
||||
<!-- Accounts populated here. -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountSelectActions">
|
||||
<button id="accountSelectConfirm" class="overlayKeybindEnter" type="submit"><%- lang('overlay.accountSelectConfirm') %></button>
|
||||
<div id="accountSelectCancelWrapper">
|
||||
<button id="accountSelectCancel" class="overlayKeybindEsc"><%- lang('overlay.accountSelectCancel') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="overlayContent">
|
||||
<span id="overlayTitle">Lorem Ipsum:<br>Finis Illud</span>
|
||||
<span id="overlayDesc">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud..</span>
|
||||
<div id="overlayActionContainer">
|
||||
<button id="overlayAcknowledge" class="overlayKeybindEnter">Conare Iterum</button>
|
||||
<div id="overlayDismissWrapper">
|
||||
<button id="overlayDismiss" style="display: none;" class="overlayKeybindEsc">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/overlay.js"></script>
|
||||
</div>
|
393
app/settings.ejs
Normal file
@ -0,0 +1,393 @@
|
||||
<div id="settingsContainer" style="display: none;">
|
||||
<div id="settingsContainerLeft">
|
||||
<div id="settingsNavContainer">
|
||||
<div id="settingsNavHeader">
|
||||
<span id="settingsNavHeaderText"><%- lang('settings.navHeaderText') %></span>
|
||||
</div>
|
||||
<div id="settingsNavItemsContainer">
|
||||
<div id="settingsNavItemsContent">
|
||||
<button class="settingsNavItem" rSc="settingsTabAccount" id="settingsNavAccount" selected><%- lang('settings.navAccount') %></button>
|
||||
<button class="settingsNavItem" rSc="settingsTabMinecraft"><%- lang('settings.navMinecraft') %></button>
|
||||
<button class="settingsNavItem" rSc="settingsTabMods"><%- lang('settings.navMods') %></button>
|
||||
<button class="settingsNavItem" rSc="settingsTabJava"><%- lang('settings.navJava') %></button>
|
||||
<button class="settingsNavItem" rSc="settingsTabLauncher"><%- lang('settings.navLauncher') %></button>
|
||||
<div class="settingsNavSpacer"></div>
|
||||
<button class="settingsNavItem" rSc="settingsTabAbout"><%- lang('settings.navAbout') %></button>
|
||||
<button class="settingsNavItem" rSc="settingsTabUpdate" id="settingsNavUpdate"><%- lang('settings.navUpdates') %></button>
|
||||
<div id="settingsNavContentBottom">
|
||||
<div class="settingsNavDivider"></div>
|
||||
<button id="settingsNavDone"><%- lang('settings.navDone') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsContainerRight">
|
||||
<div id="settingsTabAccount" class="settingsTab">
|
||||
<div class="settingsTabHeader">
|
||||
<span class="settingsTabHeaderText"><%- lang('settings.tabAccountHeaderText') %></span>
|
||||
<span class="settingsTabHeaderDesc"><%- lang('settings.tabAccountHeaderDesc') %></span>
|
||||
</div>
|
||||
<div class="settingsAuthAccountTypeContainer">
|
||||
<div class="settingsAuthAccountTypeHeader">
|
||||
<div class="settingsAuthAccountTypeHeaderLeft">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
|
||||
<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>
|
||||
<span><%- lang('settings.microsoftAccount') %></span>
|
||||
</div>
|
||||
<div class="settingsAuthAccountTypeHeaderRight">
|
||||
<button class="settingsAddAuthAccount" id="settingsAddMicrosoftAccount"><%- lang('settings.addMicrosoftAccount') %></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsCurrentAccounts" id="settingsCurrentMicrosoftAccounts">
|
||||
<!-- Microsoft auth accounts populated here. -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsAuthAccountTypeContainer">
|
||||
<div class="settingsAuthAccountTypeHeader">
|
||||
<div class="settingsAuthAccountTypeHeaderLeft">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" 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>
|
||||
<span><%- lang('settings.mojangAccount') %></span>
|
||||
</div>
|
||||
<div class="settingsAuthAccountTypeHeaderRight">
|
||||
<button class="settingsAddAuthAccount" id="settingsAddMojangAccount"><%- lang('settings.addMojangAccount') %></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsCurrentAccounts" id="settingsCurrentMojangAccounts">
|
||||
<!-- Mojang auth accounts populated here. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTabMinecraft" class="settingsTab" style="display: none;">
|
||||
<div class="settingsTabHeader">
|
||||
<span class="settingsTabHeaderText"><%- lang('settings.minecraftTabHeaderText') %></span>
|
||||
<span class="settingsTabHeaderDesc"><%- lang('settings.minecraftTabHeaderDesc') %></span>
|
||||
</div>
|
||||
<div id="settingsGameResolutionContainer">
|
||||
<span class="settingsFieldTitle"><%- lang('settings.gameResolutionTitle') %></span>
|
||||
<div id="settingsGameResolutionContent">
|
||||
<input type="number" id="settingsGameWidth" min="0" cValue="GameWidth">
|
||||
<div id="settingsGameResolutionCross">✖</div>
|
||||
<input type="number" id="settingsGameHeight" min="0" cValue="GameHeight">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsFieldContainer">
|
||||
<div class="settingsFieldLeft">
|
||||
<span class="settingsFieldTitle"><%- lang('settings.launchFullscreenTitle') %></span>
|
||||
</div>
|
||||
<div class="settingsFieldRight">
|
||||
<label class="toggleSwitch">
|
||||
<input type="checkbox" cValue="Fullscreen">
|
||||
<span class="toggleSwitchSlider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsFieldContainer">
|
||||
<div class="settingsFieldLeft">
|
||||
<span class="settingsFieldTitle"><%- lang('settings.autoConnectTitle') %></span>
|
||||
</div>
|
||||
<div class="settingsFieldRight">
|
||||
<label class="toggleSwitch">
|
||||
<input type="checkbox" cValue="AutoConnect">
|
||||
<span class="toggleSwitchSlider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsFieldContainer">
|
||||
<div class="settingsFieldLeft">
|
||||
<span class="settingsFieldTitle"><%- lang('settings.launchDetachedTitle') %></span>
|
||||
<span class="settingsFieldDesc"><%- lang('settings.launchDetachedDesc') %></span>
|
||||
</div>
|
||||
<div class="settingsFieldRight">
|
||||
<label class="toggleSwitch">
|
||||
<input type="checkbox" cValue="LaunchDetached">
|
||||
<span class="toggleSwitchSlider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTabMods" class="settingsTab" style="display: none;">
|
||||
<div class="settingsTabHeader">
|
||||
<span class="settingsTabHeaderText"><%- lang('settings.tabModsHeaderText') %></span>
|
||||
<span class="settingsTabHeaderDesc"><%- lang('settings.tabModsHeaderDesc') %></span>
|
||||
</div>
|
||||
<div class="settingsSelServContainer">
|
||||
<div class="settingsSelServContent">
|
||||
|
||||
</div>
|
||||
<div class="settingsSwitchServerContainer">
|
||||
<div class="settingsSwitchServerContent">
|
||||
<button class="settingsSwitchServerButton"><%- lang('settings.switchServerButton') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsModsContainer">
|
||||
<div id="settingsReqModsContainer">
|
||||
<div class="settingsModsHeader"><%- lang('settings.requiredMods') %></div>
|
||||
<div id="settingsReqModsContent">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsOptModsContainer">
|
||||
<div class="settingsModsHeader"><%- lang('settings.optionalMods') %></div>
|
||||
<div id="settingsOptModsContent">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsDropinModsContainer">
|
||||
<div class="settingsModsHeader"><%- lang('settings.dropinMods') %></div>
|
||||
<button id="settingsDropinFileSystemButton"><%- lang('settings.addMods') %> <span id="settingsDropinRefreshNote"><%- lang('settings.dropinRefreshNote') %></span></button>
|
||||
<div id="settingsDropinModsContent">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsShadersContainer">
|
||||
<div class="settingsModsHeader"><%- lang('settings.shaderpacks') %></div>
|
||||
<div id="settingsShaderpackDesc"><%- lang('settings.shaderpackDesc') %></div>
|
||||
<div id="settingsShaderpackWrapper">
|
||||
<button id="settingsShaderpackButton"> + </button>
|
||||
<div class="settingsSelectContainer">
|
||||
<div class="settingsSelectSelected" id="settingsShadersSelected"><%- lang('settings.selectShaderpack') %></div>
|
||||
<div class="settingsSelectOptions" id="settingsShadersOptions" hidden>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTabJava" class="settingsTab" style="display: none;">
|
||||
<div class="settingsTabHeader">
|
||||
<span class="settingsTabHeaderText"><%- lang('settings.tabJavaHeaderText') %></span>
|
||||
<span class="settingsTabHeaderDesc"><%- lang('settings.tabJavaHeaderDesc') %></span>
|
||||
</div>
|
||||
<div class="settingsSelServContainer">
|
||||
<div class="settingsSelServContent">
|
||||
|
||||
</div>
|
||||
<div class="settingsSwitchServerContainer">
|
||||
<div class="settingsSwitchServerContent">
|
||||
<button class="settingsSwitchServerButton"><%- lang('settings.switchServerButton') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsMemoryContainer">
|
||||
<div id="settingsMemoryTitle"><%- lang('settings.memoryTitle') %></div>
|
||||
<div id="settingsMemoryContent">
|
||||
<div id="settingsMemoryContentLeft">
|
||||
<div class="settingsMemoryContentItem">
|
||||
<span class="settingsMemoryHeader"><%- lang('settings.maxRAM') %></span>
|
||||
<div class="settingsMemoryActionContainer">
|
||||
<div id="settingsMaxRAMRange" class="rangeSlider" cValue="MaxRAM" serverDependent min="3" max="8" value="3" step="0.5">
|
||||
<div class="rangeSliderBar"></div>
|
||||
<div class="rangeSliderTrack"></div>
|
||||
</div>
|
||||
<span id="settingsMaxRAMLabel" class="settingsMemoryLabel">3G</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsMemoryContentItem">
|
||||
<span class="settingsMemoryHeader"><%- lang('settings.minRAM') %></span>
|
||||
<div class="settingsMemoryActionContainer">
|
||||
<div id="settingsMinRAMRange" class="rangeSlider" cValue="MinRAM" serverDependent min="3" max="8" value="3" step="0.5">
|
||||
<div class="rangeSliderBar"></div>
|
||||
<div class="rangeSliderTrack"></div>
|
||||
</div>
|
||||
<span id="settingsMinRAMLabel" class="settingsMemoryLabel">3G</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsMemoryDesc"><%- lang('settings.memoryDesc') %></div>
|
||||
</div>
|
||||
<div id="settingsMemoryContentRight">
|
||||
<div id="settingsMemoryStatus">
|
||||
<div class="settingsMemoryStatusContainer">
|
||||
<span class="settingsMemoryStatusTitle"><%- lang('settings.memoryTotalTitle') %></span>
|
||||
<span id="settingsMemoryTotal" class="settingsMemoryStatusValue">16G</span>
|
||||
</div>
|
||||
<div class="settingsMemoryStatusContainer">
|
||||
<span class="settingsMemoryStatusTitle"><%- lang('settings.memoryAvailableTitle') %></span>
|
||||
<span id="settingsMemoryAvail" class="settingsMemoryStatusValue">7.3G</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsFileSelContainer">
|
||||
<div class="settingsFileSelTitle"><%- lang('settings.javaExecutableTitle') %></div>
|
||||
<div class="settingsFileSelContent">
|
||||
<div id="settingsJavaExecDetails"><!-- Invalid Selection --></div>
|
||||
<div class="settingsFileSelActions">
|
||||
<div class="settingsFileSelIcon">
|
||||
<svg class="settingsFileSelSVG" x="0px" y="0px" viewBox="0 0 305.001 305.001">
|
||||
<g>
|
||||
<path d="M150.99,56.513c-14.093,9.912-30.066,21.147-38.624,39.734c-14.865,32.426,30.418,67.798,32.353,69.288c0.45,0.347,0.988,0.519,1.525,0.519c0.57,0,1.141-0.195,1.605-0.583c0.899-0.752,1.154-2.029,0.614-3.069c-0.164-0.316-16.418-31.888-15.814-54.539c0.214-7.888,11.254-16.837,22.942-26.312c10.705-8.678,22.839-18.514,29.939-30.02c15.586-25.327-1.737-50.231-1.914-50.479c-0.688-0.966-1.958-1.317-3.044-0.84c-1.085,0.478-1.686,1.652-1.438,2.811c0.035,0.164,3.404,16.633-5.97,33.6C169.301,43.634,160.816,49.603,150.99,56.513z"></path>
|
||||
<path d="M210.365,67.682c0.994-0.749,1.286-2.115,0.684-3.205c-0.602-1.09-1.913-1.571-3.077-1.129c-2.394,0.91-58.627,22.585-58.627,48.776c0,18.053,7.712,27.591,13.343,34.556c2.209,2.731,4.116,5.09,4.744,7.104c1.769,5.804-2.422,16.294-4.184,19.846c-0.508,1.022-0.259,2.259,0.605,3.005c0.467,0.403,1.05,0.607,1.634,0.607c0.497,0,0.996-0.148,1.427-0.448c0.967-0.673,23.63-16.696,19.565-36.001c-1.514-7.337-5.12-12.699-8.302-17.43c-4.929-7.329-8.489-12.624-3.088-22.403C181.419,89.556,210.076,67.899,210.365,67.682z"></path>
|
||||
<path d="M63.99,177.659c-0.964,2.885-0.509,5.75,1.315,8.283c6.096,8.462,27.688,13.123,60.802,13.123c0.002,0,0.003,0,0.004,0c4.487,0,9.224-0.088,14.076-0.262c52.943-1.896,72.58-18.389,73.39-19.09c0.883-0.764,1.119-2.037,0.57-3.067c-0.549-1.029-1.733-1.546-2.864-1.235c-18.645,5.091-53.463,6.898-77.613,6.898c-27.023,0-40.785-1.946-44.154-3.383c1.729-2.374,12.392-6.613,25.605-9.212c1.263-0.248,2.131-1.414,2.006-2.695c-0.125-1.281-1.201-2.258-2.488-2.258C106.893,164.762,68.05,165.384,63.99,177.659z"></path>
|
||||
<path d="M241.148,160.673c-10.92,0-21.275,5.472-21.711,5.705c-1.01,0.541-1.522,1.699-1.245,2.811c0.278,1.111,1.277,1.892,2.423,1.893c0.232,0.001,23.293,0.189,25.382,13.365c1.85,11.367-21.82,29.785-31.097,35.923c-1.002,0.663-1.391,1.945-0.926,3.052c0.395,0.943,1.314,1.533,2.304,1.533c0.173,0,0.348-0.018,0.522-0.056c2.202-0.47,53.855-11.852,48.394-41.927C261.862,164.541,250.278,160.673,241.148,160.673z"></path>
|
||||
<path d="M205.725,216.69c0.18-0.964-0.221-1.944-1.023-2.506l-12.385-8.675c-0.604-0.423-1.367-0.556-2.076-0.368c-0.129,0.034-13.081,3.438-31.885,5.526c-7.463,0.837-15.822,1.279-24.175,1.279c-18.799,0-31.091-2.209-32.881-3.829c-0.237-0.455-0.162-0.662-0.12-0.777c0.325-0.905,2.068-1.98,3.192-2.405c1.241-0.459,1.91-1.807,1.524-3.073c-0.385-1.266-1.69-2.012-2.978-1.702c-12.424,2.998-18.499,7.191-18.057,12.461c0.785,9.343,22.428,14.139,40.725,15.408c2.631,0.18,5.477,0.272,8.456,0.272c0.002,0,0.003,0,0.005,0c30.425,0,69.429-9.546,69.819-9.643C204.818,218.423,205.544,217.654,205.725,216.69z"></path>
|
||||
<path d="M112.351,236.745c0.938-0.611,1.354-1.77,1.021-2.838c-0.332-1.068-1.331-1.769-2.453-1.755c-1.665,0.044-16.292,0.704-17.316,10.017c-0.31,2.783,0.487,5.325,2.37,7.556c5.252,6.224,19.428,9.923,43.332,11.31c2.828,0.169,5.7,0.254,8.539,0.254c30.39,0,50.857-9.515,51.714-9.92c0.831-0.393,1.379-1.209,1.428-2.127c0.049-0.917-0.409-1.788-1.193-2.267l-15.652-9.555c-0.543-0.331-1.193-0.441-1.813-0.314c-0.099,0.021-10.037,2.082-25.035,4.119c-2.838,0.385-6.392,0.581-10.562,0.581c-14.982,0-31.646-2.448-34.842-4.05C111.843,237.455,111.902,237.075,112.351,236.745z"></path>
|
||||
<path d="M133.681,290.018c69.61-0.059,106.971-12.438,114.168-20.228c2.548-2.757,2.823-5.366,2.606-7.07c-0.535-4.194-4.354-6.761-4.788-7.04c-1.045-0.672-2.447-0.496-3.262,0.444c-0.813,0.941-0.832,2.314-0.016,3.253c0.439,0.565,0.693,1.51-0.591,2.795c-2.877,2.687-31.897,10.844-80.215,13.294c-6.619,0.345-13.561,0.519-20.633,0.52c-43.262,0-74.923-5.925-79.079-9.379c1.603-2.301,12.801-5.979,24.711-8.058c1.342-0.234,2.249-1.499,2.041-2.845c-0.208-1.346-1.449-2.273-2.805-2.096c-0.336,0.045-1.475,0.115-2.796,0.195c-19.651,1.2-42.36,3.875-43.545,13.999c-0.36,3.086,0.557,5.886,2.726,8.324c5.307,5.963,20.562,13.891,91.475,13.891C133.68,290.018,133.68,290.018,133.681,290.018z"></path>
|
||||
<path d="M261.522,271.985c-0.984-0.455-2.146-0.225-2.881,0.567c-0.103,0.11-10.568,11.054-42.035,17.48c-12.047,2.414-34.66,3.638-67.211,3.638c-32.612,0-63.643-1.283-63.953-1.296c-1.296-0.063-2.405,0.879-2.581,2.155c-0.177,1.276,0.645,2.477,1.897,2.775c0.323,0.077,32.844,7.696,77.31,7.696c21.327,0,42.08-1.733,61.684-5.151c36.553-6.408,39.112-24.533,39.203-25.301C263.082,273.474,262.504,272.44,261.522,271.985z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<input class="settingsFileSelVal" id="settingsJavaExecVal" type="text" value="null" cValue="JavaExecutable" serverDependent disabled>
|
||||
<button class="settingsFileSelButton" id="settingsJavaExecSel" dialogTitle="<%- lang('settings.javaExecSelDialogTitle') %>" dialogDirectory="false"><%- lang('settings.javaExecSelButtonText') %></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsFileSelDesc"><%- lang('settings.javaExecDesc') %> <strong id="settingsJavaReqDesc"><!-- Requires Java 8 x64. --></strong><br><%- lang('settings.javaPathDesc', {'pathSuffix': `bin${process.platform === 'win32' ? '\\javaw.exe' : '/java'}`}) %></div>
|
||||
</div>
|
||||
<div id="settingsJVMOptsContainer">
|
||||
<div id="settingsJVMOptsTitle"><%- lang('settings.jvmOptsTitle') %></div>
|
||||
<div id="settingsJVMOptsContent">
|
||||
<div class="settingsFileSelIcon">
|
||||
<svg class="settingsFileSelSVG" x="0px" y="0px" viewBox="0 0 305.001 305.001">
|
||||
<g>
|
||||
<path d="M150.99,56.513c-14.093,9.912-30.066,21.147-38.624,39.734c-14.865,32.426,30.418,67.798,32.353,69.288c0.45,0.347,0.988,0.519,1.525,0.519c0.57,0,1.141-0.195,1.605-0.583c0.899-0.752,1.154-2.029,0.614-3.069c-0.164-0.316-16.418-31.888-15.814-54.539c0.214-7.888,11.254-16.837,22.942-26.312c10.705-8.678,22.839-18.514,29.939-30.02c15.586-25.327-1.737-50.231-1.914-50.479c-0.688-0.966-1.958-1.317-3.044-0.84c-1.085,0.478-1.686,1.652-1.438,2.811c0.035,0.164,3.404,16.633-5.97,33.6C169.301,43.634,160.816,49.603,150.99,56.513z"></path>
|
||||
<path d="M210.365,67.682c0.994-0.749,1.286-2.115,0.684-3.205c-0.602-1.09-1.913-1.571-3.077-1.129c-2.394,0.91-58.627,22.585-58.627,48.776c0,18.053,7.712,27.591,13.343,34.556c2.209,2.731,4.116,5.09,4.744,7.104c1.769,5.804-2.422,16.294-4.184,19.846c-0.508,1.022-0.259,2.259,0.605,3.005c0.467,0.403,1.05,0.607,1.634,0.607c0.497,0,0.996-0.148,1.427-0.448c0.967-0.673,23.63-16.696,19.565-36.001c-1.514-7.337-5.12-12.699-8.302-17.43c-4.929-7.329-8.489-12.624-3.088-22.403C181.419,89.556,210.076,67.899,210.365,67.682z"></path>
|
||||
<path d="M63.99,177.659c-0.964,2.885-0.509,5.75,1.315,8.283c6.096,8.462,27.688,13.123,60.802,13.123c0.002,0,0.003,0,0.004,0c4.487,0,9.224-0.088,14.076-0.262c52.943-1.896,72.58-18.389,73.39-19.09c0.883-0.764,1.119-2.037,0.57-3.067c-0.549-1.029-1.733-1.546-2.864-1.235c-18.645,5.091-53.463,6.898-77.613,6.898c-27.023,0-40.785-1.946-44.154-3.383c1.729-2.374,12.392-6.613,25.605-9.212c1.263-0.248,2.131-1.414,2.006-2.695c-0.125-1.281-1.201-2.258-2.488-2.258C106.893,164.762,68.05,165.384,63.99,177.659z"></path>
|
||||
<path d="M241.148,160.673c-10.92,0-21.275,5.472-21.711,5.705c-1.01,0.541-1.522,1.699-1.245,2.811c0.278,1.111,1.277,1.892,2.423,1.893c0.232,0.001,23.293,0.189,25.382,13.365c1.85,11.367-21.82,29.785-31.097,35.923c-1.002,0.663-1.391,1.945-0.926,3.052c0.395,0.943,1.314,1.533,2.304,1.533c0.173,0,0.348-0.018,0.522-0.056c2.202-0.47,53.855-11.852,48.394-41.927C261.862,164.541,250.278,160.673,241.148,160.673z"></path>
|
||||
<path d="M205.725,216.69c0.18-0.964-0.221-1.944-1.023-2.506l-12.385-8.675c-0.604-0.423-1.367-0.556-2.076-0.368c-0.129,0.034-13.081,3.438-31.885,5.526c-7.463,0.837-15.822,1.279-24.175,1.279c-18.799,0-31.091-2.209-32.881-3.829c-0.237-0.455-0.162-0.662-0.12-0.777c0.325-0.905,2.068-1.98,3.192-2.405c1.241-0.459,1.91-1.807,1.524-3.073c-0.385-1.266-1.69-2.012-2.978-1.702c-12.424,2.998-18.499,7.191-18.057,12.461c0.785,9.343,22.428,14.139,40.725,15.408c2.631,0.18,5.477,0.272,8.456,0.272c0.002,0,0.003,0,0.005,0c30.425,0,69.429-9.546,69.819-9.643C204.818,218.423,205.544,217.654,205.725,216.69z"></path>
|
||||
<path d="M112.351,236.745c0.938-0.611,1.354-1.77,1.021-2.838c-0.332-1.068-1.331-1.769-2.453-1.755c-1.665,0.044-16.292,0.704-17.316,10.017c-0.31,2.783,0.487,5.325,2.37,7.556c5.252,6.224,19.428,9.923,43.332,11.31c2.828,0.169,5.7,0.254,8.539,0.254c30.39,0,50.857-9.515,51.714-9.92c0.831-0.393,1.379-1.209,1.428-2.127c0.049-0.917-0.409-1.788-1.193-2.267l-15.652-9.555c-0.543-0.331-1.193-0.441-1.813-0.314c-0.099,0.021-10.037,2.082-25.035,4.119c-2.838,0.385-6.392,0.581-10.562,0.581c-14.982,0-31.646-2.448-34.842-4.05C111.843,237.455,111.902,237.075,112.351,236.745z"></path>
|
||||
<path d="M133.681,290.018c69.61-0.059,106.971-12.438,114.168-20.228c2.548-2.757,2.823-5.366,2.606-7.07c-0.535-4.194-4.354-6.761-4.788-7.04c-1.045-0.672-2.447-0.496-3.262,0.444c-0.813,0.941-0.832,2.314-0.016,3.253c0.439,0.565,0.693,1.51-0.591,2.795c-2.877,2.687-31.897,10.844-80.215,13.294c-6.619,0.345-13.561,0.519-20.633,0.52c-43.262,0-74.923-5.925-79.079-9.379c1.603-2.301,12.801-5.979,24.711-8.058c1.342-0.234,2.249-1.499,2.041-2.845c-0.208-1.346-1.449-2.273-2.805-2.096c-0.336,0.045-1.475,0.115-2.796,0.195c-19.651,1.2-42.36,3.875-43.545,13.999c-0.36,3.086,0.557,5.886,2.726,8.324c5.307,5.963,20.562,13.891,91.475,13.891C133.68,290.018,133.68,290.018,133.681,290.018z"></path>
|
||||
<path d="M261.522,271.985c-0.984-0.455-2.146-0.225-2.881,0.567c-0.103,0.11-10.568,11.054-42.035,17.48c-12.047,2.414-34.66,3.638-67.211,3.638c-32.612,0-63.643-1.283-63.953-1.296c-1.296-0.063-2.405,0.879-2.581,2.155c-0.177,1.276,0.645,2.477,1.897,2.775c0.323,0.077,32.844,7.696,77.31,7.696c21.327,0,42.08-1.733,61.684-5.151c36.553-6.408,39.112-24.533,39.203-25.301C263.082,273.474,262.504,272.44,261.522,271.985z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<input id="settingsJVMOptsVal" cValue="JVMOptions" serverDependent type="text">
|
||||
</div>
|
||||
<div id="settingsJVMOptsDesc"><%- lang('settings.jvmOptsDesc') %><br><a href="#" id="settingsJvmOptsLink"><!-- Available Options --></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTabLauncher" class="settingsTab" style="display: none;">
|
||||
<div class="settingsTabHeader">
|
||||
<span class="settingsTabHeaderText"><%- lang('settings.launcherTabHeaderText') %></span>
|
||||
<span class="settingsTabHeaderDesc"><%- lang('settings.launcherTabHeaderDesc') %></span>
|
||||
</div>
|
||||
<div class="settingsFieldContainer">
|
||||
<div class="settingsFieldLeft">
|
||||
<span class="settingsFieldTitle"><%- lang('settings.allowPrereleaseTitle') %></span>
|
||||
<span class="settingsFieldDesc"><%- lang('settings.allowPrereleaseDesc') %></span>
|
||||
</div>
|
||||
<div class="settingsFieldRight">
|
||||
<label class="toggleSwitch">
|
||||
<input type="checkbox" cValue="AllowPrerelease">
|
||||
<span class="toggleSwitchSlider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsFileSelContainer">
|
||||
<div class="settingsFileSelContent">
|
||||
<div class="settingsFieldTitle" id="settingsDataDirTitle"><%- lang('settings.dataDirectoryTitle') %></div>
|
||||
<div class="settingsFileSelActions">
|
||||
<div class="settingsFileSelIcon">
|
||||
<svg class="settingsFileSelSVG">
|
||||
<g>
|
||||
<path fill="gray" d="m10.044745,5c0,0.917174 -0.746246,1.667588 -1.667588,1.667588l-4.168971,0l-2.501382,0c-0.921009,0 -1.667588,0.750415 -1.667588,1.667588l0,6.670353l0,2.501382c0,0.917174 0.746604,1.667588 1.667588,1.667588l16.675882,0c0.921342,0 1.667588,-0.750415 1.667588,-1.667588l0,-2.501382l0,-8.337941c0,-0.917174 -0.746246,-1.667588 -1.667588,-1.667588l-8.337941,0z"/>
|
||||
<path fill="gray" d="m1.627815,1.6c-0.921009,0 -1.667588,0.746579 -1.667588,1.667588l0,4.168971l8.337941,0l0,0.833794l11.673118,0l0,-4.168971c0,-0.921009 -0.746246,-1.667588 -1.667588,-1.667588l-8.572237,0c-0.288493,-0.497692 -0.816284,-0.833794 -1.433292,-0.833794l-6.670353,0z"/>
|
||||
<path fill="lightgray" d="m10.025276,4c0,0.918984 -0.747719,1.670879 -1.670879,1.670879l-4.177198,0l-2.506319,0c-0.922827,0 -1.670879,0.751896 -1.670879,1.670879l0,6.683517l0,2.506319c0,0.918984 0.748078,1.670879 1.670879,1.670879l16.708794,0c0.923161,0 1.670879,-0.751896 1.670879,-1.670879l0,-2.506319l0,-8.354397c0,-0.918984 -0.747719,-1.670879 -1.670879,-1.670879l-8.354397,0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<input class="settingsFileSelVal" type="text" value="null" cValue="DataDirectory" disabled>
|
||||
<button class="settingsFileSelButton" dialogTitle="<%- lang('settings.selectDataDirectory') %>" dialogDirectory="true"><%- lang('settings.chooseFolder') %></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsFileSelDesc"><%- lang('settings.dataDirectoryDesc') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTabAbout" class="settingsTab" style="display: none;">
|
||||
<div class="settingsTabHeader">
|
||||
<span class="settingsTabHeaderText"><%- lang('settings.aboutTabHeaderText') %></span>
|
||||
<span class="settingsTabHeaderDesc"><%- lang('settings.aboutTabHeaderDesc') %></span>
|
||||
</div>
|
||||
<div id="settingsAboutCurrentContainer">
|
||||
<div id="settingsAboutCurrentContent">
|
||||
<div id="settingsAboutCurrentHeadline">
|
||||
<img id="settingsAboutLogo" src="./assets/images/SealCircle.png">
|
||||
<span id="settingsAboutTitle"><%- lang('settings.aboutTitle', { appName: lang('app.title') }) %></span>
|
||||
</div>
|
||||
<div id="settingsAboutCurrentVersion">
|
||||
<div id="settingsAboutCurrentVersionCheck">✓</div>
|
||||
<div id="settingsAboutCurrentVersionDetails">
|
||||
<span id="settingsAboutCurrentVersionTitle"><%- lang('settings.stableRelease') %></span>
|
||||
<div id="settingsAboutCurrentVersionLine">
|
||||
<span id="settingsAboutCurrentVersionText"><%- lang('settings.versionText') %></span>
|
||||
<span id="settingsAboutCurrentVersionValue">0.0.1-alpha.18</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsAboutButtons">
|
||||
<a href="<%- lang('settings.sourceGithubLink') %>" id="settingsAboutSourceButton" class="settingsAboutButton"><%- lang('settings.sourceGithub') %></a>
|
||||
<!-- The following must be included in third-party usage. -->
|
||||
<!-- <a href="https://github.com/dscalzi/HeliosLauncher" id="settingsAboutSourceButton" class="settingsAboutButton">Original Source</a> -->
|
||||
<a href="<%- lang('settings.supportLink') %>" id="settingsAboutSupportButton" class="settingsAboutButton"><%- lang('settings.support') %></a>
|
||||
<a href="#" id="settingsAboutDevToolsButton" class="settingsAboutButton"><%- lang('settings.devToolsConsole') %></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsChangelogContainer">
|
||||
<div class="settingsChangelogContent">
|
||||
<div class="settingsChangelogHeadline">
|
||||
<div class="settingsChangelogLabel"><%- lang('settings.releaseNotes') %></div>
|
||||
<div class="settingsChangelogTitle"><%- lang('settings.changelog') %></div>
|
||||
</div>
|
||||
<div class="settingsChangelogText">
|
||||
<%- lang('settings.noReleaseNotes') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsChangelogActions">
|
||||
<a class="settingsChangelogButton settingsAboutButton" href="#"><%- lang('settings.viewReleaseNotes') %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTabUpdate" class="settingsTab" style="display: none;">
|
||||
<div class="settingsTabHeader">
|
||||
<span class="settingsTabHeaderText"><%- lang('settings.launcherUpdatesHeaderText') %></span>
|
||||
<span class="settingsTabHeaderDesc"><%- lang('settings.launcherUpdatesHeaderDesc') %></span>
|
||||
</div>
|
||||
<div id="settingsUpdateStatusContainer">
|
||||
<div id="settingsUpdateStatusContent">
|
||||
<div id="settingsUpdateStatusHeadline">
|
||||
<span id="settingsUpdateTitle"><!-- You Are Running the Latest Version --></span>
|
||||
</div>
|
||||
<div id="settingsUpdateVersion">
|
||||
<div id="settingsUpdateVersionCheck">✓</div>
|
||||
<div id="settingsUpdateVersionDetails">
|
||||
<span id="settingsUpdateVersionTitle"><%- lang('settings.stableRelease') %></span>
|
||||
<div id="settingsUpdateVersionLine">
|
||||
<span id="settingsUpdateVersionText"><%- lang('settings.versionText') %> </span>
|
||||
<span id="settingsUpdateVersionValue">0.0.1-alpha.18</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsUpdateActionContainer">
|
||||
<button id="settingsUpdateActionButton"><%- lang('settings.checkForUpdates') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settingsChangelogContainer">
|
||||
<div class="settingsChangelogContent">
|
||||
<div class="settingsChangelogHeadline">
|
||||
<div class="settingsChangelogLabel"><%- lang('settings.whatsNew') %></div>
|
||||
<div class="settingsChangelogTitle"><%- lang('settings.updateReleaseNotes') %></div>
|
||||
</div>
|
||||
<div class="settingsChangelogText">
|
||||
<%- lang('settings.noReleaseNotes') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/settings.js"></script>
|
||||
</div>
|
8
app/waiting.ejs
Normal file
@ -0,0 +1,8 @@
|
||||
<div id="waitingContainer" style="display: none;">
|
||||
<div id="waitingContent">
|
||||
<div class="waitingSpinner"></div>
|
||||
<div id="waitingTextContainer">
|
||||
<h2><%- lang('waiting.waitingText') %></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
25
app/welcome.ejs
Normal file
@ -0,0 +1,25 @@
|
||||
<div id="welcomeContainer" style="display: none;">
|
||||
<!--<div class="cloudDiv">
|
||||
<div class="cloudTop"></div>
|
||||
<div class="cloudBottom"></div>
|
||||
</div>-->
|
||||
<div id="welcomeContent">
|
||||
<img id="welcomeImageSeal" src="assets/images/SealCircle.png"/>
|
||||
<span id="welcomeHeader"><%- lang('welcome.welcomeHeader') %></span>
|
||||
<span id="welcomeDescription"><%- lang('welcome.welcomeDescription') %></span>
|
||||
<br>
|
||||
<span id="welcomeDescCTA"><%- lang('welcome.welcomeDescCTA') %></span>
|
||||
<button id="welcomeButton">
|
||||
<div id="welcomeButtonContent">
|
||||
<%- lang('welcome.continueButton') %>
|
||||
<svg id="welcomeSVG" viewBox="0 0 24.87 13.97">
|
||||
<defs>
|
||||
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
|
||||
</defs>
|
||||
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<script src="./assets/js/scripts/welcome.js"></script>
|
||||
</div>
|
BIN
build/icon.icns
BIN
build/icon.ico
Before Width: | Height: | Size: 361 KiB |
BIN
build/icon.png
Normal file
After Width: | Height: | Size: 142 KiB |
3
dev-app-update.yml
Normal file
@ -0,0 +1,3 @@
|
||||
owner: dscalzi
|
||||
repo: HeliosLauncher
|
||||
provider: github
|
52
docs/MicrosoftAuth.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Microsoft Authentication
|
||||
|
||||
Authenticating with Microsoft is fully supported by Helios Launcher.
|
||||
|
||||
## Acquiring an Azure Client ID
|
||||
|
||||
1. Navigate to https://portal.azure.com
|
||||
2. In the search bar, search for **Azure Active Directory**.
|
||||
3. In Azure Active Directory, go to **App Registrations** on the left pane (Under *Manage*).
|
||||
4. Click **New Registration**.
|
||||
- Set **Name** to be your launcher's name.
|
||||
- Set **Supported account types** to *Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)*
|
||||
- Leave **Redirect URI** blank.
|
||||
- Register the application.
|
||||
5. You should be on the application's management page. If not, Navigate back to **App Registrations**. Select the application you just registered.
|
||||
6. Click **Authentication** on the left pane (Under *Manage*).
|
||||
7. Click **Add Platform**.
|
||||
- Select **Mobile and desktop applications**.
|
||||
- Choose `https://login.microsoftonline.com/common/oauth2/nativeclient` as the **Redirect URI**.
|
||||
- Select **Configure** to finish adding the platform.
|
||||
8. Go to **Credentials & secrets**.
|
||||
- Select **Client secrets**.
|
||||
- Click **New client secret**.
|
||||
- Set a description.
|
||||
- Click **Add**.
|
||||
- Don't copy the client secret, adding one is just a requirement from Microsoft.
|
||||
8. Navigate back to **Overview**.
|
||||
9. Copy **Application (client) ID**.
|
||||
|
||||
|
||||
## Adding the Azure Client ID to Helios Launcher.
|
||||
|
||||
In `app/assets/js/ipcconstants.js` you'll find **`AZURE_CLIENT_ID`**. Set it to your application's id.
|
||||
|
||||
Note: Azure Client ID is NOT a secret value and **can** be stored in git. Reference: https://stackoverflow.com/questions/57306964/are-azure-active-directorys-tenantid-and-clientid-considered-secrets
|
||||
|
||||
Then relaunch your app, and login. You'll be greeted with an error message, because the app isn't whitelisted yet. Microsoft needs some activity on the app before whitelisting it. __Trying to log in before requesting whitelist is mandatory.__
|
||||
|
||||
## Requesting whitelisting from Microsoft
|
||||
|
||||
1. Ensure you have completed every step of this doc page.
|
||||
2. Fill [this form](https://aka.ms/mce-reviewappid) with the required information. Remember this is a new appID for approval. You can find both the Client ID and the Tenant ID on the overview page in the Azure Portal.
|
||||
3. Give Microsoft some time to review your app.
|
||||
4. Once you have received Microsoft's approval, allow up to 24 hours for the changes to apply.
|
||||
|
||||
----
|
||||
|
||||
You can now authenticate with Microsoft through the launcher.
|
||||
|
||||
References:
|
||||
- https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
|
||||
- https://help.minecraft.net/hc/en-us/articles/16254801392141
|
533
docs/distro.md
@ -1,121 +1,416 @@
|
||||
# Documentation of the Launcher Distribution Index
|
||||
# Distribution Index
|
||||
|
||||
You can use [Nebula](https://github.com/dscalzi/Nebula) to automate the generation of a distribution index.
|
||||
|
||||
The most up to date and accurate descriptions of the distribution spec can be viewed in [helios-distribution-types](https://github.com/dscalzi/helios-distribution-types).
|
||||
|
||||
The distribution index is written in JSON. The general format of the index is as posted below.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"version": "1.0.0",
|
||||
"discord": {
|
||||
"clientID": 12334567890,
|
||||
"clientId": "12334567890123456789",
|
||||
"smallImageText": "WesterosCraft",
|
||||
"smallImageKey": "seal-circle"
|
||||
},
|
||||
"rss": "https://westeroscraft.com/articles/index.rss",
|
||||
"servers": [
|
||||
{
|
||||
"id": "Example_Server",
|
||||
"name": "WesterosCraft Example Client",
|
||||
"news_feed": "http://westeroscraft.com/forums/example/index.rss",
|
||||
"icon_url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
|
||||
"revision": "0.0.1",
|
||||
"server_ip": "mc.westeroscraft.com:1337",
|
||||
"mc_version": "1.11.2",
|
||||
"description": "Example WesterosCraft server. Connect for fun!",
|
||||
"icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
|
||||
"version": "0.0.1",
|
||||
"address": "mc.westeroscraft.com:1337",
|
||||
"minecraftVersion": "1.11.2",
|
||||
"discord": {
|
||||
"shortId": "Example",
|
||||
"largeImageText": "WesterosCraft Example Server",
|
||||
"largeImageKey": "server-example"
|
||||
},
|
||||
"default_selected": true,
|
||||
"mainServer": true,
|
||||
"autoconnect": true,
|
||||
"modules": [
|
||||
...
|
||||
"Module Objects Here"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can declare an unlimited number of servers, however you must provide valid values for the fields listed above. In addition to that, the server can declare modules.
|
||||
## Distro Index Object
|
||||
|
||||
The discord settings are to enable the use of Rich Presence on the launcher. For more details, see [discord's documentation](https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields).
|
||||
|
||||
Only one server in the array should have the `default_selected` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. This field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `default_selected` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false.
|
||||
|
||||
## Modules
|
||||
|
||||
A module is a generic representation of a file required to run the minecraft client. It takes the general form:
|
||||
|
||||
```json
|
||||
#### Example
|
||||
```JSON
|
||||
{
|
||||
"id": "group.id:artifact:version",
|
||||
"name": "Artifact {version}",
|
||||
"type": "{a valid type}",
|
||||
"artifact": {
|
||||
"size": "{file size in bytes}",
|
||||
"MD5": "{MD5 hash for the file, string}",
|
||||
"extension": ".jar",
|
||||
"url": "http://files.site.com/maven/group/id/artifact/version/artifact-version.jar"
|
||||
"version": "1.0.0",
|
||||
"discord": {
|
||||
"clientId": "12334567890123456789",
|
||||
"smallImageText": "WesterosCraft",
|
||||
"smallImageKey": "seal-circle"
|
||||
},
|
||||
"sub_modules": [
|
||||
"rss": "https://westeroscraft.com/articles/index.rss",
|
||||
"servers": []
|
||||
}
|
||||
```
|
||||
|
||||
### `DistroIndex.version: string/semver`
|
||||
|
||||
The version of the index format. Will be used in the future to gracefully push updates.
|
||||
|
||||
### `DistroIndex.discord: object`
|
||||
|
||||
Global settings for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
|
||||
|
||||
**Properties**
|
||||
|
||||
* `discord.clientId: string` - Client ID for th Application registered with Discord.
|
||||
* `discord.smallImageText: string` - Tootltip for the `smallImageKey`.
|
||||
* `discord.smallImageKey: string` - Name of the uploaded image for the small profile artwork.
|
||||
|
||||
|
||||
### `DistroIndex.rss: string/url`
|
||||
|
||||
A URL to a RSS feed. Used for loading news.
|
||||
|
||||
---
|
||||
|
||||
## Server Object
|
||||
|
||||
#### Example
|
||||
```JSON
|
||||
{
|
||||
"id": "Example_Server",
|
||||
"name": "WesterosCraft Example Client",
|
||||
"description": "Example WesterosCraft server. Connect for fun!",
|
||||
"icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
|
||||
"version": "0.0.1",
|
||||
"address": "mc.westeroscraft.com:1337",
|
||||
"minecraftVersion": "1.11.2",
|
||||
"discord": {
|
||||
"shortId": "Example",
|
||||
"largeImageText": "WesterosCraft Example Server",
|
||||
"largeImageKey": "server-example"
|
||||
},
|
||||
"mainServer": true,
|
||||
"autoconnect": true,
|
||||
"modules": []
|
||||
}
|
||||
```
|
||||
|
||||
### `Server.id: string`
|
||||
|
||||
The ID of the server. The launcher saves mod configurations and selected servers by ID. If the ID changes, all data related to the old ID **will be wiped**.
|
||||
|
||||
### `Server.name: string`
|
||||
|
||||
The name of the server. This is what users see on the UI.
|
||||
|
||||
### `Server.description: string`
|
||||
|
||||
A brief description of the server. Displayed on the UI to provide users more information.
|
||||
|
||||
### `Server.icon: string/url`
|
||||
|
||||
A URL to the server's icon. Will be displayed on the UI.
|
||||
|
||||
### `Server.version: string/semver`
|
||||
|
||||
The version of the server configuration.
|
||||
|
||||
### `Server.address: string/url`
|
||||
|
||||
The server's IP address.
|
||||
|
||||
### `Server.minecraftVersion: string`
|
||||
|
||||
The version of minecraft that the server is running.
|
||||
|
||||
### `Server.discord: object`
|
||||
|
||||
Server specific settings used for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
|
||||
|
||||
**Properties**
|
||||
|
||||
* `discord.shortId: string` - Short ID for the server. Displayed on the second status line as `Server: shortId`
|
||||
* `discord.largeImageText: string` - Ttooltip for the `largeImageKey`.
|
||||
* `discord.largeImageKey: string` - Name of the uploaded image for the large profile artwork.
|
||||
|
||||
### `Server.mainServer: boolean`
|
||||
|
||||
Only one server in the array should have the `mainServer` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. If this field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `mainServer` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false.
|
||||
|
||||
### `Server.autoconnect: boolean`
|
||||
|
||||
Whether or not the server can be autoconnected to. If false, the server will not be autoconnected to even when the user has the autoconnect setting enabled.
|
||||
|
||||
### `Server.javaOptions: JavaOptions`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
Sever-specific Java options. If not provided, defaults are used by the client.
|
||||
|
||||
### `Server.modules: Module[]`
|
||||
|
||||
An array of module objects.
|
||||
|
||||
---
|
||||
|
||||
## JavaOptions Object
|
||||
|
||||
Server-specific Java options.
|
||||
|
||||
#### Example
|
||||
```JSON
|
||||
{
|
||||
"supported": ">=17",
|
||||
"suggestedMajor": 17,
|
||||
"platformOptions": [
|
||||
{
|
||||
"platform": "darwin",
|
||||
"architecture": "arm64",
|
||||
"distribution": "CORRETTO"
|
||||
}
|
||||
],
|
||||
"ram": {
|
||||
"recommended": 3072,
|
||||
"minimum": 2048
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `JavaOptions.platformOptions: JavaPlatformOptions[]`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
Platform-specific java rules for this server configuration. Validation rules will be delegated to the client for any undefined properties. Java validation can be configured for specific platforms and architectures. The most specific ruleset will be applied.
|
||||
|
||||
Maxtrix Precedence (Highest - Lowest)
|
||||
- Current platform, current architecture (ex. win32 x64).
|
||||
- Current platform, any architecture (ex. win32).
|
||||
- Java Options base properties.
|
||||
- Client logic (default logic in the client).
|
||||
|
||||
Properties:
|
||||
|
||||
- `platformOptions.platform: string` - The platform that this validation matrix applies to.
|
||||
- `platformOptions.architecture: string` - Optional. The architecture that this validation matrix applies to. If omitted, applies to all architectures.
|
||||
- `platformOptions.distribution: string` - Optional. See `JavaOptions.distribution`.
|
||||
- `platformOptions.supported: string` - Optional. See `JavaOptions.supported`.
|
||||
- `platformOptions.suggestedMajor: number` - Optional. See `JavaOptions.suggestedMajor`.
|
||||
|
||||
### `JavaOptions.ram: object`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
This allows you to require a minimum and recommended amount of RAM per server instance. The minimum is the smallest value the user can select in the settings slider. The recommended value will be the default value selected for that server. These values are specified in megabytes and must be an interval of 512. This allows configuration in intervals of half gigabytes. In the above example, the recommended ram value is 3 GB (3072 MB) and the minimum is 2 GB (2048 MB).
|
||||
|
||||
- `ram.recommended: number` - The recommended amount of RAM in megabytes. Must be an interval of 512.
|
||||
- `ram.minimum: number` - The absolute minimum amount of RAM in megabytes. Must be an interval of 512.
|
||||
|
||||
### `JavaOptions.distribution: string`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
Preferred JDK distribution to download if no applicable installation could be found. If omitted, the client will decide (decision may be platform-specific).
|
||||
|
||||
### `JavaOptions.supported: string`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
A semver range of supported JDK versions.
|
||||
|
||||
Java version syntax is platform dependent.
|
||||
|
||||
JDK 8 and prior
|
||||
```
|
||||
1.{major}.{minor}_{patch}-b{build}
|
||||
Ex. 1.8.0_152-b16
|
||||
```
|
||||
|
||||
JDK 9+
|
||||
```
|
||||
{major}.{minor}.{patch}+{build}
|
||||
Ex. 11.0.12+7
|
||||
```
|
||||
|
||||
For processing, all versions will be translated into a semver compliant string. JDK 9+ is already semver. For versions 8 and below, `1.{major}.{minor}_{patch}-b{build}` will be translated to `{major}.{minor}.{patch}+{build}`.
|
||||
|
||||
If specified, you must also specify suggestedMajor.
|
||||
|
||||
If omitted, the client will decide based on the game version.
|
||||
|
||||
### `JavaOptions.suggestedMajor: number`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
The suggested major Java version. The suggested major should comply with the version range specified by supported, if defined. This will be used in messages displayed to the end user, and to automatically fetch a Java version.
|
||||
|
||||
NOTE If supported is specified, suggestedMajor must be set. The launcher's default value may not comply with your custom major supported range.
|
||||
|
||||
Common use case:
|
||||
- supported: '>=17.x'
|
||||
- suggestedMajor: 17
|
||||
|
||||
More involved:
|
||||
- supported: '>=16 <20'
|
||||
- suggestedMajor: 17
|
||||
|
||||
Given a wider support range, it becomes necessary to specify which major version in the range is the suggested.
|
||||
|
||||
---
|
||||
|
||||
## Module Object
|
||||
|
||||
A module is a generic representation of a file required to run the minecraft client.
|
||||
|
||||
#### Example
|
||||
```JSON
|
||||
{
|
||||
"id": "com.example:artifact:1.0.0@jar.pack.xz",
|
||||
"name": "Artifact 1.0.0",
|
||||
"type": "Library",
|
||||
"artifact": {
|
||||
"size": 4231234,
|
||||
"MD5": "7f30eefe5c51e1ae0939dab2051db75f",
|
||||
"url": "http://files.site.com/maven/com/example/artifact/1.0.0/artifact-1.0.0.jar.pack.xz"
|
||||
},
|
||||
"subModules": [
|
||||
{
|
||||
"id": "examplefile",
|
||||
"name": "Example File",
|
||||
"type": "file",
|
||||
"type": "File",
|
||||
"artifact": {
|
||||
"size": "{file size in bytes}",
|
||||
"MD5": "{MD5 hash for the file, string}",
|
||||
"size": 23423,
|
||||
"MD5": "169a5e6cf30c2cc8649755cdc5d7bad7",
|
||||
"path": "examplefile.txt",
|
||||
"url": "http://files.site.com/examplefile.txt"
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
As shown above, modules objects are allowed to declare submodules under the option `sub_modules`. This parameter is completely optional and can be omitted for modules which do not require submodules. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit.
|
||||
The parent module will be stored maven style, it's destination path will be resolved by its id. The sub module has a declared `path`, so that value will be used.
|
||||
|
||||
Modules may also declare a `required` object.
|
||||
### `Module.id: string`
|
||||
|
||||
```json
|
||||
"required": {
|
||||
"value": false, "(if the module is required)"
|
||||
"def": false "(if it's enabled by default, has no effect if value is true)"
|
||||
}
|
||||
```
|
||||
The ID of the module. All modules that are not of type `File` **MUST** use a maven identifier. Version information and other metadata is pulled from the identifier. Modules which are stored maven style use the identifier to resolve the destination path. If the `extension` is not provided, it defaults to `jar`.
|
||||
|
||||
If a module does not declare this object, both `value` and `def` default to true. Similarly, if a parameter is not included in the `required` object it will default to true. This will be used in the mod selection process down the line.
|
||||
**Template**
|
||||
|
||||
`my.group:arifact:version@extension`
|
||||
|
||||
`my/group/artifact/version/artifact-version.extension`
|
||||
|
||||
**Example**
|
||||
|
||||
`net.minecraft:launchwrapper:1.12` OR `net.minecraft:launchwrapper:1.12@jar`
|
||||
|
||||
`net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar`
|
||||
|
||||
If the module's artifact does not declare the `path` property, its path will be resolved from the ID.
|
||||
|
||||
### `Module.name: string`
|
||||
|
||||
The name of the module. Used on the UI.
|
||||
|
||||
### `Module.type: string`
|
||||
|
||||
The type of the module.
|
||||
|
||||
### `Module.classpath: boolean`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
If the module is of type `Library`, whether the library should be added to the classpath. Defaults to true.
|
||||
|
||||
### `Module.required: Required`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
Defines whether or not the module is required. If omitted, then the module will be required.
|
||||
|
||||
Only applicable for modules of type:
|
||||
* `ForgeMod`
|
||||
* `LiteMod`
|
||||
* `LiteLoader`
|
||||
|
||||
|
||||
### `Module.artifact: Artifact`
|
||||
|
||||
The download artifact for the module.
|
||||
|
||||
### `Module.subModules: Module[]`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
An array of sub modules declared by this module. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit.
|
||||
|
||||
|
||||
## Artifact Object
|
||||
|
||||
The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file.
|
||||
|
||||
Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The `id` becomes more important as it will be used to resolve the final path. The `id` must be provided in maven format, that is `group.id.maybemore:artifact:version`. From there, you need to declare the `extension` of the file in the artifact object. This effectively replaces the `path` option we used above.
|
||||
Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The module `id` will be used to resolve the final path, effectively replacing the `path` property. It must be provided in maven format. More information on this is provided in the documentation for the `id` property.
|
||||
|
||||
**It is EXTREMELY IMPORTANT that the file size is CORRECT. The launcher's download queue will not function properly otherwise.**
|
||||
The resolved/provided paths are appended to a base path depending on the module's declared type.
|
||||
|
||||
Ex.
|
||||
| Type | Path |
|
||||
| ---- | ---- |
|
||||
| `ForgeHosted` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
|
||||
| `Fabric` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
|
||||
| `LiteLoader` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
|
||||
| `Library` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
|
||||
| `ForgeMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
|
||||
| `LiteMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
|
||||
| `FabricMod` | ({`commonDirectory`}/mods/fabric/{`path` OR resolved}) |
|
||||
| `File` | ({`instanceDirectory`}/{`Server.id`}/{`path` OR resolved}) |
|
||||
|
||||
```SHELL
|
||||
type = forgemod
|
||||
id = com.westeroscraft:westerosblocks:1.0.0
|
||||
extension = .jar
|
||||
The `commonDirectory` and `instanceDirectory` values are stored in the launcher's config.json.
|
||||
|
||||
resolved_path = {base}/modstore/com/westeroscraft/westerosblocks/1.0.0/westerosblocks-1.0.0.jar
|
||||
```
|
||||
### `Artifact.size: number`
|
||||
|
||||
The resolved path depends on the type. Currently, there are several recognized module types:
|
||||
The size of the artifact.
|
||||
|
||||
- `forge-hosted` ({base}/libraries/{path OR resolved})
|
||||
- `library` ({base}/libraries/{path OR resolved})
|
||||
- `forgemod` ({base}/modstore/{path OR resolved})
|
||||
- `litemod` ({base}/modstore/{path OR resolved})
|
||||
- `file` ({base}/{path OR resolved})
|
||||
### `Artifact.MD5: string`
|
||||
|
||||
The MD5 hash of the artifact. This will be used to validate local artifacts.
|
||||
|
||||
### `Artifact.path: string`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
A relative path to where the file will be saved. This is appended to the base path for the module's declared type.
|
||||
|
||||
If this is not specified, the path will be resolved based on the module's ID.
|
||||
|
||||
### `Artifact.url: string/url`
|
||||
|
||||
The artifact's download url.
|
||||
|
||||
## Required Object
|
||||
|
||||
### `Required.value: boolean`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
If the module is required. Defaults to true if this property is omited.
|
||||
|
||||
### `Required.def: boolean`
|
||||
|
||||
**OPTIONAL**
|
||||
|
||||
If the module is enabled by default. Has no effect unless `Required.value` is false. Defaults to true if this property is omited.
|
||||
|
||||
---
|
||||
|
||||
## Module Types
|
||||
|
||||
### forge-hosted
|
||||
### ForgeHosted
|
||||
|
||||
The module type `forge-hosted` represents forge itself. Currently, the launcher only supports forge servers, as vanilla servers can be connected to via the mojang launcher. The `hosted` part is key, this means that the forge module must declare its required libraries as submodules.
|
||||
The module type `ForgeHosted` represents forge itself. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher. The `Hosted` part is key, this means that the forge module must declare its required libraries as submodules.
|
||||
|
||||
Ex.
|
||||
|
||||
@ -123,39 +418,97 @@ Ex.
|
||||
{
|
||||
"id": "net.minecraftforge:forge:1.11.2-13.20.1.2429",
|
||||
"name": "Minecraft Forge 1.11.2-13.20.1.2429",
|
||||
"type": "forge-hosted",
|
||||
"type": "ForgeHosted",
|
||||
"artifact": {
|
||||
"size": 4450992,
|
||||
"MD5": "3fcc9b0104f0261397d3cc897e55a1c5",
|
||||
"extension": ".jar",
|
||||
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar"
|
||||
},
|
||||
"sub_modules": [
|
||||
"subModules": [
|
||||
{
|
||||
"id": "net.minecraft:launchwrapper:1.12",
|
||||
"name": "Mojang (LaunchWrapper)",
|
||||
"type": "library",
|
||||
"type": "Library",
|
||||
"artifact": {
|
||||
"size": 32999,
|
||||
"MD5": "934b2d91c7c5be4a49577c9e6b40e8da",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar"
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All of forge's required libraries are declared in the `version.json` file found in the root of the forge jar file. These libraries MUST be hosted and declared a submodules or forge will not work.
|
||||
|
||||
There were plans to add a `forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented.
|
||||
There were plans to add a `Forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented.
|
||||
|
||||
---
|
||||
|
||||
### library
|
||||
### Fabric
|
||||
|
||||
The module type `library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process.
|
||||
The module type `Fabric` represents the fabric mod loader. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher.
|
||||
|
||||
Ex.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "net.fabricmc:fabric-loader:0.15.0",
|
||||
"name": "Fabric (fabric-loader)",
|
||||
"type": "Fabric",
|
||||
"artifact": {
|
||||
"size": 1196222,
|
||||
"MD5": "a43d5a142246801343b6cedef1c102c4",
|
||||
"url": "http://localhost:8080/repo/lib/net/fabricmc/fabric-loader/0.15.0/fabric-loader-0.15.0.jar"
|
||||
},
|
||||
"subModules": [
|
||||
{
|
||||
"id": "1.20.1-fabric-0.15.0",
|
||||
"name": "Fabric (version.json)",
|
||||
"type": "VersionManifest",
|
||||
"artifact": {
|
||||
"size": 2847,
|
||||
"MD5": "69a2bd43452325ba1bc882fa0904e054",
|
||||
"url": "http://localhost:8080/repo/versions/1.20.1-fabric-0.15.0/1.20.1-fabric-0.15.0.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fabric works similarly to Forge 1.13+.
|
||||
|
||||
---
|
||||
|
||||
### LiteLoader
|
||||
|
||||
The module type `LiteLoader` represents liteloader. It is handled as a library and added to the classpath at runtime. Special launch conditions are executed when liteloader is present and enabled. This module can be optional and toggled similarly to `ForgeMod` and `Litemod` modules.
|
||||
|
||||
Ex.
|
||||
```json
|
||||
{
|
||||
"id": "com.mumfrey:liteloader:1.11.2",
|
||||
"name": "Liteloader (1.11.2)",
|
||||
"type": "LiteLoader",
|
||||
"required": {
|
||||
"value": false,
|
||||
"def": false
|
||||
},
|
||||
"artifact": {
|
||||
"size": 1685422,
|
||||
"MD5": "3a98b5ed95810bf164e71c1a53be568d",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/liteloader-1.11.2.jar"
|
||||
},
|
||||
"subModules": [
|
||||
"All LiteMods go here"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Library
|
||||
|
||||
The module type `Library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process.
|
||||
|
||||
Ex.
|
||||
|
||||
@ -163,11 +516,10 @@ Ex.
|
||||
{
|
||||
"id": "net.sf.jopt-simple:jopt-simple:4.6",
|
||||
"name": "Jopt-simple 4.6",
|
||||
"type": "library",
|
||||
"type": "Library",
|
||||
"artifact": {
|
||||
"size": 62477,
|
||||
"MD5": "13560a58a79b46b82057686543e8d727",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar"
|
||||
}
|
||||
}
|
||||
@ -175,20 +527,19 @@ Ex.
|
||||
|
||||
---
|
||||
|
||||
### forgemod
|
||||
### ForgeMod
|
||||
|
||||
The module type `forgemod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format).
|
||||
The module type `ForgeMod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format).
|
||||
|
||||
Ex.
|
||||
```json
|
||||
{
|
||||
"id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133",
|
||||
"name": "WesterosBlocks (3.0.0-beta-6-133)",
|
||||
"type": "forgemod",
|
||||
"type": "ForgeMod",
|
||||
"artifact": {
|
||||
"size": 16321712,
|
||||
"MD5": "5a89e2ab18916c18965fc93a0766cc6e",
|
||||
"extension": ".jar",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar"
|
||||
}
|
||||
}
|
||||
@ -196,15 +547,33 @@ Ex.
|
||||
|
||||
---
|
||||
|
||||
### litemod
|
||||
### LiteMod
|
||||
|
||||
This module type is being actively considered and changed, until finalized there will be no documentation.
|
||||
The module type `LiteMod` represents a mod loaded by liteloader. These files are stored maven-style and passed to liteloader using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). Documentation for liteloader's implementation of this can be found on [this issue](http://develop.liteloader.com/liteloader/LiteLoader/issues/34).
|
||||
|
||||
Ex.
|
||||
```json
|
||||
{
|
||||
"id": "com.mumfrey:macrokeybindmod:0.14.4-1.11.2@litemod",
|
||||
"name": "Macro/Keybind Mod (0.14.4-1.11.2)",
|
||||
"type": "LiteMod",
|
||||
"required": {
|
||||
"value": false,
|
||||
"def": false
|
||||
},
|
||||
"artifact": {
|
||||
"size": 1670811,
|
||||
"MD5": "16080785577b391d426c62c8d3138558",
|
||||
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/macrokeybindmod.litemod"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### file
|
||||
### File
|
||||
|
||||
The module type `file` represents a generic file required by the client, another module, etc.
|
||||
The module type `file` represents a generic file required by the client, another module, etc. These files are stored in the server's instance directory.
|
||||
|
||||
Ex.
|
||||
|
||||
@ -221,7 +590,3 @@ Ex.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This format is actively under development and is likely to change.
|
1584
docs/sample_distribution.json
Normal file
51
electron-builder.yml
Normal file
@ -0,0 +1,51 @@
|
||||
appId: 'helioslauncher'
|
||||
productName: 'Helios Launcher'
|
||||
artifactName: '${productName}-setup-${version}.${ext}'
|
||||
|
||||
copyright: 'Copyright © 2018-2022 Daniel Scalzi'
|
||||
|
||||
asar: true
|
||||
compression: 'maximum'
|
||||
|
||||
files:
|
||||
- '!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.nvmrc,.eslintrc.json}'
|
||||
|
||||
extraResources:
|
||||
- 'libraries'
|
||||
|
||||
# Windows Configuration
|
||||
win:
|
||||
target:
|
||||
- target: 'nsis'
|
||||
arch: 'x64'
|
||||
|
||||
# Windows Installer Configuration
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: false
|
||||
allowElevation: true
|
||||
allowToChangeInstallationDirectory: true
|
||||
|
||||
# macOS Configuration
|
||||
mac:
|
||||
target:
|
||||
- target: 'dmg'
|
||||
arch:
|
||||
- 'x64'
|
||||
- 'arm64'
|
||||
artifactName: '${productName}-setup-${version}-${arch}.${ext}'
|
||||
category: 'public.app-category.games'
|
||||
|
||||
# Linux Configuration
|
||||
linux:
|
||||
target: 'AppImage'
|
||||
maintainer: 'Daniel Scalzi'
|
||||
vendor: 'Daniel Scalzi'
|
||||
synopsis: 'Modded Minecraft Launcher'
|
||||
description: 'Custom launcher which allows users to join modded servers. All mods, configurations, and updates are handled automatically.'
|
||||
category: 'Game'
|
||||
|
||||
|
||||
directories:
|
||||
buildResources: 'build'
|
||||
output: 'dist'
|
345
index.js
@ -1,55 +1,348 @@
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
const fs = require('fs')
|
||||
const ejse = require('ejs-electron')
|
||||
const remoteMain = require('@electron/remote/main')
|
||||
remoteMain.initialize()
|
||||
|
||||
// Requirements
|
||||
const { app, BrowserWindow, ipcMain, Menu, shell } = require('electron')
|
||||
const autoUpdater = require('electron-updater').autoUpdater
|
||||
const ejse = require('ejs-electron')
|
||||
const fs = require('fs')
|
||||
const isDev = require('./app/assets/js/isdev')
|
||||
const path = require('path')
|
||||
const semver = require('semver')
|
||||
const { pathToFileURL } = require('url')
|
||||
const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR, SHELL_OPCODE } = require('./app/assets/js/ipcconstants')
|
||||
const LangLoader = require('./app/assets/js/langloader')
|
||||
|
||||
// Setup Lang
|
||||
LangLoader.setupLanguage()
|
||||
|
||||
// Setup auto updater.
|
||||
function initAutoUpdater(event, data) {
|
||||
|
||||
if(data){
|
||||
autoUpdater.allowPrerelease = true
|
||||
} else {
|
||||
// Defaults to true if application version contains prerelease components (e.g. 0.12.1-alpha.1)
|
||||
// autoUpdater.allowPrerelease = true
|
||||
}
|
||||
|
||||
if(isDev){
|
||||
autoUpdater.autoInstallOnAppQuit = false
|
||||
autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml')
|
||||
}
|
||||
if(process.platform === 'darwin'){
|
||||
autoUpdater.autoDownload = false
|
||||
}
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
event.sender.send('autoUpdateNotification', 'update-available', info)
|
||||
})
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
event.sender.send('autoUpdateNotification', 'update-downloaded', info)
|
||||
})
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
event.sender.send('autoUpdateNotification', 'update-not-available', info)
|
||||
})
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
event.sender.send('autoUpdateNotification', 'checking-for-update')
|
||||
})
|
||||
autoUpdater.on('error', (err) => {
|
||||
event.sender.send('autoUpdateNotification', 'realerror', err)
|
||||
})
|
||||
}
|
||||
|
||||
// Open channel to listen for update actions.
|
||||
ipcMain.on('autoUpdateAction', (event, arg, data) => {
|
||||
switch(arg){
|
||||
case 'initAutoUpdater':
|
||||
console.log('Initializing auto updater.')
|
||||
initAutoUpdater(event, data)
|
||||
event.sender.send('autoUpdateNotification', 'ready')
|
||||
break
|
||||
case 'checkForUpdate':
|
||||
autoUpdater.checkForUpdates()
|
||||
.catch(err => {
|
||||
event.sender.send('autoUpdateNotification', 'realerror', err)
|
||||
})
|
||||
break
|
||||
case 'allowPrereleaseChange':
|
||||
if(!data){
|
||||
const preRelComp = semver.prerelease(app.getVersion())
|
||||
if(preRelComp != null && preRelComp.length > 0){
|
||||
autoUpdater.allowPrerelease = true
|
||||
} else {
|
||||
autoUpdater.allowPrerelease = data
|
||||
}
|
||||
} else {
|
||||
autoUpdater.allowPrerelease = data
|
||||
}
|
||||
break
|
||||
case 'installUpdateNow':
|
||||
autoUpdater.quitAndInstall()
|
||||
break
|
||||
default:
|
||||
console.log('Unknown argument', arg)
|
||||
break
|
||||
}
|
||||
})
|
||||
// Redirect distribution index event from preloader to renderer.
|
||||
ipcMain.on('distributionIndexDone', (event, res) => {
|
||||
event.sender.send('distributionIndexDone', res)
|
||||
})
|
||||
|
||||
// Handle trash item.
|
||||
ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => {
|
||||
try {
|
||||
await shell.trashItem(args[0])
|
||||
return {
|
||||
result: true
|
||||
}
|
||||
} catch(error) {
|
||||
return {
|
||||
result: false,
|
||||
error: error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Disable hardware acceleration.
|
||||
// https://electronjs.org/docs/tutorial/offscreen-rendering
|
||||
app.disableHardwareAcceleration()
|
||||
|
||||
|
||||
const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?'
|
||||
|
||||
// Microsoft Auth Login
|
||||
let msftAuthWindow
|
||||
let msftAuthSuccess
|
||||
let msftAuthViewSuccess
|
||||
let msftAuthViewOnClose
|
||||
ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => {
|
||||
if (msftAuthWindow) {
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose)
|
||||
return
|
||||
}
|
||||
msftAuthSuccess = false
|
||||
msftAuthViewSuccess = arguments_[0]
|
||||
msftAuthViewOnClose = arguments_[1]
|
||||
msftAuthWindow = new BrowserWindow({
|
||||
title: LangLoader.queryJS('index.microsoftLoginTitle'),
|
||||
backgroundColor: '#222222',
|
||||
width: 520,
|
||||
height: 600,
|
||||
frame: true,
|
||||
icon: getPlatformIcon('SealCircle')
|
||||
})
|
||||
|
||||
msftAuthWindow.on('closed', () => {
|
||||
msftAuthWindow = undefined
|
||||
})
|
||||
|
||||
msftAuthWindow.on('close', () => {
|
||||
if(!msftAuthSuccess) {
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose)
|
||||
}
|
||||
})
|
||||
|
||||
msftAuthWindow.webContents.on('did-navigate', (_, uri) => {
|
||||
if (uri.startsWith(REDIRECT_URI_PREFIX)) {
|
||||
let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&')
|
||||
let queryMap = {}
|
||||
|
||||
queries.forEach(query => {
|
||||
const [name, value] = query.split('=')
|
||||
queryMap[name] = decodeURI(value)
|
||||
})
|
||||
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess)
|
||||
|
||||
msftAuthSuccess = true
|
||||
msftAuthWindow.close()
|
||||
msftAuthWindow = null
|
||||
}
|
||||
})
|
||||
|
||||
msftAuthWindow.removeMenu()
|
||||
msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`)
|
||||
})
|
||||
|
||||
// Microsoft Auth Logout
|
||||
let msftLogoutWindow
|
||||
let msftLogoutSuccess
|
||||
let msftLogoutSuccessSent
|
||||
ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => {
|
||||
if (msftLogoutWindow) {
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN)
|
||||
return
|
||||
}
|
||||
|
||||
msftLogoutSuccess = false
|
||||
msftLogoutSuccessSent = false
|
||||
msftLogoutWindow = new BrowserWindow({
|
||||
title: LangLoader.queryJS('index.microsoftLogoutTitle'),
|
||||
backgroundColor: '#222222',
|
||||
width: 520,
|
||||
height: 600,
|
||||
frame: true,
|
||||
icon: getPlatformIcon('SealCircle')
|
||||
})
|
||||
|
||||
msftLogoutWindow.on('closed', () => {
|
||||
msftLogoutWindow = undefined
|
||||
})
|
||||
|
||||
msftLogoutWindow.on('close', () => {
|
||||
if(!msftLogoutSuccess) {
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED)
|
||||
} else if(!msftLogoutSuccessSent) {
|
||||
msftLogoutSuccessSent = true
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
|
||||
}
|
||||
})
|
||||
|
||||
msftLogoutWindow.webContents.on('did-navigate', (_, uri) => {
|
||||
if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) {
|
||||
msftLogoutSuccess = true
|
||||
setTimeout(() => {
|
||||
if(!msftLogoutSuccessSent) {
|
||||
msftLogoutSuccessSent = true
|
||||
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
|
||||
}
|
||||
|
||||
if(msftLogoutWindow) {
|
||||
msftLogoutWindow.close()
|
||||
msftLogoutWindow = null
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
|
||||
msftLogoutWindow.removeMenu()
|
||||
msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout')
|
||||
})
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let win
|
||||
|
||||
function createWindow() {
|
||||
|
||||
win = new BrowserWindow({
|
||||
width: 980,
|
||||
height: 552,
|
||||
icon: getPlatformIcon('WesterosSealSquare'),
|
||||
icon: getPlatformIcon('SealCircle'),
|
||||
frame: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js')
|
||||
}
|
||||
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
},
|
||||
backgroundColor: '#171614'
|
||||
})
|
||||
remoteMain.enable(win.webContents)
|
||||
|
||||
ejse.data('bkid', Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)))
|
||||
const data = {
|
||||
bkid: Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)),
|
||||
lang: (str, placeHolders) => LangLoader.queryEJS(str, placeHolders)
|
||||
}
|
||||
Object.entries(data).forEach(([key, val]) => ejse.data(key, val))
|
||||
|
||||
win.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'app', 'app.ejs'),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
}))
|
||||
win.loadURL(pathToFileURL(path.join(__dirname, 'app', 'app.ejs')).toString())
|
||||
|
||||
win.setMenu(null)
|
||||
/*win.once('ready-to-show', () => {
|
||||
win.show()
|
||||
})*/
|
||||
|
||||
win.setResizable(true)
|
||||
win.removeMenu()
|
||||
|
||||
win.resizable = true
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null
|
||||
})
|
||||
}
|
||||
|
||||
function getPlatformIcon(filename){
|
||||
const opSys = process.platform
|
||||
if (opSys === 'darwin') {
|
||||
filename = filename + '.icns'
|
||||
} else if (opSys === 'win32') {
|
||||
filename = filename + '.ico'
|
||||
} else {
|
||||
filename = filename + '.png'
|
||||
function createMenu() {
|
||||
|
||||
if(process.platform === 'darwin') {
|
||||
|
||||
// Extend default included application menu to continue support for quit keyboard shortcut
|
||||
let applicationSubMenu = {
|
||||
label: 'Application',
|
||||
submenu: [{
|
||||
label: 'About Application',
|
||||
selector: 'orderFrontStandardAboutPanel:'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// New edit menu adds support for text-editing keyboard shortcuts
|
||||
let editSubMenu = {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
label: 'Undo',
|
||||
accelerator: 'CmdOrCtrl+Z',
|
||||
selector: 'undo:'
|
||||
}, {
|
||||
label: 'Redo',
|
||||
accelerator: 'Shift+CmdOrCtrl+Z',
|
||||
selector: 'redo:'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Cut',
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
selector: 'cut:'
|
||||
}, {
|
||||
label: 'Copy',
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
selector: 'copy:'
|
||||
}, {
|
||||
label: 'Paste',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
selector: 'paste:'
|
||||
}, {
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
selector: 'selectAll:'
|
||||
}]
|
||||
}
|
||||
|
||||
// Bundle submenus into a single template and build a menu object with it
|
||||
let menuTemplate = [applicationSubMenu, editSubMenu]
|
||||
let menuObject = Menu.buildFromTemplate(menuTemplate)
|
||||
|
||||
// Assign it to the application
|
||||
Menu.setApplicationMenu(menuObject)
|
||||
|
||||
}
|
||||
|
||||
return path.join(__dirname, 'app', 'assets', 'images', filename)
|
||||
}
|
||||
|
||||
app.on('ready', createWindow);
|
||||
function getPlatformIcon(filename){
|
||||
let ext
|
||||
switch(process.platform) {
|
||||
case 'win32':
|
||||
ext = 'ico'
|
||||
break
|
||||
case 'darwin':
|
||||
case 'linux':
|
||||
default:
|
||||
ext = 'png'
|
||||
break
|
||||
}
|
||||
|
||||
return path.join(__dirname, 'app', 'assets', 'images', `${filename}.${ext}`)
|
||||
}
|
||||
|
||||
app.on('ready', createWindow)
|
||||
app.on('ready', createMenu)
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
|
BIN
libraries/java/PackXZExtract.jar
Normal file
6502
package-lock.json
generated
112
package.json
@ -1,81 +1,51 @@
|
||||
{
|
||||
"name": "westeroscraftlauncher",
|
||||
"version": "0.0.1",
|
||||
"description": "Custom modded launcher for Westeroscraft",
|
||||
"productName": "WesterosCraft Launcher",
|
||||
"name": "helioslauncher",
|
||||
"version": "2.1.0",
|
||||
"productName": "Helios Launcher",
|
||||
"description": "Modded Minecraft Launcher",
|
||||
"author": "Daniel Scalzi (https://github.com/dscalzi/)",
|
||||
"license": "UNLICENSED",
|
||||
"homepage": "https://github.com/dscalzi/HeliosLauncher",
|
||||
"bugs": {
|
||||
"url": "https://github.com/dscalzi/HeliosLauncher/issues"
|
||||
},
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "electron index.js",
|
||||
"dist": "SET ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true & electron-builder",
|
||||
"dist:win": "npm run dist -- --win --x64",
|
||||
"dist:mac": "npm run dist -- --mac",
|
||||
"dist:linux": "npm run dist -- --linux --x64",
|
||||
"dist:all": "npm run dist -- -wl --x64"
|
||||
"start": "electron .",
|
||||
"dist": "electron-builder build",
|
||||
"dist:win": "npm run dist -- -w",
|
||||
"dist:mac": "npm run dist -- -m",
|
||||
"dist:linux": "npm run dist -- -l",
|
||||
"lint": "eslint --config .eslintrc.json ."
|
||||
},
|
||||
"engines": {
|
||||
"node": "8.9.x"
|
||||
"node": "18.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.1.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"discord-rpc-patch": "^4.0.1",
|
||||
"ejs": "^3.1.9",
|
||||
"ejs-electron": "^2.1.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"fs-extra": "^11.1.1",
|
||||
"github-syntax-dark": "^0.5.0",
|
||||
"got": "^11.8.5",
|
||||
"helios-core": "~2.1.0",
|
||||
"helios-distribution-types": "^1.3.0",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"semver": "^7.5.4",
|
||||
"toml": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^27.1.3",
|
||||
"electron-builder": "^24.9.1",
|
||||
"eslint": "^8.55.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitlab.com/westeroscraft/electronlauncher.git"
|
||||
},
|
||||
"author": "Daniel Scalzi",
|
||||
"license": "AGPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/westeroscraft/electronlauncher/issues"
|
||||
},
|
||||
"homepage": "http://www.westeroscraft.com/",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.4.7",
|
||||
"async": "^2.6.0",
|
||||
"discord-rpc": "^3.0.0-beta.8",
|
||||
"ejs": "^2.5.7",
|
||||
"ejs-electron": "^2.0.1",
|
||||
"find-java-home": "^0.2.0",
|
||||
"jquery": "^3.2.1",
|
||||
"request-promise-native": "^1.0.5",
|
||||
"uuid": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^1.7.11",
|
||||
"electron-builder": "^19.54.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "westeroscraftlauncher",
|
||||
"productName": "WesterosCraft Launcher",
|
||||
"artifactName": "${productName}.${ext}",
|
||||
"copyright": "Copyright © 2018 WesterosCraft",
|
||||
"directories": {
|
||||
"buildResources": "build",
|
||||
"output": "dist"
|
||||
},
|
||||
"win": {
|
||||
"target": "portable",
|
||||
"icon": "build/icon.ico"
|
||||
},
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"category": "public.app-category.games",
|
||||
"icon": "build/icon.icns",
|
||||
"type": "distribution"
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage",
|
||||
"maintainer": "Daniel Scalzi",
|
||||
"vendor": "WesterosCraft",
|
||||
"synopsis": "Custom modded launcher for WesterosCraft",
|
||||
"description": "Custom launcher which allows users to join WesterosCraft. All mods, configurations, and updates are handled automatically.",
|
||||
"category": "Game"
|
||||
},
|
||||
"deb": {
|
||||
"compression": "xz",
|
||||
"packageCategory": "Games",
|
||||
"priority": "optional"
|
||||
},
|
||||
"compression": "maximum",
|
||||
"files": [
|
||||
"!{target,logs,.vscode,docs}"
|
||||
],
|
||||
"asar": true
|
||||
"url": "git+https://github.com/dscalzi/HeliosLauncher.git"
|
||||
}
|
||||
}
|
||||
|