Compare commits

..

134 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
182 changed files with 9906 additions and 24627 deletions

View File

@ -1,48 +1,66 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended"
],
"env": {
"es2022": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
"ecmaVersion": 2022,
"sourceType": "module"
},
"rules": {
"semi": "off",
"@typescript-eslint/semi": [
"indent": [
"error",
"never"
4,
{
"SwitchCase": 1
}
],
"quotes": "off",
"@typescript-eslint/quotes": [
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"single"
],
"indent": "off",
"@typescript-eslint/indent": [
"semi": [
"error",
4
"never"
],
"@typescript-eslint/member-delimiter-style": [
"no-var": [
"error"
],
"no-console": [
0
],
"no-control-regex": [
0
],
"no-unused-vars": [
"error",
{
"multiline": {
"delimiter": "none",
"requireLast": false
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
"vars": "all",
"args": "none",
"ignoreRestSiblings": false,
"argsIgnorePattern": "reject"
}
],
"@typescript-eslint/no-non-null-assertion": "off"
}
"no-async-promise-executor": [
0
]
},
"overrides": [
{
"files": [ "app/assets/js/scripts/*.js" ],
"rules": {
"no-unused-vars": [
0
],
"no-undef": [
0
]
}
}
]
}

View File

@ -1,66 +0,0 @@
{
"env": {
"es2017": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2019,
"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": [ "src/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

2
.gitignore vendored
View File

@ -4,5 +4,3 @@
/target/
/logs/
/dist/
/out/
/old/

2
.nvmrc
View File

@ -1 +1 @@
12
18

View File

@ -1,45 +0,0 @@
matrix:
include:
- os: osx
osx_image: xcode11.3
language: node_js
node_js: "12"
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true
- CSC_IDENTITY_AUTO_DISCOVERY=false
- os: linux
services: docker
language: generic
node_js: "12"
env:
- ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
script:
- |
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
ENVS=`env | grep -iE '(DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '`
docker run $ENVS --rm \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine \
/bin/bash -c "node -v && npm ci && npm run cilinux"
else
npm run cidarwin
fi
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
branches:
except:
- "/^v\\d+\\.\\d+\\.\\d+$/"

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.

View File

@ -1,10 +1,10 @@
<p align="center"><img src="./static/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
<p align="center"><img src="./app/assets/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
<h1 align="center">Helios Launcher</h1>
<em><h5 align="center">(formerly Electron Launcher)</h5></em>
[<p align="center"><img src="https://img.shields.io/travis/dscalzi/HeliosLauncher.svg?style=for-the-badge" alt="travis">](https://travis-ci.org/dscalzi/HeliosLauncher) [<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="stark"></p>
[<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>
<p align="center">Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.</p>
@ -15,6 +15,7 @@
* 🔒 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.
@ -53,9 +54,10 @@ If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/re
| Platform | File |
| -------- | ---- |
| Windows x64 | `helioslauncher-setup-VERSION.exe` |
| macOS | `helioslauncher-VERSION.dmg` |
| Linux x64 | `helioslauncher-VERSION-x86_64.AppImage` |
| 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
@ -76,11 +78,13 @@ If you want to export the console output, simply right click anywhere on the con
## Development
This section details the setup of a basic developmentment environment.
### Getting Started
**System Requirements**
* [Node.js][nodejs] v12
* [Node.js][nodejs] v18
---
@ -136,28 +140,24 @@ Paste the following into `.vscode/launch.json`
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
},
"args": ["."],
"console": "integratedTerminal",
"protocol": "inspector"
"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}/.",
"${workspaceFolder}/.",
"--remote-debugging-port=9222"
],
"webRoot": "${workspaceRoot}"
"webRoot": "${workspaceFolder}"
}
]
}
@ -177,9 +177,19 @@ Note that you **cannot** open the DevTools window while using this debug configu
---
### Note on Third-Party Usage
Please give credit to the original author and provide a link to the original source. This is free software, please do at least this much.
For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md.
---
## Resources
* [Wiki][wiki]
* [Nebula (Create Distribution.json)][nebula]
* [v2 Rewrite Branch (Inactive)][v2branch]
The best way to contact the developers is on Discord.
@ -197,5 +207,5 @@ The best way to contact the developers is on Discord.
[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'
© 2020 Daniel Scalzi All Rights Reserved
[nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula'
[v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch'

View File

@ -1,10 +1,10 @@
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-In6B8teKZQll5heMl9bS7CESTbGvuAt3VVV86BUQBDk='"/>
<title>Westeroscraft Launcher</title>
<script src="../../out/scripts/uicore.js"></script>
<script src="../../out/scripts/uibinder.js"></script>
<link type="text/css" rel="stylesheet" href="../css/launcher.css">
<title><%= lang('app.title') %></title>
<script src="./assets/js/scripts/uicore.js"></script>
<script src="./assets/js/scripts/uibinder.js"></script>
<link type="text/css" rel="stylesheet" href="./assets/css/launcher.css">
<style>
body {
/*background: url('assets/images/backgrounds/<%=bkid%>.jpg') no-repeat center center fixed;*/
@ -27,27 +27,23 @@
</style>
</head>
<body bkid="<%=bkid%>">
<% include frame.ejs %>
<%- include('frame') %>
<div id="main">
<% include welcome.ejs %>
<% include login.ejs %>
<% include settings.ejs %>
<% include landing.ejs %>
<%- include('welcome') %>
<%- include('login') %>
<%- include('waiting') %>
<%- include('loginOptions') %>
<%- include('settings') %>
<%- include('landing') %>
</div>
<% include overlay.ejs %>
<%- include('overlay') %>
<div id="loadingContainer">
<div id="loadingContent">
<div id="loadSpinnerContainer">
<img id="loadCenterImage" src="../images/LoadingSeal.png">
<img id="loadSpinnerImage" class="rotating" src="../images/LoadingText.png">
<img id="loadCenterImage" src="assets/images/LoadingSeal.png">
<img id="loadSpinnerImage" class="rotating" src="assets/images/LoadingText.png">
</div>
</div>
</div>
<script>
// Load language
for(let key of Object.keys(Lang.query('html'))){
document.getElementById(key).innerHTML = Lang.query(`html.${key}`)
}
</script>
</body>
</html>

View File

@ -1,5 +1,5 @@
/* Github Code Highlighting. */
@import "../../node_modules/github-syntax-dark/lib/github-dark.css";
@import "../../../node_modules/github-syntax-dark/lib/github-dark.css";
/*******************************************************************************
* *
@ -222,6 +222,7 @@ body, button {
align-items: center;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.50);
}
#welcomeContent {
@ -872,6 +873,175 @@ body, button {
}
*/
/*******************************************************************************
* *
* Waiting View (waiting.ejs) *
* *
******************************************************************************/
#waitingContainer {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
transition: filter 0.25s ease;
background: rgba(0, 0, 0, 0.50);
}
#waitingContent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 50%;
top: -10%;
position: relative;
}
.waitingSpinner:before {
transform: rotateX(60deg) rotateY(45deg) rotateZ(45deg);
animation: 750ms rotateBefore infinite linear reverse;
}
.waitingSpinner:after {
transform: rotateX(240deg) rotateY(45deg) rotateZ(45deg);
animation: 750ms rotateAfter infinite linear;
}
.waitingSpinner:before,
.waitingSpinner:after {
box-sizing: border-box;
content: '';
display: block;
position: fixed;
top: calc(50% - 5em);
/* left: 50%; */
margin-top: -5em;
margin-left: -5em;
width: 10em;
height: 10em;
transform-style: preserve-3d;
transform-origin: 50%;
transform: rotateY(50%);
perspective-origin: 50% 50%;
perspective: 340px;
background-size: 10em 10em;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2NnB4IiBoZWlnaHQ9IjI5N3B4IiB2aWV3Qm94PSIwIDAgMjY2IDI5NyIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8dGl0bGU+c3Bpbm5lcjwvdGl0bGU+CiAgICA8ZGVzY3JpcHRpb24+Q3JlYXRlZCB3aXRoIFNrZXRjaCAoaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoKTwvZGVzY3JpcHRpb24+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8cGF0aCBkPSJNMTcxLjUwNzgxMywzLjI1MDAwMDM4IEMyMjYuMjA4MTgzLDEyLjg1NzcxMTEgMjk3LjExMjcyMiw3MS40OTEyODIzIDI1MC44OTU1OTksMTA4LjQxMDE1NSBDMjE2LjU4MjAyNCwxMzUuODIwMzEgMTg2LjUyODQwNSw5Ny4wNjI0OTY0IDE1Ni44MDA3NzQsODUuNzczNDM0NiBDMTI3LjA3MzE0Myw3NC40ODQzNzIxIDc2Ljg4ODQ2MzIsODQuMjE2MTQ2MiA2MC4xMjg5MDY1LDEwOC40MTAxNTMgQy0xNS45ODA0Njg1LDIxOC4yODEyNDcgMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IDE0NS4yNzczNDQsMjk2LjY2Nzk2OCBDMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IC0yNS40NDkyMTg3LDI1Ny4yNDIxOTggMy4zOTg0Mzc1LDEwOC40MTAxNTUgQzE2LjMwNzA2NjEsNDEuODExNDE3NCA4NC43Mjc1ODI5LC0xMS45OTIyOTg1IDE3MS41MDc4MTMsMy4yNTAwMDAzOCBaIiBpZD0iUGF0aC0xIiBmaWxsPSIjZmZmZmZmIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==);
}
#waitingTextContainer {
position: fixed;
top: 50%;
}
@keyframes rotateBefore {
from {
transform: rotateX(60deg) rotateY(45deg) rotateZ(0deg);
}
to {
transform: rotateX(60deg) rotateY(45deg) rotateZ(-360deg);
}
}
@keyframes rotateAfter {
from {
transform: rotateX(240deg) rotateY(45deg) rotateZ(0deg);
}
to {
transform: rotateX(240deg) rotateY(45deg) rotateZ(360deg);
}
}
/*******************************************************************************
* *
* Login Options View (loginOptions.ejs) *
* *
******************************************************************************/
#loginOptionsContainer {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
transition: filter 0.25s ease;
background: rgba(0, 0, 0, 0.50);
}
#loginOptionsContent {
border-radius: 3px;
position: relative;
top: -5%;
}
.loginOptionsMainContent {
display: flex;
flex-direction: column;
align-items: center;
}
.loginOptionActions {
display: flex;
flex-direction: column;
row-gap: 10px;
}
.loginOptionButtonContainer {
width: 16em;
}
.loginOptionButton {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(126, 126, 126, 0.57);
border-radius: 3px;
height: 50px;
width: 100%;
text-align: left;
padding: 0px 25px;
cursor: pointer;
outline: none;
transition: 0.25s ease;
display: flex;
align-items: center;
column-gap: 5px;
}
.loginOptionButton:hover,
.loginOptionButton:focus {
background: rgba(54, 54, 54, 0.25);
text-shadow: 0px 0px 20px white;
}
#loginOptionCancelContainer {
position: absolute;
bottom: -100px;
}
#loginOptionCancelButton {
background: none;
border: none;
padding: 2px 0px;
font-size: 16px;
font-weight: bold;
color: lightgrey;
cursor: pointer;
outline: none;
transition: 0.25s ease;
}
#loginOptionCancelButton:hover,
#loginOptionCancelButton:focus {
text-shadow: 0px 0px 20px lightgrey;
}
#loginOptionCancelButton:active {
text-shadow: 0px 0px 20px rgba(211, 211, 211, 0.75);
color: rgba(211, 211, 211, 0.75);
}
#loginOptionCancelButton:disabled {
color: rgba(211, 211, 211, 0.75);
pointer-events: none;
}
/*******************************************************************************
* *
* Settings View (sttings.ejs) *
@ -1058,6 +1228,59 @@ body, button {
font-size: 12px;
}
/* Selected server content container */
.settingsSelServContainer {
background: rgba(0, 0, 0, 0.25);
width: 75%;
border-radius: 3px;
display: flex;
justify-content: space-between;
margin: 15px 0px;
}
/* Div which will be populated with the selected server's information. */
.settingsSelServContent {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 5px 0px;
}
/* Wrapper container for the switch server button. */
.settingsSwitchServerContainer {
display: flex;
align-items: center;
padding: 15px;
}
/* Button to switch server configurations on the mods tab. */
.settingsSwitchServerButton {
opacity: 0;
border: 1px solid rgb(255, 255, 255);
color: rgb(255, 255, 255);
background: none;
font-size: 12px;
border-radius: 3px;
font-family: 'Avenir Medium';
transition: 0.25s ease;
cursor: pointer;
outline: none;
}
.settingsSwitchServerButton:hover,
.settingsSwitchServerButton:focus {
box-shadow: 0px 0px 20px rgb(255, 255, 255);
background: rgba(255, 255, 255, 0.25);
}
.settingsSwitchServerButton:active {
box-shadow: 0px 0px 20px rgb(187, 187, 187);
background: rgba(187, 187, 187, 0.25);
border: 1px solid rgb(187, 187, 187);
color: rgb(187, 187, 187);
}
.settingsSelServContainer:hover .settingsSwitchServerButton {
opacity: 1;
}
/* Remove spin button from number inputs. */
#settingsContainer input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
@ -1233,34 +1456,26 @@ input:checked + .toggleSwitchSlider:before {
height: 30px;
}
/* File input for file selection. */
.settingsFileSelSel {
width: 0px;
height: 0px;
opacity: 0;
}
.settingsFileSelSel::-webkit-file-upload-button {
display: none;
}
/* Wrapper label to add a custom style to the file input. */
.settingsFileSelLabel {
border-left: 0px;
/* File selection button. */
.settingsFileSelButton {
border: 0px;
border-radius: 0px 3px 3px 0px;
font-size: 12px;
padding: 0px 5px;
cursor: pointer;
display: flex;
align-items: center;
background: rgba(126, 126, 126, 0.57);
transition: 0.25s ease;
white-space: nowrap;
outline: none;
}
.settingsFileSelLabel:hover,
.settingsFileSelLabel:focus,
.settingsFileSelSel:focus ~ #settingsJavaExecLabel {
.settingsFileSelButton:hover,
.settingsFileSelButton:focus {
text-shadow: 0px 0px 20px white;
}
.settingsFileSelButton:active {
text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75);
color: rgba(255, 255, 255, 0.75);
}
/* Description for the file selector. */
.settingsFileSelDesc {
@ -1277,45 +1492,65 @@ input:checked + .toggleSwitchSlider:before {
* Settings View (Account Tab)
* * */
/* Add account button styles. */
#settingsAddAccount {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(126, 126, 126, 0.57);
border-radius: 3px;
height: 50px;
.settingsAuthAccountTypeContainer {
display: flex;
width: 75%;
flex-direction: column;
}
.settingsAuthAccountTypeHeader {
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
padding: 10px 0px;
border-bottom: 1px solid #ffffff85;
margin-bottom: 30px;
}
.settingsAuthAccountTypeHeaderLeft {
display: flex;
column-gap: 5px;
}
/* Settings add account button styles. */
.settingsAddAuthAccount {
background: none;
border: none;
text-align: left;
padding: 0px 50px;
padding: 2px 0px;
color: white;
cursor: pointer;
outline: none;
transition: 0.25s ease;
}
#settingsAddAccount:hover,
#settingsAddAccount:focus {
background: rgba(54, 54, 54, 0.25);
text-shadow: 0px 0px 20px white;
.settingsAddAuthAccount:hover,
.settingsAddAuthAccount:focus {
text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white;
}
/* Settings auth accounts header. */
#settingsCurrentAccountsHeader {
margin: 20px 0px;
.settingsAddAuthAccount:active {
text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75);
color: rgba(255, 255, 255, 0.75);
}
.settingsAddAuthAccount:disabled {
color: rgba(255, 255, 255, 0.75);
pointer-events: none;
}
/* Auth account list container styles. */
#settingsCurrentAccounts {
.settingsCurrentAccounts {
margin-bottom: 5%;
}
#settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) {
.settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) {
margin-bottom: 10px;
}
#settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) {
.settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) {
margin-top: 10px;
}
/* Auth account shared styles. */
.settingsAuthAccount {
display: flex;
width: 75%;
background: rgba(0, 0, 0, 0.25);
border-radius: 3px;
border: 1px solid rgba(126, 126, 126, 0.57);
@ -1456,59 +1691,6 @@ input:checked + .toggleSwitchSlider:before {
* Settings View (Mods Tab)
* * */
/* Selected server content container */
#settingsSelServContainer {
background: rgba(0, 0, 0, 0.25);
width: 75%;
border-radius: 3px;
display: flex;
justify-content: space-between;
margin: 15px 0px;
}
/* Div which will be populated with the selected server's information. */
#settingsSelServContent {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 5px 0px;
}
/* Wrapper container for the switch server button. */
#settingsSwitchServerContainer {
display: flex;
align-items: center;
padding: 15px;
}
/* Button to switch server configurations on the mods tab. */
#settingsSwitchServerButton {
opacity: 0;
border: 1px solid rgb(255, 255, 255);
color: rgb(255, 255, 255);
background: none;
font-size: 12px;
border-radius: 3px;
font-family: 'Avenir Medium';
transition: 0.25s ease;
cursor: pointer;
outline: none;
}
#settingsSwitchServerButton:hover,
#settingsSwitchServerButton:focus {
box-shadow: 0px 0px 20px rgb(255, 255, 255);
background: rgba(255, 255, 255, 0.25);
}
#settingsSwitchServerButton:active {
box-shadow: 0px 0px 20px rgb(187, 187, 187);
background: rgba(187, 187, 187, 0.25);
border: 1px solid rgb(187, 187, 187);
color: rgb(187, 187, 187);
}
#settingsSelServContainer:hover #settingsSwitchServerButton {
opacity: 1;
}
/* Main content container for the mod elements. */
#settingsModsContainer {
width: 75%;
@ -3590,6 +3772,7 @@ input:checked + .toggleSwitchSlider:before {
font-size: 10px;
line-height: 10px;
font-weight: bold;
text-align: left;
}
/* Content container for the server listing's information. */

View File

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 244 KiB

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

View File

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 502 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 268 KiB

View File

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 456 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

Before

Width:  |  Height:  |  Size: 298 B

After

Width:  |  Height:  |  Size: 298 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 875 B

After

Width:  |  Height:  |  Size: 875 B

View File

Before

Width:  |  Height:  |  Size: 756 B

After

Width:  |  Height:  |  Size: 756 B

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

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 959 B

View File

Before

Width:  |  Height:  |  Size: 602 B

After

Width:  |  Height:  |  Size: 602 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 809 B

After

Width:  |  Height:  |  Size: 809 B

View File

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 932 B

View File

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 822 B

View File

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1018 B

View File

Before

Width:  |  Height:  |  Size: 907 B

After

Width:  |  Height:  |  Size: 907 B

View File

Before

Width:  |  Height:  |  Size: 700 B

After

Width:  |  Height:  |  Size: 700 B

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 654 B

After

Width:  |  Height:  |  Size: 654 B

View File

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

View File

@ -0,0 +1,793 @@
const fs = require('fs-extra')
const { LoggerUtil } = require('helios-core')
const os = require('os')
const path = require('path')
const logger = LoggerUtil.getLogger('ConfigManager')
const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME)
const dataPath = path.join(sysRoot, '.helioslauncher')
const launcherDir = require('@electron/remote').app.getPath('userData')
/**
* Retrieve the absolute path of the launcher directory.
*
* @returns {string} The absolute path of the launcher directory.
*/
exports.getLauncherDirectory = function(){
return launcherDir
}
/**
* Get the launcher's data directory. This is where all files related
* to game launch are installed (common, instances, java, etc).
*
* @returns {string} The absolute path of the launcher's data directory.
*/
exports.getDataDirectory = function(def = false){
return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory
}
/**
* Set the new data directory.
*
* @param {string} dataDirectory The new data directory.
*/
exports.setDataDirectory = function(dataDirectory){
config.settings.launcher.dataDirectory = dataDirectory
}
const configPath = path.join(exports.getLauncherDirectory(), 'config.json')
const configPathLEGACY = path.join(dataPath, 'config.json')
const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY)
exports.getAbsoluteMinRAM = function(ram){
if(ram?.minimum != null) {
return ram.minimum/1024
} else {
// Legacy behavior
const mem = os.totalmem()
return mem >= (6*1073741824) ? 3 : 2
}
}
exports.getAbsoluteMaxRAM = function(ram){
const mem = os.totalmem()
const gT16 = mem-(16*1073741824)
return Math.floor((mem-(gT16 > 0 ? (Number.parseInt(gT16/8) + (16*1073741824)/4) : mem/4))/1073741824)
}
function resolveSelectedRAM(ram) {
if(ram?.recommended != null) {
return `${ram.recommended}M`
} else {
// Legacy behavior
const mem = os.totalmem()
return mem >= (8*1073741824) ? '4G' : (mem >= (6*1073741824) ? '3G' : '2G')
}
}
/**
* Three types of values:
* Static = Explicitly declared.
* Dynamic = Calculated by a private function.
* Resolved = Resolved externally, defaults to null.
*/
const DEFAULT_CONFIG = {
settings: {
game: {
resWidth: 1280,
resHeight: 720,
fullscreen: false,
autoConnect: true,
launchDetached: true
},
launcher: {
allowPrerelease: false,
dataDirectory: dataPath
}
},
newsCache: {
date: null,
content: null,
dismissed: false
},
clientToken: null,
selectedServer: null, // Resolved
selectedAccount: null,
authenticationDatabase: {},
modConfigurations: [],
javaConfig: {}
}
let config = null
// Persistance Utility Functions
/**
* Save the current configuration to a file.
*/
exports.save = function(){
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8')
}
/**
* Load the configuration into memory. If a configuration file exists,
* that will be read and saved. Otherwise, a default configuration will
* be generated. Note that "resolved" values default to null and will
* need to be externally assigned.
*/
exports.load = function(){
let doLoad = true
if(!fs.existsSync(configPath)){
// Create all parent directories.
fs.ensureDirSync(path.join(configPath, '..'))
if(fs.existsSync(configPathLEGACY)){
fs.moveSync(configPathLEGACY, configPath)
} else {
doLoad = false
config = DEFAULT_CONFIG
exports.save()
}
}
if(doLoad){
let doValidate = false
try {
config = JSON.parse(fs.readFileSync(configPath, 'UTF-8'))
doValidate = true
} catch (err){
logger.error(err)
logger.info('Configuration file contains malformed JSON or is corrupt.')
logger.info('Generating a new configuration file.')
fs.ensureDirSync(path.join(configPath, '..'))
config = DEFAULT_CONFIG
exports.save()
}
if(doValidate){
config = validateKeySet(DEFAULT_CONFIG, config)
exports.save()
}
}
logger.info('Successfully Loaded')
}
/**
* @returns {boolean} Whether or not the manager has been loaded.
*/
exports.isLoaded = function(){
return config != null
}
/**
* Validate that the destination object has at least every field
* present in the source object. Assign a default value otherwise.
*
* @param {Object} srcObj The source object to reference against.
* @param {Object} destObj The destination object.
* @returns {Object} A validated destination object.
*/
function validateKeySet(srcObj, destObj){
if(srcObj == null){
srcObj = {}
}
const validationBlacklist = ['authenticationDatabase', 'javaConfig']
const keys = Object.keys(srcObj)
for(let i=0; i<keys.length; i++){
if(typeof destObj[keys[i]] === 'undefined'){
destObj[keys[i]] = srcObj[keys[i]]
} else if(typeof srcObj[keys[i]] === 'object' && srcObj[keys[i]] != null && !(srcObj[keys[i]] instanceof Array) && validationBlacklist.indexOf(keys[i]) === -1){
destObj[keys[i]] = validateKeySet(srcObj[keys[i]], destObj[keys[i]])
}
}
return destObj
}
/**
* Check to see if this is the first time the user has launched the
* application. This is determined by the existance of the data path.
*
* @returns {boolean} True if this is the first launch, otherwise false.
*/
exports.isFirstLaunch = function(){
return firstLaunch
}
/**
* Returns the name of the folder in the OS temp directory which we
* will use to extract and store native dependencies for game launch.
*
* @returns {string} The name of the folder.
*/
exports.getTempNativeFolder = function(){
return 'WCNatives'
}
// System Settings (Unconfigurable on UI)
/**
* Retrieve the news cache to determine
* whether or not there is newer news.
*
* @returns {Object} The news cache object.
*/
exports.getNewsCache = function(){
return config.newsCache
}
/**
* Set the new news cache object.
*
* @param {Object} newsCache The new news cache object.
*/
exports.setNewsCache = function(newsCache){
config.newsCache = newsCache
}
/**
* Set whether or not the news has been dismissed (checked)
*
* @param {boolean} dismissed Whether or not the news has been dismissed (checked).
*/
exports.setNewsCacheDismissed = function(dismissed){
config.newsCache.dismissed = dismissed
}
/**
* Retrieve the common directory for shared
* game files (assets, libraries, etc).
*
* @returns {string} The launcher's common directory.
*/
exports.getCommonDirectory = function(){
return path.join(exports.getDataDirectory(), 'common')
}
/**
* Retrieve the instance directory for the per
* server game directories.
*
* @returns {string} The launcher's instance directory.
*/
exports.getInstanceDirectory = function(){
return path.join(exports.getDataDirectory(), 'instances')
}
/**
* Retrieve the launcher's Client Token.
* There is no default client token.
*
* @returns {string} The launcher's Client Token.
*/
exports.getClientToken = function(){
return config.clientToken
}
/**
* Set the launcher's Client Token.
*
* @param {string} clientToken The launcher's new Client Token.
*/
exports.setClientToken = function(clientToken){
config.clientToken = clientToken
}
/**
* Retrieve the ID of the selected serverpack.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {string} The ID of the selected serverpack.
*/
exports.getSelectedServer = function(def = false){
return !def ? config.selectedServer : DEFAULT_CONFIG.clientToken
}
/**
* Set the ID of the selected serverpack.
*
* @param {string} serverID The ID of the new selected serverpack.
*/
exports.setSelectedServer = function(serverID){
config.selectedServer = serverID
}
/**
* Get an array of each account currently authenticated by the launcher.
*
* @returns {Array.<Object>} An array of each stored authenticated account.
*/
exports.getAuthAccounts = function(){
return config.authenticationDatabase
}
/**
* Returns the authenticated account with the given uuid. Value may
* be null.
*
* @param {string} uuid The uuid of the authenticated account.
* @returns {Object} The authenticated account with the given uuid.
*/
exports.getAuthAccount = function(uuid){
return config.authenticationDatabase[uuid]
}
/**
* Update the access token of an authenticated mojang account.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The new Access Token.
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.updateMojangAuthAccount = function(uuid, accessToken){
config.authenticationDatabase[uuid].accessToken = accessToken
config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion.
return config.authenticationDatabase[uuid]
}
/**
* Adds an authenticated mojang account to the database to be stored.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The accessToken of the authenticated account.
* @param {string} username The username (usually email) of the authenticated account.
* @param {string} displayName The in game name of the authenticated account.
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){
config.selectedAccount = uuid
config.authenticationDatabase[uuid] = {
type: 'mojang',
accessToken,
username: username.trim(),
uuid: uuid.trim(),
displayName: displayName.trim()
}
return config.authenticationDatabase[uuid]
}
/**
* Update the tokens of an authenticated microsoft account.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The new Access Token.
* @param {string} msAccessToken The new Microsoft Access Token
* @param {string} msRefreshToken The new Microsoft Refresh Token
* @param {date} msExpires The date when the microsoft access token expires
* @param {date} mcExpires The date when the mojang access token expires
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) {
config.authenticationDatabase[uuid].accessToken = accessToken
config.authenticationDatabase[uuid].expiresAt = mcExpires
config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken
config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken
config.authenticationDatabase[uuid].microsoft.expires_at = msExpires
return config.authenticationDatabase[uuid]
}
/**
* Adds an authenticated microsoft account to the database to be stored.
*
* @param {string} uuid The uuid of the authenticated account.
* @param {string} accessToken The accessToken of the authenticated account.
* @param {string} name The in game name of the authenticated account.
* @param {date} mcExpires The date when the mojang access token expires
* @param {string} msAccessToken The microsoft access token
* @param {string} msRefreshToken The microsoft refresh token
* @param {date} msExpires The date when the microsoft access token expires
*
* @returns {Object} The authenticated account object created by this action.
*/
exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) {
config.selectedAccount = uuid
config.authenticationDatabase[uuid] = {
type: 'microsoft',
accessToken,
username: name.trim(),
uuid: uuid.trim(),
displayName: name.trim(),
expiresAt: mcExpires,
microsoft: {
access_token: msAccessToken,
refresh_token: msRefreshToken,
expires_at: msExpires
}
}
return config.authenticationDatabase[uuid]
}
/**
* Remove an authenticated account from the database. If the account
* was also the selected account, a new one will be selected. If there
* are no accounts, the selected account will be null.
*
* @param {string} uuid The uuid of the authenticated account.
*
* @returns {boolean} True if the account was removed, false if it never existed.
*/
exports.removeAuthAccount = function(uuid){
if(config.authenticationDatabase[uuid] != null){
delete config.authenticationDatabase[uuid]
if(config.selectedAccount === uuid){
const keys = Object.keys(config.authenticationDatabase)
if(keys.length > 0){
config.selectedAccount = keys[0]
} else {
config.selectedAccount = null
config.clientToken = null
}
}
return true
}
return false
}
/**
* Get the currently selected authenticated account.
*
* @returns {Object} The selected authenticated account.
*/
exports.getSelectedAccount = function(){
return config.authenticationDatabase[config.selectedAccount]
}
/**
* Set the selected authenticated account.
*
* @param {string} uuid The UUID of the account which is to be set
* as the selected account.
*
* @returns {Object} The selected authenticated account.
*/
exports.setSelectedAccount = function(uuid){
const authAcc = config.authenticationDatabase[uuid]
if(authAcc != null) {
config.selectedAccount = uuid
}
return authAcc
}
/**
* Get an array of each mod configuration currently stored.
*
* @returns {Array.<Object>} An array of each stored mod configuration.
*/
exports.getModConfigurations = function(){
return config.modConfigurations
}
/**
* Set the array of stored mod configurations.
*
* @param {Array.<Object>} configurations An array of mod configurations.
*/
exports.setModConfigurations = function(configurations){
config.modConfigurations = configurations
}
/**
* Get the mod configuration for a specific server.
*
* @param {string} serverid The id of the server.
* @returns {Object} The mod configuration for the given server.
*/
exports.getModConfiguration = function(serverid){
const cfgs = config.modConfigurations
for(let i=0; i<cfgs.length; i++){
if(cfgs[i].id === serverid){
return cfgs[i]
}
}
return null
}
/**
* Set the mod configuration for a specific server. This overrides any existing value.
*
* @param {string} serverid The id of the server for the given mod configuration.
* @param {Object} configuration The mod configuration for the given server.
*/
exports.setModConfiguration = function(serverid, configuration){
const cfgs = config.modConfigurations
for(let i=0; i<cfgs.length; i++){
if(cfgs[i].id === serverid){
cfgs[i] = configuration
return
}
}
cfgs.push(configuration)
}
// User Configurable Settings
// Java Settings
function defaultJavaConfig(effectiveJavaOptions, ram) {
if(effectiveJavaOptions.suggestedMajor > 8) {
return defaultJavaConfig17(ram)
} else {
return defaultJavaConfig8(ram)
}
}
function defaultJavaConfig8(ram) {
return {
minRAM: resolveSelectedRAM(ram),
maxRAM: resolveSelectedRAM(ram),
executable: null,
jvmOptions: [
'-XX:+UseConcMarkSweepGC',
'-XX:+CMSIncrementalMode',
'-XX:-UseAdaptiveSizePolicy',
'-Xmn128M'
],
}
}
function defaultJavaConfig17(ram) {
return {
minRAM: resolveSelectedRAM(ram),
maxRAM: resolveSelectedRAM(ram),
executable: null,
jvmOptions: [
'-XX:+UnlockExperimentalVMOptions',
'-XX:+UseG1GC',
'-XX:G1NewSizePercent=20',
'-XX:G1ReservePercent=20',
'-XX:MaxGCPauseMillis=50',
'-XX:G1HeapRegionSize=32M'
],
}
}
/**
* Ensure a java config property is set for the given server.
*
* @param {string} serverid The server id.
* @param {*} mcVersion The minecraft version of the server.
*/
exports.ensureJavaConfig = function(serverid, effectiveJavaOptions, ram) {
if(!Object.prototype.hasOwnProperty.call(config.javaConfig, serverid)) {
config.javaConfig[serverid] = defaultJavaConfig(effectiveJavaOptions, ram)
}
}
/**
* Retrieve the minimum amount of memory for JVM initialization. This value
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {string} serverid The server id.
* @returns {string} The minimum amount of memory for JVM initialization.
*/
exports.getMinRAM = function(serverid){
return config.javaConfig[serverid].minRAM
}
/**
* Set the minimum amount of memory for JVM initialization. This value should
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {string} serverid The server id.
* @param {string} minRAM The new minimum amount of memory for JVM initialization.
*/
exports.setMinRAM = function(serverid, minRAM){
config.javaConfig[serverid].minRAM = minRAM
}
/**
* Retrieve the maximum amount of memory for JVM initialization. This value
* contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {string} serverid The server id.
* @returns {string} The maximum amount of memory for JVM initialization.
*/
exports.getMaxRAM = function(serverid){
return config.javaConfig[serverid].maxRAM
}
/**
* Set the maximum amount of memory for JVM initialization. This value should
* contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' =
* 1024 MegaBytes, etc.
*
* @param {string} serverid The server id.
* @param {string} maxRAM The new maximum amount of memory for JVM initialization.
*/
exports.setMaxRAM = function(serverid, maxRAM){
config.javaConfig[serverid].maxRAM = maxRAM
}
/**
* Retrieve the path of the Java Executable.
*
* This is a resolved configuration value and defaults to null until externally assigned.
*
* @param {string} serverid The server id.
* @returns {string} The path of the Java Executable.
*/
exports.getJavaExecutable = function(serverid){
return config.javaConfig[serverid].executable
}
/**
* Set the path of the Java Executable.
*
* @param {string} serverid The server id.
* @param {string} executable The new path of the Java Executable.
*/
exports.setJavaExecutable = function(serverid, executable){
config.javaConfig[serverid].executable = executable
}
/**
* Retrieve the additional arguments for JVM initialization. Required arguments,
* such as memory allocation, will be dynamically resolved and will not be included
* in this value.
*
* @param {string} serverid The server id.
* @returns {Array.<string>} An array of the additional arguments for JVM initialization.
*/
exports.getJVMOptions = function(serverid){
return config.javaConfig[serverid].jvmOptions
}
/**
* Set the additional arguments for JVM initialization. Required arguments,
* such as memory allocation, will be dynamically resolved and should not be
* included in this value.
*
* @param {string} serverid The server id.
* @param {Array.<string>} jvmOptions An array of the new additional arguments for JVM
* initialization.
*/
exports.setJVMOptions = function(serverid, jvmOptions){
config.javaConfig[serverid].jvmOptions = jvmOptions
}
// Game Settings
/**
* Retrieve the width of the game window.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {number} The width of the game window.
*/
exports.getGameWidth = function(def = false){
return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth
}
/**
* Set the width of the game window.
*
* @param {number} resWidth The new width of the game window.
*/
exports.setGameWidth = function(resWidth){
config.settings.game.resWidth = Number.parseInt(resWidth)
}
/**
* Validate a potential new width value.
*
* @param {number} resWidth The width value to validate.
* @returns {boolean} Whether or not the value is valid.
*/
exports.validateGameWidth = function(resWidth){
const nVal = Number.parseInt(resWidth)
return Number.isInteger(nVal) && nVal >= 0
}
/**
* Retrieve the height of the game window.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {number} The height of the game window.
*/
exports.getGameHeight = function(def = false){
return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight
}
/**
* Set the height of the game window.
*
* @param {number} resHeight The new height of the game window.
*/
exports.setGameHeight = function(resHeight){
config.settings.game.resHeight = Number.parseInt(resHeight)
}
/**
* Validate a potential new height value.
*
* @param {number} resHeight The height value to validate.
* @returns {boolean} Whether or not the value is valid.
*/
exports.validateGameHeight = function(resHeight){
const nVal = Number.parseInt(resHeight)
return Number.isInteger(nVal) && nVal >= 0
}
/**
* Check if the game should be launched in fullscreen mode.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the game is set to launch in fullscreen mode.
*/
exports.getFullscreen = function(def = false){
return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen
}
/**
* Change the status of if the game should be launched in fullscreen mode.
*
* @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode.
*/
exports.setFullscreen = function(fullscreen){
config.settings.game.fullscreen = fullscreen
}
/**
* Check if the game should auto connect to servers.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the game should auto connect to servers.
*/
exports.getAutoConnect = function(def = false){
return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect
}
/**
* Change the status of whether or not the game should auto connect to servers.
*
* @param {boolean} autoConnect Whether or not the game should auto connect to servers.
*/
exports.setAutoConnect = function(autoConnect){
config.settings.game.autoConnect = autoConnect
}
/**
* Check if the game should launch as a detached process.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the game will launch as a detached process.
*/
exports.getLaunchDetached = function(def = false){
return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached
}
/**
* Change the status of whether or not the game should launch as a detached process.
*
* @param {boolean} launchDetached Whether or not the game should launch as a detached process.
*/
exports.setLaunchDetached = function(launchDetached){
config.settings.game.launchDetached = launchDetached
}
// Launcher Settings
/**
* Check if the launcher should download prerelease versions.
*
* @param {boolean} def Optional. If true, the default value will be returned.
* @returns {boolean} Whether or not the launcher should download prerelease versions.
*/
exports.getAllowPrerelease = function(def = false){
return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease
}
/**
* Change the status of Whether or not the launcher should download prerelease versions.
*
* @param {boolean} launchDetached Whether or not the launcher should download prerelease versions.
*/
exports.setAllowPrerelease = function(allowPrerelease){
config.settings.launcher.allowPrerelease = allowPrerelease
}

View File

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

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

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

View File

@ -0,0 +1,894 @@
const AdmZip = require('adm-zip')
const child_process = require('child_process')
const crypto = require('crypto')
const fs = require('fs-extra')
const { LoggerUtil } = require('helios-core')
const { getMojangOS, isLibraryCompatible, mcVersionAtLeast } = require('helios-core/common')
const { Type } = require('helios-distribution-types')
const os = require('os')
const path = require('path')
const ConfigManager = require('./configmanager')
const logger = LoggerUtil.getLogger('ProcessBuilder')
/**
* Only forge and fabric are top level mod loaders.
*
* Forge 1.13+ launch logic is similar to fabrics, for now using usingFabricLoader flag to
* change minor details when needed.
*
* Rewrite of this module may be needed in the future.
*/
class ProcessBuilder {
constructor(distroServer, vanillaManifest, modManifest, authUser, launcherVersion){
this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.rawServer.id)
this.commonDir = ConfigManager.getCommonDirectory()
this.server = distroServer
this.vanillaManifest = vanillaManifest
this.modManifest = modManifest
this.authUser = authUser
this.launcherVersion = launcherVersion
this.forgeModListFile = path.join(this.gameDir, 'forgeMods.list') // 1.13+
this.fmlDir = path.join(this.gameDir, 'forgeModList.json')
this.llDir = path.join(this.gameDir, 'liteloaderModList.json')
this.libPath = path.join(this.commonDir, 'libraries')
this.usingLiteLoader = false
this.usingFabricLoader = false
this.llPath = null
}
/**
* Convienence method to run the functions typically used to build a process.
*/
build(){
fs.ensureDirSync(this.gameDir)
const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex'))
process.throwDeprecation = true
this.setupLiteLoader()
logger.info('Using liteloader:', this.usingLiteLoader)
this.usingFabricLoader = this.server.modules.some(mdl => mdl.rawModule.type === Type.Fabric)
logger.info('Using fabric loader:', this.usingFabricLoader)
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.rawServer.id).mods, this.server.modules)
// Mod list below 1.13
// Fabric only supports 1.14+
if(!mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
this.constructJSONModList('forge', modObj.fMods, true)
if(this.usingLiteLoader){
this.constructJSONModList('liteloader', modObj.lMods, true)
}
}
const uberModArr = modObj.fMods.concat(modObj.lMods)
let args = this.constructJVMArguments(uberModArr, tempNativePath)
if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
//args = args.concat(this.constructModArguments(modObj.fMods))
args = args.concat(this.constructModList(modObj.fMods))
}
logger.info('Launch Arguments:', args)
const child = child_process.spawn(ConfigManager.getJavaExecutable(this.server.rawServer.id), args, {
cwd: this.gameDir,
detached: ConfigManager.getLaunchDetached()
})
if(ConfigManager.getLaunchDetached()){
child.unref()
}
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
child.stdout.on('data', (data) => {
data.trim().split('\n').forEach(x => console.log(`\x1b[32m[Minecraft]\x1b[0m ${x}`))
})
child.stderr.on('data', (data) => {
data.trim().split('\n').forEach(x => console.log(`\x1b[31m[Minecraft]\x1b[0m ${x}`))
})
child.on('close', (code, signal) => {
logger.info('Exited with code', code)
fs.remove(tempNativePath, (err) => {
if(err){
logger.warn('Error while deleting temp dir', err)
} else {
logger.info('Temp dir deleted successfully.')
}
})
})
return child
}
/**
* Get the platform specific classpath separator. On windows, this is a semicolon.
* On Unix, this is a colon.
*
* @returns {string} The classpath separator for the current operating system.
*/
static getClasspathSeparator() {
return process.platform === 'win32' ? ';' : ':'
}
/**
* Determine if an optional mod is enabled from its configuration value. If the
* configuration value is null, the required object will be used to
* determine if it is enabled.
*
* A mod is enabled if:
* * The configuration is not null and one of the following:
* * The configuration is a boolean and true.
* * The configuration is an object and its 'value' property is true.
* * The configuration is null and one of the following:
* * The required object is null.
* * The required object's 'def' property is null or true.
*
* @param {Object | boolean} modCfg The mod configuration object.
* @param {Object} required Optional. The required object from the mod's distro declaration.
* @returns {boolean} True if the mod is enabled, false otherwise.
*/
static isModEnabled(modCfg, required = null){
return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.def : true
}
/**
* Function which performs a preliminary scan of the top level
* mods. If liteloader is present here, we setup the special liteloader
* launch options. Note that liteloader is only allowed as a top level
* mod. It must not be declared as a submodule.
*/
setupLiteLoader(){
for(let ll of this.server.modules){
if(ll.rawModule.type === Type.LiteLoader){
if(!ll.getRequired().value){
const modCfg = ConfigManager.getModConfiguration(this.server.rawServer.id).mods
if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessMavenIdentifier()], ll.getRequired())){
if(fs.existsSync(ll.getPath())){
this.usingLiteLoader = true
this.llPath = ll.getPath()
}
}
} else {
if(fs.existsSync(ll.getPath())){
this.usingLiteLoader = true
this.llPath = ll.getPath()
}
}
}
}
}
/**
* Resolve an array of all enabled mods. These mods will be constructed into
* a mod list format and enabled at launch.
*
* @param {Object} modCfg The mod configuration object.
* @param {Array.<Object>} mdls An array of modules to parse.
* @returns {{fMods: Array.<Object>, lMods: Array.<Object>}} An object which contains
* a list of enabled forge mods and litemods.
*/
resolveModConfiguration(modCfg, mdls){
let fMods = []
let lMods = []
for(let mdl of mdls){
const type = mdl.rawModule.type
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
const o = !mdl.getRequired().value
const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessMavenIdentifier()], mdl.getRequired())
if(!o || (o && e)){
if(mdl.subModules.length > 0){
const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessMavenIdentifier()].mods, mdl.subModules)
fMods = fMods.concat(v.fMods)
lMods = lMods.concat(v.lMods)
if(type === Type.LiteLoader){
continue
}
}
if(type === Type.ForgeMod || type === Type.FabricMod){
fMods.push(mdl)
} else {
lMods.push(mdl)
}
}
}
}
return {
fMods,
lMods
}
}
_lteMinorVersion(version) {
return Number(this.modManifest.id.split('-')[0].split('.')[1]) <= Number(version)
}
/**
* Test to see if this version of forge requires the absolute: prefix
* on the modListFile repository field.
*/
_requiresAbsolute(){
try {
if(this._lteMinorVersion(9)) {
return false
}
const ver = this.modManifest.id.split('-')[2]
const pts = ver.split('.')
const min = [14, 23, 3, 2655]
for(let i=0; i<pts.length; i++){
const parsed = Number.parseInt(pts[i])
if(parsed < min[i]){
return false
} else if(parsed > min[i]){
return true
}
}
} catch (err) {
// We know old forge versions follow this format.
// Error must be caused by newer version.
}
// Equal or errored
return true
}
/**
* Construct a mod list json object.
*
* @param {'forge' | 'liteloader'} type The mod list type to construct.
* @param {Array.<Object>} mods An array of mods to add to the mod list.
* @param {boolean} save Optional. Whether or not we should save the mod list file.
*/
constructJSONModList(type, mods, save = false){
const modList = {
repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + path.join(this.commonDir, 'modstore')
}
const ids = []
if(type === 'forge'){
for(let mod of mods){
ids.push(mod.getExtensionlessMavenIdentifier())
}
} else {
for(let mod of mods){
ids.push(mod.getMavenIdentifier())
}
}
modList.modRef = ids
if(save){
const json = JSON.stringify(modList, null, 4)
fs.writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8')
}
return modList
}
// /**
// * Construct the mod argument list for forge 1.13
// *
// * @param {Array.<Object>} mods An array of mods to add to the mod list.
// */
// constructModArguments(mods){
// const argStr = mods.map(mod => {
// return mod.getExtensionlessMavenIdentifier()
// }).join(',')
// if(argStr){
// return [
// '--fml.mavenRoots',
// path.join('..', '..', 'common', 'modstore'),
// '--fml.mods',
// argStr
// ]
// } else {
// return []
// }
// }
/**
* Construct the mod argument list for forge 1.13 and Fabric
*
* @param {Array.<Object>} mods An array of mods to add to the mod list.
*/
constructModList(mods) {
const writeBuffer = mods.map(mod => {
return this.usingFabricLoader ? mod.getPath() : mod.getExtensionlessMavenIdentifier()
}).join('\n')
if(writeBuffer) {
fs.writeFileSync(this.forgeModListFile, writeBuffer, 'UTF-8')
return this.usingFabricLoader ? [
'--fabric.addMods',
`@${this.forgeModListFile}`
] : [
'--fml.mavenRoots',
path.join('..', '..', 'common', 'modstore'),
'--fml.modLists',
this.forgeModListFile
]
} else {
return []
}
}
_processAutoConnectArg(args){
if(ConfigManager.getAutoConnect() && this.server.rawServer.autoconnect){
if(mcVersionAtLeast('1.20', this.server.rawServer.minecraftVersion)){
args.push('--quickPlayMultiplayer')
args.push(`${this.server.hostname}:${this.server.port}`)
} else {
args.push('--server')
args.push(this.server.hostname)
args.push('--port')
args.push(this.server.port)
}
}
}
/**
* Construct the argument array that will be passed to the JVM process.
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/
constructJVMArguments(mods, tempNativePath){
if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){
return this._constructJVMArguments113(mods, tempNativePath)
} else {
return this._constructJVMArguments112(mods, tempNativePath)
}
}
/**
* Construct the argument array that will be passed to the JVM process.
* This function is for 1.12 and below.
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/
_constructJVMArguments112(mods, tempNativePath){
let args = []
// Classpath Argument
args.push('-cp')
args.push(this.classpathArg(mods, tempNativePath).join(ProcessBuilder.getClasspathSeparator()))
// Java Arguments
if(process.platform === 'darwin'){
args.push('-Xdock:name=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.
*/
_resolveForgeArgs(){
const mcArgs = this.modManifest.minecraftArguments.split(' ')
const argDiscovery = /\${*(.*)}/
// Replace the declared variables with their proper values.
for(let i=0; i<mcArgs.length; ++i){
if(argDiscovery.test(mcArgs[i])){
const identifier = mcArgs[i].match(argDiscovery)[1]
let val = null
switch(identifier){
case 'auth_player_name':
val = this.authUser.displayName.trim()
break
case 'version_name':
//val = vanillaManifest.id
val = this.server.rawServer.id
break
case 'game_directory':
val = this.gameDir
break
case 'assets_root':
val = path.join(this.commonDir, 'assets')
break
case 'assets_index_name':
val = this.vanillaManifest.assets
break
case 'auth_uuid':
val = this.authUser.uuid.trim()
break
case 'auth_access_token':
val = this.authUser.accessToken
break
case 'user_type':
val = this.authUser.type === 'microsoft' ? 'msa' : 'mojang'
break
case 'user_properties': // 1.8.9 and below.
val = '{}'
break
case 'version_type':
val = this.vanillaManifest.type
break
}
if(val != null){
mcArgs[i] = val
}
}
}
// Autoconnect to the selected server.
this._processAutoConnectArg(mcArgs)
// Prepare game resolution
if(ConfigManager.getFullscreen()){
mcArgs.push('--fullscreen')
mcArgs.push(true)
} else {
mcArgs.push('--width')
mcArgs.push(ConfigManager.getGameWidth())
mcArgs.push('--height')
mcArgs.push(ConfigManager.getGameHeight())
}
// Mod List File Argument
mcArgs.push('--modListFile')
if(this._lteMinorVersion(9)) {
mcArgs.push(path.basename(this.fmlDir))
} else {
mcArgs.push('absolute:' + this.fmlDir)
}
// LiteLoader
if(this.usingLiteLoader){
mcArgs.push('--modRepo')
mcArgs.push(this.llDir)
// Set first arg to liteloader tweak class
mcArgs.unshift('com.mumfrey.liteloader.launch.LiteLoaderTweaker')
mcArgs.unshift('--tweakClass')
}
return mcArgs
}
/**
* Ensure that the classpath entries all point to jar files.
*
* @param {Array.<String>} list Array of classpath entries.
*/
_processClassPathList(list) {
const ext = '.jar'
const extLen = ext.length
for(let i=0; i<list.length; i++) {
const extIndex = list[i].indexOf(ext)
if(extIndex > -1 && extIndex !== list[i].length - extLen) {
list[i] = list[i].substring(0, extIndex + extLen)
}
}
}
/**
* Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared
* libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries,
* this method requires all enabled mods as an input
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the paths of each library required by this process.
*/
classpathArg(mods, tempNativePath){
let cpArgs = []
if(!mcVersionAtLeast('1.17', this.server.rawServer.minecraftVersion) || this.usingFabricLoader) {
// Add the version.jar to the classpath.
// Must not be added to the classpath for Forge 1.17+.
const version = this.vanillaManifest.id
cpArgs.push(path.join(this.commonDir, 'versions', version, version + '.jar'))
}
if(this.usingLiteLoader){
cpArgs.push(this.llPath)
}
// Resolve the Mojang declared libraries.
const mojangLibs = this._resolveMojangLibraries(tempNativePath)
// Resolve the server declared libraries.
const servLibs = this._resolveServerLibraries(mods)
// Merge libraries, server libs with the same
// maven identifier will override the mojang ones.
// Ex. 1.7.10 forge overrides mojang's guava with newer version.
const finalLibs = {...mojangLibs, ...servLibs}
cpArgs = cpArgs.concat(Object.values(finalLibs))
this._processClassPathList(cpArgs)
return cpArgs
}
/**
* Resolve the libraries defined by Mojang's version data. This method will also extract
* native libraries and point to the correct location for its classpath.
*
* TODO - clean up function
*
* @param {string} tempNativePath The path to store the native libraries.
* @returns {{[id: string]: string}} An object containing the paths of each library mojang declares.
*/
_resolveMojangLibraries(tempNativePath){
const nativesRegex = /.+:natives-([^-]+)(?:-(.+))?/
const libs = {}
const libArr = this.vanillaManifest.libraries
fs.ensureDirSync(tempNativePath)
for(let i=0; i<libArr.length; i++){
const lib = libArr[i]
if(isLibraryCompatible(lib.rules, lib.natives)){
// Pre-1.19 has a natives object.
if(lib.natives != null) {
// Extract the native library.
const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/']
const artifact = lib.downloads.classifiers[lib.natives[getMojangOS()].replace('${arch}', process.arch.replace('x', ''))]
// Location of native zip.
const to = path.join(this.libPath, artifact.path)
let zip = new AdmZip(to)
let zipEntries = zip.getEntries()
// Unzip the native zip.
for(let i=0; i<zipEntries.length; i++){
const fileName = zipEntries[i].entryName
let shouldExclude = false
// Exclude noted files.
exclusionArr.forEach(function(exclusion){
if(fileName.indexOf(exclusion) > -1){
shouldExclude = true
}
})
// Extract the file.
if(!shouldExclude){
fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
if(err){
logger.error('Error while extracting native library:', err)
}
})
}
}
}
// 1.19+ logic
else if(lib.name.includes('natives-')) {
const regexTest = nativesRegex.exec(lib.name)
// const os = regexTest[1]
const arch = regexTest[2] ?? 'x64'
if(arch != process.arch) {
continue
}
// Extract the native library.
const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/', '.git', '.sha1']
const artifact = lib.downloads.artifact
// Location of native zip.
const to = path.join(this.libPath, artifact.path)
let zip = new AdmZip(to)
let zipEntries = zip.getEntries()
// Unzip the native zip.
for(let i=0; i<zipEntries.length; i++){
if(zipEntries[i].isDirectory) {
continue
}
const fileName = zipEntries[i].entryName
let shouldExclude = false
// Exclude noted files.
exclusionArr.forEach(function(exclusion){
if(fileName.indexOf(exclusion) > -1){
shouldExclude = true
}
})
const extractName = fileName.includes('/') ? fileName.substring(fileName.lastIndexOf('/')) : fileName
// Extract the file.
if(!shouldExclude){
fs.writeFile(path.join(tempNativePath, extractName), zipEntries[i].getData(), (err) => {
if(err){
logger.error('Error while extracting native library:', err)
}
})
}
}
}
// No natives
else {
const dlInfo = lib.downloads
const artifact = dlInfo.artifact
const to = path.join(this.libPath, artifact.path)
const versionIndependentId = lib.name.substring(0, lib.name.lastIndexOf(':'))
libs[versionIndependentId] = to
}
}
}
return libs
}
/**
* Resolve the libraries declared by this server in order to add them to the classpath.
* This method will also check each enabled mod for libraries, as mods are permitted to
* declare libraries.
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @returns {{[id: string]: string}} An object containing the paths of each library this server requires.
*/
_resolveServerLibraries(mods){
const mdls = this.server.modules
let libs = {}
// Locate Forge/Fabric/Libraries
for(let mdl of mdls){
const type = mdl.rawModule.type
if(type === Type.ForgeHosted || type === Type.Fabric || type === Type.Library){
libs[mdl.getVersionlessMavenIdentifier()] = mdl.getPath()
if(mdl.subModules.length > 0){
const res = this._resolveModuleLibraries(mdl)
if(res.length > 0){
libs = {...libs, ...res}
}
}
}
}
//Check for any libraries in our mod list.
for(let i=0; i<mods.length; i++){
if(mods.sub_modules != null){
const res = this._resolveModuleLibraries(mods[i])
if(res.length > 0){
libs = {...libs, ...res}
}
}
}
return libs
}
/**
* Recursively resolve the path of each library required by this module.
*
* @param {Object} mdl A module object from the server distro index.
* @returns {Array.<string>} An array containing the paths of each library this module requires.
*/
_resolveModuleLibraries(mdl){
if(!mdl.subModules.length > 0){
return []
}
let libs = []
for(let sm of mdl.subModules){
if(sm.rawModule.type === Type.Library){
if(sm.rawModule.classpath ?? true) {
libs.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(mdl.subModules.length > 0){
const res = this._resolveModuleLibraries(sm)
if(res.length > 0){
libs = libs.concat(res)
}
}
}
return libs
}
}
module.exports = ProcessBuilder

File diff suppressed because it is too large Load Diff

View File

@ -21,8 +21,6 @@ const loginForm = document.getElementById('loginForm')
// Control variables.
let lu = false, lp = false
const loggerLogin = new LoggerUtil('%c[Login]', 'color: #000668; font-weight: bold')
/**
* Show a login error.
@ -154,79 +152,6 @@ function formDisabled(v){
loginRememberOption.disabled = v
}
/**
* Parses an error and returns a user-friendly title and description
* for our error overlay.
*
* @param {Error | {cause: string, error: string, errorMessage: string}} err A Node.js
* error or Mojang error response.
*/
function resolveError(err){
// Mojang Response => err.cause | err.error | err.errorMessage
// Node error => err.code | err.message
if(err.cause != null && err.cause === 'UserMigratedException') {
return {
title: Lang.queryJS('login.error.userMigrated.title'),
desc: Lang.queryJS('login.error.userMigrated.desc')
}
} else {
if(err.error != null){
if(err.error === 'ForbiddenOperationException'){
if(err.errorMessage != null){
if(err.errorMessage === 'Invalid credentials. Invalid username or password.'){
return {
title: Lang.queryJS('login.error.invalidCredentials.title'),
desc: Lang.queryJS('login.error.invalidCredentials.desc')
}
} else if(err.errorMessage === 'Invalid credentials.'){
return {
title: Lang.queryJS('login.error.rateLimit.title'),
desc: Lang.queryJS('login.error.rateLimit.desc')
}
}
}
}
} else {
// Request errors (from Node).
if(err.code != null){
if(err.code === 'ENOENT'){
// No Internet.
return {
title: Lang.queryJS('login.error.noInternet.title'),
desc: Lang.queryJS('login.error.noInternet.desc')
}
} else if(err.code === 'ENOTFOUND'){
// Could not reach server.
return {
title: Lang.queryJS('login.error.authDown.title'),
desc: Lang.queryJS('login.error.authDown.desc')
}
}
}
}
}
if(err.message != null){
if(err.message === 'NotPaidAccount'){
return {
title: Lang.queryJS('login.error.notPaid.title'),
desc: Lang.queryJS('login.error.notPaid.desc')
}
} else {
// Unknown error with request.
return {
title: Lang.queryJS('login.error.unknown.title'),
desc: err.message
}
}
} else {
// Unknown Mojang error.
return {
title: err.error,
desc: err.errorMessage
}
}
}
let loginViewOnSuccess = VIEWS.landing
let loginViewOnCancel = VIEWS.settings
let loginViewCancelHandler
@ -262,16 +187,16 @@ loginButton.addEventListener('click', () => {
// Show loading stuff.
loginLoading(true)
AuthManager.addAccount(loginUsername.value, loginPassword.value).then((value) => {
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, () => {
switchView(VIEWS.login, loginViewOnSuccess, 500, 500, async () => {
// Temporary workaround
if(loginViewOnSuccess === VIEWS.settings){
prepareSettings()
await prepareSettings()
}
loginViewOnSuccess = VIEWS.landing // Reset this for good measure.
loginCancelEnabled(false) // Reset this for good measure.
@ -285,16 +210,25 @@ loginButton.addEventListener('click', () => {
formDisabled(false)
})
}, 1000)
}).catch((err) => {
}).catch((displayableError) => {
loginLoading(false)
const errF = resolveError(err)
setOverlayContent(errF.title, errF.desc, Lang.queryJS('login.tryAgain'))
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)
loggerLogin.log('Error while logging in.', err)
})
})

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

@ -117,8 +117,8 @@ function toggleOverlay(toggleState, dismissable = false, content = 'overlayConte
}
}
function toggleServerSelection(toggleState){
prepareServerSelectionList()
async function toggleServerSelection(toggleState){
await prepareServerSelectionList()
toggleOverlay(toggleState, true, 'serverSelectContent')
}
@ -130,7 +130,7 @@ function toggleServerSelection(toggleState){
* @param {string} acknowledge Acknowledge button text.
* @param {string} dismiss Dismiss button text.
*/
function setOverlayContent(title, description, acknowledge, dismiss = 'Dismiss'){
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
@ -171,11 +171,11 @@ function setDismissHandler(handler){
/* Server Select View */
document.getElementById('serverSelectConfirm').addEventListener('click', () => {
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 = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid'))
const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid'))
updateSelectedServer(serv)
refreshServerStatus(true)
toggleOverlay(false)
@ -184,19 +184,22 @@ document.getElementById('serverSelectConfirm').addEventListener('click', () => {
}
// None are selected? Not possible right? Meh, handle it.
if(listings.length > 0){
const serv = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid'))
const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid'))
updateSelectedServer(serv)
toggleOverlay(false)
}
})
document.getElementById('accountSelectConfirm').addEventListener('click', () => {
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
@ -207,6 +210,9 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () =>
const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid'))
ConfigManager.save()
updateSelectedAccount(authAcc)
if(getCurrentView() === VIEWS.settings) {
await prepareSettings()
}
toggleOverlay(false)
validateSelectedAccount()
}
@ -261,21 +267,21 @@ function setAccountListingHandlers(){
})
}
function populateServerListings(){
const distro = DistroManager.getDistribution()
async function populateServerListings(){
const distro = await DistroAPI.getDistribution()
const giaSel = ConfigManager.getSelectedServer()
const servers = distro.getServers()
const servers = distro.servers
let htmlString = ''
for(const serv of servers){
htmlString += `<button class="serverListing" servid="${serv.getID()}" ${serv.getID() === giaSel ? 'selected' : ''}>
<img class="serverListingImg" src="${serv.getIcon()}"/>
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.getName()}</span>
<span class="serverListingDescription">${serv.getDescription()}</span>
<span class="serverListingName">${serv.rawServer.name}</span>
<span class="serverListingDescription">${serv.rawServer.description}</span>
<div class="serverListingInfo">
<div class="serverListingVersion">${serv.getMinecraftVersion()}</div>
<div class="serverListingRevision">${serv.getVersion()}</div>
${serv.isMainServer() ? `<div class="serverListingStarWrapper">
<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>
@ -283,7 +289,7 @@ function populateServerListings(){
<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">Main Server</span>
<span class="serverListingStarTooltip">${Lang.queryJS('settings.serverListing.mainServer')}</span>
</div>` : ''}
</div>
</div>
@ -299,7 +305,7 @@ function populateAccountListings(){
let htmlString = ''
for(let i=0; i<accounts.length; i++){
htmlString += `<button class="accountListing" uuid="${accounts[i].uuid}" ${i===0 ? 'selected' : ''}>
<img src="https://crafatar.com/renders/head/${accounts[i].uuid}?scale=2&default=MHF_Steve&overlay">
<img src="https://mc-heads.net/head/${accounts[i].uuid}/40">
<div class="accountListingName">${accounts[i].displayName}</div>
</button>`
}
@ -307,8 +313,8 @@ function populateAccountListings(){
}
function prepareServerSelectionList(){
populateServerListings()
async function prepareServerSelectionList(){
await populateServerListings()
setServerListingHandlers()
}

View File

@ -4,11 +4,11 @@
*/
// Requirements
const path = require('path')
const { Type } = require('helios-distribution-types')
const { AuthManager } = require('./../authmanager')
const ConfigManager = require('./../configmanager')
const DistroManager = require('./../distromanager')
const Lang = require('./../langloader')
const AuthManager = require('./assets/js/authmanager')
const ConfigManager = require('./assets/js/configmanager')
const { DistroAPI } = require('./assets/js/distromanager')
let rscShouldLoad = false
let fatalStartupError = false
@ -16,9 +16,11 @@ let fatalStartupError = false
// Mapping of each view to their container IDs.
const VIEWS = {
landing: '#landingContainer',
loginOptions: '#loginOptionsContainer',
login: '#loginContainer',
settings: '#settingsContainer',
welcome: '#welcomeContainer'
welcome: '#welcomeContainer',
waiting: '#waitingContainer'
}
// The currently shown view container.
@ -38,10 +40,10 @@ let currentView
*/
function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){
currentView = next
$(`${current}`).fadeOut(currentFadeTime, () => {
onCurrentFade()
$(`${next}`).fadeIn(nextFadeTime, () => {
onNextFade()
$(`${current}`).fadeOut(currentFadeTime, async () => {
await onCurrentFade()
$(`${next}`).fadeIn(nextFadeTime, async () => {
await onNextFade()
})
})
}
@ -55,15 +57,15 @@ function getCurrentView(){
return currentView
}
function showMainUI(data){
async function showMainUI(data){
if(!isDev){
loggerAutoUpdater.log('Initializing..')
loggerAutoUpdater.info('Initializing..')
ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease())
}
prepareSettings(true)
updateSelectedServer(data.getServer(ConfigManager.getSelectedServer()))
await prepareSettings(true)
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
refreshServerStatus()
setTimeout(() => {
document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
@ -86,8 +88,11 @@ function showMainUI(data){
currentView = VIEWS.landing
$(VIEWS.landing).fadeIn(1000)
} else {
currentView = VIEWS.login
$(VIEWS.login).fadeIn(1000)
loginOptionsCancelEnabled(false)
loginOptionsViewOnLoginSuccess = VIEWS.landing
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
currentView = VIEWS.loginOptions
$(VIEWS.loginOptions).fadeIn(1000)
}
}
@ -109,9 +114,9 @@ function showFatalStartupError(){
$('#loadingContainer').fadeOut(250, () => {
document.getElementById('overlayContainer').style.background = 'none'
setOverlayContent(
'Fatal Error: Unable to Load Distribution Index',
'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.',
'Close'
Lang.queryJS('uibinder.startup.fatalErrorTitle'),
Lang.queryJS('uibinder.startup.fatalErrorMessage'),
Lang.queryJS('uibinder.startup.closeButton')
)
setOverlayHandler(() => {
const window = remote.getCurrentWindow()
@ -128,10 +133,11 @@ function showFatalStartupError(){
* @param {Object} data The distro index object.
*/
function onDistroRefresh(data){
updateSelectedServer(data.getServer(ConfigManager.getSelectedServer()))
updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer()))
refreshServerStatus()
initNews()
syncModConfigurations(data)
ensureJavaSettings(data)
}
/**
@ -143,10 +149,10 @@ function syncModConfigurations(data){
const syncedCfgs = []
for(let serv of data.getServers()){
for(let serv of data.servers){
const id = serv.getID()
const mdls = serv.getModules()
const id = serv.rawServer.id
const mdls = serv.modules
const cfg = ConfigManager.getModConfiguration(id)
if(cfg != null){
@ -155,20 +161,20 @@ function syncModConfigurations(data){
const mods = {}
for(let mdl of mdls){
const type = mdl.getType()
const type = mdl.rawModule.type
if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
if(!mdl.getRequired().isRequired()){
const mdlID = mdl.getVersionlessID()
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.getSubModules(), mdl)
mods[mdlID] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.getSubModules(), mdl), false)
mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.subModules, mdl), false)
}
} else {
if(mdl.hasSubModules()){
const mdlID = mdl.getVersionlessID()
const v = scanOptionalSubModules(mdl.getSubModules(), mdl)
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
@ -191,15 +197,15 @@ function syncModConfigurations(data){
const mods = {}
for(let mdl of mdls){
const type = mdl.getType()
if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
if(!mdl.getRequired().isRequired()){
mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl)
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.hasSubModules()){
const v = scanOptionalSubModules(mdl.getSubModules(), mdl)
if(mdl.subModules.length > 0){
const v = scanOptionalSubModules(mdl.subModules, mdl)
if(typeof v === 'object'){
mods[mdl.getVersionlessID()] = v
mods[mdl.getVersionlessMavenIdentifier()] = v
}
}
}
@ -218,6 +224,21 @@ function syncModConfigurations(data){
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,
@ -230,17 +251,17 @@ function scanOptionalSubModules(mdls, origin){
const mods = {}
for(let mdl of mdls){
const type = mdl.getType()
const type = mdl.rawModule.type
// Optional types.
if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){
// It is optional.
if(!mdl.getRequired().isRequired()){
mods[mdl.getVersionlessID()] = scanOptionalSubModules(mdl.getSubModules(), mdl)
if(!mdl.getRequired().value){
mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl)
} else {
if(mdl.hasSubModules()){
const v = scanOptionalSubModules(mdl.getSubModules(), mdl)
const v = scanOptionalSubModules(mdl.subModules, mdl)
if(typeof v === 'object'){
mods[mdl.getVersionlessID()] = v
mods[mdl.getVersionlessMavenIdentifier()] = v
}
}
}
@ -251,13 +272,13 @@ function scanOptionalSubModules(mdls, origin){
const ret = {
mods
}
if(!origin.getRequired().isRequired()){
ret.value = origin.getRequired().isDefault()
if(!origin.getRequired().value){
ret.value = origin.getRequired().def
}
return ret
}
}
return origin.getRequired().isDefault()
return origin.getRequired().def
}
/**
@ -302,18 +323,6 @@ function mergeModConfiguration(o, n, nReq = false){
return n
}
function refreshDistributionIndex(remote, onSuccess, onError){
if(remote){
DistroManager.pullRemote()
.then(onSuccess)
.catch(onError)
} else {
DistroManager.pullLocal()
.then(onSuccess)
.catch(onError)
}
}
async function validateSelectedAccount(){
const selectedAcc = ConfigManager.getSelectedAccount()
if(selectedAcc != null){
@ -323,26 +332,54 @@ async function validateSelectedAccount(){
ConfigManager.save()
const accLen = Object.keys(ConfigManager.getAuthAccounts()).length
setOverlayContent(
'Failed to Refresh Login',
`We were unable to refresh the login for <strong>${selectedAcc.displayName}</strong>. Please ${accLen > 0 ? 'select another account or ' : ''} login again.`,
'Login',
'Select Another Account'
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(() => {
document.getElementById('loginUsername').value = selectedAcc.username
validateEmail(selectedAcc.username)
loginViewOnSuccess = getCurrentView()
loginViewOnCancel = getCurrentView()
if(accLen > 0){
loginViewCancelHandler = () => {
ConfigManager.addAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName)
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()
}
loginCancelEnabled(true)
loginOptionsCancelEnabled(true)
} else {
loginOptionsCancelEnabled(false)
}
toggleOverlay(false)
switchView(getCurrentView(), VIEWS.login)
switchView(getCurrentView(), VIEWS.loginOptions)
})
setDismissHandler(() => {
if(accLen > 1){
@ -382,14 +419,14 @@ function setSelectedAccount(uuid){
}
// Synchronous Listener
document.addEventListener('readystatechange', function(){
document.addEventListener('readystatechange', async () => {
if (document.readyState === 'interactive' || document.readyState === 'complete'){
if(rscShouldLoad){
rscShouldLoad = false
if(!fatalStartupError){
const data = DistroManager.getDistribution()
showMainUI(data)
const data = await DistroAPI.getDistribution()
await showMainUI(data)
} else {
showFatalStartupError()
}
@ -399,12 +436,13 @@ document.addEventListener('readystatechange', function(){
}, false)
// Actions that must be performed after the distribution index is downloaded.
ipcRenderer.on('distributionIndexDone', (event, res) => {
ipcRenderer.on('distributionIndexDone', async (event, res) => {
if(res) {
const data = DistroManager.getDistribution()
const data = await DistroAPI.getDistribution()
syncModConfigurations(data)
ensureJavaSettings(data)
if(document.readyState === 'interactive' || document.readyState === 'complete'){
showMainUI(data)
await showMainUI(data)
} else {
rscShouldLoad = true
}
@ -417,3 +455,12 @@ ipcRenderer.on('distributionIndexDone', (event, res) => {
}
}
})
// Util for development
async function devModeToggle() {
DistroAPI.toggleDevMode(true)
const data = await DistroAPI.refreshDistributionOrFallback()
ensureJavaSettings(data)
updateSelectedServer(data.servers[0])
syncModConfigurations(data)
}

View File

@ -1,19 +1,19 @@
// @ts-nocheck
import $ from 'jquery'
import { ipcRenderer, remote, shell, webFrame } from 'electron'
import { LoggerUtil } from '../loggerutil'
import isDev from '../isdev'
/**
* 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 = new LoggerUtil('%c[UICore]', 'color: #000668; font-weight: bold')
const loggerAutoUpdater = new LoggerUtil('%c[AutoUpdater]', 'color: #000668; font-weight: bold')
const loggerAutoUpdaterSuccess = new LoggerUtil('%c[AutoUpdater]', 'color: #209b07; font-weight: bold')
const loggerUICore = LoggerUtil.getLogger('UICore')
const loggerAutoUpdater = LoggerUtil.getLogger('AutoUpdater')
// Log deprecation and process warnings.
process.traceProcessWarnings = true
@ -35,7 +35,6 @@ remote.getCurrentWebContents().on('devtools-opened', () => {
// Disable zoom, needed for darwin.
webFrame.setZoomLevel(0)
webFrame.setVisualZoomLevelLimits(1, 1)
webFrame.setLayoutZoomLevelLimits(0, 0)
// Initialize auto updates in production environments.
let updateCheckListener
@ -43,22 +42,22 @@ if(!isDev){
ipcRenderer.on('autoUpdateNotification', (event, arg, info) => {
switch(arg){
case 'checking-for-update':
loggerAutoUpdater.log('Checking for update..')
settingsUpdateButtonStatus('Checking for Updates..', true)
loggerAutoUpdater.info('Checking for update..')
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkingForUpdateButton'), true)
break
case 'update-available':
loggerAutoUpdaterSuccess.log('New update available', info.version)
loggerAutoUpdater.info('New update available', info.version)
if(process.platform === 'darwin'){
info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/helioslauncher-setup-${info.version}.dmg`
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':
loggerAutoUpdaterSuccess.log('Update ' + info.version + ' ready to be installed.')
settingsUpdateButtonStatus('Install Now', false, () => {
loggerAutoUpdater.info('Update ' + info.version + ' ready to be installed.')
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.installNowButton'), false, () => {
if(!isDev){
ipcRenderer.send('autoUpdateAction', 'installUpdateNow')
}
@ -66,8 +65,8 @@ if(!isDev){
showUpdateUI(info)
break
case 'update-not-available':
loggerAutoUpdater.log('No new update found.')
settingsUpdateButtonStatus('Check for Updates')
loggerAutoUpdater.info('No new update found.')
settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkForUpdatesButton'))
break
case 'ready':
updateCheckListener = setInterval(() => {
@ -78,9 +77,9 @@ if(!isDev){
case 'realerror':
if(info != null && info.code != null){
if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){
loggerAutoUpdater.log('No suitable releases found.')
loggerAutoUpdater.info('No suitable releases found.')
} else if(info.code === 'ERR_XML_MISSED_ELEMENT'){
loggerAutoUpdater.log('No releases found.')
loggerAutoUpdater.info('No releases found.')
} else {
loggerAutoUpdater.error('Error during update check..', info)
loggerAutoUpdater.debug('Error Code:', info.code)
@ -88,7 +87,7 @@ if(!isDev){
}
break
default:
loggerAutoUpdater.log('Unknown argument', arg)
loggerAutoUpdater.info('Unknown argument', arg)
break
}
})
@ -108,7 +107,7 @@ function changeAllowPrerelease(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').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(() => {
@ -131,15 +130,14 @@ function showUpdateUI(info){
/* jQuery Example
$(function(){
loggerUICore.log('UICore Initialized');
loggerUICore.info('UICore Initialized');
})*/
document.addEventListener('readystatechange', function () {
if (document.readyState === 'interactive'){
loggerUICore.log('UICore Initializing..')
loggerUICore.info('UICore Initializing..')
// Bind close button.
// DONE
Array.from(document.getElementsByClassName('fCb')).map((val) => {
val.addEventListener('click', e => {
const window = remote.getCurrentWindow()
@ -148,7 +146,6 @@ document.addEventListener('readystatechange', function () {
})
// Bind restore down button.
// DONE
Array.from(document.getElementsByClassName('fRb')).map((val) => {
val.addEventListener('click', e => {
const window = remote.getCurrentWindow()
@ -157,24 +154,23 @@ document.addEventListener('readystatechange', function () {
} else {
window.maximize()
}
(document.activeElement as HTMLElement).blur()
document.activeElement.blur()
})
})
// Bind minimize button.
// DONE
Array.from(document.getElementsByClassName('fMb')).map((val) => {
val.addEventListener('click', e => {
const window = remote.getCurrentWindow()
window.minimize();
(document.activeElement as HTMLElement).blur()
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 as HTMLElement).blur()
document.activeElement.blur()
})
})
@ -188,10 +184,10 @@ document.addEventListener('readystatechange', function () {
//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'
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
}
@ -213,6 +209,6 @@ $(document).on('click', 'a[href^="http"]', function(event) {
document.addEventListener('keydown', function (e) {
if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){
let window = remote.getCurrentWindow()
window.webContents.toggleDevTools()
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

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

@ -13,7 +13,7 @@
<% } else{ %>
<div id="frameContentWin">
<div id="frameTitleDock">
<span id="frameTitleText">Helios Launcher</span>
<span id="frameTitleText"><%= lang('app.title') %></span>
</div>
<div id="frameButtonDockWin">
<button class="frameButton fMb" id="frameButton_minimize" tabIndex="-1">

View File

@ -2,8 +2,8 @@
<div id="upper">
<div id="left">
<div id="image_seal_container">
<img id="image_seal" src="../images/SealCircle.png"/>
<div id="updateAvailableTooltip">Update Available</div>
<img id="image_seal" src="assets/images/SealCircle.png"/>
<div id="updateAvailableTooltip"><%- lang('landing.updateAvailableTooltip') %></div>
</div>
</div>
<div id="content">
@ -11,9 +11,9 @@
<div id="right">
<div id="rightContainer">
<div id="user_content">
<span id="user_text">Username</span>
<span id="user_text"><%- lang('landing.usernamePlaceholder') %></span>
<div id="avatarContainer">
<button id="avatarOverlay">Edit</button>
<button id="avatarOverlay"><%- lang('landing.usernameEditButton') %></button>
</div>
</div>
<div id="mediaContent">
@ -23,14 +23,14 @@
<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">Settings</div>
<div id="settingsTooltip"><%- lang('landing.settingsTooltip') %></div>
</button>
</div>
</div>
<div class="mediaDivider"></div>
<div id="externalMedia">
<div class="mediaContainer">
<a href="https://github.com/dscalZi/HeliosLauncher" class="mediaURL" id="linkURL">
<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"/>
@ -40,7 +40,7 @@
</a>
</div>
<div class="mediaContainer">
<a href="#" class="mediaURL" id="twitterURL" disabled>
<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"/>
@ -49,7 +49,7 @@
</a>
</div>
<div class="mediaContainer">
<a href="#" class="mediaURL" id="instagramURL" disabled>
<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%">
@ -69,7 +69,7 @@
</a>
</div>
<div class="mediaContainer">
<a href="#" class="mediaURL" id="youtubeURL" disabled>
<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"/>
@ -78,7 +78,7 @@
</a>
</div>
<div class="mediaContainer">
<a href="https://discord.gg/zNWUXdt" class="mediaURL" id="discordURL">
<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"/>
@ -96,21 +96,21 @@
<div class="bot_wrapper">
<div id="content">
<div id="server_status_wrapper">
<span class="bot_label" id="landingPlayerLabel">SERVER</span>
<span id="player_count">OFFLINE</span>
<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">MOJANG STATUS</span>
<span class="bot_label"><%- lang('landing.mojangStatus') %></span>
<span id="mojang_status_icon">&#8226;</span>
<div id="mojangStatusTooltip">
<div id="mojangStatusTooltipTitle">Services</div>
<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">Non&nbsp;Essential</div>
<div id="mojangStatusNETitle"><%- lang('landing.mojangStatusNETitle') %></div>
<div class="mojangStatusNEBar"></div>
</div>
<div id="mojangStatusNonEssentialContainer">
@ -133,7 +133,7 @@
</defs>
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
&#10;<span id="newsButtonText">NEWS</span>
&#10;<span id="newsButtonText"><%- lang('landing.newsButton') %></span>
</button>
</div>
</div>
@ -141,9 +141,9 @@
<div id="right">
<div class="bot_wrapper">
<div id="launch_content">
<button id="launch_button">PLAY</button>
<button id="launch_button"><%- lang('landing.launchButton') %></button>
<div class="bot_divider"></div>
<button id="server_selection_button" class="bot_label">&#8226; No Server Selected</button>
<button id="server_selection_button" class="bot_label"><%- lang('landing.launchButtonPlaceholder') %></button>
</div>
<div id="launch_details">
<div id="launch_details_left">
@ -152,7 +152,7 @@
</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>
<span id="launch_details_text" class="bot_label"><%- lang('landing.launchDetails') %></span>
</div>
</div>
</div>
@ -184,7 +184,7 @@
<polyline class="arrowLine" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</button>
<span id="newsNavigationStatus">1 of 1</span>
<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>
@ -205,16 +205,16 @@
</div>
<div id="newsErrorContainer">
<div id="newsErrorLoading">
<span id="nELoadSpan" class="newsErrorContent">Checking for News..</span>
<span id="nELoadSpan" class="newsErrorContent"><%- lang('landing.newsErrorLoadSpan') %></span>
</div>
<div id="newsErrorFailed" style="display: none;">
<span id="nEFailedSpan" class="newsErrorContent">Failed to Load News</span>
<button id="newsErrorRetry">Try Again</button>
<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">No News</span>
<span id="nENoneSpan" class="newsErrorContent"><%- lang('landing.newsErrorNoneSpan') %></span>
</div>
</div>
</div>
<script src="../../out/scripts/landing.js"></script>
<script src="./assets/js/scripts/landing.js"></script>
</div>

View File

@ -2,21 +2,21 @@
<div id="loginCancelContainer" style="display: none;">
<button id="loginCancelButton">
<div id="loginCancelIcon">X</div>
<span id="loginCancelText">Cancel</span>
<span id="loginCancelText"><%- lang('login.loginCancelText') %></span>
</button>
</div>
<div id="loginContent">
<form id="loginForm">
<img id="loginImageSeal" src="../images/SealCircle.png"/>
<span id="loginSubheader">MINECRAFT LOGIN</span>
<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 OR USERNAME"/>
<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">
@ -24,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>
@ -54,12 +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">Helios Launcher 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>
<script src="../../out/scripts/login.js"></script>
<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>

View File

@ -1,29 +1,29 @@
<div id="overlayContainer" style="display: none;">
<div id="serverSelectContent" style="display: none;">
<span id="serverSelectHeader">Available Servers</span>
<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">Select</button>
<button id="serverSelectConfirm" class="overlayKeybindEnter" type="submit"><%- lang('overlay.serverSelectConfirm') %></button>
<div id="serverSelectCancelWrapper">
<button id="serverSelectCancel" class="overlayKeybindEsc">Cancel</button>
<button id="serverSelectCancel" class="overlayKeybindEsc"><%- lang('overlay.serverSelectCancel') %></button>
</div>
</div>
</div>
<div id="accountSelectContent" style="display: none;">
<span id="accountSelectHeader">Select an Account</span>
<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">Select</button>
<button id="accountSelectConfirm" class="overlayKeybindEnter" type="submit"><%- lang('overlay.accountSelectConfirm') %></button>
<div id="accountSelectCancelWrapper">
<button id="accountSelectCancel" class="overlayKeybindEsc">Cancel</button>
<button id="accountSelectCancel" class="overlayKeybindEsc"><%- lang('overlay.accountSelectCancel') %></button>
</div>
</div>
</div>
@ -37,5 +37,5 @@
</div>
</div>
</div>
<script src="../../out/scripts/overlay.js"></script>
<script src="./assets/js/scripts/overlay.js"></script>
</div>

View File

@ -2,21 +2,21 @@
<div id="settingsContainerLeft">
<div id="settingsNavContainer">
<div id="settingsNavHeader">
<span id="settingsNavHeaderText">Settings</span>
<span id="settingsNavHeaderText"><%- lang('settings.navHeaderText') %></span>
</div>
<div id="settingsNavItemsContainer">
<div id="settingsNavItemsContent">
<button class="settingsNavItem" rSc="settingsTabAccount" id="settingsNavAccount" selected>Account</button>
<button class="settingsNavItem" rSc="settingsTabMinecraft">Minecraft</button>
<button class="settingsNavItem" rSc="settingsTabMods">Mods</button>
<button class="settingsNavItem" rSc="settingsTabJava">Java</button>
<button class="settingsNavItem" rSc="settingsTabLauncher">Launcher</button>
<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">About</button>
<button class="settingsNavItem" rSc="settingsTabUpdate" id="settingsNavUpdate">Updates</button>
<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">Done</button>
<button id="settingsNavDone"><%- lang('settings.navDone') %></button>
</div>
</div>
</div>
@ -25,28 +25,57 @@
<div id="settingsContainerRight">
<div id="settingsTabAccount" class="settingsTab">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText">Account Settings</span>
<span class="settingsTabHeaderDesc">Add new accounts or manage existing ones.</span>
<span class="settingsTabHeaderText"><%- lang('settings.tabAccountHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.tabAccountHeaderDesc') %></span>
</div>
<div id="settingsAddAccountContainer">
<button id="settingsAddAccount">
<span id="settingsAddAccountText">&#43; Add Account</span>
</button>
<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 id="settingsCurrentAccountsHeader">
<span class="settingsFieldTitle">Current Accounts</span>
</div>
<div id="settingsCurrentAccounts">
<!-- Auth accounts populated here. -->
<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">Minecraft Settings</span>
<span class="settingsTabHeaderDesc">Options related to game launch.</span>
<span class="settingsTabHeaderText"><%- lang('settings.minecraftTabHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.minecraftTabHeaderDesc') %></span>
</div>
<div id="settingsGameResolutionContainer">
<span class="settingsFieldTitle">Game Resolution</span>
<span class="settingsFieldTitle"><%- lang('settings.gameResolutionTitle') %></span>
<div id="settingsGameResolutionContent">
<input type="number" id="settingsGameWidth" min="0" cValue="GameWidth">
<div id="settingsGameResolutionCross">&#10006;</div>
@ -55,7 +84,7 @@
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle">Launch in fullscreen.</span>
<span class="settingsFieldTitle"><%- lang('settings.launchFullscreenTitle') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
@ -66,7 +95,7 @@
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle">Automatically connect to the server on launch.</span>
<span class="settingsFieldTitle"><%- lang('settings.autoConnectTitle') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
@ -77,8 +106,8 @@
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle">Launch game process detached from launcher.</span>
<span class="settingsFieldDesc">If the game is not detached, closing the launcher will also close the game.</span>
<span class="settingsFieldTitle"><%- lang('settings.launchDetachedTitle') %></span>
<span class="settingsFieldDesc"><%- lang('settings.launchDetachedDesc') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
@ -90,46 +119,46 @@
</div>
<div id="settingsTabMods" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText">Mod Settings</span>
<span class="settingsTabHeaderDesc">Enable or disable mods.</span>
<span class="settingsTabHeaderText"><%- lang('settings.tabModsHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.tabModsHeaderDesc') %></span>
</div>
<div id="settingsSelServContainer">
<div id="settingsSelServContent">
<div class="settingsSelServContainer">
<div class="settingsSelServContent">
</div>
<div id="settingsSwitchServerContainer">
<div id="settingsSwitchServerContent">
<button id="settingsSwitchServerButton">Switch</button>
<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">Required Mods</div>
<div class="settingsModsHeader"><%- lang('settings.requiredMods') %></div>
<div id="settingsReqModsContent">
</div>
</div>
<div id="settingsOptModsContainer">
<div class="settingsModsHeader">Optional Mods</div>
<div class="settingsModsHeader"><%- lang('settings.optionalMods') %></div>
<div id="settingsOptModsContent">
</div>
</div>
<div id="settingsDropinModsContainer">
<div class="settingsModsHeader">Drop-in Mods</div>
<button id="settingsDropinFileSystemButton">+ Add Mods <span id="settingsDropinRefreshNote">(F5 to Refresh)</span></button>
<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">Shaderpacks</div>
<div id="settingsShaderpackDesc">Enable or disable shaders. Please note, shaders will only run smoothly on powerful setups. You may add custom packs here.</div>
<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">Select Shaderpack</div>
<div class="settingsSelectSelected" id="settingsShadersSelected"><%- lang('settings.selectShaderpack') %></div>
<div class="settingsSelectOptions" id="settingsShadersOptions" hidden>
</div>
@ -140,17 +169,27 @@
</div>
<div id="settingsTabJava" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText">Java Settings</span>
<span class="settingsTabHeaderDesc">Manage the Java configuration (advanced).</span>
<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">Memory</div>
<div id="settingsMemoryTitle"><%- lang('settings.memoryTitle') %></div>
<div id="settingsMemoryContent">
<div id="settingsMemoryContentLeft">
<div class="settingsMemoryContentItem">
<span class="settingsMemoryHeader">Maximum RAM</span>
<span class="settingsMemoryHeader"><%- lang('settings.maxRAM') %></span>
<div class="settingsMemoryActionContainer">
<div id="settingsMaxRAMRange" class="rangeSlider" cValue="MaxRAM" min="3" max="8" value="3" step="0.5">
<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>
@ -158,25 +197,25 @@
</div>
</div>
<div class="settingsMemoryContentItem">
<span class="settingsMemoryHeader">Minimum RAM</span>
<span class="settingsMemoryHeader"><%- lang('settings.minRAM') %></span>
<div class="settingsMemoryActionContainer">
<div id="settingsMinRAMRange" class="rangeSlider" cValue="MinRAM" min="3" max="8" value="3" step="0.5">
<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">The recommended minimum RAM is 3 gigabytes. Setting the minimum and maximum values to the same value may reduce lag.</div>
<div id="settingsMemoryDesc"><%- lang('settings.memoryDesc') %></div>
</div>
<div id="settingsMemoryContentRight">
<div id="settingsMemoryStatus">
<div class="settingsMemoryStatusContainer">
<span class="settingsMemoryStatusTitle">Total</span>
<span class="settingsMemoryStatusTitle"><%- lang('settings.memoryTotalTitle') %></span>
<span id="settingsMemoryTotal" class="settingsMemoryStatusValue">16G</span>
</div>
<div class="settingsMemoryStatusContainer">
<span class="settingsMemoryStatusTitle">Available</span>
<span class="settingsMemoryStatusTitle"><%- lang('settings.memoryAvailableTitle') %></span>
<span id="settingsMemoryAvail" class="settingsMemoryStatusValue">7.3G</span>
</div>
</div>
@ -184,9 +223,9 @@
</div>
</div>
<div class="settingsFileSelContainer">
<div class="settingsFileSelTitle">Java Executable</div>
<div class="settingsFileSelTitle"><%- lang('settings.javaExecutableTitle') %></div>
<div class="settingsFileSelContent">
<div id="settingsJavaExecDetails">Selected: Java 8 Update 172 (x64)</div>
<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">
@ -202,15 +241,14 @@
</g>
</svg>
</div>
<input class="settingsFileSelVal" id="settingsJavaExecVal" type="text" value="null" cValue="JavaExecutable" disabled>
<input class="settingsFileSelSel" id="settingsJavaExecSel" type="file" <%= process.platform === 'win32' ? 'accept=.exe' : '' %>>
<label class="settingsFileSelLabel" for="settingsJavaExecSel">Choose File</label>
<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">The Java executable is validated before game launch. <strong>Requires Java 8 x64.</strong><br>The path should end with <strong>bin<%= process.platform === 'win32' ? '\\javaw.exe' : '/java' %></strong>.</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">Additional JVM Options</div>
<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">
@ -226,20 +264,20 @@
</g>
</svg>
</div>
<input id="settingsJVMOptsVal" cValue="JVMOptions" type="text">
<input id="settingsJVMOptsVal" cValue="JVMOptions" serverDependent type="text">
</div>
<div id="settingsJVMOptsDesc">Options to be provided to the JVM at runtime. <em>-Xms</em> and <em>-Xmx</em> should not be included.<br><a href="https://docs.oracle.com/javase/8/docs/technotes/tools/<%= process.platform === 'win32' ? 'windows' : 'unix' %>/java.html">Available Options for Java 8</a>.</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">Launcher Settings</span>
<span class="settingsTabHeaderDesc">Options related to the launcher itself.</span>
<span class="settingsTabHeaderText"><%- lang('settings.launcherTabHeaderText') %></span>
<span class="settingsTabHeaderDesc"><%- lang('settings.launcherTabHeaderDesc') %></span>
</div>
<div class="settingsFieldContainer">
<div class="settingsFieldLeft">
<span class="settingsFieldTitle">Allow Pre-Release Updates.</span>
<span class="settingsFieldDesc">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.</span>
<span class="settingsFieldTitle"><%- lang('settings.allowPrereleaseTitle') %></span>
<span class="settingsFieldDesc"><%- lang('settings.allowPrereleaseDesc') %></span>
</div>
<div class="settingsFieldRight">
<label class="toggleSwitch">
@ -250,7 +288,7 @@
</div>
<div class="settingsFileSelContainer">
<div class="settingsFileSelContent">
<div class="settingsFieldTitle" id="settingsDataDirTitle">Data Directory</div>
<div class="settingsFieldTitle" id="settingsDataDirTitle"><%- lang('settings.dataDirectoryTitle') %></div>
<div class="settingsFileSelActions">
<div class="settingsFileSelIcon">
<svg class="settingsFileSelSVG">
@ -262,95 +300,94 @@
</svg>
</div>
<input class="settingsFileSelVal" type="text" value="null" cValue="DataDirectory" disabled>
<input class="settingsFileSelSel" id="settingsDataDirSel" type="file" webkitdirectory>
<label class="settingsFileSelLabel" for="settingsDataDirSel">Choose Folder</label>
<button class="settingsFileSelButton" dialogTitle="<%- lang('settings.selectDataDirectory') %>" dialogDirectory="true"><%- lang('settings.chooseFolder') %></button>
</div>
</div>
<div class="settingsFileSelDesc">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.</div>
<div class="settingsFileSelDesc"><%- lang('settings.dataDirectoryDesc') %></div>
</div>
</div>
<div id="settingsTabAbout" class="settingsTab" style="display: none;">
<div class="settingsTabHeader">
<span class="settingsTabHeaderText">About</span>
<span class="settingsTabHeaderDesc">View information and release notes for the current version.</span>
<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="../images/SealCircle.png">
<span id="settingsAboutTitle">Helios Launcher</span>
<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">Stable Release</span>
<span id="settingsAboutCurrentVersionTitle"><%- lang('settings.stableRelease') %></span>
<div id="settingsAboutCurrentVersionLine">
<span id="settingsAboutCurrentVersionText">Version </span>
<span id="settingsAboutCurrentVersionValue">0.0.1-alpha.12</span>
<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="https://github.com/dscalZi/HeliosLauncher" id="settingsAboutSourceButton" class="settingsAboutButton">Source (GitHub)</a>
<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="https://github.com/dscalZi/HeliosLauncher/issues" id="settingsAboutSupportButton" class="settingsAboutButton">Support</a>
<a href="#" id="settingsAboutDevToolsButton" class="settingsAboutButton">DevTools Console</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">Release Notes</div>
<div class="settingsChangelogTitle">Changelog</div>
<div class="settingsChangelogLabel"><%- lang('settings.releaseNotes') %></div>
<div class="settingsChangelogTitle"><%- lang('settings.changelog') %></div>
</div>
<div class="settingsChangelogText">
No Release Notes
<%- lang('settings.noReleaseNotes') %>
</div>
</div>
<div class="settingsChangelogActions">
<a class="settingsChangelogButton settingsAboutButton" href="#">View Release Notes on GitHub</a>
<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">Launcher Updates</span>
<span class="settingsTabHeaderDesc">Download, install, and review updates for the launcher.</span>
<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>
<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">Stable Release</span>
<span id="settingsUpdateVersionTitle"><%- lang('settings.stableRelease') %></span>
<div id="settingsUpdateVersionLine">
<span id="settingsUpdateVersionText">Version </span>
<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">Check for Updates</button>
<button id="settingsUpdateActionButton"><%- lang('settings.checkForUpdates') %></button>
</div>
</div>
</div>
<div class="settingsChangelogContainer">
<div class="settingsChangelogContent">
<div class="settingsChangelogHeadline">
<div class="settingsChangelogLabel">What's New</div>
<div class="settingsChangelogTitle">Update Release Notes</div>
<div class="settingsChangelogLabel"><%- lang('settings.whatsNew') %></div>
<div class="settingsChangelogTitle"><%- lang('settings.updateReleaseNotes') %></div>
</div>
<div class="settingsChangelogText">
No Release Notes
<%- lang('settings.noReleaseNotes') %>
</div>
</div>
</div>
</div>
</div>
<script src="../../out/scripts/settings.js"></script>
<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>

View File

@ -1,49 +0,0 @@
{
"html": {
"avatarOverlay": "Edit"
},
"js": {
"login": {
"error": {
"invalidValue": "* Invalid Value",
"requiredValue": "* Required",
"userMigrated": {
"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."
},
"invalidCredentials": {
"title": "Error During Login:<br>Invalid Credentials",
"desc": "The email or password you've entered is incorrect. Please try again."
},
"rateLimit": {
"title": "Error During Login:<br>Too Many Attempts",
"desc": "There have been too many login attempts with this account recently. Please try again later."
},
"noInternet": {
"title": "Error During Login:<br>No Internet Connection",
"desc": "You must be connected to the internet in order to login. Please connect and try again."
},
"authDown": {
"title": "Error During Login:<br>Authentication Server Offline",
"desc": "Mojang's authentication server is currently offline or unreachable. Please wait a bit and try again. You can check the status of the server on <a href=\"https://help.mojang.com/\">Mojang's help portal</a>."
},
"notPaid": {
"title": "Error During Login:<br>Game Not Purchased",
"desc": "The account you are trying to login with has not purchased a copy of Minecraft.<br>You may purchase a copy on <a href=\"https://minecraft.net/\">Minecraft.net</a>"
},
"unknown": {
"title": "Error During Login:<br>Unknown Error"
}
},
"login": "LOGIN",
"loggingIn": "LOGGING IN",
"success": "SUCCESS",
"tryAgain": "Try Again"
},
"landing": {
"launch": {
"pleaseWait": "Please wait.."
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
/**
* Script for welcome.ejs
*/
document.getElementById('welcomeButton').addEventListener('click', e => {
switchView(VIEWS.welcome, VIEWS.login)
})

View File

@ -1,25 +0,0 @@
<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="../images/SealCircle.png"/>
<span id="welcomeHeader">WELCOME TO WESTEROSCRAFT</span>
<span id="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.</span>
<br>
<span id="welcomeDescCTA">You are just a few clicks away from Westeros.</span>
<button id="welcomeButton">
<div id="welcomeButtonContent">
CONTINUE
<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="../../out/scripts/welcome.js"></script>
</div>

View File

@ -1,69 +0,0 @@
const builder = require('electron-builder')
const Platform = builder.Platform
function getCurrentPlatform(){
switch(process.platform){
case 'win32':
return Platform.WINDOWS
case 'darwin':
return Platform.MAC
case 'linux':
return Platform.linux
default:
console.error('Cannot resolve current platform!')
return undefined
}
}
builder.build({
targets: (process.argv[2] != null && Platform[process.argv[2]] != null ? Platform[process.argv[2]] : getCurrentPlatform()).createTarget(),
config: {
appId: 'helioslauncher',
productName: 'Helios Launcher',
artifactName: '${productName}-setup-${version}.${ext}',
copyright: 'Copyright © 2018-2020 Daniel Scalzi',
directories: {
buildResources: 'build',
output: 'dist'
},
win: {
target: [
{
target: 'nsis',
arch: 'x64'
}
]
},
nsis: {
oneClick: false,
perMachine: false,
allowElevation: true,
allowToChangeInstallationDirectory: true
},
mac: {
target: 'dmg',
category: 'public.app-category.games'
},
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'
},
compression: 'maximum',
files: [
'!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.travis.yml,.nvmrc,.eslintrc.json,build.js}'
],
extraResources: [
'libraries',
'static'
],
asar: true
}
}).then(() => {
console.log('Build complete!')
}).catch(err => {
console.error('Error during build!', err)
})

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,5 +1,9 @@
# 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
@ -141,12 +145,122 @@ Only one server in the array should have the `mainServer` property enabled. This
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.
@ -206,6 +320,12 @@ The name of the module. Used on the UI.
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**
@ -240,10 +360,12 @@ The resolved/provided paths are appended to a base path depending on the module'
| 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.
@ -288,7 +410,7 @@ If the module is enabled by default. Has no effect unless `Required.value` is fa
### ForgeHosted
The module type `ForgeHosted` represents forge itself. Currently, the launcher only supports forge servers, as vanilla servers can be connected to via the mojang launcher. The `Hosted` part is key, this means that the forge module must declare its required libraries as submodules.
The module type `ForgeHosted` represents forge itself. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher. The `Hosted` part is key, this means that the forge module must declare its required libraries as submodules.
Ex.
@ -323,6 +445,40 @@ There were plans to add a `Forge` type, in which the required libraries would be
---
### Fabric
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.

View File

@ -1,8 +1,4 @@
{
"__comment__": [
"This is an example only.",
"The file is hosted on the URL in DistroManager.js"
],
"version": "1.0.0",
"discord": {
"clientId": "385581240906022916",

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'

361
index.js Normal file
View File

@ -0,0 +1,361 @@
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('SealCircle'),
frame: false,
webPreferences: {
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'),
nodeIntegration: true,
contextIsolation: false
},
backgroundColor: '#171614'
})
remoteMain.enable(win.webContents)
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(pathToFileURL(path.join(__dirname, 'app', 'app.ejs')).toString())
/*win.once('ready-to-show', () => {
win.show()
})*/
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){
let ext
switch(process.platform) {
case 'win32':
ext = 'ico'
break
case 'darwin':
case 'linux':
default:
ext = 'png'
break
}
return path.join(__dirname, 'app', 'assets', 'images', `${filename}.${ext}`)
}
app.on('ready', createWindow)
app.on('ready', createMenu)
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})

Binary file not shown.

16164
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "helioslauncher",
"version": "2.0.0-alpha.0",
"version": "2.1.0",
"productName": "Helios Launcher",
"description": "Modded Minecraft Launcher",
"author": "Daniel Scalzi (https://github.com/dscalzi/)",
@ -10,90 +10,39 @@
"url": "https://github.com/dscalzi/HeliosLauncher/issues"
},
"private": true,
"main": "./dist/main.js",
"main": "index.js",
"scripts": {
"clean": "rimraf dist",
"tsc": "tsc",
"start": "electron .",
"cilinux": "node build.js WINDOWS && node build.js LINUX",
"cidarwin": "node build.js MAC",
"dist:win": "npm run dist -- WINDOWS",
"dist:mac": "npm run dist -- MAC",
"dist:linux": "npm run dist -- LINUX",
"lint": "eslint --ext=jsx,js,tsx,ts src",
"dev": "electron-webpack dev",
"compile": "electron-webpack",
"test": "cross-env TS_NODE_PROJECT='./tsconfig.test.json' NODE_ENV=test mocha -r ts-node/register -r tsconfig-paths/register test/**/*.ts"
"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": "12.x.x"
"node": "18.x.x"
},
"dependencies": {
"adm-zip": "^0.4.16",
"async": "^3.2.0",
"discord-rpc": "^3.1.4",
"electron-updater": "^4.3.5",
"fs-extra": "^9.0.1",
"@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.7.0",
"jquery": "^3.5.1",
"lodash": "^4.17.20",
"luxon": "^1.25.0",
"request": "^2.88.2",
"semver": "^7.3.2",
"tar-fs": "^2.1.0",
"triple-beam": "^1.3.0",
"winreg": "^1.2.4",
"winston": "^3.3.3"
"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": {
"@babel/preset-react": "^7.10.4",
"@types/adm-zip": "^0.4.33",
"@types/async": "^3.2.3",
"@types/chai": "^4.2.13",
"@types/chai-as-promised": "^7.1.3",
"@types/discord-rpc": "^3.0.4",
"@types/electron-devtools-installer": "^2.2.0",
"@types/fs-extra": "^9.0.1",
"@types/jquery": "^3.5.2",
"@types/lodash": "^4.14.161",
"@types/luxon": "^1.25.0",
"@types/mocha": "^8.0.3",
"@types/node": "^12.12.64",
"@types/react": "^16.9.51",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
"@types/react-transition-group": "^4.4.0",
"@types/request": "^2.48.5",
"@types/tar-fs": "^2.0.0",
"@types/triple-beam": "^1.3.2",
"@types/winreg": "^1.2.30",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"cross-env": "^7.0.2",
"electron": "^9.3.2",
"electron-builder": "^22.8.1",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-ts": "^4.0.1",
"eslint": "^7.10.0",
"eslint-plugin-react": "^7.21.3",
"helios-distribution-types": "1.0.0-rc.1",
"mocha": "^8.1.3",
"nock": "^13.0.4",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-hot-loader": "^4.13.0",
"react-redux": "^7.2.1",
"react-transition-group": "^4.4.1",
"redux": "^4.0.5",
"rimraf": "^3.0.2",
"ts-node": "^9.0.0",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
"electron": "^27.1.3",
"electron-builder": "^24.9.1",
"eslint": "^8.55.0"
},
"repository": {
"type": "git",

View File

@ -1,7 +0,0 @@
export interface Asset {
id: string
hash: string
size: number
url: string
path: string
}

View File

@ -1,33 +0,0 @@
export class AssetGuardError extends Error {
code?: string
stack!: string
error?: Partial<Error & {code?: string}>
constructor(message: string, error?: Partial<Error & {code?: string}>) {
super(message)
Error.captureStackTrace(this, this.constructor)
// Reference: https://github.com/sindresorhus/got/blob/master/source/core/index.ts#L340
if(error) {
this.error = error
this.code = error?.code
if (error.stack != null) {
const indexOfMessage = this.stack.indexOf(this.message) + this.message.length
const thisStackTrace = this.stack.slice(indexOfMessage).split('\n').reverse()
const errorStackTrace = error.stack.slice(error.stack.indexOf(error.message!) + error.message!.length).split('\n').reverse()
// Remove duplicated traces
while (errorStackTrace.length !== 0 && errorStackTrace[0] === thisStackTrace[0]) {
thisStackTrace.shift()
}
this.stack = `${this.stack.slice(0, indexOfMessage)}${thisStackTrace.reverse().join('\n')}${errorStackTrace.reverse().join('\n')}`
}
}
}
}

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