├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .nvmrc ├── LICENSE.txt ├── README.md ├── app ├── app.ejs ├── assets │ ├── css │ │ └── launcher.css │ ├── fonts │ │ ├── Avenir-Black.ttf │ │ ├── Avenir-BlackOblique.ttf │ │ ├── Avenir-Book.ttf │ │ ├── Avenir-BookOblique.ttf │ │ ├── Avenir-Heavy.ttf │ │ ├── Avenir-HeavyOblique.ttf │ │ ├── Avenir-Light.ttf │ │ ├── Avenir-LightOblique.ttf │ │ ├── Avenir-Medium.ttf │ │ ├── Avenir-MediumOblique.ttf │ │ ├── Avenir-Oblique.ttf │ │ ├── Avenir-Roman.ttf │ │ └── ringbearer.ttf │ ├── images │ │ ├── LoadingSeal.png │ │ ├── LoadingText.png │ │ ├── SealCircle.ico │ │ ├── SealCircle.png │ │ ├── backgrounds │ │ │ ├── 0.jpg │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ ├── 5.jpg │ │ │ ├── 6.jpg │ │ │ └── 7.jpg │ │ ├── icons │ │ │ ├── arrow.svg │ │ │ ├── discord.svg │ │ │ ├── instagram.svg │ │ │ ├── link.svg │ │ │ ├── lock.svg │ │ │ ├── microsoft.svg │ │ │ ├── mojang.svg │ │ │ ├── news.svg │ │ │ ├── profile.svg │ │ │ ├── settings.svg │ │ │ ├── sevenstar.svg │ │ │ ├── sevenstar_circle.svg │ │ │ ├── sevenstar_circle_extended.svg │ │ │ ├── sevenstar_circle_hole.svg │ │ │ ├── sevenstar_circle_hole_extended.svg │ │ │ ├── sevenstar_extended.svg │ │ │ ├── x.svg │ │ │ └── youtube.svg │ │ └── minecraft.icns │ ├── js │ │ ├── authmanager.js │ │ ├── configmanager.js │ │ ├── discordwrapper.js │ │ ├── distromanager.js │ │ ├── dropinmodutil.js │ │ ├── ipcconstants.js │ │ ├── isdev.js │ │ ├── langloader.js │ │ ├── preloader.js │ │ ├── processbuilder.js │ │ ├── scripts │ │ │ ├── landing.js │ │ │ ├── login.js │ │ │ ├── loginOptions.js │ │ │ ├── overlay.js │ │ │ ├── settings.js │ │ │ ├── uibinder.js │ │ │ ├── uicore.js │ │ │ └── welcome.js │ │ └── serverstatus.js │ └── lang │ │ ├── _custom.toml │ │ └── en_US.toml ├── frame.ejs ├── landing.ejs ├── login.ejs ├── loginOptions.ejs ├── overlay.ejs ├── settings.ejs ├── waiting.ejs └── welcome.ejs ├── build └── icon.png ├── dev-app-update.yml ├── docs ├── MicrosoftAuth.md ├── distro.md └── sample_distribution.json ├── electron-builder.yml ├── index.js ├── libraries └── java │ └── PackXZExtract.jar ├── package-lock.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2022": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 2022, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 4, 15 | { 16 | "SwitchCase": 1 17 | } 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "windows" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "never" 30 | ], 31 | "no-var": [ 32 | "error" 33 | ], 34 | "no-console": [ 35 | 0 36 | ], 37 | "no-control-regex": [ 38 | 0 39 | ], 40 | "no-unused-vars": [ 41 | "error", 42 | { 43 | "vars": "all", 44 | "args": "none", 45 | "ignoreRestSiblings": false, 46 | "argsIgnorePattern": "reject" 47 | } 48 | ], 49 | "no-async-promise-executor": [ 50 | 0 51 | ] 52 | }, 53 | "overrides": [ 54 | { 55 | "files": [ "app/assets/js/scripts/*.js" ], 56 | "rules": { 57 | "no-unused-vars": [ 58 | 0 59 | ], 60 | "no-undef": [ 61 | 0 62 | ] 63 | } 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dscalzi 2 | patreon: dscalzi 3 | custom: ['https://www.paypal.me/dscalzi'] 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | release: 7 | runs-on: ${{ matrix.os }} 8 | 9 | permissions: 10 | contents: write 11 | 12 | strategy: 13 | matrix: 14 | os: [macos-latest, ubuntu-latest, windows-latest] 15 | 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 20 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: 3.x 29 | 30 | - name: Install Dependencies 31 | run: npm ci 32 | shell: bash 33 | 34 | - name: Build 35 | env: 36 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | run: npm run dist 38 | shell: bash -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.vs/ 3 | /.vscode/ 4 | /target/ 5 | /logs/ 6 | /dist/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Daniel D. Scalzi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

aventium softworks

2 | 3 |

Helios Launcher

4 | 5 |
(formerly Electron Launcher)
6 | 7 | [

gh actions](https://github.com/dscalzi/HeliosLauncher/actions) [downloads](https://github.com/dscalzi/HeliosLauncher/releases) winter-is-coming

8 | 9 |

Join modded servers without worrying about installing Java, Forge, or other mods. We'll handle that for you.

10 | 11 | ![Screenshot 1](https://i.imgur.com/6o7SmH6.png) 12 | ![Screenshot 2](https://i.imgur.com/x3B34n1.png) 13 | 14 | ## Features 15 | 16 | * 🔒 Full account management. 17 | * Add multiple accounts and easily switch between them. 18 | * Microsoft (OAuth 2.0) + Mojang (Yggdrasil) authentication fully supported. 19 | * Credentials are never stored and transmitted directly to Mojang. 20 | * 📂 Efficient asset management. 21 | * Receive client updates as soon as we release them. 22 | * Files are validated before launch. Corrupt or incorrect files will be redownloaded. 23 | * ☕ **Automatic Java validation.** 24 | * If you have an incompatible version of Java installed, we'll install the right one *for you*. 25 | * You do not need to have Java installed to run the launcher. 26 | * 📰 News feed natively built into the launcher. 27 | * ⚙️ Intuitive settings management, including a Java control panel. 28 | * Supports all of our servers. 29 | * Switch between server configurations with ease. 30 | * View the player count of the selected server. 31 | * Automatic updates. That's right, the launcher updates itself. 32 | * View the status of Mojang's services. 33 | 34 | This is not an exhaustive list. Download and install the launcher to gauge all it can do! 35 | 36 | #### Need Help? [Check the wiki.][wiki] 37 | 38 | #### Like the project? Leave a ⭐ star on the repository! 39 | 40 | ## Downloads 41 | 42 | You can download from [GitHub Releases](https://github.com/dscalzi/HeliosLauncher/releases) 43 | 44 | #### Latest Release 45 | 46 | [![](https://img.shields.io/github/release/dscalzi/HeliosLauncher.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases/latest) 47 | 48 | #### Latest Pre-Release 49 | [![](https://img.shields.io/github/release/dscalzi/HeliosLauncher/all.svg?style=flat-square)](https://github.com/dscalzi/HeliosLauncher/releases) 50 | 51 | **Supported Platforms** 52 | 53 | If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/releases) tab, select the installer for your system. 54 | 55 | | Platform | File | 56 | | -------- | ---- | 57 | | Windows x64 | `Helios-Launcher-setup-VERSION.exe` | 58 | | macOS x64 | `Helios-Launcher-setup-VERSION-x64.dmg` | 59 | | macOS arm64 | `Helios-Launcher-setup-VERSION-arm64.dmg` | 60 | | Linux x64 | `Helios-Launcher-setup-VERSION.AppImage` | 61 | 62 | ## Console 63 | 64 | To open the console, use the following keybind. 65 | 66 | ```console 67 | ctrl + shift + i 68 | ``` 69 | 70 | Ensure that you have the console tab selected. Do not paste anything into the console unless you are 100% sure of what it will do. Pasting the wrong thing can expose sensitive information. 71 | 72 | #### Export Output to a File 73 | 74 | If you want to export the console output, simply right click anywhere on the console and click **Save as..** 75 | 76 | ![console example](https://i.imgur.com/T5e73jP.png) 77 | 78 | 79 | ## Development 80 | 81 | This section details the setup of a basic developmentment environment. 82 | 83 | ### Getting Started 84 | 85 | **System Requirements** 86 | 87 | * [Node.js][nodejs] v20 88 | 89 | --- 90 | 91 | **Clone and Install Dependencies** 92 | 93 | ```console 94 | > git clone https://github.com/dscalzi/HeliosLauncher.git 95 | > cd HeliosLauncher 96 | > npm install 97 | ``` 98 | 99 | --- 100 | 101 | **Launch Application** 102 | 103 | ```console 104 | > npm start 105 | ``` 106 | 107 | --- 108 | 109 | **Build Installers** 110 | 111 | To build for your current platform. 112 | 113 | ```console 114 | > npm run dist 115 | ``` 116 | 117 | Build for a specific platform. 118 | 119 | | Platform | Command | 120 | | ----------- | -------------------- | 121 | | Windows x64 | `npm run dist:win` | 122 | | macOS | `npm run dist:mac` | 123 | | Linux x64 | `npm run dist:linux` | 124 | 125 | Builds for macOS may not work on Windows/Linux and vice-versa. 126 | 127 | --- 128 | 129 | ### Visual Studio Code 130 | 131 | All development of the launcher should be done using [Visual Studio Code][vscode]. 132 | 133 | Paste the following into `.vscode/launch.json` 134 | 135 | ```JSON 136 | { 137 | "version": "0.2.0", 138 | "configurations": [ 139 | { 140 | "name": "Debug Main Process", 141 | "type": "node", 142 | "request": "launch", 143 | "cwd": "${workspaceFolder}", 144 | "program": "${workspaceFolder}/node_modules/electron/cli.js", 145 | "args" : ["."], 146 | "outputCapture": "std" 147 | }, 148 | { 149 | "name": "Debug Renderer Process", 150 | "type": "chrome", 151 | "request": "launch", 152 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 153 | "windows": { 154 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 155 | }, 156 | "runtimeArgs": [ 157 | "${workspaceFolder}/.", 158 | "--remote-debugging-port=9222" 159 | ], 160 | "webRoot": "${workspaceFolder}" 161 | } 162 | ] 163 | } 164 | ``` 165 | 166 | This adds two debug configurations. 167 | 168 | #### Debug Main Process 169 | 170 | This allows you to debug Electron's [main process][mainprocess]. You can debug scripts in the [renderer process][rendererprocess] by opening the DevTools Window. 171 | 172 | #### Debug Renderer Process 173 | 174 | This allows you to debug Electron's [renderer process][rendererprocess]. This requires you to install the [Debugger for Chrome][chromedebugger] extension. 175 | 176 | Note that you **cannot** open the DevTools window while using this debug configuration. Chromium only allows one debugger, opening another will crash the program. 177 | 178 | --- 179 | 180 | ### Note on Third-Party Usage 181 | 182 | 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. 183 | 184 | For instructions on setting up Microsoft Authentication, see https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md. 185 | 186 | --- 187 | 188 | ## Resources 189 | 190 | * [Wiki][wiki] 191 | * [Nebula (Create Distribution.json)][nebula] 192 | * [v2 Rewrite Branch (Inactive)][v2branch] 193 | 194 | The best way to contact the developers is on Discord. 195 | 196 | [![discord](https://discordapp.com/api/guilds/211524927831015424/embed.png?style=banner3)][discord] 197 | 198 | --- 199 | 200 | ### See you ingame. 201 | 202 | 203 | [nodejs]: https://nodejs.org/en/ 'Node.js' 204 | [vscode]: https://code.visualstudio.com/ 'Visual Studio Code' 205 | [mainprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Main Process' 206 | [rendererprocess]: https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 'Renderer Process' 207 | [chromedebugger]: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Debugger for Chrome' 208 | [discord]: https://discord.gg/zNWUXdt 'Discord' 209 | [wiki]: https://github.com/dscalzi/HeliosLauncher/wiki 'wiki' 210 | [nebula]: https://github.com/dscalzi/Nebula 'dscalzi/Nebula' 211 | [v2branch]: https://github.com/dscalzi/HeliosLauncher/tree/ts-refactor 'v2 branch' 212 | -------------------------------------------------------------------------------- /app/app.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= lang('app.title') %> 5 | 6 | 7 | 8 | 28 | 29 | 30 | <%- include('frame') %> 31 |
32 | <%- include('welcome') %> 33 | <%- include('login') %> 34 | <%- include('waiting') %> 35 | <%- include('loginOptions') %> 36 | <%- include('settings') %> 37 | <%- include('landing') %> 38 |
39 | <%- include('overlay') %> 40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-Black.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-BlackOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-BlackOblique.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-Book.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-BookOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-BookOblique.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-Heavy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-Heavy.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-HeavyOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-HeavyOblique.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-Light.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-LightOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-LightOblique.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-Medium.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-MediumOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-MediumOblique.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-Oblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-Oblique.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Avenir-Roman.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/Avenir-Roman.ttf -------------------------------------------------------------------------------- /app/assets/fonts/ringbearer.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/fonts/ringbearer.ttf -------------------------------------------------------------------------------- /app/assets/images/LoadingSeal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/LoadingSeal.png -------------------------------------------------------------------------------- /app/assets/images/LoadingText.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/LoadingText.png -------------------------------------------------------------------------------- /app/assets/images/SealCircle.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/SealCircle.ico -------------------------------------------------------------------------------- /app/assets/images/SealCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/SealCircle.png -------------------------------------------------------------------------------- /app/assets/images/backgrounds/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/0.jpg -------------------------------------------------------------------------------- /app/assets/images/backgrounds/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/1.jpg -------------------------------------------------------------------------------- /app/assets/images/backgrounds/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/2.jpg -------------------------------------------------------------------------------- /app/assets/images/backgrounds/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/3.jpg -------------------------------------------------------------------------------- /app/assets/images/backgrounds/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/4.jpg -------------------------------------------------------------------------------- /app/assets/images/backgrounds/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/5.jpg -------------------------------------------------------------------------------- /app/assets/images/backgrounds/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/6.jpg -------------------------------------------------------------------------------- /app/assets/images/backgrounds/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/backgrounds/7.jpg -------------------------------------------------------------------------------- /app/assets/images/icons/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | arrow 6 | 7 | -------------------------------------------------------------------------------- /app/assets/images/icons/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | discord 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/assets/images/icons/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/images/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | link 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/assets/images/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Lock 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/assets/images/icons/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/assets/images/icons/mojang.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/images/icons/news.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | News 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/icons/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Profile 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/assets/images/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | settings 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/assets/images/icons/sevenstar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Seven Pointed Star 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/assets/images/icons/sevenstar_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Seven Pointed Star with Circle 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/icons/sevenstar_circle_extended.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Seven Pointed Star Extended with Circle 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/assets/images/icons/sevenstar_circle_hole.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Seven Pointed Star with Circle and Hole 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/assets/images/icons/sevenstar_circle_hole_extended.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Seven Pointed Star Extended with Circle and Hole 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/images/icons/sevenstar_extended.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Seven Pointed Star Extended 6 | 7 | -------------------------------------------------------------------------------- /app/assets/images/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/icons/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | youtube 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/assets/images/minecraft.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/app/assets/images/minecraft.icns -------------------------------------------------------------------------------- /app/assets/js/authmanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthManager 3 | * 4 | * This module aims to abstract login procedures. Results from Mojang's REST api 5 | * are retrieved through our Mojang module. These results are processed and stored, 6 | * if applicable, in the config using the ConfigManager. All login procedures should 7 | * be made through this module. 8 | * 9 | * @module authmanager 10 | */ 11 | // Requirements 12 | const ConfigManager = require('./configmanager') 13 | const { LoggerUtil } = require('helios-core') 14 | const { RestResponseStatus } = require('helios-core/common') 15 | const { MojangRestAPI, MojangErrorCode } = require('helios-core/mojang') 16 | const { MicrosoftAuth, MicrosoftErrorCode } = require('helios-core/microsoft') 17 | const { AZURE_CLIENT_ID } = require('./ipcconstants') 18 | const Lang = require('./langloader') 19 | 20 | const log = LoggerUtil.getLogger('AuthManager') 21 | 22 | // Error messages 23 | 24 | function microsoftErrorDisplayable(errorCode) { 25 | switch (errorCode) { 26 | case MicrosoftErrorCode.NO_PROFILE: 27 | return { 28 | title: Lang.queryJS('auth.microsoft.error.noProfileTitle'), 29 | desc: Lang.queryJS('auth.microsoft.error.noProfileDesc') 30 | } 31 | case MicrosoftErrorCode.NO_XBOX_ACCOUNT: 32 | return { 33 | title: Lang.queryJS('auth.microsoft.error.noXboxAccountTitle'), 34 | desc: Lang.queryJS('auth.microsoft.error.noXboxAccountDesc') 35 | } 36 | case MicrosoftErrorCode.XBL_BANNED: 37 | return { 38 | title: Lang.queryJS('auth.microsoft.error.xblBannedTitle'), 39 | desc: Lang.queryJS('auth.microsoft.error.xblBannedDesc') 40 | } 41 | case MicrosoftErrorCode.UNDER_18: 42 | return { 43 | title: Lang.queryJS('auth.microsoft.error.under18Title'), 44 | desc: Lang.queryJS('auth.microsoft.error.under18Desc') 45 | } 46 | case MicrosoftErrorCode.UNKNOWN: 47 | return { 48 | title: Lang.queryJS('auth.microsoft.error.unknownTitle'), 49 | desc: Lang.queryJS('auth.microsoft.error.unknownDesc') 50 | } 51 | } 52 | } 53 | 54 | function mojangErrorDisplayable(errorCode) { 55 | switch(errorCode) { 56 | case MojangErrorCode.ERROR_METHOD_NOT_ALLOWED: 57 | return { 58 | title: Lang.queryJS('auth.mojang.error.methodNotAllowedTitle'), 59 | desc: Lang.queryJS('auth.mojang.error.methodNotAllowedDesc') 60 | } 61 | case MojangErrorCode.ERROR_NOT_FOUND: 62 | return { 63 | title: Lang.queryJS('auth.mojang.error.notFoundTitle'), 64 | desc: Lang.queryJS('auth.mojang.error.notFoundDesc') 65 | } 66 | case MojangErrorCode.ERROR_USER_MIGRATED: 67 | return { 68 | title: Lang.queryJS('auth.mojang.error.accountMigratedTitle'), 69 | desc: Lang.queryJS('auth.mojang.error.accountMigratedDesc') 70 | } 71 | case MojangErrorCode.ERROR_INVALID_CREDENTIALS: 72 | return { 73 | title: Lang.queryJS('auth.mojang.error.invalidCredentialsTitle'), 74 | desc: Lang.queryJS('auth.mojang.error.invalidCredentialsDesc') 75 | } 76 | case MojangErrorCode.ERROR_RATELIMIT: 77 | return { 78 | title: Lang.queryJS('auth.mojang.error.tooManyAttemptsTitle'), 79 | desc: Lang.queryJS('auth.mojang.error.tooManyAttemptsDesc') 80 | } 81 | case MojangErrorCode.ERROR_INVALID_TOKEN: 82 | return { 83 | title: Lang.queryJS('auth.mojang.error.invalidTokenTitle'), 84 | desc: Lang.queryJS('auth.mojang.error.invalidTokenDesc') 85 | } 86 | case MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE: 87 | return { 88 | title: Lang.queryJS('auth.mojang.error.tokenHasProfileTitle'), 89 | desc: Lang.queryJS('auth.mojang.error.tokenHasProfileDesc') 90 | } 91 | case MojangErrorCode.ERROR_CREDENTIALS_MISSING: 92 | return { 93 | title: Lang.queryJS('auth.mojang.error.credentialsMissingTitle'), 94 | desc: Lang.queryJS('auth.mojang.error.credentialsMissingDesc') 95 | } 96 | case MojangErrorCode.ERROR_INVALID_SALT_VERSION: 97 | return { 98 | title: Lang.queryJS('auth.mojang.error.invalidSaltVersionTitle'), 99 | desc: Lang.queryJS('auth.mojang.error.invalidSaltVersionDesc') 100 | } 101 | case MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE: 102 | return { 103 | title: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeTitle'), 104 | desc: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeDesc') 105 | } 106 | case MojangErrorCode.ERROR_GONE: 107 | return { 108 | title: Lang.queryJS('auth.mojang.error.accountGoneTitle'), 109 | desc: Lang.queryJS('auth.mojang.error.accountGoneDesc') 110 | } 111 | case MojangErrorCode.ERROR_UNREACHABLE: 112 | return { 113 | title: Lang.queryJS('auth.mojang.error.unreachableTitle'), 114 | desc: Lang.queryJS('auth.mojang.error.unreachableDesc') 115 | } 116 | case MojangErrorCode.ERROR_NOT_PAID: 117 | return { 118 | title: Lang.queryJS('auth.mojang.error.gameNotPurchasedTitle'), 119 | desc: Lang.queryJS('auth.mojang.error.gameNotPurchasedDesc') 120 | } 121 | case MojangErrorCode.UNKNOWN: 122 | return { 123 | title: Lang.queryJS('auth.mojang.error.unknownErrorTitle'), 124 | desc: Lang.queryJS('auth.mojang.error.unknownErrorDesc') 125 | } 126 | default: 127 | throw new Error(`Unknown error code: ${errorCode}`) 128 | } 129 | } 130 | 131 | // Functions 132 | 133 | /** 134 | * Add a Mojang account. This will authenticate the given credentials with Mojang's 135 | * authserver. The resultant data will be stored as an auth account in the 136 | * configuration database. 137 | * 138 | * @param {string} username The account username (email if migrated). 139 | * @param {string} password The account password. 140 | * @returns {Promise.} Promise which resolves the resolved authenticated account object. 141 | */ 142 | exports.addMojangAccount = async function(username, password) { 143 | try { 144 | const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken()) 145 | console.log(response) 146 | if(response.responseStatus === RestResponseStatus.SUCCESS) { 147 | 148 | const session = response.data 149 | if(session.selectedProfile != null){ 150 | const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) 151 | if(ConfigManager.getClientToken() == null){ 152 | ConfigManager.setClientToken(session.clientToken) 153 | } 154 | ConfigManager.save() 155 | return ret 156 | } else { 157 | return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID)) 158 | } 159 | 160 | } else { 161 | return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode)) 162 | } 163 | 164 | } catch (err){ 165 | log.error(err) 166 | return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN)) 167 | } 168 | } 169 | 170 | const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 } 171 | 172 | /** 173 | * Perform the full MS Auth flow in a given mode. 174 | * 175 | * AUTH_MODE.FULL = Full authorization for a new account. 176 | * AUTH_MODE.MS_REFRESH = Full refresh authorization. 177 | * AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token. 178 | * 179 | * @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken 180 | * @param {*} authMode The auth mode. 181 | * @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH. 182 | */ 183 | async function fullMicrosoftAuthFlow(entryCode, authMode) { 184 | try { 185 | 186 | let accessTokenRaw 187 | let accessToken 188 | if(authMode !== AUTH_MODE.MC_REFRESH) { 189 | const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID) 190 | if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) { 191 | return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode)) 192 | } 193 | accessToken = accessTokenResponse.data 194 | accessTokenRaw = accessToken.access_token 195 | } else { 196 | accessTokenRaw = entryCode 197 | } 198 | 199 | const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw) 200 | if(xblResponse.responseStatus === RestResponseStatus.ERROR) { 201 | return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode)) 202 | } 203 | const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data) 204 | if(xstsResonse.responseStatus === RestResponseStatus.ERROR) { 205 | return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode)) 206 | } 207 | const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data) 208 | if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) { 209 | return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode)) 210 | } 211 | const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token) 212 | if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) { 213 | return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode)) 214 | } 215 | return { 216 | accessToken, 217 | accessTokenRaw, 218 | xbl: xblResponse.data, 219 | xsts: xstsResonse.data, 220 | mcToken: mcTokenResponse.data, 221 | mcProfile: mcProfileResponse.data 222 | } 223 | } catch(err) { 224 | log.error(err) 225 | return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN)) 226 | } 227 | } 228 | 229 | /** 230 | * Calculate the expiry date. Advance the expiry time by 10 seconds 231 | * to reduce the liklihood of working with an expired token. 232 | * 233 | * @param {number} nowMs Current time milliseconds. 234 | * @param {number} epiresInS Expires in (seconds) 235 | * @returns 236 | */ 237 | function calculateExpiryDate(nowMs, epiresInS) { 238 | return nowMs + ((epiresInS-10)*1000) 239 | } 240 | 241 | /** 242 | * Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow. 243 | * The resultant data will be stored as an auth account in the configuration database. 244 | * 245 | * @param {string} authCode The authCode obtained from microsoft. 246 | * @returns {Promise.} Promise which resolves the resolved authenticated account object. 247 | */ 248 | exports.addMicrosoftAccount = async function(authCode) { 249 | 250 | const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL) 251 | 252 | // Advance expiry by 10 seconds to avoid close calls. 253 | const now = new Date().getTime() 254 | 255 | const ret = ConfigManager.addMicrosoftAuthAccount( 256 | fullAuth.mcProfile.id, 257 | fullAuth.mcToken.access_token, 258 | fullAuth.mcProfile.name, 259 | calculateExpiryDate(now, fullAuth.mcToken.expires_in), 260 | fullAuth.accessToken.access_token, 261 | fullAuth.accessToken.refresh_token, 262 | calculateExpiryDate(now, fullAuth.accessToken.expires_in) 263 | ) 264 | ConfigManager.save() 265 | 266 | return ret 267 | } 268 | 269 | /** 270 | * Remove a Mojang account. This will invalidate the access token associated 271 | * with the account and then remove it from the database. 272 | * 273 | * @param {string} uuid The UUID of the account to be removed. 274 | * @returns {Promise.} Promise which resolves to void when the action is complete. 275 | */ 276 | exports.removeMojangAccount = async function(uuid){ 277 | try { 278 | const authAcc = ConfigManager.getAuthAccount(uuid) 279 | const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) 280 | if(response.responseStatus === RestResponseStatus.SUCCESS) { 281 | ConfigManager.removeAuthAccount(uuid) 282 | ConfigManager.save() 283 | return Promise.resolve() 284 | } else { 285 | log.error('Error while removing account', response.error) 286 | return Promise.reject(response.error) 287 | } 288 | } catch (err){ 289 | log.error('Error while removing account', err) 290 | return Promise.reject(err) 291 | } 292 | } 293 | 294 | /** 295 | * Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout 296 | * through the ipc renderer. 297 | * 298 | * @param {string} uuid The UUID of the account to be removed. 299 | * @returns {Promise.} Promise which resolves to void when the action is complete. 300 | */ 301 | exports.removeMicrosoftAccount = async function(uuid){ 302 | try { 303 | ConfigManager.removeAuthAccount(uuid) 304 | ConfigManager.save() 305 | return Promise.resolve() 306 | } catch (err){ 307 | log.error('Error while removing account', err) 308 | return Promise.reject(err) 309 | } 310 | } 311 | 312 | /** 313 | * Validate the selected account with Mojang's authserver. If the account is not valid, 314 | * we will attempt to refresh the access token and update that value. If that fails, a 315 | * new login will be required. 316 | * 317 | * @returns {Promise.} Promise which resolves to true if the access token is valid, 318 | * otherwise false. 319 | */ 320 | async function validateSelectedMojangAccount(){ 321 | const current = ConfigManager.getSelectedAccount() 322 | const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken()) 323 | 324 | if(response.responseStatus === RestResponseStatus.SUCCESS) { 325 | const isValid = response.data 326 | if(!isValid){ 327 | const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken()) 328 | if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) { 329 | const session = refreshResponse.data 330 | ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken) 331 | ConfigManager.save() 332 | } else { 333 | log.error('Error while validating selected profile:', refreshResponse.error) 334 | log.info('Account access token is invalid.') 335 | return false 336 | } 337 | log.info('Account access token validated.') 338 | return true 339 | } else { 340 | log.info('Account access token validated.') 341 | return true 342 | } 343 | } 344 | 345 | } 346 | 347 | /** 348 | * Validate the selected account with Microsoft's authserver. If the account is not valid, 349 | * we will attempt to refresh the access token and update that value. If that fails, a 350 | * new login will be required. 351 | * 352 | * @returns {Promise.} Promise which resolves to true if the access token is valid, 353 | * otherwise false. 354 | */ 355 | async function validateSelectedMicrosoftAccount(){ 356 | const current = ConfigManager.getSelectedAccount() 357 | const now = new Date().getTime() 358 | const mcExpiresAt = current.expiresAt 359 | const mcExpired = now >= mcExpiresAt 360 | 361 | if(!mcExpired) { 362 | return true 363 | } 364 | 365 | // MC token expired. Check MS token. 366 | 367 | const msExpiresAt = current.microsoft.expires_at 368 | const msExpired = now >= msExpiresAt 369 | 370 | if(msExpired) { 371 | // MS expired, do full refresh. 372 | try { 373 | const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH) 374 | 375 | ConfigManager.updateMicrosoftAuthAccount( 376 | current.uuid, 377 | res.mcToken.access_token, 378 | res.accessToken.access_token, 379 | res.accessToken.refresh_token, 380 | calculateExpiryDate(now, res.accessToken.expires_in), 381 | calculateExpiryDate(now, res.mcToken.expires_in) 382 | ) 383 | ConfigManager.save() 384 | return true 385 | } catch(err) { 386 | return false 387 | } 388 | } else { 389 | // Only MC expired, use existing MS token. 390 | try { 391 | const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH) 392 | 393 | ConfigManager.updateMicrosoftAuthAccount( 394 | current.uuid, 395 | res.mcToken.access_token, 396 | current.microsoft.access_token, 397 | current.microsoft.refresh_token, 398 | current.microsoft.expires_at, 399 | calculateExpiryDate(now, res.mcToken.expires_in) 400 | ) 401 | ConfigManager.save() 402 | return true 403 | } 404 | catch(err) { 405 | return false 406 | } 407 | } 408 | } 409 | 410 | /** 411 | * Validate the selected auth account. 412 | * 413 | * @returns {Promise.} Promise which resolves to true if the access token is valid, 414 | * otherwise false. 415 | */ 416 | exports.validateSelected = async function(){ 417 | const current = ConfigManager.getSelectedAccount() 418 | 419 | if(current.type === 'microsoft') { 420 | return await validateSelectedMicrosoftAccount() 421 | } else { 422 | return await validateSelectedMojangAccount() 423 | } 424 | 425 | } 426 | -------------------------------------------------------------------------------- /app/assets/js/discordwrapper.js: -------------------------------------------------------------------------------- 1 | // Work in progress 2 | const { LoggerUtil } = require('helios-core') 3 | 4 | const logger = LoggerUtil.getLogger('DiscordWrapper') 5 | 6 | const { Client } = require('discord-rpc-patch') 7 | 8 | const Lang = require('./langloader') 9 | 10 | let client 11 | let activity 12 | 13 | exports.initRPC = function(genSettings, servSettings, initialDetails = Lang.queryJS('discord.waiting')){ 14 | client = new Client({ transport: 'ipc' }) 15 | 16 | activity = { 17 | details: initialDetails, 18 | state: Lang.queryJS('discord.state', {shortId: servSettings.shortId}), 19 | largeImageKey: servSettings.largeImageKey, 20 | largeImageText: servSettings.largeImageText, 21 | smallImageKey: genSettings.smallImageKey, 22 | smallImageText: genSettings.smallImageText, 23 | startTimestamp: new Date().getTime(), 24 | instance: false 25 | } 26 | 27 | client.on('ready', () => { 28 | logger.info('Discord RPC Connected') 29 | client.setActivity(activity) 30 | }) 31 | 32 | client.login({clientId: genSettings.clientId}).catch(error => { 33 | if(error.message.includes('ENOENT')) { 34 | logger.info('Unable to initialize Discord Rich Presence, no client detected.') 35 | } else { 36 | logger.info('Unable to initialize Discord Rich Presence: ' + error.message, error) 37 | } 38 | }) 39 | } 40 | 41 | exports.updateDetails = function(details){ 42 | activity.details = details 43 | client.setActivity(activity) 44 | } 45 | 46 | exports.shutdownRPC = function(){ 47 | if(!client) return 48 | client.clearActivity() 49 | client.destroy() 50 | client = null 51 | activity = null 52 | } -------------------------------------------------------------------------------- /app/assets/js/distromanager.js: -------------------------------------------------------------------------------- 1 | const { DistributionAPI } = require('helios-core/common') 2 | 3 | const ConfigManager = require('./configmanager') 4 | 5 | // Old WesterosCraft url. 6 | // exports.REMOTE_DISTRO_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json' 7 | exports.REMOTE_DISTRO_URL = 'https://helios-files.geekcorner.eu.org/distribution.json' 8 | 9 | const api = new DistributionAPI( 10 | ConfigManager.getLauncherDirectory(), 11 | null, // Injected forcefully by the preloader. 12 | null, // Injected forcefully by the preloader. 13 | exports.REMOTE_DISTRO_URL, 14 | false 15 | ) 16 | 17 | exports.DistroAPI = api -------------------------------------------------------------------------------- /app/assets/js/dropinmodutil.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const { ipcRenderer, shell } = require('electron') 4 | const { SHELL_OPCODE } = require('./ipcconstants') 5 | 6 | // Group #1: File Name (without .disabled, if any) 7 | // Group #2: File Extension (jar, zip, or litemod) 8 | // Group #3: If it is disabled (if string 'disabled' is present) 9 | const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/ 10 | const DISABLED_EXT = '.disabled' 11 | 12 | const SHADER_REGEX = /^(.+)\.zip$/ 13 | const SHADER_OPTION = /shaderPack=(.+)/ 14 | const SHADER_DIR = 'shaderpacks' 15 | const SHADER_CONFIG = 'optionsshaders.txt' 16 | 17 | /** 18 | * Validate that the given directory exists. If not, it is 19 | * created. 20 | * 21 | * @param {string} modsDir The path to the mods directory. 22 | */ 23 | exports.validateDir = function(dir) { 24 | fs.ensureDirSync(dir) 25 | } 26 | 27 | /** 28 | * Scan for drop-in mods in both the mods folder and version 29 | * safe mods folder. 30 | * 31 | * @param {string} modsDir The path to the mods directory. 32 | * @param {string} version The minecraft version of the server configuration. 33 | * 34 | * @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]} 35 | * An array of objects storing metadata about each discovered mod. 36 | */ 37 | exports.scanForDropinMods = function(modsDir, version) { 38 | const modsDiscovered = [] 39 | if(fs.existsSync(modsDir)){ 40 | let modCandidates = fs.readdirSync(modsDir) 41 | let verCandidates = [] 42 | const versionDir = path.join(modsDir, version) 43 | if(fs.existsSync(versionDir)){ 44 | verCandidates = fs.readdirSync(versionDir) 45 | } 46 | for(let file of modCandidates){ 47 | const match = MOD_REGEX.exec(file) 48 | if(match != null){ 49 | modsDiscovered.push({ 50 | fullName: match[0], 51 | name: match[1], 52 | ext: match[2], 53 | disabled: match[3] != null 54 | }) 55 | } 56 | } 57 | for(let file of verCandidates){ 58 | const match = MOD_REGEX.exec(file) 59 | if(match != null){ 60 | modsDiscovered.push({ 61 | fullName: path.join(version, match[0]), 62 | name: match[1], 63 | ext: match[2], 64 | disabled: match[3] != null 65 | }) 66 | } 67 | } 68 | } 69 | return modsDiscovered 70 | } 71 | 72 | /** 73 | * Add dropin mods. 74 | * 75 | * @param {FileList} files The files to add. 76 | * @param {string} modsDir The path to the mods directory. 77 | */ 78 | exports.addDropinMods = function(files, modsdir) { 79 | 80 | exports.validateDir(modsdir) 81 | 82 | for(let f of files) { 83 | if(MOD_REGEX.exec(f.name) != null) { 84 | fs.moveSync(f.path, path.join(modsdir, f.name)) 85 | } 86 | } 87 | 88 | } 89 | 90 | /** 91 | * Delete a drop-in mod from the file system. 92 | * 93 | * @param {string} modsDir The path to the mods directory. 94 | * @param {string} fullName The fullName of the discovered mod to delete. 95 | * 96 | * @returns {Promise.} True if the mod was deleted, otherwise false. 97 | */ 98 | exports.deleteDropinMod = async function(modsDir, fullName){ 99 | 100 | const res = await ipcRenderer.invoke(SHELL_OPCODE.TRASH_ITEM, path.join(modsDir, fullName)) 101 | 102 | if(!res.result) { 103 | shell.beep() 104 | console.error('Error deleting drop-in mod.', res.error) 105 | return false 106 | } 107 | 108 | return true 109 | } 110 | 111 | /** 112 | * Toggle a discovered mod on or off. This is achieved by either 113 | * adding or disabling the .disabled extension to the local file. 114 | * 115 | * @param {string} modsDir The path to the mods directory. 116 | * @param {string} fullName The fullName of the discovered mod to toggle. 117 | * @param {boolean} enable Whether to toggle on or off the mod. 118 | * 119 | * @returns {Promise.} A promise which resolves when the mod has 120 | * been toggled. If an IO error occurs the promise will be rejected. 121 | */ 122 | exports.toggleDropinMod = function(modsDir, fullName, enable){ 123 | return new Promise((resolve, reject) => { 124 | const oldPath = path.join(modsDir, fullName) 125 | const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT) 126 | 127 | fs.rename(oldPath, newPath, (err) => { 128 | if(err){ 129 | reject(err) 130 | } else { 131 | resolve() 132 | } 133 | }) 134 | }) 135 | } 136 | 137 | /** 138 | * Check if a drop-in mod is enabled. 139 | * 140 | * @param {string} fullName The fullName of the discovered mod to toggle. 141 | * @returns {boolean} True if the mod is enabled, otherwise false. 142 | */ 143 | exports.isDropinModEnabled = function(fullName){ 144 | return !fullName.endsWith(DISABLED_EXT) 145 | } 146 | 147 | /** 148 | * Scan for shaderpacks inside the shaderpacks folder. 149 | * 150 | * @param {string} instanceDir The path to the server instance directory. 151 | * 152 | * @returns {{fullName: string, name: string}[]} 153 | * An array of objects storing metadata about each discovered shaderpack. 154 | */ 155 | exports.scanForShaderpacks = function(instanceDir){ 156 | const shaderDir = path.join(instanceDir, SHADER_DIR) 157 | const packsDiscovered = [{ 158 | fullName: 'OFF', 159 | name: 'Off (Default)' 160 | }] 161 | if(fs.existsSync(shaderDir)){ 162 | let modCandidates = fs.readdirSync(shaderDir) 163 | for(let file of modCandidates){ 164 | const match = SHADER_REGEX.exec(file) 165 | if(match != null){ 166 | packsDiscovered.push({ 167 | fullName: match[0], 168 | name: match[1] 169 | }) 170 | } 171 | } 172 | } 173 | return packsDiscovered 174 | } 175 | 176 | /** 177 | * Read the optionsshaders.txt file to locate the current 178 | * enabled pack. If the file does not exist, OFF is returned. 179 | * 180 | * @param {string} instanceDir The path to the server instance directory. 181 | * 182 | * @returns {string} The file name of the enabled shaderpack. 183 | */ 184 | exports.getEnabledShaderpack = function(instanceDir){ 185 | exports.validateDir(instanceDir) 186 | 187 | const optionsShaders = path.join(instanceDir, SHADER_CONFIG) 188 | if(fs.existsSync(optionsShaders)){ 189 | const buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) 190 | const match = SHADER_OPTION.exec(buf) 191 | if(match != null){ 192 | return match[1] 193 | } else { 194 | console.warn('WARNING: Shaderpack regex failed.') 195 | } 196 | } 197 | return 'OFF' 198 | } 199 | 200 | /** 201 | * Set the enabled shaderpack. 202 | * 203 | * @param {string} instanceDir The path to the server instance directory. 204 | * @param {string} pack the file name of the shaderpack. 205 | */ 206 | exports.setEnabledShaderpack = function(instanceDir, pack){ 207 | exports.validateDir(instanceDir) 208 | 209 | const optionsShaders = path.join(instanceDir, SHADER_CONFIG) 210 | let buf 211 | if(fs.existsSync(optionsShaders)){ 212 | buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) 213 | buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`) 214 | } else { 215 | buf = `shaderPack=${pack}` 216 | } 217 | fs.writeFileSync(optionsShaders, buf, {encoding: 'utf-8'}) 218 | } 219 | 220 | /** 221 | * Add shaderpacks. 222 | * 223 | * @param {FileList} files The files to add. 224 | * @param {string} instanceDir The path to the server instance directory. 225 | */ 226 | exports.addShaderpacks = function(files, instanceDir) { 227 | 228 | const p = path.join(instanceDir, SHADER_DIR) 229 | 230 | exports.validateDir(p) 231 | 232 | for(let f of files) { 233 | if(SHADER_REGEX.exec(f.name) != null) { 234 | fs.moveSync(f.path, path.join(p, f.name)) 235 | } 236 | } 237 | 238 | } -------------------------------------------------------------------------------- /app/assets/js/ipcconstants.js: -------------------------------------------------------------------------------- 1 | // NOTE FOR THIRD-PARTY 2 | // REPLACE THIS CLIENT ID WITH YOUR APPLICATION ID. 3 | // SEE https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md 4 | exports.AZURE_CLIENT_ID = '1ce6e35a-126f-48fd-97fb-54d143ac6d45' 5 | // SEE NOTE ABOVE. 6 | 7 | 8 | // Opcodes 9 | exports.MSFT_OPCODE = { 10 | OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN', 11 | OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT', 12 | REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN', 13 | REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT' 14 | } 15 | // Reply types for REPLY opcode. 16 | exports.MSFT_REPLY_TYPE = { 17 | SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS', 18 | ERROR: 'MSFT_AUTH_REPLY_ERROR' 19 | } 20 | // Error types for ERROR reply. 21 | exports.MSFT_ERROR = { 22 | ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN', 23 | NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED' 24 | } 25 | 26 | exports.SHELL_OPCODE = { 27 | TRASH_ITEM: 'TRASH_ITEM' 28 | } -------------------------------------------------------------------------------- /app/assets/js/isdev.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 3 | const isEnvSet = 'ELECTRON_IS_DEV' in process.env 4 | 5 | module.exports = isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)) -------------------------------------------------------------------------------- /app/assets/js/langloader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const toml = require('toml') 4 | const merge = require('lodash.merge') 5 | 6 | let lang 7 | 8 | exports.loadLanguage = function(id){ 9 | lang = merge(lang || {}, toml.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.toml`))) || {}) 10 | } 11 | 12 | exports.query = function(id, placeHolders){ 13 | let query = id.split('.') 14 | let res = lang 15 | for(let q of query){ 16 | res = res[q] 17 | } 18 | let text = res === lang ? '' : res 19 | if (placeHolders) { 20 | Object.entries(placeHolders).forEach(([key, value]) => { 21 | text = text.replace(`{${key}}`, value) 22 | }) 23 | } 24 | return text 25 | } 26 | 27 | exports.queryJS = function(id, placeHolders){ 28 | return exports.query(`js.${id}`, placeHolders) 29 | } 30 | 31 | exports.queryEJS = function(id, placeHolders){ 32 | return exports.query(`ejs.${id}`, placeHolders) 33 | } 34 | 35 | exports.setupLanguage = function(){ 36 | // Load Language Files 37 | exports.loadLanguage('en_US') 38 | // Uncomment this when translations are ready 39 | //exports.loadLanguage('xx_XX') 40 | 41 | // Load Custom Language File for Launcher Customizer 42 | exports.loadLanguage('_custom') 43 | } -------------------------------------------------------------------------------- /app/assets/js/preloader.js: -------------------------------------------------------------------------------- 1 | const {ipcRenderer} = require('electron') 2 | const fs = require('fs-extra') 3 | const os = require('os') 4 | const path = require('path') 5 | 6 | const ConfigManager = require('./configmanager') 7 | const { DistroAPI } = require('./distromanager') 8 | const LangLoader = require('./langloader') 9 | const { LoggerUtil } = require('helios-core') 10 | // eslint-disable-next-line no-unused-vars 11 | const { HeliosDistribution } = require('helios-core/common') 12 | 13 | const logger = LoggerUtil.getLogger('Preloader') 14 | 15 | logger.info('Loading..') 16 | 17 | // Load ConfigManager 18 | ConfigManager.load() 19 | 20 | // Yuck! 21 | // TODO Fix this 22 | DistroAPI['commonDir'] = ConfigManager.getCommonDirectory() 23 | DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory() 24 | 25 | // Load Strings 26 | LangLoader.setupLanguage() 27 | 28 | /** 29 | * 30 | * @param {HeliosDistribution} data 31 | */ 32 | function onDistroLoad(data){ 33 | if(data != null){ 34 | 35 | // Resolve the selected server if its value has yet to be set. 36 | if(ConfigManager.getSelectedServer() == null || data.getServerById(ConfigManager.getSelectedServer()) == null){ 37 | logger.info('Determining default selected server..') 38 | ConfigManager.setSelectedServer(data.getMainServer().rawServer.id) 39 | ConfigManager.save() 40 | } 41 | } 42 | ipcRenderer.send('distributionIndexDone', data != null) 43 | } 44 | 45 | // Ensure Distribution is downloaded and cached. 46 | DistroAPI.getDistribution() 47 | .then(heliosDistro => { 48 | logger.info('Loaded distribution index.') 49 | 50 | onDistroLoad(heliosDistro) 51 | }) 52 | .catch(err => { 53 | logger.info('Failed to load an older version of the distribution index.') 54 | logger.info('Application cannot run.') 55 | logger.error(err) 56 | 57 | onDistroLoad(null) 58 | }) 59 | 60 | // Clean up temp dir incase previous launches ended unexpectedly. 61 | fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { 62 | if(err){ 63 | logger.warn('Error while cleaning natives directory', err) 64 | } else { 65 | logger.info('Cleaned natives directory.') 66 | } 67 | }) -------------------------------------------------------------------------------- /app/assets/js/scripts/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script for login.ejs 3 | */ 4 | // Validation Regexes. 5 | const validUsername = /^[a-zA-Z0-9_]{1,16}$/ 6 | const basicEmail = /^\S+@\S+\.\S+$/ 7 | //const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i 8 | 9 | // Login Elements 10 | const loginCancelContainer = document.getElementById('loginCancelContainer') 11 | const loginCancelButton = document.getElementById('loginCancelButton') 12 | const loginEmailError = document.getElementById('loginEmailError') 13 | const loginUsername = document.getElementById('loginUsername') 14 | const loginPasswordError = document.getElementById('loginPasswordError') 15 | const loginPassword = document.getElementById('loginPassword') 16 | const checkmarkContainer = document.getElementById('checkmarkContainer') 17 | const loginRememberOption = document.getElementById('loginRememberOption') 18 | const loginButton = document.getElementById('loginButton') 19 | const loginForm = document.getElementById('loginForm') 20 | 21 | // Control variables. 22 | let lu = false, lp = false 23 | 24 | 25 | /** 26 | * Show a login error. 27 | * 28 | * @param {HTMLElement} element The element on which to display the error. 29 | * @param {string} value The error text. 30 | */ 31 | function showError(element, value){ 32 | element.innerHTML = value 33 | element.style.opacity = 1 34 | } 35 | 36 | /** 37 | * Shake a login error to add emphasis. 38 | * 39 | * @param {HTMLElement} element The element to shake. 40 | */ 41 | function shakeError(element){ 42 | if(element.style.opacity == 1){ 43 | element.classList.remove('shake') 44 | void element.offsetWidth 45 | element.classList.add('shake') 46 | } 47 | } 48 | 49 | /** 50 | * Validate that an email field is neither empty nor invalid. 51 | * 52 | * @param {string} value The email value. 53 | */ 54 | function validateEmail(value){ 55 | if(value){ 56 | if(!basicEmail.test(value) && !validUsername.test(value)){ 57 | showError(loginEmailError, Lang.queryJS('login.error.invalidValue')) 58 | loginDisabled(true) 59 | lu = false 60 | } else { 61 | loginEmailError.style.opacity = 0 62 | lu = true 63 | if(lp){ 64 | loginDisabled(false) 65 | } 66 | } 67 | } else { 68 | lu = false 69 | showError(loginEmailError, Lang.queryJS('login.error.requiredValue')) 70 | loginDisabled(true) 71 | } 72 | } 73 | 74 | /** 75 | * Validate that the password field is not empty. 76 | * 77 | * @param {string} value The password value. 78 | */ 79 | function validatePassword(value){ 80 | if(value){ 81 | loginPasswordError.style.opacity = 0 82 | lp = true 83 | if(lu){ 84 | loginDisabled(false) 85 | } 86 | } else { 87 | lp = false 88 | showError(loginPasswordError, Lang.queryJS('login.error.invalidValue')) 89 | loginDisabled(true) 90 | } 91 | } 92 | 93 | // Emphasize errors with shake when focus is lost. 94 | loginUsername.addEventListener('focusout', (e) => { 95 | validateEmail(e.target.value) 96 | shakeError(loginEmailError) 97 | }) 98 | loginPassword.addEventListener('focusout', (e) => { 99 | validatePassword(e.target.value) 100 | shakeError(loginPasswordError) 101 | }) 102 | 103 | // Validate input for each field. 104 | loginUsername.addEventListener('input', (e) => { 105 | validateEmail(e.target.value) 106 | }) 107 | loginPassword.addEventListener('input', (e) => { 108 | validatePassword(e.target.value) 109 | }) 110 | 111 | /** 112 | * Enable or disable the login button. 113 | * 114 | * @param {boolean} v True to enable, false to disable. 115 | */ 116 | function loginDisabled(v){ 117 | if(loginButton.disabled !== v){ 118 | loginButton.disabled = v 119 | } 120 | } 121 | 122 | /** 123 | * Enable or disable loading elements. 124 | * 125 | * @param {boolean} v True to enable, false to disable. 126 | */ 127 | function loginLoading(v){ 128 | if(v){ 129 | loginButton.setAttribute('loading', v) 130 | loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn')) 131 | } else { 132 | loginButton.removeAttribute('loading') 133 | loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login')) 134 | } 135 | } 136 | 137 | /** 138 | * Enable or disable login form. 139 | * 140 | * @param {boolean} v True to enable, false to disable. 141 | */ 142 | function formDisabled(v){ 143 | loginDisabled(v) 144 | loginCancelButton.disabled = v 145 | loginUsername.disabled = v 146 | loginPassword.disabled = v 147 | if(v){ 148 | checkmarkContainer.setAttribute('disabled', v) 149 | } else { 150 | checkmarkContainer.removeAttribute('disabled') 151 | } 152 | loginRememberOption.disabled = v 153 | } 154 | 155 | let loginViewOnSuccess = VIEWS.landing 156 | let loginViewOnCancel = VIEWS.settings 157 | let loginViewCancelHandler 158 | 159 | function loginCancelEnabled(val){ 160 | if(val){ 161 | $(loginCancelContainer).show() 162 | } else { 163 | $(loginCancelContainer).hide() 164 | } 165 | } 166 | 167 | loginCancelButton.onclick = (e) => { 168 | switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => { 169 | loginUsername.value = '' 170 | loginPassword.value = '' 171 | loginCancelEnabled(false) 172 | if(loginViewCancelHandler != null){ 173 | loginViewCancelHandler() 174 | loginViewCancelHandler = null 175 | } 176 | }) 177 | } 178 | 179 | // Disable default form behavior. 180 | loginForm.onsubmit = () => { return false } 181 | 182 | // Bind login button behavior. 183 | loginButton.addEventListener('click', () => { 184 | // Disable form. 185 | formDisabled(true) 186 | 187 | // Show loading stuff. 188 | loginLoading(true) 189 | 190 | AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => { 191 | updateSelectedAccount(value) 192 | loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) 193 | $('.circle-loader').toggleClass('load-complete') 194 | $('.checkmark').toggle() 195 | setTimeout(() => { 196 | switchView(VIEWS.login, loginViewOnSuccess, 500, 500, async () => { 197 | // Temporary workaround 198 | if(loginViewOnSuccess === VIEWS.settings){ 199 | await prepareSettings() 200 | } 201 | loginViewOnSuccess = VIEWS.landing // Reset this for good measure. 202 | loginCancelEnabled(false) // Reset this for good measure. 203 | loginViewCancelHandler = null // Reset this for good measure. 204 | loginUsername.value = '' 205 | loginPassword.value = '' 206 | $('.circle-loader').toggleClass('load-complete') 207 | $('.checkmark').toggle() 208 | loginLoading(false) 209 | loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login')) 210 | formDisabled(false) 211 | }) 212 | }, 1000) 213 | }).catch((displayableError) => { 214 | loginLoading(false) 215 | 216 | let actualDisplayableError 217 | if(isDisplayableError(displayableError)) { 218 | msftLoginLogger.error('Error while logging in.', displayableError) 219 | actualDisplayableError = displayableError 220 | } else { 221 | // Uh oh. 222 | msftLoginLogger.error('Unhandled error during login.', displayableError) 223 | actualDisplayableError = Lang.queryJS('login.error.unknown') 224 | } 225 | 226 | setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain')) 227 | setOverlayHandler(() => { 228 | formDisabled(false) 229 | toggleOverlay(false) 230 | }) 231 | toggleOverlay(true) 232 | }) 233 | 234 | }) -------------------------------------------------------------------------------- /app/assets/js/scripts/loginOptions.js: -------------------------------------------------------------------------------- 1 | const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer') 2 | const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft') 3 | const loginOptionMojang = document.getElementById('loginOptionMojang') 4 | const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton') 5 | 6 | let loginOptionsCancellable = false 7 | 8 | let loginOptionsViewOnLoginSuccess 9 | let loginOptionsViewOnLoginCancel 10 | let loginOptionsViewOnCancel 11 | let loginOptionsViewCancelHandler 12 | 13 | function loginOptionsCancelEnabled(val){ 14 | if(val){ 15 | $(loginOptionsCancelContainer).show() 16 | } else { 17 | $(loginOptionsCancelContainer).hide() 18 | } 19 | } 20 | 21 | loginOptionMicrosoft.onclick = (e) => { 22 | switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { 23 | ipcRenderer.send( 24 | MSFT_OPCODE.OPEN_LOGIN, 25 | loginOptionsViewOnLoginSuccess, 26 | loginOptionsViewOnLoginCancel 27 | ) 28 | }) 29 | } 30 | 31 | loginOptionMojang.onclick = (e) => { 32 | switchView(getCurrentView(), VIEWS.login, 500, 500, () => { 33 | loginViewOnSuccess = loginOptionsViewOnLoginSuccess 34 | loginViewOnCancel = loginOptionsViewOnLoginCancel 35 | loginCancelEnabled(true) 36 | }) 37 | } 38 | 39 | loginOptionsCancelButton.onclick = (e) => { 40 | switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => { 41 | // Clear login values (Mojang login) 42 | // No cleanup needed for Microsoft. 43 | loginUsername.value = '' 44 | loginPassword.value = '' 45 | if(loginOptionsViewCancelHandler != null){ 46 | loginOptionsViewCancelHandler() 47 | loginOptionsViewCancelHandler = null 48 | } 49 | }) 50 | } -------------------------------------------------------------------------------- /app/assets/js/scripts/overlay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script for overlay.ejs 3 | */ 4 | 5 | /* Overlay Wrapper Functions */ 6 | 7 | /** 8 | * Check to see if the overlay is visible. 9 | * 10 | * @returns {boolean} Whether or not the overlay is visible. 11 | */ 12 | function isOverlayVisible(){ 13 | return document.getElementById('main').hasAttribute('overlay') 14 | } 15 | 16 | let overlayHandlerContent 17 | 18 | /** 19 | * Overlay keydown handler for a non-dismissable overlay. 20 | * 21 | * @param {KeyboardEvent} e The keydown event. 22 | */ 23 | function overlayKeyHandler (e){ 24 | if(e.key === 'Enter' || e.key === 'Escape'){ 25 | document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() 26 | } 27 | } 28 | /** 29 | * Overlay keydown handler for a dismissable overlay. 30 | * 31 | * @param {KeyboardEvent} e The keydown event. 32 | */ 33 | function overlayKeyDismissableHandler (e){ 34 | if(e.key === 'Enter'){ 35 | document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() 36 | } else if(e.key === 'Escape'){ 37 | document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEsc')[0].click() 38 | } 39 | } 40 | 41 | /** 42 | * Bind overlay keydown listeners for escape and exit. 43 | * 44 | * @param {boolean} state Whether or not to add new event listeners. 45 | * @param {string} content The overlay content which will be shown. 46 | * @param {boolean} dismissable Whether or not the overlay is dismissable 47 | */ 48 | function bindOverlayKeys(state, content, dismissable){ 49 | overlayHandlerContent = content 50 | document.removeEventListener('keydown', overlayKeyHandler) 51 | document.removeEventListener('keydown', overlayKeyDismissableHandler) 52 | if(state){ 53 | if(dismissable){ 54 | document.addEventListener('keydown', overlayKeyDismissableHandler) 55 | } else { 56 | document.addEventListener('keydown', overlayKeyHandler) 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Toggle the visibility of the overlay. 63 | * 64 | * @param {boolean} toggleState True to display, false to hide. 65 | * @param {boolean} dismissable Optional. True to show the dismiss option, otherwise false. 66 | * @param {string} content Optional. The content div to be shown. 67 | */ 68 | function toggleOverlay(toggleState, dismissable = false, content = 'overlayContent'){ 69 | if(toggleState == null){ 70 | toggleState = !document.getElementById('main').hasAttribute('overlay') 71 | } 72 | if(typeof dismissable === 'string'){ 73 | content = dismissable 74 | dismissable = false 75 | } 76 | bindOverlayKeys(toggleState, content, dismissable) 77 | if(toggleState){ 78 | document.getElementById('main').setAttribute('overlay', true) 79 | // Make things untabbable. 80 | $('#main *').attr('tabindex', '-1') 81 | $('#' + content).parent().children().hide() 82 | $('#' + content).show() 83 | if(dismissable){ 84 | $('#overlayDismiss').show() 85 | } else { 86 | $('#overlayDismiss').hide() 87 | } 88 | $('#overlayContainer').fadeIn({ 89 | duration: 250, 90 | start: () => { 91 | if(getCurrentView() === VIEWS.settings){ 92 | document.getElementById('settingsContainer').style.backgroundColor = 'transparent' 93 | } 94 | } 95 | }) 96 | } else { 97 | document.getElementById('main').removeAttribute('overlay') 98 | // Make things tabbable. 99 | $('#main *').removeAttr('tabindex') 100 | $('#overlayContainer').fadeOut({ 101 | duration: 250, 102 | start: () => { 103 | if(getCurrentView() === VIEWS.settings){ 104 | document.getElementById('settingsContainer').style.backgroundColor = 'rgba(0, 0, 0, 0.50)' 105 | } 106 | }, 107 | complete: () => { 108 | $('#' + content).parent().children().hide() 109 | $('#' + content).show() 110 | if(dismissable){ 111 | $('#overlayDismiss').show() 112 | } else { 113 | $('#overlayDismiss').hide() 114 | } 115 | } 116 | }) 117 | } 118 | } 119 | 120 | async function toggleServerSelection(toggleState){ 121 | await prepareServerSelectionList() 122 | toggleOverlay(toggleState, true, 'serverSelectContent') 123 | } 124 | 125 | /** 126 | * Set the content of the overlay. 127 | * 128 | * @param {string} title Overlay title text. 129 | * @param {string} description Overlay description text. 130 | * @param {string} acknowledge Acknowledge button text. 131 | * @param {string} dismiss Dismiss button text. 132 | */ 133 | function setOverlayContent(title, description, acknowledge, dismiss = Lang.queryJS('overlay.dismiss')){ 134 | document.getElementById('overlayTitle').innerHTML = title 135 | document.getElementById('overlayDesc').innerHTML = description 136 | document.getElementById('overlayAcknowledge').innerHTML = acknowledge 137 | document.getElementById('overlayDismiss').innerHTML = dismiss 138 | } 139 | 140 | /** 141 | * Set the onclick handler of the overlay acknowledge button. 142 | * If the handler is null, a default handler will be added. 143 | * 144 | * @param {function} handler 145 | */ 146 | function setOverlayHandler(handler){ 147 | if(handler == null){ 148 | document.getElementById('overlayAcknowledge').onclick = () => { 149 | toggleOverlay(false) 150 | } 151 | } else { 152 | document.getElementById('overlayAcknowledge').onclick = handler 153 | } 154 | } 155 | 156 | /** 157 | * Set the onclick handler of the overlay dismiss button. 158 | * If the handler is null, a default handler will be added. 159 | * 160 | * @param {function} handler 161 | */ 162 | function setDismissHandler(handler){ 163 | if(handler == null){ 164 | document.getElementById('overlayDismiss').onclick = () => { 165 | toggleOverlay(false) 166 | } 167 | } else { 168 | document.getElementById('overlayDismiss').onclick = handler 169 | } 170 | } 171 | 172 | /* Server Select View */ 173 | 174 | document.getElementById('serverSelectConfirm').addEventListener('click', async () => { 175 | const listings = document.getElementsByClassName('serverListing') 176 | for(let i=0; i 0){ 187 | const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid')) 188 | updateSelectedServer(serv) 189 | toggleOverlay(false) 190 | } 191 | }) 192 | 193 | document.getElementById('accountSelectConfirm').addEventListener('click', async () => { 194 | const listings = document.getElementsByClassName('accountListing') 195 | for(let i=0; i 0){ 210 | const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid')) 211 | ConfigManager.save() 212 | updateSelectedAccount(authAcc) 213 | if(getCurrentView() === VIEWS.settings) { 214 | await prepareSettings() 215 | } 216 | toggleOverlay(false) 217 | validateSelectedAccount() 218 | } 219 | }) 220 | 221 | // Bind server select cancel button. 222 | document.getElementById('serverSelectCancel').addEventListener('click', () => { 223 | toggleOverlay(false) 224 | }) 225 | 226 | document.getElementById('accountSelectCancel').addEventListener('click', () => { 227 | $('#accountSelectContent').fadeOut(250, () => { 228 | $('#overlayContent').fadeIn(250) 229 | }) 230 | }) 231 | 232 | function setServerListingHandlers(){ 233 | const listings = Array.from(document.getElementsByClassName('serverListing')) 234 | listings.map((val) => { 235 | val.onclick = e => { 236 | if(val.hasAttribute('selected')){ 237 | return 238 | } 239 | const cListings = document.getElementsByClassName('serverListing') 240 | for(let i=0; i { 254 | val.onclick = e => { 255 | if(val.hasAttribute('selected')){ 256 | return 257 | } 258 | const cListings = document.getElementsByClassName('accountListing') 259 | for(let i=0; i 277 | 278 |
279 | ${serv.rawServer.name} 280 | ${serv.rawServer.description} 281 |
282 |
${serv.rawServer.minecraftVersion}
283 |
${serv.rawServer.version}
284 | ${serv.rawServer.mainServer ? `
285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | ${Lang.queryJS('settings.serverListing.mainServer')} 293 |
` : ''} 294 |
295 |
296 | ` 297 | } 298 | document.getElementById('serverSelectListScrollable').innerHTML = htmlString 299 | 300 | } 301 | 302 | function populateAccountListings(){ 303 | const accountsObj = ConfigManager.getAuthAccounts() 304 | const accounts = Array.from(Object.keys(accountsObj), v=>accountsObj[v]) 305 | let htmlString = '' 306 | for(let i=0; i 308 | 309 |
${accounts[i].displayName}
310 | ` 311 | } 312 | document.getElementById('accountSelectListScrollable').innerHTML = htmlString 313 | 314 | } 315 | 316 | async function prepareServerSelectionList(){ 317 | await populateServerListings() 318 | setServerListingHandlers() 319 | } 320 | 321 | function prepareAccountSelectionList(){ 322 | populateAccountListings() 323 | setAccountListingHandlers() 324 | } -------------------------------------------------------------------------------- /app/assets/js/scripts/uibinder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initialize UI functions which depend on internal modules. 3 | * Loaded after core UI functions are initialized in uicore.js. 4 | */ 5 | // Requirements 6 | const path = require('path') 7 | const { Type } = require('helios-distribution-types') 8 | 9 | const AuthManager = require('./assets/js/authmanager') 10 | const ConfigManager = require('./assets/js/configmanager') 11 | const { DistroAPI } = require('./assets/js/distromanager') 12 | 13 | let rscShouldLoad = false 14 | let fatalStartupError = false 15 | 16 | // Mapping of each view to their container IDs. 17 | const VIEWS = { 18 | landing: '#landingContainer', 19 | loginOptions: '#loginOptionsContainer', 20 | login: '#loginContainer', 21 | settings: '#settingsContainer', 22 | welcome: '#welcomeContainer', 23 | waiting: '#waitingContainer' 24 | } 25 | 26 | // The currently shown view container. 27 | let currentView 28 | 29 | /** 30 | * Switch launcher views. 31 | * 32 | * @param {string} current The ID of the current view container. 33 | * @param {*} next The ID of the next view container. 34 | * @param {*} currentFadeTime Optional. The fade out time for the current view. 35 | * @param {*} nextFadeTime Optional. The fade in time for the next view. 36 | * @param {*} onCurrentFade Optional. Callback function to execute when the current 37 | * view fades out. 38 | * @param {*} onNextFade Optional. Callback function to execute when the next view 39 | * fades in. 40 | */ 41 | function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){ 42 | currentView = next 43 | $(`${current}`).fadeOut(currentFadeTime, async () => { 44 | await onCurrentFade() 45 | $(`${next}`).fadeIn(nextFadeTime, async () => { 46 | await onNextFade() 47 | }) 48 | }) 49 | } 50 | 51 | /** 52 | * Get the currently shown view container. 53 | * 54 | * @returns {string} The currently shown view container. 55 | */ 56 | function getCurrentView(){ 57 | return currentView 58 | } 59 | 60 | async function showMainUI(data){ 61 | 62 | if(!isDev){ 63 | loggerAutoUpdater.info('Initializing..') 64 | ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) 65 | } 66 | 67 | await prepareSettings(true) 68 | updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer())) 69 | refreshServerStatus() 70 | setTimeout(() => { 71 | document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' 72 | document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.jpg')` 73 | $('#main').show() 74 | 75 | const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0 76 | 77 | // If this is enabled in a development environment we'll get ratelimited. 78 | // The relaunch frequency is usually far too high. 79 | if(!isDev && isLoggedIn){ 80 | validateSelectedAccount() 81 | } 82 | 83 | if(ConfigManager.isFirstLaunch()){ 84 | currentView = VIEWS.welcome 85 | $(VIEWS.welcome).fadeIn(1000) 86 | } else { 87 | if(isLoggedIn){ 88 | currentView = VIEWS.landing 89 | $(VIEWS.landing).fadeIn(1000) 90 | } else { 91 | loginOptionsCancelEnabled(false) 92 | loginOptionsViewOnLoginSuccess = VIEWS.landing 93 | loginOptionsViewOnLoginCancel = VIEWS.loginOptions 94 | currentView = VIEWS.loginOptions 95 | $(VIEWS.loginOptions).fadeIn(1000) 96 | } 97 | } 98 | 99 | setTimeout(() => { 100 | $('#loadingContainer').fadeOut(500, () => { 101 | $('#loadSpinnerImage').removeClass('rotating') 102 | }) 103 | }, 250) 104 | 105 | }, 750) 106 | // Disable tabbing to the news container. 107 | initNews().then(() => { 108 | $('#newsContainer *').attr('tabindex', '-1') 109 | }) 110 | } 111 | 112 | function showFatalStartupError(){ 113 | setTimeout(() => { 114 | $('#loadingContainer').fadeOut(250, () => { 115 | document.getElementById('overlayContainer').style.background = 'none' 116 | setOverlayContent( 117 | Lang.queryJS('uibinder.startup.fatalErrorTitle'), 118 | Lang.queryJS('uibinder.startup.fatalErrorMessage'), 119 | Lang.queryJS('uibinder.startup.closeButton') 120 | ) 121 | setOverlayHandler(() => { 122 | const window = remote.getCurrentWindow() 123 | window.close() 124 | }) 125 | toggleOverlay(true) 126 | }) 127 | }, 750) 128 | } 129 | 130 | /** 131 | * Common functions to perform after refreshing the distro index. 132 | * 133 | * @param {Object} data The distro index object. 134 | */ 135 | function onDistroRefresh(data){ 136 | updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer())) 137 | refreshServerStatus() 138 | initNews() 139 | syncModConfigurations(data) 140 | ensureJavaSettings(data) 141 | } 142 | 143 | /** 144 | * Sync the mod configurations with the distro index. 145 | * 146 | * @param {Object} data The distro index object. 147 | */ 148 | function syncModConfigurations(data){ 149 | 150 | const syncedCfgs = [] 151 | 152 | for(let serv of data.servers){ 153 | 154 | const id = serv.rawServer.id 155 | const mdls = serv.modules 156 | const cfg = ConfigManager.getModConfiguration(id) 157 | 158 | if(cfg != null){ 159 | 160 | const modsOld = cfg.mods 161 | const mods = {} 162 | 163 | for(let mdl of mdls){ 164 | const type = mdl.rawModule.type 165 | 166 | if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){ 167 | if(!mdl.getRequired().value){ 168 | const mdlID = mdl.getVersionlessMavenIdentifier() 169 | if(modsOld[mdlID] == null){ 170 | mods[mdlID] = scanOptionalSubModules(mdl.subModules, mdl) 171 | } else { 172 | mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.subModules, mdl), false) 173 | } 174 | } else { 175 | if(mdl.subModules.length > 0){ 176 | const mdlID = mdl.getVersionlessMavenIdentifier() 177 | const v = scanOptionalSubModules(mdl.subModules, mdl) 178 | if(typeof v === 'object'){ 179 | if(modsOld[mdlID] == null){ 180 | mods[mdlID] = v 181 | } else { 182 | mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true) 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | syncedCfgs.push({ 191 | id, 192 | mods 193 | }) 194 | 195 | } else { 196 | 197 | const mods = {} 198 | 199 | for(let mdl of mdls){ 200 | const type = mdl.rawModule.type 201 | if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){ 202 | if(!mdl.getRequired().value){ 203 | mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl) 204 | } else { 205 | if(mdl.subModules.length > 0){ 206 | const v = scanOptionalSubModules(mdl.subModules, mdl) 207 | if(typeof v === 'object'){ 208 | mods[mdl.getVersionlessMavenIdentifier()] = v 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | syncedCfgs.push({ 216 | id, 217 | mods 218 | }) 219 | 220 | } 221 | } 222 | 223 | ConfigManager.setModConfigurations(syncedCfgs) 224 | ConfigManager.save() 225 | } 226 | 227 | /** 228 | * Ensure java configurations are present for the available servers. 229 | * 230 | * @param {Object} data The distro index object. 231 | */ 232 | function ensureJavaSettings(data) { 233 | 234 | // Nothing too fancy for now. 235 | for(const serv of data.servers){ 236 | ConfigManager.ensureJavaConfig(serv.rawServer.id, serv.effectiveJavaOptions, serv.rawServer.javaOptions?.ram) 237 | } 238 | 239 | ConfigManager.save() 240 | } 241 | 242 | /** 243 | * Recursively scan for optional sub modules. If none are found, 244 | * this function returns a boolean. If optional sub modules do exist, 245 | * a recursive configuration object is returned. 246 | * 247 | * @returns {boolean | Object} The resolved mod configuration. 248 | */ 249 | function scanOptionalSubModules(mdls, origin){ 250 | if(mdls != null){ 251 | const mods = {} 252 | 253 | for(let mdl of mdls){ 254 | const type = mdl.rawModule.type 255 | // Optional types. 256 | if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){ 257 | // It is optional. 258 | if(!mdl.getRequired().value){ 259 | mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl) 260 | } else { 261 | if(mdl.hasSubModules()){ 262 | const v = scanOptionalSubModules(mdl.subModules, mdl) 263 | if(typeof v === 'object'){ 264 | mods[mdl.getVersionlessMavenIdentifier()] = v 265 | } 266 | } 267 | } 268 | } 269 | } 270 | 271 | if(Object.keys(mods).length > 0){ 272 | const ret = { 273 | mods 274 | } 275 | if(!origin.getRequired().value){ 276 | ret.value = origin.getRequired().def 277 | } 278 | return ret 279 | } 280 | } 281 | return origin.getRequired().def 282 | } 283 | 284 | /** 285 | * Recursively merge an old configuration into a new configuration. 286 | * 287 | * @param {boolean | Object} o The old configuration value. 288 | * @param {boolean | Object} n The new configuration value. 289 | * @param {boolean} nReq If the new value is a required mod. 290 | * 291 | * @returns {boolean | Object} The merged configuration. 292 | */ 293 | function mergeModConfiguration(o, n, nReq = false){ 294 | if(typeof o === 'boolean'){ 295 | if(typeof n === 'boolean') return o 296 | else if(typeof n === 'object'){ 297 | if(!nReq){ 298 | n.value = o 299 | } 300 | return n 301 | } 302 | } else if(typeof o === 'object'){ 303 | if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true 304 | else if(typeof n === 'object'){ 305 | if(!nReq){ 306 | n.value = typeof o.value !== 'undefined' ? o.value : true 307 | } 308 | 309 | const newMods = Object.keys(n.mods) 310 | for(let i=0; i 0 337 | ? Lang.queryJS('uibinder.validateAccount.failedMessage', { 'account': selectedAcc.displayName }) 338 | : Lang.queryJS('uibinder.validateAccount.failedMessageSelectAnotherAccount', { 'account': selectedAcc.displayName }), 339 | Lang.queryJS('uibinder.validateAccount.loginButton'), 340 | Lang.queryJS('uibinder.validateAccount.selectAnotherAccountButton') 341 | ) 342 | setOverlayHandler(() => { 343 | 344 | const isMicrosoft = selectedAcc.type === 'microsoft' 345 | 346 | if(isMicrosoft) { 347 | // Empty for now 348 | } else { 349 | // Mojang 350 | // For convenience, pre-populate the username of the account. 351 | document.getElementById('loginUsername').value = selectedAcc.username 352 | validateEmail(selectedAcc.username) 353 | } 354 | 355 | loginOptionsViewOnLoginSuccess = getCurrentView() 356 | loginOptionsViewOnLoginCancel = VIEWS.loginOptions 357 | 358 | if(accLen > 0) { 359 | loginOptionsViewOnCancel = getCurrentView() 360 | loginOptionsViewCancelHandler = () => { 361 | if(isMicrosoft) { 362 | ConfigManager.addMicrosoftAuthAccount( 363 | selectedAcc.uuid, 364 | selectedAcc.accessToken, 365 | selectedAcc.username, 366 | selectedAcc.expiresAt, 367 | selectedAcc.microsoft.access_token, 368 | selectedAcc.microsoft.refresh_token, 369 | selectedAcc.microsoft.expires_at 370 | ) 371 | } else { 372 | ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) 373 | } 374 | ConfigManager.save() 375 | validateSelectedAccount() 376 | } 377 | loginOptionsCancelEnabled(true) 378 | } else { 379 | loginOptionsCancelEnabled(false) 380 | } 381 | toggleOverlay(false) 382 | switchView(getCurrentView(), VIEWS.loginOptions) 383 | }) 384 | setDismissHandler(() => { 385 | if(accLen > 1){ 386 | prepareAccountSelectionList() 387 | $('#overlayContent').fadeOut(250, () => { 388 | bindOverlayKeys(true, 'accountSelectContent', true) 389 | $('#accountSelectContent').fadeIn(250) 390 | }) 391 | } else { 392 | const accountsObj = ConfigManager.getAuthAccounts() 393 | const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v]) 394 | // This function validates the account switch. 395 | setSelectedAccount(accounts[0].uuid) 396 | toggleOverlay(false) 397 | } 398 | }) 399 | toggleOverlay(true, accLen > 0) 400 | } else { 401 | return true 402 | } 403 | } else { 404 | return true 405 | } 406 | } 407 | 408 | /** 409 | * Temporary function to update the selected account along 410 | * with the relevent UI elements. 411 | * 412 | * @param {string} uuid The UUID of the account. 413 | */ 414 | function setSelectedAccount(uuid){ 415 | const authAcc = ConfigManager.setSelectedAccount(uuid) 416 | ConfigManager.save() 417 | updateSelectedAccount(authAcc) 418 | validateSelectedAccount() 419 | } 420 | 421 | // Synchronous Listener 422 | document.addEventListener('readystatechange', async () => { 423 | 424 | if (document.readyState === 'interactive' || document.readyState === 'complete'){ 425 | if(rscShouldLoad){ 426 | rscShouldLoad = false 427 | if(!fatalStartupError){ 428 | const data = await DistroAPI.getDistribution() 429 | await showMainUI(data) 430 | } else { 431 | showFatalStartupError() 432 | } 433 | } 434 | } 435 | 436 | }, false) 437 | 438 | // Actions that must be performed after the distribution index is downloaded. 439 | ipcRenderer.on('distributionIndexDone', async (event, res) => { 440 | if(res) { 441 | const data = await DistroAPI.getDistribution() 442 | syncModConfigurations(data) 443 | ensureJavaSettings(data) 444 | if(document.readyState === 'interactive' || document.readyState === 'complete'){ 445 | await showMainUI(data) 446 | } else { 447 | rscShouldLoad = true 448 | } 449 | } else { 450 | fatalStartupError = true 451 | if(document.readyState === 'interactive' || document.readyState === 'complete'){ 452 | showFatalStartupError() 453 | } else { 454 | rscShouldLoad = true 455 | } 456 | } 457 | }) 458 | 459 | // Util for development 460 | async function devModeToggle() { 461 | DistroAPI.toggleDevMode(true) 462 | const data = await DistroAPI.refreshDistributionOrFallback() 463 | ensureJavaSettings(data) 464 | updateSelectedServer(data.servers[0]) 465 | syncModConfigurations(data) 466 | } 467 | -------------------------------------------------------------------------------- /app/assets/js/scripts/uicore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Core UI functions are initialized in this file. This prevents 3 | * unexpected errors from breaking the core features. Specifically, 4 | * actions in this file should not require the usage of any internal 5 | * modules, excluding dependencies. 6 | */ 7 | // Requirements 8 | const $ = require('jquery') 9 | const {ipcRenderer, shell, webFrame} = require('electron') 10 | const remote = require('@electron/remote') 11 | const isDev = require('./assets/js/isdev') 12 | const { LoggerUtil } = require('helios-core') 13 | const Lang = require('./assets/js/langloader') 14 | 15 | const loggerUICore = LoggerUtil.getLogger('UICore') 16 | const loggerAutoUpdater = LoggerUtil.getLogger('AutoUpdater') 17 | 18 | // Log deprecation and process warnings. 19 | process.traceProcessWarnings = true 20 | process.traceDeprecation = true 21 | 22 | // Disable eval function. 23 | // eslint-disable-next-line 24 | window.eval = global.eval = function () { 25 | throw new Error('Sorry, this app does not support window.eval().') 26 | } 27 | 28 | // Display warning when devtools window is opened. 29 | remote.getCurrentWebContents().on('devtools-opened', () => { 30 | console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold') 31 | console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px') 32 | console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px') 33 | }) 34 | 35 | // Disable zoom, needed for darwin. 36 | webFrame.setZoomLevel(0) 37 | webFrame.setVisualZoomLevelLimits(1, 1) 38 | 39 | // Initialize auto updates in production environments. 40 | let updateCheckListener 41 | if(!isDev){ 42 | ipcRenderer.on('autoUpdateNotification', (event, arg, info) => { 43 | switch(arg){ 44 | case 'checking-for-update': 45 | loggerAutoUpdater.info('Checking for update..') 46 | settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkingForUpdateButton'), true) 47 | break 48 | case 'update-available': 49 | loggerAutoUpdater.info('New update available', info.version) 50 | 51 | if(process.platform === 'darwin'){ 52 | info.darwindownload = `https://github.com/dscalzi/HeliosLauncher/releases/download/v${info.version}/Helios-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg` 53 | showUpdateUI(info) 54 | } 55 | 56 | populateSettingsUpdateInformation(info) 57 | break 58 | case 'update-downloaded': 59 | loggerAutoUpdater.info('Update ' + info.version + ' ready to be installed.') 60 | settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.installNowButton'), false, () => { 61 | if(!isDev){ 62 | ipcRenderer.send('autoUpdateAction', 'installUpdateNow') 63 | } 64 | }) 65 | showUpdateUI(info) 66 | break 67 | case 'update-not-available': 68 | loggerAutoUpdater.info('No new update found.') 69 | settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkForUpdatesButton')) 70 | break 71 | case 'ready': 72 | updateCheckListener = setInterval(() => { 73 | ipcRenderer.send('autoUpdateAction', 'checkForUpdate') 74 | }, 1800000) 75 | ipcRenderer.send('autoUpdateAction', 'checkForUpdate') 76 | break 77 | case 'realerror': 78 | if(info != null && info.code != null){ 79 | if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){ 80 | loggerAutoUpdater.info('No suitable releases found.') 81 | } else if(info.code === 'ERR_XML_MISSED_ELEMENT'){ 82 | loggerAutoUpdater.info('No releases found.') 83 | } else { 84 | loggerAutoUpdater.error('Error during update check..', info) 85 | loggerAutoUpdater.debug('Error Code:', info.code) 86 | } 87 | } 88 | break 89 | default: 90 | loggerAutoUpdater.info('Unknown argument', arg) 91 | break 92 | } 93 | }) 94 | } 95 | 96 | /** 97 | * Send a notification to the main process changing the value of 98 | * allowPrerelease. If we are running a prerelease version, then 99 | * this will always be set to true, regardless of the current value 100 | * of val. 101 | * 102 | * @param {boolean} val The new allow prerelease value. 103 | */ 104 | function changeAllowPrerelease(val){ 105 | ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val) 106 | } 107 | 108 | function showUpdateUI(info){ 109 | //TODO Make this message a bit more informative `${info.version}` 110 | document.getElementById('image_seal_container').setAttribute('update', true) 111 | document.getElementById('image_seal_container').onclick = () => { 112 | /*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later') 113 | setOverlayHandler(() => { 114 | if(!isDev){ 115 | ipcRenderer.send('autoUpdateAction', 'installUpdateNow') 116 | } else { 117 | console.error('Cannot install updates in development environment.') 118 | toggleOverlay(false) 119 | } 120 | }) 121 | setDismissHandler(() => { 122 | toggleOverlay(false) 123 | }) 124 | toggleOverlay(true, true)*/ 125 | switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { 126 | settingsNavItemListener(document.getElementById('settingsNavUpdate'), false) 127 | }) 128 | } 129 | } 130 | 131 | /* jQuery Example 132 | $(function(){ 133 | loggerUICore.info('UICore Initialized'); 134 | })*/ 135 | 136 | document.addEventListener('readystatechange', function () { 137 | if (document.readyState === 'interactive'){ 138 | loggerUICore.info('UICore Initializing..') 139 | 140 | // Bind close button. 141 | Array.from(document.getElementsByClassName('fCb')).map((val) => { 142 | val.addEventListener('click', e => { 143 | const window = remote.getCurrentWindow() 144 | window.close() 145 | }) 146 | }) 147 | 148 | // Bind restore down button. 149 | Array.from(document.getElementsByClassName('fRb')).map((val) => { 150 | val.addEventListener('click', e => { 151 | const window = remote.getCurrentWindow() 152 | if(window.isMaximized()){ 153 | window.unmaximize() 154 | } else { 155 | window.maximize() 156 | } 157 | document.activeElement.blur() 158 | }) 159 | }) 160 | 161 | // Bind minimize button. 162 | Array.from(document.getElementsByClassName('fMb')).map((val) => { 163 | val.addEventListener('click', e => { 164 | const window = remote.getCurrentWindow() 165 | window.minimize() 166 | document.activeElement.blur() 167 | }) 168 | }) 169 | 170 | // Remove focus from social media buttons once they're clicked. 171 | Array.from(document.getElementsByClassName('mediaURL')).map(val => { 172 | val.addEventListener('click', e => { 173 | document.activeElement.blur() 174 | }) 175 | }) 176 | 177 | } else if(document.readyState === 'complete'){ 178 | 179 | //266.01 180 | //170.8 181 | //53.21 182 | // Bind progress bar length to length of bot wrapper 183 | //const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width 184 | //const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width 185 | //const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width 186 | 187 | document.getElementById('launch_details').style.maxWidth = 266.01 188 | document.getElementById('launch_progress').style.width = 170.8 189 | document.getElementById('launch_details_right').style.maxWidth = 170.8 190 | document.getElementById('launch_progress_label').style.width = 53.21 191 | 192 | } 193 | 194 | }, false) 195 | 196 | /** 197 | * Open web links in the user's default browser. 198 | */ 199 | $(document).on('click', 'a[href^="http"]', function(event) { 200 | event.preventDefault() 201 | shell.openExternal(this.href) 202 | }) 203 | 204 | /** 205 | * Opens DevTools window if you hold (ctrl + shift + i). 206 | * This will crash the program if you are using multiple 207 | * DevTools, for example the chrome debugger in VS Code. 208 | */ 209 | document.addEventListener('keydown', function (e) { 210 | if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){ 211 | let window = remote.getCurrentWindow() 212 | window.toggleDevTools() 213 | } 214 | }) -------------------------------------------------------------------------------- /app/assets/js/scripts/welcome.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script for welcome.ejs 3 | */ 4 | document.getElementById('welcomeButton').addEventListener('click', e => { 5 | loginOptionsCancelEnabled(false) // False by default, be explicit. 6 | loginOptionsViewOnLoginSuccess = VIEWS.landing 7 | loginOptionsViewOnLoginCancel = VIEWS.loginOptions 8 | switchView(VIEWS.welcome, VIEWS.loginOptions) 9 | }) -------------------------------------------------------------------------------- /app/assets/js/serverstatus.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | 3 | /** 4 | * Retrieves the status of a minecraft server. 5 | * 6 | * @param {string} address The server address. 7 | * @param {number} port Optional. The port of the server. Defaults to 25565. 8 | * @returns {Promise.} A promise which resolves to an object containing 9 | * status information. 10 | */ 11 | exports.getStatus = function(address, port = 25565){ 12 | 13 | if(port == null || port == ''){ 14 | port = 25565 15 | } 16 | if(typeof port === 'string'){ 17 | port = parseInt(port) 18 | } 19 | 20 | return new Promise((resolve, reject) => { 21 | const socket = net.connect(port, address, () => { 22 | let buff = Buffer.from([0xFE, 0x01]) 23 | socket.write(buff) 24 | }) 25 | 26 | socket.setTimeout(2500, () => { 27 | socket.end() 28 | reject({ 29 | code: 'ETIMEDOUT', 30 | errno: 'ETIMEDOUT', 31 | address, 32 | port 33 | }) 34 | }) 35 | 36 | socket.on('data', (data) => { 37 | if(data != null && data != ''){ 38 | let server_info = data.toString().split('\x00\x00\x00') 39 | const NUM_FIELDS = 6 40 | if(server_info != null && server_info.length >= NUM_FIELDS){ 41 | resolve({ 42 | online: true, 43 | version: server_info[2].replace(/\u0000/g, ''), 44 | motd: server_info[3].replace(/\u0000/g, ''), 45 | onlinePlayers: server_info[4].replace(/\u0000/g, ''), 46 | maxPlayers: server_info[5].replace(/\u0000/g,'') 47 | }) 48 | } else { 49 | resolve({ 50 | online: false 51 | }) 52 | } 53 | } 54 | socket.end() 55 | }) 56 | 57 | socket.on('error', (err) => { 58 | socket.destroy() 59 | reject(err) 60 | // ENOTFOUND = Unable to resolve. 61 | // ECONNREFUSED = Unable to connect to port. 62 | }) 63 | }) 64 | 65 | } -------------------------------------------------------------------------------- /app/assets/lang/_custom.toml: -------------------------------------------------------------------------------- 1 | # Custom Language File for Launcher Customizer 2 | 3 | [ejs.app] 4 | title = "Helios Launcher" 5 | 6 | [ejs.landing] 7 | mediaGitHubURL = "https://github.com/dscalzi/HeliosLauncher" 8 | mediaXURL = "#" 9 | mediaInstagramURL = "#" 10 | mediaYouTubeURL = "#" 11 | mediaDiscordURL = "https://discord.gg/zNWUXdt" 12 | 13 | [ejs.settings] 14 | sourceGithubLink = "https://github.com/dscalZi/HeliosLauncher" 15 | supportLink = "https://github.com/dscalZi/HeliosLauncher/issues" 16 | 17 | [ejs.welcome] 18 | welcomeHeader = "WELCOME TO WESTEROSCRAFT" 19 | welcomeDescription = "Our mission is to recreate the universe imagined by author George RR Martin in his fantasy series, A Song of Ice and Fire. Through the collaborative effort of thousands of community members, we have sought to create Westeros as accurately and precisely as possible within Minecraft. The world we are creating is yours to explore. Journey from Dorne to Castle Black, and if you aren’t afraid, beyond the Wall itself, but best not delay. As the words of House Stark ominously warn: Winter is Coming." 20 | welcomeDescCTA = "You are just a few clicks away from Westeros." 21 | -------------------------------------------------------------------------------- /app/assets/lang/en_US.toml: -------------------------------------------------------------------------------- 1 | [ejs.landing] 2 | updateAvailableTooltip = "Update Available" 3 | usernamePlaceholder = "Username" 4 | usernameEditButton = "Edit" 5 | settingsTooltip = "Settings" 6 | serverStatus = "SERVER" 7 | serverStatusPlaceholder = "OFFLINE" 8 | mojangStatus = "MOJANG STATUS" 9 | mojangStatusTooltipTitle = "Services" 10 | mojangStatusNETitle = "Non Essential" 11 | newsButton = "NEWS" 12 | launchButton = "PLAY" 13 | launchButtonPlaceholder = "• No Server Selected" 14 | launchDetails = "Please wait.." 15 | newsNavigationStatus = "{currentPage} of {totalPages}" 16 | newsErrorLoadSpan = "Checking for News.." 17 | newsErrorFailedSpan = "Failed to Load News" 18 | newsErrorRetryButton = "Try Again" 19 | newsErrorNoneSpan = "No News" 20 | 21 | [ejs.login] 22 | loginCancelText = "Cancel" 23 | loginSubheader = "MINECRAFT LOGIN" 24 | loginEmailError = "* Invalid Value" 25 | loginEmailPlaceholder = "EMAIL OR USERNAME" 26 | loginPasswordError = "* Required" 27 | loginPasswordPlaceholder = "PASSWORD" 28 | loginForgotPasswordLink = "https://minecraft.net/password/forgot/" 29 | loginForgotPasswordText = "forgot password?" 30 | loginRememberMeText = "remember me?" 31 | loginButtonText = "LOGIN" 32 | loginNeedAccountLink = "https://minecraft.net/store/minecraft-java-edition/" 33 | loginNeedAccountText = "Need an Account?" 34 | loginPasswordDisclaimer1 = "Your password is sent directly to mojang and never stored." 35 | loginPasswordDisclaimer2 = "{appName} is not affiliated with Mojang AB." 36 | 37 | [ejs.loginOptions] 38 | loginOptionsTitle = "Login Options" 39 | loginWithMicrosoft = "Login with Microsoft" 40 | loginWithMojang = "Login with Mojang" 41 | cancelButton = "Cancel" 42 | 43 | [ejs.overlay] 44 | serverSelectHeader = "Available Servers" 45 | serverSelectConfirm = "Select" 46 | serverSelectCancel = "Cancel" 47 | accountSelectHeader = "Select an Account" 48 | accountSelectConfirm = "Select" 49 | accountSelectCancel = "Cancel" 50 | 51 | [ejs.settings] 52 | navHeaderText = "Settings" 53 | navAccount = "Account" 54 | navMinecraft = "Minecraft" 55 | navMods = "Mods" 56 | navJava = "Java" 57 | navLauncher = "Launcher" 58 | navAbout = "About" 59 | navUpdates = "Updates" 60 | navDone = "Done" 61 | tabAccountHeaderText = "Account Settings" 62 | tabAccountHeaderDesc = "Add new accounts or manage existing ones." 63 | microsoftAccount = "Microsoft" 64 | addMicrosoftAccount = "+ Add Microsoft Account" 65 | mojangAccount = "Mojang" 66 | addMojangAccount = "+ Add Mojang Account" 67 | minecraftTabHeaderText = "Minecraft Settings" 68 | minecraftTabHeaderDesc = "Options related to game launch." 69 | gameResolutionTitle = "Game Resolution" 70 | launchFullscreenTitle = "Launch in fullscreen." 71 | autoConnectTitle = "Automatically connect to the server on launch." 72 | launchDetachedTitle = "Launch game process detached from launcher." 73 | launchDetachedDesc = "If the game is not detached, closing the launcher will also close the game." 74 | tabModsHeaderText = "Mod Settings" 75 | tabModsHeaderDesc = "Enable or disable mods." 76 | switchServerButton = "Switch" 77 | requiredMods = "Required Mods" 78 | optionalMods = "Optional Mods" 79 | dropinMods = "Drop-in Mods" 80 | addMods = "Add Mods" 81 | dropinRefreshNote = "(F5 to Refresh)" 82 | shaderpacks = "Shaderpacks" 83 | shaderpackDesc = "Enable or disable shaders. Please note, shaders will only run smoothly on powerful setups. You may add custom packs here." 84 | selectShaderpack = "Select Shaderpack" 85 | tabJavaHeaderText = "Java Settings" 86 | tabJavaHeaderDesc = "Manage the Java configuration (advanced)." 87 | memoryTitle = "Memory" 88 | maxRAM = "Maximum RAM" 89 | minRAM = "Minimum RAM" 90 | memoryDesc = "The recommended minimum RAM is 3 gigabytes. Setting the minimum and maximum values to the same value may reduce lag." 91 | memoryTotalTitle = "Total" 92 | memoryAvailableTitle = "Available" 93 | javaExecutableTitle = "Java Executable" 94 | javaExecSelDialogTitle = "Select Java Executable" 95 | javaExecSelButtonText = "Choose File" 96 | javaExecDesc = "The Java executable is validated before game launch." 97 | javaPathDesc = "The path should end with {pathSuffix}." 98 | jvmOptsTitle = "Additional JVM Options" 99 | jvmOptsDesc = "Options to be provided to the JVM at runtime. -Xms and -Xmx should not be included." 100 | launcherTabHeaderText = "Launcher Settings" 101 | launcherTabHeaderDesc = "Options related to the launcher itself." 102 | allowPrereleaseTitle = "Allow Pre-Release Updates." 103 | allowPrereleaseDesc = "Pre-Releases include new features which may have not been fully tested or integrated.
This will always be true if you are using a pre-release version." 104 | dataDirectoryTitle = "Data Directory" 105 | selectDataDirectory = "Select Data Directory" 106 | chooseFolder = "Choose Folder" 107 | dataDirectoryDesc = "All game files and local Java installations will be stored in the data directory.
Screenshots and world saves are stored in the instance folder for the corresponding server configuration." 108 | aboutTabHeaderText = "About" 109 | aboutTabHeaderDesc = "View information and release notes for the current version." 110 | aboutTitle = "{appName}" 111 | stableRelease = "Stable Release" 112 | versionText = "Version " 113 | sourceGithub = "Source (GitHub)" 114 | support = "Support" 115 | devToolsConsole = "DevTools Console" 116 | releaseNotes = "Release Notes" 117 | changelog = "Changelog" 118 | noReleaseNotes = "No Release Notes" 119 | viewReleaseNotes = "View Release Notes on GitHub" 120 | launcherUpdatesHeaderText = "Launcher Updates" 121 | launcherUpdatesHeaderDesc = "Download, install, and review updates for the launcher." 122 | checkForUpdates = "Check for Updates" 123 | whatsNew = "What's New" 124 | updateReleaseNotes = "Update Release Notes" 125 | 126 | [ejs.waiting] 127 | waitingText = "Waiting for Microsoft.." 128 | 129 | [ejs.welcome] 130 | continueButton = "CONTINUE" 131 | 132 | 133 | [js.discord] 134 | waiting = "Waiting for Client.." 135 | state = "Server: {shortId}" 136 | 137 | [js.index] 138 | microsoftLoginTitle = "Microsoft Login" 139 | microsoftLogoutTitle = "Microsoft Logout" 140 | 141 | [js.login] 142 | login = "LOGIN" 143 | loggingIn = "LOGGING IN" 144 | success = "SUCCESS" 145 | tryAgain = "Try Again" 146 | 147 | [js.login.error] 148 | invalidValue = "* Invalid Value" 149 | requiredValue = "* Required" 150 | 151 | [js.login.error.unknown] 152 | title = "Unknown Error During Login" 153 | desc = "An unknown error has occurred. Please see the console for details." 154 | 155 | [js.landing.launch] 156 | pleaseWait = "Please wait.." 157 | failureTitle = "Error During Launch" 158 | failureText = "See console (CTRL + Shift + i) for more details." 159 | okay = "Okay" 160 | 161 | [js.landing.selectedAccount] 162 | noAccountSelected = "No Account Selected" 163 | 164 | [js.landing.selectedServer] 165 | noSelection = "No Server Selected" 166 | loading = "Loading.." 167 | 168 | [js.landing.serverStatus] 169 | server = "SERVER" 170 | offline = "OFFLINE" 171 | players = "PLAYERS" 172 | 173 | [js.landing.systemScan] 174 | checking = "Checking system info.." 175 | noCompatibleJava = "No Compatible
Java Installation Found" 176 | installJavaMessage = "In order to launch Minecraft, you need a 64-bit installation of Java {major}. Would you like us to install a copy?" 177 | installJava = "Install Java" 178 | installJavaManually = "Install Manually" 179 | javaDownloadPrepare = "Preparing Java Download.." 180 | javaDownloadFailureTitle = "Error During Java Download" 181 | javaDownloadFailureText = "See console (CTRL + Shift + i) for more details." 182 | javaRequired = "Java is Required
to Launch" 183 | javaRequiredMessage = 'A valid x64 installation of Java {major} is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.' 184 | javaRequiredDismiss = "I Understand" 185 | javaRequiredCancel = "Go Back" 186 | 187 | [js.landing.downloadJava] 188 | findJdkFailure = "Failed to find OpenJDK distribution." 189 | javaDownloadCorruptedError = "Downloaded JDK has a bad hash, the file may be corrupted." 190 | extractingJava = "Extracting Java" 191 | javaInstalled = "Java Installed!" 192 | 193 | [js.landing.dlAsync] 194 | loadingServerInfo = "Loading server information.." 195 | fatalError = "Fatal Error" 196 | unableToLoadDistributionIndex = "Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details." 197 | pleaseWait = "Please wait.." 198 | errorDuringLaunchTitle = "Error During Launch" 199 | seeConsoleForDetails = "See console (CTRL + Shift + i) for more details." 200 | validatingFileIntegrity = "Validating file integrity.." 201 | errorDuringFileVerificationTitle = "Error During File Verification" 202 | downloadingFiles = "Downloading files.." 203 | errorDuringFileDownloadTitle = "Error During File Download" 204 | preparingToLaunch = "Preparing to launch.." 205 | launchingGame = "Launching game.." 206 | launchWrapperNotDownloaded = "The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We'll contact them and try to straighten things out." 207 | doneEnjoyServer = "Done. Enjoy the server!" 208 | checkConsoleForDetails = "Please check the console (CTRL + Shift + i) for more details." 209 | 210 | [js.landing.news] 211 | checking = "Checking for News" 212 | 213 | [js.landing.discord] 214 | loading = "Loading game.." 215 | joining = "Sailing to Westeros!" 216 | joined = "Exploring the Realm!" 217 | 218 | [js.overlay] 219 | dismiss = "Dismiss" 220 | 221 | [js.settings.fileSelectors] 222 | executables = "Executables" 223 | allFiles = "All Files" 224 | 225 | [js.settings.mstfLogin] 226 | errorTitle = "Something Went Wrong" 227 | errorMessage = "Microsoft authentication failed. Please try again." 228 | okButton = "OK" 229 | 230 | [js.settings.mstfLogout] 231 | errorTitle = "Something Went Wrong" 232 | errorMessage = "Microsoft logout failed. Please try again." 233 | okButton = "OK" 234 | 235 | [js.settings.authAccountSelect] 236 | selectButton = "Select Account" 237 | selectedButton = "Selected Account ✔" 238 | 239 | [js.settings.authAccountLogout] 240 | lastAccountWarningTitle = "Warning
This is Your Last Account" 241 | lastAccountWarningMessage = "In order to use the launcher you must be logged into at least one account. You will need to login again after.

Are you sure you want to log out?" 242 | confirmButton = "I'm Sure" 243 | cancelButton = "Cancel" 244 | 245 | [js.settings.authAccountPopulate] 246 | username = "Username" 247 | uuid = "UUID" 248 | selectAccount = "Select Account" 249 | selectedAccount = "Selected Account ✓" 250 | logout = "Log Out" 251 | 252 | [js.settings.dropinMods] 253 | removeButton = "Remove" 254 | deleteFailedTitle = "Failed to Delete
Drop-in Mod {fullName}" 255 | deleteFailedMessage = "Make sure the file is not in use and try again." 256 | failedToggleTitle = "Failed to Toggle
One or More Drop-in Mods" 257 | okButton = "Okay" 258 | 259 | [js.settings.serverListing] 260 | mainServer = "Main Server" 261 | 262 | [js.settings.java] 263 | selectedJava = "Selected: Java {version} ({vendor})" 264 | invalidSelection = "Invalid Selection" 265 | requiresJava = "Requires Java {major} x64." 266 | availableOptions = "Available Options for Java {major} (HotSpot VM)" 267 | 268 | [js.settings.about] 269 | preReleaseTitle = "Pre-release" 270 | stableReleaseTitle = "Stable Release" 271 | releaseNotesFailed = "Failed to load release notes." 272 | 273 | [js.settings.updates] 274 | newReleaseTitle = "New Release Available" 275 | newPreReleaseTitle = "New Pre-release Available" 276 | downloadingButton = "Downloading.." 277 | downloadButton = 'Download from GitHubClose the launcher and run the dmg to update.' 278 | latestVersionTitle = "You Are Running the Latest Version" 279 | checkForUpdatesButton = "Check for Updates" 280 | checkingForUpdatesButton = "Checking for Updates.." 281 | 282 | [js.settings.msftLogin] 283 | errorTitle = "Microsoft Login Failed" 284 | errorMessage = "We were unable to authenticate your Microsoft account. Please try again." 285 | okButton = "OK" 286 | 287 | [js.uibinder.startup] 288 | fatalErrorTitle = "Fatal Error: Unable to Load Distribution Index" 289 | fatalErrorMessage = "A connection could not be established to our servers to download the distribution index. No local copies were available to load.

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." 290 | closeButton = "Close" 291 | 292 | [js.uibinder.validateAccount] 293 | failedMessageTitle = "Failed to Refresh Login" 294 | failedMessage = "We were unable to refresh the login for {account}. Please select another account or login again." 295 | failedMessageSelectAnotherAccount = "We were unable to refresh the login for {account}. Please login again." 296 | loginButton = "Login" 297 | selectAnotherAccountButton = "Select Another Account" 298 | 299 | [js.uicore.autoUpdate] 300 | checkingForUpdateButton = "Checking for Updates..." 301 | installNowButton = "Install Now" 302 | checkForUpdatesButton = "Check for Updates" 303 | 304 | [js.auth.microsoft.error] 305 | noProfileTitle = "Error During Login:
Profile Not Set Up" 306 | noProfileDesc = "Your Microsoft account does not yet have a Minecraft profile set up. If you have recently purchased the game or redeemed it through Xbox Game Pass, you have to set up your profile on Minecraft.net.

If you have not yet purchased the game, you can also do that on Minecraft.net." 307 | noXboxAccountTitle = "Error During Login:
No Xbox Account" 308 | noXboxAccountDesc = "Your Microsoft account has no Xbox account associated with it." 309 | xblBannedTitle = "Error During Login:
Xbox Live Unavailable" 310 | xblBannedDesc = "Your Microsoft account is from a country where Xbox Live is not available or banned." 311 | under18Title = "Error During Login:
Parental Approval Required" 312 | under18Desc = "Accounts for users under the age of 18 must be added to a Family by an adult." 313 | unknownTitle = "Unknown Error During Login" 314 | unknownDesc = "An unknown error has occurred. Please see the console for details." 315 | 316 | [js.auth.mojang.error] 317 | methodNotAllowedTitle = "Internal Error:
Method Not Allowed" 318 | methodNotAllowedDesc = "Method not allowed. Please report this error." 319 | notFoundTitle = "Internal Error:
Not Found" 320 | notFoundDesc = "The authentication endpoint was not found. Please report this issue." 321 | accountMigratedTitle = "Error During Login:
Account Migrated" 322 | accountMigratedDesc = "You've attempted to login with a migrated account. Try again using the account email as the username." 323 | invalidCredentialsTitle = "Error During Login:
Invalid Credentials" 324 | invalidCredentialsDesc = "The email or password you've entered is incorrect. Please try again." 325 | tooManyAttemptsTitle = "Error During Login:
Too Many Attempts" 326 | tooManyAttemptsDesc = "There have been too many login attempts with this account recently. Please try again later." 327 | invalidTokenTitle = "Error During Login:
Invalid Token" 328 | invalidTokenDesc = "The provided access token is invalid." 329 | tokenHasProfileTitle = "Error During Login:
Token Has Profile" 330 | tokenHasProfileDesc = "Access token already has a profile assigned. Selecting profiles is not implemented yet." 331 | credentialsMissingTitle = "Error During Login:
Credentials Missing" 332 | credentialsMissingDesc = "Username/password was not submitted or password is less than 3 characters." 333 | invalidSaltVersionTitle = "Error During Login:
Invalid Salt Version" 334 | invalidSaltVersionDesc = "Invalid salt version." 335 | unsupportedMediaTypeTitle = "Internal Error:
Unsupported Media Type" 336 | unsupportedMediaTypeDesc = "Unsupported media type. Please report this error." 337 | accountGoneTitle = "Error During Login:
Account Migrated" 338 | accountGoneDesc = "Account has been migrated to a Microsoft account. Please log in with Microsoft." 339 | unreachableTitle = "Error During Login:
Unreachable" 340 | unreachableDesc = "Unable to reach the authentication servers. Ensure that they are online and you are connected to the internet." 341 | gameNotPurchasedTitle = "Error During Login:
Game Not Purchased" 342 | gameNotPurchasedDesc = "The account you are trying to login with has not purchased a copy of Minecraft. You may purchase a copy on Minecraft.net" 343 | unknownErrorTitle = "Unknown Error During Login" 344 | unknownErrorDesc = "An unknown error has occurred. Please see the console for details." 345 | -------------------------------------------------------------------------------- /app/frame.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%if (process.platform === 'darwin') { %> 6 |
7 |
8 | 9 | 10 | 11 |
12 |
13 | <% } else{ %> 14 |
15 |
16 | <%= lang('app.title') %> 17 |
18 |
19 | 22 | 25 | 28 |
29 |
30 | <% } %> 31 |
32 |
33 |
-------------------------------------------------------------------------------- /app/landing.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/login.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/loginOptions.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/overlay.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/waiting.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/welcome.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/build/icon.png -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | owner: dscalzi 2 | repo: HeliosLauncher 3 | provider: github 4 | -------------------------------------------------------------------------------- /docs/MicrosoftAuth.md: -------------------------------------------------------------------------------- 1 | # Microsoft Authentication 2 | 3 | Authenticating with Microsoft is fully supported by Helios Launcher. 4 | 5 | ## Acquiring an Entra Client ID 6 | 7 | 1. Navigate to https://portal.azure.com 8 | 2. In the search bar, search for **Microsoft Entra ID**. 9 | 3. In Microsoft Entra ID, go to **App Registrations** on the left pane (Under *Manage*). 10 | 4. Click **New Registration**. 11 | - Set **Name** to be your launcher's name. 12 | - Set **Supported account types** to *Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)* 13 | - Leave **Redirect URI** blank. 14 | - Register the application. 15 | 5. You should be on the application's management page. If not, Navigate back to **App Registrations**. Select the application you just registered. 16 | 6. Click **Authentication** on the left pane (Under *Manage*). 17 | 7. Click **Add Platform**. 18 | - Select **Mobile and desktop applications**. 19 | - Choose `https://login.microsoftonline.com/common/oauth2/nativeclient` as the **Redirect URI**. 20 | - Select **Configure** to finish adding the platform. 21 | 8. Go to **Certificates & secrets**. 22 | - Select **Client secrets**. 23 | - Click **New client secret**. 24 | - Set a description. 25 | - Click **Add**. 26 | - Don't copy the client secret, adding one is just a requirement from Microsoft. 27 | 8. Navigate back to **Overview**. 28 | 9. Copy **Application (client) ID**. 29 | 30 | 31 | ## Adding the Entra Client ID to Helios Launcher. 32 | 33 | In `app/assets/js/ipcconstants.js` you'll find **`AZURE_CLIENT_ID`**. Set it to your application's id. 34 | 35 | Note: Entra 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 36 | 37 | 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.__ 38 | 39 | ## Requesting whitelisting from Microsoft 40 | 41 | 1. Ensure you have completed every step of this doc page. 42 | 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. 43 | 3. Give Microsoft some time to review your app. 44 | 4. Once you have received Microsoft's approval, allow up to 24 hours for the changes to apply. 45 | 46 | ---- 47 | 48 | You can now authenticate with Microsoft through the launcher. 49 | 50 | References: 51 | - https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app 52 | - https://help.minecraft.net/hc/en-us/articles/16254801392141 53 | -------------------------------------------------------------------------------- /docs/distro.md: -------------------------------------------------------------------------------- 1 | # Distribution Index 2 | 3 | You can use [Nebula](https://github.com/dscalzi/Nebula) to automate the generation of a distribution index. 4 | 5 | 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). 6 | 7 | The distribution index is written in JSON. The general format of the index is as posted below. 8 | 9 | ```json 10 | { 11 | "version": "1.0.0", 12 | "discord": { 13 | "clientId": "12334567890123456789", 14 | "smallImageText": "WesterosCraft", 15 | "smallImageKey": "seal-circle" 16 | }, 17 | "rss": "https://westeroscraft.com/articles/index.rss", 18 | "servers": [ 19 | { 20 | "id": "Example_Server", 21 | "name": "WesterosCraft Example Client", 22 | "description": "Example WesterosCraft server. Connect for fun!", 23 | "icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png", 24 | "version": "0.0.1", 25 | "address": "mc.westeroscraft.com:1337", 26 | "minecraftVersion": "1.11.2", 27 | "discord": { 28 | "shortId": "Example", 29 | "largeImageText": "WesterosCraft Example Server", 30 | "largeImageKey": "server-example" 31 | }, 32 | "mainServer": true, 33 | "autoconnect": true, 34 | "modules": [ 35 | "Module Objects Here" 36 | ] 37 | } 38 | ] 39 | } 40 | ``` 41 | 42 | ## Distro Index Object 43 | 44 | #### Example 45 | ```JSON 46 | { 47 | "version": "1.0.0", 48 | "discord": { 49 | "clientId": "12334567890123456789", 50 | "smallImageText": "WesterosCraft", 51 | "smallImageKey": "seal-circle" 52 | }, 53 | "rss": "https://westeroscraft.com/articles/index.rss", 54 | "servers": [] 55 | } 56 | ``` 57 | 58 | ### `DistroIndex.version: string/semver` 59 | 60 | The version of the index format. Will be used in the future to gracefully push updates. 61 | 62 | ### `DistroIndex.discord: object` 63 | 64 | Global settings for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to). 65 | 66 | **Properties** 67 | 68 | * `discord.clientId: string` - Client ID for th Application registered with Discord. 69 | * `discord.smallImageText: string` - Tootltip for the `smallImageKey`. 70 | * `discord.smallImageKey: string` - Name of the uploaded image for the small profile artwork. 71 | 72 | 73 | ### `DistroIndex.rss: string/url` 74 | 75 | A URL to a RSS feed. Used for loading news. 76 | 77 | --- 78 | 79 | ## Server Object 80 | 81 | #### Example 82 | ```JSON 83 | { 84 | "id": "Example_Server", 85 | "name": "WesterosCraft Example Client", 86 | "description": "Example WesterosCraft server. Connect for fun!", 87 | "icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png", 88 | "version": "0.0.1", 89 | "address": "mc.westeroscraft.com:1337", 90 | "minecraftVersion": "1.11.2", 91 | "discord": { 92 | "shortId": "Example", 93 | "largeImageText": "WesterosCraft Example Server", 94 | "largeImageKey": "server-example" 95 | }, 96 | "mainServer": true, 97 | "autoconnect": true, 98 | "modules": [] 99 | } 100 | ``` 101 | 102 | ### `Server.id: string` 103 | 104 | The ID of the server. The launcher saves mod configurations and selected servers by ID. If the ID changes, all data related to the old ID **will be wiped**. 105 | 106 | ### `Server.name: string` 107 | 108 | The name of the server. This is what users see on the UI. 109 | 110 | ### `Server.description: string` 111 | 112 | A brief description of the server. Displayed on the UI to provide users more information. 113 | 114 | ### `Server.icon: string/url` 115 | 116 | A URL to the server's icon. Will be displayed on the UI. 117 | 118 | ### `Server.version: string/semver` 119 | 120 | The version of the server configuration. 121 | 122 | ### `Server.address: string/url` 123 | 124 | The server's IP address. 125 | 126 | ### `Server.minecraftVersion: string` 127 | 128 | The version of minecraft that the server is running. 129 | 130 | ### `Server.discord: object` 131 | 132 | Server specific settings used for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to). 133 | 134 | **Properties** 135 | 136 | * `discord.shortId: string` - Short ID for the server. Displayed on the second status line as `Server: shortId` 137 | * `discord.largeImageText: string` - Ttooltip for the `largeImageKey`. 138 | * `discord.largeImageKey: string` - Name of the uploaded image for the large profile artwork. 139 | 140 | ### `Server.mainServer: boolean` 141 | 142 | Only one server in the array should have the `mainServer` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. If this field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `mainServer` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false. 143 | 144 | ### `Server.autoconnect: boolean` 145 | 146 | 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. 147 | 148 | ### `Server.javaOptions: JavaOptions` 149 | 150 | **OPTIONAL** 151 | 152 | Sever-specific Java options. If not provided, defaults are used by the client. 153 | 154 | ### `Server.modules: Module[]` 155 | 156 | An array of module objects. 157 | 158 | --- 159 | 160 | ## JavaOptions Object 161 | 162 | Server-specific Java options. 163 | 164 | #### Example 165 | ```JSON 166 | { 167 | "supported": ">=17", 168 | "suggestedMajor": 17, 169 | "platformOptions": [ 170 | { 171 | "platform": "darwin", 172 | "architecture": "arm64", 173 | "distribution": "CORRETTO" 174 | } 175 | ], 176 | "ram": { 177 | "recommended": 3072, 178 | "minimum": 2048 179 | } 180 | } 181 | ``` 182 | 183 | ### `JavaOptions.platformOptions: JavaPlatformOptions[]` 184 | 185 | **OPTIONAL** 186 | 187 | 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. 188 | 189 | Maxtrix Precedence (Highest - Lowest) 190 | - Current platform, current architecture (ex. win32 x64). 191 | - Current platform, any architecture (ex. win32). 192 | - Java Options base properties. 193 | - Client logic (default logic in the client). 194 | 195 | Properties: 196 | 197 | - `platformOptions.platform: string` - The platform that this validation matrix applies to. 198 | - `platformOptions.architecture: string` - Optional. The architecture that this validation matrix applies to. If omitted, applies to all architectures. 199 | - `platformOptions.distribution: string` - Optional. See `JavaOptions.distribution`. 200 | - `platformOptions.supported: string` - Optional. See `JavaOptions.supported`. 201 | - `platformOptions.suggestedMajor: number` - Optional. See `JavaOptions.suggestedMajor`. 202 | 203 | ### `JavaOptions.ram: object` 204 | 205 | **OPTIONAL** 206 | 207 | 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). 208 | 209 | - `ram.recommended: number` - The recommended amount of RAM in megabytes. Must be an interval of 512. 210 | - `ram.minimum: number` - The absolute minimum amount of RAM in megabytes. Must be an interval of 512. 211 | 212 | ### `JavaOptions.distribution: string` 213 | 214 | **OPTIONAL** 215 | 216 | Preferred JDK distribution to download if no applicable installation could be found. If omitted, the client will decide (decision may be platform-specific). 217 | 218 | ### `JavaOptions.supported: string` 219 | 220 | **OPTIONAL** 221 | 222 | A semver range of supported JDK versions. 223 | 224 | Java version syntax is platform dependent. 225 | 226 | JDK 8 and prior 227 | ``` 228 | 1.{major}.{minor}_{patch}-b{build} 229 | Ex. 1.8.0_152-b16 230 | ``` 231 | 232 | JDK 9+ 233 | ``` 234 | {major}.{minor}.{patch}+{build} 235 | Ex. 11.0.12+7 236 | ``` 237 | 238 | 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}`. 239 | 240 | If specified, you must also specify suggestedMajor. 241 | 242 | If omitted, the client will decide based on the game version. 243 | 244 | ### `JavaOptions.suggestedMajor: number` 245 | 246 | **OPTIONAL** 247 | 248 | 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. 249 | 250 | NOTE If supported is specified, suggestedMajor must be set. The launcher's default value may not comply with your custom major supported range. 251 | 252 | Common use case: 253 | - supported: '>=17.x' 254 | - suggestedMajor: 17 255 | 256 | More involved: 257 | - supported: '>=16 <20' 258 | - suggestedMajor: 17 259 | 260 | Given a wider support range, it becomes necessary to specify which major version in the range is the suggested. 261 | 262 | --- 263 | 264 | ## Module Object 265 | 266 | A module is a generic representation of a file required to run the minecraft client. 267 | 268 | #### Example 269 | ```JSON 270 | { 271 | "id": "com.example:artifact:1.0.0@jar.pack.xz", 272 | "name": "Artifact 1.0.0", 273 | "type": "Library", 274 | "artifact": { 275 | "size": 4231234, 276 | "MD5": "7f30eefe5c51e1ae0939dab2051db75f", 277 | "url": "http://files.site.com/maven/com/example/artifact/1.0.0/artifact-1.0.0.jar.pack.xz" 278 | }, 279 | "subModules": [ 280 | { 281 | "id": "examplefile", 282 | "name": "Example File", 283 | "type": "File", 284 | "artifact": { 285 | "size": 23423, 286 | "MD5": "169a5e6cf30c2cc8649755cdc5d7bad7", 287 | "path": "examplefile.txt", 288 | "url": "http://files.site.com/examplefile.txt" 289 | } 290 | } 291 | ] 292 | } 293 | ``` 294 | 295 | The parent module will be stored maven style, it's destination path will be resolved by its id. The sub module has a declared `path`, so that value will be used. 296 | 297 | ### `Module.id: string` 298 | 299 | The ID of the module. All modules that are not of type `File` **MUST** use a maven identifier. Version information and other metadata is pulled from the identifier. Modules which are stored maven style use the identifier to resolve the destination path. If the `extension` is not provided, it defaults to `jar`. 300 | 301 | **Template** 302 | 303 | `my.group:arifact:version@extension` 304 | 305 | `my/group/artifact/version/artifact-version.extension` 306 | 307 | **Example** 308 | 309 | `net.minecraft:launchwrapper:1.12` OR `net.minecraft:launchwrapper:1.12@jar` 310 | 311 | `net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar` 312 | 313 | If the module's artifact does not declare the `path` property, its path will be resolved from the ID. 314 | 315 | ### `Module.name: string` 316 | 317 | The name of the module. Used on the UI. 318 | 319 | ### `Module.type: string` 320 | 321 | The type of the module. 322 | 323 | ### `Module.classpath: boolean` 324 | 325 | **OPTIONAL** 326 | 327 | If the module is of type `Library`, whether the library should be added to the classpath. Defaults to true. 328 | 329 | ### `Module.required: Required` 330 | 331 | **OPTIONAL** 332 | 333 | Defines whether or not the module is required. If omitted, then the module will be required. 334 | 335 | Only applicable for modules of type: 336 | * `ForgeMod` 337 | * `LiteMod` 338 | * `LiteLoader` 339 | 340 | 341 | ### `Module.artifact: Artifact` 342 | 343 | The download artifact for the module. 344 | 345 | ### `Module.subModules: Module[]` 346 | 347 | **OPTIONAL** 348 | 349 | An array of sub modules declared by this module. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit. 350 | 351 | 352 | ## Artifact Object 353 | 354 | The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file. 355 | 356 | Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The module `id` will be used to resolve the final path, effectively replacing the `path` property. It must be provided in maven format. More information on this is provided in the documentation for the `id` property. 357 | 358 | The resolved/provided paths are appended to a base path depending on the module's declared type. 359 | 360 | | Type | Path | 361 | | ---- | ---- | 362 | | `ForgeHosted` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | 363 | | `Fabric` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | 364 | | `LiteLoader` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | 365 | | `Library` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | 366 | | `ForgeMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) | 367 | | `LiteMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) | 368 | | `FabricMod` | ({`commonDirectory`}/mods/fabric/{`path` OR resolved}) | 369 | | `File` | ({`instanceDirectory`}/{`Server.id`}/{`path` OR resolved}) | 370 | 371 | The `commonDirectory` and `instanceDirectory` values are stored in the launcher's config.json. 372 | 373 | ### `Artifact.size: number` 374 | 375 | The size of the artifact. 376 | 377 | ### `Artifact.MD5: string` 378 | 379 | The MD5 hash of the artifact. This will be used to validate local artifacts. 380 | 381 | ### `Artifact.path: string` 382 | 383 | **OPTIONAL** 384 | 385 | A relative path to where the file will be saved. This is appended to the base path for the module's declared type. 386 | 387 | If this is not specified, the path will be resolved based on the module's ID. 388 | 389 | ### `Artifact.url: string/url` 390 | 391 | The artifact's download url. 392 | 393 | ## Required Object 394 | 395 | ### `Required.value: boolean` 396 | 397 | **OPTIONAL** 398 | 399 | If the module is required. Defaults to true if this property is omited. 400 | 401 | ### `Required.def: boolean` 402 | 403 | **OPTIONAL** 404 | 405 | If the module is enabled by default. Has no effect unless `Required.value` is false. Defaults to true if this property is omited. 406 | 407 | --- 408 | 409 | ## Module Types 410 | 411 | ### ForgeHosted 412 | 413 | 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. 414 | 415 | Ex. 416 | 417 | ```json 418 | { 419 | "id": "net.minecraftforge:forge:1.11.2-13.20.1.2429", 420 | "name": "Minecraft Forge 1.11.2-13.20.1.2429", 421 | "type": "ForgeHosted", 422 | "artifact": { 423 | "size": 4450992, 424 | "MD5": "3fcc9b0104f0261397d3cc897e55a1c5", 425 | "url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar" 426 | }, 427 | "subModules": [ 428 | { 429 | "id": "net.minecraft:launchwrapper:1.12", 430 | "name": "Mojang (LaunchWrapper)", 431 | "type": "Library", 432 | "artifact": { 433 | "size": 32999, 434 | "MD5": "934b2d91c7c5be4a49577c9e6b40e8da", 435 | "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar" 436 | } 437 | } 438 | ] 439 | } 440 | ``` 441 | 442 | All of forge's required libraries are declared in the `version.json` file found in the root of the forge jar file. These libraries MUST be hosted and declared a submodules or forge will not work. 443 | 444 | There were plans to add a `Forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented. 445 | 446 | --- 447 | 448 | ### Fabric 449 | 450 | 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. 451 | 452 | Ex. 453 | 454 | ```json 455 | { 456 | "id": "net.fabricmc:fabric-loader:0.15.0", 457 | "name": "Fabric (fabric-loader)", 458 | "type": "Fabric", 459 | "artifact": { 460 | "size": 1196222, 461 | "MD5": "a43d5a142246801343b6cedef1c102c4", 462 | "url": "http://localhost:8080/repo/lib/net/fabricmc/fabric-loader/0.15.0/fabric-loader-0.15.0.jar" 463 | }, 464 | "subModules": [ 465 | { 466 | "id": "1.20.1-fabric-0.15.0", 467 | "name": "Fabric (version.json)", 468 | "type": "VersionManifest", 469 | "artifact": { 470 | "size": 2847, 471 | "MD5": "69a2bd43452325ba1bc882fa0904e054", 472 | "url": "http://localhost:8080/repo/versions/1.20.1-fabric-0.15.0/1.20.1-fabric-0.15.0.json" 473 | } 474 | } 475 | } 476 | ``` 477 | 478 | Fabric works similarly to Forge 1.13+. 479 | 480 | --- 481 | 482 | ### LiteLoader 483 | 484 | 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. 485 | 486 | Ex. 487 | ```json 488 | { 489 | "id": "com.mumfrey:liteloader:1.11.2", 490 | "name": "Liteloader (1.11.2)", 491 | "type": "LiteLoader", 492 | "required": { 493 | "value": false, 494 | "def": false 495 | }, 496 | "artifact": { 497 | "size": 1685422, 498 | "MD5": "3a98b5ed95810bf164e71c1a53be568d", 499 | "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/liteloader-1.11.2.jar" 500 | }, 501 | "subModules": [ 502 | "All LiteMods go here" 503 | ] 504 | } 505 | ``` 506 | 507 | --- 508 | 509 | ### Library 510 | 511 | The module type `Library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process. 512 | 513 | Ex. 514 | 515 | ```json 516 | { 517 | "id": "net.sf.jopt-simple:jopt-simple:4.6", 518 | "name": "Jopt-simple 4.6", 519 | "type": "Library", 520 | "artifact": { 521 | "size": 62477, 522 | "MD5": "13560a58a79b46b82057686543e8d727", 523 | "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar" 524 | } 525 | } 526 | ``` 527 | 528 | --- 529 | 530 | ### ForgeMod 531 | 532 | The module type `ForgeMod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). 533 | 534 | Ex. 535 | ```json 536 | { 537 | "id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133", 538 | "name": "WesterosBlocks (3.0.0-beta-6-133)", 539 | "type": "ForgeMod", 540 | "artifact": { 541 | "size": 16321712, 542 | "MD5": "5a89e2ab18916c18965fc93a0766cc6e", 543 | "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar" 544 | } 545 | } 546 | ``` 547 | 548 | --- 549 | 550 | ### LiteMod 551 | 552 | The module type `LiteMod` represents a mod loaded by liteloader. These files are stored maven-style and passed to liteloader using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). Documentation for liteloader's implementation of this can be found on [this issue](http://develop.liteloader.com/liteloader/LiteLoader/issues/34). 553 | 554 | Ex. 555 | ```json 556 | { 557 | "id": "com.mumfrey:macrokeybindmod:0.14.4-1.11.2@litemod", 558 | "name": "Macro/Keybind Mod (0.14.4-1.11.2)", 559 | "type": "LiteMod", 560 | "required": { 561 | "value": false, 562 | "def": false 563 | }, 564 | "artifact": { 565 | "size": 1670811, 566 | "MD5": "16080785577b391d426c62c8d3138558", 567 | "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/macrokeybindmod.litemod" 568 | } 569 | } 570 | ``` 571 | 572 | --- 573 | 574 | ### File 575 | 576 | The module type `file` represents a generic file required by the client, another module, etc. These files are stored in the server's instance directory. 577 | 578 | Ex. 579 | 580 | ```json 581 | { 582 | "id": "com.westeroscraft:westeroscraftrp:2017-08-16", 583 | "name": "WesterosCraft Resource Pack (2017-08-16)", 584 | "type": "file", 585 | "artifact": { 586 | "size": 45241339, 587 | "MD5": "ec2d9fdb14d5c2eafe5975a240202f1a", 588 | "path": "resourcepacks/WesterosCraft.zip", 589 | "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/resourcepacks/WesterosCraft.zip" 590 | } 591 | } 592 | ``` 593 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: 'helioslauncher' 2 | productName: 'Helios Launcher' 3 | artifactName: '${productName}-setup-${version}.${ext}' 4 | 5 | copyright: 'Copyright © 2018-2024 Daniel Scalzi' 6 | 7 | asar: true 8 | compression: 'maximum' 9 | 10 | files: 11 | - '!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.nvmrc,.eslintrc.json}' 12 | 13 | extraResources: 14 | - 'libraries' 15 | 16 | # Windows Configuration 17 | win: 18 | target: 19 | - target: 'nsis' 20 | arch: 'x64' 21 | 22 | # Windows Installer Configuration 23 | nsis: 24 | oneClick: false 25 | perMachine: false 26 | allowElevation: true 27 | allowToChangeInstallationDirectory: true 28 | 29 | # macOS Configuration 30 | mac: 31 | target: 32 | - target: 'dmg' 33 | arch: 34 | - 'x64' 35 | - 'arm64' 36 | artifactName: '${productName}-setup-${version}-${arch}.${ext}' 37 | category: 'public.app-category.games' 38 | 39 | # Linux Configuration 40 | linux: 41 | target: 'AppImage' 42 | maintainer: 'Daniel Scalzi' 43 | vendor: 'Daniel Scalzi' 44 | synopsis: 'Modded Minecraft Launcher' 45 | description: 'Custom launcher which allows users to join modded servers. All mods, configurations, and updates are handled automatically.' 46 | category: 'Game' 47 | 48 | 49 | directories: 50 | buildResources: 'build' 51 | output: 'dist' -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const remoteMain = require('@electron/remote/main') 2 | remoteMain.initialize() 3 | 4 | // Requirements 5 | const { app, BrowserWindow, ipcMain, Menu, shell } = require('electron') 6 | const autoUpdater = require('electron-updater').autoUpdater 7 | const ejse = require('ejs-electron') 8 | const fs = require('fs') 9 | const isDev = require('./app/assets/js/isdev') 10 | const path = require('path') 11 | const semver = require('semver') 12 | const { pathToFileURL } = require('url') 13 | const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR, SHELL_OPCODE } = require('./app/assets/js/ipcconstants') 14 | const LangLoader = require('./app/assets/js/langloader') 15 | 16 | // Setup Lang 17 | LangLoader.setupLanguage() 18 | 19 | // Setup auto updater. 20 | function initAutoUpdater(event, data) { 21 | 22 | if(data){ 23 | autoUpdater.allowPrerelease = true 24 | } else { 25 | // Defaults to true if application version contains prerelease components (e.g. 0.12.1-alpha.1) 26 | // autoUpdater.allowPrerelease = true 27 | } 28 | 29 | if(isDev){ 30 | autoUpdater.autoInstallOnAppQuit = false 31 | autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml') 32 | } 33 | if(process.platform === 'darwin'){ 34 | autoUpdater.autoDownload = false 35 | } 36 | autoUpdater.on('update-available', (info) => { 37 | event.sender.send('autoUpdateNotification', 'update-available', info) 38 | }) 39 | autoUpdater.on('update-downloaded', (info) => { 40 | event.sender.send('autoUpdateNotification', 'update-downloaded', info) 41 | }) 42 | autoUpdater.on('update-not-available', (info) => { 43 | event.sender.send('autoUpdateNotification', 'update-not-available', info) 44 | }) 45 | autoUpdater.on('checking-for-update', () => { 46 | event.sender.send('autoUpdateNotification', 'checking-for-update') 47 | }) 48 | autoUpdater.on('error', (err) => { 49 | event.sender.send('autoUpdateNotification', 'realerror', err) 50 | }) 51 | } 52 | 53 | // Open channel to listen for update actions. 54 | ipcMain.on('autoUpdateAction', (event, arg, data) => { 55 | switch(arg){ 56 | case 'initAutoUpdater': 57 | console.log('Initializing auto updater.') 58 | initAutoUpdater(event, data) 59 | event.sender.send('autoUpdateNotification', 'ready') 60 | break 61 | case 'checkForUpdate': 62 | autoUpdater.checkForUpdates() 63 | .catch(err => { 64 | event.sender.send('autoUpdateNotification', 'realerror', err) 65 | }) 66 | break 67 | case 'allowPrereleaseChange': 68 | if(!data){ 69 | const preRelComp = semver.prerelease(app.getVersion()) 70 | if(preRelComp != null && preRelComp.length > 0){ 71 | autoUpdater.allowPrerelease = true 72 | } else { 73 | autoUpdater.allowPrerelease = data 74 | } 75 | } else { 76 | autoUpdater.allowPrerelease = data 77 | } 78 | break 79 | case 'installUpdateNow': 80 | autoUpdater.quitAndInstall() 81 | break 82 | default: 83 | console.log('Unknown argument', arg) 84 | break 85 | } 86 | }) 87 | // Redirect distribution index event from preloader to renderer. 88 | ipcMain.on('distributionIndexDone', (event, res) => { 89 | event.sender.send('distributionIndexDone', res) 90 | }) 91 | 92 | // Handle trash item. 93 | ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => { 94 | try { 95 | await shell.trashItem(args[0]) 96 | return { 97 | result: true 98 | } 99 | } catch(error) { 100 | return { 101 | result: false, 102 | error: error 103 | } 104 | } 105 | }) 106 | 107 | // Disable hardware acceleration. 108 | // https://electronjs.org/docs/tutorial/offscreen-rendering 109 | app.disableHardwareAcceleration() 110 | 111 | 112 | const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?' 113 | 114 | // Microsoft Auth Login 115 | let msftAuthWindow 116 | let msftAuthSuccess 117 | let msftAuthViewSuccess 118 | let msftAuthViewOnClose 119 | ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => { 120 | if (msftAuthWindow) { 121 | ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose) 122 | return 123 | } 124 | msftAuthSuccess = false 125 | msftAuthViewSuccess = arguments_[0] 126 | msftAuthViewOnClose = arguments_[1] 127 | msftAuthWindow = new BrowserWindow({ 128 | title: LangLoader.queryJS('index.microsoftLoginTitle'), 129 | backgroundColor: '#222222', 130 | width: 520, 131 | height: 600, 132 | frame: true, 133 | icon: getPlatformIcon('SealCircle') 134 | }) 135 | 136 | msftAuthWindow.on('closed', () => { 137 | msftAuthWindow = undefined 138 | }) 139 | 140 | msftAuthWindow.on('close', () => { 141 | if(!msftAuthSuccess) { 142 | ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose) 143 | } 144 | }) 145 | 146 | msftAuthWindow.webContents.on('did-navigate', (_, uri) => { 147 | if (uri.startsWith(REDIRECT_URI_PREFIX)) { 148 | let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&') 149 | let queryMap = {} 150 | 151 | queries.forEach(query => { 152 | const [name, value] = query.split('=') 153 | queryMap[name] = decodeURI(value) 154 | }) 155 | 156 | ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess) 157 | 158 | msftAuthSuccess = true 159 | msftAuthWindow.close() 160 | msftAuthWindow = null 161 | } 162 | }) 163 | 164 | msftAuthWindow.removeMenu() 165 | 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`) 166 | }) 167 | 168 | // Microsoft Auth Logout 169 | let msftLogoutWindow 170 | let msftLogoutSuccess 171 | let msftLogoutSuccessSent 172 | ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => { 173 | if (msftLogoutWindow) { 174 | ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN) 175 | return 176 | } 177 | 178 | msftLogoutSuccess = false 179 | msftLogoutSuccessSent = false 180 | msftLogoutWindow = new BrowserWindow({ 181 | title: LangLoader.queryJS('index.microsoftLogoutTitle'), 182 | backgroundColor: '#222222', 183 | width: 520, 184 | height: 600, 185 | frame: true, 186 | icon: getPlatformIcon('SealCircle') 187 | }) 188 | 189 | msftLogoutWindow.on('closed', () => { 190 | msftLogoutWindow = undefined 191 | }) 192 | 193 | msftLogoutWindow.on('close', () => { 194 | if(!msftLogoutSuccess) { 195 | ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED) 196 | } else if(!msftLogoutSuccessSent) { 197 | msftLogoutSuccessSent = true 198 | ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) 199 | } 200 | }) 201 | 202 | msftLogoutWindow.webContents.on('did-navigate', (_, uri) => { 203 | if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) { 204 | msftLogoutSuccess = true 205 | setTimeout(() => { 206 | if(!msftLogoutSuccessSent) { 207 | msftLogoutSuccessSent = true 208 | ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) 209 | } 210 | 211 | if(msftLogoutWindow) { 212 | msftLogoutWindow.close() 213 | msftLogoutWindow = null 214 | } 215 | }, 5000) 216 | } 217 | }) 218 | 219 | msftLogoutWindow.removeMenu() 220 | msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout') 221 | }) 222 | 223 | // Keep a global reference of the window object, if you don't, the window will 224 | // be closed automatically when the JavaScript object is garbage collected. 225 | let win 226 | 227 | function createWindow() { 228 | 229 | win = new BrowserWindow({ 230 | width: 980, 231 | height: 552, 232 | icon: getPlatformIcon('SealCircle'), 233 | frame: false, 234 | webPreferences: { 235 | preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'), 236 | nodeIntegration: true, 237 | contextIsolation: false 238 | }, 239 | backgroundColor: '#171614' 240 | }) 241 | remoteMain.enable(win.webContents) 242 | 243 | const data = { 244 | bkid: Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)), 245 | lang: (str, placeHolders) => LangLoader.queryEJS(str, placeHolders) 246 | } 247 | Object.entries(data).forEach(([key, val]) => ejse.data(key, val)) 248 | 249 | win.loadURL(pathToFileURL(path.join(__dirname, 'app', 'app.ejs')).toString()) 250 | 251 | /*win.once('ready-to-show', () => { 252 | win.show() 253 | })*/ 254 | 255 | win.removeMenu() 256 | 257 | win.resizable = true 258 | 259 | win.on('closed', () => { 260 | win = null 261 | }) 262 | } 263 | 264 | function createMenu() { 265 | 266 | if(process.platform === 'darwin') { 267 | 268 | // Extend default included application menu to continue support for quit keyboard shortcut 269 | let applicationSubMenu = { 270 | label: 'Application', 271 | submenu: [{ 272 | label: 'About Application', 273 | selector: 'orderFrontStandardAboutPanel:' 274 | }, { 275 | type: 'separator' 276 | }, { 277 | label: 'Quit', 278 | accelerator: 'Command+Q', 279 | click: () => { 280 | app.quit() 281 | } 282 | }] 283 | } 284 | 285 | // New edit menu adds support for text-editing keyboard shortcuts 286 | let editSubMenu = { 287 | label: 'Edit', 288 | submenu: [{ 289 | label: 'Undo', 290 | accelerator: 'CmdOrCtrl+Z', 291 | selector: 'undo:' 292 | }, { 293 | label: 'Redo', 294 | accelerator: 'Shift+CmdOrCtrl+Z', 295 | selector: 'redo:' 296 | }, { 297 | type: 'separator' 298 | }, { 299 | label: 'Cut', 300 | accelerator: 'CmdOrCtrl+X', 301 | selector: 'cut:' 302 | }, { 303 | label: 'Copy', 304 | accelerator: 'CmdOrCtrl+C', 305 | selector: 'copy:' 306 | }, { 307 | label: 'Paste', 308 | accelerator: 'CmdOrCtrl+V', 309 | selector: 'paste:' 310 | }, { 311 | label: 'Select All', 312 | accelerator: 'CmdOrCtrl+A', 313 | selector: 'selectAll:' 314 | }] 315 | } 316 | 317 | // Bundle submenus into a single template and build a menu object with it 318 | let menuTemplate = [applicationSubMenu, editSubMenu] 319 | let menuObject = Menu.buildFromTemplate(menuTemplate) 320 | 321 | // Assign it to the application 322 | Menu.setApplicationMenu(menuObject) 323 | 324 | } 325 | 326 | } 327 | 328 | function getPlatformIcon(filename){ 329 | let ext 330 | switch(process.platform) { 331 | case 'win32': 332 | ext = 'ico' 333 | break 334 | case 'darwin': 335 | case 'linux': 336 | default: 337 | ext = 'png' 338 | break 339 | } 340 | 341 | return path.join(__dirname, 'app', 'assets', 'images', `${filename}.${ext}`) 342 | } 343 | 344 | app.on('ready', createWindow) 345 | app.on('ready', createMenu) 346 | 347 | app.on('window-all-closed', () => { 348 | // On macOS it is common for applications and their menu bar 349 | // to stay active until the user quits explicitly with Cmd + Q 350 | if (process.platform !== 'darwin') { 351 | app.quit() 352 | } 353 | }) 354 | 355 | app.on('activate', () => { 356 | // On macOS it's common to re-create a window in the app when the 357 | // dock icon is clicked and there are no other windows open. 358 | if (win === null) { 359 | createWindow() 360 | } 361 | }) -------------------------------------------------------------------------------- /libraries/java/PackXZExtract.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dscalzi/HeliosLauncher/f8b7b9251c2d50ea06c8c9c6b63ea4ad05630644/libraries/java/PackXZExtract.jar -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helioslauncher", 3 | "version": "2.2.1", 4 | "productName": "Helios Launcher", 5 | "description": "Modded Minecraft Launcher", 6 | "author": "Daniel Scalzi (https://github.com/dscalzi/)", 7 | "license": "UNLICENSED", 8 | "homepage": "https://github.com/dscalzi/HeliosLauncher", 9 | "bugs": { 10 | "url": "https://github.com/dscalzi/HeliosLauncher/issues" 11 | }, 12 | "private": true, 13 | "main": "index.js", 14 | "scripts": { 15 | "start": "electron .", 16 | "dist": "electron-builder build", 17 | "dist:win": "npm run dist -- -w", 18 | "dist:mac": "npm run dist -- -m", 19 | "dist:linux": "npm run dist -- -l", 20 | "lint": "eslint --config .eslintrc.json ." 21 | }, 22 | "engines": { 23 | "node": "20.x.x" 24 | }, 25 | "dependencies": { 26 | "@electron/remote": "^2.1.2", 27 | "adm-zip": "^0.5.16", 28 | "discord-rpc-patch": "^4.0.1", 29 | "ejs": "^3.1.10", 30 | "ejs-electron": "^3.0.0", 31 | "electron-updater": "^6.3.9", 32 | "fs-extra": "^11.1.1", 33 | "github-syntax-dark": "^0.5.0", 34 | "got": "^11.8.5", 35 | "helios-core": "~2.2.4", 36 | "helios-distribution-types": "^1.3.0", 37 | "jquery": "^3.7.1", 38 | "lodash.merge": "^4.6.2", 39 | "semver": "^7.6.3", 40 | "toml": "^3.0.0" 41 | }, 42 | "devDependencies": { 43 | "electron": "^33.2.1", 44 | "electron-builder": "^25.1.8", 45 | "eslint": "^8.57.1" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/dscalzi/HeliosLauncher.git" 50 | } 51 | } 52 | --------------------------------------------------------------------------------