├── .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 | 
2 |
3 | # Solar Tweaks
4 |
5 | 
6 | 
7 | 
8 | 
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 |
34 |
35 |
58 |
67 |
68 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
70 | properly without JavaScript enabled. Please enable it to continue.
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
19 |
20 |
21 |
27 |
28 |
89 |
--------------------------------------------------------------------------------
/src/components/Card/CardItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
23 |
24 |
66 |
--------------------------------------------------------------------------------
/src/components/Content.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/Content/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | DOWNLOAD LOGS
14 |
15 |
16 |
21 |
22 | Solar Tweaks is a modification tool for Lunar Client. By modifying
23 | the game's code when it gets loaded, we add a variety of features
24 | that enhance your Minecraft experience. Browse and configure our
25 | Modules to your own needs under the Engine tab, launch with a single
26 | click, and enjoy a new fully improved Lunar Client.
27 |
28 | We are not affiliated with "Mojang, AB" or "Moonsworth, LLC". We are
29 | just a bunch of people that love Lunar Client and want to make it
30 | even better.
31 |
32 |
33 |
34 |
40 |
44 |
45 | GITHUB DISCUSSIONS
46 |
47 |
48 |
54 |
55 |
56 | OPEN ST DIRECTORY
57 |
58 |
59 |
65 |
69 |
70 | LUNAR CLIENT
71 |
72 |
73 |
79 |
86 | {{ SentryEnabled ? 'DISABLE' : 'ENABLE' }} LOGGING
87 |
88 |
89 |
90 |
91 |
92 |
101 | Main Developers
102 |
103 |
113 |
114 |
115 | View on Github
116 |
117 |
118 |
119 | Other Helpers
120 |
121 |
131 |
132 |
133 | View on Github
134 |
135 |
136 |
137 | Translators
138 |
139 |
148 |
149 |
150 | View on Github
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
323 |
324 |
433 |
--------------------------------------------------------------------------------
/src/components/Content/Debug.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 | Clear Settings
12 | Open Settings File
13 |
14 | Disable Developer Mode
15 |
16 |
17 |
18 |
19 | Reset Sentry Notif
20 |
21 |
22 | Disable Theme Swapper
23 |
24 |
25 | Reset Shown Tutorial
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
78 |
79 |
86 |
87 |
125 |
--------------------------------------------------------------------------------
/src/components/Content/Engine.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
15 |
{{ menu.module.displayName }}
16 |
17 |
18 | {{ menu.module.description }}
19 |
20 |
21 |
22 |
27 |
32 | {{ option.displayName }}:
33 |
34 |
46 |
58 |
59 |
60 |
61 |
65 |
66 |
67 |
72 |
Command Aliases
73 |
74 |
75 | Create aliases for commands in-gameNOTE: Aliases (the text boxes on the left) cannot contain spaces or
78 | any other characters that are disallowed in commands
80 |
81 |
82 |
130 |
131 |
132 |
{{ inDepth.displayName }}
133 |
{{ inDepth.description }}
134 |
135 |
136 |
142 |
143 |
148 |
149 |
{{ item.displayName }}
150 |
155 | OPTIONS
156 |
157 |
165 | {{ item.enabled ? 'ENABLED' : 'DISABLED' }}
166 |
167 |
168 |
172 |
173 |
178 |
179 |
Command Aliases
180 |
181 | EDIT
182 |
183 |
184 |
185 |
186 |
187 |
435 |
436 |
898 |
--------------------------------------------------------------------------------
/src/components/Content/ErrorModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
40 |
41 |
147 |
--------------------------------------------------------------------------------
/src/components/Content/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
27 |
28 |
36 |
37 |
44 | {{ post.description }}
45 |
46 |
47 |
48 |
49 |
54 | {{ post.author }}
55 |
56 |
57 |
58 | {{
60 | post.button ? post.button.text : 'Read more'
61 | }}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
183 |
184 |
390 |
--------------------------------------------------------------------------------
/src/components/Content/Servers.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
17 |
18 |
19 | {{ server.name }}
20 | Players Online: {{ server.playerCount }}
22 |
23 | IP: {{ server.ip }}
25 |
26 | Status: {{ server.status }}
28 |
29 |
30 |
31 |
36 |
37 |
38 |
39 |
40 |
41 |
48 |
49 |
53 |
54 |
55 |
56 |
57 |
83 |
84 |
85 |
86 |
261 |
262 |
463 |
--------------------------------------------------------------------------------
/src/components/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
32 |
82 |
83 |
150 |
--------------------------------------------------------------------------------
/src/components/SentryNotification.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
PRIVACY POLICY
4 |
5 | We use a new debug log system to track errors and implement fixes as fast
6 | as we can. Do you agree to this feature?
7 |
8 |
9 | I AGREE
10 |
11 |
12 | DISABLE
13 |
14 |
15 |
16 |
17 |
44 |
45 |
107 |
--------------------------------------------------------------------------------
/src/components/TitleBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
17 |
18 |
23 | {{ link.name }}
24 |
25 |
26 |
27 |
28 |
45 |
46 |
47 |
48 |
49 |
50 |
182 |
183 |
309 |
--------------------------------------------------------------------------------
/src/components/Tutorial.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
Select a Language
13 |
18 |
23 | {{ language }}
24 |
25 |
26 |
27 | Done
28 |
29 |
30 |
31 |
35 |
36 | {{ availableStages[selectedLanguage][currentStage].title }}
37 |
38 |
39 | {{ availableStages[selectedLanguage][currentStage].text }}
40 |
41 |
42 |
Back
43 |
44 |
49 |
50 |
51 | {{
52 | currentStage === availableStages[selectedLanguage].length - 1
53 | ? 'Complete'
54 | : 'Next'
55 | }}
56 |
57 |
58 |
59 |
60 |
63 |
64 |
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 |
--------------------------------------------------------------------------------