Compare commits

...

397 Commits

Author SHA1 Message Date
Daniel Scalzi
258cd0d421
2.1.0 2023-12-03 18:06:25 -05:00
Daniel Scalzi
f65eb2f2d6
Dependency upgrade. 2023-12-03 18:05:47 -05:00
jebibot
fb1cb7b415
feat: support Fabric (#313)
* feat: support Fabric

* fix: GAME_LAUNCH_REGEX for Fabric

* Small refactor.

* Update documentation.

* Upgrade helios-distribution-types, helios-core.

---------

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
2023-12-03 18:02:57 -05:00
Daniel Scalzi
9b898cc033
2.0.6 2023-11-25 19:09:40 -05:00
Daniel Scalzi
5a6217467a
Electron 27, dependency upgrade. 2023-11-25 18:58:34 -05:00
jebibot
3d470d9a32
feat: localize discord RPC, window title, button (#314)
* feat: localize discord RPC, window title, button

* fix: settings.dropinMods.okButton key
2023-11-25 18:31:41 -05:00
jebibot
cf7fd2f411
fix: auto connect for 1.20+ (#316) 2023-11-25 18:29:09 -05:00
jebibot
16790ca416
fix: Discord RPC check (#315) 2023-11-25 18:28:21 -05:00
Daniel Scalzi
7e95771957
Remove dependence on node crypto module in landing.js 2023-11-11 23:37:59 -05:00
Kamesuta
ab7e3c301c
Fix js.uicore.autoUpdate name in en_US.toml (#307) 2023-10-14 16:37:03 -04:00
Daniel Scalzi
b019f40802
Dependency upgrade. 2023-10-05 15:31:06 -04:00
Kamesuta
9d80d3b1d5
Localize HeliosLauncher UI using lang files (#301)
* First step to use Language .json file in ejs

* i18n for landing.ejs

* i18n for login.ejs

* i18n for loginOptions.ejs

* i18n for overlay.ejs

* i18n for settings.ejs

* i18n for waiting.ejs

* i18n for welcome.ejs

* langloader.js placeholder support

* i18n for landing.js

* i18n for login.js

* i18n for overlay.js

* i18n for settings.js

* i18n for uibinder.js

* i18n for uicore.js

* remove html language replacement

* use toml for i18n

* Fix mojang/microsoft status icon is undefined

* cascadable langloader

* separate lang file for customization

* move some placeholder text to _placeholder.toml

* Update

* Reduce package lock diff.

* Remove another placeholder.

* Checkbox does not require translation.

* Icons don't need translation.

* Leave placeholders inline.

* Fix translation for news pages.

* Remove more unneeded translations.

---------

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
2023-10-05 15:26:32 -04:00
Daniel Scalzi
92f2aab2a1
2.0.5 2023-08-21 11:49:40 -04:00
Daniel Scalzi
06ba2ebe69
Electron 26, helios-core 2.0.5, dependency upgrade. 2023-08-21 11:46:47 -04:00
GeekCorner
e6cf76b436
docs (Microsoft Auth): Add docs regarding whitelist system (#300)
* docs (Microsoft Auth): Add docs regarding whitelist system

* docs: Commit Dan's suggestion

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>

* docs: Commit Dan's second suggestion

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>

* docs: Commit Dan's third suggestion

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>

* docs: Commit Dan's fourth suggestion

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>

* docs: Commit Dan's fifth suggestion

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>

---------

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
2023-07-26 13:18:57 -04:00
Daniel Scalzi
5de3acb96a
Electron 25. 2023-07-23 23:58:13 -04:00
Daniel Scalzi
d89b270c20
2.0.4 - Harden Java discovery logic, Electron 24. 2023-04-12 21:41:34 -04:00
Daniel Scalzi
9e26b288fc
Fix RAM slider megabyte calculation. 2023-04-05 19:18:29 -04:00
Daniel Scalzi
22d2e064f3
2.0.3 - Improve error message for users without a mc profile. 2023-03-30 18:25:09 -04:00
Daniel Scalzi
ab3d4e9499
2.0.2 - Make version parse regex more lenient, grant explicit permissions to github actions. 2023-03-28 21:40:33 -04:00
Daniel Scalzi
3295430c54
2.0.1 - Build is optional in Java versions (esp. Oracle). 2023-03-25 18:57:23 -04:00
Daniel Scalzi
e4ddf898f9
v2.0.0 2023-03-24 17:46:52 -04:00
Daniel Scalzi
22233831dc
Merge pull request #270 from dscalzi/assetguard-2
assetguard2
2023-03-24 17:43:29 -04:00
Daniel Scalzi
9c9f70a7ca
Merge branch 'master' of https://github.com/dscalzi/HeliosLauncher into assetguard-2 2023-03-24 17:24:06 -04:00
Daniel Scalzi
11f5501a25
Update actions. 2023-03-24 16:57:34 -04:00
Daniel Scalzi
e96231579e
Reference helios-core v2.0.0 release 2023-03-24 16:40:08 -04:00
Daniel Scalzi
776c46d7e1
Set download progress to zero immediately, don't wait for progress event. 2023-03-24 15:46:37 -04:00
Daniel Scalzi
47378d63d2
Async is no longer needed. 2023-03-23 21:59:06 -04:00
Daniel Scalzi
ac8724c868
2.0.0-rc.3 2023-03-23 21:19:55 -04:00
Daniel Scalzi
ba265af4e9
2.0.0-rc.2 2023-03-22 22:51:22 -04:00
Daniel Scalzi
aa9a03c7b1
css fix. 2023-03-20 21:27:23 -04:00
Daniel Scalzi
ee96980dee
New distribution url. 2023-03-20 21:24:56 -04:00
Daniel Scalzi
2f27cdbaf4
2.0.0-rc.1 2023-03-19 22:46:19 -04:00
Daniel Scalzi
c9aeb0e0aa
Update distro.md. 2023-03-19 22:31:42 -04:00
Daniel Scalzi
2abe214005
dependency upgrade. 2023-03-19 22:13:44 -04:00
Daniel Scalzi
f3c1e42a03
v1.10.0 2023-03-19 20:12:22 -04:00
Daniel Scalzi
e639061fa8
Hopefully catch all errors during launch process. 2023-03-19 19:34:35 -04:00
Daniel Scalzi
e3af7669d8
cleanup. 2023-03-18 22:07:22 -04:00
Daniel Scalzi
a731fa90ea
Server specific ram. 2023-03-18 21:22:18 -04:00
Daniel Scalzi
3ef5fabb35
bugfixes. 2023-03-18 19:01:22 -04:00
Daniel Scalzi
12a84c1cce
Reference prerelease lib versions. 2023-03-18 03:19:43 -04:00
Daniel Scalzi
a1837aa20e
bugfix. 2023-03-18 03:05:37 -04:00
Daniel Scalzi
16ad59685e
Integrate java download with AG2, remove AG1. 2023-03-18 02:49:10 -04:00
Daniel Scalzi
15f7560916
Replace all javaguard logic, logic in landing.js needs to be rewritten to integrate. 2023-03-13 02:06:58 -04:00
Daniel Scalzi
e314599d99
fix rebase. 2023-03-07 21:30:15 -05:00
Daniel Scalzi
28c9c65220
remove more replaced code. 2023-03-07 21:10:48 -05:00
Daniel Scalzi
43b26ef1b9
delete more stuff 2023-03-07 21:10:48 -05:00
Daniel Scalzi
e9a5f80a36
Start to prune original asset guard to see what still needs to be replaced. 2023-03-07 21:10:48 -05:00
Daniel Scalzi
9a4129c11a
Inject common/instance dir. 2023-03-07 21:10:47 -05:00
Daniel Scalzi
b32857e7de
Progress checkin, mostly works. 2023-03-07 21:10:47 -05:00
Daniel Scalzi
a22bd32cb1
Replace distromanager, assetguard is probably broken. 2023-03-07 21:10:47 -05:00
Daniel Scalzi
45630c068c
Fix style violation. 2023-03-07 21:10:30 -05:00
Ritsu
fb586cca69
(Apple Silicon) aarch64 OpenJDK detect and install (#273)
* (Apple Silicon) aarch64 OpenJDK detect and install

* Fix undefined reference on amd64 macOS

* Revise logic a bit.

* variable isARM64, remove includes

* const isARM64

* forgot replace in _latestCorretto

* Update assetguard.js

* Update assetguard.js

---------

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
2023-03-07 09:11:44 -05:00
Daniel Scalzi
ecfe0de0fb
Electron 23, Node 18. 2023-02-12 18:58:06 -05:00
Daniel Scalzi
1c598e7980
Electron 22. 2023-01-08 14:20:58 -05:00
Daniel Scalzi
248937c22d
Improve logging. 2022-12-24 22:17:54 -05:00
GeekCorner
5d44cc3408
fix: Readme badge (#264) 2022-12-16 15:22:45 -05:00
GeekCorner
9224531b62
fix: variable references (#227)
* fix: Launcher freeze

* Fix syntax.

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
2022-12-09 23:49:47 -05:00
Daniel Scalzi
e3ee03ef73
1.17+ Support / Java Settings by Instance (#261)
* Patches to get 1.17 working, need to revise into real solutions.

* Add version.jar to cp until 1.17.

* Add server selection button to Java settings tab in preparation for by-instance java settings.

* Java settings by instance

* Use classpath flag instead of hardcode.

* Support 1.19.

* Fix refresh of java exec details.

* Add doc.

* Fix auto download of jdk 17.

* Dependency upgrade.
2022-11-27 18:13:50 -05:00
Daniel Scalzi
190bb4cf85
Electron 21, replace status api, dependency upgrade. 2022-11-06 22:39:07 -05:00
Daniel Scalzi
fa47c9c42c
Electron 20, dependency upgrade. 2022-09-04 13:09:59 -04:00
Daniel Scalzi
e7dd171cea
Dependency upgrade. 2022-07-29 11:49:59 -04:00
DamsDev1
2e1ab3c266
Fix expiration token date which return always NaN (#226) 2022-05-21 16:08:29 -04:00
Daniel Scalzi
0a80a3b073
Dependency upgrade. 2022-05-21 14:41:52 -04:00
Daniel Scalzi
ee61ea4979
Improve handling of JVM arguments on the settings view. 2022-04-03 17:32:59 -04:00
Daniel Scalzi
4e2c9ce3ec
Set user type to msa for msft accounts. 2022-04-03 17:08:55 -04:00
Daniel Scalzi
0bc74d9c66
Electron 18. 2022-04-03 16:00:09 -04:00
Daniel Scalzi
d7d0803661
Update dependencies. 2022-03-25 15:08:38 -04:00
Daniel Scalzi
15fc12b625
Update to Electron 17, fix deleting drop-in mods from the settings view. 2022-03-04 00:02:52 -05:00
Daniel Scalzi
ccf099a5cf
Fix macOS executable name. 2022-02-15 17:45:47 -05:00
Daniel Scalzi
b092722488
Calculate expiry date should not be async. 2022-02-15 09:09:26 -05:00
Daniel Scalzi
fc289262a8
Show GitHub actions build status in the README.
Rename workflow since slash character is not supported by shields.io.
2022-02-14 02:28:16 -05:00
Daniel Scalzi
a8c92ab272
Fix branch name in comment. 2022-02-12 11:47:21 -05:00
Daniel Scalzi
f6b787c526
Update README.md 2022-02-12 10:58:19 -05:00
Daniel Scalzi
78a4f7bbf3
v1.9.0 2022-02-11 20:24:25 -05:00
Daniel Scalzi
58e68c116c
Microsoft Authentication (#216) 2022-02-11 19:51:28 -05:00
Daniel Scalzi
ad47617cd0
Replace mojang.js with helios-core implementation.
Updated: Mojang Auth, status check (pending rework).
2022-02-06 18:23:44 -05:00
Daniel Scalzi
2fdb217e64
Electron 16, dependency upgrade. 2022-01-23 18:37:21 -05:00
Daniel Scalzi
c1d36d2b03
Fix build with native dependencies. (#213)
Build still fails on macOS, likely because the native dependency does not support arm64. Republished the dependency with the optional native components removed to solve the problem.
2022-01-23 18:26:16 -05:00
Daniel Scalzi
9c6d75f812
Implement helios-core and use Server List Ping protocol. 2021-10-31 02:20:03 -04:00
Daniel Scalzi
a2168da999
Update eletron to v15, target node 16 to match. 2021-10-14 23:55:31 -04:00
Daniel Scalzi
430e840514
Update JDK handling to account for AdoptOpenJDK migration to Adoptium.
Fix bug with AdmZip by replacing it for the jdk extraction task. It will have to eventually be replaced for everything else.
Remove disclaimer about Oracle JDK, as it is no longer used.
2021-10-14 23:17:40 -04:00
Daniel Scalzi
f9e4fd8561
Fix Let's Encrypt DST Root CA X3 certificate expiration. 2021-10-01 21:57:22 -04:00
Peter
54e6572754
Use MCHeads instead of Crafatar (#181)
* Update landing.js

This changes the Skin/Avatar server displayed on the landing page to one that displays the hat layer which then features the skin correctly.

* Create landing.js

This changes the Skin/Avatar server displayed on the landing page to one that displays the hat layer which then features the skin correctly.

* Updated remaining Crafatar URLS

Changed Crafatar URLS to matching mc-heads url.
2021-07-19 11:08:55 -04:00
ZaptoInc
79135f310d
Fixed broken links on the login page (#159) 2021-07-19 01:40:28 -04:00
Daniel Scalzi
31a51b8e7f
MIT license. 2021-07-19 00:52:57 -04:00
Daniel Scalzi
84c13e6972
Update PackXZExtract. 2021-06-24 18:22:29 -04:00
Daniel Scalzi
1b48ad58e6
Revert discord-rpc. 2021-06-23 20:46:00 -04:00
Daniel Scalzi
2c487f71ad
Electron 13.
Deleting drop-in mods seems to be broken. If you're having this issue,
please comment on this issue so electron fixes it.. https://github.com/electron/electron/issues/29598
2021-06-23 20:27:04 -04:00
Daniel Scalzi
61dbabdba3
Dependency upgrade. 2021-05-13 22:34:37 -04:00
Daniel Scalzi
cd1ca7edf5
Add support for building arm64 dmg (Apple Silicon processors) (#157). 2021-05-13 22:27:00 -04:00
Daniel Scalzi
be4a42b50a
Use GitHub Actions for building, refactor the builder configuration. 2021-05-13 22:26:51 -04:00
Daniel Scalzi
48d0c9e549
Dependency upgrade. 2021-03-23 18:57:16 -04:00
Daniel Scalzi
a9fa7c4ff9
Upgrade xcode, node version on travis. 2021-03-07 11:24:00 -05:00
Daniel Scalzi
cb8d1bb00f
Electron 12, Node 14, dependency upgrade. 2021-03-07 11:17:23 -05:00
Daniel Scalzi
2743585b12
Dependency upgrade, fix status (#138). 2021-02-01 18:05:45 -05:00
Daniel Scalzi
a6ca2e800c
Update executable names on README. 2021-01-20 11:03:20 -05:00
Daniel Scalzi
779a9a54ec
Dependency upgrade. 2020-12-11 16:00:55 -05:00
Daniel Scalzi
8723a192b4
Minecraft.net status works again, dependency upgrade. 2020-12-09 20:06:10 -05:00
Daniel Scalzi
c93d4922a6
Electron 11, workaround for main process debugging in VS Code. 2020-11-16 23:11:24 -05:00
Daniel Scalzi
d48abf444c
Dependency upgrade. 2020-11-09 19:25:37 -05:00
Daniel Scalzi
3ea41b42e5
Point to https mojang endpoint. 2020-09-16 13:49:10 -04:00
Daniel Scalzi
faa5f5267b
v1.8.0 2020-09-13 13:42:34 -04:00
Daniel Scalzi
c0776dcf61
Set broken mojang services to green until their API is fixed. 2020-09-13 13:35:49 -04:00
Daniel Scalzi
25e7e5aa55
Tweaks to Java discovery.
Add AdoptOpenJDK directory to the file scan.
Remove TLS reject unauthorized flag.
Use async/await for the fs scan function. Code was originally wrote
using fs and not fs-extra.
2020-09-13 03:05:08 -04:00
Daniel Scalzi
17e36fa5a2
Return after rejection. 2020-09-13 02:22:24 -04:00
Daniel Scalzi
2156099ea8
Electron 10. 2020-09-13 02:10:11 -04:00
Daniel Scalzi
9a2c1fd9b9
Use corretto on macOS since they use an older version of Xcode. (#70)
Vendor name is now displayed above the selected Java version on the settings page. This is to allow for easier differentiation between versions (ex. Amazon Corretto vs AdoptOpenJDK).
2020-09-08 23:19:07 -04:00
Daniel Scalzi
cc86f2a257
Use --fml.modLists instead of --fml.mods to avoid potential cli length limit issues. 2020-09-05 22:43:09 -04:00
Daniel Scalzi
e6897dad2c
Dependency upgrade. 2020-09-02 19:10:28 -04:00
Daniel Scalzi
bd19b16530
Include user's displayName in server joined regex. 2020-08-25 17:11:40 -04:00
Daniel Scalzi
46853157ec
Dependency upgrade. 2020-08-10 17:41:57 -04:00
Daniel Scalzi
0203295e7c
Dependency upgrade. 2020-07-15 20:15:28 -04:00
Daniel Scalzi
6b755fef15
MD5 comparisons should be case insensitive. 2020-07-07 22:47:22 -04:00
Daniel Scalzi
bace2e12e1
Dependency upgrade, tweak readme & funding.yml. 2020-07-03 18:29:47 -04:00
Daniel Scalzi
eeaa2e98d0
Update RPC game joined regex. (closes #80) 2020-06-29 11:28:55 -04:00
Daniel Scalzi
bec1385c55
Fix bug in autoconnect check. 2020-06-29 10:31:05 -04:00
Daniel Scalzi
f795b28d23
Fix launching on 1.8.9 and 1.9.4 (resolves #79). 2020-06-23 15:03:17 -04:00
Daniel Scalzi
ef8f36b519
Move sample distribution to docs folder to avoid confusion. 2020-06-17 16:23:35 -04:00
Daniel Scalzi
b09cd2ef28
Re-enable --server and --port on patched 1.15.2 builds. (#74) 2020-06-12 19:56:57 -04:00
Daniel Scalzi
1bdb413ab5
Fix autoconnect for 1.13, 1.14. Disabled on 1.15+.
Autoconnect is causing a OpenGL stack overflow exception on 1.15+ for some reason. Disabled it for now.
Finally changed .westeroscraft to .helioslauncher.
2020-06-09 13:10:06 -04:00
Daniel Scalzi
71b25d3e5c
Minor fixes and improvements.
Fixed issue with passing fullscreen argment to 1.13+, although it doesnt seem to be working well clientside.
Improved the forge version check logic.
Fixed launch area toggling and added a min linger time so that the transition doesnt look abrupt.
Upgraded dependencies.
2020-06-08 14:00:07 -04:00
Daniel Scalzi
7f821f36d7
Add support for 1.12.2 compiled with ForgeGradle 3. (#73)
The latest 1.12.2 builds have been upgraded to ForgeGradle 3. Handling is slighly different, as the version.json
format is different and no longer stored in the universal jar. You have to provide the version.json as a VersionManifest
module, the same as 1.13+. The rest of the launch handling hasn't changed.

Nebula supports the new 1.12.2 format.
2020-06-02 19:30:12 -04:00
Daniel Scalzi
1430d0faa2
icns icon no longer works as macOS platform icon (resolves #68). 2020-05-23 22:21:51 -04:00
Daniel Scalzi
8726638a23
Electron 9, Fixed breaking changes from 7 and 8.
Fixed file selectors not behaving properly due to breaking change in Electron 7 (#67).
Renamed shell.openItem to shell.openPath (Electron 9 breaking change).

Resolves #67.
2020-05-21 21:02:58 -04:00
Daniel Scalzi
b1abe07aeb
Update login url. 2020-05-21 14:38:22 -04:00
Daniel Scalzi
b3f8ff9595 Fix dmg executable name. 2020-05-19 20:31:35 -04:00
Daniel Scalzi
64dfc541dc
Added version to artifactName, dependency upgrade.
Closes #63.
2020-05-11 19:32:39 -04:00
dependabot[bot]
c151744ff9
Bump jquery from 3.4.1 to 3.5.0 (#60)
Bumps [jquery](https://github.com/jquery/jquery) from 3.4.1 to 3.5.0.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.4.1...3.5.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-04-29 19:47:48 -04:00
VolanDeVovan
9d1aa497a7
Send across ipc only unique value (#56)
* Send across ipc only unique value

* Fix equality check.

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
2020-04-11 20:44:20 -04:00
Daniel Scalzi
141a753893
Electron 8. 2020-03-18 19:54:46 -04:00
Daniel Scalzi
8b5a7bb02e
Create FUNDING.yml 2020-03-16 22:00:11 -04:00
Daniel Scalzi
1a2e9f3be4
Ensure libs are properly added to classpath as .jar 2020-03-08 21:10:43 -04:00
Daniel Scalzi
5fc6be444a
Updated dependencies. 2020-03-08 20:46:56 -04:00
Daniel Scalzi
96db607912
Fix mod loading for 1.7.10.
The launcher was never intended for use with 1.7.10, however since people
do use it for this version I generated a 1.7.10 distribution with Nebula
and tested it out. Several bugs were identified and fixed.

* Don't prefix modList paths with absolute on 1.7.10.
* Allow library overrides.

Forge 1.7.10 updates mojang's guava version. As such the library
resolver now supports overriding mojang libraries through the
distribution.json.

Fixes #45.
2020-01-18 12:02:00 -05:00
Daniel Scalzi
1110119df0
Dep upgrade, minor tweaks. 2020-01-17 04:42:16 -05:00
Daniel Scalzi
4f504cd470
Updated dependencies, deprecated methods. 2020-01-01 15:37:07 -05:00
Daniel Scalzi
19ee187f10
Electron v7, dependency upgrade, lint. 2019-11-16 17:17:55 -05:00
Oskar Stawiarski
9761c1d9d0 Older forge versions don't support lenient json. (#40) 2019-11-16 17:02:00 -05:00
Daniel Scalzi
8b04f476ee Update PackXZExtract to v1.1.0 2019-10-19 15:20:42 -04:00
Daniel Scalzi
90cd2c4614 v1.7.0 2019-10-17 19:14:15 -04:00
Daniel Scalzi
da706813ba Dependency Upgrade. 2019-09-23 14:13:36 -04:00
Daniel Scalzi
d087ca5a60 Add updated 1.13 configuration example. 2019-09-22 19:20:54 -04:00
Daniel Scalzi
28631610ab Update loading spinner images. 2019-09-06 02:18:16 -04:00
Daniel Scalzi
cbc8b07467 Rebrand to Helios Launcher. 2019-09-05 16:08:47 -04:00
Daniel Scalzi
590cefc5d4 Electron 6, dependency upgrade. 2019-08-26 17:57:27 -04:00
Daniel Scalzi
0194c2b6f1 Node v12, Dependency Upgrade. 2019-07-11 20:51:58 -04:00
Daniel Scalzi
d94365f535
Use OpenJDK downloads instead of Oracle (no longer possible).
Makes use of https://adoptopenjdk.net/, star them on GitHub, great product.
2019-06-02 18:11:39 -04:00
Daniel Scalzi
555cb0125c
Electron@5 2019-06-02 16:15:57 -04:00
Daniel Scalzi
68362021f5
Dependency upgrade. 2019-05-28 21:34:19 -04:00
Daniel Scalzi
b54b206b9a
Use programmatic API from Electron-Builder.
It seems that the package-json based configuration is deprecated.
See https://github.com/electron-userland/electron-builder/issues/3751
2019-04-23 17:35:07 -04:00
Daniel Scalzi
ef112cef3a
Updated dependencies. 2019-04-23 17:06:05 -04:00
Daniel Scalzi
887bb67ab0
Fix error with loading lang file from packaged app. 2019-04-23 16:53:32 -04:00
Daniel Scalzi
c141475404
Initial work on localization. 2019-04-07 23:33:40 -04:00
Daniel Scalzi
a0520a9458
Updated dependencies. 2019-03-17 23:11:31 -04:00
Daniel Scalzi
448440a604
VersionJar is not needed. (#26) 2019-03-06 22:47:45 -05:00
Daniel Scalzi
90818245f3
Some more cleanup. 2019-03-06 00:40:23 -05:00
Daniel Scalzi
644a32de37
Start removing specific branding. 2019-03-06 00:37:31 -05:00
Daniel Scalzi
5c0a293390
Break up assetguard. 2019-03-05 23:05:01 -05:00
Daniel Scalzi
1fc118ee8c
Updated forge 1.13 configuration, updated dependencies. 2019-02-28 19:40:38 -05:00
Daniel Scalzi
e08c3a903a
Add support for mod loading on Forge 1.13. 2019-02-18 18:17:31 -05:00
Daniel Scalzi
81367bc619
1.13 Update Phase 1
Mojang has changed its manifest format for 1.13.
Forge is no longer a universal jar, it requires more hosted files, all of which are generated by the installer.
We can no longer extract the version manifest from forge's jar and have to include it in the distribution.
This commit adds support for launching forge only, mods are currently not supported from the distribution.
Handling of 1.13 launches are subject to change as we move forward.
2019-02-18 06:31:01 -05:00
Daniel Scalzi
e8e7f85c64
Updated deps. 2019-02-17 19:21:23 -05:00
Daniel Scalzi
c834ca971a
Order the launch arguments better.
Classpath -> JVM -> Main Class -> Liteloader -> Minecraft ->
Client Options -> Mod list
2019-02-14 20:22:12 -05:00
Daniel Scalzi
6b96770c98
Update dependencies (clean install). 2019-02-12 19:20:54 -05:00
Daniel Scalzi
46f8b195d6
v1.5.2 2019-02-05 17:35:31 -05:00
Daniel Scalzi
6d1ecd73ee
Show a meaningful message if users try to login without a paid account. 2019-02-05 17:26:00 -05:00
Daniel Scalzi
c74e3f0676
Updated dependencies. 2019-02-05 17:06:45 -05:00
Daniel Scalzi
8bd040aece
v1.5.1 2019-01-20 22:32:23 -05:00
Daniel Scalzi
90651d4fbe
Reset clientToken if all accounts are removed. 2019-01-20 22:29:05 -05:00
Daniel Scalzi
3e20b1f63a
Updated dependencies. 2019-01-20 06:59:56 -05:00
Daniel Scalzi
8c0bf8faac
Improve error handling during launch (#21)
If anything happens to the forked process, the main should now pick up on it and alert the user.
Should no longer get 'stuck at 100%' issues when the forked process fails, for whatever reason.
2019-01-20 06:55:13 -05:00
Daniel Scalzi
cb12c0786e
Fix element class name for the data directory title. 2019-01-04 14:46:24 -05:00
Daniel Scalzi
de15440e6d
Added option to change data folder location. (#17)
Removed commonDirectory.
Removed instanceDirectory.
Added dataDirectory. The common and instance directories are now resolved from this.
The config.json and distribution.json are now stored in Electron's data folder (app.getPath('userData')).
Users can edit the dataDirectory under launcher settings.
2019-01-04 14:25:27 -05:00
Daniel Scalzi
e12197526d
Allow users to change install directory on windows. 2019-01-02 18:48:00 -05:00
Daniel Scalzi
d2982ca387
v1.4.1 - Forge 1.12.2 (4/10) + requires absolute: prefix. 2018-12-31 11:27:40 -05:00
Daniel Scalzi
d7fe519923
Preliminary Java 9+ Support (#20).
We will still not allow these versions to be used until they have been fully verified on our far-future 1.13 test server.
2018-12-31 10:39:27 -05:00
Daniel Scalzi
be533af38b
v1.4.0 - Added Shader Options, Electron v4. 2018-12-21 16:06:34 -05:00
Daniel Scalzi
b98a4ec21f
Shaderpack settings nearly finalized.
Added saving function, add pack button, drag and drop functionality to the button, ability to
refresh the list of packs.
Added height limit to the dropdown with scrolling. Cleaned up some styles.
2018-12-21 08:02:24 -05:00
Daniel Scalzi
a4d4b69791
Initial work on shader options, electron@4. 2018-12-21 06:51:08 -05:00
Daniel Scalzi
f5b1418d18
Reduced size of background 7. Removed some unused images.
7.jpg : 24MB -> 5MB
Updated electron, electron-builder.
2018-12-21 02:29:33 -05:00
Daniel Scalzi
f65793c0fb
Previous commit should have been v1.3.0.
A new feature is included in this version, requiring a minor bump.
2018-12-17 00:36:19 -05:00
Daniel Scalzi
001a2cbe81
v1.2.1 - Fixed issue w/ native lib parsing.
In 1.12.2, some natives do not provide a rules
object and instead just omit the classifier
from the natives object. We now check for that.

Updated electron to v3.0.12.
2018-12-17 00:11:23 -05:00
Daniel Scalzi
f613bdba4d
Updated dependencies. 2018-12-14 00:27:15 -05:00
Daniel Scalzi
f5ce7734b8
Ensure mods dir exists on file drop. 2018-12-01 10:06:23 -05:00
Daniel Scalzi
d779eacf61
Added drag/drop functionality to the add mods button.
You can now drag one or more files onto the add mods button in order to add them to the mods directory. Only jar, litemod, and zip files will be moved.
Changed eslint to use a single configuration file, with overrides for the UI scripts.
Now using fs-extra, replace usages of rimraf and mkdirp with fs-extra functions.
2018-12-01 08:20:42 -05:00
Daniel Scalzi
d9c9b32446
v1.2.0 2018-11-25 22:48:23 -05:00
Daniel Scalzi
d9858958c9
Oracle now uses https, update body parser accordingly. 2018-11-25 22:18:59 -05:00
Mars Geldard
9d5f708dfc Added Application Menu for macOS (#19) 2018-11-25 22:06:33 -05:00
Daniel Scalzi
684e884d9c
Mod config bug fixes, electron upgrade.
If the instance mods directory does not exist when the 'Add Drop-In Mod' button is clicked, it will be created.
The update selected server code has been modified. Previously, the server would be updated before the mod config was saved. This has been fixed so that the mod config is saved before the server is switched.
Updated electron to v3.0.10.
2018-11-20 05:19:59 -05:00
Daniel Scalzi
79d1d59451
v1.1.0 2018-11-18 22:17:18 -05:00
Daniel Scalzi
c6051f9942
v1.1.0-pre.2 2018-11-18 22:03:17 -05:00
Daniel Scalzi
acd6143d30
Modify AutoUpdater behavior on macOS.
The AutoUpdater requires that macOS builds be code signed. That is currently not possible.
As a workaround, the autoupdater on mac will now alert users that an update is available and give the option to download the file directly from GitHub.
Closes #16.
2018-11-18 21:51:48 -05:00
Daniel Scalzi
11cb9e6c28
v1.0.6-pre.2 2018-11-18 17:08:50 -05:00
Daniel Scalzi
bcd8082afe
Fix native lib extraction for old MC versions.
#18
2018-11-18 04:33:59 -05:00
Daniel Scalzi
03273232c6
Fix for native lib parsing in old MC versions.
Fixes #18.
2018-11-18 04:19:17 -05:00
Daniel Scalzi
37d35a6938
Updated electron-updater to v4 2018-11-17 23:30:44 -05:00
Daniel Scalzi
affacbf56e
Cleanup 2018-11-11 18:28:58 -05:00
Daniel Scalzi
e6b9728fe5
Updated electron-builder, electron-updater. 2018-11-09 01:03:28 -05:00
Daniel Scalzi
ee2a4a9319
v1.0.5 2018-11-06 02:46:36 -05:00
Daniel Scalzi
a9f12ee329
Fixed 'Failed to verify username.'
Send first request without clientToken, store the returned clientToken, use that for subsequent requests. Dont generate random UUID for clientToken, it can sometimes be 'bad'.
Removed dependency 'uuid'.
Updated 'electron-builder'.
2018-11-04 02:03:55 -05:00
Daniel Scalzi
eb946d6a2a
v1.0.5-pre.2 - Further work on investigating mac issue.
Upgraded xcode image to v10.
Added proper handler for auto-update errors.
Upgraded dependencies.
2018-11-01 17:37:05 -04:00
Daniel Scalzi
b7f03fa778
v1.0.5-pre.1 - Investigating dmg auto-update issue. 2018-10-31 20:44:35 -04:00
Daniel Scalzi
ce86840a87
v1.0.4 - Logic cleanup, minor bug fixes.
Removed the old workaround logic for queueing archives for extraction.
Removed the old forge callback (this was replaced a while ago).
Fixed a typo in _parseDistroModules (getType).
Use discordrpc straight from the repository. The author has refused to push the bugfix for nearly a month.
Fix timestamp issue on discord rich presence.
Dependency upgrades.
2018-10-31 01:25:38 -04:00
Daniel Scalzi
96d08955a4
v1.0.3 - Moved from deprecated mojang endpoint, dep upgrade. 2018-10-28 17:19:41 -04:00
Daniel Scalzi
2b10133f88
v1.0.3-pre.2 - Updated dependencies. 2018-10-28 16:46:53 -04:00
Daniel Scalzi
92a8b431d6
Electron-updater should be at ^3.1.3 2018-10-25 13:39:36 -04:00
Daniel Scalzi
1a3acec994
v1.0.3-pre.1 2018-10-25 12:53:52 -04:00
Daniel Scalzi
8a7b947500
Move back to electron-updater (fix included), dep upgrade. 2018-10-25 12:51:51 -04:00
Daniel Scalzi
f089993ea4
Use launchermeta.mojang.com instead of S3 (deprecated). (#12)
The deprecated endpoint will be removed by the end of 2018.
2018-10-25 12:45:32 -04:00
Daniel Scalzi
86c7245c41
Updated PackXZExtract to v1.0.2. 2018-10-18 01:39:27 -04:00
Daniel Scalzi
23cb2b8164
v1.0.2 2018-10-14 23:16:14 -04:00
Daniel Scalzi
d54a20a002
v1.0.2-pre.4 2018-10-14 19:44:40 -04:00
Daniel Scalzi
e6874b50bb
Rename electron-updater to electron-updater-bin for now. 2018-10-14 19:08:47 -04:00
Daniel Scalzi
b93ecf29e3
v1.0.2-pre.3 2018-10-14 18:42:13 -04:00
Daniel Scalzi
2da80c2047
Upgraded deps. 2018-10-05 22:24:55 -04:00
Daniel Scalzi
5c9b5d9398
v1.0.2 - Updated Electron to v3.0.0, more 2018-09-26 13:04:46 -04:00
Daniel Scalzi
2090021b30
v1.0.2-pre.1 2018-09-23 02:20:51 -04:00
Daniel Scalzi
3acc213544
electron-is-dev is now useless, use our own copy. 2018-09-23 02:19:16 -04:00
Daniel Scalzi
b2e9223b10
Updated to Electron v3.0.0, renderer to node v10.
Fixed deprecation of Buffer constructor in node v10.
Set flag to print the stack trace of node warnings in the renderer.
2018-09-22 05:07:18 -04:00
Daniel Scalzi
72f822d2b2
Fixed travis build script. 2018-09-22 04:01:59 -04:00
Daniel Scalzi
53fcf6773a
Upgraded dependencies. Unpinned Node.js minor version. 2018-09-18 00:06:20 -04:00
Daniel Scalzi
514b952cf4
v1.0.1
Fixes #6
2018-09-05 13:55:17 -04:00
Daniel Scalzi
8fc2ebf831
Upgraded Node.js to v10.9.0 2018-09-05 13:47:59 -04:00
Daniel Scalzi
2a9db6c646
Various fixes to New UI.
Add proper styling for spoiler blocks. (#6)
Fixed minor issue with relative url replacement regex.
Upgraded eslint@5.5.0
2018-09-05 02:30:26 -04:00
Daniel Scalzi
7851fdbefb
v1.0.0 2018-08-31 12:42:18 -04:00
Daniel Scalzi
e53b92c38b
Fix styling error when logging out of last account. 2018-08-29 13:44:28 -04:00
Daniel Scalzi
cf7e9adb89
v1.0.0-beta.6 - Revert UUID related change, updated deps. 2018-08-29 13:24:48 -04:00
Daniel Scalzi
db5653a7b7
Added additional fix for 'Failed to verify username'.
It seemed that login may also randomly fail if the user had a no-dash uuid stored. Corrected this.
2018-08-24 16:26:50 -04:00
Daniel Scalzi
70b83a6397
v1.0.0-beta.5 - Fixed Mojang issue resulting in "Failed to verify username" for specific users.
The Mojang API may randomly return extraneous spaces around the displayName and username fields. Launching the game with these values will result in the aforementioned error. The launcher now trims these values as a precautionary measure.
2018-08-24 15:17:32 -04:00
Daniel Scalzi
29a8f2a345
v1.0.0-beta.4 - Updated electron to v2.0.8 2018-08-22 17:59:38 -04:00
Daniel Scalzi
0346597afe
Wrap calls to console.log through LoggerUtil (prefixes). 2018-08-22 14:21:49 -04:00
Daniel Scalzi
37606dc8d2
Improvements to overlay keybinds.
Added dynamic keybinder for overlays. If dismissable, the Escape key will dismiss the overlay. If not dismissable, Enter & Escape will trigger the main button.
If the selected account is not valid and you have more than one other account, you may cancel logging in to return to the original error overlay.
Updated electron-builder@20.28.2.
Fixed minor bug where the background of the settings UI was set to transparent. This occurred if the validation error overlay was shown on the settings UI, and you chose the login option.
2018-08-22 10:54:09 -04:00
Daniel Scalzi
afe69a21c6
Small changes and cleanup to Java related UI.
The manual installation overlay now links to the Java Management wiki article.
The Available Options for Java 8 href now links to the os-specific page (/windows for windows, /unix for macOS and Linux).
The default config now uses the same value as max ram for min ram.
2018-08-20 04:02:27 -04:00
Daniel Scalzi
58a8215b05
v1.0.0-beta.3 - Added error handling for LaunchWrapper failure.
Fixes #5.
2018-08-19 03:16:08 -04:00
Daniel Scalzi
845721a830
Encode game stderr stream in utf-8. Updated deps.
semver@5.5.1
esline@5.4.0
2018-08-19 00:42:52 -04:00
Daniel Scalzi
50317c3fc2
v1.0.0-beta.2
Fixed bug which prevented mods with declared extensions in their identifiers from being loaded.
Synced distribution.json with remote.
Updated electron-builder.
Updated readme to include information on beta testing.
2018-08-15 09:17:59 -04:00
Daniel Scalzi
d581a2896a
v1.0.0-beta.1 2018-08-14 01:00:47 -04:00
Daniel Scalzi
e602848f9c
Upgraded Node.js to v10.8.x.
Switched npm to use UNLICENSED (All Rights Reserved).
Added option to build debian (.deb) binary. (#3)
2018-08-14 00:53:32 -04:00
Daniel Scalzi
4d2f50b136
v0.0.1-alpha.18 2018-08-13 08:28:51 -04:00
Daniel Scalzi
f1d89c0e6b
Fixed game status monitoring.
Switched to version-safe regex for monitoring game launch progress.
Game output is now encoded in utf-8 to avoid multiple conversions from Uint8Array to String.
Note that game output has line breaks. Trim any content before testing it against regular expressions.
2018-08-13 08:25:25 -04:00
Daniel Scalzi
4f416220c2
Added failsafe to configuration loading.
If file is malformed or corrupt, generate a fresh configuration file.
2018-08-13 06:31:46 -04:00
Daniel Scalzi
1566ff4e4c
Updated the auto-update UI.
Added a settings tab to manage updates.
On this tab you can:
* Init auto update check.
* View update information (version, prerelease vs release, etc).
* View update changelog.
* Install updates (when downloaded).
The green glow on the landing page now takes users to the settings tab. Updates can be installed from there.
This UI can be changed in the future if needed.
2018-08-13 06:13:13 -04:00
Daniel Scalzi
96d393696d Updated deps.
electron@2.0.7
electron-builder@20.27.1
electron-updater@3.1.1
2018-08-11 04:34:42 -04:00
Daniel Scalzi
9eef48b110
Added relevant links to readme. 2018-08-08 22:37:37 -04:00
Daniel Scalzi
2ec91a633b
v0.0.1-alpha.17 2018-08-07 04:55:25 -04:00
Daniel Scalzi
b46ac97493
General cleanup for mods tab code. 2018-08-07 04:41:26 -04:00
Daniel Scalzi
556199aa55
Added support for drop-in mods on the UI. 2018-08-07 04:16:15 -04:00
Daniel Scalzi
ff3f2bfb8d
Added server status bar to the top of the mods tab.
The user is also given the option to switch servers from this status bar.
2018-08-07 00:58:32 -04:00
Daniel Scalzi
7301dfc71a
Updated deps. 2018-08-07 00:01:26 -04:00
Daniel Scalzi
0c98cc2447
Added support for optional submods of required mods. 2018-07-29 16:32:41 +02:00
Daniel Scalzi
60ba7b4f8a
Add functionality to mods tab.
Mod configurations can now be changed and saved.
Still pending optimization to allow required mods to
properly declare optional mods.
Show main UI on interactive event. We shouldn't wait for
remote images to load first.
2018-07-29 14:42:11 +02:00
Daniel Scalzi
34b806c3a8 Updated mercurius updater to 1.12.2 on distro. 2018-07-26 04:44:36 +02:00
Daniel Scalzi
40506c4283 Cleaned up distribution.json for mod toggle UI.
Removed the version from the module's name. The version can be retrieved
from the identifier and placed wherever desired.
Reorganized the modules to have the more popular/important mods listed
first. The order in which modules are declared is the order they appear
on the UI.
Updated the docs to make it clear that maven identifiers are required for
all modules not of type File.
2018-07-26 04:44:36 +02:00
Daniel Scalzi
e583133c8b Initial work on mod toggle UI.
This still needs a lot of work.
2018-07-26 04:44:36 +02:00
Daniel Scalzi
6e71cd6b46
v0.0.1-alpha.16 - Fixed download errors.
Fixed error 'self signed certificate in certificate chain'.
Disabled reject unauthorized in assetexec process.
Fixed error messages from assetexec to renderer.
Updated elctron-builder to v20.25.0.
2018-07-26 04:30:47 +02:00
Daniel Scalzi
5bceaa92d0
Updated electron-builder to v20.24.5 2018-07-23 17:20:42 -04:00
Daniel Scalzi
372a76b0f2
v0.0.1-alpha.15 2018-07-22 23:40:45 -04:00
Daniel Scalzi
04cf2ee718
Fixed styling issue with a.commit-links.
Text outside the tt element did not behave properly on hover. This has been corrected.
2018-07-22 23:33:48 -04:00
Daniel Scalzi
cb28bce992
Rewrote readme file. 2018-07-22 17:54:35 -04:00
Daniel Scalzi
810e81521c
Added eslint. To lint, npm run lint.
Linted the entire project. Using different rules for the script files
as there are a lot of undefined variables just because of the way the
DOM's global scope works.

Mostly just code cleanup, however the linter did catch a minor bug with
a settings regex. That has been corrected.
2018-07-22 13:31:15 -04:00
Daniel Scalzi
ededf85892
Update deps: electron-builder@20.24.4 electron-updater@3.0.3 2018-07-22 12:02:43 -04:00
Daniel Scalzi
7dcce68455
Updated Distribution Index spec and impl.
Added distromanager.js to represent distro elements.
Moved all distro refresh code to distromanager.js.
Overhauled assetexec.js.
Overhauled handling of assetexec.js output in landing.js.
Overhauled events emitted by assetguard.js.
Improved doenload processing in assetguard.
Updated discord-rpc to v3.0.0.
Replaced westeroscraft.json with distribution.json.
Use npm in travis for windows + linux.
Remove file extension from imports.
Added liteloader + macromod + shaders to distribution.json.
2018-07-22 11:40:15 -04:00
Daniel Scalzi
5b0b1924cf
Updated dependencies. 2018-07-14 14:00:37 -04:00
Daniel Scalzi
97979df431
Upgraded dependencies, pinned Node.js version. 2018-07-05 21:06:06 +02:00
Daniel Scalzi
6759b143c4
Show installing progress bar during extractions. 2018-07-02 02:24:13 -04:00
Daniel Scalzi
41aae0c418
Changed email to uuid on the account settings view.
I'm not a fan of showing a user's email on the UI.
2018-07-02 02:08:48 -04:00
Daniel Scalzi
3131002d02
Use full body skin render for the avatar image.
There's room for improvement here, but it works for now.
2018-07-02 02:01:28 -04:00
Daniel Scalzi
651c2d6f35
Updated electron-builder to v20.19.1. 2018-06-30 15:32:37 -04:00
Daniel Scalzi
85331a7088
Added keyboard controls to the News UI.
On the landing view, the up arrow will open the News UI.
On the News UI, the left and right arrow keys will switch articles.

Thanks to aaronholtomcook/ElectronLauncher (26659f8c38)
2018-06-30 15:16:31 -04:00
Daniel Scalzi
52d90276bf
Added styling for github syntax highlighting. 2018-06-30 14:23:42 -04:00
Daniel Scalzi
13a6dcea63
v0.0.1-alpha.14 2018-06-30 04:00:04 -04:00
Daniel Scalzi
8f172a41e6
Documented new functions in processbuilder. Minor fixes.
Recursive call now uses the proper parameter (.mods).
Submodules are only parsed if the parent mod is enabled for launch.
2018-06-30 03:16:57 -04:00
Daniel Scalzi
dab195a996
Fixed travis build script.
Upgraded from xcode 9.2 to 9.4.
Use yarn instead of npm for windows and linux builds.
Unpin minor version of node in package.json.
2018-06-30 00:28:45 -04:00
Daniel Scalzi
aa48a756c5
Updated dependencies. 2018-06-29 18:44:36 -04:00
Daniel Scalzi
8ab5b2db1d
Updated distro index documentation to include liteloader. 2018-06-23 22:16:20 -04:00
Daniel Scalzi
932443fcb8
Removing deprecated files.
Launchprocess.js has been deprecated for over a year, pending removal once liteloader support was added.
The user modlist idea was scrapped a while ago. User mods can be stored inside of the mods folder for each server instance.
2018-06-23 22:02:42 -04:00
Daniel Scalzi
aa0e1a20ca
Added initial support of liteloader + optional submodules.
Liteloader is loaded as a library, with special launch conditions being executed when it is enabled. Litemods are constructed into a mod list and passed to liteloader via the --modRepo argument.

The launcher now supports optional submodules. These are parsed recursively, there is no depth limit. Typically the depth will be only 2 as litemods are optional submoduless of liteloader.
2018-06-23 21:03:49 -04:00
Daniel Scalzi
145a2fe77b
Added initial support for optional mods.
Optional mods are stored by ID in the configuration. Their enabled state is stored here. The mod configurations are updated each time the distro index is refreshed. Configurations are stored by server id. If the id no longer exists (changed/removed), the mod configuration is removed. If new optional mods are added, they are added to the configuration. If they are removed, they are removed from the configuration.

Currently only top level optional mods are supported.
2018-06-23 15:17:26 -04:00
Daniel Scalzi
4196856d31
Fixed range slider rendering when value is out of range.
If the value of a ranged slider is out of range, the slider will snap to either 0% or 100%.
Reduced memory label margin to preserve sufficient spacing.
2018-06-21 21:04:10 -04:00
Daniel Scalzi
9769458499
Fixes for the overlay on the settings UI.
The background of the settings UI is set to transparent when the overlay is toggled.
The color of the frame bar has been adjusted to what it should actually be.
2018-06-21 20:54:07 -04:00
Daniel Scalzi
c0714ecbc6
Added functionality to avatar overlay.
Overlay changed from div to button.
Clicking on the overlay will bring you directly to the settings account tab.
Modified overlay background color.
2018-06-21 12:38:21 -04:00
Daniel Scalzi
a68abaf89c
Added styling for changelog anchors. 2018-06-21 10:01:03 -04:00
Daniel Scalzi
f44d3b69d3
v0.0.1-alpha.13 - Updated tar-fs. 2018-06-20 09:18:09 -04:00
Daniel Scalzi
2d0c4c76eb
Enhanced News UI.
Added a drop shadow so that the news no longer scrolls into nothing.
Tweaked several styles to make the UI look a lot better.
2018-06-20 09:12:44 -04:00
Daniel Scalzi
e2e48f6444
Enhanced the settings UI structure.
The left side of the settings UI now has a more comprehensive layout.
Settings tabs now scroll all the way to the frame bar. When the content is scrolled out of view, a drop shadow is displayed. This removes the awkward feel of content scrolling into nothing.
2018-06-20 07:38:53 -04:00
Daniel Scalzi
5a16416db5
Added about settings tab.
This tab displays information about the current version of the application. Release notes are fetched from GitHub's atom feed and displayed here as well.
2018-06-20 06:15:10 -04:00
Daniel Scalzi
e7752b4374
v0.0.1-alpha.12 - Completed Java Settings Tab.
Added JVM Options to the Java tab.
Small clean up for the Java tab's styles and bindings.
2018-06-14 06:09:09 -04:00
Daniel Scalzi
109c24bc79
Added option to change the Java exec to the settings UI. 2018-06-14 03:49:55 -04:00
Daniel Scalzi
7cf0e1f049
Various CSS updates.
Most of the UI is now unselectable.
The account selecton styles now conform with the auth account styles.
2018-06-12 04:12:22 -04:00
Daniel Scalzi
b61a9a2c55
Tweaking memory range sliders.
Fixed minor issue where an extra tick was added.
The bar fill color now changes to yellow and red depending on how much memory is being allocated.
2018-06-12 03:48:36 -04:00
Daniel Scalzi
6ac48a63b5
Added memory management to Java tab. 2018-06-12 03:25:36 -04:00
Daniel Scalzi
34e8da2aa2
Minor modification to toggle switch styles. 2018-06-11 22:45:30 -04:00
Daniel Scalzi
1fd69207c0
Added .nvmrc file (#1) 2018-06-11 22:16:01 -04:00
Daniel Scalzi
db7ba0d450
Initial work on Java tab. Added custom range slider.
Also added two more settings tabs. These are experimental and subject to change.
2018-06-11 22:11:05 -04:00
Daniel Scalzi
08eb04775e
Added indication for when there is new news. 2018-06-04 23:08:03 -04:00
Daniel Scalzi
05fe516249
v0.0.1-alpha.10 - Hotfix for startup error. 2018-06-04 20:06:34 -04:00
Daniel Scalzi
50d85d30cc
v0.0.1-alpha.9 - Added option to enable/disable prereleases.
Added option to enable/disable prerelease updates.
Implemented launcher tab on the settings UI, as this is the only current value.
Added semver dependency.
2018-06-04 19:34:47 -04:00
Daniel Scalzi
790a3e0e8b
v0.0.1-alpha.8 - Implemented Minecraft Settings Tab.
Implemented minecraft settings tab.
Updated distro index documentation to reflext the file system overhaul.
2018-06-04 16:28:17 -04:00
Daniel Scalzi
0cc861f614
Overhauling file system structure.
Common files such as assets, libraries, and mods have been externalized into a 'common' folder. Each server now has its own instance folder to allow saving per version files. This resolves issues with resourcepacks and mod configurations being overriden, and still preserves our optimizations in storing libraries and mods maven style.
2018-06-04 00:17:20 -04:00
Daniel Scalzi
97e9c15baf
Updated discord-rpc, changed ad hoc fix to api fix. 2018-06-03 15:35:39 -04:00
Daniel Scalzi
74a60a61c2
v0.0.1-alpha.6 - Finalizing the settings account tab.
Added a done button which closes the settings UI.
Displays a warning before the user logs out of the last saved account. If they proceed with the logout, they will be redirected to the login UI.
Added startup handling for when the user has 0 saved accounts. They will be brought directly to the login UI.
Accounts are now validated each time they are switched.
2018-05-30 23:32:51 -04:00
Daniel Scalzi
91c842dd40
Added UI and implementation for the account settings tab.
Features:
* Add a new account.
* Switch accounts.
* Log out of an account.

Also added a cancel button to the login UI which is only shown when a user is adding an account. In that case, the operation should be and is cancellable.
2018-05-30 22:22:17 -04:00
Daniel Scalzi
2dcbb45bdb
Further progress on settings UI.
Added a tooltip to the settings button on the landing UI.
Settings button now opens to the (incomplete) settings UI.
Added navigation bar to the settings UI.
Implemented tabbing between settings tabs.
More to come.
2018-05-30 16:00:07 -04:00
Daniel Scalzi
30c258da2d
More work on settings, removing css redundancy. 2018-05-30 11:41:04 -04:00
Daniel Scalzi
ee55446cd6
Temporary fix to DiscordRPC not shutting down properly.
Something must have changed on discord's end, as our previous solution is no longer working. The latest changes to the module include a clearActivity() method, however it is not available on npm. Until it is, we are using the implementation directly.
2018-05-29 23:42:27 -04:00
Daniel Scalzi
daa6faac86
v0.0.1-alpha.6
Fixed AuthManager sending incorrect client token to the auth endpoint.
Fixed minor issues with the validate selected function.
Fixed minor issue related to UI transitions.
Added account validations on startup and when account is switched.
Updated dependencies.
2018-05-29 21:47:55 -04:00
Daniel Scalzi
5a692d9088
Fix settings container rendering. 2018-05-22 22:35:56 -04:00
Daniel Scalzi
e9e2ec162a
Added settings container, updated dependencies. 2018-05-22 22:34:35 -04:00
Daniel Scalzi
a67dac23cf
Fixes related to offline startup.
Fixed incorrect function name for local distro index loading.
Fixed mojang tooltip not showing statuses when offline.
Added timeout of 2500ms to news loading, remote distro index retrieval, and mojang status loading.
Updates async to fix lodash vulnerability.
2018-05-22 08:41:22 -04:00
Daniel Scalzi
ba916aa953
v0.0.1-alpha.5
Added tooltip UI which shows the status of each Mojang service.
Updated dependencies.
2018-05-17 03:11:44 -04:00
Daniel Scalzi
f5f5b72bed
Fixed nsis installer opening application twice.
Updated electronupdater, should fix the nsis issue.
2018-05-15 07:53:37 -04:00
Daniel Scalzi
f4abbef58c
Increasing version, various small fixes.
Fixed launcher.js not checking the correct return value when verifying the configured Java executable.
Removed debug logging from configmanager.js.
Updated dependencies.
2018-05-15 06:07:28 -04:00
Daniel Scalzi
71cbd109c4
Various fixes.
Fixed issue where news button could not be tabbed to when news UI is active.
Fixed incorrect java version sorting (now behaves as intended).
2018-05-15 02:30:26 -04:00
Daniel Scalzi
d2c435ce51
Added Java Validations for Linux.
Also abstracted and optimized the common functions for resolving Java between the three supported operating systems.
Changes made to win32 and darwin validations will require testing to ensure everything is functional.
2018-05-15 01:05:10 -04:00
Daniel Scalzi
54e3861ba8
Fixing linux data directory. 2018-05-14 22:33:38 -04:00
Daniel Scalzi
49bad485f6
Upgrading ejs-electron to fix startup error. 2018-05-10 21:06:59 -04:00
Daniel Scalzi
0d11749ad4
Updating dependencies to fix mime vulnerability. 2018-05-10 19:20:58 -04:00
Daniel Scalzi
e3890b2057
v0.0.1-alpha.3 - Updated dependencies. 2018-05-10 05:56:19 -04:00
Daniel Scalzi
f0a66e7a02
Various fixes for the news UI.
Made the layout for the status container more comprehensive.
Added a spacer to the bottom of news article content.
Disabled tabbing between the landing and news containers.
2018-05-10 05:48:55 -04:00
Daniel Scalzi
848440ed1c
Fixed an issue with registry scan, minor UI changes.
The registry scan function exited early if no Java Development Key was found. This has been corrected by redoing the exit checks.
Social media buttons now lose focus after they are clicked to prevent a sticky appearance.
Changed the load spinner to have the text rotate within the center image.
2018-05-10 04:36:52 -04:00
Daniel Scalzi
f0b21330a0
v0.0.1-alpha.2 Fixed console.debug error. 2018-05-10 00:22:00 -04:00
Daniel Scalzi
d33476bcf9
ConfigManager improvements. v0.0.1-alpha.1
Added new configuration option, launchDetached.
Added validation function to ConfigManager to add missing keys (due to updates).
Updated westeroscraft.json
Game process can now be detached from the launcher.
2018-05-10 00:01:38 -04:00
Daniel Scalzi
f1a98f2d45
Added mojang account validation UI.
Other minor fixes included. Bumped version from dev to alpha. Testing to begin soon.
2018-05-09 22:23:37 -04:00
Daniel Scalzi
15a83a7736
Attempting to make references to the distribution index more streamlined. WIP 2018-05-08 20:10:46 -04:00
Daniel Scalzi
f161e196be
Improving application startup flow.
Major improvements to distribution index loading.
Implemented new transitional UI for startup.
If **no** distribution index is loaded on startup, completion of startup will be prevented and the user will be shown a message displaying the issue.
Launch errors are now shown as overlays.
Many more minor fixes and enhancements.
2018-05-08 06:34:16 -04:00
Daniel Scalzi
cd4f7918c8
Pipe output from forked processes back to parent.
Also cleaned up the code a bit. Switched to use lambda declarations in promises and renamed 'fulfill' to 'resolve', as it should be,
2018-05-07 18:15:59 -04:00
Daniel Scalzi
0c1ebd0ce0
Distribution data is now pulled from our servers. 2018-05-07 01:34:57 -04:00
Daniel Scalzi
40de1e3cd3
Added News UI.
Added News UI and implemented functionality.
Removed westeroscraft.xml as we don't need to cache it.
Updated westeroscraft.json.
Updated Electron to v2.0.0
2018-05-06 21:45:20 -04:00
Daniel Scalzi
252b82a944
Enhancing social media button responsiveness (focus styles). 2018-04-29 22:36:28 -04:00
Daniel Scalzi
39fd7e19ef
Make menu button slide up (experimental). Disable tabindex on overlay. 2018-04-29 18:39:57 -04:00
Daniel Scalzi
4106b2b069
Many fixes and adjustments to the upper right landing UI.
The user_text span now displays the currently selected account.
2018-04-29 18:05:59 -04:00
Daniel Scalzi
ea758aee1f
Various optimizations related to library extraction.
Updated PackXZExtract to v1.0.1.
If no files are queued for extraction, the step is skipped.
The UI now shows some indication that the extraction is in progress.
2018-04-28 23:34:23 -04:00
Daniel Scalzi
d08cfbf248
Change library extraction to be queue based to ensure it completes. 2018-04-28 20:52:13 -04:00
Daniel Scalzi
0216582827
Application now checks for updates every 30 minutes. 2018-04-28 18:45:19 -04:00
Daniel Scalzi
f1cf433ca8
Fixed issue where extraction library could not be run from asar. 2018-04-28 18:07:39 -04:00
Daniel Scalzi
95afe5c63a
Added basic auto update implementation. 2018-04-28 16:26:38 -04:00
Daniel Scalzi
5b74ecef21
Experimental changes in preparation for auto-update support. 2018-04-28 04:16:09 -04:00
Daniel Scalzi
c6637d18e1
Syncing distro files (Added 1.11.2 + 1.12.2 test servers). 2018-04-27 16:22:46 -04:00
Daniel Scalzi
1a7c8fd70f
Fixing request dependency. 2018-04-27 00:33:30 -04:00
Daniel Scalzi
009a1b58af
Added basic functionality to server selection UI.
The server list is now dynamically generated based on the servers listed in the distribution index. Also, moved to event.key for key bindings as event.keyCode is deprecated.
2018-04-27 00:04:09 -04:00
Daniel Scalzi
ae3c8854f4
Upgrading Node.js from 8.11.x to 10.0.x. 2018-04-26 19:44:30 -04:00
Daniel Scalzi
5fe43ac8e9
Added basic functionality to server selection UI.
Basic selection and updating of the selected server has been added. There are a few subtle mechanics which need to be added still, such as keybind shortcuts (enter to submit, etc). In addition, functionality still needs to be added to generate the list of servers from the manifest file.

Fixed a minor issue with the login view.
Updated play button styles.
Updated dependencies.
2018-04-26 18:41:26 -04:00
Daniel Scalzi
4b708f59fe
Minor modifications to menu animation. 2018-04-26 04:00:51 -04:00
Daniel Scalzi
22f5eabe49
Experimenting with the menu slide animation. 2018-04-26 03:49:45 -04:00
Daniel Scalzi
6e55442b25
Server selection view can now be opened from the landing view.
The temporary span which displayed the selected server has been changed to a button. Clicking this button will open the server selection view. The server selection view is still pending full implementation.
2018-04-26 02:39:47 -04:00
Daniel Scalzi
4b8133474d
Various changes and improvements.
Added warning message when console is opened, this is in preparation for alpha.
Added a new background image. Restored random backgrounds on launch (for now).
Changed the overlay dismiss/cancel buttons from anchors to buttons.
2018-04-26 02:01:46 -04:00
Daniel Scalzi
2f66d44824
Further organization of the browser scripts.
Moved server selection styles to launcher.css, as they are nearly finished.
Moved overlay convenience functions to overlay.js.
Moved launch area (landing.ejs) convenience functions to landing.js.
Various cleanups and documentation also added.
2018-04-25 20:11:10 -04:00
Daniel Scalzi
92d8a5e254
Disable eval(), its use is not needed and improves security. 2018-04-25 17:51:10 -04:00
Daniel Scalzi
1b38629084
Organizing UI scripts.
Moved landing.ejs specific scripts to a dedicated file. General cleanup for other script files. Need to examine the remaining code in actionbinder.js to determine the most logical place for it.
2018-04-25 17:40:46 -04:00
Daniel Scalzi
4d26298b98
Added Content-Security-Policy to ensure that external scripts cannot be loaded.
Moved inline scripts to their own files. Moved all front-end scripts to /assets/js/scripts.
2018-04-25 17:06:10 -04:00
Daniel Scalzi
8d5cd2b00b
Changed server listing element from div to button.
This is necessary to implement proper functionality. Work on that will be done shortly.
2018-04-19 22:06:34 -04:00
Daniel Scalzi
f7e24fd092
Removed original server selection implementation. We will be using the overlay. 2018-04-19 01:48:18 -04:00
Daniel Scalzi
61538fdde5
Improving dynamic structuring within selection UI.
The server list is now centered. The minimum height was set to 40%, with a maximum of 65%. The UI will now scale to ideally fit the given number of servers. Four servers can be shown before scrolling enables.
2018-04-19 00:51:56 -04:00
Daniel Scalzi
9d04eb2227
Tweaking the server selection UI.
Removed divider. Moved star. Changed version tag back to green.
2018-04-18 23:50:26 -04:00
Daniel Scalzi
e994f4c474
Added new server selection UI.
The UI is still being tweaked and modified. Functionality will be added once it is finalized.
2018-04-15 23:06:16 -04:00
Daniel Scalzi
714daace18
Added server status retrieval and implemented it on UI.
The player count on the landing page is now functional. If the server cannot be reached, the label and value will change to SERVER and OFFLINE, respectively. This behavior can be modified.
2018-04-15 22:35:14 -04:00
Daniel Scalzi
8446af4669
Added seven pointed star svgs and test server icon (temp). 2018-04-15 22:31:51 -04:00
Daniel Scalzi
6e8d4fe9bc
Fixed old bug with argument splicing in processbuilder. 2018-04-15 01:26:40 -04:00
Daniel Scalzi
9448b9b5a3
Add additional arguments for mac. 2018-04-15 00:49:20 -04:00
Daniel Scalzi
2f899822b5
Fixing classpath argument for Unix-like systems (macOS, Linux).
Game should now launch on these two systems.
2018-04-15 00:21:26 -04:00
Daniel Scalzi
be39d60705
Native files are now stored in the OS temp directory.
Temp folder is deleted when minecraft is closed. If the Node.js process ends abruptly, the folder will not delete. As a dirty way to counter this, the directory we extract native files to is cleaned in the preloader. Maybe we'll come up with a more elegant solution in the future.
2018-04-15 00:00:08 -04:00
Daniel Scalzi
5475ac0c69
Added function to remove an authenticated account to authmanager.js. 2018-04-14 22:43:58 -04:00
Daniel Scalzi
631c3cd6d4
Added option to dismiss Java download prompt.
You will be given an option to install Java manually. Selecting this will bring you to a final prompt which gives some useful information about installing Java and where you can find help. You have two options on this prompt. The first is to simply aknowledge it, which will dismiss it. The second is to go back to the first prompt.
2018-04-14 22:20:59 -04:00
Daniel Scalzi
66a3854a24
Better error handling with Mojang REST api.
Added handling for when there is no internet connection or Mojang's auth server is unreachable to both the login and status modules.
2018-04-14 18:42:45 -04:00
Daniel Scalzi
b5386c0257
Added dynamic frames for darwin + win32.
The darwin frame is the same as the original, however the button behavior is corrected. The win32 frame uses the traditional buttons found on windows. Also added a small logo image to the windows frame. Each frame is dynamically loaded on startup via ejs.

Also disabled the server selection test UI.
2018-04-14 16:54:10 -04:00
Daniel Scalzi
28cd147ca0
Fixing some syntax issues.
It was late, I was tired, this was the result.
2018-04-12 22:40:48 -04:00
Daniel Scalzi
12aa9f9c5b
Removing '/Contents/Home' from Java root on darwin to have the code rely on the resolve executable method. 2018-04-12 22:13:26 -04:00
Daniel Scalzi
fc81016dc6
More updates for Java validations on darwin.
Also, now require version 8u52+, as pack200 algo changed after 8u45.
2018-04-12 21:38:27 -04:00
Daniel Scalzi
2a551f18ba
Fixing default zoom level (darwin). 2018-04-12 18:39:31 -04:00
Daniel Scalzi
f257208e2f
Disable zoom on UI (darwin). 2018-04-12 18:13:09 -04:00
Daniel Scalzi
05ebb80f19
Fixing variable rename typo. 2018-04-12 16:42:26 -04:00
Daniel Scalzi
4eb9d267eb
First attempt at Java validations for darwin (osx). 2018-04-12 16:12:45 -04:00
Daniel Scalzi
40a02726ad
Updated welcome text, testing a potential server selection UI. 2018-04-10 16:35:31 -04:00
Daniel Scalzi
8d682b15b3
Added remove account function to config manager. 2018-04-08 20:56:44 -04:00
Daniel Scalzi
9b4a2d4ef9
Added travis build script. 2018-04-08 13:32:27 -04:00
Daniel Scalzi
07946714cc
Adding form behavior to login.ejs.
Basic desired behavior which is now supported is submission when the enter key is pressed.
2018-04-07 20:41:09 -04:00
Daniel Scalzi
a16a22e2e1
Modifications to welcome view, smoothing welcome transitions.
Disabled clouds on welcome.ejs. Added some welcome text to replace the lorem ipsum. The alignment was also changed from center to justify, to make the UI look sharper. Transitions between each view was changed from 250ms to 500ms.

Removed some testing code from login.ejs.
2018-04-07 19:58:23 -04:00
Daniel Scalzi
5a8ae0485a
Minor fixes for the Java download error overlay. 2018-04-07 18:24:13 -04:00
Daniel Scalzi
0a79634b8a
Implemented Java validations within the UI.
When a user attemps to launch, the configured Java executable will be validated. If it is invalid, we will look for a valid installation. If no valid installation is found, the user will be prompted with an option to install Java. An option to decline needs to be added. If they choose to install, it will download, extract, and update the executable in the config. The game will then be launched.

Also added progress tracking for asset validations, as they can potentially take a bit longer. Showing progress assures the user that the program isn't stuck or broken.
2018-04-07 18:06:49 -04:00
Daniel Scalzi
9b63d9bb58
Integrating new overlay mechanics with login view.
Removed login.ejs-specific overlay. Removed blur transition and reduced overlay fade from 500ms to 250ms. Minor modification to the overlay css.
2018-04-07 13:29:40 -04:00
Daniel Scalzi
5335e0124b
Further work on overlay mechanism. 2018-04-06 12:33:20 -04:00
Daniel Scalzi
92cb88a23a
Removing deprecated files (javaguard.js, index.ejs). 2018-04-04 15:46:19 -04:00
Daniel Scalzi
c6f9121806
Making error overlay more generic, WIP 2018-04-02 22:43:11 -04:00
Daniel Scalzi
5c7e0c3c8a
Various additions and fixes to the launch controller.
Fixed an issue with concurrency when there are no downloads queued on launch. Added support for static function execution in AssetExec. Fixed a binding issue in uicore.js caused by delayed div loading. For now the solution is just hard coding in the value, will probably switch these two a css file later on. Included the launcher's x64 runtime directory in Java scans.
2018-04-02 18:40:32 -04:00
Daniel Scalzi
ec9e95c130
Working on binding each view together.
Separate views are stored in an ejs file. When the app starts, each file will be loaded, with the DOM elements hidden. Based on the state of the application, a specific view will be fadded in. Switching between views will use this principle.

Moved contents of index.ejs to landing.ejs to make it compatible with the new format. As a result, index.ejs is deprecated and will be removed once it is no longer needed for reference.
2018-04-02 16:05:48 -04:00
Daniel Scalzi
7fb33c6813
Rough completion of JRE downloads from oracle.
Mojang fallback cannot be implemented due to limitations of decompressing large lzma files in node.js. This can probably be worked around by packaging with some command line binary, but it honestly it not worth it.
2018-03-31 20:45:24 -04:00
Daniel Scalzi
ae387757bc
Discord-rpc has resolved the issue with the timeout function, updating to beta 10. Updating to node.js 8.11.x. 2018-03-31 13:46:52 -04:00
Daniel Scalzi
13cc555afd
Game now saves to OS-specific data directory. Fixed issue where logs folder did not save to correct location. Fixed issue with authentication code. Continuing work on Java validation code. 2018-03-31 13:05:05 -04:00
Daniel Scalzi
f8131d9322
First phase of moving Java validation code to assetguard.js
Static utility functions have been moved. Functions which handle Java downloads will be moved next. AssetGuard is becoming a lengthy file, to cope with this I've added region comments so that specific sections can be collapsed and expanded when needed.
2018-03-30 08:31:54 -04:00
Daniel Scalzi
aacf15efc5
Using object for JavaGuard. May move java downloading processes to AssetGuard as a non-default category. Very much a WIP. 2018-03-29 19:45:05 -04:00
Daniel Scalzi
2062865e7f
Got Java download working, just need to integrate it into the program and add some safeguards to the code. 2018-03-28 21:19:56 -04:00
Daniel Scalzi
4b2cac1eff
Completed Java validation for windows.
Possible paths are pulled from the registry and JAVA_HOME. These are sorted by version and whether they point to a JRE or JDK. Each path will be validated using an experimental option which yields the arch of the binary. The first one to  be validated will be selected and returned.
2018-03-28 17:13:11 -04:00
Daniel Scalzi
4fd202d180
Cleaning up jsdocs to be more aligned with the standard. 2018-03-28 16:42:10 -04:00
Daniel Scalzi
d3c5997baa
Initial work on Java detection/validation system. 2018-03-28 16:13:57 -04:00
Daniel Scalzi
5e8342aec9
Updating dependencies. 2018-03-28 09:45:08 -04:00
Daniel Scalzi
454964ce0b Initial work on welcome view, upgraded dependencies. 2018-02-19 20:27:42 -05:00
92 changed files with 17036 additions and 10475 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
dist

66
.eslintrc.json Normal file
View 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
View File

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

38
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Build
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
permissions:
contents: write
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Install Dependencies
run: npm ci
shell: bash
- name: Build
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run dist
shell: bash

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
18

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-2022 Daniel D. Scalzi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

212
README.md
View File

@ -1,39 +1,138 @@
# Getting Started #
<p align="center"><img src="./app/assets/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
System Requirements:
* [Node.js](https://nodejs.org/en/) v8.9.0+
<h1 align="center">Helios Launcher</h1>
This repository is dedicated to the development of the new custom launcher for the [WesterosCraft](http://www.westeroscraft.com/) server. This project is developed primarily with [Node.js](https://nodejs.org/en/) and the [Electron](https://electron.atom.io/) framework. For further reference you may view [the repository of the new launcher written in JavaFX/Java](https://gitlab.com/westeroscraft/WesteroscraftNewLauncher) which was discontinued. You may also view the repository of the [current launcher](https://gitlab.com/westeroscraft/westeroscraftlaunchercore), a modified fork of MCUpdater.
<em><h5 align="center">(formerly Electron Launcher)</h5></em>
For authentication with Mojang, we are currently planning on using [node-mojang](https://github.com/jamen/node-mojang). This will automatically be downloaded if you follow the simple installation instructions below.
[<p align="center"><img src="https://img.shields.io/github/actions/workflow/status/dscalzi/HeliosLauncher/build.yml?branch=master&style=for-the-badge" alt="gh actions">](https://github.com/dscalzi/HeliosLauncher/actions) [<img src="https://img.shields.io/github/downloads/dscalzi/HeliosLauncher/total.svg?style=for-the-badge" alt="downloads">](https://github.com/dscalzi/HeliosLauncher/releases) <img src="https://forthebadge.com/images/badges/winter-is-coming.svg" height="28px" alt="winter-is-coming"></p>
### Recommended IDE ###
<p align="center">Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.</p>
The recommended IDE for this project is [VS Code](https://code.visualstudio.com/), an open source code editor by Microsoft. This editor is available on nearly every major platform (Windows, macOS, Linux). If you choose to use another editor, such as [Atom](https://atom.io/), please gitignore the IDE specific settings directory, if it hasn't been already.
![Screenshot 1](https://i.imgur.com/6o7SmH6.png)
![Screenshot 2](https://i.imgur.com/x3B34n1.png)
### Installation ###
## Features
To begin working on this project clone the repository and open run the following command on the command line. This will download all of the required dependencies.
* 🔒 Full account management.
* Add multiple accounts and easily switch between them.
* Microsoft (OAuth 2.0) + Mojang (Yggdrasil) authentication fully supported.
* Credentials are never stored and transmitted directly to Mojang.
* 📂 Efficient asset management.
* Receive client updates as soon as we release them.
* Files are validated before launch. Corrupt or incorrect files will be redownloaded.
* ☕ **Automatic Java validation.**
* If you have an incompatible version of Java installed, we'll install the right one *for you*.
* You do not need to have Java installed to run the launcher.
* 📰 News feed natively built into the launcher.
* ⚙️ Intuitive settings management, including a Java control panel.
* Supports all of our servers.
* Switch between server configurations with ease.
* View the player count of the selected server.
* Automatic updates. That's right, the launcher updates itself.
* View the status of Mojang's services.
```shell
npm install
This is not an exhaustive list. Download and install the launcher to gauge all it can do!
#### Need Help? [Check the wiki.][wiki]
#### Like the project? Leave a ⭐ star on the repository!
## Downloads
You can download from [GitHub Releases](https://github.com/dscalzi/HeliosLauncher/releases)
#### Latest Release
[![](https://img.shields.io/github/release/dscalzi/HeliosLauncher.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases/latest)
#### Latest Pre-Release
[![](https://img.shields.io/github/release/dscalzi/HeliosLauncher/all.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases)
**Supported Platforms**
If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/releases) tab, select the installer for your system.
| Platform | File |
| -------- | ---- |
| Windows x64 | `Helios-Launcher-setup-VERSION.exe` |
| macOS x64 | `Helios-Launcher-setup-VERSION-x64.dmg` |
| macOS arm64 | `Helios-Launcher-setup-VERSION-arm64.dmg` |
| Linux x64 | `Helios-Launcher-setup-VERSION.AppImage` |
## Console
To open the console, use the following keybind.
```console
ctrl + shift + i
```
# Launching #
Ensure that you have the console tab selected. Do not paste anything into the console unless you are 100% sure of what it will do. Pasting the wrong thing can expose sensitive information.
### Command Line ###
#### Export Output to a File
There are several different ways to launch this project. One way is simply to run the following command on the command line.
If you want to export the console output, simply right click anywhere on the console and click **Save as..**
```shell
npm start
![console example](https://i.imgur.com/T5e73jP.png)
## Development
This section details the setup of a basic developmentment environment.
### Getting Started
**System Requirements**
* [Node.js][nodejs] v18
---
**Clone and Install Dependencies**
```console
> git clone https://github.com/dscalzi/HeliosLauncher.git
> cd HeliosLauncher
> npm install
```
### Visual Studio Code ###
---
If you use VS Code, you can run this directly from the IDE. Copy the following code into your launch.json file. This will require you to also install [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome).
**Launch Application**
```json
```console
> npm start
```
---
**Build Installers**
To build for your current platform.
```console
> npm run dist
```
Build for a specific platform.
| Platform | Command |
| ----------- | -------------------- |
| Windows x64 | `npm run dist:win` |
| macOS | `npm run dist:mac` |
| Linux x64 | `npm run dist:linux` |
Builds for macOS may not work on Windows/Linux and vice-versa.
---
### Visual Studio Code
All development of the launcher should be done using [Visual Studio Code][vscode].
Paste the following into `.vscode/launch.json`
```JSON
{
"version": "0.2.0",
"configurations": [
@ -41,67 +140,72 @@ If you use VS Code, you can run this directly from the IDE. Copy the following c
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
},
"program": "${workspaceRoot}/index.js",
"console": "integratedTerminal",
"protocol": "inspector",
"timeout": 100000000
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/node_modules/electron/cli.js",
"args" : ["."],
"outputCapture": "std"
},
{
"name": "Debug Renderer Process",
"type": "chrome",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"runtimeArgs": [
"${workspaceRoot}/index.js",
"${workspaceFolder}/.",
"--remote-debugging-port=9222"
],
"webRoot": "${workspaceRoot}"
"webRoot": "${workspaceFolder}"
}
]
}
```
This will create two launch configurations from which you can debug the launcher. The first configuration, **Debug Main Process**, will allow you to debug the main Electron process. The second configuration, **Debug Renderer Process**, will allow you to debug the rendering of web pages (ie the UI).
This adds two debug configurations.
You can find more information [here](http://code.matsu.io/1).
#### Debug Main Process
### Notes on DevTools Window ###
This allows you to debug Electron's [main process][mainprocess]. You can debug scripts in the [renderer process][rendererprocess] by opening the DevTools Window.
Once you run the program, you can open the DevTools window by using the following keybind.
#### Debug Renderer Process
```shell
ctrl + shift + i
```
This allows you to debug Electron's [renderer process][rendererprocess]. This requires you to install the [Debugger for Chrome][chromedebugger] extension.
Please note that if you are debugging the application with VS Code and have launched the program using the **Debug Renderer Process** configuration you cannot open the DevTools window. If you attempt to do so, the program will crash. Remote debugging cannot be done with multiple DevTools clients.
Note that you **cannot** open the DevTools window while using this debug configuration. Chromium only allows one debugger, opening another will crash the program.
# Building
---
Run either of the build scrips noted below. Note that each platform can only be build when running on the specific operating system. Currently investigating ways to run multi-platform builds efficiently.
### Note on Third-Party Usage
### Build for Current Platform
Please give credit to the original author and provide a link to the original source. This is free software, please do at least this much.
* `npm run dist`
For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md.
### Platforms Specifc Builds
---
* Windows x64 (win32 x64)
* `npm run dist:win`
* MacOS (darwin x64)
* `npm run dist:mac`
* Linux (linux x64)
* `npm run dist:linux`
## Resources
# Issues / Further Support #
* [Wiki][wiki]
* [Nebula (Create Distribution.json)][nebula]
* [v2 Rewrite Branch (Inactive)][v2branch]
If you run into any issue which cannot be resolved via a quick google search, create an issue using the tab above.
The best way to contact the developers is on Discord.
Much of the discussion regarding this launcher is done on Discord, feel free to join us there [![Discord](https://discordapp.com/api/guilds/98469309352775680/widget.png)](https://discord.gg/hqdjs3m)
[![discord](https://discordapp.com/api/guilds/211524927831015424/embed.png?style=banner3)][discord]
---
### See you ingame.
[nodejs]: https://nodejs.org/en/ 'Node.js'
[vscode]: https://code.visualstudio.com/ 'Visual Studio Code'
[mainprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Main Process'
[rendererprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Renderer Process'
[chromedebugger]: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Debugger for Chrome'
[discord]: https://discord.gg/zNWUXdt 'Discord'
[wiki]: https://github.com/dscalzi/HeliosLauncher/wiki 'wiki'
[nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula'
[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch'

View File

@ -1,24 +1,49 @@
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Westeroscraft Launcher</title>
<script src="./assets/js/uicore.js"></script>
<script src="./assets/js/actionbinder.js"></script>
<meta charset="utf-8" http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-In6B8teKZQll5heMl9bS7CESTbGvuAt3VVV86BUQBDk='"/>
<title><%= lang('app.title') %></title>
<script src="./assets/js/scripts/uicore.js"></script>
<script src="./assets/js/scripts/uibinder.js"></script>
<link type="text/css" rel="stylesheet" href="./assets/css/launcher.css">
<style>
body {
background: url('assets/images/backgrounds/<%=0%>.jpg') no-repeat center center fixed;
/*background: url('assets/images/backgrounds/<%=bkid%>.jpg') no-repeat center center fixed;*/
transition: background-image 1s ease;
background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAPwBwAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8VooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9k=');
background-size: cover;
-webkit-user-select: none;
}
#main {
display: none;
height: calc(100% - 22px);
background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 100%);
width: 100%;
position: absolute;
z-index: 10;
}
#main[overlay] {
filter: blur(3px) contrast(0.9) brightness(1.0);
}
</style>
</head>
<body>
<% include frame.ejs %>
<body bkid="<%=bkid%>">
<%- include('frame') %>
<div id="main">
<% include login.ejs %>
<%- include('welcome') %>
<%- include('login') %>
<%- include('waiting') %>
<%- include('loginOptions') %>
<%- include('settings') %>
<%- include('landing') %>
</div>
<%- include('overlay') %>
<div id="loadingContainer">
<div id="loadingContent">
<div id="loadSpinnerContainer">
<img id="loadCenterImage" src="assets/images/LoadingSeal.png">
<img id="loadSpinnerImage" class="rotating" src="assets/images/LoadingText.png">
</div>
</div>
</div>
</body>
</html>

View File

@ -1,8 +0,0 @@
{
"repositoryRoot": "/modstore",
"modRef": [
"my.custom:mod:1.11.2",
"another.custom:mod:1.11.2"
],
"parentList": "/mods/WesterosCraft-1.11.2.json"
}

View File

@ -1,9 +0,0 @@
{
"repositoryRoot": "C:\\Users\\Asus\\Desktop\\LauncherElectron\\mcfiles\\modstore",
"modRef": [
"com.westeroscraft:westerosblocks:3.0.0-beta-71",
"mezz:jei:1.11.2-4.3.5.277",
"net.optifine:optifine:1.11.2_HD_U_B9",
"chatbubbles:chatbubbles:1.0.1_for_1.11.2"
]
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 KiB

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

View 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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

View File

@ -1,249 +0,0 @@
const cp = require('child_process')
const path = require('path')
const {AssetGuard} = require(path.join(__dirname, 'assets', 'js', 'assetguard.js'))
const ProcessBuilder = require(path.join(__dirname, 'assets', 'js', 'processbuilder.js'))
const ConfigManager = require(path.join(__dirname, 'assets', 'js', 'configmanager.js'))
const DiscordWrapper = require(path.join(__dirname, 'assets', 'js', 'discordwrapper.js'))
const Mojang = require(path.join(__dirname, 'assets', 'js', 'mojang.js'))
const AuthManager = require(path.join(__dirname, 'assets', 'js', 'authmanager.js'))
let mojangStatusListener
// Launch Elements
let launch_content, launch_details, launch_progress, launch_progress_label, launch_details_text
// Synchronous Listener
document.addEventListener('readystatechange', function(){
if (document.readyState === 'interactive'){
// Save a reference to the launch elements.
launch_content = document.getElementById('launch_content')
launch_details = document.getElementById('launch_details')
launch_progress = document.getElementById('launch_progress')
launch_progress_label = document.getElementById('launch_progress_label')
launch_details_text = document.getElementById('launch_details_text')
// Bind launch button
document.getElementById('launch_button').addEventListener('click', function(e){
console.log('Launching game..')
//testdownloads()
dlAsync()
})
// TODO convert this to dropdown menu.
// Bind selected server
document.getElementById('server_selection').innerHTML = '\u2022 ' + AssetGuard.getServerById(ConfigManager.getGameDirectory(), ConfigManager.getSelectedServer()).name
// Update Mojang Status Color
const refreshMojangStatuses = async function(){
console.log('Refreshing Mojang Statuses..')
try {
let status = 'grey'
const statuses = await Mojang.status()
greenCount = 0
for(let i=0; i<statuses.length; i++){
if(statuses[i].status === 'yellow' && status !== 'red'){
status = 'yellow'
continue
} else if(statuses[i].status === 'red'){
status = 'red'
break
}
++greenCount
}
if(greenCount == statuses.length){
status = 'green'
}
document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status)
} catch (err) {
console.error('Unable to refresh Mojang service status..', err)
}
}
refreshMojangStatuses()
// Set refresh rate to once every 5 minutes.
mojangStatusListener = setInterval(refreshMojangStatuses, 300000)
}
}, false)
// Keep reference to Minecraft Process
let proc
// Is DiscordRPC enabled
let hasRPC = false
// Joined server regex
const servJoined = /[[0-2][0-9]:[0-6][0-9]:[0-6][0-9]\] \[Client thread\/INFO\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/g
const gameJoined = /\[[0-2][0-9]:[0-6][0-9]:[0-6][0-9]\] \[Client thread\/WARN\]: Skipping bad option: lastServer:/g
const gameJoined2 = /\[[0-2][0-9]:[0-6][0-9]:[0-6][0-9]\] \[Client thread\/INFO\]: Created: \d+x\d+ textures-atlas/g
let aEx
let currentProc
let serv
let versionData
let forgeData
function dlAsync(login = true){
// Login parameter is temporary for debug purposes. Allows testing the validation/downloads without
// launching the game.
if(login) {
if(ConfigManager.getSelectedAccount() == null){
console.error('login first.')
//in devtools AuthManager.addAccount(username, pass)
return
}
}
launch_details_text.innerHTML = 'Please wait..'
launch_progress.setAttribute('max', '100')
launch_details.style.display = 'flex'
launch_content.style.display = 'none'
aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
ConfigManager.getGameDirectory(),
ConfigManager.getJavaExecutable()
])
aEx.on('message', (m) => {
if(currentProc === 'validateDistribution'){
launch_progress.setAttribute('value', 20)
launch_progress_label.innerHTML = '20%'
serv = m.result
console.log('forge stuff done')
// Begin version load.
launch_details_text.innerHTML = 'Loading version information..'
currentProc = 'loadVersionData'
aEx.send({task: 0, content: currentProc, argsArr: [serv.mc_version]})
} else if(currentProc === 'loadVersionData'){
launch_progress.setAttribute('value', 40)
launch_progress_label.innerHTML = '40%'
versionData = m.result
// Begin asset validation.
launch_details_text.innerHTML = 'Validating asset integrity..'
currentProc = 'validateAssets'
aEx.send({task: 0, content: currentProc, argsArr: [versionData]})
} else if(currentProc === 'validateAssets'){
launch_progress.setAttribute('value', 60)
launch_progress_label.innerHTML = '60%'
console.log('assets done')
// Begin library validation.
launch_details_text.innerHTML = 'Validating library integrity..'
currentProc = 'validateLibraries'
aEx.send({task: 0, content: currentProc, argsArr: [versionData]})
} else if(currentProc === 'validateLibraries'){
launch_progress.setAttribute('value', 80)
launch_progress_label.innerHTML = '80%'
console.log('libs done')
// Begin miscellaneous validation.
launch_details_text.innerHTML = 'Validating miscellaneous file integrity..'
currentProc = 'validateMiscellaneous'
aEx.send({task: 0, content: currentProc, argsArr: [versionData]})
} else if(currentProc === 'validateMiscellaneous'){
launch_progress.setAttribute('value', 100)
launch_progress_label.innerHTML = '100%'
console.log('files done')
launch_details_text.innerHTML = 'Downloading files..'
currentProc = 'processDlQueues'
aEx.send({task: 0, content: currentProc})
} else if(currentProc === 'processDlQueues'){
if(m.task === 0){
remote.getCurrentWindow().setProgressBar(m.value/m.total)
launch_progress.setAttribute('max', m.total)
launch_progress.setAttribute('value', m.value)
launch_progress_label.innerHTML = m.percent + '%'
} else if(m.task === 1){
remote.getCurrentWindow().setProgressBar(-1)
launch_details_text.innerHTML = 'Preparing to launch..'
currentProc = 'loadForgeData'
aEx.send({task: 0, content: currentProc, argsArr: [serv.id]})
} else {
console.error('Unknown download data type.', m)
}
} else if(currentProc === 'loadForgeData'){
forgeData = m.result
if(login) {
//if(!(await AuthManager.validateSelected())){
//
//}
const authUser = ConfigManager.getSelectedAccount();
console.log('authu', authUser)
let pb = new ProcessBuilder(ConfigManager.getGameDirectory(), serv, versionData, forgeData, authUser)
launch_details_text.innerHTML = 'Launching game..'
try{
proc = pb.build()
launch_details_text.innerHTML = 'Done. Enjoy the server!'
const tempListener = function(data){
if(data.indexOf('[Client thread/INFO]: -- System Details --') > -1){
launch_details.style.display = 'none'
launch_content.style.display = 'inline-flex'
if(hasRPC){
DiscordWrapper.updateDetails('Loading game..')
}
proc.stdout.removeListener('data', tempListener)
}
}
const gameStateChange = function(data){
if(servJoined.test(data)){
DiscordWrapper.updateDetails('Exploring the Realm!')
} else if(gameJoined.test(data)){
DiscordWrapper.updateDetails('Idling on Main Menu')
}
}
proc.stdout.on('data', tempListener)
proc.stdout.on('data', gameStateChange)
// Init Discord Hook (Untested)
const distro = AssetGuard.retrieveDistributionDataSync(ConfigManager.getGameDirectory)
if(distro.discord != null && serv.discord != null){
DiscordWrapper.initRPC(distro.discord, serv.discord)
hasRPC = true
proc.on('close', (code, signal) => {
console.log('Shutting down Discord Rich Presence..')
DiscordWrapper.shutdownRPC()
hasRPC = false
proc = null
})
}
} catch(err) {
//launch_details_text.innerHTML = 'Error: ' + err.message;
launch_details_text.innerHTML = 'Error: See log for details..';
console.log(err)
setTimeout(function(){
launch_details.style.display = 'none'
launch_content.style.display = 'inline-flex'
}, 5000)
}
}
// Disconnect from AssetExec
aEx.disconnect()
}
})
launch_details_text.innerHTML = 'Loading server information..'
currentProc = 'validateDistribution'
aEx.send({task: 0, content: currentProc, argsArr: [ConfigManager.getSelectedServer()]})
}

View File

@ -1,37 +0,0 @@
const {AssetGuard} = require('./assetguard.js')
const tracker = new AssetGuard(process.argv[2], process.argv[3])
console.log('AssetExec Started')
// Temporary for debug purposes.
process.on('unhandledRejection', r => console.log(r))
tracker.on('totaldlprogress', (data) => {
process.send({task: 0, total: data.total, value: data.acc, percent: parseInt((data.acc/data.total)*100)})
})
tracker.on('dlcomplete', () => {
process.send({task: 1})
})
process.on('message', (msg) => {
if(msg.task === 0){
const func = msg.content
if(typeof tracker[func] === 'function'){
const f = tracker[func]
const res = f.apply(tracker, msg.argsArr)
if(res instanceof Promise){
res.then((v) => {
process.send({result: v})
})
} else {
process.send({result: res})
}
}
}
})
process.on('disconnect', () => {
console.log('AssetExec Disconnected')
process.exit(0)
})

View File

@ -1,972 +0,0 @@
/**
* AssetGuard
*
* This module aims to provide a comprehensive and stable method for processing
* and downloading game assets for the WesterosCraft server. Download meta is
* for several identifiers (categories) is stored inside of an AssetGuard object.
* This meta data is initially empty until one of the module's processing functions
* are called. That function will process the corresponding asset index and validate
* any exisitng local files. If a file is missing or fails validation, it will be
* placed into a download queue (array). This queue is wrapped in a download tracker object
* so that essential information can be cached. The download tracker object is then
* assigned as the value of the identifier in the AssetGuard object. These download
* trackers will remain idle until an async process is started to process them.
*
* Once the async process is started, any enqueued assets will be downloaded. The AssetGuard
* object will emit events throughout the download whose name correspond to the identifier
* being processed. For example, if the 'assets' identifier was being processed, whenever
* the download stream recieves data, the event 'assetsdlprogress' will be emitted off of
* the AssetGuard instance. This can be listened to by external modules allowing for
* categorical tracking of the downloading process.
*
* @module assetguard
*/
// Requirements
const AdmZip = require('adm-zip')
const async = require('async')
const child_process = require('child_process')
const crypto = require('crypto')
const EventEmitter = require('events')
const fs = require('fs')
const mkpath = require('mkdirp');
const path = require('path')
const request = require('request')
// Classes
/** Class representing a base asset. */
class Asset {
/**
* Create an asset.
*
* @param {any} id - id of the asset.
* @param {String} hash - hash value of the asset.
* @param {Number} size - size in bytes of the asset.
* @param {String} from - url where the asset can be found.
* @param {String} to - absolute local file path of the asset.
*/
constructor(id, hash, size, from, to){
this.id = id
this.hash = hash
this.size = size
this.from = from
this.to = to
}
}
/** Class representing a mojang library. */
class Library extends Asset {
/**
* Converts the process.platform OS names to match mojang's OS names.
*/
static mojangFriendlyOS(){
const opSys = process.platform
if (opSys === 'darwin') {
return 'osx';
} else if (opSys === 'win32'){
return 'windows';
} else if (opSys === 'linux'){
return 'linux';
} else {
return 'unknown_os';
}
}
/**
* Checks whether or not a library is valid for download on a particular OS, following
* the rule format specified in the mojang version data index. If the allow property has
* an OS specified, then the library can ONLY be downloaded on that OS. If the disallow
* property has instead specified an OS, the library can be downloaded on any OS EXCLUDING
* the one specified.
*
* @param {Object} rules - the Library's download rules.
* @returns {Boolean} - true if the Library follows the specified rules, otherwise false.
*/
static validateRules(rules){
if(rules == null) return true
let result = true
rules.forEach(function(rule){
const action = rule['action']
const osProp = rule['os']
if(action != null){
if(osProp != null){
const osName = osProp['name']
const osMoj = Library.mojangFriendlyOS()
if(action === 'allow'){
result = osName === osMoj
return
} else if(action === 'disallow'){
result = osName !== osMoj
return
}
}
}
})
return result
}
}
class DistroModule extends Asset {
/**
* Create a DistroModule. This is for processing,
* not equivalent to the module objects in the
* distro index.
*
* @param {any} id - id of the asset.
* @param {String} hash - hash value of the asset.
* @param {Number} size - size in bytes of the asset.
* @param {String} from - url where the asset can be found.
* @param {String} to - absolute local file path of the asset.
* @param {String} type - the module type.
*/
constructor(id, hash, size, from, to, type){
super(id, hash, size, from, to)
this.type = type
}
}
/**
* Class representing a download tracker. This is used to store meta data
* about a download queue, including the queue itself.
*/
class DLTracker {
/**
* Create a DLTracker
*
* @param {Array.<Asset>} dlqueue - an array containing assets queued for download.
* @param {Number} dlsize - the combined size of each asset in the download queue array.
* @param {function(Asset)} callback - optional callback which is called when an asset finishes downloading.
*/
constructor(dlqueue, dlsize, callback = null){
this.dlqueue = dlqueue
this.dlsize = dlsize
this.callback = callback
}
}
let distributionData = null
/**
* Central object class used for control flow. This object stores data about
* categories of downloads. Each category is assigned an identifier with a
* DLTracker object as its value. Combined information is also stored, such as
* the total size of all the queued files in each category. This event is used
* to emit events so that external modules can listen into processing done in
* this module.
*/
class AssetGuard extends EventEmitter {
/**
* Create an instance of AssetGuard.
* On creation the object's properties are never-null default
* values. Each identifier is resolved to an empty DLTracker.
*
* @param {String} basePath - base path for asset validation (game root).
* @param {String} javaexec - path to a java executable which will be used
* to finalize installation.
*/
constructor(basePath, javaexec){
super()
this.totaldlsize = 0;
this.progress = 0;
this.assets = new DLTracker([], 0)
this.libraries = new DLTracker([], 0)
this.files = new DLTracker([], 0)
this.forge = new DLTracker([], 0)
this.basePath = basePath
this.javaexec = javaexec
}
// Static Utility Functions
/**
* Resolve an artifact id into a path. For example, on windows
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
* net\minecraftforge\forge\1.11.2-13.20.0.2282\forge-1.11.2-13.20.0.2282.jar
*
* @param {String} artifactid - the artifact id string.
* @param {String} extension - the extension of the file at the resolved path.
* @returns {String} - the resolved relative path from the artifact id.
*/
static _resolvePath(artifactid, extension){
let ps = artifactid.split(':')
let cs = ps[0].split('.')
cs.push(ps[1])
cs.push(ps[2])
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
return path.join.apply(path, cs)
}
/**
* Resolve an artifact id into a URL. For example,
* 'net.minecraftforge:forge:1.11.2-13.20.0.2282', '.jar' becomes
* net/minecraftforge/forge/1.11.2-13.20.0.2282/forge-1.11.2-13.20.0.2282.jar
*
* @param {String} artifactid - the artifact id string.
* @param {String} extension - the extension of the file at the resolved url.
* @returns {String} - the resolved relative URL from the artifact id.
*/
static _resolveURL(artifactid, extension){
let ps = artifactid.split(':')
let cs = ps[0].split('.')
cs.push(ps[1])
cs.push(ps[2])
cs.push(ps[1].concat('-').concat(ps[2]).concat(extension))
return cs.join('/')
}
/**
* Calculates the hash for a file using the specified algorithm.
*
* @param {Buffer} buf - the buffer containing file data.
* @param {String} algo - the hash algorithm.
* @returns {String} - the calculated hash in hex.
*/
static _calculateHash(buf, algo){
return crypto.createHash(algo).update(buf).digest('hex')
}
/**
* Used to parse a checksums file. This is specifically designed for
* the checksums.sha1 files found inside the forge scala dependencies.
*
* @param {String} content - the string content of the checksums file.
* @returns {Object} - an object with keys being the file names, and values being the hashes.
*/
static _parseChecksumsFile(content){
let finalContent = {}
let lines = content.split('\n')
for(let i=0; i<lines.length; i++){
let bits = lines[i].split(' ')
if(bits[1] == null) {
continue
}
finalContent[bits[1]] = bits[0]
}
return finalContent
}
/**
* Validate that a file exists and matches a given hash value.
*
* @param {String} filePath - the path of the file to validate.
* @param {String} algo - the hash algorithm to check against.
* @param {String} hash - the existing hash to check against.
* @returns {Boolean} - true if the file exists and calculated hash matches the given hash, otherwise false.
*/
static _validateLocal(filePath, algo, hash){
if(fs.existsSync(filePath)){
//No hash provided, have to assume it's good.
if(hash == null){
return true
}
let fileName = path.basename(filePath)
let buf = fs.readFileSync(filePath)
let calcdhash = AssetGuard._calculateHash(buf, algo)
return calcdhash === hash
}
return false;
}
/**
* Statically retrieve the distribution data.
*
* @param {String} basePath - base path for asset validation (game root).
* @param {Boolean} cached - optional. False if the distro should be freshly downloaded, else
* a cached copy will be returned.
* @returns {Promise.<Object>} - A promise which resolves to the distribution data object.
*/
static retrieveDistributionData(basePath, cached = true){
return new Promise(function(fulfill, reject){
if(!cached || distributionData == null){
// TODO Download file from upstream.
//const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
// TODO Save file to path.join(basePath, 'westeroscraft.json')
// TODO Fulfill with JSON.parse()
// Workaround while file is not hosted.
fs.readFile(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8', (err, data) => {
distributionData = JSON.parse(data)
fulfill(distributionData)
})
} else {
fulfill(distributionData)
}
})
}
/**
* Statically retrieve the distribution data.
*
* @param {String} basePath - base path for asset validation (game root).
* @param {Boolean} cached - optional. False if the distro should be freshly downloaded, else
* a cached copy will be returned.
* @returns {Object} - The distribution data object.
*/
static retrieveDistributionDataSync(basePath, cached = true){
if(!cached || distributionData == null){
distributionData = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8'))
}
return distributionData
}
/**
* Resolve the default selected server from the distribution index.
*
* @param {String} basePath - base path for asset validation (game root).
* @returns {Object} - An object resolving to the default selected server.
*/
static resolveSelectedServer(basePath){
const distro = AssetGuard.retrieveDistributionDataSync(basePath)
const servers = distro.servers
for(let i=0; i<servers.length; i++){
if(servers[i].default_selected){
return servers[i].id
}
}
// If no server declares default_selected, default to the first one declared.
return (servers.length > 0) ? servers[0].id : null
}
/**
* Gets a server from the distro index which maches the provided ID.
* Returns null if the ID could not be found or the distro index has
* not yet been loaded.
*
* @param {String} basePath - base path for asset validation (game root).
* @param {String} serverID - The id of the server to retrieve.
* @returns {Object} - The server object whose id matches the parameter.
*/
static getServerById(basePath, serverID){
if(distributionData == null){
AssetGuard.retrieveDistributionDataSync(basePath, false)
}
const servers = distributionData.servers
let serv = null
for(let i=0; i<servers.length; i++){
if(servers[i].id === serverID){
serv = servers[i]
}
}
return serv
}
/**
* Validates a file in the style used by forge's version index.
*
* @param {String} filePath - the path of the file to validate.
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
* @returns {Boolean} - true if the file exists and the hashes match, otherwise false.
*/
static _validateForgeChecksum(filePath, checksums){
if(fs.existsSync(filePath)){
if(checksums == null || checksums.length === 0){
return true
}
let buf = fs.readFileSync(filePath)
let calcdhash = AssetGuard._calculateHash(buf, 'sha1')
let valid = checksums.includes(calcdhash)
if(!valid && filePath.endsWith('.jar')){
valid = AssetGuard._validateForgeJar(filePath, checksums)
}
return valid
}
return false
}
/**
* Validates a forge jar file dependency who declares a checksums.sha1 file.
* This can be an expensive task as it usually requires that we calculate thousands
* of hashes.
*
* @param {Buffer} buf - the buffer of the jar file.
* @param {Array.<String>} checksums - the checksums listed in the forge version index.
* @returns {Boolean} - true if all hashes declared in the checksums.sha1 file match the actual hashes.
*/
static _validateForgeJar(buf, checksums){
// Double pass method was the quickest I found. I tried a version where we store data
// to only require a single pass, plus some quick cleanup but that seemed to take slightly more time.
const hashes = {}
let expected = {}
const zip = new AdmZip(buf)
const zipEntries = zip.getEntries()
//First pass
for(let i=0; i<zipEntries.length; i++){
let entry = zipEntries[i]
if(entry.entryName === 'checksums.sha1'){
expected = AssetGuard._parseChecksumsFile(zip.readAsText(entry))
}
hashes[entry.entryName] = AssetGuard._calculateHash(entry.getData(), 'sha1')
}
if(!checksums.includes(hashes['checksums.sha1'])){
return false
}
//Check against expected
const expectedEntries = Object.keys(expected)
for(let i=0; i<expectedEntries.length; i++){
if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
return false
}
}
return true
}
/**
* Extracts and unpacks a file from .pack.xz format.
*
* @param {Array.<String>} filePaths - The paths of the files to be extracted and unpacked.
* @returns {Promise.<Void>} - An empty promise to indicate the extraction has completed.
*/
static _extractPackXZ(filePaths, javaExecutable){
return new Promise(function(fulfill, reject){
const libPath = path.join(__dirname, '..', 'libraries', 'java', 'PackXZExtract.jar')
const filePath = filePaths.join(',')
const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath])
child.stdout.on('data', (data) => {
//console.log('PackXZExtract:', data.toString('utf8'))
})
child.stderr.on('data', (data) => {
//console.log('PackXZExtract:', data.toString('utf8'))
})
child.on('close', (code, signal) => {
//console.log('PackXZExtract: Exited with code', code)
fulfill()
})
})
}
/**
* Function which finalizes the forge installation process. This creates a 'version'
* instance for forge and saves its version.json file into that instance. If that
* instance already exists, the contents of the version.json file are read and returned
* in a promise.
*
* @param {Asset} asset - The Asset object representing Forge.
* @param {String} basePath - Base path for asset validation (game root).
* @returns {Promise.<Object>} - A promise which resolves to the contents of forge's version.json.
*/
static _finalizeForgeAsset(asset, basePath){
return new Promise(function(fulfill, reject){
fs.readFile(asset.to, (err, data) => {
const zip = new AdmZip(data)
const zipEntries = zip.getEntries()
for(let i=0; i<zipEntries.length; i++){
if(zipEntries[i].entryName === 'version.json'){
const forgeVersion = JSON.parse(zip.readAsText(zipEntries[i]))
const versionPath = path.join(basePath, 'versions', forgeVersion.id)
const versionFile = path.join(versionPath, forgeVersion.id + '.json')
if(!fs.existsSync(versionFile)){
mkpath.sync(versionPath)
fs.writeFileSync(path.join(versionPath, forgeVersion.id + '.json'), zipEntries[i].getData())
fulfill(forgeVersion)
} else {
//Read the saved file to allow for user modifications.
fulfill(JSON.parse(fs.readFileSync(versionFile, 'utf-8')))
}
return
}
}
//We didn't find forge's version.json.
reject('Unable to finalize Forge processing, version.json not found! Has forge changed their format?')
})
})
}
/**
* Initiate an async download process for an AssetGuard DLTracker.
*
* @param {String} identifier - the identifier of the AssetGuard DLTracker.
* @param {Number} limit - optional. The number of async processes to run in parallel.
* @returns {Boolean} - true if the process began, otherwise false.
*/
startAsyncProcess(identifier, limit = 5){
const self = this
let acc = 0
const concurrentDlTracker = this[identifier]
const concurrentDlQueue = concurrentDlTracker.dlqueue.slice(0)
if(concurrentDlQueue.length === 0){
return false
} else {
async.eachLimit(concurrentDlQueue, limit, function(asset, cb){
let count = 0;
mkpath.sync(path.join(asset.to, ".."))
let req = request(asset.from)
req.pause()
req.on('response', (resp) => {
if(resp.statusCode === 200){
let writeStream = fs.createWriteStream(asset.to)
writeStream.on('close', () => {
//console.log('DLResults ' + asset.size + ' ' + count + ' ', asset.size === count)
if(concurrentDlTracker.callback != null){
concurrentDlTracker.callback.apply(concurrentDlTracker, [asset])
}
cb()
})
req.pipe(writeStream)
req.resume()
} else {
req.abort()
console.log('Failed to download ' + asset.from + '. Response code', resp.statusCode)
self.progress += asset.size*1
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
cb()
}
})
req.on('data', function(chunk){
count += chunk.length
self.progress += chunk.length
acc += chunk.length
self.emit(identifier + 'dlprogress', acc)
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
})
}, function(err){
if(err){
self.emit(identifier + 'dlerror')
console.log('An item in ' + identifier + ' failed to process');
} else {
self.emit(identifier + 'dlcomplete')
console.log('All ' + identifier + ' have been processed successfully')
}
self.totaldlsize -= self[identifier].dlsize
self.progress -= self[identifier].dlsize
self[identifier] = new DLTracker([], 0)
if(self.totaldlsize === 0) {
self.emit('dlcomplete')
}
})
return true
}
}
// Validation Functions
/**
* Loads the version data for a given minecraft version.
*
* @param {String} version - the game version for which to load the index data.
* @param {Boolean} force - optional. If true, the version index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Object>} - Promise which resolves to the version data object.
*/
loadVersionData(version, force = false){
const self = this
return new Promise(function(fulfill, reject){
const name = version + '.json'
const url = 'https://s3.amazonaws.com/Minecraft.Download/versions/' + version + '/' + name
const versionPath = path.join(self.basePath, 'versions', version)
const versionFile = path.join(versionPath, name)
if(!fs.existsSync(versionFile) || force){
//This download will never be tracked as it's essential and trivial.
request.head(url, function(err, res, body){
console.log('Preparing download of ' + version + ' assets.')
mkpath.sync(versionPath)
const stream = request(url).pipe(fs.createWriteStream(versionFile))
stream.on('finish', function(){
fulfill(JSON.parse(fs.readFileSync(versionFile)))
})
})
} else {
fulfill(JSON.parse(fs.readFileSync(versionFile)))
}
})
}
/**
* Public asset validation function. This function will handle the validation of assets.
* It will parse the asset index specified in the version data, analyzing each
* asset entry. In this analysis it will check to see if the local file exists and is valid.
* If not, it will be added to the download queue for the 'assets' identifier.
*
* @param {Object} versionData - the version data for the assets.
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
validateAssets(versionData, force = false){
const self = this
return new Promise(function(fulfill, reject){
self._assetChainIndexData(versionData, force).then(() => {
fulfill()
})
})
}
//Chain the asset tasks to provide full async. The below functions are private.
/**
* Private function used to chain the asset validation process. This function retrieves
* the index data.
* @param {Object} versionData
* @param {Boolean} force
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
_assetChainIndexData(versionData, force = false){
const self = this
return new Promise(function(fulfill, reject){
//Asset index constants.
const assetIndex = versionData.assetIndex
const name = assetIndex.id + '.json'
const indexPath = path.join(self.basePath, 'assets', 'indexes')
const assetIndexLoc = path.join(indexPath, name)
let data = null
if(!fs.existsSync(assetIndexLoc) || force){
console.log('Downloading ' + versionData.id + ' asset index.')
mkpath.sync(indexPath)
const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc))
stream.on('finish', function() {
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
self._assetChainValidateAssets(versionData, data).then(() => {
fulfill()
})
})
} else {
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
self._assetChainValidateAssets(versionData, data).then(() => {
fulfill()
})
}
})
}
/**
* Private function used to chain the asset validation process. This function processes
* the assets and enqueues missing or invalid files.
* @param {Object} versionData
* @param {Boolean} force
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
_assetChainValidateAssets(versionData, indexData){
const self = this
return new Promise(function(fulfill, reject){
//Asset constants
const resourceURL = 'http://resources.download.minecraft.net/'
const localPath = path.join(self.basePath, 'assets')
const indexPath = path.join(localPath, 'indexes')
const objectPath = path.join(localPath, 'objects')
const assetDlQueue = []
let dlSize = 0;
//const objKeys = Object.keys(data.objects)
async.forEachOfLimit(indexData.objects, 10, function(value, key, cb){
const hash = value.hash
const assetName = path.join(hash.substring(0, 2), hash)
const urlName = hash.substring(0, 2) + "/" + hash
const ast = new Asset(key, hash, String(value.size), resourceURL + urlName, path.join(objectPath, assetName))
if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){
dlSize += (ast.size*1)
assetDlQueue.push(ast)
}
cb()
}, function(err){
self.assets = new DLTracker(assetDlQueue, dlSize)
fulfill()
})
})
}
/**
* Public library validation function. This function will handle the validation of libraries.
* It will parse the version data, analyzing each library entry. In this analysis, it will
* check to see if the local file exists and is valid. If not, it will be added to the download
* queue for the 'libraries' identifier.
*
* @param {Object} versionData - the version data for the assets.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
validateLibraries(versionData){
const self = this
return new Promise(function(fulfill, reject){
const libArr = versionData.libraries
const libPath = path.join(self.basePath, 'libraries')
const libDlQueue = []
let dlSize = 0
//Check validity of each library. If the hashs don't match, download the library.
async.eachLimit(libArr, 5, function(lib, cb){
if(Library.validateRules(lib.rules)){
let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
if(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){
dlSize += (libItm.size*1)
libDlQueue.push(libItm)
}
}
cb()
}, function(err){
self.libraries = new DLTracker(libDlQueue, dlSize)
fulfill()
})
})
}
/**
* Public miscellaneous mojang file validation function. These files will be enqueued under
* the 'files' identifier.
*
* @param {Object} versionData - the version data for the assets.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
validateMiscellaneous(versionData){
const self = this
return new Promise(async function(fulfill, reject){
await self.validateClient(versionData)
await self.validateLogConfig(versionData)
fulfill()
})
}
/**
* Validate client file - artifact renamed from client.jar to '{version}'.jar.
*
* @param {Object} versionData - the version data for the assets.
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
validateClient(versionData, force = false){
const self = this
return new Promise(function(fulfill, reject){
const clientData = versionData.downloads.client
const version = versionData.id
const targetPath = path.join(self.basePath, 'versions', version)
const targetFile = version + '.jar'
let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile))
if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){
self.files.dlqueue.push(client)
self.files.dlsize += client.size*1
fulfill()
} else {
fulfill()
}
})
}
/**
* Validate log config.
*
* @param {Object} versionData - the version data for the assets.
* @param {Boolean} force - optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<Void>} - An empty promise to indicate the async processing has completed.
*/
validateLogConfig(versionData){
const self = this
return new Promise(function(fulfill, reject){
const client = versionData.logging.client
const file = client.file
const targetPath = path.join(self.basePath, 'assets', 'log_configs')
let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id))
if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){
self.files.dlqueue.push(logConfig)
self.files.dlsize += logConfig.size*1
fulfill()
} else {
fulfill()
}
})
}
/**
* Validate the distribution.
*
* @param {String} serverpackid - The id of the server to validate.
* @returns {Promise.<Object>} - A promise which resolves to the server distribution object.
*/
validateDistribution(serverpackid){
const self = this
return new Promise(function(fulfill, reject){
AssetGuard.retrieveDistributionData(self.basePath, false).then((value) => {
/*const servers = value.servers
let serv = null
for(let i=0; i<servers.length; i++){
if(servers[i].id === serverpackid){
serv = servers[i]
break
}
}*/
const serv = AssetGuard.getServerById(self.basePath, serverpackid)
if(serv == null) {
console.error('Invalid server pack id:', serverpackid)
}
self.forge = self._parseDistroModules(serv.modules, serv.mc_version)
//Correct our workaround here.
let decompressqueue = self.forge.callback
self.forge.callback = function(asset){
if(asset.to.toLowerCase().endsWith('.pack.xz')){
AssetGuard._extractPackXZ([asset.to], self.javaexec)
}
if(asset.type === 'forge-hosted' || asset.type === 'forge'){
AssetGuard._finalizeForgeAsset(asset, self.basePath)
}
}
fulfill(serv)
})
})
}
/*//TODO The file should be hosted, the following code is for local testing.
_chainValidateDistributionIndex(basePath){
return new Promise(function(fulfill, reject){
//const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
//const targetFile = path.join(basePath, 'westeroscraft.json')
//TEMP WORKAROUND TO TEST WHILE THIS IS NOT HOSTED
fs.readFile(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8', (err, data) => {
fulfill(JSON.parse(data))
})
})
}*/
_parseDistroModules(modules, version){
let alist = []
let asize = 0;
//This may be removed soon, considering the most efficient way to extract.
let decompressqueue = []
for(let i=0; i<modules.length; i++){
let ob = modules[i]
let obType = ob.type
let obArtifact = ob.artifact
let obPath = obArtifact.path == null ? AssetGuard._resolvePath(ob.id, obArtifact.extension) : obArtifact.path
switch(obType){
case 'forge-hosted':
case 'forge':
case 'library':
obPath = path.join(this.basePath, 'libraries', obPath)
break
case 'forgemod':
//obPath = path.join(this.basePath, 'mods', obPath)
obPath = path.join(this.basePath, 'modstore', obPath)
break
case 'litemod':
//obPath = path.join(this.basePath, 'mods', version, obPath)
obPath = path.join(this.basePath, 'modstore', obPath)
break
case 'file':
default:
obPath = path.join(this.basePath, obPath)
}
let artifact = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, obType)
const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath
if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){
asize += artifact.size*1
alist.push(artifact)
if(validationPath !== obPath) decompressqueue.push(obPath)
}
//Recursively process the submodules then combine the results.
if(ob.sub_modules != null){
let dltrack = this._parseDistroModules(ob.sub_modules, version)
asize += dltrack.dlsize*1
alist = alist.concat(dltrack.dlqueue)
decompressqueue = decompressqueue.concat(dltrack.callback)
}
}
//Since we have no callback at this point, we use this value to store the decompressqueue.
return new DLTracker(alist, asize, decompressqueue)
}
/**
* Loads Forge's version.json data into memory for the specified server id.
*
* @param {String} serverpack - The id of the server to load Forge data for.
* @returns {Promise.<Object>} - A promise which resolves to Forge's version.json data.
*/
loadForgeData(serverpack){
const self = this
return new Promise(async function(fulfill, reject){
let distro = AssetGuard.retrieveDistributionDataSync(self.basePath)
const servers = distro.servers
let serv = null
for(let i=0; i<servers.length; i++){
if(servers[i].id === serverpack){
serv = servers[i]
break
}
}
const modules = serv.modules
for(let i=0; i<modules.length; i++){
const ob = modules[i]
if(ob.type === 'forge-hosted' || ob.type === 'forge'){
let obArtifact = ob.artifact
let obPath = obArtifact.path == null ? path.join(self.basePath, 'libraries', AssetGuard._resolvePath(ob.id, obArtifact.extension)) : obArtifact.path
let asset = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, ob.type)
let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.basePath)
fulfill(forgeData)
return
}
}
reject('No forge module found!')
})
}
_parseForgeLibraries(){
/* TODO
* Forge asset validations are already implemented. When there's nothing much
* to work on, implement forge downloads using forge's version.json. This is to
* have the code on standby if we ever need it (since it's half implemented already).
*/
}
/**
* This function will initiate the download processed for the specified identifiers. If no argument is
* given, all identifiers will be initiated. Note that in order for files to be processed you need to run
* the processing function corresponding to that identifier. If you run this function without processing
* the files, it is likely nothing will be enqueued in the object and processing will complete
* immediately. Once all downloads are complete, this function will fire the 'dlcomplete' event on the
* global object instance.
*
* @param {Array.<{id: string, limit: number}>} identifiers - optional. The identifiers to process and corresponding parallel async task limit.
*/
processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){
this.progress = 0;
let shouldFire = true
// Assign dltracking variables.
this.totaldlsize = 0
this.progress = 0
for(let i=0; i<identifiers.length; i++){
this.totaldlsize += this[identifiers[i].id].dlsize
}
for(let i=0; i<identifiers.length; i++){
let iden = identifiers[i]
let r = this.startAsyncProcess(iden.id, iden.limit)
if(r) shouldFire = false
}
if(shouldFire){
this.emit('dlcomplete')
}
}
}
module.exports = {
AssetGuard,
Asset,
Library
}

View File

@ -1,34 +1,315 @@
const ConfigManager = require('./configmanager.js')
const Mojang = require('./mojang.js')
/**
* AuthManager
*
* This module aims to abstract login procedures. Results from Mojang's REST api
* are retrieved through our Mojang module. These results are processed and stored,
* if applicable, in the config using the ConfigManager. All login procedures should
* be made through this module.
*
* @module authmanager
*/
// Requirements
const ConfigManager = require('./configmanager')
const { LoggerUtil } = require('helios-core')
const { RestResponseStatus } = require('helios-core/common')
const { MojangRestAPI, mojangErrorDisplayable, MojangErrorCode } = require('helios-core/mojang')
const { MicrosoftAuth, microsoftErrorDisplayable, MicrosoftErrorCode } = require('helios-core/microsoft')
const { AZURE_CLIENT_ID } = require('./ipcconstants')
exports.addAccount = async function(username, password){
const log = LoggerUtil.getLogger('AuthManager')
// Functions
/**
* Add a Mojang account. This will authenticate the given credentials with Mojang's
* authserver. The resultant data will be stored as an auth account in the
* configuration database.
*
* @param {string} username The account username (email if migrated).
* @param {string} password The account password.
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
*/
exports.addMojangAccount = async function(username, password) {
try {
const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken)
} catch (err){
return Promise.reject(err)
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)
}
const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
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))
}
}
const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 }
/**
* Perform the full MS Auth flow in a given mode.
*
* AUTH_MODE.FULL = Full authorization for a new account.
* AUTH_MODE.MS_REFRESH = Full refresh authorization.
* AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token.
*
* @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken
* @param {*} authMode The auth mode.
* @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH.
*/
async function fullMicrosoftAuthFlow(entryCode, authMode) {
try {
let accessTokenRaw
let accessToken
if(authMode !== AUTH_MODE.MC_REFRESH) {
const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID)
if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) {
return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode))
}
accessToken = accessTokenResponse.data
accessTokenRaw = accessToken.access_token
} else {
accessTokenRaw = entryCode
}
const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw)
if(xblResponse.responseStatus === RestResponseStatus.ERROR) {
return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode))
}
const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data)
if(xstsResonse.responseStatus === RestResponseStatus.ERROR) {
return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode))
}
const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data)
if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) {
return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode))
}
const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token)
if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) {
return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode))
}
return {
accessToken,
accessTokenRaw,
xbl: xblResponse.data,
xsts: xstsResonse.data,
mcToken: mcTokenResponse.data,
mcProfile: mcProfileResponse.data
}
} catch(err) {
log.error(err)
return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN))
}
}
/**
* Calculate the expiry date. Advance the expiry time by 10 seconds
* to reduce the liklihood of working with an expired token.
*
* @param {number} nowMs Current time milliseconds.
* @param {number} epiresInS Expires in (seconds)
* @returns
*/
function calculateExpiryDate(nowMs, epiresInS) {
return nowMs + ((epiresInS-10)*1000)
}
/**
* Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow.
* The resultant data will be stored as an auth account in the configuration database.
*
* @param {string} authCode The authCode obtained from microsoft.
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
*/
exports.addMicrosoftAccount = async function(authCode) {
const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL)
// Advance expiry by 10 seconds to avoid close calls.
const now = new Date().getTime()
const ret = ConfigManager.addMicrosoftAuthAccount(
fullAuth.mcProfile.id,
fullAuth.mcToken.access_token,
fullAuth.mcProfile.name,
calculateExpiryDate(now, fullAuth.mcToken.expires_in),
fullAuth.accessToken.access_token,
fullAuth.accessToken.refresh_token,
calculateExpiryDate(now, fullAuth.accessToken.expires_in)
)
ConfigManager.save()
return ret
}
exports.validateSelected = async function(){
const current = ConfigManager.getSelectedAccount()
const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken())
console.log(isValid)
if(!isValid){
/**
* 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 session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken())
console.log('ses', session)
ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
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){
if(err && err.message === 'ForbiddenOperationException'){
log.error('Error while removing account', err)
return Promise.reject(err)
}
}
/**
* Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout
* through the ipc renderer.
*
* @param {string} uuid The UUID of the account to be removed.
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
*/
exports.removeMicrosoftAccount = async function(uuid){
try {
ConfigManager.removeAuthAccount(uuid)
ConfigManager.save()
return Promise.resolve()
} catch (err){
log.error('Error while removing account', err)
return Promise.reject(err)
}
}
/**
* Validate the selected account with Mojang's authserver. If the account is not valid,
* we will attempt to refresh the access token and update that value. If that fails, a
* new login will be required.
*
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
* otherwise false.
*/
async function validateSelectedMojangAccount(){
const current = ConfigManager.getSelectedAccount()
const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken())
if(response.responseStatus === RestResponseStatus.SUCCESS) {
const isValid = response.data
if(!isValid){
const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken())
if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) {
const session = refreshResponse.data
ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken)
ConfigManager.save()
} else {
log.error('Error while validating selected profile:', refreshResponse.error)
log.info('Account access token is invalid.')
return false
}
log.info('Account access token validated.')
return true
} else {
log.info('Account access token validated.')
return true
}
}
}
/**
* Validate the selected account with Microsoft's authserver. If the account is not valid,
* we will attempt to refresh the access token and update that value. If that fails, a
* new login will be required.
*
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
* otherwise false.
*/
async function validateSelectedMicrosoftAccount(){
const current = ConfigManager.getSelectedAccount()
const now = new Date().getTime()
const mcExpiresAt = current.expiresAt
const mcExpired = now >= mcExpiresAt
if(!mcExpired) {
return true
}
// MC token expired. Check MS token.
const msExpiresAt = current.microsoft.expires_at
const msExpired = now >= msExpiresAt
if(msExpired) {
// MS expired, do full refresh.
try {
const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH)
ConfigManager.updateMicrosoftAuthAccount(
current.uuid,
res.mcToken.access_token,
res.accessToken.access_token,
res.accessToken.refresh_token,
calculateExpiryDate(now, res.accessToken.expires_in),
calculateExpiryDate(now, res.mcToken.expires_in)
)
ConfigManager.save()
return true
} catch(err) {
return false
}
} else {
// Only MC expired, use existing MS token.
try {
const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH)
ConfigManager.updateMicrosoftAuthAccount(
current.uuid,
res.mcToken.access_token,
current.microsoft.access_token,
current.microsoft.refresh_token,
current.microsoft.expires_at,
calculateExpiryDate(now, res.mcToken.expires_in)
)
ConfigManager.save()
return true
}
catch(err) {
return false
}
}
return true
}
/**
* 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 true
return await validateSelectedMojangAccount()
}
}

View File

@ -1,12 +1,72 @@
const fs = require('fs')
const mkpath = require('mkdirp')
const fs = require('fs-extra')
const { LoggerUtil } = require('helios-core')
const os = require('os')
const path = require('path')
const uuidV4 = require('uuid/v4')
function resolveMaxRAM(){
const logger = LoggerUtil.getLogger('ConfigManager')
const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME)
const dataPath = path.join(sysRoot, '.helioslauncher')
const launcherDir = require('@electron/remote').app.getPath('userData')
/**
* Retrieve the absolute path of the launcher directory.
*
* @returns {string} The absolute path of the launcher directory.
*/
exports.getLauncherDirectory = function(){
return launcherDir
}
/**
* Get the launcher's data directory. This is where all files related
* to game launch are installed (common, instances, java, etc).
*
* @returns {string} The absolute path of the launcher's data directory.
*/
exports.getDataDirectory = function(def = false){
return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory
}
/**
* Set the new data directory.
*
* @param {string} dataDirectory The new data directory.
*/
exports.setDataDirectory = function(dataDirectory){
config.settings.launcher.dataDirectory = dataDirectory
}
const configPath = path.join(exports.getLauncherDirectory(), 'config.json')
const configPathLEGACY = path.join(dataPath, 'config.json')
const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY)
exports.getAbsoluteMinRAM = function(ram){
if(ram?.minimum != null) {
return ram.minimum/1024
} else {
// Legacy behavior
const mem = os.totalmem()
return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G')
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')
}
}
/**
@ -17,35 +77,32 @@ function resolveMaxRAM(){
*/
const DEFAULT_CONFIG = {
settings: {
java: {
minRAM: '2G',
maxRAM: resolveMaxRAM(), // Dynamic
executable: 'C:\\Program Files\\Java\\jdk1.8.0_152\\bin\\javaw.exe', // TODO Resolve
jvmOptions: [
'-XX:+UseConcMarkSweepGC',
'-XX:+CMSIncrementalMode',
'-XX:-UseAdaptiveSizePolicy',
'-Xmn128M'
],
},
game: {
directory: path.join(__dirname, '..', '..', '..', 'target', 'test', 'mcfiles'),
resWidth: 1280,
resHeight: 720,
fullscreen: false,
autoConnect: true
autoConnect: true,
launchDetached: true
},
launcher: {
allowPrerelease: false,
dataDirectory: dataPath
}
},
clientToken: uuidV4().replace(/-/g, ''),
newsCache: {
date: null,
content: null,
dismissed: false
},
clientToken: null,
selectedServer: null, // Resolved
selectedAccount: null,
authenticationDatabase: {}
authenticationDatabase: {},
modConfigurations: [],
javaConfig: {}
}
let config = null;
let config = null
// Persistance Utility Functions
@ -53,8 +110,7 @@ let config = null;
* Save the current configuration to a file.
*/
exports.save = function(){
const filePath = path.join(config.settings.game.directory, 'config.json')
fs.writeFileSync(filePath, JSON.stringify(config, null, 4), 'UTF-8')
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8')
}
/**
@ -64,27 +120,146 @@ exports.save = function(){
* need to be externally assigned.
*/
exports.load = function(){
// Determine the effective configuration.
const EFFECTIVE_CONFIG = config == null ? DEFAULT_CONFIG : config
const filePath = path.join(EFFECTIVE_CONFIG.settings.game.directory, 'config.json')
let doLoad = true
if(!fs.existsSync(filePath)){
if(!fs.existsSync(configPath)){
// Create all parent directories.
mkpath.sync(path.join(filePath, '..'))
fs.ensureDirSync(path.join(configPath, '..'))
if(fs.existsSync(configPathLEGACY)){
fs.moveSync(configPathLEGACY, configPath)
} else {
doLoad = false
config = DEFAULT_CONFIG
exports.save()
} else {
config = JSON.parse(fs.readFileSync(filePath, 'UTF-8'))
}
}
if(doLoad){
let doValidate = false
try {
config = JSON.parse(fs.readFileSync(configPath, 'UTF-8'))
doValidate = true
} catch (err){
logger.error(err)
logger.info('Configuration file contains malformed JSON or is corrupt.')
logger.info('Generating a new configuration file.')
fs.ensureDirSync(path.join(configPath, '..'))
config = DEFAULT_CONFIG
exports.save()
}
if(doValidate){
config = validateKeySet(DEFAULT_CONFIG, config)
exports.save()
}
}
logger.info('Successfully Loaded')
}
/**
* @returns {boolean} Whether or not the manager has been loaded.
*/
exports.isLoaded = function(){
return config != null
}
/**
* Validate that the destination object has at least every field
* present in the source object. Assign a default value otherwise.
*
* @param {Object} srcObj The source object to reference against.
* @param {Object} destObj The destination object.
* @returns {Object} A validated destination object.
*/
function validateKeySet(srcObj, destObj){
if(srcObj == null){
srcObj = {}
}
const validationBlacklist = ['authenticationDatabase', 'javaConfig']
const keys = Object.keys(srcObj)
for(let i=0; i<keys.length; i++){
if(typeof destObj[keys[i]] === 'undefined'){
destObj[keys[i]] = srcObj[keys[i]]
} else if(typeof srcObj[keys[i]] === 'object' && srcObj[keys[i]] != null && !(srcObj[keys[i]] instanceof Array) && validationBlacklist.indexOf(keys[i]) === -1){
destObj[keys[i]] = validateKeySet(srcObj[keys[i]], destObj[keys[i]])
}
}
return destObj
}
/**
* Check to see if this is the first time the user has launched the
* application. This is determined by the existance of the data path.
*
* @returns {boolean} True if this is the first launch, otherwise false.
*/
exports.isFirstLaunch = function(){
return firstLaunch
}
/**
* Returns the name of the folder in the OS temp directory which we
* will use to extract and store native dependencies for game launch.
*
* @returns {string} The name of the folder.
*/
exports.getTempNativeFolder = function(){
return 'WCNatives'
}
// System Settings (Unconfigurable on UI)
/**
* Retrieve the news cache to determine
* whether or not there is newer news.
*
* @returns {Object} The news cache object.
*/
exports.getNewsCache = function(){
return config.newsCache
}
/**
* Set the new news cache object.
*
* @param {Object} newsCache The new news cache object.
*/
exports.setNewsCache = function(newsCache){
config.newsCache = newsCache
}
/**
* Set whether or not the news has been dismissed (checked)
*
* @param {boolean} dismissed Whether or not the news has been dismissed (checked).
*/
exports.setNewsCacheDismissed = function(dismissed){
config.newsCache.dismissed = dismissed
}
/**
* Retrieve the common directory for shared
* game files (assets, libraries, etc).
*
* @returns {string} The launcher's common directory.
*/
exports.getCommonDirectory = function(){
return path.join(exports.getDataDirectory(), 'common')
}
/**
* Retrieve the instance directory for the per
* server game directories.
*
* @returns {string} The launcher's instance directory.
*/
exports.getInstanceDirectory = function(){
return path.join(exports.getDataDirectory(), 'instances')
}
/**
* Retrieve the launcher's Client Token.
* There is no default client token.
*
* @returns {String} - the launcher's Client Token.
* @returns {string} The launcher's Client Token.
*/
exports.getClientToken = function(){
return config.clientToken
@ -93,7 +268,7 @@ exports.getClientToken = function(){
/**
* Set the launcher's Client Token.
*
* @param {String} clientToken - the launcher's new Client Token.
* @param {string} clientToken The launcher's new Client Token.
*/
exports.setClientToken = function(clientToken){
config.clientToken = clientToken
@ -102,8 +277,8 @@ exports.setClientToken = function(clientToken){
/**
* Retrieve the ID of the selected serverpack.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {String} - the ID of the selected serverpack.
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {string} The ID of the selected serverpack.
*/
exports.getSelectedServer = function(def = false){
return !def ? config.selectedServer : DEFAULT_CONFIG.clientToken
@ -112,7 +287,7 @@ exports.getSelectedServer = function(def = false){
/**
* Set the ID of the selected serverpack.
*
* @param {String} serverID - the ID of the new selected serverpack.
* @param {string} serverID The ID of the new selected serverpack.
*/
exports.setSelectedServer = function(serverID){
config.selectedServer = serverID
@ -121,7 +296,7 @@ exports.setSelectedServer = function(serverID){
/**
* Get an array of each account currently authenticated by the launcher.
*
* @returns {Array.<Object>} - an array of each stored authenticated account.
* @returns {Array.<Object>} An array of each stored authenticated account.
*/
exports.getAuthAccounts = function(){
return config.authenticationDatabase
@ -131,70 +306,267 @@ exports.getAuthAccounts = function(){
* Returns the authenticated account with the given uuid. Value may
* be null.
*
* @param {String} uuid - the uuid of the authenticated account.
* @returns {Object} - the authenticated account with the given uuid.
* @param {string} uuid The uuid of the authenticated account.
* @returns {Object} The authenticated account with the given uuid.
*/
exports.getAuthAccount = function(uuid){
return config.authenticationDatabase[uuid]
}
/**
* Update the access token of an authenticated account.
* Update the access token of an authenticated mojang account.
*
* @param {String} uuid - uuid of the authenticated account.
* @param {String} accessToken - the new Access Token.
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The new Access Token.
*
* @returns {Object} - the authenticated account object created by this action.
* @returns {Object} The authenticated account object created by this action.
*/
exports.updateAuthAccount = function(uuid, accessToken){
exports.updateMojangAuthAccount = function(uuid, accessToken){
config.authenticationDatabase[uuid].accessToken = accessToken
config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion.
return config.authenticationDatabase[uuid]
}
/**
* Adds an authenticated account to the database to be stored.
* Adds an authenticated mojang account to the database to be stored.
*
* @param {String} uuid - uuid of the authenticated account.
* @param {String} accessToken - accessToken of the authenticated account.
* @param {String} username - username (usually email) of the authenticated account.
* @param {String} displayName - in game name of the authenticated account.
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The accessToken of the authenticated account.
* @param {string} username The username (usually email) of the authenticated account.
* @param {string} displayName The in game name of the authenticated account.
*
* @returns {Object} - the authenticated account object created by this action.
* @returns {Object} The authenticated account object created by this action.
*/
exports.addAuthAccount = function(uuid, accessToken, username, displayName){
exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
config.selectedAccount = uuid
config.authenticationDatabase[uuid] = {
type: 'mojang',
accessToken,
username,
uuid,
displayName
username: username.trim(),
uuid: uuid.trim(),
displayName: displayName.trim()
}
return config.authenticationDatabase[uuid]
}
/**
* Update the tokens of an authenticated microsoft account.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The new Access Token.
* @param {string} msAccessToken The new Microsoft Access Token
* @param {string} msRefreshToken The new Microsoft Refresh Token
* @param {date} msExpires The date when the microsoft access token expires
* @param {date} mcExpires The date when the mojang access token expires
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) {
config.authenticationDatabase[uuid].accessToken = accessToken
config.authenticationDatabase[uuid].expiresAt = mcExpires
config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken
config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken
config.authenticationDatabase[uuid].microsoft.expires_at = msExpires
return config.authenticationDatabase[uuid]
}
/**
* Adds an authenticated microsoft account to the database to be stored.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The accessToken of the authenticated account.
* @param {string} name The in game name of the authenticated account.
* @param {date} mcExpires The date when the mojang access token expires
* @param {string} msAccessToken The microsoft access token
* @param {string} msRefreshToken The microsoft refresh token
* @param {date} msExpires The date when the microsoft access token expires
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) {
config.selectedAccount = uuid
config.authenticationDatabase[uuid] = {
type: 'microsoft',
accessToken,
username: name.trim(),
uuid: uuid.trim(),
displayName: name.trim(),
expiresAt: mcExpires,
microsoft: {
access_token: msAccessToken,
refresh_token: msRefreshToken,
expires_at: msExpires
}
}
return config.authenticationDatabase[uuid]
}
/**
* Remove an authenticated account from the database. If the account
* was also the selected account, a new one will be selected. If there
* are no accounts, the selected account will be null.
*
* @param {string} uuid The uuid of the authenticated account.
*
* @returns {boolean} True if the account was removed, false if it never existed.
*/
exports.removeAuthAccount = function(uuid){
if(config.authenticationDatabase[uuid] != null){
delete config.authenticationDatabase[uuid]
if(config.selectedAccount === uuid){
const keys = Object.keys(config.authenticationDatabase)
if(keys.length > 0){
config.selectedAccount = keys[0]
} else {
config.selectedAccount = null
config.clientToken = null
}
}
return true
}
return false
}
/**
* Get the currently selected authenticated account.
*
* @returns {Object} - the selected authenticated account.
* @returns {Object} The selected authenticated account.
*/
exports.getSelectedAccount = function(){
return config.authenticationDatabase[config.selectedAccount]
}
/**
* Set the selected authenticated account.
*
* @param {string} uuid The UUID of the account which is to be set
* as the selected account.
*
* @returns {Object} The selected authenticated account.
*/
exports.setSelectedAccount = function(uuid){
const authAcc = config.authenticationDatabase[uuid]
if(authAcc != null) {
config.selectedAccount = uuid
}
return authAcc
}
/**
* Get an array of each mod configuration currently stored.
*
* @returns {Array.<Object>} An array of each stored mod configuration.
*/
exports.getModConfigurations = function(){
return config.modConfigurations
}
/**
* Set the array of stored mod configurations.
*
* @param {Array.<Object>} configurations An array of mod configurations.
*/
exports.setModConfigurations = function(configurations){
config.modConfigurations = configurations
}
/**
* Get the mod configuration for a specific server.
*
* @param {string} serverid The id of the server.
* @returns {Object} The mod configuration for the given server.
*/
exports.getModConfiguration = function(serverid){
const cfgs = config.modConfigurations
for(let i=0; i<cfgs.length; i++){
if(cfgs[i].id === serverid){
return cfgs[i]
}
}
return null
}
/**
* Set the mod configuration for a specific server. This overrides any existing value.
*
* @param {string} serverid The id of the server for the given mod configuration.
* @param {Object} configuration The mod configuration for the given server.
*/
exports.setModConfiguration = function(serverid, configuration){
const cfgs = config.modConfigurations
for(let i=0; i<cfgs.length; i++){
if(cfgs[i].id === serverid){
cfgs[i] = configuration
return
}
}
cfgs.push(configuration)
}
// User Configurable Settings
// Java Settings
function defaultJavaConfig(effectiveJavaOptions, ram) {
if(effectiveJavaOptions.suggestedMajor > 8) {
return defaultJavaConfig17(ram)
} else {
return defaultJavaConfig8(ram)
}
}
function defaultJavaConfig8(ram) {
return {
minRAM: resolveSelectedRAM(ram),
maxRAM: resolveSelectedRAM(ram),
executable: null,
jvmOptions: [
'-XX:+UseConcMarkSweepGC',
'-XX:+CMSIncrementalMode',
'-XX:-UseAdaptiveSizePolicy',
'-Xmn128M'
],
}
}
function defaultJavaConfig17(ram) {
return {
minRAM: resolveSelectedRAM(ram),
maxRAM: resolveSelectedRAM(ram),
executable: null,
jvmOptions: [
'-XX:+UnlockExperimentalVMOptions',
'-XX:+UseG1GC',
'-XX:G1NewSizePercent=20',
'-XX:G1ReservePercent=20',
'-XX:MaxGCPauseMillis=50',
'-XX:G1HeapRegionSize=32M'
],
}
}
/**
* Ensure a java config property is set for the given server.
*
* @param {string} serverid The server id.
* @param {*} mcVersion The minecraft version of the server.
*/
exports.ensureJavaConfig = function(serverid, effectiveJavaOptions, ram) {
if(!Object.prototype.hasOwnProperty.call(config.javaConfig, serverid)) {
config.javaConfig[serverid] = defaultJavaConfig(effectiveJavaOptions, ram)
}
}
/**
* Retrieve the minimum amount of memory for JVM initialization. This value
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {String} - the minimum amount of memory for JVM initialization.
* @param {string} serverid The server id.
* @returns {string} The minimum amount of memory for JVM initialization.
*/
exports.getMinRAM = function(def = false){
return !def ? config.settings.java.minRAM : DEFAULT_CONFIG.settings.java.minRAM
exports.getMinRAM = function(serverid){
return config.javaConfig[serverid].minRAM
}
/**
@ -202,10 +574,11 @@ exports.getMinRAM = function(def = false){
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {String} minRAM - the new minimum amount of memory for JVM initialization.
* @param {string} serverid The server id.
* @param {string} minRAM The new minimum amount of memory for JVM initialization.
*/
exports.setMinRAM = function(minRAM){
config.settings.java.minRAM = minRAM
exports.setMinRAM = function(serverid, minRAM){
config.javaConfig[serverid].minRAM = minRAM
}
/**
@ -213,11 +586,11 @@ exports.setMinRAM = function(minRAM){
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {String} - the maximum amount of memory for JVM initialization.
* @param {string} serverid The server id.
* @returns {string} The maximum amount of memory for JVM initialization.
*/
exports.getMaxRAM = function(def = false){
return !def ? config.settings.java.maxRAM : resolveMaxRAM()
exports.getMaxRAM = function(serverid){
return config.javaConfig[serverid].maxRAM
}
/**
@ -225,10 +598,11 @@ exports.getMaxRAM = function(def = false){
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {String} maxRAM - the new maximum amount of memory for JVM initialization.
* @param {string} serverid The server id.
* @param {string} maxRAM The new maximum amount of memory for JVM initialization.
*/
exports.setMaxRAM = function(maxRAM){
config.settings.java.maxRAM = maxRAM
exports.setMaxRAM = function(serverid, maxRAM){
config.javaConfig[serverid].maxRAM = maxRAM
}
/**
@ -236,19 +610,21 @@ exports.setMaxRAM = function(maxRAM){
*
* This is a resolved configuration value and defaults to null until externally assigned.
*
* @returns {String} - the path of the Java Executable.
* @param {string} serverid The server id.
* @returns {string} The path of the Java Executable.
*/
exports.getJavaExecutable = function(){
return config.settings.java.executable
exports.getJavaExecutable = function(serverid){
return config.javaConfig[serverid].executable
}
/**
* Set the path of the Java Executable.
*
* @param {String} executable - the new path of the Java Executable.
* @param {string} serverid The server id.
* @param {string} executable The new path of the Java Executable.
*/
exports.setJavaExecutable = function(executable){
config.settings.java.executable = executable
exports.setJavaExecutable = function(serverid, executable){
config.javaConfig[serverid].executable = executable
}
/**
@ -256,11 +632,11 @@ exports.setJavaExecutable = function(executable){
* such as memory allocation, will be dynamically resolved and will not be included
* in this value.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {Array.<String>} - an array of the additional arguments for JVM initialization.
* @param {string} serverid The server id.
* @returns {Array.<string>} An array of the additional arguments for JVM initialization.
*/
exports.getJVMOptions = function(def = false){
return !def ? config.settings.java.jvmOptions : DEFAULT_CONFIG.settings.java.jvmOptions
exports.getJVMOptions = function(serverid){
return config.javaConfig[serverid].jvmOptions
}
/**
@ -268,39 +644,21 @@ exports.getJVMOptions = function(def = false){
* such as memory allocation, will be dynamically resolved and should not be
* included in this value.
*
* @param {Array.<String>} jvmOptions - an array of the new additional arguments for JVM
* @param {string} serverid The server id.
* @param {Array.<string>} jvmOptions An array of the new additional arguments for JVM
* initialization.
*/
exports.setJVMOptions = function(jvmOptions){
config.settings.java.jvmOptions = jvmOptions
exports.setJVMOptions = function(serverid, jvmOptions){
config.javaConfig[serverid].jvmOptions = jvmOptions
}
// Game Settings
/**
* Retrieve the absolute path of the game directory.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {String} - the absolute path of the game directory.
*/
exports.getGameDirectory = function(def = false){
return !def ? config.settings.game.directory : DEFAULT_CONFIG.settings.game.directory
}
/**
* Set the absolute path of the game directory.
*
* @param {String} directory - the absolute path of the new game directory.
*/
exports.setGameDirectory = function(directory){
config.settings.game.directory = directory
}
/**
* Retrieve the width of the game window.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {Number} - the width of the game window.
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {number} The width of the game window.
*/
exports.getGameWidth = function(def = false){
return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth
@ -309,17 +667,28 @@ exports.getGameWidth = function(def = false){
/**
* Set the width of the game window.
*
* @param {Number} resWidth - the new width of the game window.
* @param {number} resWidth The new width of the game window.
*/
exports.setGameWidth = function(resWidth){
config.settings.game.resWidth = resWidth
config.settings.game.resWidth = Number.parseInt(resWidth)
}
/**
* Validate a potential new width value.
*
* @param {number} resWidth The width value to validate.
* @returns {boolean} Whether or not the value is valid.
*/
exports.validateGameWidth = function(resWidth){
const nVal = Number.parseInt(resWidth)
return Number.isInteger(nVal) && nVal >= 0
}
/**
* Retrieve the height of the game window.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {Number} - the height of the game window.
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {number} The height of the game window.
*/
exports.getGameHeight = function(def = false){
return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight
@ -328,26 +697,37 @@ exports.getGameHeight = function(def = false){
/**
* Set the height of the game window.
*
* @param {Number} resHeight - the new height of the game window.
* @param {number} resHeight The new height of the game window.
*/
exports.setGameHeight = function(resHeight){
config.settings.game.resHeight = resHeight
config.settings.game.resHeight = Number.parseInt(resHeight)
}
/**
* Validate a potential new height value.
*
* @param {number} resHeight The height value to validate.
* @returns {boolean} Whether or not the value is valid.
*/
exports.validateGameHeight = function(resHeight){
const nVal = Number.parseInt(resHeight)
return Number.isInteger(nVal) && nVal >= 0
}
/**
* Check if the game should be launched in fullscreen mode.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {Boolean} - whether or not the game is set to launch in fullscreen mode.
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the game is set to launch in fullscreen mode.
*/
exports.isFullscreen = function(def = false){
exports.getFullscreen = function(def = false){
return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen
}
/**
* Change the status of if the game should be launched in fullscreen mode.
*
* @param {Boolean} fullscreen - whether or not the game should launch in fullscreen mode.
* @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode.
*/
exports.setFullscreen = function(fullscreen){
config.settings.game.fullscreen = fullscreen
@ -356,18 +736,58 @@ exports.setFullscreen = function(fullscreen){
/**
* Check if the game should auto connect to servers.
*
* @param {Boolean} def - optional. If true, the default value will be returned.
* @returns {Boolean} - whether or not the game should auto connect to servers.
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the game should auto connect to servers.
*/
exports.isAutoConnect = function(def = false){
exports.getAutoConnect = function(def = false){
return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect
}
/**
* Change the status of whether or not the game should auto connect to servers.
*
* @param {Boolean} autoConnect - whether or not the game should auto connect to servers.
* @param {boolean} autoConnect Whether or not the game should auto connect to servers.
*/
exports.setAutoConnect = function(autoConnect){
config.settings.game.autoConnect = autoConnect
}
/**
* Check if the game should launch as a detached process.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the game will launch as a detached process.
*/
exports.getLaunchDetached = function(def = false){
return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached
}
/**
* Change the status of whether or not the game should launch as a detached process.
*
* @param {boolean} launchDetached Whether or not the game should launch as a detached process.
*/
exports.setLaunchDetached = function(launchDetached){
config.settings.game.launchDetached = launchDetached
}
// Launcher Settings
/**
* Check if the launcher should download prerelease versions.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the launcher should download prerelease versions.
*/
exports.getAllowPrerelease = function(def = false){
return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease
}
/**
* Change the status of Whether or not the launcher should download prerelease versions.
*
* @param {boolean} launchDetached Whether or not the launcher should download prerelease versions.
*/
exports.setAllowPrerelease = function(allowPrerelease){
config.settings.launcher.allowPrerelease = allowPrerelease
}

View File

@ -1,49 +1,52 @@
// Work in progress
const {Client} = require('discord-rpc')
const ConfigManager = require('./configmanager.js')
const { LoggerUtil } = require('helios-core')
let rpc
const logger = LoggerUtil.getLogger('DiscordWrapper')
const { Client } = require('discord-rpc-patch')
const Lang = require('./langloader')
let client
let activity
exports.initRPC = function(genSettings, servSettings, initialDetails = 'Waiting for Client..'){
rpc = new Client({ transport: 'ipc' })
exports.initRPC = function(genSettings, servSettings, initialDetails = Lang.queryJS('discord.waiting')){
client = new Client({ transport: 'ipc' })
rpc.on('ready', () => {
activity = {
details: initialDetails,
state: 'Server: ' + servSettings.shortId,
state: Lang.queryJS('discord.state', {shortId: servSettings.shortId}),
largeImageKey: servSettings.largeImageKey,
largeImageText: servSettings.largeImageText,
smallImageKey: genSettings.smallImageKey,
smallImageText: genSettings.smallImageText,
startTimestamp: new Date().getTime() / 1000,
startTimestamp: new Date().getTime(),
instance: false
}
rpc.setActivity(activity)
client.on('ready', () => {
logger.info('Discord RPC Connected')
client.setActivity(activity)
})
rpc.login(genSettings.clientID).catch(error => {
client.login({clientId: genSettings.clientId}).catch(error => {
if(error.message.includes('ENOENT')) {
console.log('Unable to initialize Discord Rich Presence, no client detected.')
logger.info('Unable to initialize Discord Rich Presence, no client detected.')
} else {
console.log('Unable to initialize Discord Rich Presence: ' + error.message, error)
logger.info('Unable to initialize Discord Rich Presence: ' + error.message, error)
}
})
}
exports.updateDetails = function(details){
if(activity == null){
console.error('Discord RPC is not initialized and therefore cannot be updated.')
}
activity.details = details
rpc.setActivity(activity)
client.setActivity(activity)
}
exports.shutdownRPC = function(){
if(!rpc) return
rpc.setActivity({})
rpc.destroy()
rpc = null
if(!client) return
client.clearActivity()
client.destroy()
client = null
activity = null
}

View File

@ -0,0 +1,17 @@
const { DistributionAPI } = require('helios-core/common')
const ConfigManager = require('./configmanager')
// Old WesterosCraft url.
// exports.REMOTE_DISTRO_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
exports.REMOTE_DISTRO_URL = 'https://helios-files.geekcorner.eu.org/distribution.json'
const api = new DistributionAPI(
ConfigManager.getLauncherDirectory(),
null, // Injected forcefully by the preloader.
null, // Injected forcefully by the preloader.
exports.REMOTE_DISTRO_URL,
false
)
exports.DistroAPI = api

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

View File

@ -0,0 +1,28 @@
// NOTE FOR THIRD-PARTY
// REPLACE THIS CLIENT ID WITH YOUR APPLICATION ID.
// SEE https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md
exports.AZURE_CLIENT_ID = '1ce6e35a-126f-48fd-97fb-54d143ac6d45'
// SEE NOTE ABOVE.
// Opcodes
exports.MSFT_OPCODE = {
OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN',
OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT',
REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN',
REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT'
}
// Reply types for REPLY opcode.
exports.MSFT_REPLY_TYPE = {
SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS',
ERROR: 'MSFT_AUTH_REPLY_ERROR'
}
// Error types for ERROR reply.
exports.MSFT_ERROR = {
ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN',
NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED'
}
exports.SHELL_OPCODE = {
TRASH_ITEM: 'TRASH_ITEM'
}

5
app/assets/js/isdev.js Normal file
View 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))

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

View File

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

View File

@ -1,222 +0,0 @@
/**
* File is officially deprecated in favor of processbuilder.js,
* will be removed once the new module is 100% complete and this
* is no longer needed for reference.
*/
const mojang = require('mojang')
const uuidV4 = require('uuid/v4')
const path = require('path')
const child_process = require('child_process')
const ag = require('./assetguard.js')
const AdmZip = require('adm-zip')
const fs = require('fs')
const mkpath = require('mkdirp');
function launchMinecraft(versionData, forgeData, basePath){
const authPromise = mojang.auth('email', 'pass', uuidV4(), {
name: 'Minecraft',
version: 1
})
authPromise.then(function(data){
console.log(data)
const args = finalizeArgumentsForge(versionData, forgeData, data, basePath)
//BRUTEFORCE for testing
//args.push('-mods modstore\\chatbubbles\\chatbubbles\\1.0.1_for_1.11.2\\mod_chatBubbles-1.0.1_for_1.11.2.litemod,modstore\\com\\westeroscraft\\westerosblocks\\3.0.0-beta-71\\westerosblocks-3.0.0-beta-71.jar,modstore\\mezz\\jei\\1.11.2-4.3.5.277\\jei-1.11.2-4.3.5.277.jar,modstore\\net\\optifine\\optifine\\1.11.2_HD_U_B9\\optifine-1.11.2_HD_U_B9.jar')
//args.push('--modListFile absolute:C:\\Users\\Asus\\Desktop\\LauncherElectron\\app\\assets\\WesterosCraft-1.11.2.json')
//TODO make this dynamic
const child = child_process.spawn('C:\\Program Files\\Java\\jdk1.8.0_152\\bin\\javaw.exe', args)
child.stdout.on('data', (data) => {
console.log('Minecraft:', data.toString('utf8'))
})
child.stderr.on('data', (data) => {
console.log('Minecraft:', data.toString('utf8'))
})
child.on('close', (code, signal) => {
console.log('Exited with code', code)
})
})
}
function finalizeArgumentsForge(versionData, forgeData, authData, basePath){
const mcArgs = forgeData['minecraftArguments']
const gameProfile = authData['selectedProfile']
const regex = new RegExp('\\${*(.*)}')
const argArr = mcArgs.split(' ')
const staticArgs = ['-Xmx4G',
'-XX:+UseConcMarkSweepGC',
'-XX:+CMSIncrementalMode',
'-XX:-UseAdaptiveSizePolicy',
'-Xmn128M',
'-Djava.library.path=' + path.join(basePath, 'natives'),
'-cp',
classpathArg(versionData, basePath).concat(forgeClasspathArg(forgeData, basePath)).join(';'),
forgeData.mainClass]
for(let i=0; i<argArr.length; i++){
if(regex.test(argArr[i])){
const identifier = argArr[i].match(regex)[1]
let newVal = argArr[i]
switch(identifier){
case 'auth_player_name':
newVal = gameProfile['name']
break
case 'version_name':
//newVal = versionData['id']
newVal = 'WesterosCraft-1.11.2'
break
case 'game_directory':
newVal = basePath
break
case 'assets_root':
newVal = path.join(basePath, 'assets')
break
case 'assets_index_name':
newVal = versionData['assets']
break
case 'auth_uuid':
newVal = gameProfile['id']
break
case 'auth_access_token':
newVal = authData['accessToken']
break
case 'user_type':
newVal = 'MOJANG'
break
case 'version_type':
newVal = versionData['type']
break
}
argArr[i] = newVal
}
}
return staticArgs.concat(argArr)
}
function finalizeArguments(versionData, authData, basePath){
const mcArgs = versionData['minecraftArguments']
const gameProfile = authData['selectedProfile']
const regex = new RegExp('\\${*(.*)}')
const argArr = mcArgs.split(' ')
const staticArgs = ['-Xmx1G',
'-XX:+UseConcMarkSweepGC',
'-XX:+CMSIncrementalMode',
'-XX:-UseAdaptiveSizePolicy',
'-Xmn128M',
'-Djava.library.path=' + path.join(basePath, 'natives'),
'-cp',
classpathArg(versionData, basePath).join(';'),
versionData.mainClass]
for(let i=0; i<argArr.length; i++){
if(regex.test(argArr[i])){
const identifier = argArr[i].match(regex)[1]
let newVal = argArr[i]
switch(identifier){
case 'auth_player_name':
newVal = gameProfile['name']
break
case 'version_name':
newVal = versionData['id']
break
case 'game_directory':
newVal = basePath
break
case 'assets_root':
newVal = path.join(basePath, 'assets')
break
case 'assets_index_name':
newVal = versionData['assets']
break
case 'auth_uuid':
newVal = gameProfile['id']
break
case 'auth_access_token':
newVal = authData['accessToken']
break
case 'user_type':
newVal = 'MOJANG'
break
case 'version_type':
newVal = versionData['type']
break
}
argArr[i] = newVal
}
}
return staticArgs.concat(argArr)
}
function forgeClasspathArg(forgeData, basePath){
const libArr = forgeData['libraries']
const libPath = path.join(basePath, 'libraries')
const cpArgs = []
for(let i=0; i<libArr.length; i++){
const lib = libArr[i]
const to = path.join(libPath, ag._resolvePath(lib.name, '.jar'))
cpArgs.push(to)
}
return cpArgs
}
function classpathArg(versionData, basePath){
const libArr = versionData['libraries']
const libPath = path.join(basePath, 'libraries')
const nativePath = path.join(basePath, 'natives')
const version = versionData['id']
const cpArgs = [path.join(basePath, 'versions', version, version + '.jar')]
libArr.forEach(function(lib){
if(ag.Library.validateRules(lib['rules'])){
if(lib['natives'] == null){
const dlInfo = lib['downloads']
const artifact = dlInfo['artifact']
const to = path.join(libPath, artifact['path'])
cpArgs.push(to)
} else {
//Now we need to extract natives.
const natives = lib['natives']
const extractInst = lib['extract']
const exclusionArr = extractInst['exclude']
const opSys = ag.Library.mojangFriendlyOS()
const indexId = natives[opSys]
const dlInfo = lib['downloads']
const classifiers = dlInfo['classifiers']
const artifact = classifiers[indexId]
const to = path.join(libPath, artifact['path'])
let zip = new AdmZip(to)
let zipEntries = zip.getEntries()
for(let i=0; i<zipEntries.length; i++){
const fileName = zipEntries[i].entryName
let shouldExclude = false
exclusionArr.forEach(function(exclusion){
if(exclusion.indexOf(fileName) > -1){
shouldExclude = true
}
})
if(!shouldExclude){
mkpath.sync(path.join(nativePath, fileName, '..'))
fs.writeFile(path.join(nativePath, fileName), zipEntries[i].getData())
}
}
cpArgs.push(to)
}
}
})
//BRUTEFORCE LIB INJECTION
//FOR TESTING ONLY
cpArgs.push(path.join(libPath, 'com', 'mumfrey', 'liteloader', '1.11.2-SNAPSHOT', 'liteloader-1.11.2-SNAPSHOT.jar'))
return cpArgs
}
module.exports = {
launchMinecraft
}

View File

@ -1,220 +0,0 @@
const request = require('request')
const minecraftAgent = {
name: 'Minecraft',
version: 1
}
const authpath = 'https://authserver.mojang.com'
const statuses = [
{
service: 'minecraft.net',
status: 'grey',
name: 'Minecraft.net'
},
{
service: 'api.mojang.com',
status: 'grey',
name: 'Public API'
},
{
service: 'textures.minecraft.net',
status: 'grey',
name: 'Minecraft Skins'
},
{
service: 'authserver.mojang.com',
status: 'grey',
name: 'Authentication Service'
},
{
service: 'sessionserver.mojang.com',
status: 'grey',
name: 'Multiplayer Session Service'
},
{
service: 'account.mojang.com',
status: 'grey',
name: 'Mojang accounts website'
}
]
/**
* Converts a Mojang status color to a hex value. Valid statuses
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
* to our project which represends an unknown status.
*
* @param {String} status - a valid status code.
* @returns {String} - the hex color of the status code.
*/
exports.statusToHex = function(status){
switch(status.toLowerCase()){
case 'green':
return '#a5c325'
case 'yellow':
return '#eac918'
case 'red':
return '#c32625'
case 'grey':
default:
return '#848484'
}
}
/**
* Retrieves the status of Mojang's services.
* The response is condensed into a single object. Each service is
* a key, where the value is an object containing a status and name
* property.
*
* @see http://wiki.vg/Mojang_API#API_Status
*/
exports.status = function(){
return new Promise(function(fulfill, reject) {
request.get('https://status.mojang.com/check',
{
json: true
},
function(error, response, body){
if(response.statusCode === 200){
for(let i=0; i<body.length; i++){
const key = Object.keys(body[i])[0]
inner:
for(let j=0; j<statuses.length; j++){
if(statuses[j].service === key) {
statuses[j].status = body[i][key]
break inner
}
}
}
fulfill(statuses)
} else {
reject()
}
})
})
}
/**
* Authenticate a user with their Mojang credentials.
*
* @param {String} username - user's username, this is often an email.
* @param {String} password - user's password.
* @param {String} clientToken - launcher's Client Token.
* @param {Boolean} requestUser - optional. Adds user object to the reponse.
* @param {Object} agent - optional. Provided by default. Adds user info to the response.
*
* @see http://wiki.vg/Authentication#Authenticate
*/
exports.authenticate = function(username, password, clientToken, requestUser = true, agent = minecraftAgent){
return new Promise(function(fulfill, reject){
request.post(authpath + '/authenticate',
{
json: true,
body: {
agent,
username,
password,
clientToken,
requestUser
}
},
function(error, response, body){
if(response.statusCode === 200){
fulfill(body)
} else {
reject(body)
}
})
})
}
/**
* Validate an access token. This should always be done before launching.
* The client token should match the one used to create the access token.
*
* @param {String} accessToken - the access token to validate.
* @param {String} clientToken - the launcher's client token.
*
* @see http://wiki.vg/Authentication#Validate
*/
exports.validate = function(accessToken, clientToken){
return new Promise(function(fulfill, reject){
request.post(authpath + '/validate',
{
json: true,
body: {
accessToken,
clientToken
}
},
function(error, response, body){
if(response.statusCode === 403){
fulfill(false)
} else {
// 204 if valid
fulfill(true)
}
})
})
}
/**
* Invalidates an access token. The clientToken must match the
* token used to create the provided accessToken.
*
* @param {String} accessToken - the access token to invalidate.
* @param {String} clientToken - the launcher's client token.
*
* @see http://wiki.vg/Authentication#Invalidate
*/
exports.invalidate = function(accessToken, clientToken){
return new Promise(function(fulfill, reject){
request.post(authpath + '/invalidate',
{
json: true,
body: {
accessToken,
clientToken
}
},
function(error, response, body){
if(response.statusCode === 200){
fulfill()
} else {
reject()
}
})
})
}
/**
* Refresh a user's authentication. This should be used to keep a user logged
* in without asking them for their credentials again. A new access token will
* be generated using a recent invalid access token. See Wiki for more info.
*
* @param {String} accessToken - the old access token.
* @param {String} clientToken - the launcher's client token.
* @param {Boolean} requestUser - optional. Adds user object to the reponse.
*
* @see http://wiki.vg/Authentication#Refresh
*/
exports.refresh = function(accessToken, clientToken, requestUser = true){
return new Promise(function(fulfill, reject){
request.post(authpath + '/refresh',
{
json: true,
body: {
accessToken,
clientToken,
requestUser
}
},
function(error, response, body){
if(response.statusCode === 200){
fulfill(body)
} else {
reject(response.body)
}
})
})
}

View File

@ -1,18 +1,67 @@
const {AssetGuard} = require('./assetguard.js')
const ConfigManager = require('./configmanager.js')
const {ipcRenderer} = require('electron')
const fs = require('fs-extra')
const os = require('os')
const path = require('path')
console.log('Preloading')
const ConfigManager = require('./configmanager')
const { DistroAPI } = require('./distromanager')
const LangLoader = require('./langloader')
const { LoggerUtil } = require('helios-core')
// eslint-disable-next-line no-unused-vars
const { HeliosDistribution } = require('helios-core/common')
const logger = LoggerUtil.getLogger('Preloader')
logger.info('Loading..')
// Load ConfigManager
ConfigManager.load()
// Ensure Distribution is downloaded and cached.
AssetGuard.retrieveDistributionDataSync(ConfigManager.getGameDirectory(), false)
// Yuck!
// TODO Fix this
DistroAPI['commonDir'] = ConfigManager.getCommonDirectory()
DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory()
// 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){
console.log('Determining default selected server..')
ConfigManager.setSelectedServer(AssetGuard.resolveSelectedServer(ConfigManager.getGameDirectory()))
if(ConfigManager.getSelectedServer() == null || data.getServerById(ConfigManager.getSelectedServer()) == null){
logger.info('Determining default selected server..')
ConfigManager.setSelectedServer(data.getMainServer().rawServer.id)
ConfigManager.save()
}
}
ipcRenderer.send('distributionIndexDone', data != null)
}
// Ensure Distribution is downloaded and cached.
DistroAPI.getDistribution()
.then(heliosDistro => {
logger.info('Loaded distribution index.')
onDistroLoad(heliosDistro)
})
.catch(err => {
logger.info('Failed to load an older version of the distribution index.')
logger.info('Application cannot run.')
logger.error(err)
onDistroLoad(null)
})
// Clean up temp dir incase previous launches ended unexpectedly.
fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => {
if(err){
logger.warn('Error while cleaning natives directory', err)
} else {
logger.info('Cleaned natives directory.')
}
})

View File

@ -1,218 +1,701 @@
/**
* The initial iteration of this file will not support optional submodules.
* Support will be added down the line, only top-level modules will recieve optional support.
*
*
* TODO why are logs not working??????
*/
const AdmZip = require('adm-zip')
const {AssetGuard, Library} = require('./assetguard.js')
const child_process = require('child_process')
const ConfigManager = require('./configmanager.js')
const fs = require('fs')
const mkpath = require('mkdirp')
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 {URL} = require('url')
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(gameDirectory, distroServer, versionData, forgeData, authUser){
this.dir = gameDirectory
constructor(distroServer, vanillaManifest, modManifest, authUser, launcherVersion){
this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.rawServer.id)
this.commonDir = ConfigManager.getCommonDirectory()
this.server = distroServer
this.versionData = versionData
this.forgeData = forgeData
this.vanillaManifest = vanillaManifest
this.modManifest = modManifest
this.authUser = authUser
this.fmlDir = path.join(this.dir, 'versions', this.server.id + '.json')
this.libPath = path.join(this.dir, 'libraries')
}
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')
static shouldInclude(mdle){
//If the module should be included by default
return mdle.required == null || mdle.required.value == null || mdle.required.value === true || (mdle.required.value === false && (mdle.required.def == null || mdle.required.def === true))
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
const mods = this.resolveDefaultMods()
this.constructFMLModList(mods, true)
const args = this.constructJVMArguments(mods)
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)
console.log(args)
// 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 child = child_process.spawn(ConfigManager.getJavaExecutable(), args)
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) => {
console.log('Minecraft:', data.toString('utf8'))
data.trim().split('\n').forEach(x => console.log(`\x1b[32m[Minecraft]\x1b[0m ${x}`))
})
child.stderr.on('data', (data) => {
console.log('Minecraft:', data.toString('utf8'))
data.trim().split('\n').forEach(x => console.log(`\x1b[31m[Minecraft]\x1b[0m ${x}`))
})
child.on('close', (code, signal) => {
console.log('Exited with code', code)
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
}
resolveDefaultMods(options = {type: 'forgemod'}){
//Returns array of default forge mods to load.
const mods = []
const mdles = this.server.modules
/**
* 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' ? ';' : ':'
}
for(let i=0; i<mdles.length; ++i){
if(mdles[i].type != null && mdles[i].type === options.type){
if(ProcessBuilder.shouldInclude(mdles[i])){
mods.push(mdles[i])
/**
* 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()
}
}
}
}
}
return mods
/**
* 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')
}
constructFMLModList(mods, save = false){
const modList = {}
modList.repositoryRoot = path.join(this.dir, 'modstore')
const ids = []
for(let i=0; i<mods.length; ++i){
ids.push(mods[i].id)
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(this.fmlDir, json, 'UTF-8')
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.
* @returns {Array.<String>} - An array containing the full JVM arguments for this 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){
constructJVMArguments(mods, tempNativePath){
if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
return this._constructJVMArguments113(mods, tempNativePath)
} else {
return this._constructJVMArguments112(mods, tempNativePath)
}
}
let args = ['-Xmx' + ConfigManager.getMaxRAM(),
'-Xms' + ConfigManager.getMinRAM(),,
'-Djava.library.path=' + path.join(this.dir, 'natives'),
'-cp',
this.classpathArg(mods).join(';'),
this.forgeData.mainClass]
/**
* 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){
// For some reason this will add an undefined value unless
// the delete count is 1. I suspect this is unintended behavior
// by the function.. need to keep an eye on this.
args.splice(2, 1, ...ConfigManager.getJVMOptions())
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=HeliosLauncher')
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=HeliosLauncher')
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.
* @returns {Array.<string>} An array containing the arguments required by forge.
*/
_resolveForgeArgs(){
const mcArgs = this.forgeData.minecraftArguments.split(' ')
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;
let val = null
switch(identifier){
case 'auth_player_name':
val = this.authUser.displayName
val = this.authUser.displayName.trim()
break
case 'version_name':
//val = versionData.id
val = this.server.id
//val = vanillaManifest.id
val = this.server.rawServer.id
break
case 'game_directory':
val = this.dir
val = this.gameDir
break
case 'assets_root':
val = path.join(this.dir, 'assets')
val = path.join(this.commonDir, 'assets')
break
case 'assets_index_name':
val = this.versionData.assets
val = this.vanillaManifest.assets
break
case 'auth_uuid':
val = this.authUser.uuid
val = this.authUser.uuid.trim()
break
case 'auth_access_token':
val = this.authUser.accessToken
break
case 'user_type':
val = 'MOJANG'
val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
break
case 'user_properties': // 1.8.9 and below.
val = '{}'
break
case 'version_type':
val = this.versionData.type
val = this.vanillaManifest.type
break
}
if(val != null){
mcArgs[i] = val;
mcArgs[i] = val
}
}
}
mcArgs.push('--modListFile')
mcArgs.push('absolute:' + this.fmlDir)
// Autoconnect to the selected server.
this._processAutoConnectArg(mcArgs)
// Prepare game resolution
if(ConfigManager.isFullscreen()){
mcArgs.unshift('--fullscreen')
if(ConfigManager.getFullscreen()){
mcArgs.push('--fullscreen')
mcArgs.push(true)
} else {
mcArgs.unshift(ConfigManager.getGameWidth())
mcArgs.unshift('--width')
mcArgs.unshift(ConfigManager.getGameHeight())
mcArgs.unshift('--height')
mcArgs.push('--width')
mcArgs.push(ConfigManager.getGameWidth())
mcArgs.push('--height')
mcArgs.push(ConfigManager.getGameHeight())
}
// Prepare autoconnect
if(ConfigManager.isAutoConnect() && this.server.autoconnect){
const serverURL = new URL('my://' + this.server.server_ip)
mcArgs.unshift(serverURL.hostname)
mcArgs.unshift('--server')
if(serverURL.port){
mcArgs.unshift(serverURL.port)
mcArgs.unshift('--port')
// 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.
* @returns {Array.<String>} - An array containing the paths of each library required by this 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 paths of each library required by this process.
*/
classpathArg(mods){
classpathArg(mods, tempNativePath){
let cpArgs = []
if(!mcVersionAtLeast('1.17', this.server.rawServer.minecraftVersion) || this.usingFabricLoader) {
// Add the version.jar to the classpath.
const version = this.versionData.id
cpArgs.push(path.join(this.dir, 'versions', version, version + '.jar'))
// 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()
cpArgs = cpArgs.concat(mojangLibs)
const mojangLibs = this._resolveMojangLibraries(tempNativePath)
// Resolve the server declared libraries.
const servLibs = this._resolveServerLibraries(mods)
cpArgs = cpArgs.concat(servLibs)
// 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
}
@ -223,31 +706,24 @@ class ProcessBuilder {
*
* TODO - clean up function
*
* @returns {Array.<String>} - An array containing the paths of each library mojang declares.
* @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(){
const libs = []
_resolveMojangLibraries(tempNativePath){
const nativesRegex = /.+:natives-([^-]+)(?:-(.+))?/
const libs = {}
const libArr = this.versionData.libraries
const nativePath = path.join(this.dir, 'natives')
const libArr = this.vanillaManifest.libraries
fs.ensureDirSync(tempNativePath)
for(let i=0; i<libArr.length; i++){
const lib = libArr[i]
if(Library.validateRules(lib.rules)){
if(lib.natives == null){
const dlInfo = lib.downloads
const artifact = dlInfo.artifact
const to = path.join(this.libPath, artifact.path)
libs.push(to)
} else {
if(isLibraryCompatible(lib.rules, lib.natives)){
// Pre-1.19 has a natives object.
if(lib.natives != null) {
// Extract the native library.
const natives = lib.natives
const extractInst = lib.extract
const exclusionArr = extractInst.exclude
const opSys = Library.mojangFriendlyOS()
const indexId = natives[opSys]
const dlInfo = lib.downloads
const classifiers = dlInfo.classifiers
const artifact = classifiers[indexId]
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)
@ -270,17 +746,73 @@ class ProcessBuilder {
// Extract the file.
if(!shouldExclude){
mkpath.sync(path.join(nativePath, fileName, '..'))
fs.writeFile(path.join(nativePath, fileName), zipEntries[i].getData(), (err) => {
fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
if(err){
console.error('Error while extracting native library:', err)
logger.error('Error while extracting native library:', err)
}
})
}
}
}
// 1.19+ logic
else if(lib.name.includes('natives-')) {
libs.push(to)
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
}
}
}
@ -293,22 +825,22 @@ class ProcessBuilder {
* 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 {Array.<String>} - An array containing the paths of each library this server requires.
* @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 mdles = this.server.modules
let libs = []
const mdls = this.server.modules
let libs = {}
// Locate Forge/Libraries
for(let i=0; i<mdles.length; i++){
if(mdles[i].type != null && (mdles[i].type === 'forge-hosted' || mdles[i].type === 'library')){
let lib = mdles[i]
libs.push(path.join(this.libPath, lib.artifact.path == null ? AssetGuard._resolvePath(lib.id, lib.artifact.extension) : lib.artifact.path))
if(lib.sub_modules != null){
const res = this._resolveModuleLibraries(lib)
// 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)
if(res.length > 0){
libs = libs.concat(res)
libs = {...libs, ...res}
}
}
}
@ -319,7 +851,7 @@ class ProcessBuilder {
if(mods.sub_modules != null){
const res = this._resolveModuleLibraries(mods[i])
if(res.length > 0){
libs = libs.concat(res)
libs = {...libs, ...res}
}
}
}
@ -330,22 +862,24 @@ class ProcessBuilder {
/**
* Recursively resolve the path of each library required by this module.
*
* @param {Object} mdle - A module object from the server distro index.
* @returns {Array.<String>} - An array containing the paths of each library this module requires.
* @param {Object} mdl A module object from the server distro index.
* @returns {Array.<string>} An array containing the paths of each library this module requires.
*/
_resolveModuleLibraries(mdle){
if(mdle.sub_modules == null){
_resolveModuleLibraries(mdl){
if(!mdl.subModules.length > 0){
return []
}
let libs = []
for(let i=0; i<mdle.sub_modules.length; i++){
const sm = mdle.sub_modules[i]
if(sm.type != null && sm.type == 'library'){
libs.push(path.join(this.libPath, sm.artifact.path == null ? AssetGuard._resolvePath(sm.id, sm.artifact.extension) : sm.artifact.path))
for(let sm of mdl.subModules){
if(sm.rawModule.type === Type.Library){
if(sm.rawModule.classpath ?? true) {
libs.push(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(mdle.sub_modules != null){
if(mdl.subModules.length > 0){
const res = this._resolveModuleLibraries(sm)
if(res.length > 0){
libs = libs.concat(res)
@ -354,6 +888,7 @@ class ProcessBuilder {
}
return libs
}
}
module.exports = ProcessBuilder

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,234 @@
/**
* Script for login.ejs
*/
// Validation Regexes.
const validUsername = /^[a-zA-Z0-9_]{1,16}$/
const basicEmail = /^\S+@\S+\.\S+$/
//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
// Login Elements
const loginCancelContainer = document.getElementById('loginCancelContainer')
const loginCancelButton = document.getElementById('loginCancelButton')
const loginEmailError = document.getElementById('loginEmailError')
const loginUsername = document.getElementById('loginUsername')
const loginPasswordError = document.getElementById('loginPasswordError')
const loginPassword = document.getElementById('loginPassword')
const checkmarkContainer = document.getElementById('checkmarkContainer')
const loginRememberOption = document.getElementById('loginRememberOption')
const loginButton = document.getElementById('loginButton')
const loginForm = document.getElementById('loginForm')
// Control variables.
let lu = false, lp = false
/**
* Show a login error.
*
* @param {HTMLElement} element The element on which to display the error.
* @param {string} value The error text.
*/
function showError(element, value){
element.innerHTML = value
element.style.opacity = 1
}
/**
* Shake a login error to add emphasis.
*
* @param {HTMLElement} element The element to shake.
*/
function shakeError(element){
if(element.style.opacity == 1){
element.classList.remove('shake')
void element.offsetWidth
element.classList.add('shake')
}
}
/**
* Validate that an email field is neither empty nor invalid.
*
* @param {string} value The email value.
*/
function validateEmail(value){
if(value){
if(!basicEmail.test(value) && !validUsername.test(value)){
showError(loginEmailError, Lang.queryJS('login.error.invalidValue'))
loginDisabled(true)
lu = false
} else {
loginEmailError.style.opacity = 0
lu = true
if(lp){
loginDisabled(false)
}
}
} else {
lu = false
showError(loginEmailError, Lang.queryJS('login.error.requiredValue'))
loginDisabled(true)
}
}
/**
* Validate that the password field is not empty.
*
* @param {string} value The password value.
*/
function validatePassword(value){
if(value){
loginPasswordError.style.opacity = 0
lp = true
if(lu){
loginDisabled(false)
}
} else {
lp = false
showError(loginPasswordError, Lang.queryJS('login.error.invalidValue'))
loginDisabled(true)
}
}
// Emphasize errors with shake when focus is lost.
loginUsername.addEventListener('focusout', (e) => {
validateEmail(e.target.value)
shakeError(loginEmailError)
})
loginPassword.addEventListener('focusout', (e) => {
validatePassword(e.target.value)
shakeError(loginPasswordError)
})
// Validate input for each field.
loginUsername.addEventListener('input', (e) => {
validateEmail(e.target.value)
})
loginPassword.addEventListener('input', (e) => {
validatePassword(e.target.value)
})
/**
* Enable or disable the login button.
*
* @param {boolean} v True to enable, false to disable.
*/
function loginDisabled(v){
if(loginButton.disabled !== v){
loginButton.disabled = v
}
}
/**
* Enable or disable loading elements.
*
* @param {boolean} v True to enable, false to disable.
*/
function loginLoading(v){
if(v){
loginButton.setAttribute('loading', v)
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn'))
} else {
loginButton.removeAttribute('loading')
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login'))
}
}
/**
* Enable or disable login form.
*
* @param {boolean} v True to enable, false to disable.
*/
function formDisabled(v){
loginDisabled(v)
loginCancelButton.disabled = v
loginUsername.disabled = v
loginPassword.disabled = v
if(v){
checkmarkContainer.setAttribute('disabled', v)
} else {
checkmarkContainer.removeAttribute('disabled')
}
loginRememberOption.disabled = v
}
let loginViewOnSuccess = VIEWS.landing
let loginViewOnCancel = VIEWS.settings
let loginViewCancelHandler
function loginCancelEnabled(val){
if(val){
$(loginCancelContainer).show()
} else {
$(loginCancelContainer).hide()
}
}
loginCancelButton.onclick = (e) => {
switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => {
loginUsername.value = ''
loginPassword.value = ''
loginCancelEnabled(false)
if(loginViewCancelHandler != null){
loginViewCancelHandler()
loginViewCancelHandler = null
}
})
}
// Disable default form behavior.
loginForm.onsubmit = () => { return false }
// Bind login button behavior.
loginButton.addEventListener('click', () => {
// Disable form.
formDisabled(true)
// Show loading stuff.
loginLoading(true)
AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => {
updateSelectedAccount(value)
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success'))
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
setTimeout(() => {
switchView(VIEWS.login, loginViewOnSuccess, 500, 500, async () => {
// Temporary workaround
if(loginViewOnSuccess === VIEWS.settings){
await prepareSettings()
}
loginViewOnSuccess = VIEWS.landing // Reset this for good measure.
loginCancelEnabled(false) // Reset this for good measure.
loginViewCancelHandler = null // Reset this for good measure.
loginUsername.value = ''
loginPassword.value = ''
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
loginLoading(false)
loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login'))
formDisabled(false)
})
}, 1000)
}).catch((displayableError) => {
loginLoading(false)
let actualDisplayableError
if(isDisplayableError(displayableError)) {
msftLoginLogger.error('Error while logging in.', displayableError)
actualDisplayableError = displayableError
} else {
// Uh oh.
msftLoginLogger.error('Unhandled error during login.', displayableError)
actualDisplayableError = Lang.queryJS('login.error.unknown')
}
setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain'))
setOverlayHandler(() => {
formDisabled(false)
toggleOverlay(false)
})
toggleOverlay(true)
})
})

View File

@ -0,0 +1,50 @@
const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer')
const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft')
const loginOptionMojang = document.getElementById('loginOptionMojang')
const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton')
let loginOptionsCancellable = false
let loginOptionsViewOnLoginSuccess
let loginOptionsViewOnLoginCancel
let loginOptionsViewOnCancel
let loginOptionsViewCancelHandler
function loginOptionsCancelEnabled(val){
if(val){
$(loginOptionsCancelContainer).show()
} else {
$(loginOptionsCancelContainer).hide()
}
}
loginOptionMicrosoft.onclick = (e) => {
switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => {
ipcRenderer.send(
MSFT_OPCODE.OPEN_LOGIN,
loginOptionsViewOnLoginSuccess,
loginOptionsViewOnLoginCancel
)
})
}
loginOptionMojang.onclick = (e) => {
switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
loginViewOnSuccess = loginOptionsViewOnLoginSuccess
loginViewOnCancel = loginOptionsViewOnLoginCancel
loginCancelEnabled(true)
})
}
loginOptionsCancelButton.onclick = (e) => {
switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => {
// Clear login values (Mojang login)
// No cleanup needed for Microsoft.
loginUsername.value = ''
loginPassword.value = ''
if(loginOptionsViewCancelHandler != null){
loginOptionsViewCancelHandler()
loginOptionsViewCancelHandler = null
}
})
}

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,466 @@
/**
* Initialize UI functions which depend on internal modules.
* Loaded after core UI functions are initialized in uicore.js.
*/
// Requirements
const path = require('path')
const { Type } = require('helios-distribution-types')
const AuthManager = require('./assets/js/authmanager')
const ConfigManager = require('./assets/js/configmanager')
const { DistroAPI } = require('./assets/js/distromanager')
let rscShouldLoad = false
let fatalStartupError = false
// Mapping of each view to their container IDs.
const VIEWS = {
landing: '#landingContainer',
loginOptions: '#loginOptionsContainer',
login: '#loginContainer',
settings: '#settingsContainer',
welcome: '#welcomeContainer',
waiting: '#waitingContainer'
}
// The currently shown view container.
let currentView
/**
* Switch launcher views.
*
* @param {string} current The ID of the current view container.
* @param {*} next The ID of the next view container.
* @param {*} currentFadeTime Optional. The fade out time for the current view.
* @param {*} nextFadeTime Optional. The fade in time for the next view.
* @param {*} onCurrentFade Optional. Callback function to execute when the current
* view fades out.
* @param {*} onNextFade Optional. Callback function to execute when the next view
* fades in.
*/
function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){
currentView = next
$(`${current}`).fadeOut(currentFadeTime, async () => {
await onCurrentFade()
$(`${next}`).fadeIn(nextFadeTime, async () => {
await onNextFade()
})
})
}
/**
* Get the currently shown view container.
*
* @returns {string} The currently shown view container.
*/
function getCurrentView(){
return currentView
}
async function showMainUI(data){
if(!isDev){
loggerAutoUpdater.info('Initializing..')
ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease())
}
await prepareSettings(true)
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
refreshServerStatus()
setTimeout(() => {
document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')`
$('#main').show()
const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0
// If this is enabled in a development environment we'll get ratelimited.
// The relaunch frequency is usually far too high.
if(!isDev && isLoggedIn){
validateSelectedAccount()
}
if(ConfigManager.isFirstLaunch()){
currentView = VIEWS.welcome
$(VIEWS.welcome).fadeIn(1000)
} else {
if(isLoggedIn){
currentView = VIEWS.landing
$(VIEWS.landing).fadeIn(1000)
} else {
loginOptionsCancelEnabled(false)
loginOptionsViewOnLoginSuccess = VIEWS.landing
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
currentView = VIEWS.loginOptions
$(VIEWS.loginOptions).fadeIn(1000)
}
}
setTimeout(() => {
$('#loadingContainer').fadeOut(500, () => {
$('#loadSpinnerImage').removeClass('rotating')
})
}, 250)
}, 750)
// Disable tabbing to the news container.
initNews().then(() => {
$('#newsContainer *').attr('tabindex', '-1')
})
}
function showFatalStartupError(){
setTimeout(() => {
$('#loadingContainer').fadeOut(250, () => {
document.getElementById('overlayContainer').style.background = 'none'
setOverlayContent(
Lang.queryJS('uibinder.startup.fatalErrorTitle'),
Lang.queryJS('uibinder.startup.fatalErrorMessage'),
Lang.queryJS('uibinder.startup.closeButton')
)
setOverlayHandler(() => {
const window = remote.getCurrentWindow()
window.close()
})
toggleOverlay(true)
})
}, 750)
}
/**
* Common functions to perform after refreshing the distro index.
*
* @param {Object} data The distro index object.
*/
function onDistroRefresh(data){
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
refreshServerStatus()
initNews()
syncModConfigurations(data)
ensureJavaSettings(data)
}
/**
* Sync the mod configurations with the distro index.
*
* @param {Object} data The distro index object.
*/
function syncModConfigurations(data){
const syncedCfgs = []
for(let serv of data.servers){
const id = serv.rawServer.id
const mdls = serv.modules
const cfg = ConfigManager.getModConfiguration(id)
if(cfg != null){
const modsOld = cfg.mods
const mods = {}
for(let mdl of mdls){
const type = mdl.rawModule.type
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
if(!mdl.getRequired().value){
const mdlID = mdl.getVersionlessMavenIdentifier()
if(modsOld[mdlID] == null){
mods[mdlID] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.subModules, mdl), false)
}
} else {
if(mdl.subModules.length > 0){
const mdlID = mdl.getVersionlessMavenIdentifier()
const v = scanOptionalSubModules(mdl.subModules, mdl)
if(typeof v === 'object'){
if(modsOld[mdlID] == null){
mods[mdlID] = v
} else {
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true)
}
}
}
}
}
}
syncedCfgs.push({
id,
mods
})
} else {
const mods = {}
for(let mdl of mdls){
const type = mdl.rawModule.type
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
if(!mdl.getRequired().value){
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
if(mdl.subModules.length > 0){
const v = scanOptionalSubModules(mdl.subModules, mdl)
if(typeof v === 'object'){
mods[mdl.getVersionlessMavenIdentifier()] = v
}
}
}
}
}
syncedCfgs.push({
id,
mods
})
}
}
ConfigManager.setModConfigurations(syncedCfgs)
ConfigManager.save()
}
/**
* Ensure java configurations are present for the available servers.
*
* @param {Object} data The distro index object.
*/
function ensureJavaSettings(data) {
// Nothing too fancy for now.
for(const serv of data.servers){
ConfigManager.ensureJavaConfig(serv.rawServer.id, serv.effectiveJavaOptions, serv.rawServer.javaOptions?.ram)
}
ConfigManager.save()
}
/**
* Recursively scan for optional sub modules. If none are found,
* this function returns a boolean. If optional sub modules do exist,
* a recursive configuration object is returned.
*
* @returns {boolean | Object} The resolved mod configuration.
*/
function scanOptionalSubModules(mdls, origin){
if(mdls != null){
const mods = {}
for(let mdl of mdls){
const type = mdl.rawModule.type
// Optional types.
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
// It is optional.
if(!mdl.getRequired().value){
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
if(mdl.hasSubModules()){
const v = scanOptionalSubModules(mdl.subModules, mdl)
if(typeof v === 'object'){
mods[mdl.getVersionlessMavenIdentifier()] = v
}
}
}
}
}
if(Object.keys(mods).length > 0){
const ret = {
mods
}
if(!origin.getRequired().value){
ret.value = origin.getRequired().def
}
return ret
}
}
return origin.getRequired().def
}
/**
* Recursively merge an old configuration into a new configuration.
*
* @param {boolean | Object} o The old configuration value.
* @param {boolean | Object} n The new configuration value.
* @param {boolean} nReq If the new value is a required mod.
*
* @returns {boolean | Object} The merged configuration.
*/
function mergeModConfiguration(o, n, nReq = false){
if(typeof o === 'boolean'){
if(typeof n === 'boolean') return o
else if(typeof n === 'object'){
if(!nReq){
n.value = o
}
return n
}
} else if(typeof o === 'object'){
if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true
else if(typeof n === 'object'){
if(!nReq){
n.value = typeof o.value !== 'undefined' ? o.value : true
}
const newMods = Object.keys(n.mods)
for(let i=0; i<newMods.length; i++){
const mod = newMods[i]
if(o.mods[mod] != null){
n.mods[mod] = mergeModConfiguration(o.mods[mod], n.mods[mod])
}
}
return n
}
}
// If for some reason we haven't been able to merge,
// wipe the old value and use the new one. Just to be safe
return n
}
async function validateSelectedAccount(){
const selectedAcc = ConfigManager.getSelectedAccount()
if(selectedAcc != null){
const val = await AuthManager.validateSelected()
if(!val){
ConfigManager.removeAuthAccount(selectedAcc.uuid)
ConfigManager.save()
const accLen = Object.keys(ConfigManager.getAuthAccounts()).length
setOverlayContent(
Lang.queryJS('uibinder.validateAccount.failedMessageTitle'),
accLen > 0
? Lang.queryJS('uibinder.validateAccount.failedMessage', { 'account': selectedAcc.displayName })
: Lang.queryJS('uibinder.validateAccount.failedMessageSelectAnotherAccount', { 'account': selectedAcc.displayName }),
Lang.queryJS('uibinder.validateAccount.loginButton'),
Lang.queryJS('uibinder.validateAccount.selectAnotherAccountButton')
)
setOverlayHandler(() => {
const isMicrosoft = selectedAcc.type === 'microsoft'
if(isMicrosoft) {
// Empty for now
} else {
// Mojang
// For convenience, pre-populate the username of the account.
document.getElementById('loginUsername').value = selectedAcc.username
validateEmail(selectedAcc.username)
}
loginOptionsViewOnLoginSuccess = getCurrentView()
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
if(accLen > 0) {
loginOptionsViewOnCancel = getCurrentView()
loginOptionsViewCancelHandler = () => {
if(isMicrosoft) {
ConfigManager.addMicrosoftAuthAccount(
selectedAcc.uuid,
selectedAcc.accessToken,
selectedAcc.username,
selectedAcc.expiresAt,
selectedAcc.microsoft.access_token,
selectedAcc.microsoft.refresh_token,
selectedAcc.microsoft.expires_at
)
} else {
ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
}
ConfigManager.save()
validateSelectedAccount()
}
loginOptionsCancelEnabled(true)
} else {
loginOptionsCancelEnabled(false)
}
toggleOverlay(false)
switchView(getCurrentView(), VIEWS.loginOptions)
})
setDismissHandler(() => {
if(accLen > 1){
prepareAccountSelectionList()
$('#overlayContent').fadeOut(250, () => {
bindOverlayKeys(true, 'accountSelectContent', true)
$('#accountSelectContent').fadeIn(250)
})
} else {
const accountsObj = ConfigManager.getAuthAccounts()
const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v])
// This function validates the account switch.
setSelectedAccount(accounts[0].uuid)
toggleOverlay(false)
}
})
toggleOverlay(true, accLen > 0)
} else {
return true
}
} else {
return true
}
}
/**
* Temporary function to update the selected account along
* with the relevent UI elements.
*
* @param {string} uuid The UUID of the account.
*/
function setSelectedAccount(uuid){
const authAcc = ConfigManager.setSelectedAccount(uuid)
ConfigManager.save()
updateSelectedAccount(authAcc)
validateSelectedAccount()
}
// Synchronous Listener
document.addEventListener('readystatechange', async () => {
if (document.readyState === 'interactive' || document.readyState === 'complete'){
if(rscShouldLoad){
rscShouldLoad = false
if(!fatalStartupError){
const data = await DistroAPI.getDistribution()
await showMainUI(data)
} else {
showFatalStartupError()
}
}
}
}, false)
// Actions that must be performed after the distribution index is downloaded.
ipcRenderer.on('distributionIndexDone', async (event, res) => {
if(res) {
const data = await DistroAPI.getDistribution()
syncModConfigurations(data)
ensureJavaSettings(data)
if(document.readyState === 'interactive' || document.readyState === 'complete'){
await showMainUI(data)
} else {
rscShouldLoad = true
}
} else {
fatalStartupError = true
if(document.readyState === 'interactive' || document.readyState === 'complete'){
showFatalStartupError()
} else {
rscShouldLoad = true
}
}
})
// Util for development
async function devModeToggle() {
DistroAPI.toggleDevMode(true)
const data = await DistroAPI.refreshDistributionOrFallback()
ensureJavaSettings(data)
updateSelectedServer(data.servers[0])
syncModConfigurations(data)
}

View File

@ -0,0 +1,214 @@
/**
* Core UI functions are initialized in this file. This prevents
* unexpected errors from breaking the core features. Specifically,
* actions in this file should not require the usage of any internal
* modules, excluding dependencies.
*/
// Requirements
const $ = require('jquery')
const {ipcRenderer, shell, webFrame} = require('electron')
const remote = require('@electron/remote')
const isDev = require('./assets/js/isdev')
const { LoggerUtil } = require('helios-core')
const Lang = require('./assets/js/langloader')
const loggerUICore = LoggerUtil.getLogger('UICore')
const loggerAutoUpdater = LoggerUtil.getLogger('AutoUpdater')
// Log deprecation and process warnings.
process.traceProcessWarnings = true
process.traceDeprecation = true
// Disable eval function.
// eslint-disable-next-line
window.eval = global.eval = function () {
throw new Error('Sorry, this app does not support window.eval().')
}
// Display warning when devtools window is opened.
remote.getCurrentWebContents().on('devtools-opened', () => {
console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold')
console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px')
console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px')
})
// Disable zoom, needed for darwin.
webFrame.setZoomLevel(0)
webFrame.setVisualZoomLevelLimits(1, 1)
// Initialize auto updates in production environments.
let updateCheckListener
if(!isDev){
ipcRenderer.on('autoUpdateNotification', (event, arg, info) => {
switch(arg){
case 'checking-for-update':
loggerAutoUpdater.info('Checking for update..')
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkingForUpdateButton'), true)
break
case 'update-available':
loggerAutoUpdater.info('New update available', info.version)
if(process.platform === 'darwin'){
info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/Helios-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg`
showUpdateUI(info)
}
populateSettingsUpdateInformation(info)
break
case 'update-downloaded':
loggerAutoUpdater.info('Update ' + info.version + ' ready to be installed.')
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.installNowButton'), false, () => {
if(!isDev){
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
}
})
showUpdateUI(info)
break
case 'update-not-available':
loggerAutoUpdater.info('No new update found.')
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkForUpdatesButton'))
break
case 'ready':
updateCheckListener = setInterval(() => {
ipcRenderer.send('autoUpdateAction', 'checkForUpdate')
}, 1800000)
ipcRenderer.send('autoUpdateAction', 'checkForUpdate')
break
case 'realerror':
if(info != null && info.code != null){
if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){
loggerAutoUpdater.info('No suitable releases found.')
} else if(info.code === 'ERR_XML_MISSED_ELEMENT'){
loggerAutoUpdater.info('No releases found.')
} else {
loggerAutoUpdater.error('Error during update check..', info)
loggerAutoUpdater.debug('Error Code:', info.code)
}
}
break
default:
loggerAutoUpdater.info('Unknown argument', arg)
break
}
})
}
/**
* Send a notification to the main process changing the value of
* allowPrerelease. If we are running a prerelease version, then
* this will always be set to true, regardless of the current value
* of val.
*
* @param {boolean} val The new allow prerelease value.
*/
function changeAllowPrerelease(val){
ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val)
}
function showUpdateUI(info){
//TODO Make this message a bit more informative `${info.version}`
document.getElementById('image_seal_container').setAttribute('update', true)
document.getElementById('image_seal_container').onclick = () => {
/*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later')
setOverlayHandler(() => {
if(!isDev){
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
} else {
console.error('Cannot install updates in development environment.')
toggleOverlay(false)
}
})
setDismissHandler(() => {
toggleOverlay(false)
})
toggleOverlay(true, true)*/
switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
settingsNavItemListener(document.getElementById('settingsNavUpdate'), false)
})
}
}
/* jQuery Example
$(function(){
loggerUICore.info('UICore Initialized');
})*/
document.addEventListener('readystatechange', function () {
if (document.readyState === 'interactive'){
loggerUICore.info('UICore Initializing..')
// Bind close button.
Array.from(document.getElementsByClassName('fCb')).map((val) => {
val.addEventListener('click', e => {
const window = remote.getCurrentWindow()
window.close()
})
})
// Bind restore down button.
Array.from(document.getElementsByClassName('fRb')).map((val) => {
val.addEventListener('click', e => {
const window = remote.getCurrentWindow()
if(window.isMaximized()){
window.unmaximize()
} else {
window.maximize()
}
document.activeElement.blur()
})
})
// Bind minimize button.
Array.from(document.getElementsByClassName('fMb')).map((val) => {
val.addEventListener('click', e => {
const window = remote.getCurrentWindow()
window.minimize()
document.activeElement.blur()
})
})
// Remove focus from social media buttons once they're clicked.
Array.from(document.getElementsByClassName('mediaURL')).map(val => {
val.addEventListener('click', e => {
document.activeElement.blur()
})
})
} else if(document.readyState === 'complete'){
//266.01
//170.8
//53.21
// Bind progress bar length to length of bot wrapper
//const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width
//const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width
//const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width
document.getElementById('launch_details').style.maxWidth = 266.01
document.getElementById('launch_progress').style.width = 170.8
document.getElementById('launch_details_right').style.maxWidth = 170.8
document.getElementById('launch_progress_label').style.width = 53.21
}
}, false)
/**
* Open web links in the user's default browser.
*/
$(document).on('click', 'a[href^="http"]', function(event) {
event.preventDefault()
shell.openExternal(this.href)
})
/**
* Opens DevTools window if you hold (ctrl + shift + i).
* This will crash the program if you are using multiple
* DevTools, for example the chrome debugger in VS Code.
*/
document.addEventListener('keydown', function (e) {
if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){
let window = remote.getCurrentWindow()
window.toggleDevTools()
}
})

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

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

View File

@ -1,75 +0,0 @@
/**
* Core UI functions are initialized in this file. This prevents
* unexpected errors from breaking the core features. Specifically,
* actions in this file should not require the usage of any internal
* modules, excluding dependencies.
*/
const $ = require('jquery');
const {remote, shell} = require('electron')
/* jQuery Example
$(function(){
console.log('UICore Initialized');
})*/
document.addEventListener('readystatechange', function () {
if (document.readyState === 'interactive'){
console.log('UICore Initializing..');
// Bind close button.
document.getElementById("frame_btn_close").addEventListener("click", function (e) {
const window = remote.getCurrentWindow()
window.close()
})
// Bind restore down button.
document.getElementById("frame_btn_restoredown").addEventListener("click", function (e) {
const window = remote.getCurrentWindow()
if(window.isMaximized()){
window.unmaximize();
} else {
window.maximize()
}
})
// Bind minimize button.
document.getElementById("frame_btn_minimize").addEventListener("click", function (e) {
const window = remote.getCurrentWindow()
window.minimize()
})
} else if(document.readyState === 'complete'){
// Bind progress bar length to length of bot wrapper
const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width
const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width
const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width
document.getElementById("launch_details").style.maxWidth = targetWidth
document.getElementById("launch_progress").style.width = targetWidth2
document.getElementById("launch_details_right").style.maxWidth = targetWidth2
document.getElementById("launch_progress_label").style.width = targetWidth3
}
}, false)
/**
* Open web links in the user's default browser.
*/
$(document).on('click', 'a[href^="http"]', function(event) {
event.preventDefault();
//console.log(os.homedir())
shell.openExternal(this.href)
})
/**
* Opens DevTools window if you hold (ctrl + shift + i).
* This will crash the program if you are using multiple
* DevTools, for example the chrome debugger in VS Code.
*/
document.addEventListener('keydown', function (e) {
if(e.keyCode == 73 && e.ctrlKey && e.shiftKey){
let window = remote.getCurrentWindow()
window.toggleDevTools()
}
})

View File

@ -1,44 +0,0 @@
const app = require('electron')
const remote = require('electron').BrowserWindow
/**
* Doesn't work yet.
*/
exports.setIconBadge = function(text){
if(process.platform === 'darwin'){
app.dock.setBadge('' + text)
} else if (process.platform === 'win32'){
const win = remote.getFocusedWindow()
if(text === ''){
win.setOverlayIcon(null, '')
return;
}
//Create badge
const canvas = document.createElement('canvas')
canvas.height = 140;
canvas.width = 140;
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#a02d2a'
ctx.beginPath()
ctx.ellipse(70, 70, 70, 70, 0, 0, 2 * Math.PI)
ctx.fill()
ctx.textAlign = 'center'
ctx.fillStyle = 'white'
if(text.length > 2 ){
ctx.font = '75px sans-serif'
ctx.fillText('' + text, 70, 98)
} else if (text.length > 1){
ctx.font = '100px sans-serif'
ctx.fillText('' + text, 70, 105)
} else {
ctx.font = '125px sans-serif'
ctx.fillText('' + text, 70, 112)
}
const badgeDataURL = canvas.toDataURL()
const img = NativeImage.createFromDataURL(badgeDataURL)
win.setOverlayIcon(img, '' + text)
}
}

View File

@ -0,0 +1,20 @@
# Custom Language File for Launcher Customizer
[ejs.app]
title = "Helios Launcher"
[ejs.landing]
mediaGitHubURL = "https://github.com/dscalzi/HeliosLauncher"
mediaTwitterURL = "#"
mediaInstagramURL = "#"
mediaYouTubeURL = "#"
mediaDiscordURL = "https://discord.gg/zNWUXdt"
[ejs.settings]
sourceGithubLink = "https://github.com/dscalZi/HeliosLauncher"
supportLink = "https://github.com/dscalZi/HeliosLauncher/issues"
[ejs.welcome]
welcomeHeader = "WELCOME TO WESTEROSCRAFT"
welcomeDescription = "Our mission is to recreate the universe imagined by author George RR Martin in his fantasy series, A Song of Ice and Fire. Through the collaborative effort of thousands of community members, we have sought to create Westeros as accurately and precisely as possible within Minecraft. The world we are creating is yours to explore. Journey from Dorne to Castle Black, and if you arent afraid, beyond the Wall itself, but best not delay. As the words of House Stark ominously warn: Winter is Coming."
welcomeDescCTA = "You are just a few clicks away from Westeros."

297
app/assets/lang/en_US.toml Normal file
View File

@ -0,0 +1,297 @@
[ejs.landing]
updateAvailableTooltip = "Update Available"
usernamePlaceholder = "Username"
usernameEditButton = "Edit"
settingsTooltip = "Settings"
serverStatus = "SERVER"
serverStatusPlaceholder = "OFFLINE"
mojangStatus = "MOJANG STATUS"
mojangStatusTooltipTitle = "Services"
mojangStatusNETitle = "Non&nbsp;Essential"
newsButton = "NEWS"
launchButton = "PLAY"
launchButtonPlaceholder = "&#8226; No Server Selected"
launchDetails = "Please wait.."
newsNavigationStatus = "{currentPage} of {totalPages}"
newsErrorLoadSpan = "Checking for News.."
newsErrorFailedSpan = "Failed to Load News"
newsErrorRetryButton = "Try Again"
newsErrorNoneSpan = "No News"
[ejs.login]
loginCancelText = "Cancel"
loginSubheader = "MINECRAFT LOGIN"
loginEmailError = "* Invalid Value"
loginEmailPlaceholder = "EMAIL OR USERNAME"
loginPasswordError = "* Required"
loginPasswordPlaceholder = "PASSWORD"
loginForgotPasswordLink = "https://minecraft.net/password/forgot/"
loginForgotPasswordText = "forgot password?"
loginRememberMeText = "remember me?"
loginButtonText = "LOGIN"
loginNeedAccountLink = "https://minecraft.net/store/minecraft-java-edition/"
loginNeedAccountText = "Need an Account?"
loginPasswordDisclaimer1 = "Your password is sent directly to mojang and never stored."
loginPasswordDisclaimer2 = "{appName} is not affiliated with Mojang AB."
[ejs.loginOptions]
loginOptionsTitle = "Login Options"
loginWithMicrosoft = "Login with Microsoft"
loginWithMojang = "Login with Mojang"
cancelButton = "Cancel"
[ejs.overlay]
serverSelectHeader = "Available Servers"
serverSelectConfirm = "Select"
serverSelectCancel = "Cancel"
accountSelectHeader = "Select an Account"
accountSelectConfirm = "Select"
accountSelectCancel = "Cancel"
[ejs.settings]
navHeaderText = "Settings"
navAccount = "Account"
navMinecraft = "Minecraft"
navMods = "Mods"
navJava = "Java"
navLauncher = "Launcher"
navAbout = "About"
navUpdates = "Updates"
navDone = "Done"
tabAccountHeaderText = "Account Settings"
tabAccountHeaderDesc = "Add new accounts or manage existing ones."
microsoftAccount = "Microsoft"
addMicrosoftAccount = "+ Add Microsoft Account"
mojangAccount = "Mojang"
addMojangAccount = "+ Add Mojang Account"
minecraftTabHeaderText = "Minecraft Settings"
minecraftTabHeaderDesc = "Options related to game launch."
gameResolutionTitle = "Game Resolution"
launchFullscreenTitle = "Launch in fullscreen."
autoConnectTitle = "Automatically connect to the server on launch."
launchDetachedTitle = "Launch game process detached from launcher."
launchDetachedDesc = "If the game is not detached, closing the launcher will also close the game."
tabModsHeaderText = "Mod Settings"
tabModsHeaderDesc = "Enable or disable mods."
switchServerButton = "Switch"
requiredMods = "Required Mods"
optionalMods = "Optional Mods"
dropinMods = "Drop-in Mods"
addMods = "Add Mods"
dropinRefreshNote = "(F5 to Refresh)"
shaderpacks = "Shaderpacks"
shaderpackDesc = "Enable or disable shaders. Please note, shaders will only run smoothly on powerful setups. You may add custom packs here."
selectShaderpack = "Select Shaderpack"
tabJavaHeaderText = "Java Settings"
tabJavaHeaderDesc = "Manage the Java configuration (advanced)."
memoryTitle = "Memory"
maxRAM = "Maximum RAM"
minRAM = "Minimum RAM"
memoryDesc = "The recommended minimum RAM is 3 gigabytes. Setting the minimum and maximum values to the same value may reduce lag."
memoryTotalTitle = "Total"
memoryAvailableTitle = "Available"
javaExecutableTitle = "Java Executable"
javaExecSelDialogTitle = "Select Java Executable"
javaExecSelButtonText = "Choose File"
javaExecDesc = "The Java executable is validated before game launch."
javaPathDesc = "The path should end with <strong>{pathSuffix}</strong>."
jvmOptsTitle = "Additional JVM Options"
jvmOptsDesc = "Options to be provided to the JVM at runtime. <em>-Xms</em> and <em>-Xmx</em> should not be included."
launcherTabHeaderText = "Launcher Settings"
launcherTabHeaderDesc = "Options related to the launcher itself."
allowPrereleaseTitle = "Allow Pre-Release Updates."
allowPrereleaseDesc = "Pre-Releases include new features which may have not been fully tested or integrated.<br>This will always be true if you are using a pre-release version."
dataDirectoryTitle = "Data Directory"
selectDataDirectory = "Select Data Directory"
chooseFolder = "Choose Folder"
dataDirectoryDesc = "All game files and local Java installations will be stored in the data directory.<br>Screenshots and world saves are stored in the instance folder for the corresponding server configuration."
aboutTabHeaderText = "About"
aboutTabHeaderDesc = "View information and release notes for the current version."
aboutTitle = "{appName}"
stableRelease = "Stable Release"
versionText = "Version "
sourceGithub = "Source (GitHub)"
support = "Support"
devToolsConsole = "DevTools Console"
releaseNotes = "Release Notes"
changelog = "Changelog"
noReleaseNotes = "No Release Notes"
viewReleaseNotes = "View Release Notes on GitHub"
launcherUpdatesHeaderText = "Launcher Updates"
launcherUpdatesHeaderDesc = "Download, install, and review updates for the launcher."
checkForUpdates = "Check for Updates"
whatsNew = "What's New"
updateReleaseNotes = "Update Release Notes"
[ejs.waiting]
waitingText = "Waiting for Microsoft.."
[ejs.welcome]
continueButton = "CONTINUE"
[js.discord]
waiting = "Waiting for Client.."
state = "Server: {shortId}"
[js.index]
microsoftLoginTitle = "Microsoft Login"
microsoftLogoutTitle = "Microsoft Logout"
[js.login]
login = "LOGIN"
loggingIn = "LOGGING IN"
success = "SUCCESS"
tryAgain = "Try Again"
[js.login.error]
invalidValue = "* Invalid Value"
requiredValue = "* Required"
[js.login.error.unknown]
title = "Unknown Error During Login"
desc = "An unknown error has occurred. Please see the console for details."
[js.landing.launch]
pleaseWait = "Please wait.."
failureTitle = "Error During Launch"
failureText = "See console (CTRL + Shift + i) for more details."
okay = "Okay"
[js.landing.selectedAccount]
noAccountSelected = "No Account Selected"
[js.landing.selectedServer]
noSelection = "No Server Selected"
loading = "Loading.."
[js.landing.serverStatus]
server = "SERVER"
offline = "OFFLINE"
players = "PLAYERS"
[js.landing.systemScan]
checking = "Checking system info.."
noCompatibleJava = "No Compatible<br>Java Installation Found"
installJavaMessage = "In order to launch Minecraft, you need a 64-bit installation of Java {major}. Would you like us to install a copy?"
installJava = "Install Java"
installJavaManually = "Install Manually"
javaDownloadPrepare = "Preparing Java Download.."
javaDownloadFailureTitle = "Error During Java Download"
javaDownloadFailureText = "See console (CTRL + Shift + i) for more details."
javaRequired = "Java is Required<br>to Launch"
javaRequiredMessage = 'A valid x64 installation of Java {major} is required to launch.<br><br>Please refer to our <a href="https://github.com/dscalzi/HeliosLauncher/wiki/Java-Management#manually-installing-a-valid-version-of-java">Java Management Guide</a> for instructions on how to manually install Java.'
javaRequiredDismiss = "I Understand"
javaRequiredCancel = "Go Back"
[js.landing.downloadJava]
findJdkFailure = "Failed to find OpenJDK distribution."
javaDownloadCorruptedError = "Downloaded JDK has a bad hash, the file may be corrupted."
extractingJava = "Extracting Java"
javaInstalled = "Java Installed!"
[js.landing.dlAsync]
loadingServerInfo = "Loading server information.."
fatalError = "Fatal Error"
unableToLoadDistributionIndex = "Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details."
pleaseWait = "Please wait.."
errorDuringLaunchTitle = "Error During Launch"
seeConsoleForDetails = "See console (CTRL + Shift + i) for more details."
validatingFileIntegrity = "Validating file integrity.."
errorDuringFileVerificationTitle = "Error During File Verification"
downloadingFiles = "Downloading files.."
errorDuringFileDownloadTitle = "Error During File Download"
preparingToLaunch = "Preparing to launch.."
launchingGame = "Launching game.."
launchWrapperNotDownloaded = "The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.<br><br>To fix this issue, temporarily turn off your antivirus software and launch the game again.<br><br>If you have time, please <a href=\"https://github.com/dscalzi/HeliosLauncher/issues\">submit an issue</a> and let us know what antivirus software you use. We'll contact them and try to straighten things out."
doneEnjoyServer = "Done. Enjoy the server!"
checkConsoleForDetails = "Please check the console (CTRL + Shift + i) for more details."
[js.landing.news]
checking = "Checking for News"
[js.landing.discord]
loading = "Loading game.."
joining = "Sailing to Westeros!"
joined = "Exploring the Realm!"
[js.overlay]
dismiss = "Dismiss"
[js.settings.fileSelectors]
executables = "Executables"
allFiles = "All Files"
[js.settings.mstfLogin]
errorTitle = "Something Went Wrong"
errorMessage = "Microsoft authentication failed. Please try again."
okButton = "OK"
[js.settings.mstfLogout]
errorTitle = "Something Went Wrong"
errorMessage = "Microsoft logout failed. Please try again."
okButton = "OK"
[js.settings.authAccountSelect]
selectButton = "Select Account"
selectedButton = "Selected Account &#10004;"
[js.settings.authAccountLogout]
lastAccountWarningTitle = "Warning<br>This is Your Last Account"
lastAccountWarningMessage = "In order to use the launcher you must be logged into at least one account. You will need to login again after.<br><br>Are you sure you want to log out?"
confirmButton = "I'm Sure"
cancelButton = "Cancel"
[js.settings.authAccountPopulate]
username = "Username"
uuid = "UUID"
selectAccount = "Select Account"
selectedAccount = "Selected Account ✓"
logout = "Log Out"
[js.settings.dropinMods]
removeButton = "Remove"
deleteFailedTitle = "Failed to Delete<br>Drop-in Mod {fullName}"
deleteFailedMessage = "Make sure the file is not in use and try again."
failedToggleTitle = "Failed to Toggle<br>One or More Drop-in Mods"
okButton = "Okay"
[js.settings.serverListing]
mainServer = "Main Server"
[js.settings.java]
selectedJava = "Selected: Java {version} ({vendor})"
invalidSelection = "Invalid Selection"
requiresJava = "Requires Java {major} x64."
availableOptions = "Available Options for Java {major} (HotSpot VM)"
[js.settings.about]
preReleaseTitle = "Pre-release"
stableReleaseTitle = "Stable Release"
releaseNotesFailed = "Failed to load release notes."
[js.settings.updates]
newReleaseTitle = "New Release Available"
newPreReleaseTitle = "New Pre-release Available"
downloadingButton = "Downloading.."
downloadButton = 'Download from GitHub<span style="font-size: 10px;color: gray;text-shadow: none !important;">Close the launcher and run the dmg to update.</span>'
latestVersionTitle = "You Are Running the Latest Version"
checkForUpdatesButton = "Check for Updates"
checkingForUpdatesButton = "Checking for Updates.."
[js.uibinder.startup]
fatalErrorTitle = "Fatal Error: Unable to Load Distribution Index"
fatalErrorMessage = "A connection could not be established to our servers to download the distribution index. No local copies were available to load. <br><br>The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. Ensure you are connected to the internet and relaunch the application."
closeButton = "Close"
[js.uibinder.validateAccount]
failedMessageTitle = "Failed to Refresh Login"
failedMessage = "We were unable to refresh the login for <strong>{account}</strong>. Please select another account or login again."
failedMessageSelectAnotherAccount = "We were unable to refresh the login for <strong>{account}</strong>. Please login again."
loginButton = "Login"
selectAnotherAccountButton = "Select Another Account"
[js.uicore.autoUpdate]
checkingForUpdateButton = "Checking for Updates..."
installNowButton = "Install Now"
checkForUpdatesButton = "Check for Updates"

View File

@ -1,411 +0,0 @@
{
"version": "1.0",
"discord": {
"clientID": "385581240906022916",
"smallImageText": "WesterosCraft",
"smallImageKey": "seal-circle"
},
"servers": [
{
"id": "WesterosCraft-1.11.2",
"name": "WesterosCraft Production Server",
"news_feed": "http://www.westeroscraft.com/api/rss.php?preset_id=12700544",
"icon_url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/server-prod.png",
"revision": "0.0.1",
"server_ip": "mc.westeroscraft.com",
"mc_version": "1.11.2",
"discord": {
"shortId": "Production",
"largeImageText": "WesterosCraft Production Server",
"largeImageKey": "server-prod"
},
"default_selected": true,
"autoconnect": true,
"modules": [
{
"id": "net.minecraftforge:forge:1.11.2-13.20.1.2429",
"name": "Minecraft Forge 1.11.2-13.20.1.2429",
"type": "forge-hosted",
"artifact": {
"size": 4450992,
"MD5": "3fcc9b0104f0261397d3cc897e55a1c5",
"extension": ".jar",
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar"
},
"sub_modules": [
{
"id": "net.minecraft:launchwrapper:1.12",
"name": "Mojang (LaunchWrapper)",
"type": "library",
"artifact": {
"size": 32999,
"MD5": "934b2d91c7c5be4a49577c9e6b40e8da",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar"
}
},
{
"id": "org.ow2.asm:asm-all:5.0.3",
"name": "Mojang (ASM)",
"type": "library",
"artifact": {
"size": 241639,
"MD5": "c5cc4613bbdfba3ccf5f0ab85390d0b8",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/asm-all-5.0.3.jar"
}
},
{
"id": "org.scala-lang:scala-library:2.11.1",
"name": "Minecraft Forge (scala-library)",
"type": "library",
"artifact": {
"size": 1474672,
"MD5": "379c15c4f724421c6d5d7aecedaf87a6",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-library-2.11.1.jar.pack.xz"
}
},
{
"id": "org.scala-lang:scala-compiler:2.11.1",
"name": "Minecraft Forge (scala-compiler)",
"type": "library",
"artifact": {
"size": 3076920,
"MD5": "7d89e952f2d5c74577310cd2c28e3f20",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-compiler-2.11.1.jar.pack.xz"
}
},
{
"id": "org.scala-lang:scala-actors-migration_2.11:1.1.0",
"name": "Minecraft Forge (scala-actors-migration)",
"type": "library",
"artifact": {
"size": 21324,
"MD5": "04e3428b2600ace33c7ae2bf1f6c0a4c",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-actors-migration_2.11-1.1.0.jar.pack.xz"
}
},
{
"id": "org.scala-lang.plugins:scala-continuations-library_2.11:1.0.2",
"name": "Minecraft Forge (scala-continuations-library)",
"type": "library",
"artifact": {
"size": 7956,
"MD5": "ed9b1d27aba8ac4090a3749c4dfc895a",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-library_2.11-1.0.2.jar.pack.xz"
}
},
{
"id": "org.scala-lang.plugins:scala-continuations-plugin_2.11.1:1.0.2",
"name": "Minecraft Forge (scala-continuations-plugin)",
"type": "library",
"artifact": {
"size": 46140,
"MD5": "a8232db22a72a981de6b1399eb86dff7",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-plugin_2.11.1-1.0.2.jar.pack.xz"
}
},
{
"id": "org.scala-lang:scala-parser-combinators_2.11:1.0.1",
"name": "Minecraft Forge (scala-parser-combinators)",
"type": "library",
"artifact": {
"size": 85568,
"MD5": "2e50a7df17680daadacca69f07f8a16d",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-parser-combinators_2.11-1.0.1.jar.pack.xz"
}
},
{
"id": "org.scala-lang:scala-reflect:2.11.1",
"name": "Minecraft Forge (scala-reflect)",
"type": "library",
"artifact": {
"size": 1070312,
"MD5": "84e5dc81c10e2bd74c579c9d0332fdd9",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-reflect-2.11.1.jar.pack.xz"
}
},
{
"id": "org.scala-lang:scala-swing_2.11:1.0.1",
"name": "Minecraft Forge (scala-swing)",
"type": "library",
"artifact": {
"size": 736795,
"MD5": "1d360289e697022a3f57abaad344b28f",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-swing_2.11-1.0.1.jar"
}
},
{
"id": "org.scala-lang:scala-xml_2.11:1.0.2",
"name": "Minecraft Forge (scala-xml)",
"type": "library",
"artifact": {
"size": 217812,
"MD5": "cc891b094a4c32dedc56bfefe9b072ff",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-xml_2.11-1.0.2.jar.pack.xz"
}
},
{
"id": "com.typesafe.akka:akka-actor_2.11:2.3.3",
"name": "Minecraft Forge (akka-actor)",
"type": "library",
"artifact": {
"size": 746612,
"MD5": "25cb22c3078e9fb3f7a861c912924862",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/akka-actor_2.11-2.3.3.jar.pack.xz"
}
},
{
"id": "com.typesafe:config:1.2.1",
"name": "Minecraft Forge (typesafe-config)",
"type": "library",
"artifact": {
"size": 56636,
"MD5": "10ec4ccabc4e68aac9cf87165ead5d7d",
"extension": ".jar.pack.xz",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/config-1.2.1.jar.pack.xz"
}
},
{
"id": "lzma:lzma:0.0.1",
"name": "Mojang (LZMA)",
"type": "library",
"artifact": {
"size": 5762,
"MD5": "a3e3c3186e41c4a1a3027ba2bb23cdc6",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/lzma-0.0.1.jar"
}
},
{
"id": "net.sf.trove4j:trove4j:3.0.3",
"name": "Trove4J 3.0.3",
"type": "library",
"artifact": {
"size": 2523218,
"MD5": "8fc4d4e0129244f9fd39650c5f30feb2",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/trove4j-3.0.3.jar"
}
},
{
"id": "java3d:vecmath:1.5.2",
"name": "Vecmath 1.5.2",
"type": "library",
"artifact": {
"size": 318956,
"MD5": "e5d2b7f46c4800a32f62ce75676a5710",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/vecmath-1.5.2.jar"
}
},
{
"id": "net.sf.jopt-simple:jopt-simple:4.6",
"name": "Jopt-simple 4.6",
"type": "library",
"artifact": {
"size": 62477,
"MD5": "13560a58a79b46b82057686543e8d727",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar"
}
},
{
"id": "net.minecraftforge:MercuriusUpdater:1.11.2",
"name": "MercuriusUpdater 1.11.2",
"type": "library",
"artifact": {
"size": 15146,
"MD5": "7556d06064ebbfa3b334a15092d725d0",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/MercuriusUpdater-1.11.2.jar"
}
}
]
},
{
"id": "net.optifine:optifine:1.11.2_HD_U_C3",
"name": "Optifine (1.11.2_HD_U_C3)",
"type": "forgemod",
"artifact": {
"size": 2106193,
"MD5": "82f495594cd50e1fda7a8aa0246239fc",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/OptiFine.jar"
}
},
{
"id": "mezz:jei:1.11.2-4.5.0.290",
"name": "JustEnoughItems (1.11.2-4.5.0.290)",
"type": "forgemod",
"artifact": {
"size": 538740,
"MD5": "f4d931f6db6210621a86fa1e7eae8016",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/jei.jar"
}
},
{
"id": "mcp.mobius:waila:1.7.1_1.11.2",
"name": "Waila (1.7.1_1.11.2)",
"type": "forgemod",
"required": {
"value": false
},
"artifact": {
"size": 542744,
"MD5": "26258a3557bf333e8f4ce8b1e9481031",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/Waila.jar"
}
},
{
"id": "com.github.hexomod:worldeditcuife2:2.1.1-mf-1.11.2-13.20.0.2228",
"name": "WorldEditCUI (v2.1.1-mf-1.11.2-13.20.0.2228)",
"type": "forgemod",
"required": {
"value": false
},
"artifact": {
"size": 461691,
"MD5": "439f82b69f3464969163c188818c677b",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/worldeditcuife.jar"
}
},
{
"id": "techbrew:journeymap:1.11.2-5.4.7",
"name": "JourneyMap (1.11.2-5.4.7)",
"type": "forgemod",
"required": {
"value": false
},
"artifact": {
"size": 1735525,
"MD5": "1c3380502eb7b9a495581b2402d144df",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/journeymap.jar"
}
},
{
"id": "octarine-noise:betterfoliage:1.11.2-2.1.8",
"name": "BetterFoliage (1.11.2-2.1.8)",
"type": "forgemod",
"required": {
"value": false
},
"artifact": {
"size": 4676029,
"MD5": "b2dd47e42da56fb49a07a0d38df91bc4",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/BetterFoliage.jar"
},
"sub_modules": [
{
"id": "betterfoliage.cfg",
"name": "BetterFoliage Configuration File",
"type": "file",
"artifact": {
"size": 7878,
"MD5": "6dd38f873c4129af05a2d6c500cbe954",
"path": "/config/betterfoliage.cfg",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/betterfoliage.cfg"
}
}
]
},
{
"id": "org.blockartistry:dynsurround:1.11.2-3.4.6.2",
"name": "DynamicSurroundings (1.11.2-3.4.6.2)",
"type": "forgemod",
"required": {
"value": false
},
"artifact": {
"size": 21853035,
"MD5": "82a6aac5420151960b8dd709deee5423",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/DynamicSurroundings.jar"
},
"sub_modules": [
{
"id": "dsurround.cfg",
"name": "DynamicSurroundings General Configuration File",
"type": "file",
"artifact": {
"size": 19736,
"MD5": "4c64fc6cbbb83b18012ed4820b0b496e",
"path": "/config/dsurround/dsurround.cfg",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/dsurround.cfg"
}
},
{
"id": "westeros.json",
"name": "DynamicSurroundings WesterosCraft Configuration File",
"type": "file",
"artifact": {
"size": 608,
"MD5": "44eab112ff24d0bd29974c270de868ba",
"path": "/config/dsurround/westeros.json",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/westeros.json"
}
}
]
},
{
"id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133",
"name": "WesterosBlocks (3.0.0-beta-6-133)",
"type": "forgemod",
"artifact": {
"size": 16321712,
"MD5": "5a89e2ab18916c18965fc93a0766cc6e",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar"
}
},
{
"id": "com.westeroscraft:westeroscraftrp:2017-08-16",
"name": "WesterosCraft Resource Pack (2017-08-16)",
"type": "file",
"artifact": {
"size": 45241339,
"MD5": "ec2d9fdb14d5c2eafe5975a240202f1a",
"path": "resourcepacks/WesterosCraft.zip",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/resourcepacks/WesterosCraft.zip"
}
},
{
"id": "options.txt",
"name": "Default Client Options",
"type": "file",
"artifact": {
"size": 1973,
"path": "options.txt",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/options-1.11.2.txt"
}
},
{
"id": "servers.dat",
"name": "Saved Client Servers",
"type": "file",
"artifact": {
"size": 84,
"MD5": "71d99e229d7d2b8d2a6423e46832a4b8",
"path": "servers.dat",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/servers.dat"
}
}
]
}
]
}

View File

@ -1,403 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ServerPack version="3.0">
<Server id="WesterosCraft-1.11.2" name="WesterosCraft Production Client" newsUrl="http://www.westeroscraft.com/home/" iconUrl="http://mc.westeroscraft.com/WesterosCraftLauncher/files/server-prod.png"
revision="3.4.17" serverAddress="mc.westeroscraft.com" generateList="true" version="1.11.2" autoConnect="true" mainClass="net.minecraft.launchwrapper.Launch"
libOverrides="com.google.guava:guava:17.0 org.apache.commons:commons-lang3:3.3.2">
<Module id="forge" name="Minecraft Forge 13.20.1.2429">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/forge-1.11.2-13.20.1.2429-universal.jar</URL>
<Required>true</Required>
<ModType order="1" jreArgs="-Dfml.ignorePatchDiscrepancies=true -Dfml.ignoreInvalidMinecraftCertificates=true" launchArgs="--tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker">Library</ModType>
<MD5>3fcc9b0104f0261397d3cc897e55a1c5</MD5>
<Submodule id="launchwrapper" name="Mojang (LaunchWrapper)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar</URL>
<MD5>934b2d91c7c5be4a49577c9e6b40e8da</MD5>
<Required>true</Required>
<ModType order="2">Library</ModType>
</Submodule>
<Submodule id="asm" name="Mojang (ASM)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/asm-all-5.0.3.jar</URL>
<MD5>c5cc4613bbdfba3ccf5f0ab85390d0b8</MD5>
<Required>true</Required>
<ModType order="3">Library</ModType>
</Submodule>
<Submodule id="scala-lib" name="Minecraft Forge (scala-library)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-library-2.11.1.jar.pack.xz</URL>
<MD5>379c15c4f724421c6d5d7aecedaf87a6</MD5>
<Required>true</Required>
<ModType order="4">Library</ModType>
</Submodule>
<Submodule id="scala-compiler" name="Minecraft Forge (scala-compiler)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-compiler-2.11.1.jar.pack.xz</URL>
<MD5>7d89e952f2d5c74577310cd2c28e3f20</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-actors-migration" name="Minecraft Forge (scala-actors-migration)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-actors-migration_2.11-1.1.0.jar.pack.xz</URL>
<MD5>55e0dc48a2a122353628a463b9499cf0</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-continuations-library" name="Minecraft Forge (scala-continuations-library)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-library_2.11-1.0.2.jar.pack.xz</URL>
<MD5>820008ee6df308dafa0a6c478270aa19</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-continuations-plugin" name="Minecraft Forge (scala-continuations-plugin)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-plugin_2.11.1-1.0.2.jar.pack.xz</URL>
<MD5>657a7ad23928bf4bee68202c7d7e1d56</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-parser-combinators" name="Minecraft Forge (scala-parser-combinators)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-parser-combinators_2.11-1.0.1.jar.pack.xz</URL>
<MD5>de496467c91610e55e6e711b6bdbf10c</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-reflect" name="Minecraft Forge (scala-reflect)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-reflect-2.11.1.jar.pack.xz</URL>
<MD5>abbdaf710e856f6e65376706c9505952</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-swing" name="Minecraft Forge (scala-swing)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-swing_2.11-1.0.1.jar</URL>
<MD5>1d360289e697022a3f57abaad344b28f</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-xml" name="Minecraft Forge (scala-xml)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-xml_2.11-1.0.2.jar.pack.xz</URL>
<MD5>cc891b094a4c32dedc56bfefe9b072ff</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="akka-actor" name="Minecraft Forge (akka-actor)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/akka-actor_2.11-2.3.3.jar.pack.xz</URL>
<MD5>25cb22c3078e9fb3f7a861c912924862</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="typesafe-config" name="Minecraft Forge (typesafe-config)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/config-1.2.1.jar.pack.xz</URL>
<MD5>f24b281c40daef1280d5ef26b11caa63</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="lzma" name="Mojang (LZMA)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/lzma-0.0.1.jar</URL>
<MD5>a3e3c3186e41c4a1a3027ba2bb23cdc6</MD5>
<Required>true</Required>
<ModType order="6">Library</ModType>
</Submodule>
<Submodule id="trove4j" name="Trove4J 3.0.3">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/trove4j-3.0.3.jar</URL>
<MD5>8fc4d4e0129244f9fd39650c5f30feb2</MD5>
<Required>true</Required>
<ModType order="7">Library</ModType>
</Submodule>
<Submodule id="vecmath" name="Vecmath 1.5.2">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/vecmath-1.5.2.jar</URL>
<MD5>e5d2b7f46c4800a32f62ce75676a5710</MD5>
<Required>true</Required>
<ModType order="8">Library</ModType>
</Submodule>
<Submodule id="jopt-simple" name="Jopt-simple 4.6">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar</URL>
<MD5>13560a58a79b46b82057686543e8d727</MD5>
<Required>true</Required>
<ModType order="9">Library</ModType>
</Submodule>
<Submodule id="MercuriusUpdater" name="MercuriusUpdater 1.11.2">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/MercuriusUpdater-1.11.2.jar</URL>
<MD5>13560a58a79b46b82057686543e8d727</MD5>
<Required>true</Required>
<ModType order="10">Library</ModType>
</Submodule>
</Module>
<Module name="WesterosBlocks (3.0.0-beta-6-133)" id="WesterosBlocks">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<MD5>5a89e2ab18916c18965fc93a0766cc6e</MD5>
</Module>
<Module name="OptiFine (1.11.2_HD_U_C3)" id="OptiFine">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/OptiFine.jar</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<MD5>82f495594cd50e1fda7a8aa0246239fc</MD5>
</Module>
<Module name="JustEnoughItems (1.11.2-4.5.0.290)" id="JustEnoughItems">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/jei.jar</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<MD5>f4d931f6db6210621a86fa1e7eae8016</MD5>
</Module>
<Module name="Waila (1.7.1_1.11.2)" id="Waila">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/Waila.jar</URL>
<Required isDefault="false">false</Required>
<ModType>Regular</ModType>
<MD5>26258a3557bf333e8f4ce8b1e9481031</MD5>
</Module>
<Module name="WorldEditCUI (v2.1.1-mf-1.11.2-13.20.0.2228)" id="worldeditcuife">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/worldeditcuife.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>439f82b69f3464969163c188818c677b</MD5>
</Module>
<Module id="journeymap" name="JourneyMap (1.11.2-5.4.7)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/journeymap.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>1c3380502eb7b9a495581b2402d144df</MD5>
</Module>
<Module id="BetterFoliage" name="BetterFoliage (1.11.2-2.1.8)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/BetterFoliage.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>b2dd47e42da56fb49a07a0d38df91bc4</MD5>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/betterfoliage.cfg</URL>
<Path>/config/betterfoliage.cfg</Path>
<MD5>6dd38f873c4129af05a2d6c500cbe954</MD5>
</ConfigFile>
</Module>
<Module id="DynamicSurroundings" name="DynamicSurroundings (1.11.2-3.4.6.2)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/DynamicSurroundings.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>82a6aac5420151960b8dd709deee5423</MD5>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/dsurround.cfg</URL>
<Path>/config/dsurround/dsurround.cfg</Path>
<MD5>4c64fc6cbbb83b18012ed4820b0b496e</MD5>
</ConfigFile>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/config/dsurround/westeros.json</URL>
<Path>/config/dsurround/westeros.json</Path>
<MD5>44eab112ff24d0bd29974c270de868ba</MD5>
</ConfigFile>
</Module>
<Module id="WesterosCraftRP" name="WesterosCraft Resource Pack (2017-08-16)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/resourcepacks/WesterosCraft.zip</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<ModPath>/resourcepacks/WesterosCraft.zip</ModPath>
<MD5>ec2d9fdb14d5c2eafe5975a240202f1a</MD5>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/files/options-1.11.2.txt</URL>
<Path>/options.txt</Path>
<NoOverwrite>true</NoOverwrite>
</ConfigFile>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/servers.dat</URL>
<Path>/servers.dat</Path>
<MD5>71d99e229d7d2b8d2a6423e46832a4b8</MD5>
</ConfigFile>
</Module>
</Server>
<Server id="WesterosCraftTest-1.11.2" name="WesterosCraft Test Client" newsUrl="http://www.westeroscraft.com/home/" iconUrl="http://mc.westeroscraft.com/WesterosCraftLauncher/files/server-test.png"
revision="3.4.18" serverAddress="mc.westeroscraft.com:4444" generateList="false" version="1.11.2" autoConnect="true" mainClass="net.minecraft.launchwrapper.Launch"
libOverrides="com.google.guava:guava:17.0 org.apache.commons:commons-lang3:3.3.2">
<Module id="forge" name="Minecraft Forge 13.20.1.2476">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/forge-1.11.2-13.20.1.2476-universal.jar</URL>
<Required>true</Required>
<ModType order="1" jreArgs="-Dfml.ignorePatchDiscrepancies=true -Dfml.ignoreInvalidMinecraftCertificates=true" launchArgs="--tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker">Library</ModType>
<MD5>7cef816cc01d53a04a180f0214d2982a</MD5>
<Submodule id="launchwrapper" name="Mojang (LaunchWrapper)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar</URL>
<MD5>934b2d91c7c5be4a49577c9e6b40e8da</MD5>
<Required>true</Required>
<ModType order="2">Library</ModType>
</Submodule>
<Submodule id="asm" name="Mojang (ASM)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/asm-all-5.0.3.jar</URL>
<MD5>c5cc4613bbdfba3ccf5f0ab85390d0b8</MD5>
<Required>true</Required>
<ModType order="3">Library</ModType>
</Submodule>
<Submodule id="scala-lib" name="Minecraft Forge (scala-library)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-library-2.11.1.jar.pack.xz</URL>
<MD5>379c15c4f724421c6d5d7aecedaf87a6</MD5>
<Required>true</Required>
<ModType order="4">Library</ModType>
</Submodule>
<Submodule id="scala-compiler" name="Minecraft Forge (scala-compiler)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-compiler-2.11.1.jar.pack.xz</URL>
<MD5>7d89e952f2d5c74577310cd2c28e3f20</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-actors-migration" name="Minecraft Forge (scala-actors-migration)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-actors-migration_2.11-1.1.0.jar.pack.xz</URL>
<MD5>55e0dc48a2a122353628a463b9499cf0</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-continuations-library" name="Minecraft Forge (scala-continuations-library)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-library_2.11-1.0.2.jar.pack.xz</URL>
<MD5>820008ee6df308dafa0a6c478270aa19</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-continuations-plugin" name="Minecraft Forge (scala-continuations-plugin)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-continuations-plugin_2.11.1-1.0.2.jar.pack.xz</URL>
<MD5>657a7ad23928bf4bee68202c7d7e1d56</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-parser-combinators" name="Minecraft Forge (scala-parser-combinators)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-parser-combinators_2.11-1.0.1.jar.pack.xz</URL>
<MD5>de496467c91610e55e6e711b6bdbf10c</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-reflect" name="Minecraft Forge (scala-reflect)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-reflect-2.11.1.jar.pack.xz</URL>
<MD5>abbdaf710e856f6e65376706c9505952</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-swing" name="Minecraft Forge (scala-swing)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-swing_2.11-1.0.1.jar</URL>
<MD5>1d360289e697022a3f57abaad344b28f</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="scala-xml" name="Minecraft Forge (scala-xml)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/scala-xml_2.11-1.0.2.jar.pack.xz</URL>
<MD5>cc891b094a4c32dedc56bfefe9b072ff</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="akka-actor" name="Minecraft Forge (akka-actor)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/akka-actor_2.11-2.3.3.jar.pack.xz</URL>
<MD5>25cb22c3078e9fb3f7a861c912924862</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="typesafe-config" name="Minecraft Forge (typesafe-config)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/config-1.2.1.jar.pack.xz</URL>
<MD5>f24b281c40daef1280d5ef26b11caa63</MD5>
<Required>true</Required>
<ModType order="5">Library</ModType>
</Submodule>
<Submodule id="lzma" name="Mojang (LZMA)">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/lzma-0.0.1.jar</URL>
<MD5>a3e3c3186e41c4a1a3027ba2bb23cdc6</MD5>
<Required>true</Required>
<ModType order="6">Library</ModType>
</Submodule>
<Submodule id="trove4j" name="Trove4J 3.0.3">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/trove4j-3.0.3.jar</URL>
<MD5>8fc4d4e0129244f9fd39650c5f30feb2</MD5>
<Required>true</Required>
<ModType order="7">Library</ModType>
</Submodule>
<Submodule id="vecmath" name="Vecmath 1.5.2">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/vecmath-1.5.2.jar</URL>
<MD5>e5d2b7f46c4800a32f62ce75676a5710</MD5>
<Required>true</Required>
<ModType order="8">Library</ModType>
</Submodule>
<Submodule id="jopt-simple" name="Jopt-simple 4.6">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar</URL>
<MD5>13560a58a79b46b82057686543e8d727</MD5>
<Required>true</Required>
<ModType order="9">Library</ModType>
</Submodule>
<Submodule id="MercuriusUpdater" name="MercuriusUpdater 1.11.2">
<URL priority="0">http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/MercuriusUpdater-1.11.2.jar</URL>
<MD5>13560a58a79b46b82057686543e8d727</MD5>
<Required>true</Required>
<ModType order="10">Library</ModType>
</Submodule>
</Module>
<Module name="WesterosBlocks (3.0.0-beta-6-133)" id="WesterosBlocks">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/WesterosBlocks.jar</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<MD5>5a89e2ab18916c18965fc93a0766cc6e</MD5>
</Module>
<Module name="OptiFine (1.11.2_HD_U_C3)" id="OptiFine">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/OptiFine.jar</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<MD5>82f495594cd50e1fda7a8aa0246239fc</MD5>
</Module>
<Module name="JustEnoughItems (1.11.2-4.5.0.290)" id="JustEnoughItems">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/jei.jar</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<MD5>f4d931f6db6210621a86fa1e7eae8016</MD5>
</Module>
<Module name="Waila (1.7.1_1.11.2)" id="Waila">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/Waila.jar</URL>
<Required isDefault="false">false</Required>
<ModType>Regular</ModType>
<MD5>26258a3557bf333e8f4ce8b1e9481031</MD5>
</Module>
<Module name="WorldEditCUI (v2.1.1-mf-1.11.2-13.20.0.2228)" id="worldeditcuife">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/worldeditcuife.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>439f82b69f3464969163c188818c677b</MD5>
</Module>
<Module id="journeymap" name="JourneyMap (1.11.2-5.4.7)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/journeymap.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>1c3380502eb7b9a495581b2402d144df</MD5>
</Module>
<Module id="BetterFoliage" name="BetterFoliage (1.11.2-2.1.8)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/BetterFoliage.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>b2dd47e42da56fb49a07a0d38df91bc4</MD5>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/config/betterfoliage.cfg</URL>
<Path>/config/betterfoliage.cfg</Path>
<MD5>6dd38f873c4129af05a2d6c500cbe954</MD5>
</ConfigFile>
</Module>
<Module id="DynamicSurroundings" name="DynamicSurroundings (1.11.2-3.4.6.2)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/mods/DynamicSurroundings.jar</URL>
<Required isDefault="true">false</Required>
<ModType>Regular</ModType>
<MD5>82a6aac5420151960b8dd709deee5423</MD5>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/config/dsurround/dsurround.cfg</URL>
<Path>/config/dsurround/dsurround.cfg</Path>
<MD5>4c64fc6cbbb83b18012ed4820b0b496e</MD5>
</ConfigFile>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/config/dsurround/westeros.json</URL>
<Path>/config/dsurround/westeros.json</Path>
<MD5>44eab112ff24d0bd29974c270de868ba</MD5>
</ConfigFile>
</Module>
<Module id="WesterosCraftRP" name="WesterosCraft Resource Pack (2017-08-16)">
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/resourcepacks/WesterosCraft.zip</URL>
<Required>true</Required>
<ModType>Regular</ModType>
<ModPath>/resourcepacks/WesterosCraft.zip</ModPath>
<MD5>ec2d9fdb14d5c2eafe5975a240202f1a</MD5>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/files/options-1.11.2.txt</URL>
<Path>/options.txt</Path>
<NoOverwrite>true</NoOverwrite>
</ConfigFile>
<ConfigFile>
<URL>http://mc.westeroscraft.com/WesterosCraftLauncher/test-1.11.2/servers.dat</URL>
<Path>/servers.dat</Path>
<MD5>594de6063df993b5fde31c7290226ee4</MD5>
</ConfigFile>
</Module>
</Server>
</ServerPack>

View File

@ -1,7 +1,33 @@
<div id="frame_bar">
<div id="frame_btn_dock">
<button class="frame_btn" id="frame_btn_close" tabIndex="-1"></button>
<button class="frame_btn" id="frame_btn_restoredown" tabIndex="-1"></button>
<button class="frame_btn" id="frame_btn_minimize" tabIndex="-1"></button>
<div id="frameBar">
<div id="frameResizableTop" class="frameDragPadder"></div>
<div id="frameMain">
<div class="frameResizableVert frameDragPadder"></div>
<%if (process.platform === 'darwin') { %>
<div id="frameContentDarwin">
<div id="frameButtonDockDarwin">
<button class="frameButtonDarwin fCb" id="frameButtonDarwin_close" tabIndex="-1"></button>
<button class="frameButtonDarwin fMb" id="frameButtonDarwin_minimize" tabIndex="-1"></button>
<button class="frameButtonDarwin fRb" id="frameButtonDarwin_restoredown" tabIndex="-1"></button>
</div>
</div>
<% } else{ %>
<div id="frameContentWin">
<div id="frameTitleDock">
<span id="frameTitleText"><%= lang('app.title') %></span>
</div>
<div id="frameButtonDockWin">
<button class="frameButton fMb" id="frameButton_minimize" tabIndex="-1">
<svg name="TitleBarMinimize" width="10" height="10" viewBox="0 0 12 12"><rect stroke="#ffffff" fill="#ffffff" width="10" height="1" x="1" y="6"></rect></svg>
</button>
<button class="frameButton fRb" id="frameButton_restoredown" tabIndex="-1">
<svg name="TitleBarMaximize" width="10" height="10" viewBox="0 0 12 12"><rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="#ffffff" stroke-width="1.4px"></rect></svg>
</button>
<button class="frameButton fCb" id="frameButton_close" tabIndex="-1">
<svg name="TitleBarClose" width="10" height="10" viewBox="0 0 12 12"><polygon stroke="#ffffff" fill="#ffffff" fill-rule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"></polygon></svg>
</button>
</div>
</div>
<% } %>
<div class="frameResizableVert frameDragPadder"></div>
</div>
</div>

View File

@ -1,162 +0,0 @@
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Westeroscraft Launcher</title>
<script src="./assets/js/uicore.js"></script>
<script src="./assets/js/actionbinder.js"></script>
<link type="text/css" rel="stylesheet" href="./assets/css/launcher.css">
<style>
body {
background: url('assets/images/backgrounds/<%=0%>.jpg') no-repeat center center fixed;
background-size: cover;
}
</style>
</head>
<body>
<% include frame.ejs %>
<div id="main">
<div id="upper">
<div id="left">
<img id="image_seal" src="assets/images/WesterosSealCircle.png"/>
</div>
<div id="content">
</div>
<div id="right">
<div id="rightContainer">
<div id="user_content">
<span id="user_text">Username</span>
<div id="avatarContainer">
<div id="avatarOverlay">Edit</div>
<img id="avatarImage" src="https://cdn.discordapp.com/avatars/169197209630277642/6650b5a50e1cb3d00a79b9b88b9a0cd4.png"/>
</div>
</div>
<div id="mediaContent">
<div id="internalMedia">
<div class="mediaContainer">
<svg id="newsSVG" class="mediaSVG" viewBox="18 14 106.36 104.43">
<defs>
<style>.cls-2,.cls-3{fill:none;stroke-miterlimit:10;stroke-width:6px;}</style>
</defs>
<path class="cls-2" d="M115.36,113.8H27.18a6.67,6.67,0,0,1-6.67-6.67V19.27H108.2V107.1a6.71,6.71,0,0,0,6.71,6.7h0a6.71,6.71,0,0,0,6.71-6.71v-75H108.15"/>
<line class="cls-3" x1="73.75" y1="36.18" x2="97.14" y2="36.18"/>
<g>
<rect class="cls-1" x="31.77" y="32.96" width="33.79" height="20.76"/>
<line class="cls-3" x1="73.75" y1="50.22" x2="97.14" y2="50.22"/>
<line class="cls-3" x1="31.66" y1="64.25" x2="97.14" y2="64.25"/>
<line class="cls-3" x1="31.66" y1="78.28" x2="97.14" y2="78.28"/>
<line class="cls-3" x1="31.66" y1="92.31" x2="97.14" y2="92.31"/>
<line class="cls-3" x1="31.66" y1="92.31" x2="97.14" y2="92.31"/>
</g>
</svg>
</div>
</div>
<div class="mediaDivider"></div>
<div id="externalMedia">
<div class="mediaContainer">
<a href="http://www.westeroscraft.com">
<svg id="linkSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
<g>
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
<path d="M66,73.88a3.85,3.85,0,0,0,1.73-.42,8.22,8.22,0,0,1-.94-3.76,8.36,8.36,0,0,1,8.35-8.35H95A8.35,8.35,0,1,1,95,78H85.8a21.51,21.51,0,0,1-6.65,8.72H95a17.07,17.07,0,0,0,0-34.15H75.13A17,17,0,0,0,58.59,73.88Z"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="https://twitter.com/westeroscraft">
<svg id="twitterSVG" class="mediaSVG" viewBox="0 0 5000 4060" preserveAspectRatio="xMidYMid meet">
<g>
<path d="M1210 4048 c-350 -30 -780 -175 -1124 -378 -56 -33 -86 -57 -86 -68 0 -16 7 -17 83 -9 114 12 349 1 493 -22 295 -49 620 -180 843 -341 l54 -38 -49 -7 c-367 -49 -660 -256 -821 -582 -30 -61 -53 -120 -51 -130 3 -16 12 -17 73 -13 97 7 199 5 270 -4 l60 -9 -65 -22 c-341 -117 -609 -419 -681 -769 -18 -88 -26 -226 -13 -239 4 -3 32 7 63 22 68 35 198 77 266 86 28 4 58 9 68 12 10 2 -22 -34 -72 -82 -240 -232 -353 -532 -321 -852 15 -149 79 -347 133 -418 16 -20 17 -19 49 20 377 455 913 795 1491 945 160 41 346 74 485 86 l82 7 -7 -59 c-5 -33 -7 -117 -6 -189 2 -163 31 -286 103 -430 141 -285 422 -504 708 -550 112 -19 333 -19 442 0 180 30 335 108 477 239 l58 54 95 -24 c143 -36 286 -89 427 -160 70 -35 131 -60 135 -56 19 19 -74 209 -151 312 -50 66 -161 178 -216 217 l-30 22 73 -14 c111 -21 257 -63 353 -101 99 -39 99 -39 99 -19 0 57 -237 326 -412 468 l-88 71 6 51 c4 28 1 130 -5 226 -30 440 -131 806 -333 1202 -380 745 -1036 1277 -1823 1477 -243 62 -430 81 -786 78 -134 0 -291 -5 -349 -10z"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="https://www.instagram.com/westeroscraft/">
<svg id="instagramSVG" class="mediaSVG" viewBox="0 0 5040 5040">
<defs>
<radialGradient id="instaFill" cx="30%" cy="107%" r="150%">
<stop offset="0%" stop-color="#fdf497"/>
<stop offset="5%" stop-color="#fdf497"/>
<stop offset="45%" stop-color="#fd5949"/>
<stop offset="60%" stop-color="#d6249f"/>
<stop offset="90%" stop-color="#285AEB"/>
</radialGradient>
</defs>
<g>
<path d="M1390 5024 c-163 -9 -239 -19 -315 -38 -281 -70 -477 -177 -660 -361 -184 -184 -292 -380 -361 -660 -43 -171 -53 -456 -53 -1445 0 -989 10 -1274 53 -1445 69 -280 177 -476 361 -660 184 -184 380 -292 660 -361 171 -43 456 -53 1445 -53 989 0 1274 10 1445 53 280 69 476 177 660 361 184 184 292 380 361 660 43 171 53 456 53 1445 0 989 -10 1274 -53 1445 -69 280 -177 476 -361 660 -184 184 -380 292 -660 361 -174 44 -454 53 -1470 52 -599 0 -960 -5 -1105 -14z m2230 -473 c58 -6 141 -18 185 -27 397 -78 638 -318 719 -714 37 -183 41 -309 41 -1290 0 -981 -4 -1107 -41 -1290 -81 -395 -319 -633 -714 -714 -183 -37 -309 -41 -1290 -41 -981 0 -1107 4 -1290 41 -397 81 -636 322 -714 719 -33 166 -38 296 -43 1100 -5 796 3 1203 27 1380 67 489 338 758 830 825 47 7 162 15 255 20 250 12 1907 4 2035 -9z"/>
<path d="M2355 3819 c-307 -42 -561 -172 -780 -400 -244 -253 -359 -543 -359 -899 0 -361 116 -648 367 -907 262 -269 563 -397 937 -397 374 0 675 128 937 397 251 259 367 546 367 907 0 361 -116 648 -367 907 -197 203 -422 326 -690 378 -101 20 -317 27 -412 14z m400 -509 c275 -88 470 -284 557 -560 20 -65 23 -95 23 -230 0 -135 -3 -165 -23 -230 -88 -278 -284 -474 -562 -562 -65 -20 -95 -23 -230 -23 -135 0 -165 3 -230 23 -278 88 -474 284 -562 562 -20 65 -23 95 -23 230 0 135 3 165 23 230 73 230 219 403 427 507 134 67 212 83 390 79 111 -3 155 -8 210 -26z"/>
<path d="M3750 1473 c-29 -11 -66 -38 -106 -77 -70 -71 -94 -126 -94 -221 0 -95 24 -150 94 -221 72 -71 126 -94 225 -94 168 0 311 143 311 311 0 99 -23 154 -94 225 -43 42 -76 66 -110 77 -61 21 -166 21 -226 0z"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="https://www.youtube.com/user/WesterosCraft">
<svg id="youtubeSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
<g>
<path d="M84.8,69.52,65.88,79.76V59.27Zm23.65.59c0-5.14-.79-17.63-3.94-20.57S99,45.86,73.37,45.86s-28,.73-31.14,3.68S38.29,65,38.29,70.11s.79,17.63,3.94,20.57,5.52,3.68,31.14,3.68,28-.74,31.14-3.68,3.94-15.42,3.94-20.57"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="https://discord.gg/hqdjs3m">
<svg id="discordSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
<g>
<path d="M81.23,78.48a6.14,6.14,0,1,1,6.14-6.14,6.14,6.14,0,0,1-6.14,6.14M60,78.48a6.14,6.14,0,1,1,6.14-6.14A6.14,6.14,0,0,1,60,78.48M104.41,73c-.92-7.7-8.24-22.9-8.24-22.9A43,43,0,0,0,88,45.59a17.88,17.88,0,0,0-8.38-1.27l-.13,1.06a23.52,23.52,0,0,1,5.8,1.95,87.59,87.59,0,0,1,8.17,4.87s-10.32-5.63-22.27-5.63a51.32,51.32,0,0,0-23.2,5.63,87.84,87.84,0,0,1,8.17-4.87,23.57,23.57,0,0,1,5.8-1.95l-.13-1.06a17.88,17.88,0,0,0-8.38,1.27,42.84,42.84,0,0,0-8.21,4.56S37.87,65.35,37,73s-.37,11.54-.37,11.54,4.22,5.68,9.9,7.14,7.7,1.47,7.7,1.47l3.75-4.68a21.22,21.22,0,0,1-4.65-2A24.47,24.47,0,0,1,47.93,82S61.16,88.4,70.68,88.4c10,0,22.75-6.44,22.75-6.44a24.56,24.56,0,0,1-5.35,4.56,21.22,21.22,0,0,1-4.65,2l3.75,4.68s2,0,7.7-1.47,9.89-7.14,9.89-7.14.55-3.85-.37-11.54"/>
</g>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="lower">
<div id="left">
<div class="bot_wrapper">
<div id="content">
<span class="bot_label">PLAYERS</span>
<span id="player_count">18/100</span>
<div class="bot_divider"></div>
<span class="bot_label">MOJANG STATUS</span>
<span id="mojang_status_icon">&#8226;</span>
</div>
</div>
</div>
<div id="center">
<div class="bot_wrapper">
<div id="content">
<button id="menu_button">
<img src="assets/images/icons/arrow.svg" id="menu_img"/>
&#10;<span id="menu_text">MENU</span>
</button>
</div>
</div>
</div>
<div id="right">
<div class="bot_wrapper">
<div id="launch_content">
<button id="launch_button">PLAY</button>
<div class="bot_divider"></div>
<!-- Span until we implement the real selection -->
<span class="bot_label" id="server_selection">&#8226; No Server Selected</span>
</div>
<div id="launch_details">
<div id="launch_details_left">
<span id="launch_progress_label">0%</span>
<div class="bot_divider"></div>
</div>
<div id="launch_details_right">
<progress id="launch_progress" value="22" max="100"></progress>
<span id="launch_details_text" class="bot_label">Please wait..</span>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

220
app/landing.ejs Normal file
View File

@ -0,0 +1,220 @@
<div id="landingContainer" style="display: none;">
<div id="upper">
<div id="left">
<div id="image_seal_container">
<img id="image_seal" src="assets/images/SealCircle.png"/>
<div id="updateAvailableTooltip"><%- lang('landing.updateAvailableTooltip') %></div>
</div>
</div>
<div id="content">
</div>
<div id="right">
<div id="rightContainer">
<div id="user_content">
<span id="user_text"><%- lang('landing.usernamePlaceholder') %></span>
<div id="avatarContainer">
<button id="avatarOverlay"><%- lang('landing.usernameEditButton') %></button>
</div>
</div>
<div id="mediaContent">
<div id="internalMedia">
<div class="mediaContainer" id="settingsMediaContainer">
<button class="mediaButton" id="settingsMediaButton">
<svg id="settingsSVG" class="mediaSVG" viewBox="0 0 141.36 137.43">
<path d="M70.70475616319865,83.36934004916053 a15.320781354859122,15.320781354859122 0 1 1 14.454501310561755,-15.296030496450625 A14.850515045097694,14.850515045097694 0 0 1 70.70475616319865,83.36934004916053 M123.25082856443602,55.425620905968366 h-12.375429204248078 A45.54157947163293,45.54157947163293 0 0 0 107.21227231573047,46.243052436416285 l8.613298726156664,-9.108315894326587 a9.727087354538993,9.727087354538993 0 0 0 0,-13.167456673319956 l-3.465120177189462,-3.6631270444574313 a8.489544434114185,8.489544434114185 0 0 0 -12.375429204248078,0 l-8.613298726156664,9.108315894326587 A40.442902639482725,40.442902639482725 0 0 0 81.99114759747292,25.427580514871032 V12.532383284044531 a9.108315894326587,9.108315894326587 0 0 0 -8.811305593424633,-9.306322761594556 h-4.950171681699231 a9.108315894326587,9.108315894326587 0 0 0 -8.811305593424633,9.306322761594556 v12.895197230826497 a40.17064319698927,40.17064319698927 0 0 0 -9.331073620003052,4.0591407789933704 l-8.613298726156664,-9.108315894326587 a8.489544434114185,8.489544434114185 0 0 0 -12.375429204248078,0 L25.58394128451018,23.967279868769744 a9.727087354538993,9.727087354538993 0 0 0 0,13.167456673319956 L34.19724001066683,46.243052436416285 a45.07131316187151,45.07131316187151 0 0 0 -3.6631270444574313,9.083565035918088 h-12.375429204248078 a9.083565035918088,9.083565035918088 0 0 0 -8.811305593424633,9.306322761594556 v5.197680265784193 a9.108315894326587,9.108315894326587 0 0 0 8.811305593424633,9.306322761594556 h11.979415469712139 a45.69008462208391,45.69008462208391 0 0 0 4.0591407789933704,10.642869115653347 l-8.613298726156664,9.108315894326587 a9.727087354538993,9.727087354538993 0 0 0 0,13.167456673319956 l3.465120177189462,3.6631270444574313 a8.489544434114185,8.489544434114185 0 0 0 12.375429204248078,0 l8.613298726156664,-9.108315894326587 a40.49240435629971,40.49240435629971 0 0 0 9.331073620003052,4.0591407789933704 v12.895197230826497 a9.083565035918088,9.083565035918088 0 0 0 8.811305593424633,9.306322761594556 h4.950171681699231 A9.083565035918088,9.083565035918088 0 0 0 81.99114759747292,123.68848839660077 V110.79329116577425 a40.78941465720167,40.78941465720167 0 0 0 9.331073620003052,-4.0591407789933704 l8.613298726156664,9.108315894326587 a8.489544434114185,8.489544434114185 0 0 0 12.375429204248078,0 l3.465120177189462,-3.6631270444574313 a9.727087354538993,9.727087354538993 0 0 0 0,-13.167456673319956 l-8.613298726156664,-9.108315894326587 a45.665333763675406,45.665333763675406 0 0 0 4.034389920584874,-10.642869115653347 h12.004166328120636 a9.108315894326587,9.108315894326587 0 0 0 8.811305593424633,-9.306322761594556 v-5.197680265784193 a9.083565035918088,9.083565035918088 0 0 0 -8.811305593424633,-9.306322761594556 " id="svg_3" class=""/>
</svg>
<div id="settingsTooltip"><%- lang('landing.settingsTooltip') %></div>
</button>
</div>
</div>
<div class="mediaDivider"></div>
<div id="externalMedia">
<div class="mediaContainer">
<a href="<%- lang('landing.mediaGitHubURL') %>" class="mediaURL" id="linkURL">
<svg id="linkSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
<g>
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
<path d="M66,73.88a3.85,3.85,0,0,0,1.73-.42,8.22,8.22,0,0,1-.94-3.76,8.36,8.36,0,0,1,8.35-8.35H95A8.35,8.35,0,1,1,95,78H85.8a21.51,21.51,0,0,1-6.65,8.72H95a17.07,17.07,0,0,0,0-34.15H75.13A17,17,0,0,0,58.59,73.88Z"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="<%- lang('landing.mediaTwitterURL') %>" class="mediaURL" id="twitterURL">
<svg id="twitterSVG" class="mediaSVG" viewBox="0 0 5000 4060" preserveAspectRatio="xMidYMid meet">
<g>
<path d="M1210 4048 c-350 -30 -780 -175 -1124 -378 -56 -33 -86 -57 -86 -68 0 -16 7 -17 83 -9 114 12 349 1 493 -22 295 -49 620 -180 843 -341 l54 -38 -49 -7 c-367 -49 -660 -256 -821 -582 -30 -61 -53 -120 -51 -130 3 -16 12 -17 73 -13 97 7 199 5 270 -4 l60 -9 -65 -22 c-341 -117 -609 -419 -681 -769 -18 -88 -26 -226 -13 -239 4 -3 32 7 63 22 68 35 198 77 266 86 28 4 58 9 68 12 10 2 -22 -34 -72 -82 -240 -232 -353 -532 -321 -852 15 -149 79 -347 133 -418 16 -20 17 -19 49 20 377 455 913 795 1491 945 160 41 346 74 485 86 l82 7 -7 -59 c-5 -33 -7 -117 -6 -189 2 -163 31 -286 103 -430 141 -285 422 -504 708 -550 112 -19 333 -19 442 0 180 30 335 108 477 239 l58 54 95 -24 c143 -36 286 -89 427 -160 70 -35 131 -60 135 -56 19 19 -74 209 -151 312 -50 66 -161 178 -216 217 l-30 22 73 -14 c111 -21 257 -63 353 -101 99 -39 99 -39 99 -19 0 57 -237 326 -412 468 l-88 71 6 51 c4 28 1 130 -5 226 -30 440 -131 806 -333 1202 -380 745 -1036 1277 -1823 1477 -243 62 -430 81 -786 78 -134 0 -291 -5 -349 -10z"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="<%- lang('landing.mediaInstagramURL') %>" class="mediaURL" id="instagramURL">
<svg id="instagramSVG" class="mediaSVG" viewBox="0 0 5040 5040">
<defs>
<radialGradient id="instaFill" cx="30%" cy="107%" r="150%">
<stop offset="0%" stop-color="#fdf497"/>
<stop offset="5%" stop-color="#fdf497"/>
<stop offset="45%" stop-color="#fd5949"/>
<stop offset="60%" stop-color="#d6249f"/>
<stop offset="90%" stop-color="#285AEB"/>
</radialGradient>
</defs>
<g>
<path d="M1390 5024 c-163 -9 -239 -19 -315 -38 -281 -70 -477 -177 -660 -361 -184 -184 -292 -380 -361 -660 -43 -171 -53 -456 -53 -1445 0 -989 10 -1274 53 -1445 69 -280 177 -476 361 -660 184 -184 380 -292 660 -361 171 -43 456 -53 1445 -53 989 0 1274 10 1445 53 280 69 476 177 660 361 184 184 292 380 361 660 43 171 53 456 53 1445 0 989 -10 1274 -53 1445 -69 280 -177 476 -361 660 -184 184 -380 292 -660 361 -174 44 -454 53 -1470 52 -599 0 -960 -5 -1105 -14z m2230 -473 c58 -6 141 -18 185 -27 397 -78 638 -318 719 -714 37 -183 41 -309 41 -1290 0 -981 -4 -1107 -41 -1290 -81 -395 -319 -633 -714 -714 -183 -37 -309 -41 -1290 -41 -981 0 -1107 4 -1290 41 -397 81 -636 322 -714 719 -33 166 -38 296 -43 1100 -5 796 3 1203 27 1380 67 489 338 758 830 825 47 7 162 15 255 20 250 12 1907 4 2035 -9z"/>
<path d="M2355 3819 c-307 -42 -561 -172 -780 -400 -244 -253 -359 -543 -359 -899 0 -361 116 -648 367 -907 262 -269 563 -397 937 -397 374 0 675 128 937 397 251 259 367 546 367 907 0 361 -116 648 -367 907 -197 203 -422 326 -690 378 -101 20 -317 27 -412 14z m400 -509 c275 -88 470 -284 557 -560 20 -65 23 -95 23 -230 0 -135 -3 -165 -23 -230 -88 -278 -284 -474 -562 -562 -65 -20 -95 -23 -230 -23 -135 0 -165 3 -230 23 -278 88 -474 284 -562 562 -20 65 -23 95 -23 230 0 135 3 165 23 230 73 230 219 403 427 507 134 67 212 83 390 79 111 -3 155 -8 210 -26z"/>
<path d="M3750 1473 c-29 -11 -66 -38 -106 -77 -70 -71 -94 -126 -94 -221 0 -95 24 -150 94 -221 72 -71 126 -94 225 -94 168 0 311 143 311 311 0 99 -23 154 -94 225 -43 42 -76 66 -110 77 -61 21 -166 21 -226 0z"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="<%- lang('landing.mediaYouTubeURL') %>" class="mediaURL" id="youtubeURL">
<svg id="youtubeSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
<g>
<path d="M84.8,69.52,65.88,79.76V59.27Zm23.65.59c0-5.14-.79-17.63-3.94-20.57S99,45.86,73.37,45.86s-28,.73-31.14,3.68S38.29,65,38.29,70.11s.79,17.63,3.94,20.57,5.52,3.68,31.14,3.68,28-.74,31.14-3.68,3.94-15.42,3.94-20.57"/>
</g>
</svg>
</a>
</div>
<div class="mediaContainer">
<a href="<%- lang('landing.mediaDiscordURL') %>" class="mediaURL" id="discordURL">
<svg id="discordSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
<g>
<path d="M81.23,78.48a6.14,6.14,0,1,1,6.14-6.14,6.14,6.14,0,0,1-6.14,6.14M60,78.48a6.14,6.14,0,1,1,6.14-6.14A6.14,6.14,0,0,1,60,78.48M104.41,73c-.92-7.7-8.24-22.9-8.24-22.9A43,43,0,0,0,88,45.59a17.88,17.88,0,0,0-8.38-1.27l-.13,1.06a23.52,23.52,0,0,1,5.8,1.95,87.59,87.59,0,0,1,8.17,4.87s-10.32-5.63-22.27-5.63a51.32,51.32,0,0,0-23.2,5.63,87.84,87.84,0,0,1,8.17-4.87,23.57,23.57,0,0,1,5.8-1.95l-.13-1.06a17.88,17.88,0,0,0-8.38,1.27,42.84,42.84,0,0,0-8.21,4.56S37.87,65.35,37,73s-.37,11.54-.37,11.54,4.22,5.68,9.9,7.14,7.7,1.47,7.7,1.47l3.75-4.68a21.22,21.22,0,0,1-4.65-2A24.47,24.47,0,0,1,47.93,82S61.16,88.4,70.68,88.4c10,0,22.75-6.44,22.75-6.44a24.56,24.56,0,0,1-5.35,4.56,21.22,21.22,0,0,1-4.65,2l3.75,4.68s2,0,7.7-1.47,9.89-7.14,9.89-7.14.55-3.85-.37-11.54"/>
</g>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="lower">
<div id="left">
<div class="bot_wrapper">
<div id="content">
<div id="server_status_wrapper">
<span class="bot_label" id="landingPlayerLabel"><%- lang('landing.serverStatus') %></span>
<span id="player_count"><%- lang('landing.serverStatusPlaceholder') %></span>
</div>
<div class="bot_divider"></div>
<div id="mojangStatusWrapper">
<span class="bot_label"><%- lang('landing.mojangStatus') %></span>
<span id="mojang_status_icon">&#8226;</span>
<div id="mojangStatusTooltip">
<div id="mojangStatusTooltipTitle"><%- lang('landing.mojangStatusTooltipTitle') %></div>
<div id="mojangStatusEssentialContainer">
<!-- Essential Mojang services are populated here. -->
</div>
<div id="mojangStatusNEContainer">
<div class="mojangStatusNEBar"></div>
<div id="mojangStatusNETitle"><%- lang('landing.mojangStatusNETitle') %></div>
<div class="mojangStatusNEBar"></div>
</div>
<div id="mojangStatusNonEssentialContainer">
<!-- Non Essential Mojang services are populated here. -->
</div>
</div>
</div>
</div>
</div>
</div>
<div id="center">
<div class="bot_wrapper">
<div id="content">
<button id="newsButton">
<!--<img src="assets/images/icons/arrow.svg" id="newsButtonSVG"/>-->
<div id="newsButtonAlert" style="display: none;"></div>
<svg id="newsButtonSVG" viewBox="0 0 24.87 13.97">
<defs>
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;}</style>
</defs>
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
&#10;<span id="newsButtonText"><%- lang('landing.newsButton') %></span>
</button>
</div>
</div>
</div>
<div id="right">
<div class="bot_wrapper">
<div id="launch_content">
<button id="launch_button"><%- lang('landing.launchButton') %></button>
<div class="bot_divider"></div>
<button id="server_selection_button" class="bot_label"><%- lang('landing.launchButtonPlaceholder') %></button>
</div>
<div id="launch_details">
<div id="launch_details_left">
<span id="launch_progress_label">0%</span>
<div class="bot_divider"></div>
</div>
<div id="launch_details_right">
<progress id="launch_progress" value="22" max="100"></progress>
<span id="launch_details_text" class="bot_label"><%- lang('landing.launchDetails') %></span>
</div>
</div>
</div>
</div>
</div>
<div id="newsContainer">
<div id="newsContent" article="-1" style="display: none;">
<div id="newsStatusContainer">
<div id="newsStatusContent">
<div id="newsTitleContainer">
<a id="newsArticleTitle" href="#">Lorem Ipsum</a>
</div>
<div id="newsMetaContainer">
<div id="newsArticleDateWrapper">
<span id="newsArticleDate">Mar 15, 44 BC, 9:14 AM</span>
</div>
<div id="newsArticleAuthorWrapper">
<span id="newsArticleAuthor">by Cicero</span>
</div>
<a href="#" id="newsArticleComments">0 Comments</a>
</div>
</div>
<div id="newsNavigationContainer">
<button id="newsNavigateLeft">
<svg id="newsNavigationLeftSVG" viewBox="0 0 24.87 13.97">
<defs>
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
</defs>
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</button>
<span id="newsNavigationStatus"><%- lang('landing.newsNavigationStatus', { currentPage: 1, totalPages: 1 }) %></span>
<button id="newsNavigateRight">
<svg id="newsNavigationRightSVG" viewBox="0 0 24.87 13.97">
<defs>
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
</defs>
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</button>
</div>
</div>
<div id="newsArticleContainer">
<div id="newsArticleContent">
<div id="newsArticleContentScrollable">
<!-- Article Content -->
</div>
</div>
</div>
</div>
<div id="newsErrorContainer">
<div id="newsErrorLoading">
<span id="nELoadSpan" class="newsErrorContent"><%- lang('landing.newsErrorLoadSpan') %></span>
</div>
<div id="newsErrorFailed" style="display: none;">
<span id="nEFailedSpan" class="newsErrorContent"><%- lang('landing.newsErrorFailedSpan') %></span>
<button id="newsErrorRetry"><%- lang('landing.newsErrorRetryButton') %></button>
</div>
<div id="newsErrorNone" style="display: none;">
<span id="nENoneSpan" class="newsErrorContent"><%- lang('landing.newsErrorNoneSpan') %></span>
</div>
</div>
</div>
<script src="./assets/js/scripts/landing.js"></script>
</div>

View File

@ -1,16 +1,22 @@
<div id="loginContainer">
<div id="loginContainer" style="display: none;">
<div id="loginCancelContainer" style="display: none;">
<button id="loginCancelButton">
<div id="loginCancelIcon">X</div>
<span id="loginCancelText"><%- lang('login.loginCancelText') %></span>
</button>
</div>
<div id="loginContent">
<div id='loginForm'>
<img id="loginImageSeal" src="assets/images/WesterosSealCircle.png"/>
<span class="loginSpan" id="loginSubheader">MEMBER LOGIN</span>
<form id="loginForm">
<img id="loginImageSeal" src="assets/images/SealCircle.png"/>
<span id="loginSubheader"><%- lang('login.loginSubheader') %></span>
<div class="loginFieldContainer">
<svg id="profileSVG" class="loginSVG" viewBox="40 37 65.36 61.43">
<g>
<path d="M86.77,58.12A13.79,13.79,0,1,0,73,71.91,13.79,13.79,0,0,0,86.77,58.12M97,103.67a3.41,3.41,0,0,0,3.39-3.84,27.57,27.57,0,0,0-54.61,0,3.41,3.41,0,0,0,3.39,3.84Z"/>
</g>
</svg>
<span class="loginErrorSpan" id="loginEmailError">* Invalid Value</span>
<input id="loginUsername" class="loginField" type="text" placeholder="EMAIL"/>
<span class="loginErrorSpan" id="loginEmailError"><%- lang('login.loginEmailError') %></span>
<input id="loginUsername" class="loginField" type="text" placeholder="<%- lang('login.loginEmailPlaceholder') %>"/>
</div>
<div class="loginFieldContainer">
<svg id="lockSVG" class="loginSVG" viewBox="40 32 60.36 70.43">
@ -18,22 +24,22 @@
<path d="M86.16,54a16.38,16.38,0,1,0-32,0H44V102.7H96V54Zm-25.9-3.39a9.89,9.89,0,1,1,19.77,0A9.78,9.78,0,0,1,79.39,54H60.89A9.78,9.78,0,0,1,60.26,50.59ZM70,96.2a6.5,6.5,0,0,1-6.5-6.5,6.39,6.39,0,0,1,3.1-5.4V67h6.5V84.11a6.42,6.42,0,0,1,3.39,5.6A6.5,6.5,0,0,1,70,96.2Z"/>
</g>
</svg>
<span class="loginErrorSpan" id="loginPasswordError">* Required</span>
<input id="loginPassword" class="loginField" type="password" placeholder="PASSWORD"/>
<span class="loginErrorSpan" id="loginPasswordError"><%- lang('login.loginPasswordError') %></span>
<input id="loginPassword" class="loginField" type="password" placeholder="<%- lang('login.loginPasswordPlaceholder') %>"/>
</div>
<div id="loginOptions">
<span class="loginSpanDim">
<a href="https://help.mojang.com/customer/en/portal/articles/329524-change-or-forgot-password">forgot password?</a>
<a href="<%- lang('login.loginForgotPasswordLink') %>"><%- lang('login.loginForgotPasswordText') %></a>
</span>
<label id="checkmarkContainer">
<input id="loginRememberOption" type="checkbox" checked>
<span id="loginRememberText" class="loginSpanDim">remember me?</span>
<span id="loginRememberText" class="loginSpanDim"><%- lang('login.loginRememberMeText') %></span>
<span class="loginCheckmark"></span>
</label>
</div>
<button id="loginButton" disabled>
<div id="loginButtonContent">
LOGIN
<%- lang('login.loginButtonText') %>
<svg id="loginSVG" viewBox="0 0 24.87 13.97">
<defs>
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
@ -48,220 +54,12 @@
</button>
<div id="loginDisclaimer">
<span class="loginSpanDim" id="loginRegisterSpan">
<a href="https://minecraft.net/en-us/store/minecraft/">Need an Account?</a>
<a href="<%- lang('login.loginNeedAccountLink') %>"><%- lang('login.loginNeedAccountText') %></a>
</span>
<p class="loginDisclaimerText">Your password is sent directly to mojang and never stored.</p>
<p class="loginDisclaimerText">WesterosCraft is not affiliated with Mojang AB.</p>
<p class="loginDisclaimerText"><%- lang('login.loginPasswordDisclaimer1') %></p>
<p class="loginDisclaimerText"><%- lang('login.loginPasswordDisclaimer2', { appName: lang('app.title') }) %></p>
</div>
</form>
</div>
</div>
<div id="loginErrorContainer">
<div id="loginErrorContent">
<span id="loginErrorTitle">LOGIN FAILED:<br>INVALID CREDENTIALS</span>
<span id="loginErrorDesc">Either the email or password you supplied is invalid. Please ensure everything is correct and try again.</span>
<button id="loginErrorAcknowledge">Try Again</button>
</div>
</div>
<script type="application/javascript">
//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
// Validation Regexes.
const validUsername = /^[a-zA-Z0-9_]{1,16}$/
const basicEmail = /^\S+@\S+\.\S+$/
// DOM cache.
const loginContainer = document.getElementById('loginContainer')
const loginErrorTitle = document.getElementById('loginErrorTitle')
const loginErrorDesc = document.getElementById('loginErrorDesc')
const loginErrorAcknowledge = document.getElementById('loginErrorAcknowledge')
const loginEmailError = document.getElementById('loginEmailError')
const loginUsername = document.getElementById('loginUsername')
const loginPasswordError = document.getElementById('loginPasswordError')
const loginPassword = document.getElementById('loginPassword')
const checkmarkContainer = document.getElementById('checkmarkContainer')
const loginRememberOption = document.getElementById('loginRememberOption')
const loginButton = document.getElementById('loginButton')
// Control variables.
let lu = false, lp = false
// Show error element.
function showError(element, value){
element.innerHTML = value
element.style.opacity = 1
}
// Shake error element.
function shakeError(element){
if(element.style.opacity == 1){
element.classList.remove('shake')
void element.offsetWidth
element.classList.add('shake')
}
}
// Validate email field is neither empty nor invalid.
function validateEmail(value){
if(value){
if(!basicEmail.test(value) && !validUsername.test(value)){
showError(loginEmailError, '* Invalid Value')
loginDisabled(true)
lu = false
} else {
loginEmailError.style.opacity = 0
lu = true
if(lp){
loginDisabled(false)
}
}
} else {
lu = false
showError(loginEmailError, '* Required')
loginDisabled(true)
}
}
// Validate password field is not empty.
function validatePassword(value){
if(value){
loginPasswordError.style.opacity = 0
lp = true
if(lu){
loginDisabled(false)
}
} else {
lp = false
showError(loginPasswordError, '* Required')
loginDisabled(true)
}
}
// Emphasize errors with shake when focus is lost.
loginUsername.addEventListener('focusout', (e) => {
validateEmail(e.target.value)
shakeError(loginEmailError)
})
loginPassword.addEventListener('focusout', (e) => {
validatePassword(e.target.value)
shakeError(loginPasswordError)
})
// Validate input for each field.
loginUsername.addEventListener('input', (e) => {
validateEmail(e.target.value)
})
loginPassword.addEventListener('input', (e) => {
validatePassword(e.target.value)
})
// Enable or disable login button.
function loginDisabled(v){
if(loginButton.disabled !== v){
loginButton.disabled = v
}
}
// Enable or disable loading elements.
function loginLoading(v){
if(v){
loginButton.setAttribute('loading', v)
loginButton.innerHTML = loginButton.innerHTML.replace('LOGIN', 'LOGGING IN')
} else {
loginButton.removeAttribute('loading')
loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'LOGIN')
}
}
// Disable or enable login form.
function formDisabled(v){
loginDisabled(v)
loginUsername.disabled = v
loginPassword.disabled = v
if(v){
checkmarkContainer.setAttribute('disabled', v)
} else {
checkmarkContainer.removeAttribute('disabled')
}
loginRememberOption.disabled = v
}
function resolveError(err){
if(err.cause != null && err.cause === 'UserMigratedException') {
return {
title: 'Error During Login:<br>Invalid Credentials',
desc: 'You\'ve attempted to login with a migrated account. Try again using the account email as the username.'
}
} else {
if(err.error != null){
if(err.error === 'ForbiddenOperationException'){
if(err.errorMessage != null){
if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){
return {
title: 'Error During Login:<br>Invalid Credentials',
desc: 'The email or password you\'ve entered is incorrect. Please try again.'
}
} else if(err.errorMessage === 'Invalid credentials.'){
return {
title: 'Error During Login:<br>Too Many Attempts',
desc: 'There have been too many login attempts with this account recently. Please try again later.'
}
}
}
}
}
}
return {
title: err.error,
desc: err.errorMessage
}
}
loginButton.addEventListener('click', () => {
// Disable form.
formDisabled(true)
// Show loading stuff.
loginLoading(true)
AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => {
loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'SUCCESS')
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
console.log(value)
}).catch((err) => {
loginLoading(false)
$('#loginErrorContainer').css('display', 'flex').hide().fadeIn(250)
loginContainer.setAttribute('error', true)
const errF = resolveError(err)
loginErrorTitle.innerHTML = errF.title
loginErrorDesc.innerHTML = errF.desc
console.log(err)
})
// Temp for debugging, use procedure with real code.
setTimeout(() => {
loginButton.innerHTML = loginButton.innerHTML.replace('LOGGING IN', 'SUCCESS')
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
}, 2500)
})
loginErrorAcknowledge.addEventListener('click', () => {
formDisabled(false)
loginContainer.removeAttribute('error', false)
$('#loginErrorContainer').fadeOut(250)
})
</script>
<!-- Will reuse this down the line, then it will be removed from this file. -->
<!--<div id="loginLoading">
<div id="loginLoadingContent">
<div id="loadSpinnerContainer">
<img id="loadCenterImage" src="assets/images/westeroscraftlogo1.png">
<img id="loadSpinnerImage" class="rotating" src="assets/images/westeroscraftlogo2.png">
</div>
<span id="loadDescText">LOGGING IN</span>
</div>
</div>-->
<script src="./assets/js/scripts/login.js"></script>
</div>

34
app/loginOptions.ejs Normal file
View File

@ -0,0 +1,34 @@
<div id="loginOptionsContainer" style="display: none;">
<div id="loginOptionsContent">
<div class="loginOptionsMainContent">
<h2><%- lang('loginOptions.loginOptionsTitle') %></h2>
<div class="loginOptionActions">
<div class="loginOptionButtonContainer">
<button id="loginOptionMicrosoft" class="loginOptionButton">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
<path fill="#f35325" d="M1 1h10v10H1z" />
<path fill="#81bc06" d="M12 1h10v10H12z" />
<path fill="#05a6f0" d="M1 12h10v10H1z" />
<path fill="#ffba08" d="M12 12h10v10H12z" />
</svg>
<span><%- lang('loginOptions.loginWithMicrosoft') %></span>
</button>
</div>
<div class="loginOptionButtonContainer">
<button id="loginOptionMojang" class="loginOptionButton">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 9.677 9.667">
<path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
<path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
<path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
</svg>
<span><%- lang('loginOptions.loginWithMojang') %></span>
</button>
</div>
</div>
<div id="loginOptionCancelContainer" style="display: none;">
<button id="loginOptionCancelButton"><%- lang('loginOptions.cancelButton') %></button>
</div>
</div>
</div>
<script src="./assets/js/scripts/loginOptions.js"></script>
</div>

41
app/overlay.ejs Normal file
View File

@ -0,0 +1,41 @@
<div id="overlayContainer" style="display: none;">
<div id="serverSelectContent" style="display: none;">
<span id="serverSelectHeader"><%- lang('overlay.serverSelectHeader') %></span>
<div id="serverSelectList">
<div id="serverSelectListScrollable">
<!-- Server listings populated here. -->
</div>
</div>
<div id="serverSelectActions">
<button id="serverSelectConfirm" class="overlayKeybindEnter" type="submit"><%- lang('overlay.serverSelectConfirm') %></button>
<div id="serverSelectCancelWrapper">
<button id="serverSelectCancel" class="overlayKeybindEsc"><%- lang('overlay.serverSelectCancel') %></button>
</div>
</div>
</div>
<div id="accountSelectContent" style="display: none;">
<span id="accountSelectHeader"><%- lang('overlay.accountSelectHeader') %></span>
<div id="accountSelectList">
<div id="accountSelectListScrollable">
<!-- Accounts populated here. -->
</div>
</div>
<div id="accountSelectActions">
<button id="accountSelectConfirm" class="overlayKeybindEnter" type="submit"><%- lang('overlay.accountSelectConfirm') %></button>
<div id="accountSelectCancelWrapper">
<button id="accountSelectCancel" class="overlayKeybindEsc"><%- lang('overlay.accountSelectCancel') %></button>
</div>
</div>
</div>
<div id="overlayContent">
<span id="overlayTitle">Lorem Ipsum:<br>Finis Illud</span>
<span id="overlayDesc">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud..</span>
<div id="overlayActionContainer">
<button id="overlayAcknowledge" class="overlayKeybindEnter">Conare Iterum</button>
<div id="overlayDismissWrapper">
<button id="overlayDismiss" style="display: none;" class="overlayKeybindEsc">Dismiss</button>
</div>
</div>
</div>
<script src="./assets/js/scripts/overlay.js"></script>
</div>

393
app/settings.ejs Normal file
View File

@ -0,0 +1,393 @@
<div id="settingsContainer" style="display: none;">
<div id="settingsContainerLeft">
<div id="settingsNavContainer">
<div id="settingsNavHeader">
<span id="settingsNavHeaderText"><%- lang('settings.navHeaderText') %></span>
</div>
<div id="settingsNavItemsContainer">
<div id="settingsNavItemsContent">
<button class="settingsNavItem" rSc="settingsTabAccount" id="settingsNavAccount" selected><%- lang('settings.navAccount') %></button>
<button class="settingsNavItem" rSc="settingsTabMinecraft"><%- lang('settings.navMinecraft') %></button>
<button class="settingsNavItem" rSc="settingsTabMods"><%- lang('settings.navMods') %></button>
<button class="settingsNavItem" rSc="settingsTabJava"><%- lang('settings.navJava') %></button>
<button class="settingsNavItem" rSc="settingsTabLauncher"><%- lang('settings.navLauncher') %></button>
<div class="settingsNavSpacer"></div>
<button class="settingsNavItem" rSc="settingsTabAbout"><%- lang('settings.navAbout') %></button>
<button class="settingsNavItem" rSc="settingsTabUpdate" id="settingsNavUpdate"><%- lang('settings.navUpdates') %></button>
<div id="settingsNavContentBottom">
<div class="settingsNavDivider"></div>
<button id="settingsNavDone"><%- lang('settings.navDone') %></button>
</div>
</div>
</div>
</div>
</div>
<div id="settingsContainerRight">
<div id="settingsTabAccount" class="settingsTab">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText"><%- lang('settings.tabAccountHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.tabAccountHeaderDesc') %></span>
</div>
<div class="settingsAuthAccountTypeContainer">
<div class="settingsAuthAccountTypeHeader">
<div class="settingsAuthAccountTypeHeaderLeft">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 23 23">
<path fill="#f35325" d="M1 1h10v10H1z" />
<path fill="#81bc06" d="M12 1h10v10H12z" />
<path fill="#05a6f0" d="M1 12h10v10H1z" />
<path fill="#ffba08" d="M12 12h10v10H12z" />
</svg>
<span><%- lang('settings.microsoftAccount') %></span>
</div>
<div class="settingsAuthAccountTypeHeaderRight">
<button class="settingsAddAuthAccount" id="settingsAddMicrosoftAccount"><%- lang('settings.addMicrosoftAccount') %></button>
</div>
</div>
<div class="settingsCurrentAccounts" id="settingsCurrentMicrosoftAccounts">
<!-- Microsoft auth accounts populated here. -->
</div>
</div>
<div class="settingsAuthAccountTypeContainer">
<div class="settingsAuthAccountTypeHeader">
<div class="settingsAuthAccountTypeHeaderLeft">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 9.677 9.667">
<path d="M-26.332-12.098h2.715c-1.357.18-2.574 1.23-2.715 2.633z" fill="#fff" />
<path d="M2.598.022h7.07L9.665 7c-.003 1.334-1.113 2.46-2.402 2.654H0V2.542C.134 1.2 1.3.195 2.598.022z" fill="#db2331" />
<path d="M1.54 2.844c.314-.76 1.31-.46 1.954-.528.785-.083 1.503.272 2.1.758l.164-.9c.327.345.587.756.964 1.052.28.254.655-.342.86-.013.42.864.408 1.86.54 2.795l-.788-.373C6.9 4.17 5.126 3.052 3.656 3.685c-1.294.592-1.156 2.65.06 3.255 1.354.703 2.953.51 4.405.292-.07.42-.34.87-.834.816l-4.95.002c-.5.055-.886-.413-.838-.89l.04-4.315z" fill="#fff" />
</svg>
<span><%- lang('settings.mojangAccount') %></span>
</div>
<div class="settingsAuthAccountTypeHeaderRight">
<button class="settingsAddAuthAccount" id="settingsAddMojangAccount"><%- lang('settings.addMojangAccount') %></button>
</div>
</div>
<div class="settingsCurrentAccounts" id="settingsCurrentMojangAccounts">
<!-- Mojang auth accounts populated here. -->
</div>
</div>
</div>
<div id="settingsTabMinecraft" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText"><%- lang('settings.minecraftTabHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.minecraftTabHeaderDesc') %></span>
</div>
<div id="settingsGameResolutionContainer">
<span class="settingsFieldTitle"><%- lang('settings.gameResolutionTitle') %></span>
<div id="settingsGameResolutionContent">
<input type="number" id="settingsGameWidth" min="0" cValue="GameWidth">
<div id="settingsGameResolutionCross">&#10006;</div>
<input type="number" id="settingsGameHeight" min="0" cValue="GameHeight">
</div>
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle"><%- lang('settings.launchFullscreenTitle') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
<input type="checkbox" cValue="Fullscreen">
<span class="toggleSwitchSlider"></span>
</label>
</div>
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle"><%- lang('settings.autoConnectTitle') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
<input type="checkbox" cValue="AutoConnect">
<span class="toggleSwitchSlider"></span>
</label>
</div>
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle"><%- lang('settings.launchDetachedTitle') %></span>
<span class="settingsFieldDesc"><%- lang('settings.launchDetachedDesc') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
<input type="checkbox" cValue="LaunchDetached">
<span class="toggleSwitchSlider"></span>
</label>
</div>
</div>
</div>
<div id="settingsTabMods" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText"><%- lang('settings.tabModsHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.tabModsHeaderDesc') %></span>
</div>
<div class="settingsSelServContainer">
<div class="settingsSelServContent">
</div>
<div class="settingsSwitchServerContainer">
<div class="settingsSwitchServerContent">
<button class="settingsSwitchServerButton"><%- lang('settings.switchServerButton') %></button>
</div>
</div>
</div>
<div id="settingsModsContainer">
<div id="settingsReqModsContainer">
<div class="settingsModsHeader"><%- lang('settings.requiredMods') %></div>
<div id="settingsReqModsContent">
</div>
</div>
<div id="settingsOptModsContainer">
<div class="settingsModsHeader"><%- lang('settings.optionalMods') %></div>
<div id="settingsOptModsContent">
</div>
</div>
<div id="settingsDropinModsContainer">
<div class="settingsModsHeader"><%- lang('settings.dropinMods') %></div>
<button id="settingsDropinFileSystemButton"><%- lang('settings.addMods') %> <span id="settingsDropinRefreshNote"><%- lang('settings.dropinRefreshNote') %></span></button>
<div id="settingsDropinModsContent">
</div>
</div>
<div id="settingsShadersContainer">
<div class="settingsModsHeader"><%- lang('settings.shaderpacks') %></div>
<div id="settingsShaderpackDesc"><%- lang('settings.shaderpackDesc') %></div>
<div id="settingsShaderpackWrapper">
<button id="settingsShaderpackButton"> + </button>
<div class="settingsSelectContainer">
<div class="settingsSelectSelected" id="settingsShadersSelected"><%- lang('settings.selectShaderpack') %></div>
<div class="settingsSelectOptions" id="settingsShadersOptions" hidden>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="settingsTabJava" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText"><%- lang('settings.tabJavaHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.tabJavaHeaderDesc') %></span>
</div>
<div class="settingsSelServContainer">
<div class="settingsSelServContent">
</div>
<div class="settingsSwitchServerContainer">
<div class="settingsSwitchServerContent">
<button class="settingsSwitchServerButton"><%- lang('settings.switchServerButton') %></button>
</div>
</div>
</div>
<div id="settingsMemoryContainer">
<div id="settingsMemoryTitle"><%- lang('settings.memoryTitle') %></div>
<div id="settingsMemoryContent">
<div id="settingsMemoryContentLeft">
<div class="settingsMemoryContentItem">
<span class="settingsMemoryHeader"><%- lang('settings.maxRAM') %></span>
<div class="settingsMemoryActionContainer">
<div id="settingsMaxRAMRange" class="rangeSlider" cValue="MaxRAM" serverDependent min="3" max="8" value="3" step="0.5">
<div class="rangeSliderBar"></div>
<div class="rangeSliderTrack"></div>
</div>
<span id="settingsMaxRAMLabel" class="settingsMemoryLabel">3G</span>
</div>
</div>
<div class="settingsMemoryContentItem">
<span class="settingsMemoryHeader"><%- lang('settings.minRAM') %></span>
<div class="settingsMemoryActionContainer">
<div id="settingsMinRAMRange" class="rangeSlider" cValue="MinRAM" serverDependent min="3" max="8" value="3" step="0.5">
<div class="rangeSliderBar"></div>
<div class="rangeSliderTrack"></div>
</div>
<span id="settingsMinRAMLabel" class="settingsMemoryLabel">3G</span>
</div>
</div>
<div id="settingsMemoryDesc"><%- lang('settings.memoryDesc') %></div>
</div>
<div id="settingsMemoryContentRight">
<div id="settingsMemoryStatus">
<div class="settingsMemoryStatusContainer">
<span class="settingsMemoryStatusTitle"><%- lang('settings.memoryTotalTitle') %></span>
<span id="settingsMemoryTotal" class="settingsMemoryStatusValue">16G</span>
</div>
<div class="settingsMemoryStatusContainer">
<span class="settingsMemoryStatusTitle"><%- lang('settings.memoryAvailableTitle') %></span>
<span id="settingsMemoryAvail" class="settingsMemoryStatusValue">7.3G</span>
</div>
</div>
</div>
</div>
</div>
<div class="settingsFileSelContainer">
<div class="settingsFileSelTitle"><%- lang('settings.javaExecutableTitle') %></div>
<div class="settingsFileSelContent">
<div id="settingsJavaExecDetails"><!-- Invalid Selection --></div>
<div class="settingsFileSelActions">
<div class="settingsFileSelIcon">
<svg class="settingsFileSelSVG" x="0px" y="0px" viewBox="0 0 305.001 305.001">
<g>
<path d="M150.99,56.513c-14.093,9.912-30.066,21.147-38.624,39.734c-14.865,32.426,30.418,67.798,32.353,69.288c0.45,0.347,0.988,0.519,1.525,0.519c0.57,0,1.141-0.195,1.605-0.583c0.899-0.752,1.154-2.029,0.614-3.069c-0.164-0.316-16.418-31.888-15.814-54.539c0.214-7.888,11.254-16.837,22.942-26.312c10.705-8.678,22.839-18.514,29.939-30.02c15.586-25.327-1.737-50.231-1.914-50.479c-0.688-0.966-1.958-1.317-3.044-0.84c-1.085,0.478-1.686,1.652-1.438,2.811c0.035,0.164,3.404,16.633-5.97,33.6C169.301,43.634,160.816,49.603,150.99,56.513z"></path>
<path d="M210.365,67.682c0.994-0.749,1.286-2.115,0.684-3.205c-0.602-1.09-1.913-1.571-3.077-1.129c-2.394,0.91-58.627,22.585-58.627,48.776c0,18.053,7.712,27.591,13.343,34.556c2.209,2.731,4.116,5.09,4.744,7.104c1.769,5.804-2.422,16.294-4.184,19.846c-0.508,1.022-0.259,2.259,0.605,3.005c0.467,0.403,1.05,0.607,1.634,0.607c0.497,0,0.996-0.148,1.427-0.448c0.967-0.673,23.63-16.696,19.565-36.001c-1.514-7.337-5.12-12.699-8.302-17.43c-4.929-7.329-8.489-12.624-3.088-22.403C181.419,89.556,210.076,67.899,210.365,67.682z"></path>
<path d="M63.99,177.659c-0.964,2.885-0.509,5.75,1.315,8.283c6.096,8.462,27.688,13.123,60.802,13.123c0.002,0,0.003,0,0.004,0c4.487,0,9.224-0.088,14.076-0.262c52.943-1.896,72.58-18.389,73.39-19.09c0.883-0.764,1.119-2.037,0.57-3.067c-0.549-1.029-1.733-1.546-2.864-1.235c-18.645,5.091-53.463,6.898-77.613,6.898c-27.023,0-40.785-1.946-44.154-3.383c1.729-2.374,12.392-6.613,25.605-9.212c1.263-0.248,2.131-1.414,2.006-2.695c-0.125-1.281-1.201-2.258-2.488-2.258C106.893,164.762,68.05,165.384,63.99,177.659z"></path>
<path d="M241.148,160.673c-10.92,0-21.275,5.472-21.711,5.705c-1.01,0.541-1.522,1.699-1.245,2.811c0.278,1.111,1.277,1.892,2.423,1.893c0.232,0.001,23.293,0.189,25.382,13.365c1.85,11.367-21.82,29.785-31.097,35.923c-1.002,0.663-1.391,1.945-0.926,3.052c0.395,0.943,1.314,1.533,2.304,1.533c0.173,0,0.348-0.018,0.522-0.056c2.202-0.47,53.855-11.852,48.394-41.927C261.862,164.541,250.278,160.673,241.148,160.673z"></path>
<path d="M205.725,216.69c0.18-0.964-0.221-1.944-1.023-2.506l-12.385-8.675c-0.604-0.423-1.367-0.556-2.076-0.368c-0.129,0.034-13.081,3.438-31.885,5.526c-7.463,0.837-15.822,1.279-24.175,1.279c-18.799,0-31.091-2.209-32.881-3.829c-0.237-0.455-0.162-0.662-0.12-0.777c0.325-0.905,2.068-1.98,3.192-2.405c1.241-0.459,1.91-1.807,1.524-3.073c-0.385-1.266-1.69-2.012-2.978-1.702c-12.424,2.998-18.499,7.191-18.057,12.461c0.785,9.343,22.428,14.139,40.725,15.408c2.631,0.18,5.477,0.272,8.456,0.272c0.002,0,0.003,0,0.005,0c30.425,0,69.429-9.546,69.819-9.643C204.818,218.423,205.544,217.654,205.725,216.69z"></path>
<path d="M112.351,236.745c0.938-0.611,1.354-1.77,1.021-2.838c-0.332-1.068-1.331-1.769-2.453-1.755c-1.665,0.044-16.292,0.704-17.316,10.017c-0.31,2.783,0.487,5.325,2.37,7.556c5.252,6.224,19.428,9.923,43.332,11.31c2.828,0.169,5.7,0.254,8.539,0.254c30.39,0,50.857-9.515,51.714-9.92c0.831-0.393,1.379-1.209,1.428-2.127c0.049-0.917-0.409-1.788-1.193-2.267l-15.652-9.555c-0.543-0.331-1.193-0.441-1.813-0.314c-0.099,0.021-10.037,2.082-25.035,4.119c-2.838,0.385-6.392,0.581-10.562,0.581c-14.982,0-31.646-2.448-34.842-4.05C111.843,237.455,111.902,237.075,112.351,236.745z"></path>
<path d="M133.681,290.018c69.61-0.059,106.971-12.438,114.168-20.228c2.548-2.757,2.823-5.366,2.606-7.07c-0.535-4.194-4.354-6.761-4.788-7.04c-1.045-0.672-2.447-0.496-3.262,0.444c-0.813,0.941-0.832,2.314-0.016,3.253c0.439,0.565,0.693,1.51-0.591,2.795c-2.877,2.687-31.897,10.844-80.215,13.294c-6.619,0.345-13.561,0.519-20.633,0.52c-43.262,0-74.923-5.925-79.079-9.379c1.603-2.301,12.801-5.979,24.711-8.058c1.342-0.234,2.249-1.499,2.041-2.845c-0.208-1.346-1.449-2.273-2.805-2.096c-0.336,0.045-1.475,0.115-2.796,0.195c-19.651,1.2-42.36,3.875-43.545,13.999c-0.36,3.086,0.557,5.886,2.726,8.324c5.307,5.963,20.562,13.891,91.475,13.891C133.68,290.018,133.68,290.018,133.681,290.018z"></path>
<path d="M261.522,271.985c-0.984-0.455-2.146-0.225-2.881,0.567c-0.103,0.11-10.568,11.054-42.035,17.48c-12.047,2.414-34.66,3.638-67.211,3.638c-32.612,0-63.643-1.283-63.953-1.296c-1.296-0.063-2.405,0.879-2.581,2.155c-0.177,1.276,0.645,2.477,1.897,2.775c0.323,0.077,32.844,7.696,77.31,7.696c21.327,0,42.08-1.733,61.684-5.151c36.553-6.408,39.112-24.533,39.203-25.301C263.082,273.474,262.504,272.44,261.522,271.985z"></path>
</g>
</svg>
</div>
<input class="settingsFileSelVal" id="settingsJavaExecVal" type="text" value="null" cValue="JavaExecutable" serverDependent disabled>
<button class="settingsFileSelButton" id="settingsJavaExecSel" dialogTitle="<%- lang('settings.javaExecSelDialogTitle') %>" dialogDirectory="false"><%- lang('settings.javaExecSelButtonText') %></button>
</div>
</div>
<div class="settingsFileSelDesc"><%- lang('settings.javaExecDesc') %> <strong id="settingsJavaReqDesc"><!-- Requires Java 8 x64. --></strong><br><%- lang('settings.javaPathDesc', {'pathSuffix': `bin${process.platform === 'win32' ? '\\javaw.exe' : '/java'}`}) %></div>
</div>
<div id="settingsJVMOptsContainer">
<div id="settingsJVMOptsTitle"><%- lang('settings.jvmOptsTitle') %></div>
<div id="settingsJVMOptsContent">
<div class="settingsFileSelIcon">
<svg class="settingsFileSelSVG" x="0px" y="0px" viewBox="0 0 305.001 305.001">
<g>
<path d="M150.99,56.513c-14.093,9.912-30.066,21.147-38.624,39.734c-14.865,32.426,30.418,67.798,32.353,69.288c0.45,0.347,0.988,0.519,1.525,0.519c0.57,0,1.141-0.195,1.605-0.583c0.899-0.752,1.154-2.029,0.614-3.069c-0.164-0.316-16.418-31.888-15.814-54.539c0.214-7.888,11.254-16.837,22.942-26.312c10.705-8.678,22.839-18.514,29.939-30.02c15.586-25.327-1.737-50.231-1.914-50.479c-0.688-0.966-1.958-1.317-3.044-0.84c-1.085,0.478-1.686,1.652-1.438,2.811c0.035,0.164,3.404,16.633-5.97,33.6C169.301,43.634,160.816,49.603,150.99,56.513z"></path>
<path d="M210.365,67.682c0.994-0.749,1.286-2.115,0.684-3.205c-0.602-1.09-1.913-1.571-3.077-1.129c-2.394,0.91-58.627,22.585-58.627,48.776c0,18.053,7.712,27.591,13.343,34.556c2.209,2.731,4.116,5.09,4.744,7.104c1.769,5.804-2.422,16.294-4.184,19.846c-0.508,1.022-0.259,2.259,0.605,3.005c0.467,0.403,1.05,0.607,1.634,0.607c0.497,0,0.996-0.148,1.427-0.448c0.967-0.673,23.63-16.696,19.565-36.001c-1.514-7.337-5.12-12.699-8.302-17.43c-4.929-7.329-8.489-12.624-3.088-22.403C181.419,89.556,210.076,67.899,210.365,67.682z"></path>
<path d="M63.99,177.659c-0.964,2.885-0.509,5.75,1.315,8.283c6.096,8.462,27.688,13.123,60.802,13.123c0.002,0,0.003,0,0.004,0c4.487,0,9.224-0.088,14.076-0.262c52.943-1.896,72.58-18.389,73.39-19.09c0.883-0.764,1.119-2.037,0.57-3.067c-0.549-1.029-1.733-1.546-2.864-1.235c-18.645,5.091-53.463,6.898-77.613,6.898c-27.023,0-40.785-1.946-44.154-3.383c1.729-2.374,12.392-6.613,25.605-9.212c1.263-0.248,2.131-1.414,2.006-2.695c-0.125-1.281-1.201-2.258-2.488-2.258C106.893,164.762,68.05,165.384,63.99,177.659z"></path>
<path d="M241.148,160.673c-10.92,0-21.275,5.472-21.711,5.705c-1.01,0.541-1.522,1.699-1.245,2.811c0.278,1.111,1.277,1.892,2.423,1.893c0.232,0.001,23.293,0.189,25.382,13.365c1.85,11.367-21.82,29.785-31.097,35.923c-1.002,0.663-1.391,1.945-0.926,3.052c0.395,0.943,1.314,1.533,2.304,1.533c0.173,0,0.348-0.018,0.522-0.056c2.202-0.47,53.855-11.852,48.394-41.927C261.862,164.541,250.278,160.673,241.148,160.673z"></path>
<path d="M205.725,216.69c0.18-0.964-0.221-1.944-1.023-2.506l-12.385-8.675c-0.604-0.423-1.367-0.556-2.076-0.368c-0.129,0.034-13.081,3.438-31.885,5.526c-7.463,0.837-15.822,1.279-24.175,1.279c-18.799,0-31.091-2.209-32.881-3.829c-0.237-0.455-0.162-0.662-0.12-0.777c0.325-0.905,2.068-1.98,3.192-2.405c1.241-0.459,1.91-1.807,1.524-3.073c-0.385-1.266-1.69-2.012-2.978-1.702c-12.424,2.998-18.499,7.191-18.057,12.461c0.785,9.343,22.428,14.139,40.725,15.408c2.631,0.18,5.477,0.272,8.456,0.272c0.002,0,0.003,0,0.005,0c30.425,0,69.429-9.546,69.819-9.643C204.818,218.423,205.544,217.654,205.725,216.69z"></path>
<path d="M112.351,236.745c0.938-0.611,1.354-1.77,1.021-2.838c-0.332-1.068-1.331-1.769-2.453-1.755c-1.665,0.044-16.292,0.704-17.316,10.017c-0.31,2.783,0.487,5.325,2.37,7.556c5.252,6.224,19.428,9.923,43.332,11.31c2.828,0.169,5.7,0.254,8.539,0.254c30.39,0,50.857-9.515,51.714-9.92c0.831-0.393,1.379-1.209,1.428-2.127c0.049-0.917-0.409-1.788-1.193-2.267l-15.652-9.555c-0.543-0.331-1.193-0.441-1.813-0.314c-0.099,0.021-10.037,2.082-25.035,4.119c-2.838,0.385-6.392,0.581-10.562,0.581c-14.982,0-31.646-2.448-34.842-4.05C111.843,237.455,111.902,237.075,112.351,236.745z"></path>
<path d="M133.681,290.018c69.61-0.059,106.971-12.438,114.168-20.228c2.548-2.757,2.823-5.366,2.606-7.07c-0.535-4.194-4.354-6.761-4.788-7.04c-1.045-0.672-2.447-0.496-3.262,0.444c-0.813,0.941-0.832,2.314-0.016,3.253c0.439,0.565,0.693,1.51-0.591,2.795c-2.877,2.687-31.897,10.844-80.215,13.294c-6.619,0.345-13.561,0.519-20.633,0.52c-43.262,0-74.923-5.925-79.079-9.379c1.603-2.301,12.801-5.979,24.711-8.058c1.342-0.234,2.249-1.499,2.041-2.845c-0.208-1.346-1.449-2.273-2.805-2.096c-0.336,0.045-1.475,0.115-2.796,0.195c-19.651,1.2-42.36,3.875-43.545,13.999c-0.36,3.086,0.557,5.886,2.726,8.324c5.307,5.963,20.562,13.891,91.475,13.891C133.68,290.018,133.68,290.018,133.681,290.018z"></path>
<path d="M261.522,271.985c-0.984-0.455-2.146-0.225-2.881,0.567c-0.103,0.11-10.568,11.054-42.035,17.48c-12.047,2.414-34.66,3.638-67.211,3.638c-32.612,0-63.643-1.283-63.953-1.296c-1.296-0.063-2.405,0.879-2.581,2.155c-0.177,1.276,0.645,2.477,1.897,2.775c0.323,0.077,32.844,7.696,77.31,7.696c21.327,0,42.08-1.733,61.684-5.151c36.553-6.408,39.112-24.533,39.203-25.301C263.082,273.474,262.504,272.44,261.522,271.985z"></path>
</g>
</svg>
</div>
<input id="settingsJVMOptsVal" cValue="JVMOptions" serverDependent type="text">
</div>
<div id="settingsJVMOptsDesc"><%- lang('settings.jvmOptsDesc') %><br><a href="#" id="settingsJvmOptsLink"><!-- Available Options --></a></div>
</div>
</div>
<div id="settingsTabLauncher" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText"><%- lang('settings.launcherTabHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.launcherTabHeaderDesc') %></span>
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle"><%- lang('settings.allowPrereleaseTitle') %></span>
<span class="settingsFieldDesc"><%- lang('settings.allowPrereleaseDesc') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
<input type="checkbox" cValue="AllowPrerelease">
<span class="toggleSwitchSlider"></span>
</label>
</div>
</div>
<div class="settingsFileSelContainer">
<div class="settingsFileSelContent">
<div class="settingsFieldTitle" id="settingsDataDirTitle"><%- lang('settings.dataDirectoryTitle') %></div>
<div class="settingsFileSelActions">
<div class="settingsFileSelIcon">
<svg class="settingsFileSelSVG">
<g>
<path fill="gray" d="m10.044745,5c0,0.917174 -0.746246,1.667588 -1.667588,1.667588l-4.168971,0l-2.501382,0c-0.921009,0 -1.667588,0.750415 -1.667588,1.667588l0,6.670353l0,2.501382c0,0.917174 0.746604,1.667588 1.667588,1.667588l16.675882,0c0.921342,0 1.667588,-0.750415 1.667588,-1.667588l0,-2.501382l0,-8.337941c0,-0.917174 -0.746246,-1.667588 -1.667588,-1.667588l-8.337941,0z"/>
<path fill="gray" d="m1.627815,1.6c-0.921009,0 -1.667588,0.746579 -1.667588,1.667588l0,4.168971l8.337941,0l0,0.833794l11.673118,0l0,-4.168971c0,-0.921009 -0.746246,-1.667588 -1.667588,-1.667588l-8.572237,0c-0.288493,-0.497692 -0.816284,-0.833794 -1.433292,-0.833794l-6.670353,0z"/>
<path fill="lightgray" d="m10.025276,4c0,0.918984 -0.747719,1.670879 -1.670879,1.670879l-4.177198,0l-2.506319,0c-0.922827,0 -1.670879,0.751896 -1.670879,1.670879l0,6.683517l0,2.506319c0,0.918984 0.748078,1.670879 1.670879,1.670879l16.708794,0c0.923161,0 1.670879,-0.751896 1.670879,-1.670879l0,-2.506319l0,-8.354397c0,-0.918984 -0.747719,-1.670879 -1.670879,-1.670879l-8.354397,0z"/>
</g>
</svg>
</div>
<input class="settingsFileSelVal" type="text" value="null" cValue="DataDirectory" disabled>
<button class="settingsFileSelButton" dialogTitle="<%- lang('settings.selectDataDirectory') %>" dialogDirectory="true"><%- lang('settings.chooseFolder') %></button>
</div>
</div>
<div class="settingsFileSelDesc"><%- lang('settings.dataDirectoryDesc') %></div>
</div>
</div>
<div id="settingsTabAbout" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText"><%- lang('settings.aboutTabHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.aboutTabHeaderDesc') %></span>
</div>
<div id="settingsAboutCurrentContainer">
<div id="settingsAboutCurrentContent">
<div id="settingsAboutCurrentHeadline">
<img id="settingsAboutLogo" src="./assets/images/SealCircle.png">
<span id="settingsAboutTitle"><%- lang('settings.aboutTitle', { appName: lang('app.title') }) %></span>
</div>
<div id="settingsAboutCurrentVersion">
<div id="settingsAboutCurrentVersionCheck">&#10003;</div>
<div id="settingsAboutCurrentVersionDetails">
<span id="settingsAboutCurrentVersionTitle"><%- lang('settings.stableRelease') %></span>
<div id="settingsAboutCurrentVersionLine">
<span id="settingsAboutCurrentVersionText"><%- lang('settings.versionText') %></span>
<span id="settingsAboutCurrentVersionValue">0.0.1-alpha.18</span>
</div>
</div>
</div>
</div>
<div id="settingsAboutButtons">
<a href="<%- lang('settings.sourceGithubLink') %>" id="settingsAboutSourceButton" class="settingsAboutButton"><%- lang('settings.sourceGithub') %></a>
<!-- The following must be included in third-party usage. -->
<!-- <a href="https://github.com/dscalzi/HeliosLauncher" id="settingsAboutSourceButton" class="settingsAboutButton">Original Source</a> -->
<a href="<%- lang('settings.supportLink') %>" id="settingsAboutSupportButton" class="settingsAboutButton"><%- lang('settings.support') %></a>
<a href="#" id="settingsAboutDevToolsButton" class="settingsAboutButton"><%- lang('settings.devToolsConsole') %></a>
</div>
</div>
<div class="settingsChangelogContainer">
<div class="settingsChangelogContent">
<div class="settingsChangelogHeadline">
<div class="settingsChangelogLabel"><%- lang('settings.releaseNotes') %></div>
<div class="settingsChangelogTitle"><%- lang('settings.changelog') %></div>
</div>
<div class="settingsChangelogText">
<%- lang('settings.noReleaseNotes') %>
</div>
</div>
<div class="settingsChangelogActions">
<a class="settingsChangelogButton settingsAboutButton" href="#"><%- lang('settings.viewReleaseNotes') %></a>
</div>
</div>
</div>
<div id="settingsTabUpdate" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText"><%- lang('settings.launcherUpdatesHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.launcherUpdatesHeaderDesc') %></span>
</div>
<div id="settingsUpdateStatusContainer">
<div id="settingsUpdateStatusContent">
<div id="settingsUpdateStatusHeadline">
<span id="settingsUpdateTitle"><!-- You Are Running the Latest Version --></span>
</div>
<div id="settingsUpdateVersion">
<div id="settingsUpdateVersionCheck">&#10003;</div>
<div id="settingsUpdateVersionDetails">
<span id="settingsUpdateVersionTitle"><%- lang('settings.stableRelease') %></span>
<div id="settingsUpdateVersionLine">
<span id="settingsUpdateVersionText"><%- lang('settings.versionText') %> </span>
<span id="settingsUpdateVersionValue">0.0.1-alpha.18</span>
</div>
</div>
</div>
<div id="settingsUpdateActionContainer">
<button id="settingsUpdateActionButton"><%- lang('settings.checkForUpdates') %></button>
</div>
</div>
</div>
<div class="settingsChangelogContainer">
<div class="settingsChangelogContent">
<div class="settingsChangelogHeadline">
<div class="settingsChangelogLabel"><%- lang('settings.whatsNew') %></div>
<div class="settingsChangelogTitle"><%- lang('settings.updateReleaseNotes') %></div>
</div>
<div class="settingsChangelogText">
<%- lang('settings.noReleaseNotes') %>
</div>
</div>
</div>
</div>
</div>
<script src="./assets/js/scripts/settings.js"></script>
</div>

8
app/waiting.ejs Normal file
View File

@ -0,0 +1,8 @@
<div id="waitingContainer" style="display: none;">
<div id="waitingContent">
<div class="waitingSpinner"></div>
<div id="waitingTextContainer">
<h2><%- lang('waiting.waitingText') %></h2>
</div>
</div>
</div>

25
app/welcome.ejs Normal file
View File

@ -0,0 +1,25 @@
<div id="welcomeContainer" style="display: none;">
<!--<div class="cloudDiv">
<div class="cloudTop"></div>
<div class="cloudBottom"></div>
</div>-->
<div id="welcomeContent">
<img id="welcomeImageSeal" src="assets/images/SealCircle.png"/>
<span id="welcomeHeader"><%- lang('welcome.welcomeHeader') %></span>
<span id="welcomeDescription"><%- lang('welcome.welcomeDescription') %></span>
<br>
<span id="welcomeDescCTA"><%- lang('welcome.welcomeDescCTA') %></span>
<button id="welcomeButton">
<div id="welcomeButtonContent">
<%- lang('welcome.continueButton') %>
<svg id="welcomeSVG" viewBox="0 0 24.87 13.97">
<defs>
<style>.arrowLine{fill:none;stroke:#FFF;stroke-width:2px;transition: 0.25s ease;}</style>
</defs>
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</div>
</button>
</div>
<script src="./assets/js/scripts/welcome.js"></script>
</div>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

3
dev-app-update.yml Normal file
View File

@ -0,0 +1,3 @@
owner: dscalzi
repo: HeliosLauncher
provider: github

52
docs/MicrosoftAuth.md Normal file
View File

@ -0,0 +1,52 @@
# Microsoft Authentication
Authenticating with Microsoft is fully supported by Helios Launcher.
## Acquiring an Azure Client ID
1. Navigate to https://portal.azure.com
2. In the search bar, search for **Azure Active Directory**.
3. In Azure Active Directory, go to **App Registrations** on the left pane (Under *Manage*).
4. Click **New Registration**.
- Set **Name** to be your launcher's name.
- Set **Supported account types** to *Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)*
- Leave **Redirect URI** blank.
- Register the application.
5. You should be on the application's management page. If not, Navigate back to **App Registrations**. Select the application you just registered.
6. Click **Authentication** on the left pane (Under *Manage*).
7. Click **Add Platform**.
- Select **Mobile and desktop applications**.
- Choose `https://login.microsoftonline.com/common/oauth2/nativeclient` as the **Redirect URI**.
- Select **Configure** to finish adding the platform.
8. Go to **Credentials & secrets**.
- Select **Client secrets**.
- Click **New client secret**.
- Set a description.
- Click **Add**.
- Don't copy the client secret, adding one is just a requirement from Microsoft.
8. Navigate back to **Overview**.
9. Copy **Application (client) ID**.
## Adding the Azure Client ID to Helios Launcher.
In `app/assets/js/ipcconstants.js` you'll find **`AZURE_CLIENT_ID`**. Set it to your application's id.
Note: Azure Client ID is NOT a secret value and **can** be stored in git. Reference: https://stackoverflow.com/questions/57306964/are-azure-active-directorys-tenantid-and-clientid-considered-secrets
Then relaunch your app, and login. You'll be greeted with an error message, because the app isn't whitelisted yet. Microsoft needs some activity on the app before whitelisting it. __Trying to log in before requesting whitelist is mandatory.__
## Requesting whitelisting from Microsoft
1. Ensure you have completed every step of this doc page.
2. Fill [this form](https://aka.ms/mce-reviewappid) with the required information. Remember this is a new appID for approval. You can find both the Client ID and the Tenant ID on the overview page in the Azure Portal.
3. Give Microsoft some time to review your app.
4. Once you have received Microsoft's approval, allow up to 24 hours for the changes to apply.
----
You can now authenticate with Microsoft through the launcher.
References:
- https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
- https://help.minecraft.net/hc/en-us/articles/16254801392141

View File

@ -1,121 +1,416 @@
# Documentation of the Launcher Distribution Index
# Distribution Index
You can use [Nebula](https://github.com/dscalzi/Nebula) to automate the generation of a distribution index.
The most up to date and accurate descriptions of the distribution spec can be viewed in [helios-distribution-types](https://github.com/dscalzi/helios-distribution-types).
The distribution index is written in JSON. The general format of the index is as posted below.
```json
{
"version": "1.0",
"version": "1.0.0",
"discord": {
"clientID": 12334567890,
"clientId": "12334567890123456789",
"smallImageText": "WesterosCraft",
"smallImageKey": "seal-circle"
},
"rss": "https://westeroscraft.com/articles/index.rss",
"servers": [
{
"id": "Example_Server",
"name": "WesterosCraft Example Client",
"news_feed": "http://westeroscraft.com/forums/example/index.rss",
"icon_url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
"revision": "0.0.1",
"server_ip": "mc.westeroscraft.com:1337",
"mc_version": "1.11.2",
"description": "Example WesterosCraft server. Connect for fun!",
"icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
"version": "0.0.1",
"address": "mc.westeroscraft.com:1337",
"minecraftVersion": "1.11.2",
"discord": {
"shortId": "Example",
"largeImageText": "WesterosCraft Example Server",
"largeImageKey": "server-example"
},
"default_selected": true,
"mainServer": true,
"autoconnect": true,
"modules": [
...
"Module Objects Here"
]
}
]
}
```
You can declare an unlimited number of servers, however you must provide valid values for the fields listed above. In addition to that, the server can declare modules.
## Distro Index Object
The discord settings are to enable the use of Rich Presence on the launcher. For more details, see [discord's documentation](https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields).
Only one server in the array should have the `default_selected` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. This field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `default_selected` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false.
## Modules
A module is a generic representation of a file required to run the minecraft client. It takes the general form:
```json
#### Example
```JSON
{
"id": "group.id:artifact:version",
"name": "Artifact {version}",
"type": "{a valid type}",
"artifact": {
"size": "{file size in bytes}",
"MD5": "{MD5 hash for the file, string}",
"extension": ".jar",
"url": "http://files.site.com/maven/group/id/artifact/version/artifact-version.jar"
"version": "1.0.0",
"discord": {
"clientId": "12334567890123456789",
"smallImageText": "WesterosCraft",
"smallImageKey": "seal-circle"
},
"sub_modules": [
{
"id": "examplefile",
"name": "Example File",
"type": "file",
"artifact": {
"size": "{file size in bytes}",
"MD5": "{MD5 hash for the file, string}",
"path": "examplefile.txt",
"url": "http://files.site.com/examplefile.txt"
}
},
...
]
"rss": "https://westeroscraft.com/articles/index.rss",
"servers": []
}
```
As shown above, modules objects are allowed to declare submodules under the option `sub_modules`. This parameter is completely optional and can be omitted for modules which do not require submodules. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit.
### `DistroIndex.version: string/semver`
Modules may also declare a `required` object.
The version of the index format. Will be used in the future to gracefully push updates.
```json
"required": {
"value": false, "(if the module is required)"
"def": false "(if it's enabled by default, has no effect if value is true)"
}
```
### `DistroIndex.discord: object`
If a module does not declare this object, both `value` and `def` default to true. Similarly, if a parameter is not included in the `required` object it will default to true. This will be used in the mod selection process down the line.
Global settings for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
**Properties**
* `discord.clientId: string` - Client ID for th Application registered with Discord.
* `discord.smallImageText: string` - Tootltip for the `smallImageKey`.
* `discord.smallImageKey: string` - Name of the uploaded image for the small profile artwork.
The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file.
### `DistroIndex.rss: string/url`
Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The `id` becomes more important as it will be used to resolve the final path. The `id` must be provided in maven format, that is `group.id.maybemore:artifact:version`. From there, you need to declare the `extension` of the file in the artifact object. This effectively replaces the `path` option we used above.
**It is EXTREMELY IMPORTANT that the file size is CORRECT. The launcher's download queue will not function properly otherwise.**
Ex.
```SHELL
type = forgemod
id = com.westeroscraft:westerosblocks:1.0.0
extension = .jar
resolved_path = {base}/modstore/com/westeroscraft/westerosblocks/1.0.0/westerosblocks-1.0.0.jar
```
The resolved path depends on the type. Currently, there are several recognized module types:
- `forge-hosted` ({base}/libraries/{path OR resolved})
- `library` ({base}/libraries/{path OR resolved})
- `forgemod` ({base}/modstore/{path OR resolved})
- `litemod` ({base}/modstore/{path OR resolved})
- `file` ({base}/{path OR resolved})
A URL to a RSS feed. Used for loading news.
---
## Server Object
### forge-hosted
#### Example
```JSON
{
"id": "Example_Server",
"name": "WesterosCraft Example Client",
"description": "Example WesterosCraft server. Connect for fun!",
"icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
"version": "0.0.1",
"address": "mc.westeroscraft.com:1337",
"minecraftVersion": "1.11.2",
"discord": {
"shortId": "Example",
"largeImageText": "WesterosCraft Example Server",
"largeImageKey": "server-example"
},
"mainServer": true,
"autoconnect": true,
"modules": []
}
```
The module type `forge-hosted` represents forge itself. Currently, the launcher only supports forge servers, as vanilla servers can be connected to via the mojang launcher. The `hosted` part is key, this means that the forge module must declare its required libraries as submodules.
### `Server.id: string`
The ID of the server. The launcher saves mod configurations and selected servers by ID. If the ID changes, all data related to the old ID **will be wiped**.
### `Server.name: string`
The name of the server. This is what users see on the UI.
### `Server.description: string`
A brief description of the server. Displayed on the UI to provide users more information.
### `Server.icon: string/url`
A URL to the server's icon. Will be displayed on the UI.
### `Server.version: string/semver`
The version of the server configuration.
### `Server.address: string/url`
The server's IP address.
### `Server.minecraftVersion: string`
The version of minecraft that the server is running.
### `Server.discord: object`
Server specific settings used for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
**Properties**
* `discord.shortId: string` - Short ID for the server. Displayed on the second status line as `Server: shortId`
* `discord.largeImageText: string` - Ttooltip for the `largeImageKey`.
* `discord.largeImageKey: string` - Name of the uploaded image for the large profile artwork.
### `Server.mainServer: boolean`
Only one server in the array should have the `mainServer` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. If this field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `mainServer` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false.
### `Server.autoconnect: boolean`
Whether or not the server can be autoconnected to. If false, the server will not be autoconnected to even when the user has the autoconnect setting enabled.
### `Server.javaOptions: JavaOptions`
**OPTIONAL**
Sever-specific Java options. If not provided, defaults are used by the client.
### `Server.modules: Module[]`
An array of module objects.
---
## JavaOptions Object
Server-specific Java options.
#### Example
```JSON
{
"supported": ">=17",
"suggestedMajor": 17,
"platformOptions": [
{
"platform": "darwin",
"architecture": "arm64",
"distribution": "CORRETTO"
}
],
"ram": {
"recommended": 3072,
"minimum": 2048
}
}
```
### `JavaOptions.platformOptions: JavaPlatformOptions[]`
**OPTIONAL**
Platform-specific java rules for this server configuration. Validation rules will be delegated to the client for any undefined properties. Java validation can be configured for specific platforms and architectures. The most specific ruleset will be applied.
Maxtrix Precedence (Highest - Lowest)
- Current platform, current architecture (ex. win32 x64).
- Current platform, any architecture (ex. win32).
- Java Options base properties.
- Client logic (default logic in the client).
Properties:
- `platformOptions.platform: string` - The platform that this validation matrix applies to.
- `platformOptions.architecture: string` - Optional. The architecture that this validation matrix applies to. If omitted, applies to all architectures.
- `platformOptions.distribution: string` - Optional. See `JavaOptions.distribution`.
- `platformOptions.supported: string` - Optional. See `JavaOptions.supported`.
- `platformOptions.suggestedMajor: number` - Optional. See `JavaOptions.suggestedMajor`.
### `JavaOptions.ram: object`
**OPTIONAL**
This allows you to require a minimum and recommended amount of RAM per server instance. The minimum is the smallest value the user can select in the settings slider. The recommended value will be the default value selected for that server. These values are specified in megabytes and must be an interval of 512. This allows configuration in intervals of half gigabytes. In the above example, the recommended ram value is 3 GB (3072 MB) and the minimum is 2 GB (2048 MB).
- `ram.recommended: number` - The recommended amount of RAM in megabytes. Must be an interval of 512.
- `ram.minimum: number` - The absolute minimum amount of RAM in megabytes. Must be an interval of 512.
### `JavaOptions.distribution: string`
**OPTIONAL**
Preferred JDK distribution to download if no applicable installation could be found. If omitted, the client will decide (decision may be platform-specific).
### `JavaOptions.supported: string`
**OPTIONAL**
A semver range of supported JDK versions.
Java version syntax is platform dependent.
JDK 8 and prior
```
1.{major}.{minor}_{patch}-b{build}
Ex. 1.8.0_152-b16
```
JDK 9+
```
{major}.{minor}.{patch}+{build}
Ex. 11.0.12+7
```
For processing, all versions will be translated into a semver compliant string. JDK 9+ is already semver. For versions 8 and below, `1.{major}.{minor}_{patch}-b{build}` will be translated to `{major}.{minor}.{patch}+{build}`.
If specified, you must also specify suggestedMajor.
If omitted, the client will decide based on the game version.
### `JavaOptions.suggestedMajor: number`
**OPTIONAL**
The suggested major Java version. The suggested major should comply with the version range specified by supported, if defined. This will be used in messages displayed to the end user, and to automatically fetch a Java version.
NOTE If supported is specified, suggestedMajor must be set. The launcher's default value may not comply with your custom major supported range.
Common use case:
- supported: '>=17.x'
- suggestedMajor: 17
More involved:
- supported: '>=16 <20'
- suggestedMajor: 17
Given a wider support range, it becomes necessary to specify which major version in the range is the suggested.
---
## Module Object
A module is a generic representation of a file required to run the minecraft client.
#### Example
```JSON
{
"id": "com.example:artifact:1.0.0@jar.pack.xz",
"name": "Artifact 1.0.0",
"type": "Library",
"artifact": {
"size": 4231234,
"MD5": "7f30eefe5c51e1ae0939dab2051db75f",
"url": "http://files.site.com/maven/com/example/artifact/1.0.0/artifact-1.0.0.jar.pack.xz"
},
"subModules": [
{
"id": "examplefile",
"name": "Example File",
"type": "File",
"artifact": {
"size": 23423,
"MD5": "169a5e6cf30c2cc8649755cdc5d7bad7",
"path": "examplefile.txt",
"url": "http://files.site.com/examplefile.txt"
}
}
]
}
```
The parent module will be stored maven style, it's destination path will be resolved by its id. The sub module has a declared `path`, so that value will be used.
### `Module.id: string`
The ID of the module. All modules that are not of type `File` **MUST** use a maven identifier. Version information and other metadata is pulled from the identifier. Modules which are stored maven style use the identifier to resolve the destination path. If the `extension` is not provided, it defaults to `jar`.
**Template**
`my.group:arifact:version@extension`
`my/group/artifact/version/artifact-version.extension`
**Example**
`net.minecraft:launchwrapper:1.12` OR `net.minecraft:launchwrapper:1.12@jar`
`net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar`
If the module's artifact does not declare the `path` property, its path will be resolved from the ID.
### `Module.name: string`
The name of the module. Used on the UI.
### `Module.type: string`
The type of the module.
### `Module.classpath: boolean`
**OPTIONAL**
If the module is of type `Library`, whether the library should be added to the classpath. Defaults to true.
### `Module.required: Required`
**OPTIONAL**
Defines whether or not the module is required. If omitted, then the module will be required.
Only applicable for modules of type:
* `ForgeMod`
* `LiteMod`
* `LiteLoader`
### `Module.artifact: Artifact`
The download artifact for the module.
### `Module.subModules: Module[]`
**OPTIONAL**
An array of sub modules declared by this module. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit.
## Artifact Object
The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file.
Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The module `id` will be used to resolve the final path, effectively replacing the `path` property. It must be provided in maven format. More information on this is provided in the documentation for the `id` property.
The resolved/provided paths are appended to a base path depending on the module's declared type.
| Type | Path |
| ---- | ---- |
| `ForgeHosted` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `Fabric` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `LiteLoader` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `Library` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `ForgeMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
| `LiteMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
| `FabricMod` | ({`commonDirectory`}/mods/fabric/{`path` OR resolved}) |
| `File` | ({`instanceDirectory`}/{`Server.id`}/{`path` OR resolved}) |
The `commonDirectory` and `instanceDirectory` values are stored in the launcher's config.json.
### `Artifact.size: number`
The size of the artifact.
### `Artifact.MD5: string`
The MD5 hash of the artifact. This will be used to validate local artifacts.
### `Artifact.path: string`
**OPTIONAL**
A relative path to where the file will be saved. This is appended to the base path for the module's declared type.
If this is not specified, the path will be resolved based on the module's ID.
### `Artifact.url: string/url`
The artifact's download url.
## Required Object
### `Required.value: boolean`
**OPTIONAL**
If the module is required. Defaults to true if this property is omited.
### `Required.def: boolean`
**OPTIONAL**
If the module is enabled by default. Has no effect unless `Required.value` is false. Defaults to true if this property is omited.
---
## Module Types
### ForgeHosted
The module type `ForgeHosted` represents forge itself. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher. The `Hosted` part is key, this means that the forge module must declare its required libraries as submodules.
Ex.
@ -123,39 +418,97 @@ Ex.
{
"id": "net.minecraftforge:forge:1.11.2-13.20.1.2429",
"name": "Minecraft Forge 1.11.2-13.20.1.2429",
"type": "forge-hosted",
"type": "ForgeHosted",
"artifact": {
"size": 4450992,
"MD5": "3fcc9b0104f0261397d3cc897e55a1c5",
"extension": ".jar",
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar"
},
"sub_modules": [
"subModules": [
{
"id": "net.minecraft:launchwrapper:1.12",
"name": "Mojang (LaunchWrapper)",
"type": "library",
"type": "Library",
"artifact": {
"size": 32999,
"MD5": "934b2d91c7c5be4a49577c9e6b40e8da",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar"
}
},
...
}
]
}
```
All of forge's required libraries are declared in the `version.json` file found in the root of the forge jar file. These libraries MUST be hosted and declared a submodules or forge will not work.
There were plans to add a `forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented.
There were plans to add a `Forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented.
---
### library
### Fabric
The module type `library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process.
The module type `Fabric` represents the fabric mod loader. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher.
Ex.
```json
{
"id": "net.fabricmc:fabric-loader:0.15.0",
"name": "Fabric (fabric-loader)",
"type": "Fabric",
"artifact": {
"size": 1196222,
"MD5": "a43d5a142246801343b6cedef1c102c4",
"url": "http://localhost:8080/repo/lib/net/fabricmc/fabric-loader/0.15.0/fabric-loader-0.15.0.jar"
},
"subModules": [
{
"id": "1.20.1-fabric-0.15.0",
"name": "Fabric (version.json)",
"type": "VersionManifest",
"artifact": {
"size": 2847,
"MD5": "69a2bd43452325ba1bc882fa0904e054",
"url": "http://localhost:8080/repo/versions/1.20.1-fabric-0.15.0/1.20.1-fabric-0.15.0.json"
}
}
}
```
Fabric works similarly to Forge 1.13+.
---
### LiteLoader
The module type `LiteLoader` represents liteloader. It is handled as a library and added to the classpath at runtime. Special launch conditions are executed when liteloader is present and enabled. This module can be optional and toggled similarly to `ForgeMod` and `Litemod` modules.
Ex.
```json
{
"id": "com.mumfrey:liteloader:1.11.2",
"name": "Liteloader (1.11.2)",
"type": "LiteLoader",
"required": {
"value": false,
"def": false
},
"artifact": {
"size": 1685422,
"MD5": "3a98b5ed95810bf164e71c1a53be568d",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/liteloader-1.11.2.jar"
},
"subModules": [
"All LiteMods go here"
]
}
```
---
### Library
The module type `Library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process.
Ex.
@ -163,11 +516,10 @@ Ex.
{
"id": "net.sf.jopt-simple:jopt-simple:4.6",
"name": "Jopt-simple 4.6",
"type": "library",
"type": "Library",
"artifact": {
"size": 62477,
"MD5": "13560a58a79b46b82057686543e8d727",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar"
}
}
@ -175,20 +527,19 @@ Ex.
---
### forgemod
### ForgeMod
The module type `forgemod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format).
The module type `ForgeMod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format).
Ex.
```json
{
"id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133",
"name": "WesterosBlocks (3.0.0-beta-6-133)",
"type": "forgemod",
"type": "ForgeMod",
"artifact": {
"size": 16321712,
"MD5": "5a89e2ab18916c18965fc93a0766cc6e",
"extension": ".jar",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar"
}
}
@ -196,15 +547,33 @@ Ex.
---
### litemod
### LiteMod
This module type is being actively considered and changed, until finalized there will be no documentation.
The module type `LiteMod` represents a mod loaded by liteloader. These files are stored maven-style and passed to liteloader using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). Documentation for liteloader's implementation of this can be found on [this issue](http://develop.liteloader.com/liteloader/LiteLoader/issues/34).
Ex.
```json
{
"id": "com.mumfrey:macrokeybindmod:0.14.4-1.11.2@litemod",
"name": "Macro/Keybind Mod (0.14.4-1.11.2)",
"type": "LiteMod",
"required": {
"value": false,
"def": false
},
"artifact": {
"size": 1670811,
"MD5": "16080785577b391d426c62c8d3138558",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/macrokeybindmod.litemod"
}
}
```
---
### file
### File
The module type `file` represents a generic file required by the client, another module, etc.
The module type `file` represents a generic file required by the client, another module, etc. These files are stored in the server's instance directory.
Ex.
@ -221,7 +590,3 @@ Ex.
}
}
```
---
This format is actively under development and is likely to change.

File diff suppressed because it is too large Load Diff

51
electron-builder.yml Normal file
View File

@ -0,0 +1,51 @@
appId: 'helioslauncher'
productName: 'Helios Launcher'
artifactName: '${productName}-setup-${version}.${ext}'
copyright: 'Copyright © 2018-2022 Daniel Scalzi'
asar: true
compression: 'maximum'
files:
- '!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.nvmrc,.eslintrc.json}'
extraResources:
- 'libraries'
# Windows Configuration
win:
target:
- target: 'nsis'
arch: 'x64'
# Windows Installer Configuration
nsis:
oneClick: false
perMachine: false
allowElevation: true
allowToChangeInstallationDirectory: true
# macOS Configuration
mac:
target:
- target: 'dmg'
arch:
- 'x64'
- 'arm64'
artifactName: '${productName}-setup-${version}-${arch}.${ext}'
category: 'public.app-category.games'
# Linux Configuration
linux:
target: 'AppImage'
maintainer: 'Daniel Scalzi'
vendor: 'Daniel Scalzi'
synopsis: 'Modded Minecraft Launcher'
description: 'Custom launcher which allows users to join modded servers. All mods, configurations, and updates are handled automatically.'
category: 'Game'
directories:
buildResources: 'build'
output: 'dist'

341
index.js
View File

@ -1,55 +1,348 @@
const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')
const fs = require('fs')
const remoteMain = require('@electron/remote/main')
remoteMain.initialize()
// Requirements
const { app, BrowserWindow, ipcMain, Menu, shell } = require('electron')
const autoUpdater = require('electron-updater').autoUpdater
const ejse = require('ejs-electron')
const fs = require('fs')
const isDev = require('./app/assets/js/isdev')
const path = require('path')
const semver = require('semver')
const { pathToFileURL } = require('url')
const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR, SHELL_OPCODE } = require('./app/assets/js/ipcconstants')
const LangLoader = require('./app/assets/js/langloader')
// Setup Lang
LangLoader.setupLanguage()
// Setup auto updater.
function initAutoUpdater(event, data) {
if(data){
autoUpdater.allowPrerelease = true
} else {
// Defaults to true if application version contains prerelease components (e.g. 0.12.1-alpha.1)
// autoUpdater.allowPrerelease = true
}
if(isDev){
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml')
}
if(process.platform === 'darwin'){
autoUpdater.autoDownload = false
}
autoUpdater.on('update-available', (info) => {
event.sender.send('autoUpdateNotification', 'update-available', info)
})
autoUpdater.on('update-downloaded', (info) => {
event.sender.send('autoUpdateNotification', 'update-downloaded', info)
})
autoUpdater.on('update-not-available', (info) => {
event.sender.send('autoUpdateNotification', 'update-not-available', info)
})
autoUpdater.on('checking-for-update', () => {
event.sender.send('autoUpdateNotification', 'checking-for-update')
})
autoUpdater.on('error', (err) => {
event.sender.send('autoUpdateNotification', 'realerror', err)
})
}
// Open channel to listen for update actions.
ipcMain.on('autoUpdateAction', (event, arg, data) => {
switch(arg){
case 'initAutoUpdater':
console.log('Initializing auto updater.')
initAutoUpdater(event, data)
event.sender.send('autoUpdateNotification', 'ready')
break
case 'checkForUpdate':
autoUpdater.checkForUpdates()
.catch(err => {
event.sender.send('autoUpdateNotification', 'realerror', err)
})
break
case 'allowPrereleaseChange':
if(!data){
const preRelComp = semver.prerelease(app.getVersion())
if(preRelComp != null && preRelComp.length > 0){
autoUpdater.allowPrerelease = true
} else {
autoUpdater.allowPrerelease = data
}
} else {
autoUpdater.allowPrerelease = data
}
break
case 'installUpdateNow':
autoUpdater.quitAndInstall()
break
default:
console.log('Unknown argument', arg)
break
}
})
// Redirect distribution index event from preloader to renderer.
ipcMain.on('distributionIndexDone', (event, res) => {
event.sender.send('distributionIndexDone', res)
})
// Handle trash item.
ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => {
try {
await shell.trashItem(args[0])
return {
result: true
}
} catch(error) {
return {
result: false,
error: error
}
}
})
// Disable hardware acceleration.
// https://electronjs.org/docs/tutorial/offscreen-rendering
app.disableHardwareAcceleration()
const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?'
// Microsoft Auth Login
let msftAuthWindow
let msftAuthSuccess
let msftAuthViewSuccess
let msftAuthViewOnClose
ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => {
if (msftAuthWindow) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose)
return
}
msftAuthSuccess = false
msftAuthViewSuccess = arguments_[0]
msftAuthViewOnClose = arguments_[1]
msftAuthWindow = new BrowserWindow({
title: LangLoader.queryJS('index.microsoftLoginTitle'),
backgroundColor: '#222222',
width: 520,
height: 600,
frame: true,
icon: getPlatformIcon('SealCircle')
})
msftAuthWindow.on('closed', () => {
msftAuthWindow = undefined
})
msftAuthWindow.on('close', () => {
if(!msftAuthSuccess) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose)
}
})
msftAuthWindow.webContents.on('did-navigate', (_, uri) => {
if (uri.startsWith(REDIRECT_URI_PREFIX)) {
let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&')
let queryMap = {}
queries.forEach(query => {
const [name, value] = query.split('=')
queryMap[name] = decodeURI(value)
})
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess)
msftAuthSuccess = true
msftAuthWindow.close()
msftAuthWindow = null
}
})
msftAuthWindow.removeMenu()
msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`)
})
// Microsoft Auth Logout
let msftLogoutWindow
let msftLogoutSuccess
let msftLogoutSuccessSent
ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => {
if (msftLogoutWindow) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN)
return
}
msftLogoutSuccess = false
msftLogoutSuccessSent = false
msftLogoutWindow = new BrowserWindow({
title: LangLoader.queryJS('index.microsoftLogoutTitle'),
backgroundColor: '#222222',
width: 520,
height: 600,
frame: true,
icon: getPlatformIcon('SealCircle')
})
msftLogoutWindow.on('closed', () => {
msftLogoutWindow = undefined
})
msftLogoutWindow.on('close', () => {
if(!msftLogoutSuccess) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED)
} else if(!msftLogoutSuccessSent) {
msftLogoutSuccessSent = true
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
}
})
msftLogoutWindow.webContents.on('did-navigate', (_, uri) => {
if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) {
msftLogoutSuccess = true
setTimeout(() => {
if(!msftLogoutSuccessSent) {
msftLogoutSuccessSent = true
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
}
if(msftLogoutWindow) {
msftLogoutWindow.close()
msftLogoutWindow = null
}
}, 5000)
}
})
msftLogoutWindow.removeMenu()
msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout')
})
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win
function createWindow() {
win = new BrowserWindow({
width: 980,
height: 552,
icon: getPlatformIcon('WesterosSealSquare'),
icon: getPlatformIcon('SealCircle'),
frame: false,
webPreferences: {
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js')
}
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'),
nodeIntegration: true,
contextIsolation: false
},
backgroundColor: '#171614'
})
remoteMain.enable(win.webContents)
ejse.data('bkid', Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)))
const data = {
bkid: Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)),
lang: (str, placeHolders) => LangLoader.queryEJS(str, placeHolders)
}
Object.entries(data).forEach(([key, val]) => ejse.data(key, val))
win.loadURL(url.format({
pathname: path.join(__dirname, 'app', 'app.ejs'),
protocol: 'file:',
slashes: true
}))
win.loadURL(pathToFileURL(path.join(__dirname, 'app', 'app.ejs')).toString())
win.setMenu(null)
/*win.once('ready-to-show', () => {
win.show()
})*/
win.setResizable(true)
win.removeMenu()
win.resizable = true
win.on('closed', () => {
win = null
})
}
function createMenu() {
if(process.platform === 'darwin') {
// Extend default included application menu to continue support for quit keyboard shortcut
let applicationSubMenu = {
label: 'Application',
submenu: [{
label: 'About Application',
selector: 'orderFrontStandardAboutPanel:'
}, {
type: 'separator'
}, {
label: 'Quit',
accelerator: 'Command+Q',
click: () => {
app.quit()
}
}]
}
// New edit menu adds support for text-editing keyboard shortcuts
let editSubMenu = {
label: 'Edit',
submenu: [{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
selector: 'undo:'
}, {
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
selector: 'redo:'
}, {
type: 'separator'
}, {
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
selector: 'cut:'
}, {
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
selector: 'copy:'
}, {
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
selector: 'paste:'
}, {
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
selector: 'selectAll:'
}]
}
// Bundle submenus into a single template and build a menu object with it
let menuTemplate = [applicationSubMenu, editSubMenu]
let menuObject = Menu.buildFromTemplate(menuTemplate)
// Assign it to the application
Menu.setApplicationMenu(menuObject)
}
}
function getPlatformIcon(filename){
const opSys = process.platform
if (opSys === 'darwin') {
filename = filename + '.icns'
} else if (opSys === 'win32') {
filename = filename + '.ico'
} else {
filename = filename + '.png'
let ext
switch(process.platform) {
case 'win32':
ext = 'ico'
break
case 'darwin':
case 'linux':
default:
ext = 'png'
break
}
return path.join(__dirname, 'app', 'assets', 'images', filename)
return path.join(__dirname, 'app', 'assets', 'images', `${filename}.${ext}`)
}
app.on('ready', createWindow);
app.on('ready', createWindow)
app.on('ready', createMenu)
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar

Binary file not shown.

6458
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,81 +1,51 @@
{
"name": "westeroscraftlauncher",
"version": "0.0.1",
"description": "Custom modded launcher for Westeroscraft",
"productName": "WesterosCraft Launcher",
"name": "helioslauncher",
"version": "2.1.0",
"productName": "Helios Launcher",
"description": "Modded Minecraft Launcher",
"author": "Daniel Scalzi (https://github.com/dscalzi/)",
"license": "UNLICENSED",
"homepage": "https://github.com/dscalzi/HeliosLauncher",
"bugs": {
"url": "https://github.com/dscalzi/HeliosLauncher/issues"
},
"private": true,
"main": "index.js",
"scripts": {
"start": "electron index.js",
"dist": "SET ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true & electron-builder",
"dist:win": "npm run dist -- --win --x64",
"dist:mac": "npm run dist -- --mac",
"dist:linux": "npm run dist -- --linux --x64",
"dist:all": "npm run dist -- -wl --x64"
"start": "electron .",
"dist": "electron-builder build",
"dist:win": "npm run dist -- -w",
"dist:mac": "npm run dist -- -m",
"dist:linux": "npm run dist -- -l",
"lint": "eslint --config .eslintrc.json ."
},
"engines": {
"node": "8.9.x"
"node": "18.x.x"
},
"dependencies": {
"@electron/remote": "^2.1.0",
"adm-zip": "^0.5.9",
"discord-rpc-patch": "^4.0.1",
"ejs": "^3.1.9",
"ejs-electron": "^2.1.1",
"electron-updater": "^6.1.7",
"fs-extra": "^11.1.1",
"github-syntax-dark": "^0.5.0",
"got": "^11.8.5",
"helios-core": "~2.1.0",
"helios-distribution-types": "^1.3.0",
"jquery": "^3.7.1",
"lodash.merge": "^4.6.2",
"semver": "^7.5.4",
"toml": "^3.0.0"
},
"devDependencies": {
"electron": "^27.1.3",
"electron-builder": "^24.9.1",
"eslint": "^8.55.0"
},
"repository": {
"type": "git",
"url": "git+https://gitlab.com/westeroscraft/electronlauncher.git"
},
"author": "Daniel Scalzi",
"license": "AGPL-3.0",
"bugs": {
"url": "https://gitlab.com/westeroscraft/electronlauncher/issues"
},
"homepage": "http://www.westeroscraft.com/",
"dependencies": {
"adm-zip": "^0.4.7",
"async": "^2.6.0",
"discord-rpc": "^3.0.0-beta.8",
"ejs": "^2.5.7",
"ejs-electron": "^2.0.1",
"find-java-home": "^0.2.0",
"jquery": "^3.2.1",
"request-promise-native": "^1.0.5",
"uuid": "^3.2.1"
},
"devDependencies": {
"electron": "^1.7.11",
"electron-builder": "^19.54.0"
},
"build": {
"appId": "westeroscraftlauncher",
"productName": "WesterosCraft Launcher",
"artifactName": "${productName}.${ext}",
"copyright": "Copyright © 2018 WesterosCraft",
"directories": {
"buildResources": "build",
"output": "dist"
},
"win": {
"target": "portable",
"icon": "build/icon.ico"
},
"mac": {
"target": "dmg",
"category": "public.app-category.games",
"icon": "build/icon.icns",
"type": "distribution"
},
"linux": {
"target": "AppImage",
"maintainer": "Daniel Scalzi",
"vendor": "WesterosCraft",
"synopsis": "Custom modded launcher for WesterosCraft",
"description": "Custom launcher which allows users to join WesterosCraft. All mods, configurations, and updates are handled automatically.",
"category": "Game"
},
"deb": {
"compression": "xz",
"packageCategory": "Games",
"priority": "optional"
},
"compression": "maximum",
"files": [
"!{target,logs,.vscode,docs}"
],
"asar": true
"url": "git+https://github.com/dscalzi/HeliosLauncher.git"
}
}