Compare commits
470 Commits
Author | SHA1 | Date | |
---|---|---|---|
55b9f063ba | |||
|
18e6fc67fb | ||
|
0d199038b6 | ||
|
3f03fbc155 | ||
|
501addacbd | ||
|
ec08d70b2b | ||
|
b800af378d | ||
|
bc9db0247a | ||
|
e401608c33 | ||
|
6aaeeff9a4 | ||
|
9cca37ca8a | ||
|
03dac9ed6d | ||
|
eb683f89ec | ||
|
ae0e9e227d | ||
|
dc15bbfde8 | ||
|
0d23f5c45b | ||
|
fc4823a01f | ||
|
95eebc18a7 | ||
|
d03ff90f78 | ||
|
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 | ||
|
8ea4ae8ec2 | ||
|
52aea274a7 | ||
|
d50b3d2d41 | ||
|
1284c54525 | ||
|
a3e7dfc570 | ||
|
6c5e9303b4 | ||
|
69f9d297d0 | ||
|
d7aa79f7d3 | ||
|
2e4a2be8b4 | ||
|
45997c17f5 | ||
|
e236a38757 | ||
|
e555dd747f | ||
|
965b56c6bd | ||
|
d96766528e | ||
|
aee8f016bf | ||
|
a6184c81d5 | ||
|
e7785fa603 | ||
|
d4d7be7c47 | ||
|
1d10b0209a | ||
|
6d6d876aa5 | ||
|
45998a2239 | ||
|
5afd9815a5 | ||
|
3c7d7d79af | ||
|
a4012458be | ||
|
98684a9c1f | ||
|
0dde4bce42 | ||
|
086bfc8593 | ||
|
52ab270ce3 | ||
|
4c2c46f535 | ||
|
aac789bf2c | ||
|
dbf024cce6 | ||
|
dd9e1418c8 | ||
|
5352239f91 | ||
|
2c7dc16247 | ||
|
0ec64e6ded | ||
|
c74006357e | ||
|
dd7ae6bf56 | ||
|
abbfb52c1b | ||
|
6c393f111f | ||
|
2df6c82177 | ||
|
246def86d8 | ||
|
1654e7a2c4 | ||
|
5ed660d3ab | ||
|
6eb6177d4b | ||
|
02758b27b2 | ||
|
0d013f7d6f | ||
|
f9444858da | ||
|
05ba3a5190 | ||
|
898e371519 | ||
|
8bb6df921c | ||
|
455f199457 | ||
|
700a5c50cf | ||
|
582ea96dfe | ||
|
6a44694a32 |
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: 20
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm run dist
|
||||
shell: bash
|
3
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
/node_modules/
|
||||
/.vs/
|
||||
/.vscode/
|
||||
/mcfiles/
|
||||
/target/
|
||||
/logs/
|
||||
/dist/
|
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2024 Daniel D. Scalzi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
206
README.md
@ -1,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/) v7.9.0+
|
||||
<h1 align="center">ONIMAI.RU MC 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.
|
||||

|
||||

|
||||
|
||||
### 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://github.com/dscalzi/HeliosLauncher/releases/latest)
|
||||
|
||||
#### Latest Pre-Release
|
||||
[](https://github.com/dscalzi/HeliosLauncher/releases)
|
||||
|
||||
**Supported Platforms**
|
||||
|
||||
If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/releases) tab, select the installer for your system.
|
||||
|
||||
| Platform | File |
|
||||
| -------- | ---- |
|
||||
| Windows x64 | `Helios-Launcher-setup-VERSION.exe` |
|
||||
| macOS x64 | `Helios-Launcher-setup-VERSION-x64.dmg` |
|
||||
| macOS arm64 | `Helios-Launcher-setup-VERSION-arm64.dmg` |
|
||||
| Linux x64 | `Helios-Launcher-setup-VERSION.AppImage` |
|
||||
|
||||
## Console
|
||||
|
||||
To open the console, use the following keybind.
|
||||
|
||||
```console
|
||||
ctrl + shift + i
|
||||
```
|
||||
|
||||
# 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
|
||||

|
||||
|
||||
|
||||
## Development
|
||||
|
||||
This section details the setup of a basic developmentment environment.
|
||||
|
||||
### Getting Started
|
||||
|
||||
**System Requirements**
|
||||
|
||||
* [Node.js][nodejs] v20
|
||||
|
||||
---
|
||||
|
||||
**Clone and Install Dependencies**
|
||||
|
||||
```console
|
||||
> git clone https://github.com/dscalzi/HeliosLauncher.git
|
||||
> cd HeliosLauncher
|
||||
> npm install
|
||||
```
|
||||
|
||||
### Visual Studio Code ###
|
||||
---
|
||||
|
||||
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,49 +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": "legacy"
|
||||
"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 typing the following keys in sequence on the main window.
|
||||
#### Debug Renderer Process
|
||||
|
||||
```shell
|
||||
wcdev
|
||||
```
|
||||
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.
|
||||
|
||||
# Issues / Further Support #
|
||||
---
|
||||
|
||||
If you run into any issue which cannot be resolved via a quick google search, create an issue using the tab above.
|
||||
### Note on Third-Party Usage
|
||||
|
||||
Much of the discussion regarding this launcher is done on Discord, feel free to join us there [](https://discord.gg/hqdjs3m)
|
||||
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.
|
||||
|
||||
For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
* [Wiki][wiki]
|
||||
* [Nebula (Create Distribution.json)][nebula]
|
||||
* [v2 Rewrite Branch (Inactive)][v2branch]
|
||||
|
||||
The best way to contact the developers is on Discord.
|
||||
|
||||
[][discord]
|
||||
|
||||
---
|
||||
|
||||
### See you ingame.
|
||||
|
||||
|
||||
[nodejs]: https://nodejs.org/en/ 'Node.js'
|
||||
[vscode]: https://code.visualstudio.com/ 'Visual Studio Code'
|
||||
[mainprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Main Process'
|
||||
[rendererprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Renderer Process'
|
||||
[chromedebugger]: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Debugger for Chrome'
|
||||
[discord]: https://discord.gg/zNWUXdt 'Discord'
|
||||
[wiki]: https://github.com/dscalzi/HeliosLauncher/wiki 'wiki'
|
||||
[nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula'
|
||||
[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch'
|
||||
|
@ -1,180 +0,0 @@
|
||||
<<<<<<< HEAD
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="assets/css/global.css">
|
||||
<link rel="stylesheet" type="text/css" href="assets/css/header.css">
|
||||
<link rel="stylesheet" type="text/css" href="assets/css/nav.css">
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
.inner_div{
|
||||
width:100%;
|
||||
background-color: black;
|
||||
border: 5px solid #a02d2a;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
<div id="header_container">
|
||||
<div id="header_seal_container">
|
||||
<img id="header_seal" src="./assets/images/WesterosSealSquare.png"/>
|
||||
</div>
|
||||
<div id="header_img_container">
|
||||
<img id="header_img" src="./assets/images/WCTextCrop.png" />
|
||||
</div>
|
||||
<div id="header_social_container">
|
||||
<a href="http://facebook.com/westeroscraft" class="header_social_link">
|
||||
<img src="./assets/images/facebook.png" class="header_social_img" />
|
||||
</a>
|
||||
<a href="http://reddit.com/r/westeroscraft" class="header_social_link">
|
||||
<img src="./assets/images/reddit.png" class="header_social_img" />
|
||||
</a>
|
||||
<a href="http://twitter.com/westeroscraft" class="header_social_link">
|
||||
<img src="./assets/images/twitter.png" class="header_social_img" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-2 col-sm-2">
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-3 col-sm-3">
|
||||
|
||||
<img class="player_icon" src="https://minotar.net/helm/pufferboss.png">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-9 col-sm-9">
|
||||
|
||||
<p class="input_text">Email</p>
|
||||
<input class="input" id="EMAIL">
|
||||
|
||||
<p class="input_text">Password</p>
|
||||
<input type="password" id="PASSWORD" class="input" />
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-10 col-sm-10">
|
||||
|
||||
<ul>
|
||||
<li><a href="">NEWS</a></li>
|
||||
<li><a href="">MAP</a></li>
|
||||
<li><a href="">MODS</a></li>
|
||||
<li><a href="">FAQ</a></li>
|
||||
<li><a href="">LOG</a></li>
|
||||
<li><a href="">SETTINGS</a></li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="inner_div">
|
||||
|
||||
<p class="title"> Post title </p>
|
||||
|
||||
<p class="subtite">Posted by: PERSON 23.4.17</p>
|
||||
|
||||
<pre style="text-align: left; word-break: break-all; overflow-wrap: break-word;"> Hello WesterosCraft Builders and Honored Guests! As you know,
|
||||
progress on Kingslanding had stalled, but no longer -- we want to finish this big, beautiful, city in style and
|
||||
are calling all builders to the capital!
|
||||
There has been a lot of progress with Kingslanding planning and we are happy to...
|
||||
</pre>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
|
||||
|
||||
</html>
|
||||
=======
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Westeroscraft Launcher</title>
|
||||
<script src="./assets/js/script.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="./assets/css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header_container">
|
||||
<div id="header_seal_container">
|
||||
<img id="header_seal" src="./assets/images/WesterosSealSquare.png"/>
|
||||
</div>
|
||||
<div id="header_img_container">
|
||||
<img id="header_img" src="./assets/images/WCTextCrop.png" />
|
||||
</div>
|
||||
<div id="header_social_container">
|
||||
<a href="http://facebook.com/westeroscraft" class="header_social_link">
|
||||
<img src="./assets/images/facebook.png" class="header_social_img" />
|
||||
</a>
|
||||
<a href="http://reddit.com/r/westeroscraft" class="header_social_link">
|
||||
<img src="./assets/images/reddit.png" class="header_social_img" />
|
||||
</a>
|
||||
<a href="http://twitter.com/westeroscraft" class="header_social_link">
|
||||
<img src="./assets/images/twitter.png" class="header_social_img" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="body_left_container">
|
||||
|
||||
</div>
|
||||
<div id="body_right_container">
|
||||
<div id="main_toggle_group">
|
||||
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>News</label></div>
|
||||
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Map</label></div>
|
||||
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Mods</label></div>
|
||||
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>FAQ</label></div>
|
||||
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Log</label></div>
|
||||
<div><label><input type="radio" name="main_group" class="mtoggle_button"/>Settings</label></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
>>>>>>> refs/remotes/origin/master
|
49
app/app.ejs
Normal file
@ -0,0 +1,49 @@
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-In6B8teKZQll5heMl9bS7CESTbGvuAt3VVV86BUQBDk='"/>
|
||||
<title><%= lang('app.title') %></title>
|
||||
<script src="./assets/js/scripts/uicore.js"></script>
|
||||
<script src="./assets/js/scripts/uibinder.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="./assets/css/launcher.css">
|
||||
<style>
|
||||
body {
|
||||
/*background: url('assets/images/backgrounds/<%=bkid%>.jpg') no-repeat center center fixed;*/
|
||||
transition: background-image 1s ease;
|
||||
background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAPwBwAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8VooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9k=');
|
||||
background-size: cover;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
#main {
|
||||
display: none;
|
||||
height: calc(100% - 22px);
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
#main[overlay] {
|
||||
filter: blur(3px) contrast(0.9) brightness(1.0);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body bkid="<%=bkid%>">
|
||||
<%- include('frame') %>
|
||||
<div id="main">
|
||||
<%- include('welcome') %>
|
||||
<%- include('login') %>
|
||||
<%- include('waiting') %>
|
||||
<%- include('loginOptions') %>
|
||||
<%- include('settings') %>
|
||||
<%- include('landing') %>
|
||||
</div>
|
||||
<%- include('overlay') %>
|
||||
<div id="loadingContainer">
|
||||
<div id="loadingContent">
|
||||
<div id="loadSpinnerContainer">
|
||||
<img id="loadCenterImage" src="assets/images/LoadingSeal.png">
|
||||
<img id="loadSpinnerImage" class="rotating" src="assets/images/LoadingText.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,55 +0,0 @@
|
||||
|
||||
@font-face {
|
||||
font-family: ringbearer;
|
||||
src: url('../fonts/ringbearer.ttf');
|
||||
}
|
||||
|
||||
/* Logger font, found on https://fonts.google.com/specimen/Inconsolata?selection.family=Inconsolata */
|
||||
@font-face {
|
||||
font-family: inconsolata;
|
||||
src: url('../fonts/Inconsolata-Bold.ttf');
|
||||
}
|
||||
|
||||
|
||||
html , body{
|
||||
background: url('../images/BrownWithWignette.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
|
||||
a, a:hover
|
||||
{
|
||||
color: black;
|
||||
text-decoration:none;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
|
||||
pre {border: 0; background-color: transparent;}
|
||||
|
||||
|
||||
|
||||
input{
|
||||
width: 100%;
|
||||
padding-left: 3px;
|
||||
border: none;
|
||||
border-bottom: 3px solid #ECECEC;
|
||||
font-size: 20px;
|
||||
outline:none;
|
||||
}
|
||||
|
||||
|
||||
p, img, pre, span, label, h1 ,h2 ,h3 {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
-webkit-user-drag: none;
|
||||
-khtml-user-drag: none;
|
||||
-moz-user-drag: none;
|
||||
-o-user-drag: none;
|
||||
user-drag: none;
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
#header_container {
|
||||
background-color: black;
|
||||
padding: 5px;
|
||||
font-size: 0px;
|
||||
text-align: center;
|
||||
border-bottom: thick solid #a02d2a;
|
||||
border-bottom-width: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Div container for the seal image. */
|
||||
#header_seal_container {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Div container for the header image. */
|
||||
#header_img_container {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Seal and header images. */
|
||||
#header_seal,
|
||||
#header_img {
|
||||
height: 75px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Div container for the social buttons. */
|
||||
#header_social_container {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Social buttons. */
|
||||
.header_social_img {
|
||||
height: 25px;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
3979
app/assets/css/launcher.css
Normal file
@ -1,351 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Fonts *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Primary font for the application, found on http://www.dafont.com/ringbearer.font */
|
||||
@font-face {
|
||||
font-family: ringbearer;
|
||||
src: url('../fonts/ringbearer.ttf');
|
||||
}
|
||||
|
||||
/* Logger font, found on https://fonts.google.com/specimen/Inconsolata?selection.family=Inconsolata */
|
||||
@font-face {
|
||||
font-family: inconsolata;
|
||||
src: url('../fonts/Inconsolata-Regular.ttf');
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Body *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Reset body, html, and div presets. */
|
||||
body, html, div {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
background: url('../images/BrownWithWignette.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#main_content {
|
||||
height: calc(100% - 90px);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #a02d2a;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 7px 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #a02d2a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Header *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Main div header container. */
|
||||
#header_container {
|
||||
background-color: black;
|
||||
padding: 5px;
|
||||
font-size: 0px;
|
||||
text-align: center;
|
||||
border-bottom: thick solid #a02d2a;
|
||||
border-bottom-width: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Div container for the seal image. */
|
||||
#header_seal_container {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Div container for the header image. */
|
||||
#header_img_container {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Seal and header images. */
|
||||
#header_seal,
|
||||
#header_img {
|
||||
height: 75px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Div container for the social buttons. */
|
||||
#header_social_container {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Social buttons. */
|
||||
.header_social_img {
|
||||
height: 25px;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Toggle Buttons *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.toggle-btn {
|
||||
color: #000000;
|
||||
font-size: 15px;
|
||||
font-family: 'ringbearer';
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
width: 90px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.toggle-btn-grp .toggle-btn {
|
||||
border-radius: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-btn:not(.success):hover {
|
||||
box-shadow: inset 0 -4.5px 0 0 #000000;
|
||||
}
|
||||
|
||||
.toggle-btn.success {
|
||||
background: #a02d2a;
|
||||
color: #ffffff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.visuallyhidden {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus {
|
||||
clip: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Left Body Container *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
#left_pane {
|
||||
float: left;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#welcome_text_container {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#welcome_text {
|
||||
font-family: 'ringbearer';
|
||||
font-size: 16px;
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
#login_pane {
|
||||
background: linear-gradient(141deg, #000000 -5%, #a02d2a 120%, #000000 150%);
|
||||
padding-top: 5px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.28);
|
||||
width: 97%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#login_pane_header_container {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.login_pane_header {
|
||||
font-size: 14px;
|
||||
font-family: 'Segoe UI';
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login_container {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login_image {
|
||||
height: 85px;
|
||||
width: auto;
|
||||
display: block;
|
||||
border: 3px solid #000000;
|
||||
}
|
||||
|
||||
#login_container_right input,
|
||||
#login_container_right label {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login_container_left {
|
||||
padding: 10px 0 10px 10px;
|
||||
}
|
||||
|
||||
#login_container_right {
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login_field {
|
||||
font-size: 12px;
|
||||
font-family: 'Segoe UI';
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
/* new */
|
||||
display: table;
|
||||
}
|
||||
|
||||
#button_login {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Right Body Container *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
#right_pane {
|
||||
float: right;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
#content_container {
|
||||
border: 3px solid #a02d2a;
|
||||
height: calc(98% - 41.33px);
|
||||
width: 98%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#launcher-log {
|
||||
font-family: 'inconsolata';
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
background-color: #1a1b1c;
|
||||
border: none;
|
||||
outline: none;
|
||||
overflow-y: scroll;
|
||||
display: block;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
padding: 5px 10px;
|
||||
text-shadow: 0.01px 0.01px 0.01px #ffffff;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
#launcher-log::selection {
|
||||
background: rgba(160, 45, 42, .9);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#launcher-log::-webkit-scrollbar {
|
||||
background-color: #a02d2a;
|
||||
}
|
||||
|
||||
#launcher-log::-webkit-scrollbar-thumb:window-inactive,
|
||||
#launcher-log::-webkit-scrollbar-thumb {
|
||||
background: black
|
||||
}
|
||||
|
||||
.log_debug {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Login View *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
#login_view {
|
||||
background: rgba(0, 0, 0, 0.76);
|
||||
height: calc(100% - 90px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#login_content {
|
||||
width: 50%;
|
||||
height: 75%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#login_view #content_main {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#login_view #login_content_image {
|
||||
height: 125px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#login_view #content_main #right {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.login_input {
|
||||
background-color: #a02d2a;
|
||||
}
|
||||
|
||||
.login_input::-webkit-input-placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
BIN
app/assets/fonts/Avenir-Black.ttf
Normal file
BIN
app/assets/fonts/Avenir-BlackOblique.ttf
Normal file
BIN
app/assets/fonts/Avenir-Book.ttf
Normal file
BIN
app/assets/fonts/Avenir-BookOblique.ttf
Normal file
BIN
app/assets/fonts/Avenir-Heavy.ttf
Normal file
BIN
app/assets/fonts/Avenir-HeavyOblique.ttf
Normal file
BIN
app/assets/fonts/Avenir-Light.ttf
Normal file
BIN
app/assets/fonts/Avenir-LightOblique.ttf
Normal file
BIN
app/assets/fonts/Avenir-Medium.ttf
Normal file
BIN
app/assets/fonts/Avenir-MediumOblique.ttf
Normal file
BIN
app/assets/fonts/Avenir-Oblique.ttf
Normal file
BIN
app/assets/fonts/Avenir-Roman.ttf
Normal file
BIN
app/assets/images/LoadingSeal.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
app/assets/images/LoadingText.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
app/assets/images/SealCircle.ico
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
app/assets/images/SealCircle.png
Normal file
After Width: | Height: | Size: 20 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: 94 KiB |
BIN
app/assets/images/backgrounds/0.jpg
Normal file
After Width: | Height: | Size: 604 KiB |
BIN
app/assets/images/backgrounds/1.jpg
Normal file
After Width: | Height: | Size: 781 KiB |
BIN
app/assets/images/backgrounds/2.jpg
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
app/assets/images/backgrounds/3.jpg
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
app/assets/images/backgrounds/4.jpg
Normal file
After Width: | Height: | Size: 343 KiB |
BIN
app/assets/images/backgrounds/5.jpg
Normal file
After Width: | Height: | Size: 500 KiB |
BIN
app/assets/images/backgrounds/6.jpg
Normal file
After Width: | Height: | Size: 580 KiB |
BIN
app/assets/images/backgrounds/7.jpg
Normal file
After Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 4.0 KiB |
7
app/assets/images/icons/arrow.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 24.87 13.97">
|
||||
<defs>
|
||||
<style>.cls-1{fill:none;stroke:#FFF;stroke-width:2px;}</style>
|
||||
</defs>
|
||||
<title>arrow</title>
|
||||
<polyline class="cls-1" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
|
||||
</svg>
|
After Width: | Height: | Size: 298 B |
10
app/assets/images/icons/discord.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
||||
<defs>
|
||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
||||
<clipPath id="clip-path"><rect class="cls-1" x="36.42" y="44.23" width="68.52" height="48.96"/></clipPath>
|
||||
</defs>
|
||||
<title>discord</title>
|
||||
<g class="cls-2">
|
||||
<path d="M81.23,78.48a6.14,6.14,0,1,1,6.14-6.14,6.14,6.14,0,0,1-6.14,6.14M60,78.48a6.14,6.14,0,1,1,6.14-6.14A6.14,6.14,0,0,1,60,78.48M104.41,73c-.92-7.7-8.24-22.9-8.24-22.9A43,43,0,0,0,88,45.59a17.88,17.88,0,0,0-8.38-1.27l-.13,1.06a23.52,23.52,0,0,1,5.8,1.95,87.59,87.59,0,0,1,8.17,4.87s-10.32-5.63-22.27-5.63a51.32,51.32,0,0,0-23.2,5.63,87.84,87.84,0,0,1,8.17-4.87,23.57,23.57,0,0,1,5.8-1.95l-.13-1.06a17.88,17.88,0,0,0-8.38,1.27,42.84,42.84,0,0,0-8.21,4.56S37.87,65.35,37,73s-.37,11.54-.37,11.54,4.22,5.68,9.9,7.14,7.7,1.47,7.7,1.47l3.75-4.68a21.22,21.22,0,0,1-4.65-2A24.47,24.47,0,0,1,47.93,82S61.16,88.4,70.68,88.4c10,0,22.75-6.44,22.75-6.44a24.56,24.56,0,0,1-5.35,4.56,21.22,21.22,0,0,1-4.65,2l3.75,4.68s2,0,7.7-1.47,9.89-7.14,9.89-7.14.55-3.85-.37-11.54"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
9
app/assets/images/icons/instagram.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="504px" height="504px" viewBox="0 0 5040 5040" preserveAspectRatio="xMidYMid meet">
|
||||
<g id="layer101" fill="#000000" stroke="none">
|
||||
<path d="M1390 5024 c-163 -9 -239 -19 -315 -38 -281 -70 -477 -177 -660 -361 -184 -184 -292 -380 -361 -660 -43 -171 -53 -456 -53 -1445 0 -989 10 -1274 53 -1445 69 -280 177 -476 361 -660 184 -184 380 -292 660 -361 171 -43 456 -53 1445 -53 989 0 1274 10 1445 53 280 69 476 177 660 361 184 184 292 380 361 660 43 171 53 456 53 1445 0 989 -10 1274 -53 1445 -69 280 -177 476 -361 660 -184 184 -380 292 -660 361 -174 44 -454 53 -1470 52 -599 0 -960 -5 -1105 -14z m2230 -473 c58 -6 141 -18 185 -27 397 -78 638 -318 719 -714 37 -183 41 -309 41 -1290 0 -981 -4 -1107 -41 -1290 -81 -395 -319 -633 -714 -714 -183 -37 -309 -41 -1290 -41 -981 0 -1107 4 -1290 41 -397 81 -636 322 -714 719 -33 166 -38 296 -43 1100 -5 796 3 1203 27 1380 67 489 338 758 830 825 47 7 162 15 255 20 250 12 1907 4 2035 -9z"/>
|
||||
<path d="M2355 3819 c-307 -42 -561 -172 -780 -400 -244 -253 -359 -543 -359 -899 0 -361 116 -648 367 -907 262 -269 563 -397 937 -397 374 0 675 128 937 397 251 259 367 546 367 907 0 361 -116 648 -367 907 -197 203 -422 326 -690 378 -101 20 -317 27 -412 14z m400 -509 c275 -88 470 -284 557 -560 20 -65 23 -95 23 -230 0 -135 -3 -165 -23 -230 -88 -278 -284 -474 -562 -562 -65 -20 -95 -23 -230 -23 -135 0 -165 3 -230 23 -278 88 -474 284 -562 562 -20 65 -23 95 -23 230 0 135 3 165 23 230 73 230 219 403 427 507 134 67 212 83 390 79 111 -3 155 -8 210 -26z"/>
|
||||
<path d="M3750 1473 c-29 -11 -66 -38 -106 -77 -70 -71 -94 -126 -94 -221 0 -95 24 -150 94 -221 72 -71 126 -94 225 -94 168 0 311 143 311 311 0 99 -23 154 -94 225 -43 42 -76 66 -110 77 -61 21 -166 21 -226 0z"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
11
app/assets/images/icons/link.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
||||
<defs>
|
||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
||||
<clipPath id="clip-path"><rect class="cls-1" x="29.3" y="52.62" width="82.77" height="34.15"/></clipPath>
|
||||
</defs>
|
||||
<title>link</title>
|
||||
<g class="cls-2">
|
||||
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
|
||||
<path d="M66,73.88a3.85,3.85,0,0,0,1.73-.42,8.22,8.22,0,0,1-.94-3.76,8.36,8.36,0,0,1,8.35-8.35H95A8.35,8.35,0,1,1,95,78H85.8a21.51,21.51,0,0,1-6.65,8.72H95a17.07,17.07,0,0,0,0-34.15H75.13A17,17,0,0,0,58.59,73.88Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 875 B |
12
app/assets/images/icons/lock.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
||||
<defs>
|
||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#231f20;}</style>
|
||||
<clipPath id="clip-path">
|
||||
<rect class="cls-1" x="44.02" y="34.21" width="51.96" height="68.48"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<title>Lock</title>
|
||||
<g class="cls-2">
|
||||
<path class="cls-3" d="M86.16,54a16.38,16.38,0,1,0-32,0H44V102.7H96V54Zm-25.9-3.39a9.89,9.89,0,1,1,19.77,0A9.78,9.78,0,0,1,79.39,54H60.89A9.78,9.78,0,0,1,60.26,50.59ZM70,96.2a6.5,6.5,0,0,1-6.5-6.5,6.39,6.39,0,0,1,3.1-5.4V67h6.5V84.11a6.42,6.42,0,0,1,3.39,5.6A6.5,6.5,0,0,1,70,96.2Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 756 B |
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 |
14
app/assets/images/icons/news.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 141.36 137.43">
|
||||
<defs>
|
||||
<style>.cls-1{fill:#231f20;}.cls-2,.cls-3{fill:none;stroke-miterlimit:10;stroke-width:6px;}.cls-2{stroke:#231f20;}.cls-3{stroke:#000;}</style>
|
||||
</defs>
|
||||
<title>News</title>
|
||||
<rect class="cls-1" x="31.77" y="32.96" width="33.79" height="20.76"/>
|
||||
<path class="cls-2" d="M115.36,113.8H27.18a6.67,6.67,0,0,1-6.67-6.67V19.27H108.2V107.1a6.71,6.71,0,0,0,6.71,6.7h0a6.71,6.71,0,0,0,6.71-6.71v-75H108.15"/>
|
||||
<line class="cls-3" x1="73.75" y1="36.18" x2="97.14" y2="36.18"/>
|
||||
<line class="cls-3" x1="73.75" y1="50.22" x2="97.14" y2="50.22"/>
|
||||
<line class="cls-3" x1="31.66" y1="64.25" x2="97.14" y2="64.25"/>
|
||||
<line class="cls-3" x1="31.66" y1="78.28" x2="97.14" y2="78.28"/>
|
||||
<line class="cls-3" x1="31.66" y1="92.31" x2="97.14" y2="92.31"/>
|
||||
<line class="cls-3" x1="31.66" y1="92.31" x2="97.14" y2="92.31"/>
|
||||
</svg>
|
After Width: | Height: | Size: 959 B |
10
app/assets/images/icons/profile.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
||||
<defs>
|
||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
||||
<clipPath id="clip-path"><rect class="cls-1" x="45.51" y="44.33" width="55.14" height="59.33"/></clipPath>
|
||||
</defs>
|
||||
<title>Profile</title>
|
||||
<g class="cls-2">
|
||||
<path d="M86.77,58.12A13.79,13.79,0,1,0,73,71.91,13.79,13.79,0,0,0,86.77,58.12M97,103.67a3.41,3.41,0,0,0,3.39-3.84,27.57,27.57,0,0,0-54.61,0,3.41,3.41,0,0,0,3.39,3.84Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 602 B |
10
app/assets/images/icons/settings.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
||||
<defs>
|
||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
||||
<clipPath id="clip-path"><rect class="cls-1" x="45.65" y="42.62" width="49.58" height="52.43"/></clipPath>
|
||||
</defs>
|
||||
<title>settings</title>
|
||||
<g class="cls-2">
|
||||
<path d="M70.44,75a6.19,6.19,0,1,1,5.84-6.18A6,6,0,0,1,70.44,75M91.67,63.71h-5A18.4,18.4,0,0,0,85.19,60l3.48-3.68a3.93,3.93,0,0,0,0-5.32l-1.4-1.48a3.43,3.43,0,0,0-5,0l-3.48,3.68A16.34,16.34,0,0,0,75,51.59V46.38a3.68,3.68,0,0,0-3.56-3.76h-2a3.68,3.68,0,0,0-3.56,3.76v5.21a16.23,16.23,0,0,0-3.77,1.64l-3.48-3.68a3.43,3.43,0,0,0-5,0L52.21,51a3.93,3.93,0,0,0,0,5.32L55.69,60a18.21,18.21,0,0,0-1.48,3.67h-5a3.67,3.67,0,0,0-3.56,3.76v2.1a3.68,3.68,0,0,0,3.56,3.76h4.84a18.46,18.46,0,0,0,1.64,4.3l-3.48,3.68a3.93,3.93,0,0,0,0,5.32l1.4,1.48a3.43,3.43,0,0,0,5,0l3.48-3.68a16.36,16.36,0,0,0,3.77,1.64v5.21a3.67,3.67,0,0,0,3.56,3.76h2A3.67,3.67,0,0,0,75,91.29V86.08a16.48,16.48,0,0,0,3.77-1.64l3.48,3.68a3.43,3.43,0,0,0,5,0l1.4-1.48a3.93,3.93,0,0,0,0-5.32l-3.48-3.68a18.45,18.45,0,0,0,1.63-4.3h4.85a3.68,3.68,0,0,0,3.56-3.76v-2.1a3.67,3.67,0,0,0-3.56-3.76"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
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 |
13
app/assets/images/icons/twitter.svg
Normal file
After Width: | Height: | Size: 9.8 KiB |
10
app/assets/images/icons/youtube.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 141.36 137.43">
|
||||
<defs>
|
||||
<style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}</style>
|
||||
<clipPath id="clip-path"><rect class="cls-1" x="38.29" y="45.86" width="70.16" height="48.48"/></clipPath>
|
||||
</defs>
|
||||
<title>youtube</title>
|
||||
<g class="cls-2">
|
||||
<path d="M84.8,69.52,65.88,79.76V59.27Zm23.65.59c0-5.14-.79-17.63-3.94-20.57S99,45.86,73.37,45.86s-28,.73-31.14,3.68S38.29,65,38.29,70.11s.79,17.63,3.94,20.57,5.52,3.68,31.14,3.68,28-.74,31.14-3.68,3.94-15.42,3.94-20.57"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 654 B |
Before Width: | Height: | Size: 1012 KiB |
40
app/assets/images/lotip22.svg
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
app/assets/images/minecraft.icns
Normal file
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 606 KiB |
Before Width: | Height: | Size: 227 KiB |
@ -1,239 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const request = require('request')
|
||||
const path = require('path')
|
||||
const mkpath = require('mkdirp');
|
||||
const async = require('async')
|
||||
const crypto = require('crypto')
|
||||
const Library = require('./library.js')
|
||||
const {BrowserWindow} = require('electron')
|
||||
|
||||
/**
|
||||
* PHASING THIS OUT, WILL BE REMOVED WHEN ASSET GUARD MODULE IS COMPLETE!
|
||||
*/
|
||||
|
||||
function Asset(from, to, size, hash){
|
||||
this.from = from
|
||||
this.to = to
|
||||
this.size = size
|
||||
this.hash = hash
|
||||
}
|
||||
|
||||
function AssetIndex(id, sha1, size, url, totalSize){
|
||||
this.id = id
|
||||
this.sha1 = sha1
|
||||
this.size = size
|
||||
this.url = url
|
||||
this.totalSize = totalSize
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will download the version index data and read it into a Javascript
|
||||
* Object. This object will then be returned.
|
||||
*/
|
||||
parseVersionData = function(version, basePath){
|
||||
const name = version + '.json'
|
||||
const baseURL = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
|
||||
const versionPath = path.join(basePath, 'versions', version)
|
||||
|
||||
return new Promise(function(fulfill, reject){
|
||||
request.head(baseURL, function(err, res, body){
|
||||
console.log('Preparing download of ' + version + ' assets.')
|
||||
mkpath.sync(versionPath)
|
||||
const stream = request(baseURL).pipe(fs.createWriteStream(path.join(versionPath, name)))
|
||||
stream.on('finish', function(){
|
||||
fulfill(JSON.parse(fs.readFileSync(path.join(versionPath, name))))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the client for version. This file is 'client.jar' although
|
||||
* it must be renamed to '{version}'.jar.
|
||||
*/
|
||||
downloadClient = function(versionData, basePath){
|
||||
const dls = versionData['downloads']
|
||||
const clientData = dls['client']
|
||||
const url = clientData['url']
|
||||
const size = clientData['size']
|
||||
const version = versionData['id']
|
||||
const sha1 = clientData['sha1']
|
||||
const targetPath = path.join(basePath, 'versions', version)
|
||||
const targetFile = version + '.jar'
|
||||
|
||||
if(!validateLocalIntegrity(path.join(targetPath, targetFile), 'sha1', sha1)){
|
||||
request.head(url, function(err, res, body){
|
||||
console.log('Downloading ' + version + ' client..')
|
||||
mkpath.sync(targetPath)
|
||||
const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, targetFile)))
|
||||
stream.on('finish', function(){
|
||||
console.log('Finished downloading ' + version + ' client.')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
downloadLogConfig = function(versionData, basePath){
|
||||
const logging = versionData['logging']
|
||||
const client = logging['client']
|
||||
const file = client['file']
|
||||
const version = versionData['id']
|
||||
const sha1 = file['sha1']
|
||||
const targetPath = path.join(basePath, 'assets', 'log_configs')
|
||||
const name = file['id']
|
||||
const url = file['url']
|
||||
|
||||
if(!validateLocalIntegrity(path.join(targetPath, name), 'sha1', sha1)){
|
||||
request.head(url, function(err, res, body){
|
||||
console.log('Downloading ' + version + ' log config..')
|
||||
mkpath.sync(targetPath)
|
||||
const stream = request(url).pipe(fs.createWriteStream(path.join(targetPath, name)))
|
||||
stream.on('finish', function(){
|
||||
console.log('Finished downloading ' + version + ' log config..')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
downloadLibraries = function(versionData, basePath){
|
||||
const libArr = versionData['libraries']
|
||||
const libPath = path.join(basePath, 'libraries')
|
||||
|
||||
let win = BrowserWindow.getFocusedWindow()
|
||||
const libDlQueue = []
|
||||
let dlSize = 0
|
||||
|
||||
//Check validity of each library. If the hashs don't match, download the library.
|
||||
libArr.forEach(function(lib, index){
|
||||
if(Library.validateRules(lib.rules)){
|
||||
let artifact = null
|
||||
if(lib.natives == null){
|
||||
artifact = lib.downloads.artifact
|
||||
} else {
|
||||
artifact = lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
|
||||
}
|
||||
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
|
||||
if(!validateLocalIntegrity(libItm.to, 'sha1', libItm.sha1)){
|
||||
dlSize += libItm.size
|
||||
libDlQueue.push(libItm)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let acc = 0;
|
||||
|
||||
//Download all libraries that failed validation.
|
||||
async.eachLimit(libDlQueue, 1, function(lib, cb){
|
||||
mkpath.sync(path.join(lib.to, '..'))
|
||||
let req = request(lib.from)
|
||||
let writeStream = fs.createWriteStream(lib.to)
|
||||
req.pipe(writeStream)
|
||||
|
||||
req.on('data', function(chunk){
|
||||
acc += chunk.length
|
||||
//console.log('Progress', acc/dlSize)
|
||||
win.setProgressBar(acc/dlSize)
|
||||
})
|
||||
writeStream.on('close', cb)
|
||||
}, function(err){
|
||||
if(err){
|
||||
console.log('A library failed to process');
|
||||
} else {
|
||||
console.log('All libraries have been processed successfully');
|
||||
}
|
||||
win.setProgressBar(-1)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index url, this function will asynchonously download the
|
||||
* assets associated with that version.
|
||||
*/
|
||||
downloadAssets = function(versionData, basePath){
|
||||
//Asset index constants.
|
||||
const assetIndex = versionData.assetIndex
|
||||
const indexURL = assetIndex.url
|
||||
const gameVersion = versionData.id
|
||||
const assetVersion = assetIndex.id
|
||||
const name = assetVersion + '.json'
|
||||
|
||||
//Asset constants
|
||||
const resourceURL = 'http://resources.download.minecraft.net/'
|
||||
const localPath = path.join(basePath, 'assets')
|
||||
const indexPath = path.join(localPath, 'indexes')
|
||||
const objectPath = path.join(localPath, 'objects')
|
||||
|
||||
let win = BrowserWindow.getFocusedWindow()
|
||||
|
||||
const assetIndexLoc = path.join(indexPath, name)
|
||||
/*if(!fs.existsSync(assetIndexLoc)){
|
||||
|
||||
}*/
|
||||
console.log('Downloading ' + gameVersion + ' asset index.')
|
||||
mkpath.sync(indexPath)
|
||||
const stream = request(indexURL).pipe(fs.createWriteStream(assetIndexLoc))
|
||||
stream.on('finish', function() {
|
||||
const data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
|
||||
const assetDlQueue = []
|
||||
let dlSize = 0;
|
||||
Object.keys(data.objects).forEach(function(key, index){
|
||||
const ob = data.objects[key]
|
||||
const hash = ob.hash
|
||||
const assetName = path.join(hash.substring(0, 2), hash)
|
||||
const urlName = hash.substring(0, 2) + "/" + hash
|
||||
const ast = new Asset(resourceURL + urlName, path.join(objectPath, assetName), ob.size, String(ob.hash))
|
||||
if(!validateLocalIntegrity(ast.to, 'sha1', ast.hash)){
|
||||
dlSize += ast.size
|
||||
assetDlQueue.push(ast)
|
||||
}
|
||||
})
|
||||
|
||||
let acc = 0;
|
||||
async.eachLimit(assetDlQueue, 5, function(asset, cb){
|
||||
mkpath.sync(path.join(asset.to, ".."))
|
||||
let req = request(asset.from)
|
||||
let writeStream = fs.createWriteStream(asset.to)
|
||||
req.pipe(writeStream)
|
||||
req.on('data', function(chunk){
|
||||
acc += chunk.length
|
||||
console.log('Progress', acc/dlSize)
|
||||
win.setProgressBar(acc/dlSize)
|
||||
})
|
||||
writeStream.on('close', cb)
|
||||
}, function(err){
|
||||
if(err){
|
||||
console.log('An asset failed to process');
|
||||
} else {
|
||||
console.log('All assets have been processed successfully');
|
||||
}
|
||||
win.setProgressBar(-1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
validateLocalIntegrity = function(filePath, algo, hash){
|
||||
if(fs.existsSync(filePath)){
|
||||
let fileName = path.basename(filePath)
|
||||
console.log('Validating integrity of local file', fileName)
|
||||
let shasum = crypto.createHash(algo)
|
||||
let content = fs.readFileSync(filePath)
|
||||
shasum.update(content)
|
||||
let localhash = shasum.digest('hex')
|
||||
if(localhash === hash){
|
||||
console.log('Hash value of ' + fileName + ' matches the index hash, woo!')
|
||||
return true
|
||||
} else {
|
||||
console.log('Hash value of ' + fileName + ' (' + localhash + ')' + ' does not match the index hash. Redownloading..')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseVersionData,
|
||||
downloadClient,
|
||||
downloadLogConfig,
|
||||
downloadLibraries,
|
||||
downloadAssets
|
||||
}
|
@ -1,856 +0,0 @@
|
||||
/**
|
||||
* AssetGuard
|
||||
*
|
||||
* This module aims to provide a comprehensive and stable method for processing
|
||||
* and downloading game assets for the WesterosCraft server. A central object
|
||||
* stores download meta for several identifiers (categories). This meta data
|
||||
* is initially empty until one of the module's processing functions are called.
|
||||
* That function will process the corresponding asset index and validate any exisitng
|
||||
* local files. If a file is missing or fails validation, it will be placed into an
|
||||
* array which acts as a queue. This queue is wrapped in a download tracker object
|
||||
* so that essential information can be cached. The download tracker object is then
|
||||
* assigned as the value of the identifier in the central object. These download
|
||||
* trackers will remain idle until an async process is started to process them.
|
||||
*
|
||||
* Once the async process is started, any enqueued assets will be downloaded. The central
|
||||
* object will emit events throughout the download whose name correspond to the identifier
|
||||
* being processed. For example, if the 'assets' identifier was being processed, whenever
|
||||
* the download stream recieves data, the event 'assetsdlprogress' will be emitted off of
|
||||
* the central object instance. This can be listened to by external modules allowing for
|
||||
* categorical tracking of the downloading process.
|
||||
*
|
||||
* @module assetguard
|
||||
*/
|
||||
// Requirements
|
||||
const fs = require('fs')
|
||||
const request = require('request')
|
||||
const path = require('path')
|
||||
const mkpath = require('mkdirp');
|
||||
const async = require('async')
|
||||
const crypto = require('crypto')
|
||||
const AdmZip = require('adm-zip')
|
||||
const child_process = require('child_process')
|
||||
const EventEmitter = require('events')
|
||||
const {remote} = require('electron')
|
||||
|
||||
// Classes
|
||||
|
||||
/** Class representing a base asset. */
|
||||
class Asset{
|
||||
/**
|
||||
* Create an asset.
|
||||
*
|
||||
* @param {any} id - id of the asset.
|
||||
* @param {String} hash - hash value of the asset.
|
||||
* @param {Number} size - size in bytes of the asset.
|
||||
* @param {String} from - url where the asset can be found.
|
||||
* @param {String} to - absolute local file path of the asset.
|
||||
*/
|
||||
constructor(id, hash, size, from, to){
|
||||
this.id = id
|
||||
this.hash = hash
|
||||
this.size = size
|
||||
this.from = from
|
||||
this.to = to
|
||||
}
|
||||
}
|
||||
|
||||
/** Class representing a mojang library. */
|
||||
class Library extends Asset{
|
||||
|
||||
/**
|
||||
* Converts the process.platform OS names to match mojang's OS names.
|
||||
*/
|
||||
static mojangFriendlyOS(){
|
||||
const opSys = process.platform
|
||||
if (opSys === 'darwin') {
|
||||
return 'osx';
|
||||
} else if (opSys === 'win32'){
|
||||
return 'windows';
|
||||
} else if (opSys === 'linux'){
|
||||
return 'linux';
|
||||
} else {
|
||||
return 'unknown_os';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a library is valid for download on a particular OS, following
|
||||
* the rule format specified in the mojang version data index. If the allow property has
|
||||
* an OS specified, then the library can ONLY be downloaded on that OS. If the disallow
|
||||
* property has instead specified an OS, the library can be downloaded on any OS EXCLUDING
|
||||
* the one specified.
|
||||
*
|
||||
* @param {Object} rules - the Library's download rules.
|
||||
* @returns {Boolean} - true if the Library follows the specified rules, otherwise false.
|
||||
*/
|
||||
static validateRules(rules){
|
||||
if(rules == null) return true
|
||||
|
||||
let result = true
|
||||
rules.forEach(function(rule){
|
||||
const action = rule['action']
|
||||
const osProp = rule['os']
|
||||
if(action != null){
|
||||
if(osProp != null){
|
||||
const osName = osProp['name']
|
||||
const osMoj = Library.mojangFriendlyOS()
|
||||
if(action === 'allow'){
|
||||
result = osName === osMoj
|
||||
return
|
||||
} else if(action === 'disallow'){
|
||||
result = osName !== osMoj
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class DistroModule extends Asset {
|
||||
|
||||
/**
|
||||
* Create a DistroModule. This is for processing,
|
||||
* not equivalent to the module objects in the
|
||||
* distro index.
|
||||
*
|
||||
* @param {any} id - id of the asset.
|
||||
* @param {String} hash - hash value of the asset.
|
||||
* @param {Number} size - size in bytes of the asset.
|
||||
* @param {String} from - url where the asset can be found.
|
||||
* @param {String} to - absolute local file path of the asset.
|
||||
* @param {String} type - the module type.
|
||||
*/
|
||||
constructor(id, hash, size, from, to, type){
|
||||
super(id, hash, size, from, to)
|
||||
this.type = type
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a download tracker. This is used to store meta data
|
||||
* about a download queue, including the queue itself.
|
||||
*/
|
||||
class DLTracker {
|
||||
/**
|
||||
* Create a DLTracker
|
||||
*
|
||||
* @param {Array.<Asset>} dlqueue - an array containing assets queued for download.
|
||||
* @param {Number} dlsize - the combined size of each asset in the download queue array.
|
||||
* @param {function(Asset)} callback - optional callback which is called when an asset finishes downloading.
|
||||
*/
|
||||
constructor(dlqueue, dlsize, callback = null){
|
||||
this.dlqueue = dlqueue
|
||||
this.dlsize = dlsize
|
||||
this.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Central object class used for control flow. This object stores data about
|
||||
* categories of downloads. Each category is assigned an identifier with a
|
||||
* DLTracker object as its value. Combined information is also stored, such as
|
||||
* the total size of all the queued files in each category. This event is used
|
||||
* to emit events so that external modules can listen into processing done in
|
||||
* this module.
|
||||
*/
|
||||
class AssetGuard extends EventEmitter{
|
||||
/**
|
||||
* AssetGuard class should only ever have one instance which is defined in
|
||||
* this module. On creation the object's properties are never-null default
|
||||
* values. Each identifier is resolved to an empty DLTracker.
|
||||
*/
|
||||
constructor(){
|
||||
super()
|
||||
this.totaldlsize = 0;
|
||||
this.progress = 0;
|
||||
this.assets = new DLTracker([], 0)
|
||||
this.libraries = new DLTracker([], 0)
|
||||
this.files = new DLTracker([], 0)
|
||||
this.forge = new DLTracker([], 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global static final instance of AssetGuard
|
||||
*/
|
||||
const instance = new AssetGuard()
|
||||
|
||||
// Utility Functions
|
||||
|
||||
/**
|
||||
* Resolve an artifact id into a path. For example, on windows
|
||||
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
|
||||
* net\minecraftforge\forge\1.11.2-13.20.0.2282\forge-1.11.2-13.20.0.2282.jar
|
||||
*
|
||||
* @param {String} artifactid - the artifact id string.
|
||||
* @param {String} extension - the extension of the file at the resolved path.
|
||||
* @returns {String} - the resolved relative path from the artifact id.
|
||||
*/
|
||||
function _resolvePath(artifactid, extension){
|
||||
let ps = artifactid.split(':')
|
||||
let cs = ps[0].split('.')
|
||||
|
||||
cs.push(ps[1])
|
||||
cs.push(ps[2])
|
||||
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
|
||||
|
||||
return path.join.apply(path, cs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an artifact id into a URL. For example,
|
||||
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
|
||||
* net/minecraftforge/forge/1.11.2-13.20.0.2282/forge-1.11.2-13.20.0.2282.jar
|
||||
*
|
||||
* @param {String} artifactid - the artifact id string.
|
||||
* @param {String} extension - the extension of the file at the resolved url.
|
||||
* @returns {String} - the resolved relative URL from the artifact id.
|
||||
*/
|
||||
function _resolveURL(artifactid, extension){
|
||||
let ps = artifactid.split(':')
|
||||
let cs = ps[0].split('.')
|
||||
|
||||
cs.push(ps[1])
|
||||
cs.push(ps[2])
|
||||
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
|
||||
|
||||
return cs.join('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the hash for a file using the specified algorithm.
|
||||
*
|
||||
* @param {Buffer} buf - the buffer containing file data.
|
||||
* @param {String} algo - the hash algorithm.
|
||||
* @returns {String} - the calculated hash in hex.
|
||||
*/
|
||||
function _calculateHash(buf, algo){
|
||||
return crypto.createHash(algo).update(buf).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to parse a checksums file. This is specifically designed for
|
||||
* the checksums.sha1 files found inside the forge scala dependencies.
|
||||
*
|
||||
* @param {String} content - the string content of the checksums file.
|
||||
* @returns {Object} - an object with keys being the file names, and values being the hashes.
|
||||
*/
|
||||
function _parseChecksumsFile(content){
|
||||
let finalContent = {}
|
||||
let lines = content.split('\n')
|
||||
for(let i=0; i<lines.length; i++){
|
||||
let bits = lines[i].split(' ')
|
||||
if(bits[1] == null) {
|
||||
continue
|
||||
}
|
||||
finalContent[bits[1]] = bits[0]
|
||||
}
|
||||
return finalContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a file exists and matches a given hash value.
|
||||
*
|
||||
* @param {String} filePath - the path of the file to validate.
|
||||
* @param {String} algo - the hash algorithm to check against.
|
||||
* @param {String} hash - the existing hash to check against.
|
||||
* @returns {Boolean} - true if the file exists and calculated hash matches the given hash, otherwise false.
|
||||
*/
|
||||
function _validateLocal(filePath, algo, hash){
|
||||
//No hash provided, have to assume it's good.
|
||||
if(hash == null){
|
||||
return true
|
||||
}
|
||||
if(fs.existsSync(filePath)){
|
||||
let fileName = path.basename(filePath)
|
||||
let buf = fs.readFileSync(filePath)
|
||||
let calcdhash = _calculateHash(buf, algo)
|
||||
return calcdhash === hash
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a file in the style used by forge's version index.
|
||||
*
|
||||
* @param {String} filePath - the path of the file to validate.
|
||||
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
|
||||
* @returns {Boolean} - true if the file exists and the hashes match, otherwise false.
|
||||
*/
|
||||
function _validateForgeChecksum(filePath, checksums){
|
||||
if(fs.existsSync(filePath)){
|
||||
if(checksums == null || checksums.length === 0){
|
||||
return true
|
||||
}
|
||||
let buf = fs.readFileSync(filePath)
|
||||
let calcdhash = _calculateHash(buf, 'sha1')
|
||||
let valid = checksums.includes(calcdhash)
|
||||
if(!valid && filePath.endsWith('.jar')){
|
||||
valid = _validateForgeJar(filePath, checksums)
|
||||
}
|
||||
return valid
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a forge jar file dependency who declares a checksums.sha1 file.
|
||||
* This can be an expensive task as it usually requires that we calculate thousands
|
||||
* of hashes.
|
||||
*
|
||||
* @param {Buffer} buf - the buffer of the jar file.
|
||||
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
|
||||
* @returns {Boolean} - true if all hashes declared in the checksums.sha1 file match the actual hashes.
|
||||
*/
|
||||
function _validateForgeJar(buf, checksums){
|
||||
|
||||
const hashes = {}
|
||||
let expected = {}
|
||||
|
||||
const zip = new AdmZip(buf)
|
||||
const zipEntries = zip.getEntries()
|
||||
|
||||
//First pass
|
||||
for(let i=0; i<zipEntries.length; i++){
|
||||
let entry = zipEntries[i]
|
||||
if(entry.entryName === 'checksums.sha1'){
|
||||
expected = _parseChecksumsFile(zip.readAsText(entry))
|
||||
}
|
||||
hashes[entry.entryName] = _calculateHash(entry.getData(), 'sha1')
|
||||
}
|
||||
|
||||
if(!checksums.includes(hashes['checksums.sha1'])){
|
||||
return false
|
||||
}
|
||||
|
||||
//Check against expected
|
||||
const expectedEntries = Object.keys(expected)
|
||||
for(let i=0; i<expectedEntries.length; i++){
|
||||
if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and unpacks a file from .pack.xz format.
|
||||
*
|
||||
* @param {Array.<String>} filePaths - The paths of the files to be extracted and unpacked.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the extraction has completed.
|
||||
*/
|
||||
function _extractPackXZ(filePaths){
|
||||
return new Promise(function(fulfill, reject){
|
||||
const libPath = path.join(__dirname, '..', 'libraries', 'java', 'PackXZExtract.jar')
|
||||
const filePath = filePaths.join(',')
|
||||
const child = child_process.spawn('C:\\Program Files\\Java\\jre1.8.0_131\\bin\\javaw.exe', ['-jar', libPath, '-packxz', filePath])
|
||||
child.stdout.on('data', (data) => {
|
||||
//console.log('PackXZExtract:', data.toString('utf8'))
|
||||
})
|
||||
child.stderr.on('data', (data) => {
|
||||
//console.log('PackXZExtract:', data.toString('utf8'))
|
||||
})
|
||||
child.on('close', (code, signal) => {
|
||||
//console.log('PackXZExtract: Exited with code', code)
|
||||
fulfill()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function _finalizeForgeAsset(asset, basePath){
|
||||
return new Promise(function(fulfill, reject){
|
||||
fs.readFile(asset.to, (err, data) => {
|
||||
const zip = new AdmZip(data)
|
||||
const zipEntries = zip.getEntries()
|
||||
|
||||
for(let i=0; i<zipEntries.length; i++){
|
||||
if(zipEntries[i].entryName === 'version.json'){
|
||||
const forgeVersion = JSON.parse(zip.readAsText(zipEntries[i]))
|
||||
const versionPath = path.join(basePath, 'versions', forgeVersion.id)
|
||||
const versionFile = path.join(versionPath, forgeVersion.id + '.json')
|
||||
if(!fs.existsSync(versionFile)){
|
||||
mkpath.sync(versionPath)
|
||||
fs.writeFileSync(path.join(versionPath, forgeVersion.id + '.json'), zipEntries[i].getData())
|
||||
fulfill(forgeVersion)
|
||||
} else {
|
||||
fulfill(JSON.parse(fs.readFileSync(versionFile, 'utf-8')))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate an async download process for an AssetGuard DLTracker.
|
||||
*
|
||||
* @param {String} identifier - the identifier of the AssetGuard DLTracker.
|
||||
* @param {Number} limit - optional. The number of async processes to run in parallel.
|
||||
* @returns {Boolean} - true if the process began, otherwise false.
|
||||
*/
|
||||
function startAsyncProcess(identifier, limit = 5){
|
||||
let win = remote.getCurrentWindow()
|
||||
|
||||
let acc = 0
|
||||
const concurrentDlTracker = instance[identifier]
|
||||
const concurrentDlQueue = concurrentDlTracker.dlqueue.slice(0)
|
||||
if(concurrentDlQueue.length === 0){
|
||||
return false
|
||||
} else {
|
||||
console.log(instance.progress)
|
||||
async.eachLimit(concurrentDlQueue, limit, function(asset, cb){
|
||||
let count = 0;
|
||||
mkpath.sync(path.join(asset.to, ".."))
|
||||
let req = request(asset.from)
|
||||
req.pause()
|
||||
req.on('response', (resp) => {
|
||||
if(resp.statusCode === 200){
|
||||
let writeStream = fs.createWriteStream(asset.to)
|
||||
writeStream.on('close', () => {
|
||||
//console.log('DLResults ' + asset.size + ' ' + count + ' ', asset.size === count)
|
||||
if(concurrentDlTracker.callback != null){
|
||||
concurrentDlTracker.callback.apply(concurrentDlTracker, [asset])
|
||||
}
|
||||
cb()
|
||||
})
|
||||
req.pipe(writeStream)
|
||||
req.resume()
|
||||
} else {
|
||||
req.abort()
|
||||
console.log('Failed to download ' + asset.from + '. Response code', resp.statusCode)
|
||||
instance.progress += asset.size*1
|
||||
win.setProgressBar(instance.progress/instance.totaldlsize)
|
||||
cb()
|
||||
}
|
||||
})
|
||||
req.on('data', function(chunk){
|
||||
count += chunk.length
|
||||
instance.progress += chunk.length
|
||||
acc += chunk.length
|
||||
instance.emit(identifier + 'dlprogress', acc)
|
||||
//console.log(identifier + ' Progress', acc/instance[identifier].dlsize)
|
||||
win.setProgressBar(instance.progress/instance.totaldlsize)
|
||||
})
|
||||
}, function(err){
|
||||
if(err){
|
||||
instance.emit(identifier + 'dlerror')
|
||||
console.log('An item in ' + identifier + ' failed to process');
|
||||
} else {
|
||||
instance.emit(identifier + 'dlcomplete')
|
||||
console.log('All ' + identifier + ' have been processed successfully')
|
||||
}
|
||||
instance.totaldlsize -= instance[identifier].dlsize
|
||||
instance.progress -= instance[identifier].dlsize
|
||||
instance[identifier] = new DLTracker([], 0)
|
||||
if(instance.totaldlsize === 0) {
|
||||
win.setProgressBar(-1)
|
||||
instance.emit('dlcomplete')
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Validation Functions
|
||||
|
||||
/**
|
||||
* Loads the version data for a given minecraft version.
|
||||
*
|
||||
* @param {String} version - the game version for which to load the index data.
|
||||
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||
* @param {Boolean} force - optional. If true, the version index will be downloaded even if it exists locally. Defaults to false.
|
||||
* @returns {Promise.<Object>} - Promise which resolves to the version data object.
|
||||
*/
|
||||
function loadVersionData(version, basePath, force = false){
|
||||
return new Promise(function(fulfill, reject){
|
||||
const name = version + '.json'
|
||||
const url = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
|
||||
const versionPath = path.join(basePath, 'versions', version)
|
||||
const versionFile = path.join(versionPath, name)
|
||||
if(!fs.existsSync(versionFile) || force){
|
||||
//This download will never be tracked as it's essential and trivial.
|
||||
request.head(url, function(err, res, body){
|
||||
console.log('Preparing download of ' + version + ' assets.')
|
||||
mkpath.sync(versionPath)
|
||||
const stream = request(url).pipe(fs.createWriteStream(versionFile))
|
||||
stream.on('finish', function(){
|
||||
fulfill(JSON.parse(fs.readFileSync(versionFile)))
|
||||
})
|
||||
})
|
||||
} else {
|
||||
fulfill(JSON.parse(fs.readFileSync(versionFile)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Public asset validation function. This function will handle the validation of assets.
|
||||
* It will parse the asset index specified in the version data, analyzing each
|
||||
* asset entry. In this analysis it will check to see if the local file exists and is valid.
|
||||
* If not, it will be added to the download queue for the 'assets' identifier.
|
||||
*
|
||||
* @param {Object} versionData - the version data for the assets.
|
||||
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
function validateAssets(versionData, basePath, force = false){
|
||||
return new Promise(function(fulfill, reject){
|
||||
_assetChainIndexData(versionData, basePath, force).then(() => {
|
||||
fulfill()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//Chain the asset tasks to provide full async. The below functions are private.
|
||||
/**
|
||||
* Private function used to chain the asset validation process. This function retrieves
|
||||
* the index data.
|
||||
* @param {Object} versionData
|
||||
* @param {String} basePath
|
||||
* @param {Boolean} force
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
function _assetChainIndexData(versionData, basePath, force = false){
|
||||
return new Promise(function(fulfill, reject){
|
||||
//Asset index constants.
|
||||
const assetIndex = versionData.assetIndex
|
||||
const name = assetIndex.id + '.json'
|
||||
const indexPath = path.join(basePath, 'assets', 'indexes')
|
||||
const assetIndexLoc = path.join(indexPath, name)
|
||||
|
||||
let data = null
|
||||
if(!fs.existsSync(assetIndexLoc) || force){
|
||||
console.log('Downloading ' + versionData.id + ' asset index.')
|
||||
mkpath.sync(indexPath)
|
||||
const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc))
|
||||
stream.on('finish', function() {
|
||||
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
|
||||
_assetChainValidateAssets(versionData, basePath, data).then(() => {
|
||||
fulfill()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
|
||||
_assetChainValidateAssets(versionData, basePath, data).then(() => {
|
||||
fulfill()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Private function used to chain the asset validation process. This function processes
|
||||
* the assets and enqueues missing or invalid files.
|
||||
* @param {Object} versionData
|
||||
* @param {String} basePath
|
||||
* @param {Boolean} force
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
function _assetChainValidateAssets(versionData, basePath, indexData){
|
||||
return new Promise(function(fulfill, reject){
|
||||
|
||||
//Asset constants
|
||||
const resourceURL = 'http://resources.download.minecraft.net/'
|
||||
const localPath = path.join(basePath, 'assets')
|
||||
const indexPath = path.join(localPath, 'indexes')
|
||||
const objectPath = path.join(localPath, 'objects')
|
||||
|
||||
const assetDlQueue = []
|
||||
let dlSize = 0;
|
||||
//const objKeys = Object.keys(data.objects)
|
||||
async.forEachOfLimit(indexData.objects, 10, function(value, key, cb){
|
||||
const hash = value.hash
|
||||
const assetName = path.join(hash.substring(0, 2), hash)
|
||||
const urlName = hash.substring(0, 2) + "/" + hash
|
||||
const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName))
|
||||
if(!_validateLocal(ast.to, 'sha1', ast.hash)){
|
||||
dlSize += (ast.size*1)
|
||||
assetDlQueue.push(ast)
|
||||
}
|
||||
cb()
|
||||
}, function(err){
|
||||
instance.assets = new DLTracker(assetDlQueue, dlSize)
|
||||
instance.totaldlsize += dlSize*1
|
||||
fulfill()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Public library validation function. This function will handle the validation of libraries.
|
||||
* It will parse the version data, analyzing each library entry. In this analysis, it will
|
||||
* check to see if the local file exists and is valid. If not, it will be added to the download
|
||||
* queue for the 'libraries' identifier.
|
||||
*
|
||||
* @param {Object} versionData - the version data for the assets.
|
||||
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
function validateLibraries(versionData, basePath){
|
||||
return new Promise(function(fulfill, reject){
|
||||
|
||||
const libArr = versionData.libraries
|
||||
const libPath = path.join(basePath, 'libraries')
|
||||
|
||||
const libDlQueue = []
|
||||
let dlSize = 0
|
||||
|
||||
//Check validity of each library. If the hashs don't match, download the library.
|
||||
async.eachLimit(libArr, 5, function(lib, cb){
|
||||
if(Library.validateRules(lib.rules)){
|
||||
let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
|
||||
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
|
||||
if(!_validateLocal(libItm.to, 'sha1', libItm.hash)){
|
||||
dlSize += (libItm.size*1)
|
||||
libDlQueue.push(libItm)
|
||||
}
|
||||
}
|
||||
cb()
|
||||
}, function(err){
|
||||
instance.libraries = new DLTracker(libDlQueue, dlSize)
|
||||
instance.totaldlsize += dlSize*1
|
||||
fulfill()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Public miscellaneous mojang file validation function. These files will be enqueued under
|
||||
* the 'files' identifier.
|
||||
*
|
||||
* @param {Object} versionData - the version data for the assets.
|
||||
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
function validateMiscellaneous(versionData, basePath){
|
||||
return new Promise(async function(fulfill, reject){
|
||||
await validateClient(versionData, basePath)
|
||||
await validateLogConfig(versionData, basePath)
|
||||
fulfill()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate client file - artifact renamed from client.jar to '{version}'.jar.
|
||||
*
|
||||
* @param {Object} versionData - the version data for the assets.
|
||||
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
function validateClient(versionData, basePath, force = false){
|
||||
return new Promise(function(fulfill, reject){
|
||||
const clientData = versionData.downloads.client
|
||||
const version = versionData.id
|
||||
const targetPath = path.join(basePath, 'versions', version)
|
||||
const targetFile = version + '.jar'
|
||||
|
||||
let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile))
|
||||
|
||||
if(!_validateLocal(client.to, 'sha1', client.hash) || force){
|
||||
instance.files.dlqueue.push(client)
|
||||
instance.files.dlsize += client.size*1
|
||||
fulfill()
|
||||
} else {
|
||||
fulfill()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate log config.
|
||||
*
|
||||
* @param {Object} versionData - the version data for the assets.
|
||||
* @param {String} basePath - the absolute file path which will be prepended to the given relative paths.
|
||||
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
|
||||
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
|
||||
*/
|
||||
function validateLogConfig(versionData, basePath){
|
||||
return new Promise(function(fulfill, reject){
|
||||
const client = versionData.logging.client
|
||||
const file = client.file
|
||||
const targetPath = path.join(basePath, 'assets', 'log_configs')
|
||||
|
||||
let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id))
|
||||
|
||||
if(!_validateLocal(logConfig.to, 'sha1', logConfig.hash)){
|
||||
instance.files.dlqueue.push(logConfig)
|
||||
instance.files.dlsize += client.size*1
|
||||
fulfill()
|
||||
} else {
|
||||
fulfill()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function validateDistribution(serverpackid, basePath){
|
||||
return new Promise(function(fulfill, reject){
|
||||
_chainValidateDistributionIndex(basePath).then((value) => {
|
||||
let servers = value.servers
|
||||
let serv = null
|
||||
for(let i=0; i<servers.length; i++){
|
||||
if(servers[i].id === serverpackid){
|
||||
serv = servers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
instance.forge = _parseDistroModules(serv.modules, basePath, serv.mc_version)
|
||||
//Correct our workaround here.
|
||||
let decompressqueue = instance.forge.callback
|
||||
instance.forge.callback = function(asset){
|
||||
if(asset.to.toLowerCase().endsWith('.pack.xz')){
|
||||
_extractPackXZ([asset.to])
|
||||
}
|
||||
if(asset.type === 'forge-hosted' || asset.type === 'forge'){
|
||||
_finalizeForgeAsset(asset, basePath)
|
||||
}
|
||||
}
|
||||
instance.totaldlsize += instance.forge.dlsize*1
|
||||
fulfill()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//TODO The distro index should be downloaded in the 'pre-loader'. This is because
|
||||
//we will eventually NEED the index to generate the server list on the ui.
|
||||
function _chainValidateDistributionIndex(basePath){
|
||||
return new Promise(function(fulfill, reject){
|
||||
//const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
|
||||
const targetFile = path.join(basePath, 'westeroscraft.json')
|
||||
|
||||
//TEMP WORKAROUND TO TEST WHILE THIS IS NOT HOSTED
|
||||
fs.readFile(path.join(basePath, '..', 'app', 'assets', 'westeroscraft.json'), 'utf-8', (err, data) => {
|
||||
fulfill(JSON.parse(data))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function _parseDistroModules(modules, basePath, version){
|
||||
let alist = []
|
||||
let asize = 0;
|
||||
//This may be removed soon, considering the most efficient way to extract.
|
||||
let decompressqueue = []
|
||||
for(let i=0; i<modules.length; i++){
|
||||
let ob = modules[i]
|
||||
let obType = ob.type
|
||||
let obArtifact = ob.artifact
|
||||
let obPath = obArtifact.path == null ? _resolvePath(ob.id, obArtifact.extension) : obArtifact.path
|
||||
switch(obType){
|
||||
case 'forge-hosted':
|
||||
case 'forge':
|
||||
obPath = path.join(basePath, 'libraries', obPath)
|
||||
break
|
||||
case 'library':
|
||||
obPath = path.join(basePath, 'libraries', obPath)
|
||||
break
|
||||
case 'forgemod':
|
||||
obPath = path.join(basePath, 'mods', obPath)
|
||||
break
|
||||
case 'litemod':
|
||||
obPath = path.join(basePath, 'mods', version, obPath)
|
||||
break
|
||||
case 'file':
|
||||
default:
|
||||
obPath = path.join(basePath, obPath)
|
||||
}
|
||||
let artifact = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, obType)
|
||||
if(obPath.toLowerCase().endsWith('.pack.xz')){
|
||||
if(!_validateLocal(obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')), 'MD5', artifact.hash)){
|
||||
asize += artifact.size*1
|
||||
alist.push(artifact)
|
||||
decompressqueue.push(obPath)
|
||||
}
|
||||
} else if(!_validateLocal(obPath, 'MD5', artifact.hash)){
|
||||
asize += artifact.size*1
|
||||
alist.push(artifact)
|
||||
}
|
||||
if(ob.sub_modules != null){
|
||||
let dltrack = _parseDistroModules(ob.sub_modules, basePath, version)
|
||||
asize += dltrack.dlsize*1
|
||||
alist = alist.concat(dltrack.dlqueue)
|
||||
decompressqueue = decompressqueue.concat(dltrack.callback)
|
||||
}
|
||||
}
|
||||
return new DLTracker(alist, asize, decompressqueue)
|
||||
}
|
||||
|
||||
function loadForgeData(serverpack, basePath){
|
||||
return new Promise(async function(fulfill, reject){
|
||||
let distro = await _chainValidateDistributionIndex(basePath)
|
||||
|
||||
const servers = distro.servers
|
||||
let serv = null
|
||||
for(let i=0; i<servers.length; i++){
|
||||
if(servers[i].id === serverpack){
|
||||
serv = servers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const modules = serv.modules
|
||||
for(let i=0; i<modules.length; i++){
|
||||
const ob = modules[i]
|
||||
if(ob.type === 'forge-hosted' || ob.type === 'forge'){
|
||||
let obArtifact = ob.artifact
|
||||
let obPath = obArtifact.path == null ? path.join(basePath, 'libraries', _resolvePath(ob.id, obArtifact.extension)) : obArtifact.path
|
||||
let asset = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, ob.type)
|
||||
let forgeData = await _finalizeForgeAsset(asset, basePath)
|
||||
fulfill(forgeData)
|
||||
return
|
||||
}
|
||||
}
|
||||
reject('No forge module found!')
|
||||
})
|
||||
}
|
||||
|
||||
function _parseForgeLibraries(){
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will initiate the download processed for the specified identifiers. If no argument is
|
||||
* given, all identifiers will be initiated. Note that in order for files to be processed you need to run
|
||||
* the processing function corresponding to that identifier. If you run this function without processing
|
||||
* the files, it is likely nothing will be enqueued in the global object and processing will complete
|
||||
* immediately. Once all downloads are complete, this function will fire the 'dlcomplete' event on the
|
||||
* global object instance.
|
||||
*
|
||||
* @param {Array.<{id: string, limit: number}>} identifiers - optional. The identifiers to process and corresponding parallel async task limit.
|
||||
*/
|
||||
function processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){
|
||||
this.progress = 0;
|
||||
let win = remote.getCurrentWindow()
|
||||
|
||||
let shouldFire = true
|
||||
|
||||
for(let i=0; i<identifiers.length; i++){
|
||||
let iden = identifiers[i]
|
||||
let r = startAsyncProcess(iden.id, iden.limit)
|
||||
if(r) shouldFire = false
|
||||
}
|
||||
|
||||
if(shouldFire){
|
||||
instance.emit('dlcomplete')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadVersionData,
|
||||
loadForgeData,
|
||||
validateAssets,
|
||||
validateLibraries,
|
||||
validateMiscellaneous,
|
||||
validateDistribution,
|
||||
processDlQueues,
|
||||
instance,
|
||||
Asset,
|
||||
Library,
|
||||
_resolvePath
|
||||
}
|
447
app/assets/js/authmanager.js
Normal file
@ -0,0 +1,447 @@
|
||||
/**
|
||||
* AuthManager
|
||||
*
|
||||
* This module aims to abstract login procedures. Results from Mojang's REST api
|
||||
* are retrieved through our Mojang module. These results are processed and stored,
|
||||
* if applicable, in the config using the ConfigManager. All login procedures should
|
||||
* be made through this module.
|
||||
*
|
||||
* @module authmanager
|
||||
*/
|
||||
// Requirements
|
||||
const ConfigManager = require('./configmanager')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
const { RestResponseStatus } = require('helios-core/common')
|
||||
const { MojangRestAPI, MojangErrorCode } = require('helios-core/mojang')
|
||||
const { MicrosoftAuth, MicrosoftErrorCode } = require('helios-core/microsoft')
|
||||
const { AZURE_CLIENT_ID } = require('./ipcconstants')
|
||||
const Lang = require('./langloader')
|
||||
|
||||
const log = LoggerUtil.getLogger('AuthManager')
|
||||
|
||||
// Error messages
|
||||
|
||||
function microsoftErrorDisplayable(errorCode) {
|
||||
switch (errorCode) {
|
||||
case MicrosoftErrorCode.NO_PROFILE:
|
||||
return {
|
||||
title: Lang.queryJS('auth.microsoft.error.noProfileTitle'),
|
||||
desc: Lang.queryJS('auth.microsoft.error.noProfileDesc')
|
||||
}
|
||||
case MicrosoftErrorCode.NO_XBOX_ACCOUNT:
|
||||
return {
|
||||
title: Lang.queryJS('auth.microsoft.error.noXboxAccountTitle'),
|
||||
desc: Lang.queryJS('auth.microsoft.error.noXboxAccountDesc')
|
||||
}
|
||||
case MicrosoftErrorCode.XBL_BANNED:
|
||||
return {
|
||||
title: Lang.queryJS('auth.microsoft.error.xblBannedTitle'),
|
||||
desc: Lang.queryJS('auth.microsoft.error.xblBannedDesc')
|
||||
}
|
||||
case MicrosoftErrorCode.UNDER_18:
|
||||
return {
|
||||
title: Lang.queryJS('auth.microsoft.error.under18Title'),
|
||||
desc: Lang.queryJS('auth.microsoft.error.under18Desc')
|
||||
}
|
||||
case MicrosoftErrorCode.UNKNOWN:
|
||||
return {
|
||||
title: Lang.queryJS('auth.microsoft.error.unknownTitle'),
|
||||
desc: Lang.queryJS('auth.microsoft.error.unknownDesc')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mojangErrorDisplayable(errorCode) {
|
||||
switch(errorCode) {
|
||||
case MojangErrorCode.ERROR_METHOD_NOT_ALLOWED:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.methodNotAllowedTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.methodNotAllowedDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_NOT_FOUND:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.notFoundTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.notFoundDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_USER_MIGRATED:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.accountMigratedTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.accountMigratedDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_INVALID_CREDENTIALS:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.invalidCredentialsTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.invalidCredentialsDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_RATELIMIT:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.tooManyAttemptsTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.tooManyAttemptsDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_INVALID_TOKEN:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.invalidTokenTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.invalidTokenDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.tokenHasProfileTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.tokenHasProfileDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_CREDENTIALS_MISSING:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.credentialsMissingTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.credentialsMissingDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_INVALID_SALT_VERSION:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.invalidSaltVersionTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.invalidSaltVersionDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_GONE:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.accountGoneTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.accountGoneDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_UNREACHABLE:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.unreachableTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.unreachableDesc')
|
||||
}
|
||||
case MojangErrorCode.ERROR_NOT_PAID:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.gameNotPurchasedTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.gameNotPurchasedDesc')
|
||||
}
|
||||
case MojangErrorCode.UNKNOWN:
|
||||
return {
|
||||
title: Lang.queryJS('auth.mojang.error.unknownErrorTitle'),
|
||||
desc: Lang.queryJS('auth.mojang.error.unknownErrorDesc')
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown error code: ${errorCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Functions
|
||||
|
||||
/**
|
||||
* Add a Mojang account. This will authenticate the given credentials with Mojang's
|
||||
* authserver. The resultant data will be stored as an auth account in the
|
||||
* configuration database.
|
||||
*
|
||||
* @param {string} username The account username (email if migrated).
|
||||
* @param {string} password The account password.
|
||||
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
||||
*/
|
||||
exports.addMojangAccount = async function(username, password) {
|
||||
try {
|
||||
const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken())
|
||||
console.log(response)
|
||||
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
||||
|
||||
const session = response.data
|
||||
if(session.selectedProfile != null){
|
||||
const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
|
||||
if(ConfigManager.getClientToken() == null){
|
||||
ConfigManager.setClientToken(session.clientToken)
|
||||
}
|
||||
ConfigManager.save()
|
||||
return ret
|
||||
} else {
|
||||
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID))
|
||||
}
|
||||
|
||||
} else {
|
||||
return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode))
|
||||
}
|
||||
|
||||
} catch (err){
|
||||
log.error(err)
|
||||
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
|
||||
}
|
||||
}
|
||||
|
||||
exports.addOfflineAccount = async function(username) {
|
||||
try {
|
||||
const ret = ConfigManager.addOfflineAccount(username)
|
||||
ConfigManager.save()
|
||||
return ret
|
||||
} catch (err){
|
||||
log.error(err)
|
||||
return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN))
|
||||
}
|
||||
}
|
||||
|
||||
const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 }
|
||||
|
||||
/**
|
||||
* Perform the full MS Auth flow in a given mode.
|
||||
*
|
||||
* AUTH_MODE.FULL = Full authorization for a new account.
|
||||
* AUTH_MODE.MS_REFRESH = Full refresh authorization.
|
||||
* AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token.
|
||||
*
|
||||
* @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken
|
||||
* @param {*} authMode The auth mode.
|
||||
* @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH.
|
||||
*/
|
||||
async function fullMicrosoftAuthFlow(entryCode, authMode) {
|
||||
try {
|
||||
|
||||
let accessTokenRaw
|
||||
let accessToken
|
||||
if(authMode !== AUTH_MODE.MC_REFRESH) {
|
||||
const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID)
|
||||
if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) {
|
||||
return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode))
|
||||
}
|
||||
accessToken = accessTokenResponse.data
|
||||
accessTokenRaw = accessToken.access_token
|
||||
} else {
|
||||
accessTokenRaw = entryCode
|
||||
}
|
||||
|
||||
const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw)
|
||||
if(xblResponse.responseStatus === RestResponseStatus.ERROR) {
|
||||
return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode))
|
||||
}
|
||||
const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data)
|
||||
if(xstsResonse.responseStatus === RestResponseStatus.ERROR) {
|
||||
return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode))
|
||||
}
|
||||
const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data)
|
||||
if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) {
|
||||
return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode))
|
||||
}
|
||||
const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token)
|
||||
if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) {
|
||||
return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode))
|
||||
}
|
||||
return {
|
||||
accessToken,
|
||||
accessTokenRaw,
|
||||
xbl: xblResponse.data,
|
||||
xsts: xstsResonse.data,
|
||||
mcToken: mcTokenResponse.data,
|
||||
mcProfile: mcProfileResponse.data
|
||||
}
|
||||
} catch(err) {
|
||||
log.error(err)
|
||||
return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the expiry date. Advance the expiry time by 10 seconds
|
||||
* to reduce the liklihood of working with an expired token.
|
||||
*
|
||||
* @param {number} nowMs Current time milliseconds.
|
||||
* @param {number} epiresInS Expires in (seconds)
|
||||
* @returns
|
||||
*/
|
||||
function calculateExpiryDate(nowMs, epiresInS) {
|
||||
return nowMs + ((epiresInS-10)*1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow.
|
||||
* The resultant data will be stored as an auth account in the configuration database.
|
||||
*
|
||||
* @param {string} authCode The authCode obtained from microsoft.
|
||||
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
|
||||
*/
|
||||
exports.addMicrosoftAccount = async function(authCode) {
|
||||
|
||||
const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL)
|
||||
|
||||
// Advance expiry by 10 seconds to avoid close calls.
|
||||
const now = new Date().getTime()
|
||||
|
||||
const ret = ConfigManager.addMicrosoftAuthAccount(
|
||||
fullAuth.mcProfile.id,
|
||||
fullAuth.mcToken.access_token,
|
||||
fullAuth.mcProfile.name,
|
||||
calculateExpiryDate(now, fullAuth.mcToken.expires_in),
|
||||
fullAuth.accessToken.access_token,
|
||||
fullAuth.accessToken.refresh_token,
|
||||
calculateExpiryDate(now, fullAuth.accessToken.expires_in)
|
||||
)
|
||||
ConfigManager.save()
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Mojang account. This will invalidate the access token associated
|
||||
* with the account and then remove it from the database.
|
||||
*
|
||||
* @param {string} uuid The UUID of the account to be removed.
|
||||
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
|
||||
*/
|
||||
exports.removeMojangAccount = async function(uuid){
|
||||
try {
|
||||
const authAcc = ConfigManager.getAuthAccount(uuid)
|
||||
const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken())
|
||||
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
||||
ConfigManager.removeAuthAccount(uuid)
|
||||
ConfigManager.save()
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
log.error('Error while removing account', response.error)
|
||||
return Promise.reject(response.error)
|
||||
}
|
||||
} catch (err){
|
||||
log.error('Error while removing account', err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout
|
||||
* through the ipc renderer.
|
||||
*
|
||||
* @param {string} uuid The UUID of the account to be removed.
|
||||
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
|
||||
*/
|
||||
exports.removeMicrosoftAccount = async function(uuid){
|
||||
try {
|
||||
ConfigManager.removeAuthAccount(uuid)
|
||||
ConfigManager.save()
|
||||
return Promise.resolve()
|
||||
} catch (err){
|
||||
log.error('Error while removing account', err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
exports.removeOfflineAccount = async function(uuid){
|
||||
try {
|
||||
ConfigManager.removeAuthAccount(uuid)
|
||||
ConfigManager.save()
|
||||
return Promise.resolve()
|
||||
} catch (err){
|
||||
log.error('Error while removing account', err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the selected account with Mojang's authserver. If the account is not valid,
|
||||
* we will attempt to refresh the access token and update that value. If that fails, a
|
||||
* new login will be required.
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
async function validateSelectedMojangAccount(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
|
||||
|
||||
if(response.responseStatus === RestResponseStatus.SUCCESS) {
|
||||
const isValid = response.data
|
||||
if(!isValid){
|
||||
const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
|
||||
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
|
||||
const session = refreshResponse.data
|
||||
ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
|
||||
ConfigManager.save()
|
||||
} else {
|
||||
log.error('Error while validating selected profile:', refreshResponse.error)
|
||||
log.info('Account access token is invalid.')
|
||||
return false
|
||||
}
|
||||
log.info('Account access token validated.')
|
||||
return true
|
||||
} else {
|
||||
log.info('Account access token validated.')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the selected account with Microsoft's authserver. If the account is not valid,
|
||||
* we will attempt to refresh the access token and update that value. If that fails, a
|
||||
* new login will be required.
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
async function validateSelectedMicrosoftAccount(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
const now = new Date().getTime()
|
||||
const mcExpiresAt = current.expiresAt
|
||||
const mcExpired = now >= mcExpiresAt
|
||||
|
||||
if(!mcExpired) {
|
||||
return true
|
||||
}
|
||||
|
||||
// MC token expired. Check MS token.
|
||||
|
||||
const msExpiresAt = current.microsoft.expires_at
|
||||
const msExpired = now >= msExpiresAt
|
||||
|
||||
if(msExpired) {
|
||||
// MS expired, do full refresh.
|
||||
try {
|
||||
const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH)
|
||||
|
||||
ConfigManager.updateMicrosoftAuthAccount(
|
||||
current.uuid,
|
||||
res.mcToken.access_token,
|
||||
res.accessToken.access_token,
|
||||
res.accessToken.refresh_token,
|
||||
calculateExpiryDate(now, res.accessToken.expires_in),
|
||||
calculateExpiryDate(now, res.mcToken.expires_in)
|
||||
)
|
||||
ConfigManager.save()
|
||||
return true
|
||||
} catch(err) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// Only MC expired, use existing MS token.
|
||||
try {
|
||||
const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH)
|
||||
|
||||
ConfigManager.updateMicrosoftAuthAccount(
|
||||
current.uuid,
|
||||
res.mcToken.access_token,
|
||||
current.microsoft.access_token,
|
||||
current.microsoft.refresh_token,
|
||||
current.microsoft.expires_at,
|
||||
calculateExpiryDate(now, res.mcToken.expires_in)
|
||||
)
|
||||
ConfigManager.save()
|
||||
return true
|
||||
}
|
||||
catch(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the selected auth account.
|
||||
*
|
||||
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
|
||||
* otherwise false.
|
||||
*/
|
||||
exports.validateSelected = async function(){
|
||||
const current = ConfigManager.getSelectedAccount()
|
||||
|
||||
if(current.type === 'microsoft') {
|
||||
return await validateSelectedMicrosoftAccount()
|
||||
} else {
|
||||
return await validateSelectedMojangAccount()
|
||||
}
|
||||
|
||||
}
|
808
app/assets/js/configmanager.js
Normal file
@ -0,0 +1,808 @@
|
||||
const fs = require('fs-extra')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
const { randomUUID } = require('crypto')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
const logger = LoggerUtil.getLogger('ConfigManager')
|
||||
|
||||
const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME)
|
||||
|
||||
const dataPath = path.join(sysRoot, '.helioslauncher')
|
||||
|
||||
const launcherDir = require('@electron/remote').app.getPath('userData')
|
||||
|
||||
/**
|
||||
* Retrieve the absolute path of the launcher directory.
|
||||
*
|
||||
* @returns {string} The absolute path of the launcher directory.
|
||||
*/
|
||||
exports.getLauncherDirectory = function(){
|
||||
return launcherDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the launcher's data directory. This is where all files related
|
||||
* to game launch are installed (common, instances, java, etc).
|
||||
*
|
||||
* @returns {string} The absolute path of the launcher's data directory.
|
||||
*/
|
||||
exports.getDataDirectory = function(def = false){
|
||||
return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the new data directory.
|
||||
*
|
||||
* @param {string} dataDirectory The new data directory.
|
||||
*/
|
||||
exports.setDataDirectory = function(dataDirectory){
|
||||
config.settings.launcher.dataDirectory = dataDirectory
|
||||
}
|
||||
|
||||
const configPath = path.join(exports.getLauncherDirectory(), 'config.json')
|
||||
const configPathLEGACY = path.join(dataPath, 'config.json')
|
||||
const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY)
|
||||
|
||||
exports.getAbsoluteMinRAM = function(ram){
|
||||
if(ram?.minimum != null) {
|
||||
return ram.minimum/1024
|
||||
} else {
|
||||
// Legacy behavior
|
||||
const mem = os.totalmem()
|
||||
return mem >= (6*1073741824) ? 3 : 2
|
||||
}
|
||||
}
|
||||
|
||||
exports.getAbsoluteMaxRAM = function(ram){
|
||||
const mem = os.totalmem()
|
||||
const gT16 = mem-(16*1073741824)
|
||||
return Math.floor((mem-(gT16 > 0 ? (Number.parseInt(gT16/8) + (16*1073741824)/4) : mem/4))/1073741824)
|
||||
}
|
||||
|
||||
function resolveSelectedRAM(ram) {
|
||||
if(ram?.recommended != null) {
|
||||
return `${ram.recommended}M`
|
||||
} else {
|
||||
// Legacy behavior
|
||||
const mem = os.totalmem()
|
||||
return mem >= (8*1073741824) ? '4G' : (mem >= (6*1073741824) ? '3G' : '2G')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Three types of values:
|
||||
* Static = Explicitly declared.
|
||||
* Dynamic = Calculated by a private function.
|
||||
* Resolved = Resolved externally, defaults to null.
|
||||
*/
|
||||
const DEFAULT_CONFIG = {
|
||||
settings: {
|
||||
game: {
|
||||
resWidth: 1280,
|
||||
resHeight: 720,
|
||||
fullscreen: false,
|
||||
autoConnect: true,
|
||||
launchDetached: true
|
||||
},
|
||||
launcher: {
|
||||
allowPrerelease: false,
|
||||
dataDirectory: dataPath
|
||||
}
|
||||
},
|
||||
newsCache: {
|
||||
date: null,
|
||||
content: null,
|
||||
dismissed: false
|
||||
},
|
||||
clientToken: null,
|
||||
selectedServer: null, // Resolved
|
||||
selectedAccount: null,
|
||||
authenticationDatabase: {},
|
||||
modConfigurations: [],
|
||||
javaConfig: {}
|
||||
}
|
||||
|
||||
let config = null
|
||||
|
||||
// Persistance Utility Functions
|
||||
|
||||
/**
|
||||
* Save the current configuration to a file.
|
||||
*/
|
||||
exports.save = function(){
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the configuration into memory. If a configuration file exists,
|
||||
* that will be read and saved. Otherwise, a default configuration will
|
||||
* be generated. Note that "resolved" values default to null and will
|
||||
* need to be externally assigned.
|
||||
*/
|
||||
exports.load = function(){
|
||||
let doLoad = true
|
||||
|
||||
if(!fs.existsSync(configPath)){
|
||||
// Create all parent directories.
|
||||
fs.ensureDirSync(path.join(configPath, '..'))
|
||||
if(fs.existsSync(configPathLEGACY)){
|
||||
fs.moveSync(configPathLEGACY, configPath)
|
||||
} else {
|
||||
doLoad = false
|
||||
config = DEFAULT_CONFIG
|
||||
exports.save()
|
||||
}
|
||||
}
|
||||
if(doLoad){
|
||||
let doValidate = false
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, 'UTF-8'))
|
||||
doValidate = true
|
||||
} catch (err){
|
||||
logger.error(err)
|
||||
logger.info('Configuration file contains malformed JSON or is corrupt.')
|
||||
logger.info('Generating a new configuration file.')
|
||||
fs.ensureDirSync(path.join(configPath, '..'))
|
||||
config = DEFAULT_CONFIG
|
||||
exports.save()
|
||||
}
|
||||
if(doValidate){
|
||||
config = validateKeySet(DEFAULT_CONFIG, config)
|
||||
exports.save()
|
||||
}
|
||||
}
|
||||
logger.info('Successfully Loaded')
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether or not the manager has been loaded.
|
||||
*/
|
||||
exports.isLoaded = function(){
|
||||
return config != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the destination object has at least every field
|
||||
* present in the source object. Assign a default value otherwise.
|
||||
*
|
||||
* @param {Object} srcObj The source object to reference against.
|
||||
* @param {Object} destObj The destination object.
|
||||
* @returns {Object} A validated destination object.
|
||||
*/
|
||||
function validateKeySet(srcObj, destObj){
|
||||
if(srcObj == null){
|
||||
srcObj = {}
|
||||
}
|
||||
const validationBlacklist = ['authenticationDatabase', 'javaConfig']
|
||||
const keys = Object.keys(srcObj)
|
||||
for(let i=0; i<keys.length; i++){
|
||||
if(typeof destObj[keys[i]] === 'undefined'){
|
||||
destObj[keys[i]] = srcObj[keys[i]]
|
||||
} else if(typeof srcObj[keys[i]] === 'object' && srcObj[keys[i]] != null && !(srcObj[keys[i]] instanceof Array) && validationBlacklist.indexOf(keys[i]) === -1){
|
||||
destObj[keys[i]] = validateKeySet(srcObj[keys[i]], destObj[keys[i]])
|
||||
}
|
||||
}
|
||||
return destObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this is the first time the user has launched the
|
||||
* application. This is determined by the existance of the data path.
|
||||
*
|
||||
* @returns {boolean} True if this is the first launch, otherwise false.
|
||||
*/
|
||||
exports.isFirstLaunch = function(){
|
||||
return firstLaunch
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the folder in the OS temp directory which we
|
||||
* will use to extract and store native dependencies for game launch.
|
||||
*
|
||||
* @returns {string} The name of the folder.
|
||||
*/
|
||||
exports.getTempNativeFolder = function(){
|
||||
return 'WCNatives'
|
||||
}
|
||||
|
||||
// System Settings (Unconfigurable on UI)
|
||||
|
||||
/**
|
||||
* Retrieve the news cache to determine
|
||||
* whether or not there is newer news.
|
||||
*
|
||||
* @returns {Object} The news cache object.
|
||||
*/
|
||||
exports.getNewsCache = function(){
|
||||
return config.newsCache
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the new news cache object.
|
||||
*
|
||||
* @param {Object} newsCache The new news cache object.
|
||||
*/
|
||||
exports.setNewsCache = function(newsCache){
|
||||
config.newsCache = newsCache
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not the news has been dismissed (checked)
|
||||
*
|
||||
* @param {boolean} dismissed Whether or not the news has been dismissed (checked).
|
||||
*/
|
||||
exports.setNewsCacheDismissed = function(dismissed){
|
||||
config.newsCache.dismissed = dismissed
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the common directory for shared
|
||||
* game files (assets, libraries, etc).
|
||||
*
|
||||
* @returns {string} The launcher's common directory.
|
||||
*/
|
||||
exports.getCommonDirectory = function(){
|
||||
return path.join(exports.getDataDirectory(), 'common')
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the instance directory for the per
|
||||
* server game directories.
|
||||
*
|
||||
* @returns {string} The launcher's instance directory.
|
||||
*/
|
||||
exports.getInstanceDirectory = function(){
|
||||
return path.join(exports.getDataDirectory(), 'instances')
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the launcher's Client Token.
|
||||
* There is no default client token.
|
||||
*
|
||||
* @returns {string} The launcher's Client Token.
|
||||
*/
|
||||
exports.getClientToken = function(){
|
||||
return config.clientToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the launcher's Client Token.
|
||||
*
|
||||
* @param {string} clientToken The launcher's new Client Token.
|
||||
*/
|
||||
exports.setClientToken = function(clientToken){
|
||||
config.clientToken = clientToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ID of the selected serverpack.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {string} The ID of the selected serverpack.
|
||||
*/
|
||||
exports.getSelectedServer = function(def = false){
|
||||
return !def ? config.selectedServer : DEFAULT_CONFIG.clientToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ID of the selected serverpack.
|
||||
*
|
||||
* @param {string} serverID The ID of the new selected serverpack.
|
||||
*/
|
||||
exports.setSelectedServer = function(serverID){
|
||||
config.selectedServer = serverID
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of each account currently authenticated by the launcher.
|
||||
*
|
||||
* @returns {Array.<Object>} An array of each stored authenticated account.
|
||||
*/
|
||||
exports.getAuthAccounts = function(){
|
||||
return config.authenticationDatabase
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authenticated account with the given uuid. Value may
|
||||
* be null.
|
||||
*
|
||||
* @param {string} uuid The uuid of the authenticated account.
|
||||
* @returns {Object} The authenticated account with the given uuid.
|
||||
*/
|
||||
exports.getAuthAccount = function(uuid){
|
||||
return config.authenticationDatabase[uuid]
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the access token of an authenticated mojang account.
|
||||
*
|
||||
* @param {string} uuid The uuid of the authenticated account.
|
||||
* @param {string} accessToken The new Access Token.
|
||||
*
|
||||
* @returns {Object} The authenticated account object created by this action.
|
||||
*/
|
||||
exports.updateMojangAuthAccount = function(uuid, accessToken){
|
||||
config.authenticationDatabase[uuid].accessToken = accessToken
|
||||
config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion.
|
||||
return config.authenticationDatabase[uuid]
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an authenticated mojang account to the database to be stored.
|
||||
*
|
||||
* @param {string} uuid The uuid of the authenticated account.
|
||||
* @param {string} accessToken The accessToken of the authenticated account.
|
||||
* @param {string} username The username (usually email) of the authenticated account.
|
||||
* @param {string} displayName The in game name of the authenticated account.
|
||||
*
|
||||
* @returns {Object} The authenticated account object created by this action.
|
||||
*/
|
||||
exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
|
||||
config.selectedAccount = uuid
|
||||
config.authenticationDatabase[uuid] = {
|
||||
type: 'mojang',
|
||||
accessToken,
|
||||
username: username.trim(),
|
||||
uuid: uuid.trim(),
|
||||
displayName: displayName.trim()
|
||||
}
|
||||
return config.authenticationDatabase[uuid]
|
||||
}
|
||||
|
||||
exports.addOfflineAccount = function(username){
|
||||
console.log("Yeah I try to create new offline account")
|
||||
uuid = randomUUID()
|
||||
config.selectedAccount = uuid
|
||||
config.authenticationDatabase[uuid] = {
|
||||
type: 'offline',
|
||||
password: '',
|
||||
username: username.trim(),
|
||||
uuid: uuid.trim(),
|
||||
displayName: username.trim()
|
||||
}
|
||||
return config.authenticationDatabase[uuid]
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tokens of an authenticated microsoft account.
|
||||
*
|
||||
* @param {string} uuid The uuid of the authenticated account.
|
||||
* @param {string} accessToken The new Access Token.
|
||||
* @param {string} msAccessToken The new Microsoft Access Token
|
||||
* @param {string} msRefreshToken The new Microsoft Refresh Token
|
||||
* @param {date} msExpires The date when the microsoft access token expires
|
||||
* @param {date} mcExpires The date when the mojang access token expires
|
||||
*
|
||||
* @returns {Object} The authenticated account object created by this action.
|
||||
*/
|
||||
exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) {
|
||||
config.authenticationDatabase[uuid].accessToken = accessToken
|
||||
config.authenticationDatabase[uuid].expiresAt = mcExpires
|
||||
config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken
|
||||
config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken
|
||||
config.authenticationDatabase[uuid].microsoft.expires_at = msExpires
|
||||
return config.authenticationDatabase[uuid]
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an authenticated microsoft account to the database to be stored.
|
||||
*
|
||||
* @param {string} uuid The uuid of the authenticated account.
|
||||
* @param {string} accessToken The accessToken of the authenticated account.
|
||||
* @param {string} name The in game name of the authenticated account.
|
||||
* @param {date} mcExpires The date when the mojang access token expires
|
||||
* @param {string} msAccessToken The microsoft access token
|
||||
* @param {string} msRefreshToken The microsoft refresh token
|
||||
* @param {date} msExpires The date when the microsoft access token expires
|
||||
*
|
||||
* @returns {Object} The authenticated account object created by this action.
|
||||
*/
|
||||
exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) {
|
||||
config.selectedAccount = uuid
|
||||
config.authenticationDatabase[uuid] = {
|
||||
type: 'microsoft',
|
||||
accessToken,
|
||||
username: name.trim(),
|
||||
uuid: uuid.trim(),
|
||||
displayName: name.trim(),
|
||||
expiresAt: mcExpires,
|
||||
microsoft: {
|
||||
access_token: msAccessToken,
|
||||
refresh_token: msRefreshToken,
|
||||
expires_at: msExpires
|
||||
}
|
||||
}
|
||||
return config.authenticationDatabase[uuid]
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an authenticated account from the database. If the account
|
||||
* was also the selected account, a new one will be selected. If there
|
||||
* are no accounts, the selected account will be null.
|
||||
*
|
||||
* @param {string} uuid The uuid of the authenticated account.
|
||||
*
|
||||
* @returns {boolean} True if the account was removed, false if it never existed.
|
||||
*/
|
||||
exports.removeAuthAccount = function(uuid){
|
||||
if(config.authenticationDatabase[uuid] != null){
|
||||
delete config.authenticationDatabase[uuid]
|
||||
if(config.selectedAccount === uuid){
|
||||
const keys = Object.keys(config.authenticationDatabase)
|
||||
if(keys.length > 0){
|
||||
config.selectedAccount = keys[0]
|
||||
} else {
|
||||
config.selectedAccount = null
|
||||
config.clientToken = null
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected authenticated account.
|
||||
*
|
||||
* @returns {Object} The selected authenticated account.
|
||||
*/
|
||||
exports.getSelectedAccount = function(){
|
||||
return config.authenticationDatabase[config.selectedAccount]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected authenticated account.
|
||||
*
|
||||
* @param {string} uuid The UUID of the account which is to be set
|
||||
* as the selected account.
|
||||
*
|
||||
* @returns {Object} The selected authenticated account.
|
||||
*/
|
||||
exports.setSelectedAccount = function(uuid){
|
||||
const authAcc = config.authenticationDatabase[uuid]
|
||||
if(authAcc != null) {
|
||||
config.selectedAccount = uuid
|
||||
}
|
||||
return authAcc
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of each mod configuration currently stored.
|
||||
*
|
||||
* @returns {Array.<Object>} An array of each stored mod configuration.
|
||||
*/
|
||||
exports.getModConfigurations = function(){
|
||||
return config.modConfigurations
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the array of stored mod configurations.
|
||||
*
|
||||
* @param {Array.<Object>} configurations An array of mod configurations.
|
||||
*/
|
||||
exports.setModConfigurations = function(configurations){
|
||||
config.modConfigurations = configurations
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mod configuration for a specific server.
|
||||
*
|
||||
* @param {string} serverid The id of the server.
|
||||
* @returns {Object} The mod configuration for the given server.
|
||||
*/
|
||||
exports.getModConfiguration = function(serverid){
|
||||
const cfgs = config.modConfigurations
|
||||
for(let i=0; i<cfgs.length; i++){
|
||||
if(cfgs[i].id === serverid){
|
||||
return cfgs[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mod configuration for a specific server. This overrides any existing value.
|
||||
*
|
||||
* @param {string} serverid The id of the server for the given mod configuration.
|
||||
* @param {Object} configuration The mod configuration for the given server.
|
||||
*/
|
||||
exports.setModConfiguration = function(serverid, configuration){
|
||||
const cfgs = config.modConfigurations
|
||||
for(let i=0; i<cfgs.length; i++){
|
||||
if(cfgs[i].id === serverid){
|
||||
cfgs[i] = configuration
|
||||
return
|
||||
}
|
||||
}
|
||||
cfgs.push(configuration)
|
||||
}
|
||||
|
||||
// User Configurable Settings
|
||||
|
||||
// Java Settings
|
||||
|
||||
function defaultJavaConfig(effectiveJavaOptions, ram) {
|
||||
if(effectiveJavaOptions.suggestedMajor > 8) {
|
||||
return defaultJavaConfig17(ram)
|
||||
} else {
|
||||
return defaultJavaConfig8(ram)
|
||||
}
|
||||
}
|
||||
|
||||
function defaultJavaConfig8(ram) {
|
||||
return {
|
||||
minRAM: resolveSelectedRAM(ram),
|
||||
maxRAM: resolveSelectedRAM(ram),
|
||||
executable: null,
|
||||
jvmOptions: [
|
||||
'-XX:+UseConcMarkSweepGC',
|
||||
'-XX:+CMSIncrementalMode',
|
||||
'-XX:-UseAdaptiveSizePolicy',
|
||||
'-Xmn128M'
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function defaultJavaConfig17(ram) {
|
||||
return {
|
||||
minRAM: resolveSelectedRAM(ram),
|
||||
maxRAM: resolveSelectedRAM(ram),
|
||||
executable: null,
|
||||
jvmOptions: [
|
||||
'-XX:+UnlockExperimentalVMOptions',
|
||||
'-XX:+UseG1GC',
|
||||
'-XX:G1NewSizePercent=20',
|
||||
'-XX:G1ReservePercent=20',
|
||||
'-XX:MaxGCPauseMillis=50',
|
||||
'-XX:G1HeapRegionSize=32M'
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a java config property is set for the given server.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @param {*} mcVersion The minecraft version of the server.
|
||||
*/
|
||||
exports.ensureJavaConfig = function(serverid, effectiveJavaOptions, ram) {
|
||||
if(!Object.prototype.hasOwnProperty.call(config.javaConfig, serverid)) {
|
||||
config.javaConfig[serverid] = defaultJavaConfig(effectiveJavaOptions, ram)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the minimum amount of memory for JVM initialization. This value
|
||||
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
||||
* 1024 MegaBytes, etc.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {string} The minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.getMinRAM = function(serverid){
|
||||
return config.javaConfig[serverid].minRAM
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum amount of memory for JVM initialization. This value should
|
||||
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
||||
* 1024 MegaBytes, etc.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @param {string} minRAM The new minimum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.setMinRAM = function(serverid, minRAM){
|
||||
config.javaConfig[serverid].minRAM = minRAM
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the maximum amount of memory for JVM initialization. This value
|
||||
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
||||
* 1024 MegaBytes, etc.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {string} The maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.getMaxRAM = function(serverid){
|
||||
return config.javaConfig[serverid].maxRAM
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum amount of memory for JVM initialization. This value should
|
||||
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
|
||||
* 1024 MegaBytes, etc.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @param {string} maxRAM The new maximum amount of memory for JVM initialization.
|
||||
*/
|
||||
exports.setMaxRAM = function(serverid, maxRAM){
|
||||
config.javaConfig[serverid].maxRAM = maxRAM
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the path of the Java Executable.
|
||||
*
|
||||
* This is a resolved configuration value and defaults to null until externally assigned.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {string} The path of the Java Executable.
|
||||
*/
|
||||
exports.getJavaExecutable = function(serverid){
|
||||
return config.javaConfig[serverid].executable
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path of the Java Executable.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @param {string} executable The new path of the Java Executable.
|
||||
*/
|
||||
exports.setJavaExecutable = function(serverid, executable){
|
||||
config.javaConfig[serverid].executable = executable
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the additional arguments for JVM initialization. Required arguments,
|
||||
* such as memory allocation, will be dynamically resolved and will not be included
|
||||
* in this value.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @returns {Array.<string>} An array of the additional arguments for JVM initialization.
|
||||
*/
|
||||
exports.getJVMOptions = function(serverid){
|
||||
return config.javaConfig[serverid].jvmOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the additional arguments for JVM initialization. Required arguments,
|
||||
* such as memory allocation, will be dynamically resolved and should not be
|
||||
* included in this value.
|
||||
*
|
||||
* @param {string} serverid The server id.
|
||||
* @param {Array.<string>} jvmOptions An array of the new additional arguments for JVM
|
||||
* initialization.
|
||||
*/
|
||||
exports.setJVMOptions = function(serverid, jvmOptions){
|
||||
config.javaConfig[serverid].jvmOptions = jvmOptions
|
||||
}
|
||||
|
||||
// Game Settings
|
||||
|
||||
/**
|
||||
* Retrieve the width of the game window.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {number} The width of the game window.
|
||||
*/
|
||||
exports.getGameWidth = function(def = false){
|
||||
return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the game window.
|
||||
*
|
||||
* @param {number} resWidth The new width of the game window.
|
||||
*/
|
||||
exports.setGameWidth = function(resWidth){
|
||||
config.settings.game.resWidth = Number.parseInt(resWidth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a potential new width value.
|
||||
*
|
||||
* @param {number} resWidth The width value to validate.
|
||||
* @returns {boolean} Whether or not the value is valid.
|
||||
*/
|
||||
exports.validateGameWidth = function(resWidth){
|
||||
const nVal = Number.parseInt(resWidth)
|
||||
return Number.isInteger(nVal) && nVal >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the height of the game window.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {number} The height of the game window.
|
||||
*/
|
||||
exports.getGameHeight = function(def = false){
|
||||
return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the height of the game window.
|
||||
*
|
||||
* @param {number} resHeight The new height of the game window.
|
||||
*/
|
||||
exports.setGameHeight = function(resHeight){
|
||||
config.settings.game.resHeight = Number.parseInt(resHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a potential new height value.
|
||||
*
|
||||
* @param {number} resHeight The height value to validate.
|
||||
* @returns {boolean} Whether or not the value is valid.
|
||||
*/
|
||||
exports.validateGameHeight = function(resHeight){
|
||||
const nVal = Number.parseInt(resHeight)
|
||||
return Number.isInteger(nVal) && nVal >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should be launched in fullscreen mode.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {boolean} Whether or not the game is set to launch in fullscreen mode.
|
||||
*/
|
||||
exports.getFullscreen = function(def = false){
|
||||
return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of if the game should be launched in fullscreen mode.
|
||||
*
|
||||
* @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode.
|
||||
*/
|
||||
exports.setFullscreen = function(fullscreen){
|
||||
config.settings.game.fullscreen = fullscreen
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should auto connect to servers.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {boolean} Whether or not the game should auto connect to servers.
|
||||
*/
|
||||
exports.getAutoConnect = function(def = false){
|
||||
return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of whether or not the game should auto connect to servers.
|
||||
*
|
||||
* @param {boolean} autoConnect Whether or not the game should auto connect to servers.
|
||||
*/
|
||||
exports.setAutoConnect = function(autoConnect){
|
||||
config.settings.game.autoConnect = autoConnect
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should launch as a detached process.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {boolean} Whether or not the game will launch as a detached process.
|
||||
*/
|
||||
exports.getLaunchDetached = function(def = false){
|
||||
return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of whether or not the game should launch as a detached process.
|
||||
*
|
||||
* @param {boolean} launchDetached Whether or not the game should launch as a detached process.
|
||||
*/
|
||||
exports.setLaunchDetached = function(launchDetached){
|
||||
config.settings.game.launchDetached = launchDetached
|
||||
}
|
||||
|
||||
// Launcher Settings
|
||||
|
||||
/**
|
||||
* Check if the launcher should download prerelease versions.
|
||||
*
|
||||
* @param {boolean} def Optional. If true, the default value will be returned.
|
||||
* @returns {boolean} Whether or not the launcher should download prerelease versions.
|
||||
*/
|
||||
exports.getAllowPrerelease = function(def = false){
|
||||
return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of Whether or not the launcher should download prerelease versions.
|
||||
*
|
||||
* @param {boolean} launchDetached Whether or not the launcher should download prerelease versions.
|
||||
*/
|
||||
exports.setAllowPrerelease = function(allowPrerelease){
|
||||
config.settings.launcher.allowPrerelease = allowPrerelease
|
||||
}
|
52
app/assets/js/discordwrapper.js
Normal file
@ -0,0 +1,52 @@
|
||||
// Work in progress
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
|
||||
const logger = LoggerUtil.getLogger('DiscordWrapper')
|
||||
|
||||
const { Client } = require('discord-rpc-patch')
|
||||
|
||||
const Lang = require('./langloader')
|
||||
|
||||
let client
|
||||
let activity
|
||||
|
||||
exports.initRPC = function(genSettings, servSettings, initialDetails = Lang.queryJS('discord.waiting')){
|
||||
client = new Client({ transport: 'ipc' })
|
||||
|
||||
activity = {
|
||||
details: initialDetails,
|
||||
state: Lang.queryJS('discord.state', {shortId: servSettings.shortId}),
|
||||
largeImageKey: servSettings.largeImageKey,
|
||||
largeImageText: servSettings.largeImageText,
|
||||
smallImageKey: genSettings.smallImageKey,
|
||||
smallImageText: genSettings.smallImageText,
|
||||
startTimestamp: new Date().getTime(),
|
||||
instance: false
|
||||
}
|
||||
|
||||
client.on('ready', () => {
|
||||
logger.info('Discord RPC Connected')
|
||||
client.setActivity(activity)
|
||||
})
|
||||
|
||||
client.login({clientId: genSettings.clientId}).catch(error => {
|
||||
if(error.message.includes('ENOENT')) {
|
||||
logger.info('Unable to initialize Discord Rich Presence, no client detected.')
|
||||
} else {
|
||||
logger.info('Unable to initialize Discord Rich Presence: ' + error.message, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
exports.updateDetails = function(details){
|
||||
activity.details = details
|
||||
client.setActivity(activity)
|
||||
}
|
||||
|
||||
exports.shutdownRPC = function(){
|
||||
if(!client) return
|
||||
client.clearActivity()
|
||||
client.destroy()
|
||||
client = null
|
||||
activity = null
|
||||
}
|
15
app/assets/js/distromanager.js
Normal file
@ -0,0 +1,15 @@
|
||||
const { DistributionAPI } = require('helios-core/common')
|
||||
|
||||
const ConfigManager = require('./configmanager')
|
||||
|
||||
exports.REMOTE_DISTRO_URL = 'https://git.onimai.ru/ONIMAI-SMP/distribution/raw/branch/main/distribution.json'
|
||||
|
||||
const api = new DistributionAPI(
|
||||
ConfigManager.getLauncherDirectory(),
|
||||
null, // Injected forcefully by the preloader.
|
||||
null, // Injected forcefully by the preloader.
|
||||
exports.REMOTE_DISTRO_URL,
|
||||
false
|
||||
)
|
||||
|
||||
exports.DistroAPI = api
|
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 = '58f2320b-3644-4194-9174-796e17617dd0'
|
||||
// SEE NOTE ABOVE.
|
||||
|
||||
|
||||
// Opcodes
|
||||
exports.MSFT_OPCODE = {
|
||||
OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN',
|
||||
OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT',
|
||||
REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN',
|
||||
REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT'
|
||||
}
|
||||
// Reply types for REPLY opcode.
|
||||
exports.MSFT_REPLY_TYPE = {
|
||||
SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS',
|
||||
ERROR: 'MSFT_AUTH_REPLY_ERROR'
|
||||
}
|
||||
// Error types for ERROR reply.
|
||||
exports.MSFT_ERROR = {
|
||||
ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN',
|
||||
NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED'
|
||||
}
|
||||
|
||||
exports.SHELL_OPCODE = {
|
||||
TRASH_ITEM: 'TRASH_ITEM'
|
||||
}
|
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,208 +0,0 @@
|
||||
const mojang = require('mojang')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
const path = require('path')
|
||||
const child_process = require('child_process')
|
||||
const ag = require('./assetguard.js')
|
||||
const AdmZip = require('adm-zip')
|
||||
const fs = require('fs')
|
||||
const mkpath = require('mkdirp');
|
||||
|
||||
function launchMinecraft(versionData, forgeData, basePath){
|
||||
const authPromise = mojang.auth('EMAIL', 'PASS', uuidV4(), {
|
||||
name: 'Minecraft',
|
||||
version: 1
|
||||
})
|
||||
authPromise.then(function(data){
|
||||
const args = finalizeArgumentsForge(versionData, forgeData, data, basePath)
|
||||
//TODO make this dynamic
|
||||
const child = child_process.spawn('C:\\Program Files\\Java\\jre1.8.0_131\\bin\\javaw.exe', args)
|
||||
child.stdout.on('data', (data) => {
|
||||
console.log('Minecraft:', data.toString('utf8'))
|
||||
})
|
||||
child.stderr.on('data', (data) => {
|
||||
console.log('Minecraft:', data.toString('utf8'))
|
||||
})
|
||||
child.on('close', (code, signal) => {
|
||||
console.log('Exited with code', code)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function finalizeArgumentsForge(versionData, forgeData, authData, basePath){
|
||||
const mcArgs = forgeData['minecraftArguments']
|
||||
const gameProfile = authData['selectedProfile']
|
||||
const regex = new RegExp('\\${*(.*)}')
|
||||
const argArr = mcArgs.split(' ')
|
||||
const staticArgs = ['-Xmx4G',
|
||||
'-XX:+UseConcMarkSweepGC',
|
||||
'-XX:+CMSIncrementalMode',
|
||||
'-XX:-UseAdaptiveSizePolicy',
|
||||
'-Xmn128M',
|
||||
'-Djava.library.path=' + path.join(basePath, 'natives'),
|
||||
'-cp',
|
||||
classpathArg(versionData, basePath).concat(forgeClasspathArg(forgeData, basePath)).join(';'),
|
||||
forgeData.mainClass]
|
||||
for(let i=0; i<argArr.length; i++){
|
||||
if(regex.test(argArr[i])){
|
||||
const identifier = argArr[i].match(regex)[1]
|
||||
let newVal = argArr[i]
|
||||
switch(identifier){
|
||||
case 'auth_player_name':
|
||||
newVal = gameProfile['name']
|
||||
break
|
||||
case 'version_name':
|
||||
newVal = versionData['id']
|
||||
break
|
||||
case 'game_directory':
|
||||
newVal = basePath
|
||||
break
|
||||
case 'assets_root':
|
||||
newVal = path.join(basePath, 'assets')
|
||||
break
|
||||
case 'assets_index_name':
|
||||
newVal = versionData['assets']
|
||||
break
|
||||
case 'auth_uuid':
|
||||
newVal = gameProfile['id']
|
||||
break
|
||||
case 'auth_access_token':
|
||||
newVal = authData['accessToken']
|
||||
break
|
||||
case 'user_type':
|
||||
newVal = 'MOJANG'
|
||||
break
|
||||
case 'version_type':
|
||||
newVal = versionData['type']
|
||||
break
|
||||
}
|
||||
argArr[i] = newVal
|
||||
}
|
||||
}
|
||||
|
||||
return staticArgs.concat(argArr)
|
||||
}
|
||||
|
||||
function finalizeArguments(versionData, authData, basePath){
|
||||
const mcArgs = versionData['minecraftArguments']
|
||||
const gameProfile = authData['selectedProfile']
|
||||
const regex = new RegExp('\\${*(.*)}')
|
||||
const argArr = mcArgs.split(' ')
|
||||
const staticArgs = ['-Xmx1G',
|
||||
'-XX:+UseConcMarkSweepGC',
|
||||
'-XX:+CMSIncrementalMode',
|
||||
'-XX:-UseAdaptiveSizePolicy',
|
||||
'-Xmn128M',
|
||||
'-Djava.library.path=' + path.join(basePath, 'natives'),
|
||||
'-cp',
|
||||
classpathArg(versionData, basePath).join(';'),
|
||||
versionData.mainClass]
|
||||
for(let i=0; i<argArr.length; i++){
|
||||
if(regex.test(argArr[i])){
|
||||
const identifier = argArr[i].match(regex)[1]
|
||||
let newVal = argArr[i]
|
||||
switch(identifier){
|
||||
case 'auth_player_name':
|
||||
newVal = gameProfile['name']
|
||||
break
|
||||
case 'version_name':
|
||||
newVal = versionData['id']
|
||||
break
|
||||
case 'game_directory':
|
||||
newVal = basePath
|
||||
break
|
||||
case 'assets_root':
|
||||
newVal = path.join(basePath, 'assets')
|
||||
break
|
||||
case 'assets_index_name':
|
||||
newVal = versionData['assets']
|
||||
break
|
||||
case 'auth_uuid':
|
||||
newVal = gameProfile['id']
|
||||
break
|
||||
case 'auth_access_token':
|
||||
newVal = authData['accessToken']
|
||||
break
|
||||
case 'user_type':
|
||||
newVal = 'MOJANG'
|
||||
break
|
||||
case 'version_type':
|
||||
newVal = versionData['type']
|
||||
break
|
||||
}
|
||||
argArr[i] = newVal
|
||||
}
|
||||
}
|
||||
|
||||
return staticArgs.concat(argArr)
|
||||
}
|
||||
|
||||
function forgeClasspathArg(forgeData, basePath){
|
||||
const libArr = forgeData['libraries']
|
||||
const libPath = path.join(basePath, 'libraries')
|
||||
const cpArgs = []
|
||||
for(let i=0; i<libArr.length; i++){
|
||||
const lib = libArr[i]
|
||||
const to = path.join(libPath, ag._resolvePath(lib.name, '.jar'))
|
||||
cpArgs.push(to)
|
||||
}
|
||||
return cpArgs
|
||||
}
|
||||
|
||||
function classpathArg(versionData, basePath){
|
||||
const libArr = versionData['libraries']
|
||||
const libPath = path.join(basePath, 'libraries')
|
||||
const nativePath = path.join(basePath, 'natives')
|
||||
const version = versionData['id']
|
||||
const cpArgs = [path.join(basePath, 'versions', version, version + '.jar')]
|
||||
libArr.forEach(function(lib){
|
||||
if(ag.Library.validateRules(lib['rules'])){
|
||||
if(lib['natives'] == null){
|
||||
const dlInfo = lib['downloads']
|
||||
const artifact = dlInfo['artifact']
|
||||
const to = path.join(libPath, artifact['path'])
|
||||
cpArgs.push(to)
|
||||
} else {
|
||||
//Now we need to extract natives.
|
||||
const natives = lib['natives']
|
||||
const extractInst = lib['extract']
|
||||
const exclusionArr = extractInst['exclude']
|
||||
const opSys = ag.Library.mojangFriendlyOS()
|
||||
const indexId = natives[opSys]
|
||||
const dlInfo = lib['downloads']
|
||||
const classifiers = dlInfo['classifiers']
|
||||
const artifact = classifiers[indexId]
|
||||
|
||||
const to = path.join(libPath, artifact['path'])
|
||||
|
||||
let zip = new AdmZip(to)
|
||||
let zipEntries = zip.getEntries()
|
||||
|
||||
for(let i=0; i<zipEntries.length; i++){
|
||||
const fileName = zipEntries[i].entryName
|
||||
|
||||
let shouldExclude = false
|
||||
|
||||
exclusionArr.forEach(function(exclusion){
|
||||
if(exclusion.indexOf(fileName) > -1){
|
||||
shouldExclude = true
|
||||
}
|
||||
})
|
||||
|
||||
if(!shouldExclude){
|
||||
mkpath.sync(path.join(nativePath, fileName, '..'))
|
||||
fs.writeFile(path.join(nativePath, fileName), zipEntries[i].getData())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
cpArgs.push(to)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return cpArgs
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
launchMinecraft
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
const fs = require('fs')
|
||||
|
||||
/**
|
||||
* Class used to configure mod launch args.
|
||||
*/
|
||||
export class ModList {
|
||||
|
||||
/**
|
||||
* Construct a ModList.
|
||||
*
|
||||
* @param {String} repositoryRoot - the base path of the mod locations.
|
||||
* @param {Array.<String>} modRef - array containing the mod artifact ids.
|
||||
* @param {String} parentList - parent ModList file path, null if none.
|
||||
*/
|
||||
constructor(repositoryRoot, modRef, parentList){
|
||||
if(!arguments.length){
|
||||
this.repositoryRoot = ''
|
||||
this.modRef = []
|
||||
}
|
||||
this.repositoryRoot
|
||||
this.modRef = modRef
|
||||
if(parentList != null) this.parentList = parentList
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a ModList object to the specified file path.
|
||||
*
|
||||
* @param {ModList} modList - the ModList object to export.
|
||||
* @param {String} filePath - desired filepath.
|
||||
* @returns {Promise.<String>} - a promise which resolves FML modList argument.
|
||||
*/
|
||||
static exportModList(modList, filePath){
|
||||
return new Promise(function(resolve, reject){
|
||||
fs.writeFile(filePath, JSON.stringify(modList), (err) => {
|
||||
if(err){
|
||||
reject(err.message)
|
||||
}
|
||||
resolve('--modListFile ' + filePath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} distro - the distribution index.
|
||||
*/
|
||||
static generateModList(distro){
|
||||
|
||||
}
|
||||
|
||||
}
|
67
app/assets/js/preloader.js
Normal file
@ -0,0 +1,67 @@
|
||||
const {ipcRenderer} = require('electron')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
const ConfigManager = require('./configmanager')
|
||||
const { DistroAPI } = require('./distromanager')
|
||||
const LangLoader = require('./langloader')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { HeliosDistribution } = require('helios-core/common')
|
||||
|
||||
const logger = LoggerUtil.getLogger('Preloader')
|
||||
|
||||
logger.info('Loading..')
|
||||
|
||||
// Load ConfigManager
|
||||
ConfigManager.load()
|
||||
|
||||
// Yuck!
|
||||
// TODO Fix this
|
||||
DistroAPI['commonDir'] = ConfigManager.getCommonDirectory()
|
||||
DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory()
|
||||
|
||||
// Load Strings
|
||||
LangLoader.setupLanguage()
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HeliosDistribution} data
|
||||
*/
|
||||
function onDistroLoad(data){
|
||||
if(data != null){
|
||||
|
||||
// Resolve the selected server if its value has yet to be set.
|
||||
if(ConfigManager.getSelectedServer() == null || data.getServerById(ConfigManager.getSelectedServer()) == null){
|
||||
logger.info('Determining default selected server..')
|
||||
ConfigManager.setSelectedServer(data.getMainServer().rawServer.id)
|
||||
ConfigManager.save()
|
||||
}
|
||||
}
|
||||
ipcRenderer.send('distributionIndexDone', data != null)
|
||||
}
|
||||
|
||||
// Ensure Distribution is downloaded and cached.
|
||||
DistroAPI.getDistribution()
|
||||
.then(heliosDistro => {
|
||||
logger.info('Loaded distribution index.')
|
||||
|
||||
onDistroLoad(heliosDistro)
|
||||
})
|
||||
.catch(err => {
|
||||
logger.info('Failed to load an older version of the distribution index.')
|
||||
logger.info('Application cannot run.')
|
||||
logger.error(err)
|
||||
|
||||
onDistroLoad(null)
|
||||
})
|
||||
|
||||
// Clean up temp dir incase previous launches ended unexpectedly.
|
||||
fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => {
|
||||
if(err){
|
||||
logger.warn('Error while cleaning natives directory', err)
|
||||
} else {
|
||||
logger.info('Cleaned natives directory.')
|
||||
}
|
||||
})
|
888
app/assets/js/processbuilder.js
Normal file
@ -0,0 +1,888 @@
|
||||
const AdmZip = require('adm-zip')
|
||||
const child_process = require('child_process')
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs-extra')
|
||||
const { LoggerUtil } = require('helios-core')
|
||||
const { getMojangOS, isLibraryCompatible, mcVersionAtLeast } = require('helios-core/common')
|
||||
const { Type } = require('helios-distribution-types')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
|
||||
const ConfigManager = require('./configmanager')
|
||||
|
||||
const logger = LoggerUtil.getLogger('ProcessBuilder')
|
||||
|
||||
|
||||
/**
|
||||
* Only forge and fabric are top level mod loaders.
|
||||
*
|
||||
* Forge 1.13+ launch logic is similar to fabrics, for now using usingFabricLoader flag to
|
||||
* change minor details when needed.
|
||||
*
|
||||
* Rewrite of this module may be needed in the future.
|
||||
*/
|
||||
class ProcessBuilder {
|
||||
|
||||
constructor(distroServer, vanillaManifest, modManifest, authUser, launcherVersion){
|
||||
this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.rawServer.id)
|
||||
this.commonDir = ConfigManager.getCommonDirectory()
|
||||
this.server = distroServer
|
||||
this.vanillaManifest = vanillaManifest
|
||||
this.modManifest = modManifest
|
||||
this.authUser = authUser
|
||||
this.launcherVersion = launcherVersion
|
||||
this.forgeModListFile = path.join(this.gameDir, 'forgeMods.list') // 1.13+
|
||||
this.fmlDir = path.join(this.gameDir, 'forgeModList.json')
|
||||
this.llDir = path.join(this.gameDir, 'liteloaderModList.json')
|
||||
this.libPath = path.join(this.commonDir, 'libraries')
|
||||
|
||||
this.usingLiteLoader = false
|
||||
this.usingFabricLoader = false
|
||||
this.llPath = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convienence method to run the functions typically used to build a process.
|
||||
*/
|
||||
build(){
|
||||
fs.ensureDirSync(this.gameDir)
|
||||
const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex'))
|
||||
process.throwDeprecation = true
|
||||
this.setupLiteLoader()
|
||||
logger.info('Using liteloader:', this.usingLiteLoader)
|
||||
this.usingFabricLoader = this.server.modules.some(mdl => mdl.rawModule.type === Type.Fabric)
|
||||
logger.info('Using fabric loader:', this.usingFabricLoader)
|
||||
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.rawServer.id).mods, this.server.modules)
|
||||
|
||||
// Mod list below 1.13
|
||||
// Fabric only supports 1.14+
|
||||
if(!mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
|
||||
this.constructJSONModList('forge', modObj.fMods, true)
|
||||
if(this.usingLiteLoader){
|
||||
this.constructJSONModList('liteloader', modObj.lMods, true)
|
||||
}
|
||||
}
|
||||
|
||||
const uberModArr = modObj.fMods.concat(modObj.lMods)
|
||||
let args = this.constructJVMArguments(uberModArr, tempNativePath)
|
||||
|
||||
if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
|
||||
//args = args.concat(this.constructModArguments(modObj.fMods))
|
||||
args = args.concat(this.constructModList(modObj.fMods))
|
||||
}
|
||||
|
||||
logger.info('Launch Arguments:', args)
|
||||
|
||||
const child = child_process.spawn(ConfigManager.getJavaExecutable(this.server.rawServer.id), args, {
|
||||
cwd: this.gameDir,
|
||||
detached: ConfigManager.getLaunchDetached()
|
||||
})
|
||||
|
||||
if(ConfigManager.getLaunchDetached()){
|
||||
child.unref()
|
||||
}
|
||||
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stderr.setEncoding('utf8')
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
data.trim().split('\n').forEach(x => console.log(`\x1b[32m[Minecraft]\x1b[0m ${x}`))
|
||||
|
||||
})
|
||||
child.stderr.on('data', (data) => {
|
||||
data.trim().split('\n').forEach(x => console.log(`\x1b[31m[Minecraft]\x1b[0m ${x}`))
|
||||
})
|
||||
child.on('close', (code, signal) => {
|
||||
logger.info('Exited with code', code)
|
||||
fs.remove(tempNativePath, (err) => {
|
||||
if(err){
|
||||
logger.warn('Error while deleting temp dir', err)
|
||||
} else {
|
||||
logger.info('Temp dir deleted successfully.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the platform specific classpath separator. On windows, this is a semicolon.
|
||||
* On Unix, this is a colon.
|
||||
*
|
||||
* @returns {string} The classpath separator for the current operating system.
|
||||
*/
|
||||
static getClasspathSeparator() {
|
||||
return process.platform === 'win32' ? ';' : ':'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an optional mod is enabled from its configuration value. If the
|
||||
* configuration value is null, the required object will be used to
|
||||
* determine if it is enabled.
|
||||
*
|
||||
* A mod is enabled if:
|
||||
* * The configuration is not null and one of the following:
|
||||
* * The configuration is a boolean and true.
|
||||
* * The configuration is an object and its 'value' property is true.
|
||||
* * The configuration is null and one of the following:
|
||||
* * The required object is null.
|
||||
* * The required object's 'def' property is null or true.
|
||||
*
|
||||
* @param {Object | boolean} modCfg The mod configuration object.
|
||||
* @param {Object} required Optional. The required object from the mod's distro declaration.
|
||||
* @returns {boolean} True if the mod is enabled, false otherwise.
|
||||
*/
|
||||
static isModEnabled(modCfg, required = null){
|
||||
return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.def : true
|
||||
}
|
||||
|
||||
/**
|
||||
* Function which performs a preliminary scan of the top level
|
||||
* mods. If liteloader is present here, we setup the special liteloader
|
||||
* launch options. Note that liteloader is only allowed as a top level
|
||||
* mod. It must not be declared as a submodule.
|
||||
*/
|
||||
setupLiteLoader(){
|
||||
for(let ll of this.server.modules){
|
||||
if(ll.rawModule.type === Type.LiteLoader){
|
||||
if(!ll.getRequired().value){
|
||||
const modCfg = ConfigManager.getModConfiguration(this.server.rawServer.id).mods
|
||||
if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessMavenIdentifier()], ll.getRequired())){
|
||||
if(fs.existsSync(ll.getPath())){
|
||||
this.usingLiteLoader = true
|
||||
this.llPath = ll.getPath()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(fs.existsSync(ll.getPath())){
|
||||
this.usingLiteLoader = true
|
||||
this.llPath = ll.getPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an array of all enabled mods. These mods will be constructed into
|
||||
* a mod list format and enabled at launch.
|
||||
*
|
||||
* @param {Object} modCfg The mod configuration object.
|
||||
* @param {Array.<Object>} mdls An array of modules to parse.
|
||||
* @returns {{fMods: Array.<Object>, lMods: Array.<Object>}} An object which contains
|
||||
* a list of enabled forge mods and litemods.
|
||||
*/
|
||||
resolveModConfiguration(modCfg, mdls){
|
||||
let fMods = []
|
||||
let lMods = []
|
||||
|
||||
for(let mdl of mdls){
|
||||
const type = mdl.rawModule.type
|
||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
||||
const o = !mdl.getRequired().value
|
||||
const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessMavenIdentifier()], mdl.getRequired())
|
||||
if(!o || (o && e)){
|
||||
if(mdl.subModules.length > 0){
|
||||
const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessMavenIdentifier()].mods, mdl.subModules)
|
||||
fMods = fMods.concat(v.fMods)
|
||||
lMods = lMods.concat(v.lMods)
|
||||
if(type === Type.LiteLoader){
|
||||
continue
|
||||
}
|
||||
}
|
||||
if(type === Type.ForgeMod || type === Type.FabricMod){
|
||||
fMods.push(mdl)
|
||||
} else {
|
||||
lMods.push(mdl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fMods,
|
||||
lMods
|
||||
}
|
||||
}
|
||||
|
||||
_lteMinorVersion(version) {
|
||||
return Number(this.modManifest.id.split('-')[0].split('.')[1]) <= Number(version)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if this version of forge requires the absolute: prefix
|
||||
* on the modListFile repository field.
|
||||
*/
|
||||
_requiresAbsolute(){
|
||||
try {
|
||||
if(this._lteMinorVersion(9)) {
|
||||
return false
|
||||
}
|
||||
const ver = this.modManifest.id.split('-')[2]
|
||||
const pts = ver.split('.')
|
||||
const min = [14, 23, 3, 2655]
|
||||
for(let i=0; i<pts.length; i++){
|
||||
const parsed = Number.parseInt(pts[i])
|
||||
if(parsed < min[i]){
|
||||
return false
|
||||
} else if(parsed > min[i]){
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We know old forge versions follow this format.
|
||||
// Error must be caused by newer version.
|
||||
}
|
||||
|
||||
// Equal or errored
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a mod list json object.
|
||||
*
|
||||
* @param {'forge' | 'liteloader'} type The mod list type to construct.
|
||||
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
||||
* @param {boolean} save Optional. Whether or not we should save the mod list file.
|
||||
*/
|
||||
constructJSONModList(type, mods, save = false){
|
||||
const modList = {
|
||||
repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + path.join(this.commonDir, 'modstore')
|
||||
}
|
||||
|
||||
const ids = []
|
||||
if(type === 'forge'){
|
||||
for(let mod of mods){
|
||||
ids.push(mod.getExtensionlessMavenIdentifier())
|
||||
}
|
||||
} else {
|
||||
for(let mod of mods){
|
||||
ids.push(mod.getMavenIdentifier())
|
||||
}
|
||||
}
|
||||
modList.modRef = ids
|
||||
|
||||
if(save){
|
||||
const json = JSON.stringify(modList, null, 4)
|
||||
fs.writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8')
|
||||
}
|
||||
|
||||
return modList
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Construct the mod argument list for forge 1.13
|
||||
// *
|
||||
// * @param {Array.<Object>} mods An array of mods to add to the mod list.
|
||||
// */
|
||||
// constructModArguments(mods){
|
||||
// const argStr = mods.map(mod => {
|
||||
// return mod.getExtensionlessMavenIdentifier()
|
||||
// }).join(',')
|
||||
|
||||
// if(argStr){
|
||||
// return [
|
||||
// '--fml.mavenRoots',
|
||||
// path.join('..', '..', 'common', 'modstore'),
|
||||
// '--fml.mods',
|
||||
// argStr
|
||||
// ]
|
||||
// } else {
|
||||
// return []
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
/**
|
||||
* Construct the mod argument list for forge 1.13 and Fabric
|
||||
*
|
||||
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
||||
*/
|
||||
constructModList(mods) {
|
||||
const writeBuffer = mods.map(mod => {
|
||||
return this.usingFabricLoader ? mod.getPath() : mod.getExtensionlessMavenIdentifier()
|
||||
}).join('\n')
|
||||
|
||||
if(writeBuffer) {
|
||||
fs.writeFileSync(this.forgeModListFile, writeBuffer, 'UTF-8')
|
||||
return this.usingFabricLoader ? [
|
||||
'--fabric.addMods',
|
||||
`@${this.forgeModListFile}`
|
||||
] : [
|
||||
'--fml.mavenRoots',
|
||||
path.join('..', '..', 'common', 'modstore'),
|
||||
'--fml.modLists',
|
||||
this.forgeModListFile
|
||||
]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_processAutoConnectArg(args){
|
||||
if(ConfigManager.getAutoConnect() && this.server.rawServer.autoconnect){
|
||||
if(mcVersionAtLeast('1.20', this.server.rawServer.minecraftVersion)){
|
||||
args.push('--quickPlayMultiplayer')
|
||||
args.push(`${this.server.hostname}:${this.server.port}`)
|
||||
} else {
|
||||
args.push('--server')
|
||||
args.push(this.server.hostname)
|
||||
args.push('--port')
|
||||
args.push(this.server.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the argument array that will be passed to the JVM process.
|
||||
*
|
||||
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
|
||||
* @param {string} tempNativePath The path to store the native libraries.
|
||||
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
|
||||
*/
|
||||
constructJVMArguments(mods, tempNativePath){
|
||||
if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
|
||||
return this._constructJVMArguments113(mods, tempNativePath)
|
||||
} else {
|
||||
return this._constructJVMArguments112(mods, tempNativePath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the argument array that will be passed to the JVM process.
|
||||
* This function is for 1.12 and below.
|
||||
*
|
||||
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
|
||||
* @param {string} tempNativePath The path to store the native libraries.
|
||||
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
|
||||
*/
|
||||
_constructJVMArguments112(mods, tempNativePath){
|
||||
|
||||
let args = []
|
||||
|
||||
// Classpath Argument
|
||||
args.push('-cp')
|
||||
args.push(this.classpathArg(mods, tempNativePath).join(ProcessBuilder.getClasspathSeparator()))
|
||||
|
||||
// Java Arguments
|
||||
if(process.platform === 'darwin'){
|
||||
args.push('-Xdock:name=onimairu-mc_launcher')
|
||||
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
|
||||
}
|
||||
args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id))
|
||||
args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id))
|
||||
args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id))
|
||||
args.push('-Djava.library.path=' + tempNativePath)
|
||||
|
||||
// Main Java Class
|
||||
args.push(this.modManifest.mainClass)
|
||||
|
||||
// Forge Arguments
|
||||
args = args.concat(this._resolveForgeArgs())
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the argument array that will be passed to the JVM process.
|
||||
* This function is for 1.13+
|
||||
*
|
||||
* Note: Required Libs https://github.com/MinecraftForge/MinecraftForge/blob/af98088d04186452cb364280340124dfd4766a5c/src/fmllauncher/java/net/minecraftforge/fml/loading/LibraryFinder.java#L82
|
||||
*
|
||||
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
|
||||
* @param {string} tempNativePath The path to store the native libraries.
|
||||
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
|
||||
*/
|
||||
_constructJVMArguments113(mods, tempNativePath){
|
||||
|
||||
const argDiscovery = /\${*(.*)}/
|
||||
|
||||
// JVM Arguments First
|
||||
let args = this.vanillaManifest.arguments.jvm
|
||||
|
||||
// Debug securejarhandler
|
||||
// args.push('-Dbsl.debug=true')
|
||||
|
||||
if(this.modManifest.arguments.jvm != null) {
|
||||
for(const argStr of this.modManifest.arguments.jvm) {
|
||||
args.push(argStr
|
||||
.replaceAll('${library_directory}', this.libPath)
|
||||
.replaceAll('${classpath_separator}', ProcessBuilder.getClasspathSeparator())
|
||||
.replaceAll('${version_name}', this.modManifest.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//args.push('-Dlog4j.configurationFile=D:\\WesterosCraft\\game\\common\\assets\\log_configs\\client-1.12.xml')
|
||||
|
||||
// Java Arguments
|
||||
if(process.platform === 'darwin'){
|
||||
args.push('-Xdock:name=onimairu-mc_launcher')
|
||||
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
|
||||
}
|
||||
args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id))
|
||||
args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id))
|
||||
args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id))
|
||||
|
||||
// Main Java Class
|
||||
args.push(this.modManifest.mainClass)
|
||||
|
||||
// Vanilla Arguments
|
||||
args = args.concat(this.vanillaManifest.arguments.game)
|
||||
|
||||
for(let i=0; i<args.length; i++){
|
||||
if(typeof args[i] === 'object' && args[i].rules != null){
|
||||
|
||||
let checksum = 0
|
||||
for(let rule of args[i].rules){
|
||||
if(rule.os != null){
|
||||
if(rule.os.name === getMojangOS()
|
||||
&& (rule.os.version == null || new RegExp(rule.os.version).test(os.release))){
|
||||
if(rule.action === 'allow'){
|
||||
checksum++
|
||||
}
|
||||
} else {
|
||||
if(rule.action === 'disallow'){
|
||||
checksum++
|
||||
}
|
||||
}
|
||||
} else if(rule.features != null){
|
||||
// We don't have many 'features' in the index at the moment.
|
||||
// This should be fine for a while.
|
||||
if(rule.features.has_custom_resolution != null && rule.features.has_custom_resolution === true){
|
||||
if(ConfigManager.getFullscreen()){
|
||||
args[i].value = [
|
||||
'--fullscreen',
|
||||
'true'
|
||||
]
|
||||
}
|
||||
checksum++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO splice not push
|
||||
if(checksum === args[i].rules.length){
|
||||
if(typeof args[i].value === 'string'){
|
||||
args[i] = args[i].value
|
||||
} else if(typeof args[i].value === 'object'){
|
||||
//args = args.concat(args[i].value)
|
||||
args.splice(i, 1, ...args[i].value)
|
||||
}
|
||||
|
||||
// Decrement i to reprocess the resolved value
|
||||
i--
|
||||
} else {
|
||||
args[i] = null
|
||||
}
|
||||
|
||||
} else if(typeof args[i] === 'string'){
|
||||
if(argDiscovery.test(args[i])){
|
||||
const identifier = args[i].match(argDiscovery)[1]
|
||||
let val = null
|
||||
switch(identifier){
|
||||
case 'auth_player_name':
|
||||
val = this.authUser.displayName.trim()
|
||||
break
|
||||
case 'version_name':
|
||||
//val = vanillaManifest.id
|
||||
val = this.server.rawServer.id
|
||||
break
|
||||
case 'game_directory':
|
||||
val = this.gameDir
|
||||
break
|
||||
case 'assets_root':
|
||||
val = path.join(this.commonDir, 'assets')
|
||||
break
|
||||
case 'assets_index_name':
|
||||
val = this.vanillaManifest.assets
|
||||
break
|
||||
case 'auth_uuid':
|
||||
val = this.authUser.uuid.trim()
|
||||
break
|
||||
case 'auth_access_token':
|
||||
val = this.authUser.accessToken
|
||||
break
|
||||
case 'user_type':
|
||||
val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
|
||||
break
|
||||
case 'version_type':
|
||||
val = this.vanillaManifest.type
|
||||
break
|
||||
case 'resolution_width':
|
||||
val = ConfigManager.getGameWidth()
|
||||
break
|
||||
case 'resolution_height':
|
||||
val = ConfigManager.getGameHeight()
|
||||
break
|
||||
case 'natives_directory':
|
||||
val = args[i].replace(argDiscovery, tempNativePath)
|
||||
break
|
||||
case 'launcher_name':
|
||||
val = args[i].replace(argDiscovery, 'Helios-Launcher')
|
||||
break
|
||||
case 'launcher_version':
|
||||
val = args[i].replace(argDiscovery, this.launcherVersion)
|
||||
break
|
||||
case 'classpath':
|
||||
val = this.classpathArg(mods, tempNativePath).join(ProcessBuilder.getClasspathSeparator())
|
||||
break
|
||||
}
|
||||
if(val != null){
|
||||
args[i] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Autoconnect
|
||||
this._processAutoConnectArg(args)
|
||||
|
||||
|
||||
// Forge Specific Arguments
|
||||
args = args.concat(this.modManifest.arguments.game)
|
||||
|
||||
// Filter null values
|
||||
args = args.filter(arg => {
|
||||
return arg != null
|
||||
})
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the arguments required by forge.
|
||||
*
|
||||
* @returns {Array.<string>} An array containing the arguments required by forge.
|
||||
*/
|
||||
_resolveForgeArgs(){
|
||||
const mcArgs = this.modManifest.minecraftArguments.split(' ')
|
||||
const argDiscovery = /\${*(.*)}/
|
||||
|
||||
// Replace the declared variables with their proper values.
|
||||
for(let i=0; i<mcArgs.length; ++i){
|
||||
if(argDiscovery.test(mcArgs[i])){
|
||||
const identifier = mcArgs[i].match(argDiscovery)[1]
|
||||
let val = null
|
||||
switch(identifier){
|
||||
case 'auth_player_name':
|
||||
val = this.authUser.displayName.trim()
|
||||
break
|
||||
case 'version_name':
|
||||
//val = vanillaManifest.id
|
||||
val = this.server.rawServer.id
|
||||
break
|
||||
case 'game_directory':
|
||||
val = this.gameDir
|
||||
break
|
||||
case 'assets_root':
|
||||
val = path.join(this.commonDir, 'assets')
|
||||
break
|
||||
case 'assets_index_name':
|
||||
val = this.vanillaManifest.assets
|
||||
break
|
||||
case 'auth_uuid':
|
||||
val = this.authUser.uuid.trim()
|
||||
break
|
||||
case 'auth_access_token':
|
||||
val = this.authUser.accessToken
|
||||
break
|
||||
case 'user_type':
|
||||
val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
|
||||
break
|
||||
case 'user_properties': // 1.8.9 and below.
|
||||
val = '{}'
|
||||
break
|
||||
case 'version_type':
|
||||
val = this.vanillaManifest.type
|
||||
break
|
||||
}
|
||||
if(val != null){
|
||||
mcArgs[i] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Autoconnect to the selected server.
|
||||
this._processAutoConnectArg(mcArgs)
|
||||
|
||||
// Prepare game resolution
|
||||
if(ConfigManager.getFullscreen()){
|
||||
mcArgs.push('--fullscreen')
|
||||
mcArgs.push(true)
|
||||
} else {
|
||||
mcArgs.push('--width')
|
||||
mcArgs.push(ConfigManager.getGameWidth())
|
||||
mcArgs.push('--height')
|
||||
mcArgs.push(ConfigManager.getGameHeight())
|
||||
}
|
||||
|
||||
// Mod List File Argument
|
||||
mcArgs.push('--modListFile')
|
||||
if(this._lteMinorVersion(9)) {
|
||||
mcArgs.push(path.basename(this.fmlDir))
|
||||
} else {
|
||||
mcArgs.push('absolute:' + this.fmlDir)
|
||||
}
|
||||
|
||||
|
||||
// LiteLoader
|
||||
if(this.usingLiteLoader){
|
||||
mcArgs.push('--modRepo')
|
||||
mcArgs.push(this.llDir)
|
||||
|
||||
// Set first arg to liteloader tweak class
|
||||
mcArgs.unshift('com.mumfrey.liteloader.launch.LiteLoaderTweaker')
|
||||
mcArgs.unshift('--tweakClass')
|
||||
}
|
||||
|
||||
return mcArgs
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the classpath entries all point to jar files.
|
||||
*
|
||||
* @param {Array.<String>} list Array of classpath entries.
|
||||
*/
|
||||
_processClassPathList(list) {
|
||||
|
||||
const ext = '.jar'
|
||||
const extLen = ext.length
|
||||
for(let i=0; i<list.length; i++) {
|
||||
const extIndex = list[i].indexOf(ext)
|
||||
if(extIndex > -1 && extIndex !== list[i].length - extLen) {
|
||||
list[i] = list[i].substring(0, extIndex + extLen)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared
|
||||
* libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries,
|
||||
* this method requires all enabled mods as an input
|
||||
*
|
||||
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
|
||||
* @param {string} tempNativePath The path to store the native libraries.
|
||||
* @returns {Array.<string>} An array containing the paths of each library required by this process.
|
||||
*/
|
||||
classpathArg(mods, tempNativePath){
|
||||
let cpArgs = []
|
||||
|
||||
if(!mcVersionAtLeast('1.17', this.server.rawServer.minecraftVersion) || this.usingFabricLoader) {
|
||||
// Add the version.jar to the classpath.
|
||||
// Must not be added to the classpath for Forge 1.17+.
|
||||
const version = this.vanillaManifest.id
|
||||
cpArgs.push(path.join(this.commonDir, 'versions', version, version + '.jar'))
|
||||
}
|
||||
|
||||
|
||||
if(this.usingLiteLoader){
|
||||
cpArgs.push(this.llPath)
|
||||
}
|
||||
|
||||
// Resolve the Mojang declared libraries.
|
||||
const mojangLibs = this._resolveMojangLibraries(tempNativePath)
|
||||
|
||||
// Resolve the server declared libraries.
|
||||
const servLibs = this._resolveServerLibraries(mods)
|
||||
|
||||
// Merge libraries, server libs with the same
|
||||
// maven identifier will override the mojang ones.
|
||||
// Ex. 1.7.10 forge overrides mojang's guava with newer version.
|
||||
const finalLibs = {...mojangLibs, ...servLibs}
|
||||
cpArgs = cpArgs.concat(Object.values(finalLibs))
|
||||
|
||||
this._processClassPathList(cpArgs)
|
||||
|
||||
return cpArgs
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the libraries defined by Mojang's version data. This method will also extract
|
||||
* native libraries and point to the correct location for its classpath.
|
||||
*
|
||||
* TODO - clean up function
|
||||
*
|
||||
* @param {string} tempNativePath The path to store the native libraries.
|
||||
* @returns {{[id: string]: string}} An object containing the paths of each library mojang declares.
|
||||
*/
|
||||
_resolveMojangLibraries(tempNativePath){
|
||||
const nativesRegex = /.+:natives-([^-]+)(?:-(.+))?/
|
||||
const libs = {}
|
||||
|
||||
const libArr = this.vanillaManifest.libraries
|
||||
fs.ensureDirSync(tempNativePath)
|
||||
for(let i=0; i<libArr.length; i++){
|
||||
const lib = libArr[i]
|
||||
if(isLibraryCompatible(lib.rules, lib.natives)){
|
||||
|
||||
// Pre-1.19 has a natives object.
|
||||
if(lib.natives != null) {
|
||||
// Extract the native library.
|
||||
const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/']
|
||||
const artifact = lib.downloads.classifiers[lib.natives[getMojangOS()].replace('${arch}', process.arch.replace('x', ''))]
|
||||
|
||||
// Location of native zip.
|
||||
const to = path.join(this.libPath, artifact.path)
|
||||
|
||||
let zip = new AdmZip(to)
|
||||
let zipEntries = zip.getEntries()
|
||||
|
||||
// Unzip the native zip.
|
||||
for(let i=0; i<zipEntries.length; i++){
|
||||
const fileName = zipEntries[i].entryName
|
||||
|
||||
let shouldExclude = false
|
||||
|
||||
// Exclude noted files.
|
||||
exclusionArr.forEach(function(exclusion){
|
||||
if(fileName.indexOf(exclusion) > -1){
|
||||
shouldExclude = true
|
||||
}
|
||||
})
|
||||
|
||||
// Extract the file.
|
||||
if(!shouldExclude){
|
||||
fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
|
||||
if(err){
|
||||
logger.error('Error while extracting native library:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// 1.19+ logic
|
||||
else if(lib.name.includes('natives-')) {
|
||||
|
||||
const regexTest = nativesRegex.exec(lib.name)
|
||||
// const os = regexTest[1]
|
||||
const arch = regexTest[2] ?? 'x64'
|
||||
|
||||
if(arch != process.arch) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the native library.
|
||||
const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/', '.git', '.sha1']
|
||||
const artifact = lib.downloads.artifact
|
||||
|
||||
// Location of native zip.
|
||||
const to = path.join(this.libPath, artifact.path)
|
||||
|
||||
let zip = new AdmZip(to)
|
||||
let zipEntries = zip.getEntries()
|
||||
|
||||
// Unzip the native zip.
|
||||
for(let i=0; i<zipEntries.length; i++){
|
||||
if(zipEntries[i].isDirectory) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fileName = zipEntries[i].entryName
|
||||
|
||||
let shouldExclude = false
|
||||
|
||||
// Exclude noted files.
|
||||
exclusionArr.forEach(function(exclusion){
|
||||
if(fileName.indexOf(exclusion) > -1){
|
||||
shouldExclude = true
|
||||
}
|
||||
})
|
||||
|
||||
const extractName = fileName.includes('/') ? fileName.substring(fileName.lastIndexOf('/')) : fileName
|
||||
|
||||
// Extract the file.
|
||||
if(!shouldExclude){
|
||||
fs.writeFile(path.join(tempNativePath, extractName), zipEntries[i].getData(), (err) => {
|
||||
if(err){
|
||||
logger.error('Error while extracting native library:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// No natives
|
||||
else {
|
||||
const dlInfo = lib.downloads
|
||||
const artifact = dlInfo.artifact
|
||||
const to = path.join(this.libPath, artifact.path)
|
||||
const versionIndependentId = lib.name.substring(0, lib.name.lastIndexOf(':'))
|
||||
libs[versionIndependentId] = to
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return libs
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the libraries declared by this server in order to add them to the classpath.
|
||||
* This method will also check each enabled mod for libraries, as mods are permitted to
|
||||
* declare libraries.
|
||||
*
|
||||
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
|
||||
* @returns {{[id: string]: string}} An object containing the paths of each library this server requires.
|
||||
*/
|
||||
_resolveServerLibraries(mods){
|
||||
const mdls = this.server.modules
|
||||
let libs = {}
|
||||
|
||||
// Locate Forge/Fabric/Libraries
|
||||
for(let mdl of mdls){
|
||||
const type = mdl.rawModule.type
|
||||
if(type === Type.ForgeHosted || type === Type.Fabric || type === Type.Library){
|
||||
libs[mdl.getVersionlessMavenIdentifier()] = mdl.getPath()
|
||||
if(mdl.subModules.length > 0){
|
||||
const res = this._resolveModuleLibraries(mdl)
|
||||
libs = {...libs, ...res}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Check for any libraries in our mod list.
|
||||
for(let i=0; i<mods.length; i++){
|
||||
if(mods.sub_modules != null){
|
||||
const res = this._resolveModuleLibraries(mods[i])
|
||||
libs = {...libs, ...res}
|
||||
}
|
||||
}
|
||||
|
||||
return libs
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively resolve the path of each library required by this module.
|
||||
*
|
||||
* @param {Object} mdl A module object from the server distro index.
|
||||
* @returns {{[id: string]: string}} An object containing the paths of each library this module requires.
|
||||
*/
|
||||
_resolveModuleLibraries(mdl){
|
||||
if(!mdl.subModules.length > 0){
|
||||
return {}
|
||||
}
|
||||
let libs = {}
|
||||
for(let sm of mdl.subModules){
|
||||
if(sm.rawModule.type === Type.Library){
|
||||
|
||||
if(sm.rawModule.classpath ?? true) {
|
||||
libs[sm.getVersionlessMavenIdentifier()] = sm.getPath()
|
||||
}
|
||||
}
|
||||
// If this module has submodules, we need to resolve the libraries for those.
|
||||
// To avoid unnecessary recursive calls, base case is checked here.
|
||||
if(mdl.subModules.length > 0){
|
||||
const res = this._resolveModuleLibraries(sm)
|
||||
libs = {...libs, ...res}
|
||||
}
|
||||
}
|
||||
return libs
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ProcessBuilder
|
@ -1,103 +0,0 @@
|
||||
var $ = require('jQuery');
|
||||
const remote = require('electron').remote
|
||||
const shell = require('electron').shell
|
||||
const path = require('path')
|
||||
const os = require('os');
|
||||
const ag = require(path.join(__dirname, 'assets', 'js', 'assetguard.js'))
|
||||
|
||||
function timestamp(){
|
||||
let date = new Date();
|
||||
const month = date.getMonth() < 9 ? '0'.concat((date.getMonth()+1)) : date.getMonth()
|
||||
const day = date.getDate() < 10 ? '0'.concat(date.getDate()) : date.getDate();
|
||||
let hour = date.getHours() > 12 ? date.getHours() - 12 : date.getHours();
|
||||
hour = hour < 10 ? '0'.concat(hour) : hour
|
||||
const min = date.getMinutes() < 10 ? '0'.concat(date.getMinutes()) : date.getMinutes();
|
||||
const sec = date.getSeconds() < 10 ? '0'.concat(date.getSeconds()) : date.getSeconds();
|
||||
|
||||
return '[' + month + '/' + day + '/' + date.getFullYear() + ' ' + hour + ':' + min + ':' + sec + ']'
|
||||
}
|
||||
|
||||
$(document).on('ready', function(){
|
||||
$(".toggle-btn input[type=radio]").addClass("visuallyhidden");
|
||||
$(".toggle-btn input[type=radio]").change(function() {
|
||||
if($(this).attr("name")) {
|
||||
$(this).parent().addClass("success").siblings().removeClass("success")
|
||||
} else {
|
||||
$(this).parent().toggleClass("success")
|
||||
}
|
||||
})
|
||||
console.log = function(){
|
||||
$('#launcher-log').append(timestamp() + ' [Log] - ' + Array.prototype.slice.call(arguments).join(' ') + os.EOL)
|
||||
}
|
||||
console.error = function(){
|
||||
$('#launcher-log').append('<span class="log_debug">' + timestamp() + ' [Debug] - ' + Array.prototype.slice.call(arguments).join(' ') + "</span>" + os.EOL)
|
||||
}
|
||||
console.debug = function(){
|
||||
$('#launcher-log').append('<span class="log_debug">' + timestamp() + ' [Error] - ' + Array.prototype.slice.call(arguments).join(' ') + "</span>" + os.EOL)
|
||||
}
|
||||
console.log('test')
|
||||
console.debug('test')
|
||||
})
|
||||
|
||||
/* Open web links in the user's default browser. */
|
||||
$(document).on('click', 'a[href^="http"]', function(event) {
|
||||
event.preventDefault();
|
||||
testdownloads()
|
||||
//console.log(os.homedir())
|
||||
//shell.openExternal(this.href)
|
||||
});
|
||||
|
||||
|
||||
|
||||
testdownloads = async function(){
|
||||
const lp = require(path.join(__dirname, 'assets', 'js', 'launchprocess.js'))
|
||||
const basePath = path.join(__dirname, '..', 'mcfiles')
|
||||
let versionData = await ag.loadVersionData('1.11.2', basePath)
|
||||
await ag.validateAssets(versionData, basePath)
|
||||
console.log('assets done')
|
||||
await ag.validateLibraries(versionData, basePath)
|
||||
console.log('libs done')
|
||||
await ag.validateMiscellaneous(versionData, basePath)
|
||||
console.log('files done')
|
||||
await ag.validateDistribution('WesterosCraft-1.11.2', basePath)
|
||||
console.log('forge stuff done')
|
||||
ag.instance.on('dlcomplete', async function(){
|
||||
let forgeData = await ag.loadForgeData('WesterosCraft-1.11.2', basePath)
|
||||
lp.launchMinecraft(versionData, forgeData, basePath)
|
||||
//lp.launchMinecraft(versionData, basePath)
|
||||
})
|
||||
ag.processDlQueues()
|
||||
}
|
||||
|
||||
/*Opens DevTools window if you type "wcdev" in sequence.
|
||||
This will crash the program if you are using multiple
|
||||
DevTools, for example the chrome debugger in VS Code. */
|
||||
const match = [87, 67, 68, 69, 86]
|
||||
let at = 0;
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
switch(e.keyCode){
|
||||
case match[0]:
|
||||
if(at === 0) ++at
|
||||
break
|
||||
case match[1]:
|
||||
if(at === 1) ++at
|
||||
break
|
||||
case match[2]:
|
||||
if(at === 2) ++at
|
||||
break
|
||||
case match[3]:
|
||||
if(at === 3) ++at
|
||||
break
|
||||
case match[4]:
|
||||
if(at === 4) ++at
|
||||
break
|
||||
default:
|
||||
at = 0
|
||||
}
|
||||
if(at === 5) {
|
||||
var window = remote.getCurrentWindow()
|
||||
window.toggleDevTools()
|
||||
at = 0
|
||||
}
|
||||
})
|
1026
app/assets/js/scripts/landing.js
Normal file
213
app/assets/js/scripts/login.js
Normal file
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Script for login.ejs
|
||||
*/
|
||||
// Validation Regexes.
|
||||
const validUsername = /^[a-zA-Z0-9_]{1,16}$/
|
||||
const basicEmail = /^\S+@\S+\.\S+$/
|
||||
//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||
|
||||
// Login Elements
|
||||
const loginCancelContainer = document.getElementById('loginCancelContainer')
|
||||
const loginCancelButton = document.getElementById('loginCancelButton')
|
||||
const loginEmailError = document.getElementById('loginEmailError')
|
||||
const loginUsername = document.getElementById('loginUsername')
|
||||
const checkmarkContainer = document.getElementById('checkmarkContainer')
|
||||
const loginRememberOption = document.getElementById('loginRememberOption')
|
||||
const loginButton = document.getElementById('loginButton')
|
||||
const loginForm = document.getElementById('loginForm')
|
||||
|
||||
// Control variables.
|
||||
let lu = false, lp = false
|
||||
|
||||
|
||||
/**
|
||||
* Show a login error.
|
||||
*
|
||||
* @param {HTMLElement} element The element on which to display the error.
|
||||
* @param {string} value The error text.
|
||||
*/
|
||||
function showError(element, value){
|
||||
element.innerHTML = value
|
||||
element.style.opacity = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Shake a login error to add emphasis.
|
||||
*
|
||||
* @param {HTMLElement} element The element to shake.
|
||||
*/
|
||||
function shakeError(element){
|
||||
if(element.style.opacity == 1){
|
||||
element.classList.remove('shake')
|
||||
void element.offsetWidth
|
||||
element.classList.add('shake')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an email field is neither empty nor invalid.
|
||||
*
|
||||
* @param {string} value The email value.
|
||||
*/
|
||||
function validateEmail(value){
|
||||
if(value){
|
||||
if(!basicEmail.test(value) && !validUsername.test(value)){
|
||||
showError(loginEmailError, Lang.queryJS('login.error.invalidValue'))
|
||||
loginDisabled(true)
|
||||
lu = false
|
||||
} else {
|
||||
loginEmailError.style.opacity = 0
|
||||
lu = true
|
||||
if(lp){
|
||||
loginDisabled(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lu = false
|
||||
showError(loginEmailError, Lang.queryJS('login.error.requiredValue'))
|
||||
loginDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the password field is not empty.
|
||||
*
|
||||
* @param {string} value The password value.
|
||||
*/
|
||||
function validatePassword(value){
|
||||
lp = true;
|
||||
}
|
||||
|
||||
// Emphasize errors with shake when focus is lost.
|
||||
loginUsername.addEventListener('focusout', (e) => {
|
||||
validateEmail(e.target.value)
|
||||
shakeError(loginEmailError)
|
||||
})
|
||||
|
||||
// Validate input for each field.
|
||||
loginUsername.addEventListener('input', (e) => {
|
||||
loginDisabled(false)
|
||||
})
|
||||
|
||||
/**
|
||||
* Enable or disable the login button.
|
||||
*
|
||||
* @param {boolean} v True to enable, false to disable.
|
||||
*/
|
||||
function loginDisabled(v){
|
||||
if(loginButton.disabled !== v){
|
||||
loginButton.disabled = v
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable loading elements.
|
||||
*
|
||||
* @param {boolean} v True to enable, false to disable.
|
||||
*/
|
||||
function loginLoading(v){
|
||||
if(v){
|
||||
loginButton.setAttribute('loading', v)
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn'))
|
||||
} else {
|
||||
loginButton.removeAttribute('loading')
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable login form.
|
||||
*
|
||||
* @param {boolean} v True to enable, false to disable.
|
||||
*/
|
||||
function formDisabled(v){
|
||||
loginDisabled(v)
|
||||
loginCancelButton.disabled = v
|
||||
loginUsername.disabled = v
|
||||
if(v){
|
||||
checkmarkContainer.setAttribute('disabled', v)
|
||||
} else {
|
||||
checkmarkContainer.removeAttribute('disabled')
|
||||
}
|
||||
loginRememberOption.disabled = v
|
||||
}
|
||||
|
||||
let loginViewOnSuccess = VIEWS.landing
|
||||
let loginViewOnCancel = VIEWS.settings
|
||||
let loginViewCancelHandler
|
||||
|
||||
function loginCancelEnabled(val){
|
||||
if(val){
|
||||
$(loginCancelContainer).show()
|
||||
} else {
|
||||
$(loginCancelContainer).hide()
|
||||
}
|
||||
}
|
||||
|
||||
loginCancelButton.onclick = (e) => {
|
||||
switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => {
|
||||
loginUsername.value = ''
|
||||
loginPassword.value = ''
|
||||
loginCancelEnabled(false)
|
||||
if(loginViewCancelHandler != null){
|
||||
loginViewCancelHandler()
|
||||
loginViewCancelHandler = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Disable default form behavior.
|
||||
loginForm.onsubmit = () => { return false }
|
||||
|
||||
// Bind login button behavior.
|
||||
loginButton.addEventListener('click', () => {
|
||||
// Disable form.
|
||||
formDisabled(true)
|
||||
|
||||
// Show loading stuff.
|
||||
loginLoading(true)
|
||||
|
||||
AuthManager.addOfflineAccount(loginUsername.value).then((value) => {
|
||||
updateSelectedAccount(value)
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
|
||||
$('.circle-loader').toggleClass('load-complete')
|
||||
$('.checkmark').toggle()
|
||||
setTimeout(() => {
|
||||
switchView(VIEWS.login, loginViewOnSuccess, 500, 500, async () => {
|
||||
// Temporary workaround
|
||||
if(loginViewOnSuccess === VIEWS.settings){
|
||||
await prepareSettings()
|
||||
}
|
||||
loginViewOnSuccess = VIEWS.landing // Reset this for good measure.
|
||||
loginCancelEnabled(false) // Reset this for good measure.
|
||||
loginViewCancelHandler = null // Reset this for good measure.
|
||||
loginUsername.value = ''
|
||||
$('.circle-loader').toggleClass('load-complete')
|
||||
$('.checkmark').toggle()
|
||||
loginLoading(false)
|
||||
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login'))
|
||||
formDisabled(false)
|
||||
})
|
||||
}, 1000)
|
||||
}).catch((displayableError) => {
|
||||
loginLoading(false)
|
||||
|
||||
let actualDisplayableError
|
||||
if(isDisplayableError(displayableError)) {
|
||||
msftLoginLogger.error('Error while logging in.', displayableError)
|
||||
actualDisplayableError = displayableError
|
||||
} else {
|
||||
// Uh oh.
|
||||
msftLoginLogger.error('Unhandled error during login.', displayableError)
|
||||
actualDisplayableError = Lang.queryJS('login.error.unknown')
|
||||
}
|
||||
|
||||
setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
|
||||
setOverlayHandler(() => {
|
||||
formDisabled(false)
|
||||
toggleOverlay(false)
|
||||
})
|
||||
toggleOverlay(true)
|
||||
})
|
||||
|
||||
})
|
51
app/assets/js/scripts/loginOptions.js
Normal file
@ -0,0 +1,51 @@
|
||||
const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer')
|
||||
const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft')
|
||||
//const loginOptionMojang = document.getElementById('loginOptionMojang')
|
||||
const loginOptionOffline = document.getElementById('loginOptionOffline')
|
||||
const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton')
|
||||
|
||||
let loginOptionsCancellable = false
|
||||
|
||||
let loginOptionsViewOnLoginSuccess
|
||||
let loginOptionsViewOnLoginCancel
|
||||
let loginOptionsViewOnCancel
|
||||
let loginOptionsViewCancelHandler
|
||||
|
||||
function loginOptionsCancelEnabled(val){
|
||||
if(val){
|
||||
$(loginOptionsCancelContainer).show()
|
||||
} else {
|
||||
$(loginOptionsCancelContainer).hide()
|
||||
}
|
||||
}
|
||||
|
||||
loginOptionMicrosoft.onclick = (e) => {
|
||||
switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
|
||||
ipcRenderer.send(
|
||||
MSFT_OPCODE.OPEN_LOGIN,
|
||||
loginOptionsViewOnLoginSuccess,
|
||||
loginOptionsViewOnLoginCancel
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
loginOptionOffline.onclick = (e) => {
|
||||
switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
|
||||
loginViewOnSuccess = loginOptionsViewOnLoginSuccess
|
||||
loginViewOnCancel = loginOptionsViewOnLoginCancel
|
||||
loginCancelEnabled(true)
|
||||
})
|
||||
}
|
||||
|
||||
loginOptionsCancelButton.onclick = (e) => {
|
||||
switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => {
|
||||
// Clear login values (Mojang login)
|
||||
// No cleanup needed for Microsoft.
|
||||
loginUsername.value = ''
|
||||
loginPassword.value = ''
|
||||
if(loginOptionsViewCancelHandler != null){
|
||||
loginOptionsViewCancelHandler()
|
||||
loginOptionsViewCancelHandler = null
|
||||
}
|
||||
})
|
||||
}
|
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()
|
||||
}
|
1577
app/assets/js/scripts/settings.js
Normal file
467
app/assets/js/scripts/uibinder.js
Normal file
@ -0,0 +1,467 @@
|
||||
/**
|
||||
* Initialize UI functions which depend on internal modules.
|
||||
* Loaded after core UI functions are initialized in uicore.js.
|
||||
*/
|
||||
// Requirements
|
||||
const path = require('path')
|
||||
const { Type } = require('helios-distribution-types')
|
||||
|
||||
const AuthManager = require('./assets/js/authmanager')
|
||||
const ConfigManager = require('./assets/js/configmanager')
|
||||
const { DistroAPI } = require('./assets/js/distromanager')
|
||||
|
||||
let rscShouldLoad = false
|
||||
let fatalStartupError = false
|
||||
|
||||
// Mapping of each view to their container IDs.
|
||||
const VIEWS = {
|
||||
landing: '#landingContainer',
|
||||
loginOptions: '#loginOptionsContainer',
|
||||
loginOffline: '#loginOfflineContainer',
|
||||
login: '#loginContainer',
|
||||
settings: '#settingsContainer',
|
||||
welcome: '#welcomeContainer',
|
||||
waiting: '#waitingContainer'
|
||||
}
|
||||
|
||||
// The currently shown view container.
|
||||
let currentView
|
||||
|
||||
/**
|
||||
* Switch launcher views.
|
||||
*
|
||||
* @param {string} current The ID of the current view container.
|
||||
* @param {*} next The ID of the next view container.
|
||||
* @param {*} currentFadeTime Optional. The fade out time for the current view.
|
||||
* @param {*} nextFadeTime Optional. The fade in time for the next view.
|
||||
* @param {*} onCurrentFade Optional. Callback function to execute when the current
|
||||
* view fades out.
|
||||
* @param {*} onNextFade Optional. Callback function to execute when the next view
|
||||
* fades in.
|
||||
*/
|
||||
function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){
|
||||
currentView = next
|
||||
$(`${current}`).fadeOut(currentFadeTime, async () => {
|
||||
await onCurrentFade()
|
||||
$(`${next}`).fadeIn(nextFadeTime, async () => {
|
||||
await onNextFade()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently shown view container.
|
||||
*
|
||||
* @returns {string} The currently shown view container.
|
||||
*/
|
||||
function getCurrentView(){
|
||||
return currentView
|
||||
}
|
||||
|
||||
async function showMainUI(data){
|
||||
|
||||
if(!isDev){
|
||||
loggerAutoUpdater.info('Initializing..')
|
||||
ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease())
|
||||
}
|
||||
|
||||
await prepareSettings(true)
|
||||
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
|
||||
refreshServerStatus()
|
||||
setTimeout(() => {
|
||||
document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')`
|
||||
$('#main').show()
|
||||
|
||||
const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0
|
||||
|
||||
// If this is enabled in a development environment we'll get ratelimited.
|
||||
// The relaunch frequency is usually far too high.
|
||||
if(!isDev && isLoggedIn){
|
||||
validateSelectedAccount()
|
||||
}
|
||||
|
||||
if(ConfigManager.isFirstLaunch()){
|
||||
currentView = VIEWS.welcome
|
||||
$(VIEWS.welcome).fadeIn(1000)
|
||||
} else {
|
||||
if(isLoggedIn){
|
||||
currentView = VIEWS.landing
|
||||
$(VIEWS.landing).fadeIn(1000)
|
||||
} else {
|
||||
loginOptionsCancelEnabled(false)
|
||||
loginOptionsViewOnLoginSuccess = VIEWS.landing
|
||||
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
||||
currentView = VIEWS.loginOptions
|
||||
$(VIEWS.loginOptions).fadeIn(1000)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
$('#loadingContainer').fadeOut(500, () => {
|
||||
$('#loadSpinnerImage').removeClass('rotating')
|
||||
})
|
||||
}, 250)
|
||||
|
||||
}, 750)
|
||||
// Disable tabbing to the news container.
|
||||
initNews().then(() => {
|
||||
$('#newsContainer *').attr('tabindex', '-1')
|
||||
})
|
||||
}
|
||||
|
||||
function showFatalStartupError(){
|
||||
setTimeout(() => {
|
||||
$('#loadingContainer').fadeOut(250, () => {
|
||||
document.getElementById('overlayContainer').style.background = 'none'
|
||||
setOverlayContent(
|
||||
Lang.queryJS('uibinder.startup.fatalErrorTitle'),
|
||||
Lang.queryJS('uibinder.startup.fatalErrorMessage'),
|
||||
Lang.queryJS('uibinder.startup.closeButton')
|
||||
)
|
||||
setOverlayHandler(() => {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.close()
|
||||
})
|
||||
toggleOverlay(true)
|
||||
})
|
||||
}, 750)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common functions to perform after refreshing the distro index.
|
||||
*
|
||||
* @param {Object} data The distro index object.
|
||||
*/
|
||||
function onDistroRefresh(data){
|
||||
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
|
||||
refreshServerStatus()
|
||||
initNews()
|
||||
syncModConfigurations(data)
|
||||
ensureJavaSettings(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the mod configurations with the distro index.
|
||||
*
|
||||
* @param {Object} data The distro index object.
|
||||
*/
|
||||
function syncModConfigurations(data){
|
||||
|
||||
const syncedCfgs = []
|
||||
|
||||
for(let serv of data.servers){
|
||||
|
||||
const id = serv.rawServer.id
|
||||
const mdls = serv.modules
|
||||
const cfg = ConfigManager.getModConfiguration(id)
|
||||
|
||||
if(cfg != null){
|
||||
|
||||
const modsOld = cfg.mods
|
||||
const mods = {}
|
||||
|
||||
for(let mdl of mdls){
|
||||
const type = mdl.rawModule.type
|
||||
|
||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
||||
if(!mdl.getRequired().value){
|
||||
const mdlID = mdl.getVersionlessMavenIdentifier()
|
||||
if(modsOld[mdlID] == null){
|
||||
mods[mdlID] = scanOptionalSubModules(mdl.subModules, mdl)
|
||||
} else {
|
||||
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.subModules, mdl), false)
|
||||
}
|
||||
} else {
|
||||
if(mdl.subModules.length > 0){
|
||||
const mdlID = mdl.getVersionlessMavenIdentifier()
|
||||
const v = scanOptionalSubModules(mdl.subModules, mdl)
|
||||
if(typeof v === 'object'){
|
||||
if(modsOld[mdlID] == null){
|
||||
mods[mdlID] = v
|
||||
} else {
|
||||
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncedCfgs.push({
|
||||
id,
|
||||
mods
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
const mods = {}
|
||||
|
||||
for(let mdl of mdls){
|
||||
const type = mdl.rawModule.type
|
||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
||||
if(!mdl.getRequired().value){
|
||||
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
|
||||
} else {
|
||||
if(mdl.subModules.length > 0){
|
||||
const v = scanOptionalSubModules(mdl.subModules, mdl)
|
||||
if(typeof v === 'object'){
|
||||
mods[mdl.getVersionlessMavenIdentifier()] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncedCfgs.push({
|
||||
id,
|
||||
mods
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ConfigManager.setModConfigurations(syncedCfgs)
|
||||
ConfigManager.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure java configurations are present for the available servers.
|
||||
*
|
||||
* @param {Object} data The distro index object.
|
||||
*/
|
||||
function ensureJavaSettings(data) {
|
||||
|
||||
// Nothing too fancy for now.
|
||||
for(const serv of data.servers){
|
||||
ConfigManager.ensureJavaConfig(serv.rawServer.id, serv.effectiveJavaOptions, serv.rawServer.javaOptions?.ram)
|
||||
}
|
||||
|
||||
ConfigManager.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan for optional sub modules. If none are found,
|
||||
* this function returns a boolean. If optional sub modules do exist,
|
||||
* a recursive configuration object is returned.
|
||||
*
|
||||
* @returns {boolean | Object} The resolved mod configuration.
|
||||
*/
|
||||
function scanOptionalSubModules(mdls, origin){
|
||||
if(mdls != null){
|
||||
const mods = {}
|
||||
|
||||
for(let mdl of mdls){
|
||||
const type = mdl.rawModule.type
|
||||
// Optional types.
|
||||
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
|
||||
// It is optional.
|
||||
if(!mdl.getRequired().value){
|
||||
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
|
||||
} else {
|
||||
if(mdl.hasSubModules()){
|
||||
const v = scanOptionalSubModules(mdl.subModules, mdl)
|
||||
if(typeof v === 'object'){
|
||||
mods[mdl.getVersionlessMavenIdentifier()] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(mods).length > 0){
|
||||
const ret = {
|
||||
mods
|
||||
}
|
||||
if(!origin.getRequired().value){
|
||||
ret.value = origin.getRequired().def
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
return origin.getRequired().def
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merge an old configuration into a new configuration.
|
||||
*
|
||||
* @param {boolean | Object} o The old configuration value.
|
||||
* @param {boolean | Object} n The new configuration value.
|
||||
* @param {boolean} nReq If the new value is a required mod.
|
||||
*
|
||||
* @returns {boolean | Object} The merged configuration.
|
||||
*/
|
||||
function mergeModConfiguration(o, n, nReq = false){
|
||||
if(typeof o === 'boolean'){
|
||||
if(typeof n === 'boolean') return o
|
||||
else if(typeof n === 'object'){
|
||||
if(!nReq){
|
||||
n.value = o
|
||||
}
|
||||
return n
|
||||
}
|
||||
} else if(typeof o === 'object'){
|
||||
if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true
|
||||
else if(typeof n === 'object'){
|
||||
if(!nReq){
|
||||
n.value = typeof o.value !== 'undefined' ? o.value : true
|
||||
}
|
||||
|
||||
const newMods = Object.keys(n.mods)
|
||||
for(let i=0; i<newMods.length; i++){
|
||||
|
||||
const mod = newMods[i]
|
||||
if(o.mods[mod] != null){
|
||||
n.mods[mod] = mergeModConfiguration(o.mods[mod], n.mods[mod])
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
}
|
||||
// If for some reason we haven't been able to merge,
|
||||
// wipe the old value and use the new one. Just to be safe
|
||||
return n
|
||||
}
|
||||
|
||||
async function validateSelectedAccount(){
|
||||
const selectedAcc = ConfigManager.getSelectedAccount()
|
||||
if(selectedAcc != null){
|
||||
const val = await AuthManager.validateSelected()
|
||||
if(!val){
|
||||
ConfigManager.removeAuthAccount(selectedAcc.uuid)
|
||||
ConfigManager.save()
|
||||
const accLen = Object.keys(ConfigManager.getAuthAccounts()).length
|
||||
setOverlayContent(
|
||||
Lang.queryJS('uibinder.validateAccount.failedMessageTitle'),
|
||||
accLen > 0
|
||||
? Lang.queryJS('uibinder.validateAccount.failedMessage', { 'account': selectedAcc.displayName })
|
||||
: Lang.queryJS('uibinder.validateAccount.failedMessageSelectAnotherAccount', { 'account': selectedAcc.displayName }),
|
||||
Lang.queryJS('uibinder.validateAccount.loginButton'),
|
||||
Lang.queryJS('uibinder.validateAccount.selectAnotherAccountButton')
|
||||
)
|
||||
setOverlayHandler(() => {
|
||||
|
||||
const isMicrosoft = selectedAcc.type === 'microsoft'
|
||||
|
||||
if(isMicrosoft) {
|
||||
// Empty for now
|
||||
} else {
|
||||
// Mojang
|
||||
// For convenience, pre-populate the username of the account.
|
||||
document.getElementById('loginUsername').value = selectedAcc.username
|
||||
validateEmail(selectedAcc.username)
|
||||
}
|
||||
|
||||
loginOptionsViewOnLoginSuccess = getCurrentView()
|
||||
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
|
||||
|
||||
if(accLen > 0) {
|
||||
loginOptionsViewOnCancel = getCurrentView()
|
||||
loginOptionsViewCancelHandler = () => {
|
||||
if(isMicrosoft) {
|
||||
ConfigManager.addMicrosoftAuthAccount(
|
||||
selectedAcc.uuid,
|
||||
selectedAcc.accessToken,
|
||||
selectedAcc.username,
|
||||
selectedAcc.expiresAt,
|
||||
selectedAcc.microsoft.access_token,
|
||||
selectedAcc.microsoft.refresh_token,
|
||||
selectedAcc.microsoft.expires_at
|
||||
)
|
||||
} else {
|
||||
ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
|
||||
}
|
||||
ConfigManager.save()
|
||||
validateSelectedAccount()
|
||||
}
|
||||
loginOptionsCancelEnabled(true)
|
||||
} else {
|
||||
loginOptionsCancelEnabled(false)
|
||||
}
|
||||
toggleOverlay(false)
|
||||
switchView(getCurrentView(), VIEWS.loginOptions)
|
||||
})
|
||||
setDismissHandler(() => {
|
||||
if(accLen > 1){
|
||||
prepareAccountSelectionList()
|
||||
$('#overlayContent').fadeOut(250, () => {
|
||||
bindOverlayKeys(true, 'accountSelectContent', true)
|
||||
$('#accountSelectContent').fadeIn(250)
|
||||
})
|
||||
} else {
|
||||
const accountsObj = ConfigManager.getAuthAccounts()
|
||||
const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v])
|
||||
// This function validates the account switch.
|
||||
setSelectedAccount(accounts[0].uuid)
|
||||
toggleOverlay(false)
|
||||
}
|
||||
})
|
||||
toggleOverlay(true, accLen > 0)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary function to update the selected account along
|
||||
* with the relevent UI elements.
|
||||
*
|
||||
* @param {string} uuid The UUID of the account.
|
||||
*/
|
||||
function setSelectedAccount(uuid){
|
||||
const authAcc = ConfigManager.setSelectedAccount(uuid)
|
||||
ConfigManager.save()
|
||||
updateSelectedAccount(authAcc)
|
||||
validateSelectedAccount()
|
||||
}
|
||||
|
||||
// Synchronous Listener
|
||||
document.addEventListener('readystatechange', async () => {
|
||||
|
||||
if (document.readyState === 'interactive' || document.readyState === 'complete'){
|
||||
if(rscShouldLoad){
|
||||
rscShouldLoad = false
|
||||
if(!fatalStartupError){
|
||||
const data = await DistroAPI.getDistribution()
|
||||
await showMainUI(data)
|
||||
} else {
|
||||
showFatalStartupError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, false)
|
||||
|
||||
// Actions that must be performed after the distribution index is downloaded.
|
||||
ipcRenderer.on('distributionIndexDone', async (event, res) => {
|
||||
if(res) {
|
||||
const data = await DistroAPI.getDistribution()
|
||||
syncModConfigurations(data)
|
||||
ensureJavaSettings(data)
|
||||
if(document.readyState === 'interactive' || document.readyState === 'complete'){
|
||||
await showMainUI(data)
|
||||
} else {
|
||||
rscShouldLoad = true
|
||||
}
|
||||
} else {
|
||||
fatalStartupError = true
|
||||
if(document.readyState === 'interactive' || document.readyState === 'complete'){
|
||||
showFatalStartupError()
|
||||
} else {
|
||||
rscShouldLoad = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Util for development
|
||||
async function devModeToggle() {
|
||||
DistroAPI.toggleDevMode(true)
|
||||
const data = await DistroAPI.refreshDistributionOrFallback()
|
||||
ensureJavaSettings(data)
|
||||
updateSelectedServer(data.servers[0])
|
||||
syncModConfigurations(data)
|
||||
}
|
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://git.onimai.ru/ONIMAI-SMP/Launcher/releases/download/v${info.version}/ONIMAI.RU-MC-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg`
|
||||
showUpdateUI(info)
|
||||
}
|
||||
|
||||
populateSettingsUpdateInformation(info)
|
||||
break
|
||||
case 'update-downloaded':
|
||||
loggerAutoUpdater.info('Update ' + info.version + ' ready to be installed.')
|
||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.installNowButton'), false, () => {
|
||||
if(!isDev){
|
||||
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
|
||||
}
|
||||
})
|
||||
showUpdateUI(info)
|
||||
break
|
||||
case 'update-not-available':
|
||||
loggerAutoUpdater.info('No new update found.')
|
||||
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkForUpdatesButton'))
|
||||
break
|
||||
case 'ready':
|
||||
updateCheckListener = setInterval(() => {
|
||||
ipcRenderer.send('autoUpdateAction', 'checkForUpdate')
|
||||
}, 1800000)
|
||||
ipcRenderer.send('autoUpdateAction', 'checkForUpdate')
|
||||
break
|
||||
case 'realerror':
|
||||
if(info != null && info.code != null){
|
||||
if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){
|
||||
loggerAutoUpdater.info('No suitable releases found.')
|
||||
} else if(info.code === 'ERR_XML_MISSED_ELEMENT'){
|
||||
loggerAutoUpdater.info('No releases found.')
|
||||
} else {
|
||||
loggerAutoUpdater.error('Error during update check..', info)
|
||||
loggerAutoUpdater.debug('Error Code:', info.code)
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
loggerAutoUpdater.info('Unknown argument', arg)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification to the main process changing the value of
|
||||
* allowPrerelease. If we are running a prerelease version, then
|
||||
* this will always be set to true, regardless of the current value
|
||||
* of val.
|
||||
*
|
||||
* @param {boolean} val The new allow prerelease value.
|
||||
*/
|
||||
function changeAllowPrerelease(val){
|
||||
ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val)
|
||||
}
|
||||
|
||||
function showUpdateUI(info){
|
||||
//TODO Make this message a bit more informative `${info.version}`
|
||||
document.getElementById('image_seal_container').setAttribute('update', true)
|
||||
document.getElementById('image_seal_container').onclick = () => {
|
||||
/*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later')
|
||||
setOverlayHandler(() => {
|
||||
if(!isDev){
|
||||
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
|
||||
} else {
|
||||
console.error('Cannot install updates in development environment.')
|
||||
toggleOverlay(false)
|
||||
}
|
||||
})
|
||||
setDismissHandler(() => {
|
||||
toggleOverlay(false)
|
||||
})
|
||||
toggleOverlay(true, true)*/
|
||||
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
|
||||
settingsNavItemListener(document.getElementById('settingsNavUpdate'), false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* jQuery Example
|
||||
$(function(){
|
||||
loggerUICore.info('UICore Initialized');
|
||||
})*/
|
||||
|
||||
document.addEventListener('readystatechange', function () {
|
||||
if (document.readyState === 'interactive'){
|
||||
loggerUICore.info('UICore Initializing..')
|
||||
|
||||
// Bind close button.
|
||||
Array.from(document.getElementsByClassName('fCb')).map((val) => {
|
||||
val.addEventListener('click', e => {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.close()
|
||||
})
|
||||
})
|
||||
|
||||
// Bind restore down button.
|
||||
Array.from(document.getElementsByClassName('fRb')).map((val) => {
|
||||
val.addEventListener('click', e => {
|
||||
const window = remote.getCurrentWindow()
|
||||
if(window.isMaximized()){
|
||||
window.unmaximize()
|
||||
} else {
|
||||
window.maximize()
|
||||
}
|
||||
document.activeElement.blur()
|
||||
})
|
||||
})
|
||||
|
||||
// Bind minimize button.
|
||||
Array.from(document.getElementsByClassName('fMb')).map((val) => {
|
||||
val.addEventListener('click', e => {
|
||||
const window = remote.getCurrentWindow()
|
||||
window.minimize()
|
||||
document.activeElement.blur()
|
||||
})
|
||||
})
|
||||
|
||||
// Remove focus from social media buttons once they're clicked.
|
||||
Array.from(document.getElementsByClassName('mediaURL')).map(val => {
|
||||
val.addEventListener('click', e => {
|
||||
document.activeElement.blur()
|
||||
})
|
||||
})
|
||||
|
||||
} else if(document.readyState === 'complete'){
|
||||
|
||||
//266.01
|
||||
//170.8
|
||||
//53.21
|
||||
// Bind progress bar length to length of bot wrapper
|
||||
//const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width
|
||||
//const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width
|
||||
//const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width
|
||||
|
||||
document.getElementById('launch_details').style.maxWidth = 266.01
|
||||
document.getElementById('launch_progress').style.width = 170.8
|
||||
document.getElementById('launch_details_right').style.maxWidth = 170.8
|
||||
document.getElementById('launch_progress_label').style.width = 53.21
|
||||
|
||||
}
|
||||
|
||||
}, false)
|
||||
|
||||
/**
|
||||
* Open web links in the user's default browser.
|
||||
*/
|
||||
$(document).on('click', 'a[href^="http"]', function(event) {
|
||||
event.preventDefault()
|
||||
shell.openExternal(this.href)
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens DevTools window if you hold (ctrl + shift + i).
|
||||
* This will crash the program if you are using multiple
|
||||
* DevTools, for example the chrome debugger in VS Code.
|
||||
*/
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){
|
||||
let window = remote.getCurrentWindow()
|
||||
window.toggleDevTools()
|
||||
}
|
||||
})
|
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.
|
||||
})
|
||||
})
|
||||
|
||||
}
|