├── .deepsource.toml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── build └── icons │ ├── linux │ └── 1024x1024.png │ ├── macos │ └── icon.icns │ └── win │ └── icon.ico ├── package-lock.json ├── package.json ├── public ├── index.html └── logo-gray.svg ├── src ├── App.vue ├── assets │ ├── banner.webp │ ├── cards-backgrounds │ │ ├── about.png │ │ ├── discord.png │ │ ├── engine.webp │ │ ├── post.svg │ │ ├── settings-1.png │ │ ├── settings-2.png │ │ └── tutorial-1.png │ ├── checkmark.svg │ ├── fonts │ │ ├── Minecraft-Regular.woff2 │ │ └── Panton-BlackCaps.woff2 │ ├── global.css │ ├── label.webp │ ├── logo-gray.svg │ ├── logo.svg │ ├── servers-backgrounds │ │ ├── 1.webp │ │ ├── 2.webp │ │ ├── 3.webp │ │ ├── 4.webp │ │ ├── 5.webp │ │ ├── 6.webp │ │ ├── 7.webp │ │ └── 8.webp │ └── tutorial-images │ │ └── launch.PNG ├── babel.config.js ├── background.js ├── components │ ├── Card │ │ ├── Card.vue │ │ └── CardItem.vue │ ├── Content.vue │ ├── Content │ │ ├── About.vue │ │ ├── Debug.vue │ │ ├── Engine.vue │ │ ├── ErrorModal.vue │ │ ├── Home.vue │ │ ├── Play.vue │ │ ├── Servers.vue │ │ └── Settings.vue │ ├── Footer.vue │ ├── SentryNotification.vue │ ├── TitleBar.vue │ └── Tutorial.vue ├── constants.js ├── javascript │ ├── assets.js │ ├── cache.js │ ├── directories.js │ ├── discord.js │ ├── downloader.js │ ├── engine.js │ ├── jreDownloader.js │ ├── logger.js │ ├── lunar.js │ ├── minecraft.js │ ├── settings.js │ └── updater.js ├── main.js └── store.js └── vue.config.js /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "javascript" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | plugins = ["vue"] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Actual behaviour** 24 | Description of what is actually happening. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Environment (please complete the following information):** 30 | - OS: [e.g. Windows] 31 | - Launcher Version [eg. 4.0.1] 32 | - Engine Version [eg. 1.6] (If you can't find this, leave blank) 33 | - Solar Stats Version (if applicable) 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | Also attach logs (if you have them) (launcher, game, jvm/minecraft crash reports) 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | on: 3 | push: 4 | branches: [main] 5 | workflow_dispatch: 6 | jobs: 7 | build: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [macos-11, windows-latest, ubuntu-latest] 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.10' 17 | - uses: actions/setup-node@master 18 | with: 19 | node-version: 16 20 | - name: Cache node modules 21 | id: cache-npm 22 | uses: actions/cache@v3 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | path: node_modules 27 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-build-${{ env.cache-name }}- 30 | ${{ runner.os }}-build- 31 | ${{ runner.os }}- 32 | - run: npm install 33 | - run: npm run build 34 | env: 35 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | LauncherMetadata.jsonc 5 | 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner](https://i.imgur.com/vgsmtcI.png) 2 | 3 | # Solar Tweaks 4 | 5 | ![GitHub](https://img.shields.io/github/license/Solar-Tweaks/Solar-Tweaks?style=for-the-badge) 6 | ![GitHub all releases](https://img.shields.io/github/downloads/Solar-Tweaks/Solar-Tweaks/total?style=for-the-badge) 7 | ![GitHub package.json version](https://img.shields.io/github/package-json/version/Solar-Tweaks/Solar-Tweaks?style=for-the-badge) 8 | ![Maintenance](https://img.shields.io/maintenance/yes/2023?style=for-the-badge) 9 | 10 | Solar Tweaks is a tweaking tool for Lunar Client, offering a wide range of modifications, including the ability to use mods which are unavailable by default. We are dedicated to providing our users with the most up-to-date patches and updates, and we are proud to have a large user base of over 200,000 individuals who have downloaded and used our tool. 11 | 12 | Please note that the use of Solar Tweaks is at your own risk. Any changes done to the source code of Lunar Client is in violation of their terms of service. We advise you to review Lunar Client's terms of service by clicking [here](https://www.lunarclient.com/terms) before proceeding. We cannot be held responsible for any damages that may occur as a result of using Solar Tweaks. 13 | 14 | # Installing 15 | 16 | You can download the latest version of Solar Tweaks from the [Releases](https://github.com/Solar-Tweaks/Solar-Tweaks/releases) page. You need to download the version corresponding to your operating system (OS). You are able to find a more in depth explanation on how to download [here](https://github.com/Solar-Tweaks/Solar-Tweaks/wiki/Download-Solar-Tweaks). 17 | 18 | # Usage 19 | 20 | Launching the app will present with a user interface that closely resembles the original Lunar Client Launcher. This design choice has been implemented to ensure a seamless transition for users who may wish to switch between the two launchers, reducing confusion and improving ease of use. If you have never used Lunar Client and Solar Tweaks before, visit our [Documentation](https://docs.solartweaks.com) page and Lunar Client's [Support](https://support.lunarclient.com) page. 21 | 22 | **Please note that Solar Tweaks is not a replacement for Lunar Client. Solar Tweaks is a tweaking tool for Lunar Client** 23 | 24 | Customize and personalize your Lunar Client experience by navigating to the **"Engine"** tab within the app. Additionally, the **"Settings"** tab allows for customization of launcher and JRE preferences. The game can be launched at any time by utilizing the green launch button, which is accessible from any tab. 25 | 26 | # Building from source 27 | 28 | Solar Tweaks is fully open-source, allowing for users to obtain the source code and make their own modifications. To do so, the repository can be cloned using the command: 29 | ```bash 30 | $ git clone https://github.com/Solar-Tweaks/Solar-Tweaks.git 31 | ``` 32 | Once the repository is downloaded, navigate to the directory and install the necessary dependencies using the commands: 33 | ```bash 34 | $ cd Solar-Tweaks 35 | $ npm install 36 | ``` 37 | For development purposes, run this command to build the app and start it. Hot reload is included for easier development and testing. 38 | ```bash 39 | $ npm run serve 40 | ``` 41 | To build the final version of the app, run this command. The resulting executables, installers, etc. will be located in the dist directory. 42 | ```bash 43 | $ npm run build 44 | ``` 45 | 46 | **Note:** If you are a receiving an error like this `"error:0308010C:digital envelope routines::unsupported"`, then before running any `npm run` commands, run `set NODE_OPTIONS=--openssl-legacy-provider` in Command Prompt on Windows or `export NODE_OPTIONS=--openssl-legacy-provider` on Linux. 47 | 48 | # Contribute 49 | 50 | There are lots of ways to contribute to Solar Tweaks: 51 | 52 | - Fork the repository and make your own modifications, then open a [Pull request](https://github.com/Solar-Tweaks/Solar-Tweaks/pulls). 53 | - If you encounter any glitches or bugs, submit them in [Issues page](https://github.com/Solar-Tweaks/Solar-Tweaks/issues). 54 | - Help people that are having issues in [Discussions page](https://github.com/orgs/Solar-Tweaks/discussions). 55 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /build/icons/linux/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/build/icons/linux/1024x1024.png -------------------------------------------------------------------------------- /build/icons/macos/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/build/icons/macos/icon.icns -------------------------------------------------------------------------------- /build/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/build/icons/win/icon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solartweaks", 3 | "version": "4.3.3", 4 | "author": "SolarTweaks", 5 | "contributors": [ 6 | "RichardDorian", 7 | "770grappenmaker", 8 | "RadNotRed", 9 | "TBHGodPro", 10 | "Naibuu" 11 | ], 12 | "license": "GPL-3.0", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/Solar-Tweaks/Solar-Tweaks.git" 16 | }, 17 | "scripts": { 18 | "lint": "vue-cli-service lint", 19 | "build": "vue-cli-service electron:build", 20 | "serve": "vue-cli-service electron:serve", 21 | "postinstall": "electron-builder install-app-deps", 22 | "postuninstall": "electron-builder install-app-deps", 23 | "devtools": "vue-devtools" 24 | }, 25 | "main": "background.js", 26 | "dependencies": { 27 | "@sentry/vue": "^7.17.1", 28 | "axios": "^1.1.3", 29 | "discord-rpc": "^4.0.1", 30 | "electron-settings": "^4.0.2", 31 | "extract-zip": "^2.0.1", 32 | "net": "^1.0.2", 33 | "node-machine-id": "^1.1.12", 34 | "procbridge": "^1.1.1", 35 | "uuid": "^9.0.0", 36 | "vuex": "^4.1.0", 37 | "zip-local": "^0.3.5" 38 | }, 39 | "devDependencies": { 40 | "@vue/cli-plugin-babel": "~4.5.0", 41 | "@vue/cli-plugin-eslint": "~4.5.0", 42 | "@vue/cli-service": "~4.5.0", 43 | "@vue/compiler-sfc": "^3.0.0", 44 | "@vue/devtools": "^6.4.5", 45 | "babel-eslint": "^10.1.0", 46 | "electron": "^13.0.0", 47 | "electron-builder": "^23.6.0", 48 | "electron-devtools-installer": "^3.1.0", 49 | "eslint": "^6.7.2", 50 | "eslint-plugin-vue": "^7.0.0", 51 | "sass": "^1.57.1", 52 | "sass-loader": "^10.1.1", 53 | "vue": "^3.2.41", 54 | "vue-cli-plugin-electron-builder": "~2.1.1" 55 | }, 56 | "eslintConfig": { 57 | "root": true, 58 | "env": { 59 | "node": true 60 | }, 61 | "extends": [ 62 | "plugin:vue/vue3-essential", 63 | "eslint:recommended" 64 | ], 65 | "parserOptions": { 66 | "parser": "babel-eslint" 67 | }, 68 | "rules": { 69 | "no-async-promise-executor": "off" 70 | } 71 | }, 72 | "browserslist": [ 73 | "> 1%", 74 | "last 2 versions", 75 | "not dead" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 24 | Solar Tweaks 25 | 26 | 27 |
28 | Solar Tweaks Logo 34 |
35 | 58 | 67 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 113 | 114 | 138 | -------------------------------------------------------------------------------- /src/assets/banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/banner.webp -------------------------------------------------------------------------------- /src/assets/cards-backgrounds/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/cards-backgrounds/about.png -------------------------------------------------------------------------------- /src/assets/cards-backgrounds/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/cards-backgrounds/discord.png -------------------------------------------------------------------------------- /src/assets/cards-backgrounds/engine.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/cards-backgrounds/engine.webp -------------------------------------------------------------------------------- /src/assets/cards-backgrounds/settings-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/cards-backgrounds/settings-1.png -------------------------------------------------------------------------------- /src/assets/cards-backgrounds/settings-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/cards-backgrounds/settings-2.png -------------------------------------------------------------------------------- /src/assets/cards-backgrounds/tutorial-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/cards-backgrounds/tutorial-1.png -------------------------------------------------------------------------------- /src/assets/checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/Minecraft-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/fonts/Minecraft-Regular.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Panton-BlackCaps.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/fonts/Panton-BlackCaps.woff2 -------------------------------------------------------------------------------- /src/assets/global.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); 2 | 3 | p, 4 | input, 5 | select, 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6, 12 | i, 13 | button, 14 | span, 15 | div, 16 | textarea { 17 | color: #f7f7f7; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | input { 23 | caret-color: #f7f7f7; 24 | } 25 | 26 | p { 27 | line-height: 1.5; 28 | } 29 | 30 | /* Panton */ 31 | 32 | @font-face { 33 | font-family: 'Panton'; 34 | font-style: normal; 35 | font-weight: 390; 36 | src: local('Panton'), url('./fonts/Panton-BlackCaps.woff2') format('woff'); 37 | font-display: swap; 38 | } 39 | 40 | /* Minecraft */ 41 | @font-face { 42 | font-family: 'Minecraft'; 43 | src: url('./fonts/Minecraft-Regular.woff2') format('woff2'); 44 | font-weight: normal; 45 | font-style: normal; 46 | font-display: swap; 47 | } 48 | 49 | .vertical-card-container { 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: space-between; 53 | padding: 0.5rem; 54 | } 55 | 56 | .horizontal-card-container { 57 | display: flex; 58 | flex-direction: row; 59 | justify-content: space-between; 60 | padding: 0.5rem; 61 | } 62 | 63 | * { 64 | user-select: none; 65 | -webkit-user-select: none; 66 | padding: 0; 67 | margin: 0; 68 | font-family: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 69 | text-rendering: optimizeLegibility; 70 | -webkit-font-smoothing: antialiased; 71 | -moz-osx-font-smoothing: grayscale; 72 | } 73 | 74 | img { 75 | image-rendering: -webkit-optimize-quality; 76 | image-rendering: -moz-crisp-edges; 77 | image-rendering: -o-crisp-edges; 78 | } 79 | 80 | body { 81 | background: var(--color-background); 82 | 83 | /* Fonts */ 84 | 85 | --font-primary: 'Roboto', sans-serif; 86 | --font-secondary: 'Panton'; 87 | --font-minecraft: 'Minecraft'; 88 | 89 | /* Colors */ 90 | 91 | --color-darkest-gray: #151515; 92 | --color-darker-gray: #222222; 93 | --color-dark-gray: #303030; 94 | --color-gray: #595959; 95 | --color-light-gray: #cdcdcd; 96 | --color-gray-outline: #87878740; 97 | --color-ultra-blue: #0f94f3; 98 | --color-ultra-blue-hover: #47a9ee; 99 | --color-red: #de3434; 100 | --color-red-hover: #f23d3d; 101 | --color-red-outline: #de343440; 102 | --color-green: #239649; 103 | --color-green-hover: #28af55; 104 | --color-green-outline: #28af5578; 105 | --color-blue-static: #397d9b; 106 | --color-blue: #3b96be; 107 | --color-blue-hover: #42a8d3; 108 | --color-blue-outline: #3b96be40; 109 | --color-gold: #ffda74; 110 | --color-gold-outline: #ffda7440; 111 | --color-gold-hover: #ffe6a1; 112 | --color-yellow: #ffc915; 113 | --color-orange: #e6913d; 114 | --color-orange-hover: #f29c46; 115 | --color-orange-outline: #e6913d40; 116 | --color-footer-text: #4e4e4e; 117 | 118 | /* Backgrounds */ 119 | 120 | --play-background: url('https://i.imgur.com/VtUGHHD.png'); 121 | --tooltip-background: #141414; 122 | --server-background: #17171798; 123 | --backdrop-background: #000000d6; 124 | --color-background: #121212; 125 | 126 | /* Texts */ 127 | 128 | --color-dark-text: #b2b2b2; 129 | --color-text: #bdc9c9; 130 | 131 | /* Card */ 132 | 133 | --card-background: #171717; 134 | --card-color-background: #171717; 135 | --card-color-item-background: #1d1d1b; 136 | --card-color-text: rgb(203, 203, 203); 137 | --card-color-header: white; 138 | --card-color-button: #000000; 139 | --card-color-hover-text: #fff; 140 | 141 | /* Shadows */ 142 | 143 | --text-shadow: 0px 2px 0px rgb(0 0 0 / 20%); 144 | --short-text-shadow: 0px 1px 0px rgb(0 0 0 / 20%); 145 | 146 | /* Other */ 147 | 148 | --engine-color-text: #c7c7c7; 149 | --engine-card-hover: #232323; 150 | --logo-brightness: brightness(1.5); 151 | --icon-color: brightness(1); 152 | --slider-color: rgba(255, 255, 255, 0.447); 153 | } 154 | 155 | body[data-theme='light'] { 156 | background: var(--color-background); 157 | 158 | /* Fonts */ 159 | 160 | --font-primary: 'Roboto', sans-serif; 161 | --font-secondary: 'Panton'; 162 | --font-minecraft: 'Minecraft'; 163 | 164 | /* Colors */ 165 | 166 | --color-darkest-gray: #727272; 167 | --color-darker-gray: #cbcbcb; 168 | --color-dark-gray: #cbcbcb; 169 | --color-gray: #cbcbcb; 170 | --color-light-gray: #cdcdcd; 171 | --color-gray-outline: #87878740; 172 | --color-ultra-blue: #0f94f3; 173 | --color-red: #de3434; 174 | --color-red-hover: #f23d3d; 175 | --color-red-outline: #de343440; 176 | --color-green: #239649; 177 | --color-green-hover: #28af55; 178 | --color-green-outline: #28af5578; 179 | --color-blue-static: #397d9b; 180 | --color-blue: #3b96be; 181 | --color-blue-hover: #42a8d3; 182 | --color-blue-outline: #3b96be40; 183 | --color-gold: #ffda74; 184 | --color-gold-outline: #ffda7440; 185 | --color-gold-hover: #ffe6a1; 186 | --color-yellow: #ffc915; 187 | --color-orange: #e6913d; 188 | --color-orange-hover: #f29c46; 189 | --color-orange-outline: #e6913d40; 190 | --color-footer-text: #999999; 191 | 192 | /* Backgrounds */ 193 | 194 | --play-background: url('https://i.imgur.com/VtUGHHD.png'); 195 | --tooltip-background: #141414; 196 | --server-background: #ffffff98; 197 | --backdrop-background: #111517d6; 198 | --color-background: #e7e7e7; 199 | 200 | /* Texts */ 201 | 202 | --color-dark-text: #4f4f4f; 203 | --color-text: #737373; 204 | 205 | /* Card */ 206 | 207 | --card-background: #ffffff; 208 | --card-color-background: #efefef; 209 | --card-color-item-background: #f1f1f1; 210 | --card-color-text: rgb(99, 99, 99); 211 | --card-color-header: white; 212 | --card-color-button: #000000; 213 | --card-color-hover-text: #fff; 214 | 215 | /* Shadows */ 216 | 217 | --text-shadow: 0px 2px 0px rgb(0 0 0 / 20%); 218 | --short-text-shadow: 0px 1px 0px rgb(0 0 0 / 20%); 219 | 220 | /* Other */ 221 | 222 | --engine-color-text: #4d4d4d; 223 | --engine-card-hover: #e9e9e9; 224 | --logo-brightness: brightness(1); 225 | --icon-color: brightness(0.75); 226 | --slider-color: rgba(255, 255, 255, 0.447); 227 | } 228 | 229 | ::-webkit-scrollbar { 230 | display: none; 231 | } 232 | 233 | /* Animations */ 234 | 235 | @keyframes bounce_up { 236 | 0% { 237 | transform: translateY(45px); 238 | opacity: 0; 239 | } 240 | 50% { 241 | transform: translateY(-8px); 242 | } 243 | 100% { 244 | transform: translateY(0); 245 | opacity: 1; 246 | } 247 | } 248 | 249 | @keyframes fade_up { 250 | from { 251 | transform: translateY(45px); 252 | opacity: 0; 253 | } 254 | to { 255 | transform: translateY(0); 256 | opacity: 1; 257 | } 258 | } 259 | 260 | @keyframes fade_down { 261 | from { 262 | transform: translateY(-15px); 263 | opacity: 0; 264 | } 265 | to { 266 | transform: translateY(0); 267 | opacity: 1; 268 | } 269 | } 270 | 271 | @keyframes scale_down { 272 | from { 273 | transform: scale(1.6); 274 | opacity: 0; 275 | } 276 | to { 277 | transform: scale(1); 278 | opacity: 1; 279 | } 280 | } 281 | 282 | @keyframes button_scale { 283 | from { 284 | transform: scale(1.15); 285 | } 286 | to { 287 | transform: scale(0.95); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/assets/label.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/label.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/1.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/2.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/3.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/4.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/5.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/6.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/7.webp -------------------------------------------------------------------------------- /src/assets/servers-backgrounds/8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/servers-backgrounds/8.webp -------------------------------------------------------------------------------- /src/assets/tutorial-images/launch.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarvex/SolarTweaks/51a2c47847404519b8cfe24a207c64dcc2fce6d5/src/assets/tutorial-images/launch.PNG -------------------------------------------------------------------------------- /src/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { app, BrowserWindow, protocol } from 'electron'; 4 | import { createServer } from 'net'; 5 | import { Server } from 'procbridge'; 6 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'; 7 | 8 | // import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'; 9 | const isDevelopment = !app.isPackaged; 10 | 11 | // Scheme must be registered before the app is ready 12 | protocol.registerSchemesAsPrivileged([ 13 | { scheme: 'app', privileges: { secure: true, standard: true } }, 14 | ]); 15 | 16 | async function createWindow() { 17 | // Create the browser window. 18 | const win = new BrowserWindow({ 19 | width: 1300, 20 | height: 800, 21 | frame: false, 22 | maximizable: false, 23 | fullscreenable: false, 24 | resizable: isDevelopment, 25 | webPreferences: { 26 | nodeIntegration: true, 27 | contextIsolation: false, 28 | enableRemoteModule: true, 29 | // This is a way to bypass CORS but this is not secure at all 30 | webSecurity: false, 31 | }, 32 | }); 33 | 34 | if (process.env.WEBPACK_DEV_SERVER_URL) { 35 | // Load the url of the dev server if in development mode 36 | await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL); 37 | // if (isDevelopment) win.webContents.openDevTools(); 38 | } else { 39 | createProtocol('app'); 40 | // Load the index.html when not in development 41 | win.loadURL('app://./index.html'); 42 | } 43 | } 44 | 45 | // Quit when all windows are closed. 46 | app.on('window-all-closed', () => { 47 | // On macOS it is common for applications and their menu bar 48 | // to stay active until the user quits explicitly with Cmd + Q 49 | if (process.platform !== 'darwin') { 50 | app.quit(); 51 | } 52 | }); 53 | 54 | app.on('activate', () => { 55 | // On macOS it's common to re-create a window in the app when the 56 | // dock icon is clicked and there are no other windows open. 57 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 58 | else BrowserWindow.getAllWindows().forEach((win) => win.show()); 59 | }); 60 | 61 | // This method will be called when Electron has finished 62 | // initialization and is ready to create browser windows. 63 | // Some APIs can only be used after this event occurs. 64 | app.on('ready', async () => { 65 | if (isDevelopment) { 66 | // Install Vue Devtools 67 | // try { 68 | // await installExtension(VUEJS3_DEVTOOLS); 69 | // } catch (e) { 70 | // console.error('Vue Devtools failed to install:', e.toString()); 71 | // } 72 | } 73 | createWindow(); 74 | }); 75 | 76 | // Exit cleanly on request from parent process in development mode. 77 | if (isDevelopment) { 78 | if (process.platform === 'win32') { 79 | process.on('message', (data) => { 80 | if (data === 'graceful-exit') { 81 | app.quit(); 82 | } 83 | }); 84 | } else { 85 | process.on('SIGTERM', () => { 86 | app.quit(); 87 | }); 88 | } 89 | } 90 | 91 | const IPCServer = new Server('127.0.0.1', 28189, async (method, data) => { 92 | switch (method) { 93 | case 'open-window': 94 | console.log('Opening Login Window'); 95 | return new Promise(async (res) => { 96 | const window = new BrowserWindow({ 97 | width: data.width, 98 | height: data.height, 99 | autoHideMenuBar: true, 100 | show: false, 101 | resizable: false, 102 | title: 'Loading...', 103 | fullscreenable: false, 104 | }); 105 | let finalURL = null; 106 | window.webContents.addListener('will-redirect', (event, url) => { 107 | if (url.startsWith(data.targetUrlPrefix)) { 108 | finalURL = url; 109 | window.close(); 110 | } 111 | }); 112 | window.on('close', () => { 113 | window.removeAllListeners(); 114 | res( 115 | finalURL === null 116 | ? { status: 'CLOSED_WITH_NO_URL' } 117 | : { status: 'MATCHED_TARGET_URL', url: finalURL } 118 | ); 119 | }); 120 | window.webContents.session.clearCache(); 121 | window.webContents.session.clearStorageData(); 122 | window.loadURL(data.url); 123 | window.on('show', () => { 124 | window.setAlwaysOnTop(true); 125 | window.setAlwaysOnTop(false); 126 | }); 127 | window.once('ready-to-show', () => { 128 | console.log('Showing Login Window'); 129 | window.show(); 130 | }); 131 | }); 132 | default: 133 | console.error('Unknown IPC Method:', method); 134 | break; 135 | } 136 | }); 137 | 138 | function isPortAvailable(port) { 139 | return new Promise((res) => { 140 | const server = createServer() 141 | .addListener('error', () => res(false)) 142 | .addListener('listening', () => { 143 | server.addListener('close', () => res(true)); 144 | server.close(); 145 | }) 146 | .listen(port, '127.0.0.1'); 147 | }); 148 | } 149 | async function startIPCServer() { 150 | const available = await isPortAvailable(28189); 151 | if (available) { 152 | console.log('Starting IPC Server'); 153 | IPCServer.start(); 154 | console.log('Started IPC Server'); 155 | } else { 156 | console.warn( 157 | 'Failed to start IPC Server: Port not avilable. Will try again in 30 seconds.' 158 | ); 159 | setTimeout(() => startIPCServer(), 3e4); 160 | } 161 | } 162 | 163 | startIPCServer(); 164 | -------------------------------------------------------------------------------- /src/components/Card/Card.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 27 | 28 | 89 | -------------------------------------------------------------------------------- /src/components/Card/CardItem.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 66 | -------------------------------------------------------------------------------- /src/components/Content.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/Content/About.vue: -------------------------------------------------------------------------------- 1 | 157 | 158 | 323 | 324 | 433 | -------------------------------------------------------------------------------- /src/components/Content/Debug.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 78 | 79 | 86 | 87 | 125 | -------------------------------------------------------------------------------- /src/components/Content/Engine.vue: -------------------------------------------------------------------------------- 1 | 186 | 187 | 435 | 436 | 898 | -------------------------------------------------------------------------------- /src/components/Content/ErrorModal.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 40 | 41 | 147 | -------------------------------------------------------------------------------- /src/components/Content/Home.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 183 | 184 | 390 | -------------------------------------------------------------------------------- /src/components/Content/Servers.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 261 | 262 | 463 | -------------------------------------------------------------------------------- /src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 82 | 83 | 150 | -------------------------------------------------------------------------------- /src/components/SentryNotification.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | 45 | 107 | -------------------------------------------------------------------------------- /src/components/TitleBar.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 182 | 183 | 309 | -------------------------------------------------------------------------------- /src/components/Tutorial.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 307 | 308 | 511 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os'; 2 | import { join } from 'path'; 3 | 4 | export default { 5 | links: { 6 | GH_DISCUSSIONS: 'https://github.com/orgs/Solar-Tweaks/discussions', 7 | GITHUB: 'https://github.com/Solar-Tweaks/', 8 | GITBOOK: 'https://docs.solartweaks.com', 9 | YOUTUBE: 'https://www.youtube.com/channel/UCXRhlF3x02Sc8hgWnCMXnTQ', 10 | LUNARCLIENT: 'https://lunarclient.com/', 11 | SERVER_STATUS_ENDPOINT: 'https://mcapi.us/server/status', 12 | LC_METADATA_ENDPOINT: 'https://api.lunarclientprod.com/launcher/launch', 13 | LC_LAUNCHER_METADATA_ENDPOINT: 14 | 'https://api.lunarclientprod.com/launcher/metadata', 15 | WEBSITE: 'https://solartweaks.com', 16 | }, 17 | API_URL: 'https://server.solartweaks.com/api', 18 | ENGINE: { 19 | ENGINE: 'solar-engine.jar', 20 | CONFIG: 'config.json', 21 | CONFIG_EXAMPLE: 'config.example.json', 22 | METADATA: 'metadata.json', 23 | }, 24 | UPDATERS: { 25 | INDEX: '/updater/index', 26 | LAUNCHER: '/updater/?item=launcher&version={version}', 27 | ENGINE: '/updater/?item=engine&version={version}', 28 | METADATA: '/updater/?item=metadata&version={version}', 29 | CONFIG_EXAMPLE: '/updater/?item=config&version={version}', 30 | }, 31 | ENDPOINTS: { 32 | LAUNCH: '/launch', 33 | }, 34 | DOTLUNARCLIENT: join(homedir(), '.lunarclient'), 35 | SOLARTWEAKS_DIR: join(homedir(), '.lunarclient', 'solartweaks'), 36 | SENTRY: 37 | 'https://bd3de1d6c87c403f8f23da0a91a82b1b@o4504573741039616.ingest.sentry.io/4504573745758208', 38 | 39 | // Dynamic Constants 40 | LC_LAUNCHER_VERSION: '2.15.1', 41 | }; 42 | -------------------------------------------------------------------------------- /src/javascript/assets.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { mkdir, readFile } from 'fs/promises'; 3 | import { join } from 'path'; 4 | import constants from '../constants'; 5 | import store from '../store'; 6 | import { checkHash, downloadAndSaveFile } from './downloader'; 7 | import Logger from './logger'; 8 | 9 | const logger = new Logger('launcher'); 10 | 11 | /** 12 | * Check asset file hash + other stuff (I totally remember what I done here + I'm not lazy to read it) 13 | * @param {Object} metadata Metadata from Lunar's API 14 | * @param {string} data 15 | * @param {string} index 16 | * @returns {Promise} 17 | */ 18 | async function checkAsset(metadata, data, index) { 19 | const asset = data.split('\n')[index]; 20 | if (!/[0-9a-f]{40}/.test(asset.split(' ')[1].toLowerCase())) { 21 | logger.warn(`Invalid line in index file (line ${index + 1})\n${asset}`); 22 | return; 23 | } 24 | 25 | const path = join(constants.DOTLUNARCLIENT, 'textures', asset.split(' ')[0]); 26 | const sha1 = asset.split(' ')[1].toLowerCase(); 27 | 28 | const exists = existsSync(path); 29 | if (exists) { 30 | const match = await checkHash(path, sha1, 'sha1', false); 31 | if (match) return; 32 | else 33 | await downloadAndSaveFile( 34 | metadata.textures.baseUrl + sha1, 35 | path, 36 | 'blob', 37 | sha1, 38 | 'sha1', 39 | false 40 | ); 41 | } else { 42 | await downloadAndSaveFile( 43 | metadata.textures.baseUrl + sha1, 44 | path, 45 | 'blob', 46 | sha1, 47 | 'sha1', 48 | false 49 | ); 50 | } 51 | } 52 | 53 | /** 54 | * Downloads Lunar Client Assets (cosmetics) with the given metadata 55 | * @param {Object} metadata Metadata from Lunar's API 56 | * @returns {Promise} 57 | */ 58 | export async function downloadLunarAssets(metadata) { 59 | store.commit('setLaunchingState', { 60 | title: 'LAUNCHING...', 61 | message: 'CHECKING LC ASSETS...', 62 | icon: 'fa-solid fa-folder', 63 | }); 64 | return new Promise((resolve, reject) => { 65 | const postFolderCheck = (resolve, reject) => { 66 | const indexPath = join(constants.DOTLUNARCLIENT, 'launcher-cache', 'texturesIndex.txt'); 67 | downloadAndSaveFile( 68 | metadata.textures.indexUrl, 69 | indexPath, 70 | 'blob', 71 | metadata.textures.indexSha1, 72 | 'sha1', 73 | true, 74 | true 75 | ) 76 | .then(async () => { 77 | const data = await readFile(indexPath, 'utf8'); 78 | const assets = data.split('\n'); 79 | logger.info(`Checking ${assets.length} Total Assets`); 80 | while (assets.length) { 81 | const group = assets.splice(0, 2500); 82 | logger.info(`Checking ${group.length} Assets`); 83 | await Promise.all( 84 | group.map((i) => 85 | checkAsset(metadata, data, data.split('\n').indexOf(i)) 86 | ) 87 | ); 88 | } 89 | logger.info('Completed Checking Assets'); 90 | resolve(); 91 | }) 92 | .catch(reject); 93 | }; 94 | mkdir(join(constants.DOTLUNARCLIENT, 'textures')) 95 | .then(() => { 96 | postFolderCheck(resolve, reject); 97 | }) 98 | .catch((error) => { 99 | if (error.code === 'EEXIST') postFolderCheck(resolve, reject); 100 | else reject(error); 101 | }); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /src/javascript/cache.js: -------------------------------------------------------------------------------- 1 | export default Map; 2 | 3 | /** 4 | * Little cache system used to cache API responses (mainly used by the Servers component) 5 | */ 6 | export class Cache { 7 | constructor() { 8 | this.cache = {}; 9 | } 10 | 11 | get(key) { 12 | return this.cache[key]; 13 | } 14 | 15 | set(key, value) { 16 | this.cache[key] = value; 17 | } 18 | 19 | has(key) { 20 | return Object.prototype.hasOwnProperty.call(this.cache, key); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/javascript/directories.js: -------------------------------------------------------------------------------- 1 | import { mkdir, stat } from 'fs/promises'; 2 | import Logger from './logger'; 3 | 4 | const logger = new Logger('directories'); 5 | 6 | /** 7 | * Check if the given path is a directory and exists (creates it if it doesn't exist) 8 | * @param {string} path Path of the directory to check 9 | */ 10 | export function checkDirectory(path) { 11 | return new Promise((res, rej) => { 12 | stat(path) 13 | .then(() => { 14 | logger.info('Folder ' + path + ' Exists'); 15 | res(); 16 | }) 17 | .catch(() => { 18 | mkdir(path) 19 | .then(() => { 20 | logger.info('Created Folder ' + path); 21 | res(); 22 | }) 23 | .catch((err) => { 24 | if (err.code == 'EEXIST') return res(); 25 | const error = 'Failed to Create Folder ' + path + ' ' + err; 26 | logger.throw('Failed to Create Folder ' + path, err); 27 | rej(error); 28 | }); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/javascript/discord.js: -------------------------------------------------------------------------------- 1 | import { Client } from 'discord-rpc'; 2 | import { remote } from 'electron'; 3 | import Logger from './logger'; 4 | const logger = new Logger('discord'); 5 | 6 | const clientId = '920998351430901790'; 7 | 8 | export const client = new Client({ transport: 'ipc' }); 9 | 10 | client.on('ready', async () => { 11 | logger.info('Discord RPC ready'); 12 | }); 13 | 14 | client.isConnected = false; 15 | 16 | /** 17 | * Establish connection to Discord RPC 18 | */ 19 | export function login() { 20 | client 21 | .login({ clientId }) 22 | .then(async (client) => { 23 | if (client) { 24 | logger.info(`Authed for user ${client.user.username}`); 25 | client.isConnected = true; 26 | await updateActivity('In the launcher'); 27 | } else logger.error('Failed to login to Discord RPC'); 28 | }) 29 | .catch((error) => { 30 | logger.error(error); 31 | }); 32 | } 33 | 34 | export async function disableRPC() { 35 | if (!client.isConnected) return; 36 | return await client.destroy(); 37 | } 38 | 39 | /** 40 | * Update the current Discord activity 41 | * @param {string} details The details of the activity 42 | * @param {string} [state=undefined] The state of the activity 43 | * @param {Date|number} [timestamp=null] The start timestamp of the activity 44 | * @param {'elasped'|'remaining'} [mode=null] Display style for the timestamp 45 | */ 46 | export async function updateActivity( 47 | details, 48 | state = undefined, 49 | timestamp = null, 50 | mode = null 51 | ) { 52 | if (!client.isConnected) return; 53 | logger.info('Updating Discord Activity'); 54 | const activity = { 55 | details, 56 | state, 57 | largeImageKey: 'logo', 58 | largeImageText: `Solar Tweaks ${remote.app.getVersion()}`, 59 | buttons: [ 60 | { 61 | label: '⬇️⠀Download Solar Tweaks', 62 | url: 'https://github.com/Solar-Tweaks/Solar-Tweaks', 63 | }, 64 | ], 65 | }; 66 | 67 | if (Math.random() > 0.98) 68 | activity.buttons.push({ 69 | label: '🧐⠀Super secret button', 70 | url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 71 | }); 72 | 73 | if (timestamp && mode) { 74 | if (mode === 'remaining') { 75 | activity.startTimestamp = new Date(); 76 | activity.endTimestamp = timestamp; 77 | } else { 78 | activity.startTimestamp = timestamp; 79 | } 80 | } 81 | 82 | client.setActivity(activity).catch((error) => { 83 | logger.error(error); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/javascript/downloader.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { createHash } from 'crypto'; 3 | import { createWriteStream, existsSync } from 'fs'; 4 | // import { DownloaderHelper } from 'node-downloader-helper'; 5 | 6 | import { mkdir, readFile, writeFile } from 'fs/promises'; 7 | import Logger from './logger'; 8 | const logger = new Logger('downloader'); 9 | 10 | /** 11 | * Downloads and saves a file from a URL to the given path 12 | * @param {string} url URL of the file to download 13 | * @param {string} path Path where to save the file 14 | * @param {'text'|'blob'} fileType Type of the file to download 15 | * @param {string} [hash=null] SHA1 or SHA256 hash of the file to make sure it's the same 16 | * @param {'sha1'|'sha256'} [algorithm='sha1'] Hash algorithm to use 17 | * @param {boolean} [logging=true] Whether or not to log 18 | * @param {boolean} [skipFolderCheck=false] Whether or not to check if the folder exists 19 | */ 20 | export async function downloadAndSaveFile( 21 | url, 22 | path, 23 | fileType, 24 | hash = null, 25 | algorithm = 'sha1', 26 | logging = true, 27 | skipFolderCheck = false 28 | ) { 29 | if (logging) logger.info(`Downloading ${url}...`); 30 | 31 | const response = await axios.get(url, { responseType: fileType }); 32 | 33 | if (logging) { 34 | logger.info(`Downloaded ${url}`); 35 | logger.debug(`Saving to ${path}...`); 36 | } 37 | 38 | if (!skipFolderCheck) { 39 | const folderPath = path.includes('\\') 40 | ? path.substring(0, path.lastIndexOf('\\')) 41 | : path.substring(0, path.lastIndexOf('/')); 42 | await mkdir(folderPath, { 43 | recursive: true, 44 | }); 45 | } 46 | 47 | if (fileType === 'text') { 48 | await writeFile( 49 | path, 50 | typeof response.data === 'object' 51 | ? JSON.stringify(response.data) 52 | : response.data, 53 | 'utf8' 54 | ); 55 | if (logging) logger.debug(`Saved to ${path}`); 56 | if (hash) { 57 | // eslint-disable-next-line no-unused-vars 58 | const isMatching = await checkHash(path, hash, algorithm, logging); 59 | // Handle hash mismatch 60 | } 61 | } 62 | 63 | if (fileType === 'blob') { 64 | const output = createWriteStream(path); 65 | const ws = new WritableStream(output); 66 | 67 | let blob = new Blob([response.data], { type: 'application/zip' }); 68 | 69 | /** @type {ReadableStream} */ 70 | const stream = blob.stream(); 71 | 72 | await stream.pipeTo(ws); 73 | if (hash) { 74 | // eslint-disable-next-line no-unused-vars 75 | const isMatching = await checkHash(path, hash, algorithm, logging); 76 | // Handle hash mismatch 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Checks if the given hash is matching with the given file 83 | * @param {string} path Path of the file to check 84 | * @param {string} hash Hash to check 85 | * @param {'sha1'|'sha256'} algorithm Algorithm to use 86 | * @param {boolean} [logging=true] Whether or not to log 87 | * @returns {Promise} 88 | */ 89 | export async function checkHash(path, hash, algorithm, logging = true) { 90 | if (logging) logger.debug(`Checking hash of ${path}...`); 91 | 92 | if (!existsSync(path)) return false; 93 | 94 | const fileBuffer = await readFile(path); 95 | const hashSum = createHash(algorithm); 96 | hashSum.update(fileBuffer); 97 | const fileHash = hashSum.digest('hex'); 98 | if (fileHash !== hash) { 99 | if (logging) 100 | logger.error( 101 | `Hash mismatch for ${path}\nExpected: ${hash}\nGot: ${fileHash}\nAlgorithm: ${algorithm}` 102 | ); 103 | return false; 104 | } else { 105 | if (logging) logger.debug(`Hash matches for ${path}`); 106 | return true; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/javascript/engine.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import settings from 'electron-settings'; 3 | import { stat, writeFile } from 'fs/promises'; 4 | import { join } from 'path'; 5 | import constants from '../constants'; 6 | import { downloadAndSaveFile } from '../javascript/downloader'; 7 | import Logger from '../javascript/logger'; 8 | 9 | const logger = new Logger('Engine'); 10 | 11 | /** 12 | * Verify the Engine and all files it needs exist 13 | * @returns {Promise} 14 | */ 15 | export async function verifyEngine() { 16 | const enginePath = join(constants.SOLARTWEAKS_DIR, constants.ENGINE.ENGINE); 17 | const configExamplePath = join( 18 | constants.SOLARTWEAKS_DIR, 19 | constants.ENGINE.CONFIG_EXAMPLE 20 | ); 21 | const metadataPath = join( 22 | constants.SOLARTWEAKS_DIR, 23 | constants.ENGINE.METADATA 24 | ); 25 | const configPath = join(constants.SOLARTWEAKS_DIR, constants.ENGINE.CONFIG); 26 | 27 | logger.debug('Verifying Config Exists...'); 28 | await stat(configPath).catch( 29 | async () => await writeFile(configPath, '{}', 'utf-8') 30 | ); 31 | 32 | logger.debug('Fetching Updater Index'); 33 | const release = await axios 34 | .get(`${constants.API_URL}${constants.UPDATERS.INDEX}`) 35 | .catch((reason) => { 36 | logger.throw('Failed to Fetch Updater Index', reason); 37 | }); 38 | 39 | await Promise.all([ 40 | await axios 41 | .get( 42 | `${constants.API_URL}${constants.UPDATERS.CONFIG_EXAMPLE.replace( 43 | '{version}', 44 | release?.data?.index?.stable?.config ?? 'example' 45 | )}` 46 | ) 47 | .then((res) => { 48 | logger.debug(`Fetched Config Example:`, res.data); 49 | if (res.status == 200) 50 | return writeFile( 51 | configExamplePath, 52 | JSON.stringify(res.data), 53 | 'utf-8' 54 | ); 55 | }) 56 | .catch((err) => logger.throw('Failed to Fetch Config Example:', err)), 57 | await axios 58 | .get( 59 | `${constants.API_URL}${constants.UPDATERS.METADATA.replace( 60 | '{version}', 61 | release?.data?.index?.stable?.metadata ?? '1.0.0' 62 | )}` 63 | ) 64 | .then((res) => { 65 | logger.debug(`Fetched Metadata:`, res.data); 66 | if (res.status == 200) 67 | return writeFile(metadataPath, JSON.stringify(res.data), 'utf-8'); 68 | }) 69 | .catch((err) => logger.throw('Failed to Fetch Metadata:', err)), 70 | ]); 71 | 72 | if (!release) return; 73 | 74 | const newest = release.data.index.stable.engine; 75 | if (!newest) 76 | return logger.throw( 77 | 'Unable to get newest engine version from data', 78 | JSON.stringify(release.data) 79 | ); 80 | const current = await settings.get('engineVersion'); 81 | // Check if file solar-engine.jar exists 82 | if ( 83 | !current || // Not installed (first usage) 84 | !(await stat(enginePath).catch(() => false)) || // Engine doesn't exist, download 85 | newest !== current // Out of Date 86 | ) { 87 | logger.info( 88 | `Downloading Engine v${newest}, current is ${ 89 | current ? `v${current}` : '"not installed"' 90 | }...` 91 | ); 92 | await downloadAndSaveFile( 93 | `${constants.API_URL}${constants.UPDATERS.ENGINE.replace( 94 | '{version}', 95 | newest 96 | )}`, 97 | enginePath, 98 | 'blob' 99 | ).catch((err) => logger.throw('Failed to download Engine', err)); 100 | if (current && newest) { 101 | if (!current.toString().startsWith(newest.toString().charAt(0))) 102 | await writeFile(configPath, '{}', 'utf-8'); 103 | } else if (!current) await writeFile(configPath, '{}', 'utf-8'); 104 | await settings.set('engineVersion', release.data.index.stable.engine); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/javascript/jreDownloader.js: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import extract from 'extract-zip'; 3 | import { existsSync } from 'fs'; 4 | import { move } from 'fs-extra'; 5 | import { mkdir, readdir, rm, rmdir } from 'fs/promises'; 6 | import { arch, platform } from 'os'; 7 | import { join } from 'path'; 8 | import constants from '../constants'; 9 | import { downloadAndSaveFile } from './downloader'; 10 | import Logger from './logger'; 11 | const logger = new Logger('jreDownloader'); 12 | 13 | /** 14 | * Download and extract a Java JRE from the given manifest file 15 | * @typedef {{url: string, checksum: string, folder?: string, tar?: boolean}} JREPlatform 16 | * @param {{ 17 | * name: string, 18 | * 32: JREPlatform, 19 | * 64: JREPlatform, 20 | * MacArm: JREPlatform, 21 | * MacX64: JREPlatform, 22 | * LinuxArm: JREPlatform, 23 | * LinuxX64: JREPlatform 24 | * }} _jre JRE to download 25 | * @returns {Promise} True if successful 26 | */ 27 | export async function downloadJre(_jre) { 28 | /** @type {JREPlatform} */ 29 | let jre; 30 | /** @type {'aix' | 'darwin' | 'freebsd' | 'linux' | 'openbsd' | 'sunos' | 'win32'} */ 31 | const plat = platform(); 32 | /** @type {'arm' | 'arm64' | 'ia32' | 'mips' | 'mipsel' | 'ppc' | 'ppc64' | 's390' | 's390x' | 'x64'} */ 33 | const a = arch(); 34 | if (plat === 'win32') { 35 | jre = _jre[a === 'x64' ? '64' : '32']; 36 | } else if (plat === 'darwin') { 37 | jre = _jre[a === 'x64' ? 'MacX64' : 'MacArm']; 38 | } else if (plat === 'linux') { 39 | jre = _jre[a === 'x64' ? 'LinuxX64' : 'LinuxArm']; 40 | } else { 41 | logger.warn( 42 | 'Attempted to download a JRE on a non Windows, MacOS, or Linux Operating System!' 43 | ); 44 | return false; 45 | } 46 | 47 | if (!jre) { 48 | logger.throw( 49 | `Failed to get JRE from JREs List for ${_jre.name}`, 50 | JSON.stringify(_jre) 51 | ); 52 | return false; 53 | } 54 | 55 | const jresPath = join(constants.SOLARTWEAKS_DIR, 'jres'); 56 | const jrePath = join(jresPath, _jre.name); 57 | 58 | await mkdir(jresPath).catch(() => { 59 | // Folder already exists, do nothing 60 | }); 61 | 62 | await downloadAndSaveFile( 63 | jre.url, 64 | `${jrePath}.${jre.tar ? 'tar.gz' : 'zip'}`, 65 | 'blob', 66 | jre.checksum, 67 | 'sha256', 68 | true, 69 | true 70 | ); 71 | 72 | await rmdir(jrePath).catch(() => { 73 | // Folder doesn't exist, do nothing 74 | }); 75 | await rmdir(jrePath + '_temp').catch(() => { 76 | // Folder doesn't exist, do nothing 77 | }); 78 | 79 | if (jre.tar) { 80 | await new Promise((res) => 81 | mkdir(jrePath + '_temp') 82 | .then(res) 83 | .catch(async () => { 84 | logger.info('JRE Temp Path already exists, clearing...'); 85 | await rm(jrePath + '_temp', { 86 | recursive: true, 87 | }); 88 | await mkdir(jrePath + '_temp', { 89 | recursive: true, 90 | }); 91 | }) 92 | ); 93 | if ( 94 | !(await new Promise((res) => 95 | exec( 96 | `tar -xzvf ${jrePath}.tar.gz -C ${jrePath + '_temp'}`, 97 | async (err) => { 98 | if (err) { 99 | logger.throw(`Failed to extract ${jrePath}.tar.gz`, err); 100 | await rm(`${jrePath}.tar.gz`); 101 | return res(false); 102 | } 103 | res(true); 104 | } 105 | ) 106 | )) 107 | ) 108 | return false; 109 | } else { 110 | if ( 111 | !(await new Promise((res) => 112 | extract(`${jrePath}.zip`, { dir: jrePath + '_temp' }) 113 | .then(() => res(true)) 114 | .catch(async (err) => { 115 | logger.throw(`Failed to extract ${jrePath}.zip`, err); 116 | await rm(`${jrePath}.zip`); 117 | res(false); 118 | }) 119 | )) 120 | ) 121 | return false; 122 | } 123 | 124 | let jreFolder = (await readdir(join(jrePath + '_temp')))?.[0] || jre.folder; 125 | if (!jreFolder) { 126 | logger.error( 127 | `Failed to find JRE directory in \`.lunarclient/solartweaks/jres/${jrePath}_temp\`` 128 | ); 129 | await rmdir(jrePath + '_temp', { 130 | recursive: true, 131 | }); 132 | await rm(`${jrePath}.${jre.tar ? 'tar.gz' : 'zip'}`); 133 | return false; 134 | } 135 | if (existsSync(join(jrePath + '_temp', jreFolder, 'Contents'))) 136 | jreFolder = join(jreFolder, '/Contents/Home'); 137 | await move(join(jrePath + '_temp', jreFolder), jrePath, { overwrite: true }); 138 | await rmdir(jrePath + '_temp', { 139 | recursive: true, 140 | }); 141 | await rm(`${jrePath}.${jre.tar ? 'tar.gz' : 'zip'}`); 142 | 143 | return true; 144 | } 145 | 146 | /** 147 | * Delete a downloaded JRE 148 | */ 149 | export async function removeJre(jreName) { 150 | await rm(join(constants.SOLARTWEAKS_DIR, 'jres', jreName), { 151 | recursive: true, 152 | force: true, 153 | }); 154 | logger.info(`Removed JRE ${jreName}`); 155 | } 156 | -------------------------------------------------------------------------------- /src/javascript/logger.js: -------------------------------------------------------------------------------- 1 | import { captureException } from '@sentry/vue'; 2 | import { createWriteStream } from 'fs'; 3 | import { appendFile, mkdir, readFile, stat, writeFile } from 'fs/promises'; 4 | import { join } from 'path'; 5 | import constants from '../constants'; 6 | 7 | /** 8 | * Log system used to log messages to the console and to the log file 9 | */ 10 | export default class Logger { 11 | constructor(name) { 12 | this.name = name; 13 | this.logger = console; 14 | } 15 | 16 | debug(...args) { 17 | const x = `[${this.name}]`; 18 | this.logger.debug(x, ...args); 19 | this.writeLog(`[DEBUG] ${x} ${[...args].join(' ')}`); 20 | } 21 | 22 | info(...args) { 23 | const x = `[${this.name}]`; 24 | this.logger.info(x, ...args); 25 | this.writeLog(`[INFO] ${x} ${[...args].join(' ')}`); 26 | } 27 | 28 | warn(...args) { 29 | const x = `[${this.name}]`; 30 | this.logger.warn(x, ...args); 31 | this.writeLog(`[WARN] ${x} ${[...args].join(' ')}`); 32 | } 33 | 34 | error(...args) { 35 | const x = `[${this.name}]`; 36 | this.logger.error(x, ...args); 37 | this.writeLog(`[ERROR] ${x} ${[...args].join(' ')}`); 38 | } 39 | throw(problem, err) { 40 | const error = err instanceof Error ? err : new Error(err.trim()); 41 | console.error(problem.trim(), error); 42 | captureException(`${problem.trim()}: ${error}`); 43 | } 44 | 45 | /** 46 | * Write log to file 47 | * @param {any} log 48 | */ 49 | async writeLog(log) { 50 | await appendFile( 51 | join(constants.SOLARTWEAKS_DIR, 'logs', 'launcher-latest.log'), 52 | `${log}\n` 53 | ).catch(async (reason) => { 54 | if ( 55 | (typeof reason === 'string' ? reason : reason.toString()).includes( 56 | 'no such file or directory' 57 | ) 58 | ) { 59 | await clearLogs(); 60 | await appendFile( 61 | join(constants.SOLARTWEAKS_DIR, 'logs', 'launcher-latest.log'), 62 | `${log}\n` 63 | ); 64 | } else throw new Error(reason); 65 | }); 66 | } 67 | } 68 | 69 | export async function createMinecraftLogger(version) { 70 | const logFile = join( 71 | constants.SOLARTWEAKS_DIR, 72 | 'logs', 73 | `${version}-latest.log` 74 | ); 75 | 76 | await writeFile( 77 | logFile, 78 | '' // Clear the file and creates it if it doesn't exist 79 | ); 80 | 81 | return createWriteStream(logFile, { encoding: 'utf8' }); 82 | } 83 | 84 | /** 85 | * Clears the log file 86 | */ 87 | export async function clearLogs() { 88 | await stat(join(constants.SOLARTWEAKS_DIR, 'logs')).catch(() => 89 | mkdir(join(constants.SOLARTWEAKS_DIR, 'logs'), { 90 | recursive: true, 91 | }) 92 | ); 93 | 94 | const oldLog = await readFile( 95 | join(constants.SOLARTWEAKS_DIR, 'logs', 'launcher-latest.log'), 96 | 'utf-8' 97 | ).catch(() => ''); 98 | 99 | await writeFile( 100 | join(constants.SOLARTWEAKS_DIR, 'logs', 'launcher-latest.log'), 101 | '' 102 | ); 103 | 104 | await writeFile( 105 | join(constants.SOLARTWEAKS_DIR, 'logs', 'launcher-old.log'), 106 | oldLog, 107 | 'utf-8' 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /src/javascript/lunar.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | import Logger from '../javascript/logger'; 5 | 6 | const logger = new Logger('LunarChecker'); 7 | 8 | /** 9 | * Check if the Lunar Client is installed 10 | * @returns {Boolean} 11 | */ 12 | export function doesLunarExists() { 13 | logger.info('Checking for Lunar Client...'); 14 | logger.info(`Platform: ${os.platform()}`); 15 | let lunarPath = ''; 16 | let lunarExists = false; 17 | switch (os.platform()) { 18 | case 'win32': 19 | lunarPath = path.join( 20 | os.homedir(), 21 | 'AppData', 22 | 'Local', 23 | 'Programs', 24 | 'lunarclient', 25 | 'Lunar Client.exe' 26 | ); 27 | logger.info(`Checking for Lunar Client in ${lunarPath}`); 28 | lunarExists = fs.existsSync(lunarPath); 29 | if (lunarExists) logger.info('Lunar Client found'); 30 | return lunarExists; 31 | case 'darwin': 32 | lunarPath = path.join('/', 'Applications', 'Lunar Client.app'); 33 | logger.info(`Checking for Lunar Client in ${lunarPath}`); 34 | lunarExists = fs.existsSync(lunarPath); 35 | if (lunarExists) logger.info('Lunar Client found'); 36 | return lunarExists; 37 | case 'linux': 38 | logger.error('Unsupported platform (Linux)'); 39 | return lunarExists; 40 | default: 41 | logger.error(`Unsupported platform (${os.platform()})`); 42 | return lunarExists; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/javascript/minecraft.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { exec, spawn } from 'child_process'; 3 | import { remote } from 'electron'; 4 | import settings from 'electron-settings'; 5 | import { default as extract, default as extractZip } from 'extract-zip'; 6 | import { existsSync } from 'fs'; 7 | import { mkdir, readFile, rm, stat, writeFile } from 'fs/promises'; 8 | import { machineId as _machineId } from 'node-machine-id'; 9 | import { arch as osArch, platform, release } from 'os'; 10 | import { join } from 'path'; 11 | import process from 'process'; 12 | import { v4 as uuidv4, validate as validateUUID } from 'uuid'; 13 | import constants from '../constants'; 14 | import store from '../store'; 15 | import { downloadLunarAssets } from './assets'; 16 | import { disableRPC, login as connectRPC, updateActivity } from './discord'; 17 | import { checkHash, downloadAndSaveFile } from './downloader'; 18 | import { verifyEngine } from './engine'; 19 | import Logger, { createMinecraftLogger } from './logger'; 20 | import { getDefaultJREPath, getDotMinecraftDirectory } from './settings'; 21 | import { availableVersions } from '../components/Content/Play.vue'; 22 | 23 | const logger = new Logger('launcher'); 24 | 25 | /** 26 | * Checks if the `.lunarclient` directory is valid 27 | */ 28 | export async function setupLunarClientDirectory() { 29 | logger.info('Checking .lunarclient directory'); 30 | 31 | store.commit('setLaunchingState', { 32 | title: 'LAUNCHING...', 33 | message: 'CHECKING LC FOLDER...', 34 | icon: 'fa-solid fa-folder', 35 | }); 36 | 37 | const folders = ['licenses', 'offline']; 38 | 39 | if (!existsSync(constants.DOTLUNARCLIENT)) { 40 | logger.debug('Creating .lunarclient directory...'); 41 | await mkdir(constants.DOTLUNARCLIENT) 42 | .then(() => { 43 | logger.debug('Created .lunarclient directory'); 44 | }) 45 | .catch((error) => { 46 | logger.throw("Can't create .lunarclient directory", error); 47 | }); 48 | } 49 | 50 | logger.debug('Checking .lunarclient subdirectories'); 51 | 52 | for (const index in folders) { 53 | const folder = folders[index]; 54 | 55 | // Launch state 56 | store.commit('setLaunchingState', { 57 | title: 'LAUNCHING...', 58 | message: `CHECKING SUBFOLDERS ${parseInt(index) + 1}/${folders.length}`, 59 | icon: 'fa-solid fa-folder', 60 | }); 61 | 62 | if (!existsSync(join(constants.DOTLUNARCLIENT, folder))) { 63 | logger.debug(`Creating ${folder} subdirectory...`); 64 | await mkdir(join(constants.DOTLUNARCLIENT, folder)) 65 | .then(() => { 66 | logger.debug(`Created ${folder} subdirectory`); 67 | }) 68 | .catch((error) => { 69 | logger.throw(`Can't create ${folder} subdirectory`, error); 70 | }); 71 | } else logger.debug(`${folder} subdirectory already exists, skipping...`); 72 | } 73 | } 74 | /** 75 | * Download the Official Lunar JRE 76 | * @param metadata The LC Launch Metadata 77 | * @returns Whether the download was successful 78 | */ 79 | export async function downloadDefaultJRE(metadata) { 80 | const jre = metadata.jre; 81 | const path = join(constants.DOTLUNARCLIENT, 'jre', jre.folderChecksum); 82 | await downloadAndSaveFile( 83 | jre.download.url, 84 | `${path}.${jre.download.extension}`, 85 | 'blob' 86 | ); 87 | if (jre.download.extension === 'zip') { 88 | if ( 89 | !(await new Promise((res) => 90 | extract(`${path}.zip`, { dir: path }) 91 | .then(() => res(true)) 92 | .catch(async (err) => { 93 | logger.throw(`Failed to extract ${path}.zip`, err); 94 | await rm(`${path}.zip`); 95 | res(false); 96 | }) 97 | )) 98 | ) 99 | return false; 100 | } else if (jre.download.extension === 'tar.gz') { 101 | if ( 102 | !(await new Promise((res) => 103 | mkdir(path) 104 | .then(() => res(true)) 105 | .catch(async () => { 106 | logger.info('JRE Path already exists, skipping download...'); 107 | await rm(`${path}.tar.gz`); 108 | res(false); 109 | }) 110 | )) 111 | ) 112 | return false; 113 | if ( 114 | !(await new Promise((res) => 115 | exec(`tar -xzvf ${path}.tar.gz -C ${path}`, async (err) => { 116 | if (err) { 117 | logger.throw(`Failed to extract ${path}.tar.gz`, err); 118 | await rm(`${path}.tar.gz`); 119 | res(false); 120 | } else res(true); 121 | }) 122 | )) 123 | ) 124 | return false; 125 | } else return false; 126 | await rm(`${path}.${jre.download.extension}`); 127 | await settings.set( 128 | 'jrePath', 129 | join( 130 | path, 131 | ...jre.executablePathInArchive.splice( 132 | 0, 133 | jre.executablePathInArchive.length - 1 134 | ) 135 | ) 136 | ); 137 | return true; 138 | } 139 | /** 140 | * Deal with an Invalid JRE 141 | * @param metadata The LC Launch Metadata 142 | * @returns Whether a valid JRE was selected 143 | */ 144 | export async function invalidJRE(metadata) { 145 | logger.warn( 146 | 'JRE not found! Showing error dialog and aborting launch process' 147 | ); 148 | 149 | const choice = await remote.dialog.showMessageBox({ 150 | type: 'error', 151 | title: 'JRE not found', 152 | message: 153 | 'The JRE you selected was not found or is invalid.\n\nPlease select a valid JRE in the settings page or download one using the JRE downloader.\n\nMake sure you selected the bin folder inside of the JRE.', 154 | buttons: ['Cancel launch', 'Select JRE', 'Download Default JRE'], 155 | }); 156 | 157 | if (choice.response === 1) { 158 | store.commit('setLaunchingState', { 159 | title: `LAUNCHING`, 160 | message: 'SELECTING JRE...', 161 | icon: 'fa-solid fa-gamepad', 162 | }); 163 | store.commit('setLaunching', true); 164 | // Set new folder 165 | const folder = await remote.dialog.showOpenDialog({ 166 | title: `Select the new JRE for Lunar Client (Select the bin folder)`, 167 | defaultPath: await settings.get('jrePath'), 168 | properties: ['dontAddToRecent', 'openDirectory'], 169 | }); 170 | 171 | if (folder.canceled) { 172 | store.commit('setLaunchingState', { 173 | title: `LAUNCH ${await settings.get('version')}`, 174 | message: 'READY TO LAUNCH', 175 | icon: 'fa-solid fa-gamepad', 176 | }); 177 | store.commit('setLaunching', false); 178 | return; 179 | } 180 | 181 | await settings.set('jrePath', folder.filePaths[0]); 182 | return await checkJRE(metadata); 183 | } else if (choice.response === 2) { 184 | store.commit('setLaunchingState', { 185 | title: `LAUNCHING`, 186 | message: 'DOWNLOADING DEFAULT JRE...', 187 | icon: 'fa-solid fa-gamepad', 188 | }); 189 | store.commit('setLaunching', true); 190 | const path = await getDefaultJREPath(); 191 | if (path == '') { 192 | logger.info('No JRE Already Installed, Downloading New One...'); 193 | if (!(await downloadDefaultJRE(metadata))) return false; 194 | } else { 195 | logger.info('JRE Already Installed'); 196 | await settings.set('jrePath', path); 197 | } 198 | return await checkJRE(metadata); 199 | } else { 200 | // Cancel launch or closed 201 | store.commit('setLaunchingState', { 202 | title: `LAUNCH ${await settings.get('version')}`, 203 | message: 'READY TO LAUNCH', 204 | icon: 'fa-solid fa-gamepad', 205 | }); 206 | store.commit('setLaunching', false); 207 | logger.error('JRE not found'); 208 | return false; 209 | } 210 | } 211 | 212 | /** 213 | * Checks if the JRE is valid 214 | * @param metadata The LC Launch Metadata 215 | * @returns If the JRE is valid 216 | */ 217 | export async function checkJRE(metadata) { 218 | store.commit('setLaunchingState', { 219 | title: 'LAUNCHING...', 220 | message: 'CHECKING JRE...', 221 | icon: 'fa-solid fa-folder', 222 | }); 223 | 224 | const jrePath = (await settings.get('jrePath')) ?? ''; 225 | const javaName = process.platform === 'win32' ? 'java.exe' : 'java'; 226 | 227 | const exists = { 228 | jre: await stat(jrePath).catch(() => false), // Bin folder 229 | java: await stat(join(jrePath, javaName)).catch(() => false), // Java binary 230 | }; 231 | 232 | // If one of them is missing 233 | if (!exists.jre || !exists.java) return await invalidJRE(metadata); 234 | else return true; 235 | } 236 | 237 | /** 238 | * Fetches metadata from Lunar's API 239 | * @param {boolean} [skipLaunchingState=false] Skip or not the launching state 240 | * @returns {Promise} 241 | */ 242 | export async function fetchMetadata(skipLaunchingState = false) { 243 | if (!skipLaunchingState) { 244 | // Launch state 245 | store.commit('setLaunchingState', { 246 | title: 'LAUNCHING...', 247 | message: 'FETCHING METADATA...', 248 | icon: 'fa-solid fa-download', 249 | }); 250 | } 251 | 252 | // Fetch metadata 253 | logger.info('Fetching metadata...'); 254 | const [ 255 | hwid, 256 | version, 257 | hwid_private, 258 | installation_id, 259 | module, 260 | os, 261 | os_release, 262 | arch, 263 | ] = await Promise.all([ 264 | _machineId().catch((err) => { 265 | logger.error('Failed to fetch Machine ID', err); 266 | }), 267 | settings.get('version'), 268 | getHWIDPrivate(), 269 | getInstallationID(), 270 | settings.get('module'), 271 | platform(), 272 | release(), 273 | osArch(), 274 | ]); 275 | return new Promise((resolve, reject) => { 276 | axios 277 | .post( 278 | constants.links.LC_METADATA_ENDPOINT, 279 | { 280 | hwid, 281 | installation_id, 282 | os, 283 | os_release, 284 | arch, 285 | version, 286 | branch: 'master', 287 | launch_type: 'OFFLINE', 288 | module, 289 | ...(hwid_private ? { hwid_private } : {}), 290 | }, 291 | { 'Content-Type': 'application/json', 'User-Agent': 'SolarTweaks' } 292 | ) 293 | .then((response) => { 294 | logger.debug('Fetched metadata', response.data); 295 | resolve(response.data); 296 | }) 297 | .catch((error) => { 298 | logger.throw('Failed to fetch metadata', error); 299 | reject(error); 300 | }); 301 | }); 302 | } 303 | 304 | /** 305 | * Checks license (and downloads if needed) 306 | * @param {Object} metadata Metadata from Lunar's API 307 | * @returns {Promise} 308 | */ 309 | export async function checkLicenses(metadata) { 310 | logger.info('Checking licenses...'); 311 | store.commit('setLaunchingState', { 312 | title: 'LAUNCHING...', 313 | message: `CHECKING ${metadata.licenses.length} LICENSES ...`, 314 | icon: 'fa-solid fa-gavel', 315 | }); 316 | for (const index in metadata.licenses) { 317 | const license = metadata.licenses[index]; 318 | logger.debug( 319 | `Checking license ${parseInt(index) + 1}/${metadata.licenses.length}` 320 | ); 321 | const licensePath = join( 322 | constants.DOTLUNARCLIENT, 323 | 'licenses', 324 | license.file 325 | ); 326 | 327 | if (!existsSync(licensePath)) { 328 | await downloadAndSaveFile( 329 | license.url, 330 | join(constants.DOTLUNARCLIENT, 'licenses', license.file), 331 | 'text', 332 | license.sha1, 333 | 'sha1' 334 | ).catch((error) => { 335 | logger.throw(`Failed to download ${license.file}`, error); 336 | }); 337 | } 338 | } 339 | } 340 | 341 | /** 342 | * Checks the game files (and downloads if needed) 343 | * @param {Object} metadata Metadata from Lunar's API 344 | * @returns {Promise} 345 | */ 346 | export async function checkGameFiles(metadata) { 347 | logger.info(`Checking Game Files (MC ${await settings.get('version')})...`); 348 | store.commit('setLaunchingState', { 349 | title: 'LAUNCHING...', 350 | message: `CHECKING GAMEFILES (${metadata.launchTypeData.artifacts.length})...`, 351 | icon: 'fa-solid fa-file', 352 | }); 353 | 354 | if (!existsSync(join(constants.DOTLUNARCLIENT, 'offline', 'multiver'))) { 355 | await mkdir(join(constants.DOTLUNARCLIENT, 'offline', 'multiver')).catch( 356 | (error) => { 357 | logger.throw('Failed to create version folder', error); 358 | } 359 | ); 360 | } 361 | 362 | for (const index in metadata.launchTypeData.artifacts) { 363 | const artifact = metadata.launchTypeData.artifacts[index]; 364 | const gameFilePath = join( 365 | constants.DOTLUNARCLIENT, 366 | 'offline', 367 | 'multiver', 368 | artifact.name 369 | ); 370 | logger.debug( 371 | `Checking game file ${parseInt(index) + 1}/${ 372 | metadata.launchTypeData.artifacts.length 373 | }` 374 | ); 375 | 376 | if (!(await checkHash(gameFilePath, artifact.sha1, 'sha1', true))) { 377 | await downloadAndSaveFile( 378 | artifact.url, 379 | gameFilePath, 380 | 'blob', 381 | artifact.sha1, 382 | 'sha1', 383 | true, 384 | false 385 | ) 386 | .then(() => { 387 | logger.info(`Downloaded ${artifact.name} to ${gameFilePath}`); 388 | }) 389 | .catch((error) => { 390 | logger.throw(`Failed to download ${artifact.name}`, error); 391 | }); 392 | } 393 | } 394 | } 395 | 396 | /** 397 | * Checks natives (and extract if needed) 398 | * @param {object} metadata Metadata from Lunar's API 399 | * @returns {Promise} 400 | */ 401 | export async function checkNatives(metadata) { 402 | logger.info('Checking natives...'); 403 | 404 | store.commit('setLaunchingState', { 405 | title: 'LAUNCHING...', 406 | message: 'CHECKING NATIVES...', 407 | icon: 'fa-solid fa-file', 408 | }); 409 | 410 | await rm(join(constants.DOTLUNARCLIENT, 'offline', 'multiver', 'natives')) 411 | .then(() => logger.info('Deleted Natives Directory')) 412 | .catch(() => logger.info('Natives Directory does not Exist')); 413 | 414 | const artifact = metadata.launchTypeData.artifacts.find( 415 | (artifact) => artifact.type === 'NATIVES' 416 | ); 417 | if ( 418 | existsSync( 419 | join(constants.DOTLUNARCLIENT, 'offline', 'multiver', artifact.name) 420 | ) 421 | ) { 422 | await extractZip( 423 | join(constants.DOTLUNARCLIENT, 'offline', 'multiver', artifact.name), 424 | { 425 | dir: join(constants.DOTLUNARCLIENT, 'offline', 'multiver', 'natives'), 426 | } 427 | ) 428 | .then(() => { 429 | logger.debug('Extracted natives'); 430 | }) 431 | .catch((error) => { 432 | logger.throw('Failed to extract natives', error); 433 | }); 434 | } else { 435 | logger.error('Natives not found, this should not happen'); 436 | } 437 | } 438 | 439 | /** 440 | * Check engine (and download if needed) 441 | * @returns {Promise} 442 | */ 443 | export async function checkEngine() { 444 | logger.info('Checking engine...'); 445 | 446 | store.commit('setLaunchingState', { 447 | title: 'LAUNCHING...', 448 | message: 'CHECKING ENGINE...', 449 | icon: 'fa-solid fa-file', 450 | }); 451 | 452 | await verifyEngine(); 453 | } 454 | 455 | /** 456 | * Check engine config file (and download if needed) 457 | * @returns {Promise} 458 | */ 459 | export async function checkEngineConfig() { 460 | const configPath = join(constants.SOLARTWEAKS_DIR, constants.ENGINE.CONFIG); 461 | await stat(configPath).catch(async () => { 462 | logger.info('Creating config file'); 463 | await downloadAndSaveFile( 464 | constants.ENGINE.CONFIG_EXAMPLE_URL, 465 | configPath, 466 | 'text' 467 | ).catch((err) => { 468 | logger.throw('Failed to download default engine config', err); 469 | }); 470 | logger.info('Created default engine config'); 471 | }); 472 | } 473 | 474 | /** 475 | * Get the Java arguments to launch the game 476 | * @param {Object} metadata Metadata from Lunar's API 477 | * @param {string} [serverIp=null] Server IP to connect to 478 | * @param {string} [overrideVersion=null] Version to use (overrides settings) 479 | * @param {boolean} [shortcut=false] Whether or not the arguments are for a shortcut 480 | */ 481 | export async function getJavaArguments( 482 | metadata, 483 | serverIp = null, 484 | overrideVersion = null, 485 | shortcut = false 486 | ) { 487 | const natives = join( 488 | constants.DOTLUNARCLIENT, 489 | 'offline', 490 | 'multiver', 491 | 'natives' 492 | ); 493 | 494 | const args = [...metadata.jre.extraArguments]; 495 | 496 | const nativesArgument = args.findIndex((value) => value.includes('natives')); 497 | args[nativesArgument] = args[nativesArgument].replace( 498 | 'natives', 499 | `"${natives}"` 500 | ); 501 | 502 | const version = overrideVersion ?? (await settings.get('version')); 503 | 504 | // eslint-disable-next-line no-unused-vars 505 | const lunarJarFile = (filename) => 506 | `"${join(constants.DOTLUNARCLIENT, 'offline', 'multiver', filename)}"`; 507 | 508 | const gameDir = 509 | (await settings.get('launchDirectories')).find( 510 | (directory) => 511 | directory.version === 512 | (version.split('.').length === 3 513 | ? version.split('.').splice(0, 2).join('.') 514 | : version) 515 | )?.path || getDotMinecraftDirectory(); 516 | 517 | const resolution = await settings.get('resolution'); 518 | const enginePath = join(constants.SOLARTWEAKS_DIR, constants.ENGINE.ENGINE); 519 | 520 | // Make sure the engine exists, or else the game will crash (jvm init error) 521 | await stat(enginePath) 522 | .then(() => { 523 | args.push( 524 | `-javaagent:"${enginePath}"="${join( 525 | constants.DOTLUNARCLIENT, 526 | 'solartweaks', 527 | constants.ENGINE.CONFIG 528 | )}"` 529 | ); 530 | }) 531 | .catch((e) => 532 | logger.warn( 533 | `Not adding engine in arguments; ${enginePath} does not exist!`, 534 | e 535 | ) 536 | ); 537 | 538 | const classPath = metadata.launchTypeData.artifacts 539 | .filter((a) => a.type === 'CLASS_PATH') 540 | .map((artifact) => artifact.name); 541 | 542 | if (version === '1.7.10') classPath.push('OptiFine_1.7.10_HD_U_E7'); 543 | 544 | const ram = await settings.get('ram'); 545 | 546 | args.push( 547 | ...(await settings.get('jvmArguments')) 548 | .split(' ') 549 | .filter((arg) => arg?.length), 550 | `-Xmx${ram}m`, 551 | `-Xmn${ram}m`, 552 | `-Xms${ram}m`, 553 | `-Djava.library.path="${natives}"`, 554 | `-Dsolar.launchType=${shortcut ? 'shortcut' : 'launcher'}`, 555 | '-cp', 556 | classPath.join(process.platform === 'win32' ? ';' : ':'), 557 | metadata.launchTypeData.mainClass, 558 | '--version', 559 | version, 560 | '--accessToken', 561 | '0', 562 | '--assetIndex', 563 | availableVersions 564 | .find((i) => i.id == version.split('.').slice(0, 2).join('.')) 565 | .subversions.find((i) => i.id == version).assets.id, 566 | '--userProperties', 567 | '{}', 568 | '--gameDir', 569 | `"${gameDir}"`, 570 | '--assetsDir', 571 | `"${join(gameDir, 'assets')}"`, 572 | '--texturesDir', 573 | `"${join(constants.DOTLUNARCLIENT, 'textures')}"`, 574 | '--width', 575 | resolution.width, 576 | '--height', 577 | resolution.height, 578 | '--ichorClassPath', 579 | classPath.join(','), 580 | '--ichorExternalFiles', 581 | metadata.launchTypeData.artifacts 582 | .filter((a) => a.type === 'EXTERNAL_FILE') 583 | .map((artifact) => artifact.name) 584 | .join(','), 585 | '--workingDirectory', 586 | '.', 587 | '--classpathDir', 588 | join(constants.DOTLUNARCLIENT, 'offline', 'multiver'), 589 | '--installationId', 590 | await getInstallationID() 591 | ); 592 | 593 | const machineId = await _machineId().catch((err) => { 594 | logger.error('Failed to fetch Machine ID', err); 595 | }); 596 | if (machineId) args.push('--hwid', machineId); 597 | 598 | if (serverIp) args.push('--server', `"${serverIp}"`); 599 | 600 | return args.map((arg) => (!shortcut ? `${arg}`.replace(/"/g, '') : arg)); 601 | } 602 | /** 603 | * Get the HWID Private file from the .lunarclient/launcher-cache folder or make one if it doesn't exist 604 | * @returns {Promise} The HWID Private 605 | */ 606 | export async function getHWIDPrivate() { 607 | const path = join( 608 | constants.DOTLUNARCLIENT, 609 | 'launcher-cache', 610 | 'hwid-private-do-not-share' 611 | ); 612 | try { 613 | return await readFile(path, { encoding: 'utf-8' }); 614 | } catch { 615 | const chars = 616 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 617 | let hwid_private = ''; 618 | for (let i = 0; i < 512; i++) 619 | hwid_private += chars.charAt(Math.floor(Math.random() * chars.length)); 620 | await mkdir(join(constants.DOTLUNARCLIENT, 'launcher-cache'), { 621 | recursive: true, 622 | }); 623 | await writeFile(path, hwid_private, { encoding: 'utf-8' }); 624 | return hwid_private; 625 | } 626 | } 627 | /** 628 | * Get the Installation ID file from the .lunarclient/launcher-cache folder or make one if it doesn't exist 629 | * @returns {Promise} The Installation ID 630 | */ 631 | export async function getInstallationID() { 632 | const path = join( 633 | constants.DOTLUNARCLIENT, 634 | 'launcher-cache', 635 | 'installation-id' 636 | ); 637 | let installationID; 638 | try { 639 | installationID = await readFile(path, { encoding: 'utf-8' }); 640 | if (!validateUUID(installationID)) 641 | throw new Error('Invalid Installation ID, Regenerating...'); 642 | } catch { 643 | installationID = uuidv4(); 644 | await mkdir(join(constants.DOTLUNARCLIENT, 'launcher-cache'), { 645 | recursive: true, 646 | }); 647 | await writeFile(path, installationID, { encoding: 'utf-8' }); 648 | } 649 | return installationID; 650 | } 651 | /** 652 | * Launch the game 653 | * @param {Object} metadata Metadata from Lunar's API 654 | * @param {string} [serverIp=null] Server IP to connect to 655 | * @param {boolean} [debug=false] Launch in debug mode (show console) 656 | */ 657 | export async function launchGame(metadata, serverIp = null, debug = false) { 658 | store.commit('setLaunchingState', { 659 | title: 'LAUNCHING...', 660 | message: 'STARTING JVM...', 661 | icon: 'fa-solid fa-gamepad', 662 | }); 663 | 664 | const version = await settings.get('version'); 665 | const args = await getJavaArguments(metadata, serverIp).catch((error) => { 666 | store.commit('setLaunchingState', { 667 | title: 'Error', 668 | message: error.message, 669 | icon: 'fa-solid fa-exclamation-triangle', 670 | }); 671 | logger.throw('Failed to get Java Arguments', error); 672 | }); 673 | 674 | if (!args) return logger.error('No Java Arguments'); 675 | 676 | logger.debug('Launching game with args\n\n', args.join('\n')); 677 | 678 | const javaPath = join(await settings.get('jrePath'), 'java'); 679 | const proc = spawn(javaPath, args, { 680 | cwd: join(constants.DOTLUNARCLIENT, 'offline', 'multiver'), 681 | detached: true, 682 | shell: debug, 683 | }); 684 | 685 | proc.on('error', (error) => { 686 | if (error.message.includes('ENOENT') || error.message.includes('EACCES')) { 687 | proc.kill(); 688 | invalidJRE(metadata).then((valid) => { 689 | if (valid) launchGame(...arguments); 690 | }); 691 | } else logger('Game Launch Error', error); 692 | }); 693 | 694 | function commitLaunch() { 695 | updateActivity('In Game'); 696 | store.commit('setLaunchingState', { 697 | title: `LAUNCHED`, 698 | message: 'GAME IS RUNNING', 699 | icon: 'fa-solid fa-gamepad', 700 | }); 701 | store.commit('setLaunching', true); 702 | } 703 | 704 | const minecraftLogger = await createMinecraftLogger(version); 705 | logger.debug( 706 | `Created Minecraft Logger for version ${version}. Log file path: ${minecraftLogger.path}` 707 | ); 708 | proc.stdout.pipe(minecraftLogger); 709 | proc.stderr.pipe(minecraftLogger); 710 | 711 | if (debug) await remote.shell.openPath(minecraftLogger.path); 712 | 713 | proc.stdout.once('end', () => { 714 | updateActivity('In the launcher'); 715 | store.commit('setLaunchingState', { 716 | title: `LAUNCH ${version}`, 717 | message: 'READY TO LAUNCH', 718 | icon: 'fa-solid fa-gamepad', 719 | }); 720 | store.commit('setLaunching', false); 721 | if (debug) return; 722 | remote.getCurrentWindow().show(); 723 | connectRPC(); 724 | }); 725 | 726 | async function waitForLaunch(data) { 727 | if (!data.toString('utf8').includes('Starting game!')) return; 728 | proc.stdout.removeListener('data', waitForLaunch); 729 | await new Promise((res) => setTimeout(res, 3000)); 730 | commitLaunch(); 731 | if (debug) return; 732 | await disableRPC(); 733 | switch (await settings.get('actionAfterLaunch')) { 734 | case 'close': 735 | default: 736 | remote.getCurrentWindow().close(); 737 | break; 738 | case 'hide': 739 | remote.getCurrentWindow().hide(); 740 | break; 741 | case 'keep': 742 | break; 743 | } 744 | } 745 | 746 | proc.stdout.on('data', waitForLaunch); 747 | 748 | proc.stdout.on('error', (error) => 749 | logger.throw('Failed to launch game', error) 750 | ); 751 | 752 | proc.stderr.on('error', (error) => 753 | logger.throw('Failed to launch game', error) 754 | ); 755 | } 756 | 757 | /** 758 | * Run all the checks and launch the game 759 | * @param {string} [serverIp=null] Server IP to connect to 760 | */ 761 | export async function checkAndLaunch(serverIp = null) { 762 | store.commit('setLaunching', true); 763 | updateActivity('In the launcher', 'Launching game'); 764 | 765 | const version = await settings.get('version'); 766 | const skipChecks = await settings.get('skipChecks'); 767 | 768 | let success = true; 769 | 770 | function error(action, err) { 771 | success = false; 772 | store.commit('setErrorMessage', `${action} Error: ` + (err.stack ?? err)); 773 | store.commit('setErrorModal', true); 774 | logger.throw(`Failed to ${action}`, err); 775 | } 776 | 777 | // Fetching metadata 778 | const metadata = await fetchMetadata().catch((err) => 779 | error('Fetch Metadata', err) 780 | ); 781 | 782 | if (!metadata) return logger.error('No Metadata for Launch'); 783 | 784 | if (!skipChecks) { 785 | // Check JRE 786 | if (!(await checkJRE(metadata).catch((err) => error('Check JRE', err)))) 787 | return; 788 | 789 | // Check game directory 790 | await setupLunarClientDirectory().catch((err) => 791 | error('Setup .lunarclient', err) 792 | ); 793 | 794 | // Check licenses 795 | await checkLicenses(metadata).catch((err) => error('Check Licenses', err)); 796 | 797 | // Check game files 798 | await checkGameFiles(metadata).catch((err) => 799 | error('Check Game Files', err) 800 | ); 801 | 802 | // Check natives 803 | await checkNatives(metadata).catch((err) => error('Check Natives', err)); 804 | 805 | // Check LC assets 806 | await downloadLunarAssets(metadata).catch((err) => 807 | error('Check LC Assets', err) 808 | ); 809 | 810 | // Engine config 811 | await checkEngineConfig().catch((err) => error('Check Engine Config', err)); 812 | 813 | // Check engine 814 | await checkEngine().catch((err) => error('Check Engine', err)); 815 | } 816 | 817 | if (!success) { 818 | remote.getCurrentWindow().setProgressBar(-1); 819 | store.commit('setLaunching', false); 820 | return store.commit('setLaunchingState', { 821 | title: `LAUNCH ${version}`, 822 | message: 'READY TO LAUNCH', 823 | icon: 'fa-solid fa-gamepad', 824 | }); 825 | } 826 | 827 | // Launch game 828 | await launchGame(metadata, serverIp, await settings.get('debugMode')).catch( 829 | (err) => error('Launch Game', err) 830 | ); 831 | 832 | // Trackers 833 | await axios 834 | .post(`${constants.API_URL}${constants.ENDPOINTS.LAUNCH}`, { 835 | item: 'launcher', 836 | version, 837 | }) 838 | .catch((error) => 839 | logger.warn( 840 | "Failed to track launcher launch, ignoring it, it's not important.", 841 | error 842 | ) 843 | ); 844 | } 845 | -------------------------------------------------------------------------------- /src/javascript/settings.js: -------------------------------------------------------------------------------- 1 | import settings from 'electron-settings'; 2 | import { existsSync } from 'fs'; 3 | import { readdir } from 'fs/promises'; 4 | import { join } from 'path'; 5 | import * as process from 'process'; 6 | import { platform } from 'process'; 7 | import constants from '../constants'; 8 | import Logger from './logger'; 9 | const logger = new Logger('settings'); 10 | 11 | /** 12 | * Setup settings for the application. 13 | * Also check if the settings need to be reseted to default. 14 | */ 15 | export default async function setupSettings() { 16 | logger.info(`Setting up settings at file path ${settings.file()}...`); 17 | 18 | // User's submitted servers 19 | if (!(await settings.has('servers'))) 20 | await settings.set('servers', defaultSettings.servers); 21 | 22 | // User's selected version 23 | if (!(await settings.has('version'))) 24 | await settings.set('version', defaultSettings.version); 25 | 26 | if (!(await settings.has('module'))) 27 | await settings.set('module', defaultSettings.module); 28 | 29 | // User's selected launch directories 30 | if (!(await settings.has('launchDirectories'))) 31 | await settings.set('launchDirectories', defaultSettings.launchDirectories); 32 | else { 33 | const directories = await settings.get('launchDirectories'); 34 | 35 | for (const directory of directories) { 36 | if (directory.version.split('.').length === 3) { 37 | directory.version = directory.version.split('.').splice(0, 2).join('.'); 38 | } 39 | } 40 | for (const directory of directories) { 41 | if (directories.filter((i) => i.version == directory.version).length > 1) 42 | directories.splice(directories.indexOf(directory), 1); 43 | } 44 | 45 | const versions = defaultSettings.launchDirectories.map((i) => i.version); 46 | for (const version of versions) { 47 | if (!directories.find((d) => d.version === version)) 48 | directories.push( 49 | defaultSettings.launchDirectories.find((i) => i.version === version) 50 | ); 51 | } 52 | 53 | await settings.set('launchDirectories', directories); 54 | } 55 | 56 | // User's selected ram 57 | if (!(await settings.has('ram'))) 58 | await settings.set('ram', defaultSettings.ram); 59 | 60 | // User's selected resolution 61 | if (!(await settings.has('resolution'))) 62 | await settings.set('resolution', defaultSettings.resolution); 63 | 64 | // User's selected action after launch 65 | if (!(await settings.has('actionAfterLaunch'))) 66 | await settings.set('actionAfterLaunch', defaultSettings.actionAfterLaunch); 67 | 68 | // User's custom JVM arguments 69 | if (!(await settings.has('jvmArguments'))) 70 | await settings.set('jvmArguments', defaultSettings.jvmArguments); 71 | 72 | // User's selected JRE Path 73 | if (!(await settings.has('jrePath')) || (await settings.get('jrePath')) == '') 74 | await settings.set('jrePath', await getDefaultJREPath()); 75 | 76 | // Launch in debug mode 77 | if (!(await settings.has('debugMode'))) 78 | await settings.set('debugMode', defaultSettings.debugMode); 79 | 80 | // Skip launch checks 81 | if (!(await settings.has('skipChecks'))) 82 | await settings.set('skipChecks', defaultSettings.skipChecks); 83 | 84 | // Downloaded JREs 85 | if (!(await settings.has('downloadedJres'))) 86 | await settings.set( 87 | 'downloadedJres', 88 | ( 89 | await readdir(join(constants.SOLARTWEAKS_DIR, 'jres'), { 90 | withFileTypes: true, 91 | }) 92 | ) 93 | .filter((dirent) => dirent.isDirectory()) 94 | .map((dirent) => dirent.name) 95 | ); 96 | 97 | // Starred Servers (Quick Launch Menu) 98 | if (!(await settings.has('starredServers'))) 99 | await settings.set('starredServers', defaultSettings.starredServers); 100 | 101 | // App Theme 102 | if (!(await settings.has('theme'))) 103 | await settings.set('theme', defaultSettings.theme); 104 | 105 | // Tutorial Showed 106 | if (!(await settings.has('shownTutorial'))) 107 | await settings.set('shownTutorial', defaultSettings.shownTutorial); 108 | 109 | logger.info(`Settings Setup at ${settings.file()}`); 110 | } 111 | 112 | export function getDotMinecraftDirectory() { 113 | switch (platform) { 114 | case 'win32': 115 | return join(process.env.APPDATA, '.minecraft'); 116 | case 'darwin': 117 | return join( 118 | process.env.HOME, 119 | 'Library', 120 | 'Application Support', 121 | 'minecraft' 122 | ); 123 | case 'linux': 124 | return join(process.env.HOME, '.minecraft'); 125 | default: 126 | break; 127 | } 128 | } 129 | 130 | export async function getDefaultJREPath() { 131 | const LCJresPath = join( 132 | platform === 'win32' ? process.env.USERPROFILE : process.env.HOME, 133 | '.lunarclient', 134 | 'jre' 135 | ); 136 | const dir1 = await readdir(LCJresPath); 137 | if (!dir1) { 138 | logger.warn( 139 | 'Please run normal Lunar Client to setup `jre` folder inside `.lunarclient`' 140 | ); 141 | return ''; 142 | } 143 | const jreName = dir1.find((i) => !i.includes('.')); 144 | if (!jreName) return ''; 145 | const dir2 = join(LCJresPath, jreName); 146 | if (!dir2) return ''; 147 | const dir3 = join( 148 | dir2, 149 | (await readdir(dir2))?.find((i) => i.startsWith('zulu')) || '' 150 | ); 151 | if (!dir3 || dir3 == dir2) return ''; 152 | if (existsSync(join(dir3, 'bin'))) { 153 | return join(dir3, 'bin'); 154 | } else if (existsSync(join(dir3, 'Contents'))) { 155 | return join(dir3, 'Contents/Home/bin'); 156 | } else return ''; 157 | } 158 | 159 | export const defaultSettings = { 160 | servers: [ 161 | { name: 'Hypixel', ip: 'hypixel.net', background: 7 }, 162 | { name: 'Minemen Club', ip: 'na.minemen.club', background: 3 }, 163 | { name: 'Lunar Network', ip: 'lunar.gg', background: 1 }, 164 | { name: 'ViperMC', ip: 'play.vipermc.net', background: 5 }, 165 | { name: 'BWHub', ip: 'bwhub.net', background: 4 }, 166 | ], 167 | version: '1.8.9', 168 | module: 'lunar', 169 | launchDirectories: [ 170 | { version: '1.7', path: getDotMinecraftDirectory() }, 171 | { version: '1.8', path: getDotMinecraftDirectory() }, 172 | { version: '1.12', path: getDotMinecraftDirectory() }, 173 | { version: '1.16', path: getDotMinecraftDirectory() }, 174 | { version: '1.17', path: getDotMinecraftDirectory() }, 175 | { version: '1.18', path: getDotMinecraftDirectory() }, 176 | { version: '1.19', path: getDotMinecraftDirectory() }, 177 | ], 178 | ram: 2048, 179 | resolution: { 180 | width: 854, 181 | height: 480, 182 | }, 183 | actionAfterLaunch: 'close', 184 | jvmArguments: '-XX:+DisableAttachMechanism', 185 | jrePath: '', 186 | debugMode: false, 187 | skipChecks: false, 188 | starredServers: [], 189 | theme: 'dark', 190 | shownTutorial: false, 191 | }; 192 | -------------------------------------------------------------------------------- /src/javascript/updater.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { execSync } from 'child_process'; 3 | import { remote } from 'electron'; 4 | import { unlink } from 'fs/promises'; 5 | import { join } from 'path'; 6 | import { platform } from 'process'; 7 | import constants from '../constants'; 8 | import { downloadAndSaveFile } from './downloader'; 9 | import Logger from './logger'; 10 | 11 | const logger = new Logger('updater'); 12 | 13 | /** 14 | * Checks for updates (for the launcher) 15 | */ 16 | export async function checkForUpdates() { 17 | if (platform !== 'win32') return; 18 | 19 | logger.info('Checking for updates...'); 20 | const release = await axios 21 | .get(`${constants.API_URL}${constants.UPDATERS.INDEX}`) 22 | .catch((reason) => { 23 | logger.throw('Failed to fetch updater index', reason); 24 | }); 25 | 26 | const launcherVer = parseInt(remote.app.getVersion().replace(/[^0-9]+/g, '')); 27 | const latestVer = parseInt( 28 | release.data.index.stable.launcher.replace(/[^0-9]+/g, '') 29 | ); 30 | 31 | if (launcherVer < latestVer) { 32 | logger.info( 33 | `Launcher is out of date. Latest version is ${release.data.index.stable.launcher}` 34 | ); 35 | 36 | const choice = await remote.dialog.showMessageBox({ 37 | type: 'question', 38 | title: 'Update available', 39 | message: `A new version of the launcher is available.\n\nCurrent version: ${remote.app.getVersion()}\nLatest version: ${ 40 | release.data.index.stable.launcher 41 | }\n\nWould you like to update now?`, 42 | buttons: ['Later', 'Update'], 43 | }); 44 | 45 | if (choice.response !== 1) return; // Later or closed 46 | 47 | remote.dialog.showMessageBox({ 48 | type: 'info', 49 | title: 'Downloading update...', 50 | message: 51 | 'Downloading update in the background. Please wait.\n\nThis may take a while depending on your internet speed. You can close this window and use the launcher, we will notify you when the update is ready.', 52 | }); 53 | 54 | const updateReady = async () => { 55 | const choice2 = await remote.dialog.showMessageBox({ 56 | type: 'question', 57 | title: 'Update ready', 58 | message: `The update is ready to be installed.`, 59 | buttons: ['Cancel update', 'Install update'], 60 | }); 61 | 62 | if (choice2.response === 1) return true; 63 | else return false; // Cancel update or closed 64 | }; 65 | 66 | if (platform === 'win32') { 67 | const filename = `launcher-${release.data.index.stable.launcher}-update-temp.exe`; 68 | const filePath = join(constants.SOLARTWEAKS_DIR, filename); 69 | 70 | await downloadAndSaveFile( 71 | `${constants.API_URL}${constants.UPDATERS.LAUNCHER.replace( 72 | '{version}', 73 | release.data.index.stable.launcher 74 | )}`, 75 | filePath, 76 | 'blob' 77 | ); 78 | 79 | if (!(await updateReady())) return; 80 | 81 | execSync(filePath); 82 | 83 | await unlink(filePath); 84 | } 85 | 86 | remote.app.quit(); 87 | } else logger.info('Launcher up to date'); 88 | } 89 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { remote as electron } from 'electron'; 2 | import settings from 'electron-settings'; 3 | import { createApp } from 'vue'; 4 | import App from './App.vue'; 5 | import constants from './constants'; 6 | import store from './store'; 7 | 8 | import * as Sentry from '@sentry/vue'; 9 | 10 | import Cache from './javascript/cache'; 11 | export const cache = new Cache(); 12 | 13 | const app = createApp(App).use(store).mount('#app'); 14 | 15 | if (electron.app.isPackaged) { 16 | console.log('App is packaged, initializing Sentry...'); 17 | Sentry.init({ 18 | app, 19 | dsn: constants.SENTRY, 20 | // Set tracesSampleRate to 1.0 to capture 100% 21 | // of transactions for performance monitoring. 22 | // We recommend adjusting this value in production 23 | tracesSampleRate: 1.0, 24 | async beforeSend(event) { 25 | return (await settings.get('SentryEnabled')) ? event : null; 26 | }, 27 | attachStacktrace: true, 28 | release: electron.app.getVersion(), 29 | ignoreErrors: [/AxiosError: Network Error/i], 30 | logErrors: true, 31 | }); 32 | } else console.log('App is not packaged, not initializing Sentry.'); 33 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | 3 | const store = createStore({ 4 | state: { 5 | activeTab: 'Home', 6 | playContainerHeight: 300, 7 | isLaunching: false, 8 | launchingState: { 9 | title: 'LAUNCH', 10 | message: 'READY TO LAUNCH', 11 | icon: 'fa-solid fa-gamepad', 12 | }, 13 | isShowingTutorial: false, 14 | showModal: false, 15 | errorMessage: '', 16 | }, 17 | 18 | getters: { 19 | getActiveTab: (state) => state.activeTab, 20 | getPlayContainerHeight: (state) => state.playContainerHeight, 21 | isLaunching: (state) => state.isLaunching, 22 | getLaunchingState: (state) => state.launchingState, 23 | }, 24 | 25 | mutations: { 26 | setActiveTab(state, tab) { 27 | state.activeTab = tab; 28 | }, 29 | setPlayContainerHeight(state, height) { 30 | state.playContainerHeight = height; 31 | }, 32 | setLaunching(state, isLaunching) { 33 | state.isLaunching = isLaunching; 34 | }, 35 | setLaunchingState(state, launchingState) { 36 | state.launchingState = launchingState; 37 | }, 38 | setTutorialState(state, isShowingTutorial) { 39 | state.isShowingTutorial = isShowingTutorial; 40 | }, 41 | setErrorModal(state, showModal) { 42 | state.showModal = showModal; 43 | }, 44 | setErrorMessage(state, errorMessage) { 45 | state.errorMessage = errorMessage; 46 | }, 47 | }, 48 | }); 49 | 50 | export default store; 51 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pluginOptions: { 3 | electronBuilder: { 4 | nodeIntegration: true, 5 | outputDir: 'dist', 6 | builderOptions: { 7 | appId: 'com.solartweaks.launcher', 8 | productName: 'Solar Tweaks', 9 | win: { 10 | target: 'nsis', 11 | icon: 'build/icons/win/icon.ico', 12 | publisherName: 'Solar Tweaks', 13 | verifyUpdateCodeSignature: true, 14 | requestedExecutionLevel: 'asInvoker', 15 | }, 16 | nsis: { 17 | oneClick: true, 18 | installerIcon: 'build/icons/win/icon.ico', 19 | uninstallerIcon: 'build/icons/win/icon.ico', 20 | installerHeaderIcon: 'build/icons/win/icon.ico', 21 | runAfterFinish: true, 22 | }, 23 | linux: { 24 | target: 'AppImage', 25 | maintainer: 'Solar Tweaks', 26 | vendor: 'Solar Tweaks', 27 | icon: 'build/icons/linux/1024x1024.png', 28 | synopsis: 'Solar Tweaks', 29 | description: 'Solar Tweaks', 30 | category: 'Game', 31 | }, 32 | mac: { 33 | category: 'Game', 34 | target: 'dmg', 35 | icon: 'build/icons/macos/icon.icns', 36 | }, 37 | }, 38 | }, 39 | }, 40 | }; 41 | --------------------------------------------------------------------------------