Compare commits

...

97 Commits

Author SHA1 Message Date
bc1c592f41
Merge remote-tracking branch 'upstream/develop' into develop
Signed-off-by: sneedium <sneed@sneedmc.org>
2024-03-24 01:33:10 -04:00
Lenny McLennington
ffcb0c3f41
Merge pull request #1635 from Nik-mmzd/asdf-support
Improve SDKMAN support; add ASDF support
2024-01-11 22:44:29 +00:00
McModder
02e6667761
Improve SDKMAN Java installations support; introduce ASDF Java installations auto-detection
Signed-off-by: McModder <me@modder.pw>
2024-01-09 17:38:51 +03:00
Lenny McLennington
3f54396705
Merge pull request #1627 from HeyaGlitz/develop
Make Christmas cat last longer
2024-01-07 05:10:36 +00:00
Lenny McLennington
b72d486e84
Merge pull request #1633 from PolyMC/fix-instance-import-fr-this-time
Fix instance import fr this time
2024-01-06 16:12:35 +00:00
Lenny McLennington
ba2fba10c2
Merge pull request #1631 from HeyaGlitz/sdkmandetect
Make Java auto-detect find sdkman Java installations
2024-01-06 13:51:12 +00:00
Lenny McLennington
aeac0eb9ba
Merge pull request #1629 from HeyaGlitz/storagelegendfix
Fix storage page chart legend not respecting theme
2024-01-06 13:49:44 +00:00
Lenny McLennington
0a9801fb20
Fix instance import fr this time 2024-01-06 13:45:00 +00:00
HeyaGlitz
5e22009554 Fix Java auto-detect so it finds sdkman
Signed-off-by: HeyaGlitz <30732772+HeyaGlitz@users.noreply.github.com>
2024-01-05 15:42:42 +01:00
HeyaGlitz
87ea20df5a Fix storage page chart legend not respecting theme
Signed-off-by: HeyaGlitz <heyaglitz@waifu.club>
2024-01-02 00:20:50 +01:00
HeyaGlitz
0919630ed6 Make Christmas cat last longer
The cat will now last until January 6th, the actual end of Christmas

Signed-off-by: HeyaGlitz <30732772+HeyaGlitz@users.noreply.github.com>
2023-12-26 00:01:16 +01:00
Lenny McLennington
bce5a8df35
Merge pull request #1622 from PolyMC/consistent-uuids
Create offline mode accounts with consistent Uuids
2023-12-17 10:55:44 +00:00
Lenny McLennington
24c471de88
Merge pull request #1621 from PolyMC/fix-msa-login
Fix MSA profile fetching
2023-12-17 10:54:51 +00:00
Lenny McLennington
ff74d0d7ad
Create offline mode accounts with consistent Uuids
Uses basically what already exists in Qt, just modified so I can specify
an arbitrary length byte array for the namespace. Not really sure if it
needs to be the same as the way Minecraft generates it or if I could've
just used Uuid::createUuidV3. It would've still been consistent either
way, but whatever, this works.

Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-12-09 14:28:08 +00:00
Lenny McLennington
94b01a5f5c
Fix MSA profile fetching
This reverts commit 290ccebc29 for
launcher/minecraft/auth/steps/MinecraftProfileStep.cpp.

Oops.

Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-12-09 13:38:46 +00:00
Lenny McLennington
385eaea709
Merge pull request #1616 from LennyMcLennington/fix-hang
fix hang when importing certain modpacks
2023-11-25 02:03:38 +00:00
Lenny McLennington
6431265a73
Merge pull request #1598 from Kaydax/authlib
Add support for authlibinjector
2023-11-25 02:03:22 +00:00
Lenny McLennington
f89a87e28a
Merge pull request #1615 from LennyMcLennington/relax-minimum-memory
don't set arbitrary minimum for jvm memory
2023-11-15 18:00:50 +00:00
Kaydax
53a4395d5b Fix Yggdrasil error messages when not using Mojang
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-11-12 23:37:35 +00:00
Lenny McLennington
290ccebc29 feature: Add support for authlibinjector
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-11-12 23:37:35 +00:00
Lenny McLennington
e2875ef9a5 fix hang when importing certain modpacks
Modified findFolderOfFileInZip to use a breadth-first search and support
searching for multiple file names in one pass. This should prevent or at
least make it less likely to hang while importing certain packs.

Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-11-12 23:34:14 +00:00
Lenny McLennington
d380c07d62 don't set arbitrary minimum for jvm memory
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-11-12 23:06:53 +00:00
Lenny McLennington
c4d68b7ccf
Merge pull request #1612 from PolyMC/fix-qt-6-6-0-build
Fix build on Qt 6.6.0
2023-11-08 04:03:40 +00:00
swirl
c0ca8d21ca
Fix build on Qt 6.6.0
Closes #1611

Signed-off-by: swirl <swurl@swurl.xyz>
2023-11-07 21:32:41 -05:00
Lenny McLennington
7dff3e5afc
Merge pull request #1608 from jdpatdiscord/develop
Allow compilation on 32-bit
2023-10-19 10:44:36 +01:00
jdp_
93d868230f Allow compilation on 32-bit 2023-10-18 22:12:54 -04:00
Lenny McLennington
9bc2873b89
Merge pull request #1605 from Spongecade/develop
Update Minecraft Wiki links to new domain
2023-09-28 16:02:04 +01:00
Spongecade
1471d64d11 Update Minecraft Wiki links to new domain 2023-09-28 14:26:36 +00:00
Lenny McLennington
6a06bcc4ea
Merge pull request #1587 from HeyaGlitz/develop
Remove sponsors and CF API key terms
2023-09-06 05:24:52 +01:00
Lenny McLennington
b995074440
Merge pull request #1595 from LennyMcLennington/drm-flag
Add CMake option to enable stricter DRM
2023-08-10 11:31:46 +01:00
HeyaGlitz
a6089296ce
Remove key usage notice from CMakeLists
Signed-off-by: HeyaGlitz <30732772+HeyaGlitz@users.noreply.github.com>
2023-06-25 13:13:33 +02:00
HeyaGlitz
641f687043
Remove sponsors and CF API key terms
Signed-off-by: HeyaGlitz <30732772+HeyaGlitz@users.noreply.github.com>
2023-06-25 13:03:51 +02:00
Lenny McLennington
19673a7d59
Add CMake option to enable stricter DRM 2023-06-09 00:35:41 +01:00
Lenny McLennington
2358e6faa2
Merge pull request #1581 from HeyaGlitz/develop
Update to Java 8
2023-05-30 14:44:03 +01:00
HeyaGlitz
8e842d1b6e
Update to Java 8
Signed-off-by: HeyaGlitz <heyaglitz0x99@proton.me>
2023-05-30 07:45:08 +02:00
Lenny McLennington
9cf31f2c9b
Merge pull request #1576 from jdpatdiscord/develop
Fix bug, fix more warnings (See commit description)
2023-05-09 03:48:34 +01:00
jdp_
eabd225b06 Fix bug, fix more warnings (See commit description)
This ends my series of patches fixing warnings throughout the codebase. Now, there are NO warnings except for unused parameters, tested on GCC 13.1.1 and Clang 16.0.2 with -Wall -Wextra.

Fixed dark mode crashing on Windows 8.1, 8 and 7, and removed a need for an export by moving in the function.

Any people looking at the Windows code and asking why I didn't use official version querying API's, those typically have some sort of unwanted behavior, plus checking exports to determine versions is shorter.
2023-05-08 22:08:07 -04:00
Lenny McLennington
549cfa8833
Merge pull request #1575 from jdpatdiscord/develop
Clean up codebase and fix bug
2023-05-07 11:32:11 +01:00
jdp_
9cb6200081 Cleanup codebase
for FileSystem.cpp:
Instead of checking if Linux or FreeBSD, check if its not Windows and not OSX. Chances are other operating systems run a DE that adheres to the XDG Desktop standard (.desktop). The check isn't good enough anyways since alternative shells for Windows exist, it will never be an accurate check. In any case this function is unused.

WorldListPage.cpp:
Redo confusing switch statement plagued with fall throughs, now well defined.

LaunchController.cpp:
Remove cringe. Also fix warning and make the unimplemented case(s) more explicit.

VersionProxyModel.cpp:
Add fallthrough for warning suppression.

WorldListPage.cpp:
redo `mceditState`

TranslationsModel.cpp:
Move up definition of `column` variable to when it is needed, clear up switch cases

FlameInstanceCreationTask.cpp:
Fallthrough intentionally

SkinUpload.cpp:
Make `getVariant`

ResourcePack.cpp:
Add new values for 1.19.3+

meta/Index.cpp:
Make clear switch statement behavior

JavaWizardPage.cpp:
Fix case fallthrough

Yggdrasil.cpp:
Fix case fallthrough

AccountList.cpp:
Fix case fallthrough,

WinDarkmode.cpp:
Add an explanation and fix warnings due to FARPROC casts.

Signed-off-by: jdp_ <42700985+jdpatdiscord@users.noreply.github.com>
2023-05-07 06:20:16 -04:00
jdp_
1f4d9cc12f Avoid possible conflicts with C++20 requires keyword
from -Wc++20-compat
2023-05-07 01:26:15 -04:00
jdp_
4eb9085e71 Fix pack updating & Modrinth overrides
The mechanism that both pack updating and Modrinth overrides use utilize std::filesystem::copy, which with GCC's libstdc++ has a bug on Windows where `overwrite_existing` isn't obeyed. In addition, made it clear what `overrideFolder` does by renaming it and rewriting an error message.
2023-05-07 01:06:30 -04:00
glowiak
2a9bb95e1f
Fix compile error on FreeBSD (#1560)
* Update VerifyJavaInstall.cpp

Signed-off-by: glowiak <52356948+glowiak@users.noreply.github.com>

* Update VerifyJavaInstall.cpp

Signed-off-by: glowiak <52356948+glowiak@users.noreply.github.com>

* Update VerifyJavaInstall.cpp

Signed-off-by: glowiak <52356948+glowiak@users.noreply.github.com>

* Update VerifyJavaInstall.cpp

Signed-off-by: glowiak <52356948+glowiak@users.noreply.github.com>

* Update VerifyJavaInstall.cpp

Signed-off-by: glowiak <52356948+glowiak@users.noreply.github.com>

---------

Signed-off-by: glowiak <52356948+glowiak@users.noreply.github.com>
2023-02-26 16:14:04 +00:00
Lenny McLennington
9b41a3bd48
Merge pull request #1555 from LennyMcLennington/fix-windows-builds-again-for-real-this-time
fix: change windows legacy builds to 64 bit
2023-02-19 05:16:39 +00:00
Lenny McLennington
e4211fea1a
fix: change windows legacy builds to 64 bit
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-19 05:15:49 +00:00
Lenny McLennington
a7d8cc6605
Merge pull request #1550 from PolyMC/bump-6.0
chore: bump version to 6.0
2023-02-18 06:07:27 +00:00
Lenny McLennington
9ee8f019e1
Merge pull request #1552 from PolyMC/fix_copy
Fix instance copy on Linux and macOS
2023-02-18 06:03:28 +00:00
flow
b4029553d4
chore(tests): add test for FS copy with dot folders/files
Signed-off-by: flow <flowlnlnln@gmail.com>
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-16 05:01:47 +00:00
flow
3db09c5466
fix: include hidden files when copying instances
fixes instance ccopy on linux .-.

Signed-off-by: flow <flowlnlnln@gmail.com>
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-16 05:01:47 +00:00
Lenny McLennington
3f2af124e7
chore: bump version to 6.0
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-15 03:23:00 +00:00
Lenny McLennington
403f8a7d96
Merge pull request #1541 from KaspianDev/develop
Clean & Compress Floppas
2023-02-14 22:34:51 +00:00
Lenny McLennington
94e3b95b05
Merge pull request #1548 from LennyMcLennington/fix/mr-list-text-layout
fix: Make modrinth text layout not be weird
2023-02-14 22:34:32 +00:00
Lenny McLennington
252da9a3dc
Merge pull request #1526 from Doggermelon/copy-single-file
fix: handle single files in copy tasks
2023-02-10 02:25:22 +00:00
Lenny McLennington
4398cb5dc5
Merge pull request #1546 from LennyMcLennington/fix/dont-unzip-invalid-filenames
fix(MMCZip): ignore invalid file paths in extractSubDir
2023-02-10 01:17:49 +00:00
Lenny McLennington
9f457e0ce6
fix(MMCZip): ignore invalid file paths in extractSubDir
Ignores files that have an absolute path or a path beginning with ..

Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-09 20:04:24 +00:00
Lenny McLennington
dab0dc451c
fix: modrinth text layout is no longer weird
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-09 19:36:21 +00:00
Kaspian
cdcc7d0f16 Clear more pixels 2023-02-05 10:18:48 +01:00
Kaspian
71d4e66c32 Fix & Compress Floppas 2023-02-04 21:20:43 +01:00
Lenny McLennington
7b547c842c
Merge pull request #1539 from LennyMcLennington/fix-windows-builds-for-real
Fix Windows Qt6 builds
2023-02-04 15:31:19 +00:00
Lenny McLennington
03552edbb7
Fix Windows Qt6 builds
- Fix warning about no return on main
- Fix program not starting when built with Qt6

Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-04 15:31:00 +00:00
Lenny McLennington
01b52ea6be
Merge pull request #1537 from LennyMcLennington/security-fix
fix(ModrinthInstanceCreationTask): ignore files with invalid paths
2023-02-04 12:20:56 +00:00
Lenny McLennington
c16cc2d8bb
Merge pull request #1538 from LennyMcLennington/fix-windows-build
fix windows ci builds
2023-02-04 12:08:32 +00:00
Lenny McLennington
20edcb1a2b
fix windows ci builds
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-04 11:21:52 +00:00
Lenny McLennington
67bb016623
fix(ModrinthInstanceCreationTask): ignore files with invalid paths
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2023-02-04 00:10:54 +00:00
Lenny McLennington
fe2624bf1a
Merge pull request #1531 from oluceps/fix-nixbuild
Fix build fail on nix flake
2023-01-13 00:10:20 +00:00
oluceps
eb390814d8
Fix build fail on nix flake.
This should fix nix build fail caused by introduced new qt module.

Signed-off-by: oluceps <i@oluceps.uk>
2023-01-05 21:55:05 +08:00
Lenny McLennington
21e6093520
Merge pull request #1504 from xslendix/storage
Add storage page to instances.
2023-01-04 20:23:12 +00:00
Kemonomodoki
928ff7826c fix: handle single files in copy tasks
This should fix FTB imported instances not having any mods installed.

Signed-off-by: Kemonomodoki <kemonomodoki@firemail.cc>
2022-12-29 07:30:28 -05:00
xSlendiX
91c7a700a9 Fix GitHub workflow broken.
This patch adds the needed QtCharts dependency to GitHub workflow.

Signed-off-by: xSlendiX <slendi@socopon.com>
2022-12-27 22:34:48 +02:00
Lenny McLennington
7839bd0e54
Merge pull request #1508 from xslendix/move_pussy
Add a way to change the position of the cat.
2022-12-08 06:49:58 +00:00
xSlendiX
58bc61ccbd Add a way to change the position of the cat.
This simple commit adds the ability to further customize the cat by
allowing the user to change its position.

Signed-off-by: xSlendiX <slendi@socopon.com>
2022-12-06 15:25:09 +02:00
Lenny McLennington
7f226eba9c
Merge pull request #1503 from jdpatdiscord/develop
fix recursive crash on win64
2022-12-06 00:28:15 +00:00
Lenny McLennington
3340d2c814
Merge pull request #1510 from xslendix/oh_yeah_big_floppa_time_sunglasses_emoji
Add Floppa :^)
2022-12-06 00:26:00 +00:00
xSlendiX
59c280309d Add Floppa :^)
How can we have a cat selection combo box without a floppa option?!
This is unacceptable, and this patch fixes it. Now the launcher can flop
all day long :^)

Signed-off-by: xSlendiX <slendi@socopon.com>
2022-11-26 22:04:14 +02:00
xSlendiX
3187b5c1c2 Add pie chart to StoragePage and improve UX :^)
This patch adds a nice little pie chart to visually show which parts of
the instance take most space. It also refactors some code and improve
the UX a bit more.

Signed-off-by: xSlendiX <slendi@socopon.com>
2022-11-26 14:39:41 +02:00
xSlendiX
16a4a4f811 Add icon for storage page.
Signed-off-by: xSlendiX <slendi@socopon.com>
2022-11-24 20:46:50 +02:00
Lenny McLennington
90bbde3631
build: only increase stack size in non-debug builds
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-24 18:20:18 +00:00
Lenny McLennington
211c423da1
Merge pull request #1493 from urFate/develop
Implement account per instance feature
2022-11-17 23:37:21 +00:00
Lenny McLennington
51de23d83c
Merge pull request #1505 from LennyMcLennington/fix-concurrenttask-stack-overflow
fix: prevent stack overflow in ConcurrentTask
2022-11-17 23:36:50 +00:00
xSlendiX
cf317ca3d4 Add storage page to instances.
This patch adds a new page to instance settings which allows users to
manage storage. This commit represents the start of it. Right now, only
some base features are implemented.

Signed-off-by: xSlendiX <slendi@socopon.com>
2022-11-17 20:38:45 +02:00
Lenny McLennington
a3d1e88d44
Merge pull request #1499 from HeyaGlitz/develop
Add Jinx
2022-11-17 04:34:04 +00:00
Lenny McLennington
4cccb693f2
fix: prevent stack overflow in ConcurrentTask
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-17 04:13:30 +00:00
Lenny McLennington
6ff87f773b
fix: add missing placeholder text for account override
Also changes the "No accounts available" message to be placeholder text
instead of a combobox item, if the current Qt version supports it.

Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-17 03:21:39 +00:00
Lenny McLennington
462e0ef56d
fix: disable account override if no account selected
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-17 02:12:23 +00:00
Lenny McLennington
de1de47940
fix: use profile id for per-instance account override
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-17 02:08:19 +00:00
jdp_
22fb301d88 fix recursive crash on win64
one of the contributors to PolyMC that moved over to Prism (TheLastRar) ended up finding this.
I didn't understand what it was for until I fixed & ran a 64-bit Windows build myself, and got a 0xC00000FD (Stack overflow).
This is caused by the massive recursive load of ATLauncher packs.
This is a pretty jank fix as it is, and later should be re-engineered to not be recursive.

Signed-off-by: jdp_ <42700985+jdpatdiscord@users.noreply.github.com>
2022-11-16 12:08:10 -05:00
HeyaGlitz
ddbc80e1ef
Add Jinx
Signed-off-by: HeyaGlitz <heyaglitz0x99@proton.me>
2022-11-14 21:23:50 +01:00
Lenny McLennington
cfe8f6ce9b
Merge pull request #1501 from binex-dsk/patch-1
fix windows build
2022-11-14 04:08:10 +00:00
swirl
78ec58dbf1
fix windows build
Signed-off-by: swirl <swurl@swurl.xyz>
2022-11-13 22:15:49 -05:00
Lenny McLennington
64aa817c80
Merge pull request #1500 from unix-supremacist/bsdmemfix
fix *bsd support
2022-11-13 19:22:51 +00:00
Unix
75756a5b9e fix *bsd max memory
Signed-off-by: Unix <100294596+unix-supremacist@users.noreply.github.com>
2022-11-13 11:09:17 -06:00
Lenny McLennington
1b52829e6c
Merge pull request #1494 from jdpatdiscord/develop
Have API key validators tolerate whitespace
2022-11-09 14:53:10 +00:00
Lenny McLennington
b3fb52ce51
chore: add jdp_'s copyright header
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-08 22:09:30 +00:00
Lenny McLennington
6b8e6774a0
refactor: clean up PMCKeyValidator
Renamed PMCKeyValidator to TrimmedRegExpValidator
Removed redundant variable
Removed erroneous forward-declaration in Ui namespace definition
Fixed indentation for Ui namespace definition

Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-08 22:08:06 +00:00
jdp_
c2a703b3d6
have key validation tolerate whitespace (fixes #1168)
Signed-off-by: Lenny McLennington <lenny@sneed.church>
2022-11-08 18:28:51 +00:00
urFate
5d9ff3767a Set tab index to 0 & fix english translation
Signed-off-by: urFate <georgiylakidon@gmail.com>
2022-11-07 19:09:30 +03:00
urFate
ce1a48be5d Use indexes instead of account name. Added "no accounts" indication. Added account type to combo box string.
Signed-off-by: urFate <georgiylakidon@gmail.com>
2022-11-02 12:18:14 +03:00
urFate
2a7c666932 Implement account per instance feature
Signed-off-by: urFate <georgiylakidon@gmail.com>
2022-11-01 19:10:34 +03:00
126 changed files with 2337 additions and 323 deletions

2
.clangd Normal file
View File

@ -0,0 +1,2 @@
CompileFlags:
CompilationDatabase: build

View File

@ -26,16 +26,16 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: linux qt_host: linux
qt_version: '6.2.4' qt_version: '6.2.4'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats qtcharts'
- os: windows-2022 - os: windows-2022
name: "Windows-Legacy" name: "Windows-Legacy"
msystem: mingw32 msystem: mingw64
qt_ver: 5 qt_ver: 5
- os: windows-2022 - os: windows-2022
name: "Windows" name: "Windows"
msystem: mingw32 msystem: mingw64
qt_ver: 6 qt_ver: 6
- os: macos-12 - os: macos-12
@ -44,7 +44,7 @@ jobs:
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_version: '6.3.0' qt_version: '6.3.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats qtcharts'
- os: macos-12 - os: macos-12
name: macOS-Legacy name: macOS-Legacy
@ -52,7 +52,7 @@ jobs:
qt_ver: 5 qt_ver: 5
qt_host: mac qt_host: mac
qt_version: '5.15.2' qt_version: '5.15.2'
qt_modules: '' qt_modules: 'qtcharts'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -97,6 +97,7 @@ jobs:
qt${{ matrix.qt_ver }}-base:p qt${{ matrix.qt_ver }}-base:p
qt${{ matrix.qt_ver }}-svg:p qt${{ matrix.qt_ver }}-svg:p
qt${{ matrix.qt_ver }}-imageformats:p qt${{ matrix.qt_ver }}-imageformats:p
qt${{ matrix.qt_ver }}-charts:p
quazip-qt${{ matrix.qt_ver }}:p quazip-qt${{ matrix.qt_ver }}:p
ccache:p ccache:p
nsis:p nsis:p
@ -154,7 +155,7 @@ jobs:
- name: Install Qt (Linux) - name: Install Qt (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 6 if: runner.os == 'Linux' && matrix.qt_ver != 6
run: | run: |
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5charts5-dev
- name: Install Qt (macOS and AppImage) - name: Install Qt (macOS and AppImage)
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS'
@ -280,7 +281,7 @@ jobs:
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
if [ "${{ matrix.qt_ver }}" == "5" ]; then if [ "${{ matrix.qt_ver }}" == "5" ]; then
cp /mingw32/bin/libcrypto-1_1.dll /mingw32/bin/libssl-1_1.dll ./ cp /mingw64/bin/libcrypto-3-x64.dll /mingw64/bin/libssl-3-x64.dll ./
fi fi
- name: Package (Windows, portable) - name: Package (Windows, portable)

View File

@ -34,6 +34,13 @@ set(CMAKE_C_STANDARD 11)
include(GenerateExportHeader) include(GenerateExportHeader)
set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}")
# Increases the stack size to 8MB for Windows, only when not building in Debug mode
# because we don't want random users being affected by stack overflows in release builds,
# but it's fine in debug builds for finding and fixing them
if((NOT CMAKE_BUILD_TYPE STREQUAL "Debug") AND WIN32)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
endif()
# Fix build with Qt 5.13 # Fix build with Qt 5.13
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y")
@ -76,7 +83,7 @@ set(Launcher_NEWS_OPEN_URL "https://multimc.org/posts.html" CACHE STRING "URL th
set(Launcher_HELP_URL "" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") set(Launcher_HELP_URL "" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ######## ######## Set version numbers ########
set(Launcher_VERSION_MAJOR 5) set(Launcher_VERSION_MAJOR 6)
set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
@ -120,9 +127,6 @@ execute_process(COMMAND ./id.sh OUTPUT_VARIABLE Launcher_MSA_CLIENT_ID)
set(Launcher_CURSEFORGE_API_KEY "" CACHE STRING "API key for the CurseForge platform") set(Launcher_CURSEFORGE_API_KEY "" CACHE STRING "API key for the CurseForge platform")
set(Launcher_CURSEFORGE_API_KEY_API_URL "https://cf.polymc.org/api" CACHE STRING "URL to fetch the Curseforge API key from.") set(Launcher_CURSEFORGE_API_KEY_API_URL "https://cf.polymc.org/api" CACHE STRING "URL to fetch the Curseforge API key from.")
# Curseforge API Key
execute_process(COMMAND ./cf.sh OUTPUT_VARIABLE Launcher_CURSEFORGE_API_KEY)
#### Check the current Git commit and branch #### Check the current Git commit and branch
include(GetGitRevisionDescription) include(GetGitRevisionDescription)
git_get_exact_tag(Launcher_GIT_TAG) git_get_exact_tag(Launcher_GIT_TAG)
@ -141,7 +145,8 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}")
include(QtVersionlessBackport) include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5) if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(QT_VERSION_MAJOR 5) set(QT_VERSION_MAJOR 5)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml) find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test
Xml Charts)
if(NOT Launcher_FORCE_BUNDLED_LIBS) if(NOT Launcher_FORCE_BUNDLED_LIBS)
find_package(QuaZip-Qt5 1.3 QUIET) find_package(QuaZip-Qt5 1.3 QUIET)
@ -155,7 +160,8 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6) set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml Core5Compat) find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network Test
Xml Charts Core5Compat)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS) if(NOT Launcher_FORCE_BUNDLED_LIBS)

View File

@ -567,6 +567,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// The cat // The cat
m_settings->registerSetting("TheCat", false); m_settings->registerSetting("TheCat", false);
m_settings->registerSetting("CatStyle", "BackgroundCat");
m_settings->registerSetting("CatPosition", "top right");
m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("InstSortMode", "Name");
m_settings->registerSetting("SelectedInstance", QString()); m_settings->registerSetting("SelectedInstance", QString());
@ -769,6 +771,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("translations", QDir("translations").absolutePath());
m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->addBase("authlibinjector", QDir("cache/authlibinjector").absolutePath());
m_metacache->Load(); m_metacache->Load();
qDebug() << "<> Cache initialized."; qDebug() << "<> Cache initialized.";
} }
@ -1119,9 +1122,9 @@ void Application::setApplicationTheme(const QString& name, bool initial)
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (m_mainWindow) { if (m_mainWindow) {
if (QString::compare(theme->id(), "dark") == 0) { if (QString::compare(theme->id(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true); WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), true);
} else { } else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false); WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), false);
} }
} }
#endif #endif
@ -1336,9 +1339,9 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray()));
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) { if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true); WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), true);
} else { } else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false); WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), false);
} }
#endif #endif
if(minimized) if(minimized)

View File

@ -189,11 +189,15 @@ set(MINECRAFT_SOURCES
minecraft/auth/flows/AuthFlow.h minecraft/auth/flows/AuthFlow.h
minecraft/auth/flows/Mojang.cpp minecraft/auth/flows/Mojang.cpp
minecraft/auth/flows/Mojang.h minecraft/auth/flows/Mojang.h
minecraft/auth/flows/AuthlibInjector.cpp
minecraft/auth/flows/AuthlibInjector.h
minecraft/auth/flows/MSA.cpp minecraft/auth/flows/MSA.cpp
minecraft/auth/flows/MSA.h minecraft/auth/flows/MSA.h
minecraft/auth/flows/Offline.cpp minecraft/auth/flows/Offline.cpp
minecraft/auth/flows/Offline.h minecraft/auth/flows/Offline.h
minecraft/auth/steps/AuthlibInjectorStep.cpp
minecraft/auth/steps/AuthlibInjectorStep.h
minecraft/auth/steps/OfflineStep.cpp minecraft/auth/steps/OfflineStep.cpp
minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/OfflineStep.h
minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.cpp
@ -233,6 +237,8 @@ set(MINECRAFT_SOURCES
minecraft/launch/ClaimAccount.cpp minecraft/launch/ClaimAccount.cpp
minecraft/launch/ClaimAccount.h minecraft/launch/ClaimAccount.h
minecraft/launch/ConfigureAuthlibInjector.cpp
minecraft/launch/ConfigureAuthlibInjector.h
minecraft/launch/CreateGameFolders.cpp minecraft/launch/CreateGameFolders.cpp
minecraft/launch/CreateGameFolders.h minecraft/launch/CreateGameFolders.h
minecraft/launch/ModMinecraftJar.cpp minecraft/launch/ModMinecraftJar.cpp
@ -675,6 +681,8 @@ SET(LAUNCHER_SOURCES
ui/pages/instance/ServersPage.h ui/pages/instance/ServersPage.h
ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.cpp
ui/pages/instance/WorldListPage.h ui/pages/instance/WorldListPage.h
ui/pages/instance/StoragePage.cpp
ui/pages/instance/StoragePage.h
# GUI - global settings pages # GUI - global settings pages
ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.cpp
@ -889,6 +897,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/instance/VersionPage.ui ui/pages/instance/VersionPage.ui
ui/pages/instance/WorldListPage.ui ui/pages/instance/WorldListPage.ui
ui/pages/instance/ScreenshotsPage.ui ui/pages/instance/ScreenshotsPage.ui
ui/pages/instance/StoragePage.ui
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui ui/pages/modplatform/VanillaPage.ui
@ -971,6 +980,7 @@ target_link_libraries(Launcher_logic
Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Charts
${Launcher_QT_LIBS} ${Launcher_QT_LIBS}
) )
target_link_libraries(Launcher_logic target_link_libraries(Launcher_logic

View File

@ -45,6 +45,8 @@
#include <QTextStream> #include <QTextStream>
#include <QUrl> #include <QUrl>
#include <system_error>
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#include <objbase.h> #include <objbase.h>
#include <objidl.h> #include <objidl.h>
@ -174,7 +176,7 @@ bool copy::operator()(const QString& offset)
auto src = PathCombine(m_src.absolutePath(), offset); auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset);
std::error_code err; std::error_code err{};
fs::copy_options opt = copy_opts::none; fs::copy_options opt = copy_opts::none;
@ -182,28 +184,49 @@ bool copy::operator()(const QString& offset)
if (!m_followSymlinks) if (!m_followSymlinks)
opt |= copy_opts::copy_symlinks; opt |= copy_opts::copy_symlinks;
const auto testAndCopy = [opt, &err](const QString& s, const QString& d) {
if (ensureFilePathExists(d)) {
fs::copy(toStdString(s), toStdString(d), opt, err);
} else {
// mkpath failed which means the destination directory doesn't exist.
err = std::make_error_code(std::errc::no_such_file_or_directory);
}
if (err) {
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << s;
qDebug() << "Destination file:" << d;
}
};
// We can't use copy_opts::recursive because we need to take into account the // We can't use copy_opts::recursive because we need to take into account the
// blacklisted paths, so we iterate over the source directory, and if there's no blacklist // blacklisted paths, so we iterate over the source directory, and if there's no blacklist
// match, we copy the file. // match, we copy the file.
QDir src_dir(src); if (QDir src_dir(src); src_dir.exists()) {
QDirIterator source_it(src, QDir::Filter::Files, QDirIterator::Subdirectories); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories);
while (source_it.hasNext()) { while (source_it.hasNext()) {
auto src_path = source_it.next(); auto src_path = source_it.next();
auto relative_path = src_dir.relativeFilePath(src_path); auto relative_path = src_dir.relativeFilePath(src_path);
if (m_blacklist && m_blacklist->matches(relative_path))
continue;
auto dst_path = PathCombine(dst, relative_path); auto dst_path = PathCombine(dst, relative_path);
ensureFilePathExists(dst_path);
fs::copy(toStdString(src_path), toStdString(dst_path), opt, err); if (m_blacklist && m_blacklist->matches(relative_path)) {
if (err) { qDebug() << "Attempted to copy blacklisted file:";
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path; qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path; qDebug() << "Destination file:" << dst_path;
continue;
}
testAndCopy(src_path, dst_path);
}
} else { // src_dir could still be a file, try to copy it directly.
if (m_blacklist && m_blacklist->matches(src)){
qDebug() << "Attempted to copy blacklisted file:";
qDebug() << "Source file:" << src;
qDebug() << "Destination file:" << dst;
} else {
testAndCopy(src, dst);
} }
} }
@ -340,7 +363,7 @@ QString getDesktopDir()
// Cross-platform Shortcut creation // Cross-platform Shortcut creation
bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon) bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon)
{ {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) #if !defined(Q_OS_WIN) && !defined(Q_OS_OSX)
location = PathCombine(location, name + ".desktop"); location = PathCombine(location, name + ".desktop");
QFile f(location); QFile f(location);
@ -366,49 +389,35 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
return true; return true;
#elif defined Q_OS_WIN
// TODO: Fix
// QFile file(PathCombine(location, name + ".lnk"));
// WCHAR *file_w;
// WCHAR *dest_w;
// WCHAR *args_w;
// file.fileName().toWCharArray(file_w);
// dest.toWCharArray(dest_w);
// QString argStr;
// for (int i = 0; i < args.count(); i++)
// {
// argStr.append(args[i]);
// argStr.append(" ");
// }
// argStr.toWCharArray(args_w);
// return SUCCEEDED(CreateLink(file_w, dest_w, args_w));
return false;
#else #else
qWarning("Desktop Shortcuts not supported on your platform!"); qWarning("Desktop Shortcuts not supported on your platform!");
return false; return false;
#endif #endif
} }
bool overrideFolder(QString overwritten_path, QString override_path) bool mergeFolders(QString dstpath, QString srcpath)
{ {
using copy_opts = fs::copy_options; std::error_code ec;
fs::path fullSrcPath = srcpath.toStdString();
if (!FS::ensureFolderPathExists(overwritten_path)) fs::path fullDstPath = dstpath.toStdString();
return false; for (auto& entry : fs::recursive_directory_iterator(fullSrcPath))
{
std::error_code err; fs::path relativeChild = fs::relative(entry, fullSrcPath);
fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing; if (entry.is_directory())
if (!fs::exists(fullDstPath / relativeChild))
fs::copy(toStdString(override_path), toStdString(overwritten_path), opt, err); fs::create_directory(fullDstPath / relativeChild);
if (entry.is_regular_file())
if (err) { {
qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); fs::path childDst = fullDstPath / relativeChild;
qCritical() << "Reason:" << QString::fromStdString(err.message()); if (fs::exists(childDst))
fs::remove(childDst);
fs::copy(entry, childDst, fs::copy_options::none, ec);
if (ec.value() != 0)
qCritical() << QString("File copy failed with: %1. File was %2 -> %3").arg(QString::fromStdString(ec.message()), entry.path().c_str(), childDst.c_str());
}
} }
return err.value() == 0; return ec.value() == 0;
} }
} }

View File

@ -154,5 +154,5 @@ QString getDesktopDir();
// Overrides one folder with the contents of another, preserving items exclusive to the first folder // Overrides one folder with the contents of another, preserving items exclusive to the first folder
// Equivalent to doing QDir::rename, but allowing for overrides // Equivalent to doing QDir::rename, but allowing for overrides
bool overrideFolder(QString overwritten_path, QString override_path); bool mergeFolders(QString dstpath, QString srcpath);
} }

View File

@ -164,23 +164,21 @@ void InstanceImportTask::processZipPack()
} }
else else
{ {
QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); auto [rootDirectory, fileName] = MMCZip::findFolderOfFileInZip(m_packZip.get(), {"manifest.json", "instance.cfg"});
QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); if(fileName == "manifest.json")
if (!mmcRoot.isNull())
{
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << mmcRoot;
root = mmcRoot;
m_modpackType = ModpackType::MultiMC;
}
else if(!flameRoot.isNull())
{ {
// process as Flame pack // process as Flame pack
qDebug() << "Flame:" << flameRoot; qDebug() << "Flame:" << rootDirectory;
root = flameRoot; root = rootDirectory;
m_modpackType = ModpackType::Flame; m_modpackType = ModpackType::Flame;
} }
else if (fileName == "instance.cfg")
{
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << rootDirectory;
root = rootDirectory;
m_modpackType = ModpackType::MultiMC;
}
} }
if(m_modpackType == ModpackType::Unknown) if(m_modpackType == ModpackType::Unknown)
{ {

View File

@ -904,7 +904,7 @@ bool InstanceList::commitStagedInstance(const QString& path, InstanceName const&
QString destination = FS::PathCombine(m_instDir, instID); QString destination = FS::PathCombine(m_instDir, instID);
if (should_override) { if (should_override) {
if (!FS::overrideFolder(destination, path)) { if (!FS::mergeFolders(destination, path)) {
qWarning() << "Failed to override" << path << "to" << destination; qWarning() << "Failed to override" << path << "to" << destination;
return false; return false;
} }

View File

@ -16,6 +16,7 @@
#include "ui/pages/instance/WorldListPage.h" #include "ui/pages/instance/WorldListPage.h"
#include "ui/pages/instance/ServersPage.h" #include "ui/pages/instance/ServersPage.h"
#include "ui/pages/instance/GameOptionsPage.h" #include "ui/pages/instance/GameOptionsPage.h"
#include "ui/pages/instance/StoragePage.h"
class InstancePageProvider : public QObject, public BasePageProvider class InstancePageProvider : public QObject, public BasePageProvider
{ {
@ -46,6 +47,7 @@ public:
// values.append(new GameOptionsPage(onesix.get())); // values.append(new GameOptionsPage(onesix.get()));
values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots")));
values.append(new InstanceSettingsPage(onesix.get())); values.append(new InstanceSettingsPage(onesix.get()));
values.append(new StoragePage(onesix.get()));
auto logMatcher = inst->getLogFileMatcher(); auto logMatcher = inst->getLogFileMatcher();
if(logMatcher) if(logMatcher)
{ {

View File

@ -112,7 +112,18 @@ void LaunchController::decideAccount()
} }
} }
bool overrideAccount = m_instance->settings()->get("OverrideAccount").toBool();
QString overrideAccountProfileId = m_instance->settings()->get("OverrideAccountProfileId").toString();
m_accountToUse = accounts->defaultAccount(); m_accountToUse = accounts->defaultAccount();
if (overrideAccount) {
int overrideIndex = accounts->findAccountByProfileId(overrideAccountProfileId);
if (overrideIndex != -1) {
m_accountToUse = accounts->at(overrideIndex);
}
}
if (!m_accountToUse) if (!m_accountToUse)
{ {
// If no default account is set, ask the user which one to use. // If no default account is set, ask the user which one to use.
@ -179,7 +190,7 @@ void LaunchController::login() {
switch(m_accountToUse->accountState()) { switch(m_accountToUse->accountState()) {
case AccountState::Offline: { case AccountState::Offline: {
m_session->wants_online = false; m_session->wants_online = false;
// NOTE: fallthrough is intentional [[fallthrough]];
} }
case AccountState::Online: { case AccountState::Online: {
if(!m_session->wants_online) { if(!m_session->wants_online) {
@ -212,7 +223,6 @@ void LaunchController::login() {
APPLICATION->settings()->set("LastOfflinePlayerName", usedname); APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
} }
m_session->MakeOffline(usedname); m_session->MakeOffline(usedname);
// offline flavored game from here :3
} }
if(m_accountToUse->ownsMinecraft()) { if(m_accountToUse->ownsMinecraft()) {
if(!m_accountToUse->hasProfile()) { if(!m_accountToUse->hasProfile()) {
@ -259,7 +269,7 @@ void LaunchController::login() {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh. // This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: { case AccountState::Unchecked: {
m_accountToUse->refresh(); m_accountToUse->refresh();
// NOTE: fallthrough intentional [[fallthrough]];
} }
case AccountState::Working: { case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed. // refresh is in progress, we need to wait for it to finish to proceed.
@ -272,12 +282,11 @@ void LaunchController::login() {
progDialog.execWithTask(task.get()); progDialog.execWithTask(task.get());
continue; continue;
} }
// FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
/*
case AccountState::Queued: { case AccountState::Queued: {
// FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
qWarning() << "AccountState::Queued is not implemented";
return; return;
} }
*/
case AccountState::Expired: { case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again."); auto errorString = tr("The account has expired and needs to be logged into manually again.");
QMessageBox::warning( QMessageBox::warning(
@ -314,6 +323,9 @@ void LaunchController::login() {
emitFailed(errorString); emitFailed(errorString);
return; return;
} }
default: {
qWarning() << "Invalid AccountState enum";
}
} }
} }
emitFailed(tr("Failed to launch.")); emitFailed(tr("Failed to launch."));

View File

@ -40,6 +40,7 @@
#include "FileSystem.h" #include "FileSystem.h"
#include <QDebug> #include <QDebug>
#include <deque>
// ours // ours
bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const FilterFunction filter) bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const FilterFunction filter)
@ -228,23 +229,27 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
} }
// ours // ours
QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root) std::pair<QString, QString> MMCZip::findFolderOfFileInZip(QuaZip * zip, QSet<const QString> what, const QString &root)
{ {
QuaZipDir rootDir(zip, root); std::deque<QString> pathsToTraverse;
pathsToTraverse.push_back(root);
while (!pathsToTraverse.empty())
{
QString currentPath = pathsToTraverse.front();
pathsToTraverse.pop_front();
QuaZipDir rootDir(zip, currentPath);
for(auto fileName: rootDir.entryList(QDir::Files)) for(auto fileName: rootDir.entryList(QDir::Files))
{ {
if(fileName == what) if (what.contains(fileName))
return root; return {currentPath, fileName};
} }
for(auto fileName: rootDir.entryList(QDir::Dirs)) for(auto fileName: rootDir.entryList(QDir::Dirs))
{ {
QString result = findFolderOfFileInZip(zip, what, root + fileName); pathsToTraverse.push_back(rootDir.path() + fileName);
if(!result.isEmpty())
{
return result;
} }
} }
return QString(); return {QString(), QString()};
} }
// ours // ours
@ -292,10 +297,15 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
do do
{ {
QString name = zip->getCurrentFileName(); QString name = zip->getCurrentFileName();
if(!name.startsWith(subdir)) if(!QDir::cleanPath(name).startsWith(subdir))
{ {
continue; continue;
} }
if (QDir::isAbsolutePath(name) || QDir::cleanPath(name).startsWith(".."))
{
qDebug() << "extractSubDir: Skipping file that tries to place itself in an absolute location or in a parent directory.";
continue;
}
name.remove(0, subdir.size()); name.remove(0, subdir.size());
auto original_name = name; auto original_name = name;

View File

@ -78,11 +78,11 @@ namespace MMCZip
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods); bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
/** /**
* Find a single file in archive by file name (not path) * Breath-first find a single file in archive by a list of file names (not path)
* *
* \return the path prefix where the file is * \return pair of {file parent directory, file name}
*/ */
QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString("")); std::pair<QString, QString> findFolderOfFileInZip(QuaZip * zip, QSet<const QString> what, const QString &root = QString(""));
/** /**
* Find a multiple files of the same name in archive by file name * Find a multiple files of the same name in archive by file name

View File

@ -211,6 +211,7 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return tr("Latest"); return tr("Latest");
} }
} }
[[fallthrough]];
} }
default: default:
{ {
@ -254,6 +255,7 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
} }
return pixmap; return pixmap;
} }
[[fallthrough]];
} }
default: default:
{ {

View File

@ -448,6 +448,16 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDir("/opt/jdks"); scanJavaDir("/opt/jdks");
// flatpak // flatpak
scanJavaDir("/app/jdk"); scanJavaDir("/app/jdk");
// Default SDKMAN directory can be overwritten via SDKMAN_DIR env var (default $HOME/.sdkman)
// see https://sdkman.io/install
auto sdkmanInstallPath = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(QDir::homePath(), ".sdkman"));
scanJavaDir(FS::PathCombine(sdkmanInstallPath, "candidates/java"));
// Default ASDF directory can be overwritten via ASDF_DIR or ASDF_DATA_DIR env vars (default $HOME/.asdf)
// see https://asdf-vm.com/manage/configuration.html#asdf-dir
auto asdfDataPath = qEnvironmentVariable("ASDF_DATA_DIR", qEnvironmentVariable("ASDF_DIR", FS::PathCombine(QDir::homePath(), ".asdf")));
scanJavaDir(FS::PathCombine(asdfDataPath, "installs/java"));
return addJavasFromEnv(javas); return addJavasFromEnv(javas);
} }
#else #else

View File

@ -91,4 +91,5 @@ int main(int argc, char *argv[])
case Application::Succeeded: case Application::Succeeded:
return 0; return 0;
} }
return 0;
} }

View File

@ -45,11 +45,9 @@ QVariant Index::data(const QModelIndex &index, int role) const
switch (role) switch (role)
{ {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (index.column()) if (index.column() == 0)
{ return list->humanReadable();
case 0: return list->humanReadable(); break;
default: break;
}
case UidRole: return list->uid(); case UidRole: return list->uid();
case NameRole: return list->name(); case NameRole: return list->name();
case ListPtrRole: return QVariant::fromValue(list); case ListPtrRole: return QVariant::fromValue(list);
@ -70,11 +68,8 @@ QVariant Index::headerData(int section, Qt::Orientation orientation, int role) c
{ {
return tr("Name"); return tr("Name");
} }
else
{
return QVariant(); return QVariant();
} }
}
bool Index::hasUid(const QString &uid) const bool Index::hasUid(const QString &uid) const
{ {

View File

@ -56,10 +56,10 @@ static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj)
version->setType(ensureString(obj, "type", QString())); version->setType(ensureString(obj, "type", QString()));
version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); version->setVolatile(ensureBoolean(obj, QString("volatile"), false));
RequireSet requires, conflicts; RequireSet required, conflicts;
parseRequires(obj, &requires, "requires"); parseRequires(obj, &required, "requires");
parseRequires(obj, &conflicts, "conflicts"); parseRequires(obj, &conflicts, "conflicts");
version->setRequires(requires, conflicts); version->setRequires(required, conflicts);
return version; return version;
} }
@ -176,7 +176,6 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName
{ {
if(obj.contains(keyName)) if(obj.contains(keyName))
{ {
QSet<QString> requires;
auto reqArray = requireArray(obj, keyName); auto reqArray = requireArray(obj, keyName);
auto iter = reqArray.begin(); auto iter = reqArray.begin();
while(iter != reqArray.end()) while(iter != reqArray.end())

View File

@ -111,9 +111,9 @@ void Meta::Version::setTime(const qint64 time)
emit timeChanged(); emit timeChanged();
} }
void Meta::Version::setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts) void Meta::Version::setRequires(const Meta::RequireSet &required, const Meta::RequireSet &conflicts)
{ {
m_requires = requires; m_requires = required;
m_conflicts = conflicts; m_conflicts = conflicts;
emit requiresChanged(); emit requiresChanged();
} }

View File

@ -61,7 +61,7 @@ public: /* con/des */
{ {
return m_time; return m_time;
} }
const Meta::RequireSet &requires() const const Meta::RequireSet &required() const
{ {
return m_requires; return m_requires;
} }
@ -87,7 +87,7 @@ public: /* con/des */
public: // for usage by format parsers only public: // for usage by format parsers only
void setType(const QString &type); void setType(const QString &type);
void setTime(const qint64 time); void setTime(const qint64 time);
void setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts); void setRequires(const Meta::RequireSet &required, const Meta::RequireSet &conflicts);
void setVolatile(bool volatile_); void setVolatile(bool volatile_);
void setRecommended(bool recommended); void setRecommended(bool recommended);
void setProvidesRecommendations(); void setProvidesRecommendations();

View File

@ -77,7 +77,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case ParentVersionRole: case ParentVersionRole:
{ {
// FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'. // FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'.
auto & reqs = version->requires(); auto & reqs = version->required();
auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req) auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req)
{ {
return req.uid == "net.minecraft"; return req.uid == "net.minecraft";
@ -92,7 +92,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const
case UidRole: return version->uid(); case UidRole: return version->uid();
case TimeRole: return version->time(); case TimeRole: return version->time();
case RequiresRole: return QVariant::fromValue(version->requires()); case RequiresRole: return QVariant::fromValue(version->required());
case SortRole: return version->rawTime(); case SortRole: return version->rawTime();
case VersionPtrRole: return QVariant::fromValue(version); case VersionPtrRole: return QVariant::fromValue(version);
case RecommendedRole: return version->isRecommended(); case RecommendedRole: return version->isRecommended();

View File

@ -451,9 +451,9 @@ void Component::updateCachedData()
m_cachedVolatile = file->m_volatile; m_cachedVolatile = file->m_volatile;
changed = true; changed = true;
} }
if(!deepCompare(m_cachedRequires, file->requires)) if(!deepCompare(m_cachedRequires, file->required))
{ {
m_cachedRequires = file->requires; m_cachedRequires = file->required;
changed = true; changed = true;
} }
if(!deepCompare(m_cachedConflicts, file->conflicts)) if(!deepCompare(m_cachedConflicts, file->conflicts))

View File

@ -59,6 +59,7 @@
#include "launch/steps/QuitAfterGameStop.h" #include "launch/steps/QuitAfterGameStop.h"
#include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/LauncherPartLaunch.h"
#include "minecraft/launch/ConfigureAuthlibInjector.h"
#include "minecraft/launch/DirectJavaLaunch.h" #include "minecraft/launch/DirectJavaLaunch.h"
#include "minecraft/launch/ModMinecraftJar.h" #include "minecraft/launch/ModMinecraftJar.h"
#include "minecraft/launch/ClaimAccount.h" #include "minecraft/launch/ClaimAccount.h"
@ -188,6 +189,10 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunch", false);
m_settings->registerSetting("JoinServerOnLaunchAddress", ""); m_settings->registerSetting("JoinServerOnLaunchAddress", "");
// Account override
m_settings->registerSetting("OverrideAccount", false);
m_settings->registerSetting("OverrideAccountProfileId", "");
qDebug() << "Instance-type specific settings were loaded!"; qDebug() << "Instance-type specific settings were loaded!";
setSpecificSettingsLoaded(true); setSpecificSettingsLoaded(true);
@ -256,7 +261,7 @@ QString MinecraftInstance::getLocalLibraryPath() const
bool MinecraftInstance::supportsDemo() const bool MinecraftInstance::supportsDemo() const
{ {
Version instance_ver { getPackProfile()->getComponentVersion("net.minecraft") }; Version instance_ver { getPackProfile()->getComponentVersion("net.minecraft") };
// Demo mode was introduced in 1.3.1: https://minecraft.fandom.com/wiki/Demo_mode#History // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History
// FIXME: Due to Version constraints atm, this can't handle well non-release versions // FIXME: Due to Version constraints atm, this can't handle well non-release versions
return instance_ver >= Version("1.3.1"); return instance_ver >= Version("1.3.1");
} }
@ -384,6 +389,11 @@ QStringList MinecraftInstance::javaArguments()
{ {
QStringList args; QStringList args;
if (!m_authlibinjector_javaagent->isNull())
{
args.append(QString("-javaagent:%1").arg(*m_authlibinjector_javaagent));
}
// custom args go first. we want to override them if we have our own here. // custom args go first. we want to override them if we have our own here.
args.append(extraArguments()); args.append(extraArguments());
@ -987,6 +997,12 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(step); process->appendStep(step);
} }
*m_authlibinjector_javaagent = QString();
if (!session->authlib_injector_base_url.isNull())
{
process->appendStep(new ConfigureAuthlibInjector(pptr, session->authlib_injector_base_url, m_authlibinjector_javaagent));
}
// if we aren't in offline mode,. // if we aren't in offline mode,.
if(session->status != AuthSession::PlayableOffline) if(session->status != AuthSession::PlayableOffline)
{ {

View File

@ -173,6 +173,7 @@ protected: // data
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list; mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list; mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options; mutable std::shared_ptr<GameOptions> m_game_options;
mutable std::shared_ptr<QString> m_authlibinjector_javaagent = std::make_shared<QString>();
}; };
typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr; typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr;

View File

@ -266,7 +266,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
if (root.contains("requires")) if (root.contains("requires"))
{ {
Meta::parseRequires(root, &out->requires); Meta::parseRequires(root, &out->required);
} }
QString dependsOnMinecraftVersion = root.value("mcVersion").toString(); QString dependsOnMinecraftVersion = root.value("mcVersion").toString();
if(!dependsOnMinecraftVersion.isEmpty()) if(!dependsOnMinecraftVersion.isEmpty())
@ -274,9 +274,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc
Meta::Require mcReq; Meta::Require mcReq;
mcReq.uid = "net.minecraft"; mcReq.uid = "net.minecraft";
mcReq.equalsVersion = dependsOnMinecraftVersion; mcReq.equalsVersion = dependsOnMinecraftVersion;
if (out->requires.count(mcReq) == 0) if (out->required.count(mcReq) == 0)
{ {
out->requires.insert(mcReq); out->required.insert(mcReq);
} }
} }
if (root.contains("conflicts")) if (root.contains("conflicts"))
@ -368,9 +368,9 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch
} }
root.insert("mods", array); root.insert("mods", array);
} }
if(!patch->requires.empty()) if(!patch->required.empty())
{ {
Meta::serializeRequires(root, &patch->requires, "requires"); Meta::serializeRequires(root, &patch->required, "requires");
} }
if(!patch->conflicts.empty()) if(!patch->conflicts.empty())
{ {

View File

@ -138,7 +138,7 @@ public: /* data */
* SneedMC: set of packages this depends on * SneedMC: set of packages this depends on
* NOTE: this is shared with the meta format!!! * NOTE: this is shared with the meta format!!!
*/ */
Meta::RequireSet requires; Meta::RequireSet required;
/** /**
* SneedMC: set of packages this conflicts with * SneedMC: set of packages this conflicts with

View File

@ -285,7 +285,7 @@ void World::readFromZip(const QFileInfo &file)
{ {
return; return;
} }
auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); auto [location, _] = MMCZip::findFolderOfFileInZip(&zip, {"level.dat"});
is_valid = !location.isEmpty(); is_valid = !location.isEmpty();
if (!is_valid) if (!is_valid)
{ {

View File

@ -350,6 +350,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
type = AccountType::MSA; type = AccountType::MSA;
} else if (typeS == "Mojang") { } else if (typeS == "Mojang") {
type = AccountType::Mojang; type = AccountType::Mojang;
} else if (typeS == "Authlib-Injector") {
type = AccountType::AuthlibInjector;
} else if (typeS == "Offline") { } else if (typeS == "Offline") {
type = AccountType::Offline; type = AccountType::Offline;
} else { } else {
@ -362,6 +364,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); canMigrateToMSA = data.value("canMigrateToMSA").toBool(false);
} }
if(type == AccountType::AuthlibInjector) {
authlibInjectorBaseUrl = data.value("authlibInjectorUrl").toString();
}
if(type == AccountType::MSA) { if(type == AccountType::MSA) {
auto clientIDV = data.value("msa-client-id"); auto clientIDV = data.value("msa-client-id");
if (clientIDV.isString()) { if (clientIDV.isString()) {
@ -405,8 +411,10 @@ QJsonObject AccountData::saveState() const {
tokenToJSONV3(output, userToken, "utoken"); tokenToJSONV3(output, userToken, "utoken");
tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, xboxApiToken, "xrp-main");
tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc");
} } else if (type == AccountType::AuthlibInjector) {
else if (type == AccountType::Offline) { output["type"] = "Authlib-Injector";
output["authlibInjectorUrl"] = authlibInjectorBaseUrl;
} else if (type == AccountType::Offline) {
output["type"] = "Offline"; output["type"] = "Offline";
} }
@ -428,14 +436,14 @@ QString AccountData::accessToken() const {
} }
QString AccountData::clientToken() const { QString AccountData::clientToken() const {
if(type != AccountType::Mojang) { if(type != AccountType::Mojang && type != AccountType::AuthlibInjector) {
return QString(); return QString();
} }
return yggdrasilToken.extra["clientToken"].toString(); return yggdrasilToken.extra["clientToken"].toString();
} }
void AccountData::setClientToken(QString clientToken) { void AccountData::setClientToken(QString clientToken) {
if(type != AccountType::Mojang) { if(type != AccountType::Mojang && type != AccountType::AuthlibInjector) {
return; return;
} }
yggdrasilToken.extra["clientToken"] = clientToken; yggdrasilToken.extra["clientToken"] = clientToken;
@ -449,7 +457,7 @@ void AccountData::generateClientTokenIfMissing() {
} }
void AccountData::invalidateClientToken() { void AccountData::invalidateClientToken() {
if(type != AccountType::Mojang) { if(type != AccountType::Mojang && type != AccountType::AuthlibInjector) {
return; return;
} }
yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]"));
@ -470,6 +478,7 @@ QString AccountData::profileName() const {
QString AccountData::accountDisplayString() const { QString AccountData::accountDisplayString() const {
switch(type) { switch(type) {
case AccountType::AuthlibInjector:
case AccountType::Mojang: { case AccountType::Mojang: {
return userName(); return userName();
} }

View File

@ -74,6 +74,7 @@ struct MinecraftProfile {
enum class AccountType { enum class AccountType {
MSA, MSA,
Mojang, Mojang,
AuthlibInjector,
Offline Offline
}; };
@ -85,6 +86,7 @@ enum class AccountState {
Disabled, Disabled,
Errored, Errored,
Expired, Expired,
Queued,
Gone Gone
}; };
@ -114,6 +116,9 @@ struct AccountData {
QString lastError() const; QString lastError() const;
AccountType type = AccountType::MSA; AccountType type = AccountType::MSA;
QString authlibInjectorBaseUrl;
QString authlibInjectorApiLocation;
bool legacy = false; bool legacy = false;
bool canMigrateToMSA = false; bool canMigrateToMSA = false;

View File

@ -328,6 +328,12 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case AccountState::Gone: { case AccountState::Gone: {
return tr("Gone", "Account status"); return tr("Gone", "Account status");
} }
case AccountState::Queued: {
qWarning() << "Unhandled account state Queued";
[[fallthrough]];
}
default:
return tr("Unknown", "Account status");
} }
} }
@ -341,8 +347,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
else { else {
return tr("No", "Can Migrate?"); return tr("No", "Can Migrate?");
} }
qWarning() << "Unhandled case in MigrationColumn";
[[fallthrough]];
} }
default: default:
return QVariant(); return QVariant();
} }
@ -359,7 +366,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
case ProfileNameColumn: case ProfileNameColumn:
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
} }
[[fallthrough]];
default: default:
return QVariant(); return QVariant();
} }

View File

@ -109,6 +109,10 @@ public:
MinecraftAccountPtr defaultAccount() const; MinecraftAccountPtr defaultAccount() const;
void setDefaultAccount(MinecraftAccountPtr profileId); void setDefaultAccount(MinecraftAccountPtr profileId);
bool anyAccountIsValid(); bool anyAccountIsValid();
bool drmCheck()
{
return true;
}
bool isActive() const; bool isActive() const;

View File

@ -38,8 +38,12 @@ struct AuthSession
QString player_name; QString player_name;
// profile ID // profile ID
QString uuid; QString uuid;
// 'legacy' or 'mojang', depending on account type // 'legacy' or 'mojang' or 'authlib-injector', depending on account type
QString user_type; QString user_type;
// If not using authlib injector, this is blank.
QString authlib_injector_base_url;
// Did the auth server reply? // Did the auth server reply?
bool auth_server_online = false; bool auth_server_online = false;
// Did the user request online mode? // Did the user request online mode?

View File

@ -50,7 +50,39 @@
#include "flows/MSA.h" #include "flows/MSA.h"
#include "flows/Mojang.h" #include "flows/Mojang.h"
#include "flows/AuthlibInjector.h"
#include "flows/Offline.h" #include "flows/Offline.h"
#include "minecraft/auth/AccountData.h"
// Basically the same as https://github.com/qt/qtbase/blob/5.12/src/corelib/plugin/quuid.cpp#L152C1-L173C2, but unfortunately they don't allow
// us to specify a byte array for the namespace, we only get to specify a fixed length Uuid so I have to copy it and modify it ever so slightly.
static QUuid createUuidFromName(const QByteArray &ns, const QByteArray &baseData, QCryptographicHash::Algorithm algorithm, int version)
{
QByteArray hashResult;
// create a scope so later resize won't reallocate
{
QCryptographicHash hash(algorithm);
hash.addData(ns);
hash.addData(baseData);
hashResult = hash.result();
}
hashResult.resize(16); // Sha1 will be too long
QUuid result = QUuid::fromRfc4122(hashResult);
result.data3 &= 0x0FFF;
result.data3 |= (version << 12);
result.data4[0] &= 0x3F;
result.data4[0] |= 0x80;
return result;
}
static QUuid createUuidV3(const QByteArray &ns, const QByteArray &baseData)
{
return createUuidFromName(ns, baseData, QCryptographicHash::Md5, 3);
}
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@ -82,6 +114,16 @@ MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username
return account; return account;
} }
MinecraftAccountPtr MinecraftAccount::createAuthlibInjectorFromUsername(const QString &username, QString baseUrl)
{
MinecraftAccountPtr account = createFromUsername(username);
account->data.type = AccountType::AuthlibInjector;
account->data.authlibInjectorBaseUrl = baseUrl;
account->data.minecraftEntitlement.ownsMinecraft = true;
account->data.minecraftEntitlement.canPlayMinecraft = true;
return account;
}
MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createBlankMSA()
{ {
MinecraftAccountPtr account(new MinecraftAccount()); MinecraftAccountPtr account(new MinecraftAccount());
@ -100,7 +142,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftEntitlement.ownsMinecraft = true; account->data.minecraftEntitlement.ownsMinecraft = true;
account->data.minecraftEntitlement.canPlayMinecraft = true; account->data.minecraftEntitlement.canPlayMinecraft = true;
account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.id = createUuidV3("OfflinePlayer:", username.toUtf8()).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username; account->data.minecraftProfile.name = username;
account->data.minecraftProfile.validity = Katabasis::Validity::Certain; account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
return account; return account;
@ -132,7 +174,14 @@ QPixmap MinecraftAccount::getFace() const {
shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) { shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) {
Q_ASSERT(m_currentTask.get() == nullptr); Q_ASSERT(m_currentTask.get() == nullptr);
if (data.type == AccountType::Mojang)
{
m_currentTask.reset(new MojangLogin(&data, password)); m_currentTask.reset(new MojangLogin(&data, password));
}
else if (data.type == AccountType::AuthlibInjector)
{
m_currentTask.reset(new AuthlibInjectorLogin(&data, password));
}
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
@ -173,6 +222,9 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
else if(data.type == AccountType::Offline) { else if(data.type == AccountType::Offline) {
m_currentTask.reset(new OfflineRefresh(&data)); m_currentTask.reset(new OfflineRefresh(&data));
} }
else if(data.type == AccountType::AuthlibInjector) {
m_currentTask.reset(new AuthlibInjectorRefresh(&data));
}
else { else {
m_currentTask.reset(new MojangRefresh(&data)); m_currentTask.reset(new MojangRefresh(&data));
} }
@ -300,8 +352,9 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
session->player_name = data.profileName(); session->player_name = data.profileName();
// profile ID // profile ID
session->uuid = data.profileId(); session->uuid = data.profileId();
// 'legacy' or 'mojang', depending on account type // 'legacy' or 'mojang', or 'authlib-injector' depending on account type
session->user_type = typeString(); session->user_type = typeString();
session->authlib_injector_base_url = data.authlibInjectorBaseUrl;
if (!session->access_token.isEmpty()) if (!session->access_token.isEmpty())
{ {
session->session = "token:" + data.accessToken() + ":" + data.profileId(); session->session = "token:" + data.accessToken() + ":" + data.profileId();

View File

@ -91,6 +91,8 @@ public: /* construction */
static MinecraftAccountPtr createFromUsername(const QString &username); static MinecraftAccountPtr createFromUsername(const QString &username);
static MinecraftAccountPtr createAuthlibInjectorFromUsername(const QString &username, QString baseUrl);
static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr createBlankMSA();
static MinecraftAccountPtr createOffline(const QString &username); static MinecraftAccountPtr createOffline(const QString &username);
@ -177,6 +179,10 @@ public: /* queries */
return "msa"; return "msa";
} }
break; break;
case AccountType::AuthlibInjector: {
return "authlib-injector";
}
break;
case AccountType::Offline: { case AccountType::Offline: {
return "offline"; return "offline";
} }

View File

@ -27,6 +27,25 @@
#include "Application.h" #include "Application.h"
QString Yggdrasil::getBaseUrl()
{
switch (m_data->type)
{
case AccountType::Mojang: {
return "https://authserver.mojang.com";
}
case AccountType::AuthlibInjector: {
return m_data->authlibInjectorApiLocation + "/authserver";
}
// Silence warnings about unhandled enum values for values we know shouldn't be handled.
case AccountType::MSA:
case AccountType::Offline:
break;
}
return "";
}
Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
: AccountTask(data, parent) : AccountTask(data, parent)
{ {
@ -84,7 +103,7 @@ void Yggdrasil::refresh() {
req.insert("requestUser", false); req.insert("requestUser", false);
QJsonDocument doc(req); QJsonDocument doc(req);
QUrl reqUrl("https://authserver.mojang.com/refresh"); QUrl reqUrl = getBaseUrl() + "/refresh";
QByteArray requestData = doc.toJson(); QByteArray requestData = doc.toJson();
sendRequest(reqUrl, requestData); sendRequest(reqUrl, requestData);
@ -129,7 +148,8 @@ void Yggdrasil::login(QString password) {
QJsonDocument doc(req); QJsonDocument doc(req);
QUrl reqUrl("https://authserver.mojang.com/authenticate"); QUrl reqUrl = getBaseUrl() + "/authenticate";
qDebug() << "baseurl = " << getBaseUrl() << "requrl = " << reqUrl;
QNetworkRequest netRequest(reqUrl); QNetworkRequest netRequest(reqUrl);
QByteArray requestData = doc.toJson(); QByteArray requestData = doc.toJson();
@ -273,6 +293,7 @@ void Yggdrasil::processReply() {
AccountTaskState::STATE_FAILED_GONE, AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
); );
break;
} }
default: default:
changeState( changeState(

View File

@ -90,6 +90,7 @@ public slots:
private: private:
void sendRequest(QUrl endpoint, QByteArray content); void sendRequest(QUrl endpoint, QByteArray content);
QString getBaseUrl();
protected: protected:
QNetworkReply *m_netReply = nullptr; QNetworkReply *m_netReply = nullptr;

View File

@ -0,0 +1,29 @@
#include "AuthlibInjector.h"
#include "minecraft/auth/steps/AuthlibInjectorStep.h"
#include "minecraft/auth/steps/MinecraftProfileStepMojang.h"
#include "minecraft/auth/steps/YggdrasilStep.h"
#include "minecraft/auth/steps/MinecraftProfileStep.h"
#include "minecraft/auth/steps/MigrationEligibilityStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
AuthlibInjectorRefresh::AuthlibInjectorRefresh(
AccountData *data,
QObject *parent
) : AuthFlow(data, parent) {
m_steps.append(new AuthlibInjectorStep(m_data));
m_steps.append(new YggdrasilStep(m_data, QString()));
m_steps.append(new MinecraftProfileStepMojang(m_data));
m_steps.append(new GetSkinStep(m_data));
}
AuthlibInjectorLogin::AuthlibInjectorLogin(
AccountData *data,
QString password,
QObject *parent
): AuthFlow(data, parent), m_password(password) {
m_steps.append(new AuthlibInjectorStep(m_data));
m_steps.append(new YggdrasilStep(m_data, m_password));
m_steps.append(new MinecraftProfileStepMojang(m_data));
m_steps.append(new GetSkinStep(m_data));
}

View File

@ -0,0 +1,26 @@
#pragma once
#include "AuthFlow.h"
class AuthlibInjectorRefresh : public AuthFlow
{
Q_OBJECT
public:
explicit AuthlibInjectorRefresh(
AccountData *data,
QObject *parent = 0
);
};
class AuthlibInjectorLogin : public AuthFlow
{
Q_OBJECT
public:
explicit AuthlibInjectorLogin(
AccountData *data,
QString password,
QObject *parent = 0
);
private:
QString m_password;
};

View File

@ -0,0 +1,58 @@
#include "AuthlibInjectorStep.h"
#include "Application.h"
#include <iostream>
#include <QNetworkRequest>
#include <QUuid>
AuthlibInjectorStep::AuthlibInjectorStep(AccountData* data) : AuthStep(data) {
}
AuthlibInjectorStep::~AuthlibInjectorStep() noexcept = default;
QString AuthlibInjectorStep::describe() {
return tr("Fetching authlib injector API URL");
}
void AuthlibInjectorStep::perform() {
// Default to the same as the base URL
QUrl url;
url.setScheme("https");
url.setAuthority(m_data->authlibInjectorBaseUrl);
qDebug() << url << url.toString() << url.isLocalFile();
m_data->authlibInjectorApiLocation = url.toString();
QNetworkRequest request = QNetworkRequest(url);
m_reply.reset( APPLICATION->network()->get(request));
connect(m_reply.get(), &QNetworkReply::finished, this, &AuthlibInjectorStep::onRequestDone);
qDebug() << "Fetching authlib injector API URL";
}
void AuthlibInjectorStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void AuthlibInjectorStep::onRequestDone() {
if (m_reply->hasRawHeader("x-authlib-injector-api-location"))
{
QString authlibInjectorApiLocationHeader = m_reply->rawHeader("x-authlib-injector-api-location");
QUrl url = authlibInjectorApiLocationHeader;
if (!url.isValid())
{
qDebug() << "Invalid Authlib Injector API URL specified by server: " << authlibInjectorApiLocationHeader;
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Invalid authlib injector API URL"));
}
else
{
m_data->authlibInjectorApiLocation = authlibInjectorApiLocationHeader;
qDebug() << "Authlib injector API URL: " << m_data->authlibInjectorApiLocation;
emit finished(AccountTaskState::STATE_WORKING, tr("Fetched authlib injector API URL"));
}
}
else
{
qDebug() << "Authlib injector API URL not found";
emit finished(AccountTaskState::STATE_WORKING, tr("Authlib injector API URL not found, defaulting to the supplied base URL"));
}
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class AuthlibInjectorStep : public AuthStep {
Q_OBJECT
public:
explicit AuthlibInjectorStep(AccountData *data);
virtual ~AuthlibInjectorStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone();
private:
std::unique_ptr<QNetworkReply> m_reply;
};

View File

@ -19,4 +19,7 @@ public:
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
private:
QString baseUrl;
}; };

View File

@ -6,8 +6,24 @@
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) { MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {}
QString MinecraftProfileStepMojang::getBaseUrl()
{
switch (m_data->type)
{
case AccountType::Mojang: {
return "https://sessionserver.mojang.com";
}
case AccountType::AuthlibInjector: {
return m_data->authlibInjectorApiLocation + "/sessionserver";
}
// Silence warnings about unhandled enum values for values we know shouldn't be handled.
case AccountType::MSA:
case AccountType::Offline:
break;
}
return "";
} }
MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default; MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default;
@ -24,7 +40,7 @@ void MinecraftProfileStepMojang::perform() {
} }
// use session server instead of profile due to profile endpoint being locked for locked Mojang accounts // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts
QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id); QUrl url = getBaseUrl() + "/session/minecraft/profile/" + m_data->minecraftProfile.id;
QNetworkRequest req = QNetworkRequest(url); QNetworkRequest req = QNetworkRequest(url);
AuthRequest *request = new AuthRequest(this); AuthRequest *request = new AuthRequest(this);
connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone); connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone);

View File

@ -19,4 +19,7 @@ public:
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
private:
QString getBaseUrl();
}; };

View File

@ -1,5 +1,6 @@
#include "YggdrasilStep.h" #include "YggdrasilStep.h"
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "minecraft/auth/Yggdrasil.h" #include "minecraft/auth/Yggdrasil.h"
@ -15,7 +16,14 @@ YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(dat
YggdrasilStep::~YggdrasilStep() noexcept = default; YggdrasilStep::~YggdrasilStep() noexcept = default;
QString YggdrasilStep::describe() { QString YggdrasilStep::describe() {
switch(m_data->type) {
case(AccountType::Mojang):
return tr("Logging in with Mojang account."); return tr("Logging in with Mojang account.");
case AccountType::AuthlibInjector:
return tr("Logging in with %1 account.").arg(m_data->authlibInjectorBaseUrl);
default:
break;
}
} }
void YggdrasilStep::rehydrate() { void YggdrasilStep::rehydrate() {
@ -32,7 +40,9 @@ void YggdrasilStep::perform() {
} }
void YggdrasilStep::onAuthSucceeded() { void YggdrasilStep::onAuthSucceeded() {
emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")); emit m_data->type == AccountType::Mojang
? finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang"))
: finished(AccountTaskState::STATE_WORKING, tr("Logged in with %1").arg(m_data->authlibInjectorBaseUrl));
} }
void YggdrasilStep::onAuthFailed() { void YggdrasilStep::onAuthFailed() {
@ -41,12 +51,32 @@ void YggdrasilStep::onAuthFailed() {
// m_aborted = m_yggdrasil->m_aborted; // m_aborted = m_yggdrasil->m_aborted;
auto state = m_yggdrasil->taskState(); auto state = m_yggdrasil->taskState();
QString errorMessage = tr("Mojang user authentication failed."); QString errorMessage = m_data->type == AccountType::Mojang
? tr("Mojang user authentication failed.")
: tr("%1 user authentication failed").arg(m_data->authlibInjectorBaseUrl);
// NOTE: soft error in the first step means 'offline' // NOTE: soft error in the first step means 'offline'
if(state == AccountTaskState::STATE_FAILED_SOFT) { if(state == AccountTaskState::STATE_FAILED_SOFT) {
state = AccountTaskState::STATE_OFFLINE; state = AccountTaskState::STATE_OFFLINE;
switch(m_data->type) {
case AccountType::Mojang:
{
errorMessage = tr("Mojang user authentication ended with a network error."); errorMessage = tr("Mojang user authentication ended with a network error.");
break;
}
case AccountType::AuthlibInjector:
{
if(m_data->authlibInjectorBaseUrl.isEmpty())
{
errorMessage = tr("User authentication ended with a network error, did specify a url?");
} else {
errorMessage = tr("%1 user authentication ended with a network error").arg(m_data->authlibInjectorBaseUrl);
}
break;
}
default:
break;
}
} }
emit finished(state, errorMessage); emit finished(state, errorMessage);
} }

View File

@ -0,0 +1,80 @@
#include "ConfigureAuthlibInjector.h"
#include <launch/LaunchTask.h>
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument>
#include <Qt>
#include "Application.h"
#include "minecraft/auth/AccountList.h"
#include "net/ChecksumValidator.h"
#include "net/Download.h"
#include "net/HttpMetaCache.h"
#include "net/NetAction.h"
ConfigureAuthlibInjector::ConfigureAuthlibInjector(LaunchTask* parent,
QString authlibinjector_base_url,
std::shared_ptr<QString> javaagent_arg)
: LaunchStep(parent), m_javaagent_arg{ javaagent_arg }, m_authlibinjector_base_url{ authlibinjector_base_url }
{}
void ConfigureAuthlibInjector::executeTask()
{
auto downloadFailed = [this] (QString reason) {
return emitFailed(QString("Download failed: %1").arg(reason));
};
auto entry = APPLICATION->metacache()->resolveEntry("authlibinjector", "latest.json");
entry->setStale(true);
m_job = std::make_unique<NetJob>("Download authlibinjector latest.json", APPLICATION->network());
auto latestJsonDl =
Net::Download::makeCached(QUrl("https://authlib-injector.yushi.moe/artifact/latest.json"), entry, Net::Download::Option::NoOptions);
m_job->addNetAction(latestJsonDl);
connect(m_job.get(), &NetJob::succeeded, this, [this, entry, downloadFailed] {
QFile authlibInjectorLatestJson = entry->getFullPath();
authlibInjectorLatestJson.open(QIODevice::ReadOnly);
if (!authlibInjectorLatestJson.isOpen())
return emitFailed(QString("Failed to open authlib-injector info json: %1").arg(authlibInjectorLatestJson.errorString()));
QJsonParseError json_parse_error;
QJsonDocument doc = QJsonDocument::fromJson(authlibInjectorLatestJson.readAll(), &json_parse_error);
if (json_parse_error.error != QJsonParseError::NoError)
return emitFailed(QString("Failed to parse authlib-injector info json: %1").arg(json_parse_error.errorString()));
if (!doc.isObject())
return emitFailed(QString("Failed to parse authlib-injector info json: not a json object"));
QJsonObject obj = doc.object();
QString authlibInjectorJarUrl = obj["download_url"].toString();
if (authlibInjectorJarUrl.isNull())
return emitFailed(QString("Failed to parse authlib-injector info json: download url missing"));
QString sha256Sum = obj["checksums"].toObject()["sha256"].toString();
if (sha256Sum.isNull())
return emitFailed("Failed to parse authlib-injector info json: sha256 checksum missing");
auto sha256SumRaw = QByteArray::fromHex(sha256Sum.toLatin1());
QString filename = QFileInfo(authlibInjectorJarUrl).fileName();
auto javaAgentEntry = APPLICATION->metacache()->resolveEntry("authlibinjector", filename);
m_job = std::make_unique<NetJob>("Download authlibinjector java agent", APPLICATION->network());
auto javaAgentDl = Net::Download::makeCached(QUrl(authlibInjectorJarUrl), javaAgentEntry, Net::Download::Option::MakeEternal);
javaAgentDl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha256, sha256SumRaw));
m_job->addNetAction(javaAgentDl);
connect(m_job.get(), &NetJob::succeeded, this, [this, javaAgentEntry] {
auto path = javaAgentEntry->getFullPath();
qDebug() << path;
*m_javaagent_arg = QString("%1=%2").arg(path).arg(m_authlibinjector_base_url);
emitSucceeded();
});
connect(m_job.get(), &NetJob::failed, this, downloadFailed);
m_job->start();
},
// This slot can't run instantly because it needs to wait for the netjob's code to stop running
// Since it will destroy the old netjob by reassigning the unique_ptr
Qt::QueuedConnection);
connect(m_job.get(), &NetJob::failed, this, downloadFailed);
m_job->start();
}
void ConfigureAuthlibInjector::finalize() {}

View File

@ -0,0 +1,24 @@
#pragma once
#include <launch/LaunchStep.h>
#include <minecraft/auth/MinecraftAccount.h>
#include "net/NetJob.h"
class ConfigureAuthlibInjector: public LaunchStep
{
Q_OBJECT
public:
explicit ConfigureAuthlibInjector(LaunchTask *parent, QString authlibinjector_base_url, std::shared_ptr<QString> javaagent_arg);
virtual ~ConfigureAuthlibInjector() {};
void executeTask() override;
void finalize() override;
bool canAbort() const override
{
return false;
}
private:
std::unique_ptr<NetJob> m_job;
std::shared_ptr<QString> m_javaagent_arg;
QString m_authlibinjector_base_url;
};

View File

@ -39,6 +39,9 @@
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#undef major
#undef minor
void VerifyJavaInstall::executeTask() { void VerifyJavaInstall::executeTask() {
auto instance = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance()); auto instance = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
auto packProfile = instance->getPackProfile(); auto packProfile = instance->getPackProfile();

View File

@ -82,6 +82,8 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
// FIXME: Determine if this is a legitimate fallthrough
[[fallthrough]];
} }
case SortType::VERSION: { case SortType::VERSION: {
auto this_ver = Version(version()); auto this_ver = Version(version());

View File

@ -66,6 +66,7 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
return { 1, type == SortType::ENABLED }; return { 1, type == SortType::ENABLED };
if (!enabled() && other.enabled()) if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED }; return { -1, type == SortType::ENABLED };
[[fallthrough]];
case SortType::NAME: { case SortType::NAME: {
QString this_name{ name() }; QString this_name{ name() };
QString other_name{ other.name() }; QString other_name{ other.name() };
@ -76,6 +77,7 @@ std::pair<int, bool> Resource::compare(const Resource& other, SortType type) con
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return { compare_result, type == SortType::NAME }; return { compare_result, type == SortType::NAME };
[[fallthrough]];
} }
case SortType::DATE: case SortType::DATE:
if (dateTimeChanged() > other.dateTimeChanged()) if (dateTimeChanged() > other.dateTimeChanged())

View File

@ -9,13 +9,20 @@
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
// Values taken from: // Values taken from:
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, { 1, { Version("1.6.1"), Version("1.8.9") } },
{ 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, { 2, { Version("1.9"), Version("1.10.2") } },
{ 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 3, { Version("1.11"), Version("1.12.2") } },
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
{ 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } },
{ 7, { Version("1.17"), Version("1.17.1") } },
{ 8, { Version("1.18"), Version("1.18.2") } },
{ 9, { Version("1.19"), Version("1.19.2") } }, { 9, { Version("1.19"), Version("1.19.2") } },
{ 12, { Version("1.19.3"), Version("1.19.3") } },
{ 13, { Version("1.19.4"), Version("1.19.4") } },
{ 14, { Version("1.20"), Version("1.20") } }
}; };
void ResourcePack::setPackFormat(int new_format_id) void ResourcePack::setPackFormat(int new_format_id)
@ -85,6 +92,7 @@ std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type)
auto res = Resource::compare(other, type); auto res = Resource::compare(other, type);
if (res.first != 0) if (res.first != 0)
return res; return res;
[[fallthrough]];
} }
case SortType::PACK_FORMAT: { case SortType::PACK_FORMAT: {
auto this_ver = packFormat(); auto this_ver = packFormat();

View File

@ -49,7 +49,7 @@ class ResourcePack : public Resource {
mutable QMutex m_data_lock; mutable QMutex m_data_lock;
/* The 'version' of a resource pack, as defined in the pack.mcmeta file. /* The 'version' of a resource pack, as defined in the pack.mcmeta file.
* See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta * See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
*/ */
int m_pack_format = 0; int m_pack_format = 0;

View File

@ -115,7 +115,7 @@ void processZIP(ResourcePack& pack)
zip.close(); zip.close();
} }
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) void processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
{ {
try { try {

View File

@ -42,12 +42,13 @@
QByteArray getVariant(SkinUpload::Model model) { QByteArray getVariant(SkinUpload::Model model) {
switch (model) { switch (model) {
default:
qDebug() << "Unknown skin type!";
case SkinUpload::STEVE: case SkinUpload::STEVE:
return "CLASSIC"; return "CLASSIC";
case SkinUpload::ALEX: case SkinUpload::ALEX:
return "SLIM"; return "SLIM";
default:
qDebug() << "Unknown skin type!";
return "CLASSIC";
} }
} }

View File

@ -405,7 +405,8 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
QHash<QString, QString> addonIds; QHash<QString, QString> addonIds;
for (auto const& hash : m_mods.keys()) { for (auto const& hash : m_mods.keys()) {
if (m_temp_versions.contains(hash)) { if (m_temp_versions.contains(hash)) {
auto const& data = m_temp_versions.find(hash).value(); auto const& dataObj = m_temp_versions.find(hash);
auto const& data = dataObj.value();
auto id_str = data.addonId.toString(); auto id_str = data.addonId.toString();
if (!id_str.isEmpty()) if (!id_str.isEmpty())

View File

@ -351,7 +351,7 @@ QString PackInstallTask::getVersionForLoader(QString uid)
if(m_version.loader.recommended || m_version.loader.latest) { if(m_version.loader.recommended || m_version.loader.latest) {
for (int i = 0; i < vlist->versions().size(); i++) { for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i); auto version = vlist->versions().at(i);
auto reqs = version->requires(); auto reqs = version->required();
// filter by minecraft version, if the loader depends on a certain version. // filter by minecraft version, if the loader depends on a certain version.
// not all mod loaders depend on a given Minecraft version, so we won't do this // not all mod loaders depend on a given Minecraft version, so we won't do this

View File

@ -37,7 +37,6 @@ void Flame::FileResolvingTask::executeTask()
void Flame::FileResolvingTask::netJobFinished() void Flame::FileResolvingTask::netJobFinished()
{ {
setProgress(1, 3); setProgress(1, 3);
int index = 0;
// job to check modrinth for blocked projects // job to check modrinth for blocked projects
auto job = new NetJob("Modrinth check", m_network); auto job = new NetJob("Modrinth check", m_network);
blockedProjects = QMap<File *,QByteArray *>(); blockedProjects = QMap<File *,QByteArray *>();
@ -73,7 +72,6 @@ void Flame::FileResolvingTask::netJobFinished()
blockedProjects.insert(&out, output); blockedProjects.insert(&out, output);
} }
} }
index++;
} }
connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished); connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);

View File

@ -158,7 +158,7 @@ void FlameCheckUpdate::executeTask()
pack.addonId = mod->metadata()->project_id; pack.addonId = mod->metadata()->project_id;
pack.websiteUrl = mod->homeurl(); pack.websiteUrl = mod->homeurl();
for (auto& author : mod->authors()) for (auto& author : mod->authors())
pack.authors.append({ author }); pack.authors.append({ author, "" });
pack.description = mod->description(); pack.description = mod->description();
pack.provider = ModPlatform::Provider::FLAME; pack.provider = ModPlatform::Provider::FLAME;

View File

@ -420,7 +420,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
switch (result.type) { switch (result.type) {
case Flame::File::Type::Folder: { case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever. [[fallthrough]];
} }
case Flame::File::Type::SingleFile: case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: { case Flame::File::Type::Mod: {

View File

@ -135,8 +135,6 @@ void PackInstallTask::resolveMods()
m_file_id_map.clear(); m_file_id_map.clear();
Flame::Manifest manifest; Flame::Manifest manifest;
int index = 0;
for (auto const& file : m_version.files) { for (auto const& file : m_version.files) {
if (!file.serverOnly && file.url.isEmpty()) { if (!file.serverOnly && file.url.isEmpty()) {
if (file.curseforge.file_id <= 0) { if (file.curseforge.file_id <= 0) {
@ -154,8 +152,6 @@ void PackInstallTask::resolveMods()
} else { } else {
m_file_id_map.append(-1); m_file_id_map.append(-1);
} }
index++;
} }
m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest); m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest);

View File

@ -150,7 +150,7 @@ void ModrinthCheckUpdate::executeTask()
pack.addonId = mod->metadata()->project_id; pack.addonId = mod->metadata()->project_id;
pack.websiteUrl = mod->homeurl(); pack.websiteUrl = mod->homeurl();
for (auto& author : mod->authors()) for (auto& author : mod->authors())
pack.authors.append({ author }); pack.authors.append({ author, "" });
pack.description = mod->description(); pack.description = mod->description();
pack.provider = ModPlatform::Provider::MODRINTH; pack.provider = ModPlatform::Provider::MODRINTH;

View File

@ -195,8 +195,8 @@ bool ModrinthCreationTask::createInstance()
Override::createOverrides("client-overrides", parent_folder, client_override_path); Override::createOverrides("client-overrides", parent_folder, client_override_path);
// Apply the overrides // Apply the overrides
if (!FS::overrideFolder(mcPath, client_override_path)) { if (!FS::mergeFolders(mcPath, client_override_path)) {
setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); setError(tr("Could not overwrite / create new files:\n") + "client overrides");
return false; return false;
} }
} }
@ -305,6 +305,11 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector<
Modrinth::File file; Modrinth::File file;
file.path = Json::requireString(modInfo, "path"); file.path = Json::requireString(modInfo, "path");
if (QDir::isAbsolutePath(file.path) || QDir::cleanPath(file.path).startsWith("..")) {
qDebug() << "Skipped file that tries to place itself in an absolute location or in a parent directory.";
continue;
}
auto env = Json::ensureObject(modInfo, "env"); auto env = Json::ensureObject(modInfo, "env");
// 'env' field is optional // 'env' field is optional
if (!env.isEmpty()) { if (!env.isEmpty()) {

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file> <file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file> <file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file> <file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file> <file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file> <file>scalable/worlds.svg</file>
</qresource> </qresource>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 24 24"
enable-background="new 0 0 24 24"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs80" />
<rect
fill="none"
width="24"
height="24"
id="rect66" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#FFFFFF"
d="M18.5,5h-1.1c0-0.6-0.6-1-1.4-1c-0.8,0-1.4,0.4-1.4,1h-1.1v2h5V5z"
id="path75" />
<g
id="g3784"
transform="matrix(0.73845672,0,0,0.73845672,4.237417,0.430303)"
style="fill:#e6e6e6;fill-opacity:1;stroke:#585858;stroke-opacity:1"><rect
style="fill:#e6e6e6;fill-opacity:1;stroke:#585858;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:#e6e6e6;fill-opacity:1;stroke:#585858;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#585858;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,8 +1,14 @@
<!DOCTYPE RCC> <!DOCTYPE RCC>
<RCC version="1.0"> <RCC version="1.0">
<qresource prefix="/backgrounds"> <qresource prefix="/backgrounds">
<file alias="kitteh">catbgrnd2.png</file> <file alias="defaultCatmas">catmas.png</file>
<file alias="catmas">catmas.png</file> <file alias="defaultCattiversary">cattiversary.png</file>
<file alias="cattiversary">cattiversary.png</file> <file alias="defaultCat">catbackground.png</file>
<file alias="jinxCat">jinxbg.png</file>
<file alias="jinxCatmas">jinxmas.png</file>
<file alias="jinxCattiversary">jinxversary.png</file>
<file alias="floppaCat">floppabg.png</file>
<file alias="floppaCatmas">floppaxmas.png</file>
<file alias="floppaCattiversary">floppaversary.png</file>
</qresource> </qresource>
</RCC> </RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@ -39,6 +39,7 @@
<file>scalable/status-good.svg</file> <file>scalable/status-good.svg</file>
<file>scalable/status-running.svg</file> <file>scalable/status-running.svg</file>
<file>scalable/status-yellow.svg</file> <file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file> <file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file> <file>scalable/worlds.svg</file>
</qresource> </qresource>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
fill="#757575"
height="24"
viewBox="0 0 24 24"
width="24"
version="1.1"
id="svg1594"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1598" />
<g
id="g3784-3"
transform="matrix(1.0718486,0,0,1.0718486,-5.0666791,-4.7930816)"
style="fill:#757575;fill-opacity:1;stroke:#757575;stroke-opacity:1">
<rect
style="fill:#757575;fill-opacity:1;stroke:#757575;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197-6"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" />
<rect
style="fill:#757575;fill-opacity:1;stroke:#757575;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774-7"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#757575;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1828-5"
cx="21.224644"
cy="21.072649"
r="0.95399094" />
</g>
<g
id="g3784"
transform="matrix(0.95836194,0,0,0.95836194,-3.2596703,-3.0150411)"
style="fill:#757575;fill-opacity:1;stroke:#ffffff;stroke-opacity:1">
<rect
style="fill:#757575;fill-opacity:1;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" />
<rect
style="fill:#757575;fill-opacity:1;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file> <file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file> <file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file> <file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file> <file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file> <file>scalable/worlds.svg</file>
</qresource> </qresource>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
sodipodi:docname="storage.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1540"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="4.9939416"
inkscape:cx="20.925355"
inkscape:cy="15.618925"
inkscape:window-width="909"
inkscape:window-height="1064"
inkscape:window-x="1001"
inkscape:window-y="6"
inkscape:window-maximized="1"
inkscape:current-layer="Calque_1" /><defs
id="defs5152" />
<g
id="g3784"
transform="matrix(1.4769134,0,0,1.4769134,-7.5163878,-7.1393945)"
style="stroke:#3366cc;stroke-opacity:1"><rect
style="fill:none;stroke:#3366cc;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#3366cc;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#3366cc;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file> <file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file> <file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file> <file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file> <file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file> <file>scalable/worlds.svg</file>
</qresource> </qresource>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs4421" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#3366CC"
d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z"
id="path4374" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#DAEEFF"
d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
id="path4376" />
<g
id="g4388">
</g>
<g
id="g4390">
</g>
<g
id="g4392">
</g>
<g
id="g4394">
</g>
<g
id="g4396">
</g>
<g
id="g4398">
</g>
<g
id="g4400">
</g>
<g
id="g4402">
</g>
<g
id="g4404">
</g>
<g
id="g4406">
</g>
<g
id="g4408">
</g>
<g
id="g4410">
</g>
<g
id="g4412">
</g>
<g
id="g4414">
</g>
<g
id="g4416">
</g>
<g
id="g3784"
transform="translate(0.07734108,0.33259869)"
style="stroke:#666666;stroke-opacity:1"><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#666666;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file> <file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file> <file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file> <file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file> <file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file> <file>scalable/worlds.svg</file>
</qresource> </qresource>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
sodipodi:docname="storage.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview271"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="9.9878833"
inkscape:cx="15.468743"
inkscape:cy="20.524869"
inkscape:window-width="909"
inkscape:window-height="1064"
inkscape:window-x="1001"
inkscape:window-y="6"
inkscape:window-maximized="1"
inkscape:current-layer="g3784" /><defs
id="defs3662" />
<g
id="g3645">
<rect
x="6"
y="0"
fill="none"
width="20"
height="0"
id="rect3635" />
<polygon
fill="none"
points="0,6 0,6 0,26 0,26 0,9 "
id="polygon3637" />
<polygon
fill="none"
points="32,6 32,9 32,26 32,26 32,6 "
id="polygon3639" />
<path
fill="#39B54A"
d="M32,9V6c0-3.3-2.7-6-6-6H6C2.7,0,0,2.7,0,6v3H32z"
id="path3641" />
<path
fill="#8C6239"
d="M0,9v17c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0z"
id="path3643" />
</g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#F2F2F2"
d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
id="path3647" />
<g
id="g3784"
transform="translate(0.07734108,0.33259869)"
style="stroke:#666666;stroke-opacity:1"><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#666666;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file> <file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file> <file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file> <file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file> <file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file> <file>scalable/worlds.svg</file>
</qresource> </qresource>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs2339" />
<g
id="g2306">
</g>
<g
id="g2308">
</g>
<g
id="g2310">
</g>
<g
id="g2312">
</g>
<g
id="g2314">
</g>
<g
id="g2316">
</g>
<g
id="g2318">
</g>
<g
id="g2320">
</g>
<g
id="g2322">
</g>
<g
id="g2324">
</g>
<g
id="g2326">
</g>
<g
id="g2328">
</g>
<g
id="g2330">
</g>
<g
id="g2332">
</g>
<g
id="g2334">
</g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#f2f2f2"
d="m 26.077341,32.020895 h -20 c -3.3,0 -6.00000002,-2.7 -6.00000002,-6 V 6.0208954 c 0,-3.3 2.70000002,-5.99999999 6.00000002,-5.99999999 h 20 c 3.3,0 6,2.69999999 6,5.99999999 V 26.020895 c 0,3.3 -2.7,6 -6,6 z"
id="path659"
style="fill:#000000;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4d4d4d"
d="m 28.077341,6.0208954 c 0,-1.1 -0.9,-2 -2,-2 h -20 c -1.1,0 -2,0.9 -2,2 V 26.020895 c 0,1.1 0.9,2 2,2 h 20 c 1.1,0 2,-0.9 2,-2 z"
id="path661"
style="fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-opacity:1" /><rect
style="fill:none;fill-opacity:1;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.270771"
y="6.9590669"
ry="2.1458371" /><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.270771"
y="17.769564"
ry="1.7275306" /><circle
style="fill:#666666;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.301985"
cy="21.093542"
r="0.95399094" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file> <file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file> <file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file> <file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file> <file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file> <file>scalable/worlds.svg</file>
</qresource> </qresource>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs706" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#F2F2F2"
d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z"
id="path659" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4D4D4D"
d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
id="path661" />
<g
id="g673">
</g>
<g
id="g675">
</g>
<g
id="g677">
</g>
<g
id="g679">
</g>
<g
id="g681">
</g>
<g
id="g683">
</g>
<g
id="g685">
</g>
<g
id="g687">
</g>
<g
id="g689">
</g>
<g
id="g691">
</g>
<g
id="g693">
</g>
<g
id="g695">
</g>
<g
id="g697">
</g>
<g
id="g699">
</g>
<g
id="g701">
</g>
<rect
style="fill:none;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#ffffff;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -101,9 +101,8 @@ void ConcurrentTask::startNext()
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
updateState(); updateState();
QCoreApplication::processEvents(); QMetaObject::invokeMethod(
this, [=] { next->start(); }, Qt::QueuedConnection);
next->start();
} }
void ConcurrentTask::subTaskSucceeded(Task::Ptr task) void ConcurrentTask::subTaskSucceeded(Task::Ptr task)

View File

@ -226,7 +226,9 @@ void TranslationsModel::indexReceived()
reloadLocalFiles(); reloadLocalFiles();
auto language = d->m_system_locale; auto language = d->m_system_locale;
if (!findLanguage(language)) auto languageIterator = findLanguage(language);
if (languageIterator == decltype(languageIterator){})
{ {
language = d->m_system_language; language = d->m_system_language;
} }
@ -259,7 +261,6 @@ void readIndex(const QString & path, QMap<QString, Language>& languages)
return; return;
} }
int index = 1;
try try
{ {
auto toplevel_doc = Json::requireDocument(data); auto toplevel_doc = Json::requireDocument(data);
@ -292,7 +293,6 @@ void readIndex(const QString & path, QMap<QString, Language>& languages)
lang.file_size = Json::requireInteger(langObj, "size"); lang.file_size = Json::requireInteger(langObj, "size");
languages.insert(lang.key, lang); languages.insert(lang.key, lang);
index++;
} }
} }
catch (Json::JsonException & e) catch (Json::JsonException & e)
@ -418,8 +418,6 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
return QVariant(); return QVariant();
int row = index.row(); int row = index.row();
auto column = static_cast<Column>(index.column());
if (row < 0 || row >= d->m_languages.size()) if (row < 0 || row >= d->m_languages.size())
return QVariant(); return QVariant();
@ -428,22 +426,19 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
{ {
case Qt::DisplayRole: case Qt::DisplayRole:
{ {
auto column = static_cast<Column>(index.column());
switch(column) switch(column)
{ {
case Column::Language: case Column::Language:
{
return lang.languageName(); return lang.languageName();
}
case Column::Completeness: case Column::Completeness:
{
return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1);
} default:
return QVariant();
} }
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
{
return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total)); return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total));
}
case Qt::UserRole: case Qt::UserRole:
return lang.key; return lang.key;
default: default:
@ -495,7 +490,7 @@ int TranslationsModel::columnCount(const QModelIndex& parent) const
return 2; return 2;
} }
Language * TranslationsModel::findLanguage(const QString& key) QVector<Language>::iterator TranslationsModel::findLanguage(const QString& key)
{ {
auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang) auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang)
{ {
@ -503,7 +498,7 @@ Language * TranslationsModel::findLanguage(const QString& key)
}); });
if(found == d->m_languages.end()) if(found == d->m_languages.end())
{ {
return nullptr; return {};
} }
else else
{ {
@ -514,21 +509,21 @@ Language * TranslationsModel::findLanguage(const QString& key)
bool TranslationsModel::selectLanguage(QString key) bool TranslationsModel::selectLanguage(QString key)
{ {
QString &langCode = key; QString &langCode = key;
auto langPtr = findLanguage(key); auto langIterator = findLanguage(key);
if (langCode.isEmpty()) if (langCode.isEmpty())
{ {
d->no_language_set = true; d->no_language_set = true;
} }
if(!langPtr) if (langIterator == decltype(langIterator){})
{ {
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode; langCode = defaultLangCode;
} }
else else
{ {
langCode = langPtr->key; langCode = langIterator->key;
} }
// uninstall existing translators if there are any // uninstall existing translators if there are any
@ -580,7 +575,7 @@ bool TranslationsModel::selectLanguage(QString key)
d->m_qt_translator.reset(); d->m_qt_translator.reset();
} }
if(langPtr->localFileType == FileType::PO) if(langIterator->localFileType == FileType::PO)
{ {
qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po")); auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po"));
@ -603,7 +598,7 @@ bool TranslationsModel::selectLanguage(QString key)
d->m_app_translator.reset(); d->m_app_translator.reset();
} }
} }
else if(langPtr->localFileType == FileType::QM) else if(langIterator->localFileType == FileType::QM)
{ {
d->m_app_translator.reset(new QTranslator()); d->m_app_translator.reset(new QTranslator());
if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path()))
@ -635,7 +630,7 @@ bool TranslationsModel::selectLanguage(QString key)
QModelIndex TranslationsModel::selectedIndex() QModelIndex TranslationsModel::selectedIndex()
{ {
auto found = findLanguage(d->m_selectedLanguage); auto found = findLanguage(d->m_selectedLanguage);
if(found) if(found != decltype(found){})
{ {
// QVector iterator freely converts to pointer to contained type // QVector iterator freely converts to pointer to contained type
return index(found - d->m_languages.begin(), 0, QModelIndex()); return index(found - d->m_languages.begin(), 0, QModelIndex());
@ -673,7 +668,7 @@ void TranslationsModel::updateLanguage(QString key)
return; return;
} }
auto found = findLanguage(key); auto found = findLanguage(key);
if(!found) if(found == decltype(found){})
{ {
qWarning() << "Cannot update invalid language" << key; qWarning() << "Cannot update invalid language" << key;
return; return;
@ -692,7 +687,7 @@ void TranslationsModel::downloadTranslation(QString key)
return; return;
} }
auto lang = findLanguage(key); auto lang = findLanguage(key);
if(!lang) if(lang == decltype(lang){})
{ {
qWarning() << "Will not download an unknown translation" << key; qWarning() << "Will not download an unknown translation" << key;
return; return;

View File

@ -40,7 +40,7 @@ public:
void downloadIndex(); void downloadIndex();
private: private:
Language *findLanguage(const QString & key); QVector<Language>::iterator findLanguage(const QString & key);
void reloadLocalFiles(); void reloadLocalFiles();
void downloadTranslation(QString key); void downloadTranslation(QString key);
void downloadNext(); void downloadNext();

View File

@ -1483,27 +1483,40 @@ void MainWindow::setCatBackground(bool enabled)
{ {
QDateTime now = QDateTime::currentDateTime(); QDateTime now = QDateTime::currentDateTime();
QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0));
QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); QDateTime christmasStart(QDate(now.date().year(), 12, 25), QTime(0, 0));
QString cat; QDateTime christmasEnd(QDate(now.date().year(), 1, 7), QTime(0, 0)); //end at midnight of the 7th
if(non_stupid_abs(now.daysTo(xmas)) <= 4) {
cat = "catmas"; QString cat = "default";
QString catStyleOpt = APPLICATION->settings()->get("CatStyle").toString();
if(catStyleOpt == "Floppa")
cat = "floppa";
else if(catStyleOpt == "Jinx")
cat = "jinx";
if(christmasStart <= now || now < christmasEnd) {
cat += "Catmas";
} }
else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { else if (non_stupid_abs(now.daysTo(birthday)) <= 12) {
cat = "cattiversary"; cat += "Cattiversary";
} }
else { else {
cat = "kitteh"; cat += "Cat";
} }
auto cat_position = APPLICATION->settings()->get("CatPosition").toString().toLower().trimmed();
if (cat_position != "top left" && cat_position != "bottom left" && cat_position != "bottom right" && cat_position != "top right")
cat_position = "top right";
view->setStyleSheet(QString(R"( view->setStyleSheet(QString(R"(
InstanceView InstanceView
{ {
background-image: url(:/backgrounds/%1); background-image: url(:/backgrounds/%1);
background-attachment: fixed; background-attachment: fixed;
background-clip: padding; background-clip: padding;
background-position: top right; background-position: %2;
background-repeat: none; background-repeat: none;
background-color:palette(base); background-color:palette(base);
})").arg(cat)); })").arg(cat, cat_position));
} }
else else
{ {
@ -1569,8 +1582,7 @@ void MainWindow::finalizeInstance(InstancePtr inst)
{ {
view->updateGeometries(); view->updateGeometries();
setSelectedInstanceById(inst->id()); setSelectedInstanceById(inst->id());
if (APPLICATION->accounts()->anyAccountIsValid()) if (APPLICATION->accounts()->drmCheck()) {
{
ProgressDialog loadDialog(this); ProgressDialog loadDialog(this);
auto update = inst->createUpdateTask(Net::Mode::Online); auto update = inst->createUpdateTask(Net::Mode::Online);
connect(update.get(), &Task::failed, [this](QString reason) connect(update.get(), &Task::failed, [this](QString reason)
@ -1583,9 +1595,7 @@ void MainWindow::finalizeInstance(InstancePtr inst)
loadDialog.setSkipButton(true, tr("Abort")); loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(update.get()); loadDialog.execWithTask(update.get());
} }
} } else {
else
{
CustomMessageBox::selectable( CustomMessageBox::selectable(
this, this,
tr("Error"), tr("Error"),
@ -1803,6 +1813,7 @@ void MainWindow::globalSettingsClosed()
updateMainToolBar(); updateMainToolBar();
updateToolsMenu(); updateToolsMenu();
updateStatusCenter(); updateStatusCenter();
updateCat();
// This needs to be done to prevent UI elements disappearing in the event the config is changed // This needs to be done to prevent UI elements disappearing in the event the config is changed
// but PolyMC exits abnormally, causing the window state to never be saved: // but PolyMC exits abnormally, causing the window state to never be saved:
APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); APPLICATION->settings()->set("MainWindowState", saveState().toBase64());
@ -2153,6 +2164,11 @@ void MainWindow::updateStatusCenter()
} }
} }
void MainWindow::updateCat()
{
setCatBackground(APPLICATION->settings()->get("TheCat").toBool());
}
void MainWindow::refreshCurrentInstance(bool running) void MainWindow::refreshCurrentInstance(bool running)
{ {
auto current = view->selectionModel()->currentIndex(); auto current = view->selectionModel()->currentIndex();

View File

@ -195,6 +195,8 @@ private slots:
void updateNewsLabel(); void updateNewsLabel();
void updateCat();
void konamiTriggered(); void konamiTriggered();
void globalSettingsClosed(); void globalSettingsClosed();

View File

@ -4,29 +4,102 @@
namespace WinDarkmode { namespace WinDarkmode {
/* See https://github.com/statiolake/neovim-qt/commit/da8eaba7f0e38b6b51f3bacd02a8cc2d1f7a34d8 */ template<int syscall_id, typename... arglist> __attribute((naked)) uint32_t __fastcall WinSyscall([[maybe_unused]] arglist... args)
void setDarkWinTitlebar(WId winid, bool darkmode)
{ {
HWND hwnd = reinterpret_cast<HWND>(winid); asm volatile("mov %%rcx, %%r10; movl %0, %%eax; syscall; ret"
BOOL dark = (BOOL) darkmode; :: "i"(syscall_id));
}
HMODULE hUxtheme = LoadLibraryExW(L"uxtheme.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); VOID ApplyStringProp(HWND hWnd, LPCWSTR lpString, WORD Property)
HMODULE hUser32 = GetModuleHandleW(L"user32.dll"); {
fnAllowDarkModeForWindow AllowDarkModeForWindow WORD Prop = (uint16_t)(uint64_t)GetPropW(hWnd, (LPCWSTR)(uint64_t)Property);
= reinterpret_cast<fnAllowDarkModeForWindow>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133))); if (Prop)
fnSetPreferredAppMode SetPreferredAppMode {
= reinterpret_cast<fnSetPreferredAppMode>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135))); DeleteAtom(Prop);
fnSetWindowCompositionAttribute SetWindowCompositionAttribute RemovePropW(hWnd, (LPCWSTR)(uint64_t)Property);
= reinterpret_cast<fnSetWindowCompositionAttribute>(GetProcAddress(hUser32, "SetWindowCompositionAttribute")); }
if (lpString)
{
ATOM v = AddAtomW(lpString);
if (v)
SetPropW(hWnd, (LPCWSTR)(uint64_t)Property, (HANDLE)(uint64_t)v);
}
}
SetPreferredAppMode(AllowDark); VOID AllowDarkModeForWindow(HWND hWnd, BOOL Enable)
AllowDarkModeForWindow(hwnd, dark); {
if (hWnd)
{
ApplyStringProp(hWnd, Enable ? L"Enabled" : NULL, 0xA91E);
}
return;
}
BOOL IsWindows11()
{
HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll");
return GetProcAddress(hKern32, "Wow64SetThreadDefaultGuestMachine") != NULL; // Win11 21h2+
}
BOOL IsWindows10_Only()
{
HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll");
HMODULE hNtuser = GetModuleHandleW(L"ntdll.dll");
return GetProcAddress(hKern32, "SetThreadSelectedCpuSets") != NULL
&& GetProcAddress(hNtuser, "ZwSetInformationCpuPartition") == NULL;
}
BOOL IsWindows8_0_Only()
{
HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll");
return GetProcAddress(hKern32, "CreateFile2") != NULL // Export added in 6.2 (8)
&& GetProcAddress(hKern32, "AppXFreeMemory") != NULL; // Export added in 6.2 (8), removed in 6.3 (8.1)
}
BOOL IsWindows8_1_Only()
{
HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll");
return GetProcAddress(hKern32, "CalloutOnFiberStack") != NULL // Export added in 6.3 (8.1), Removed in 10.0.10586
&& GetProcAddress(hKern32, "SetThreadSelectedCpuSets") == NULL; // Export added in 10.0 (10)
}
void setWindowDarkModeEnabled(HWND hWnd, bool Enabled)
{
AllowDarkModeForWindow(hWnd, Enabled);
BOOL DarkEnabled = (BOOL)Enabled;
WINDOWCOMPOSITIONATTRIBDATA data = { WINDOWCOMPOSITIONATTRIBDATA data = {
WCA_USEDARKMODECOLORS, WCA_USEDARKMODECOLORS,
&dark, &DarkEnabled,
sizeof(dark) sizeof(DarkEnabled)
}; };
SetWindowCompositionAttribute(hwnd, &data);
#ifdef _WIN64
constexpr int NtUserSetWindowCompositionAttribute_NT6_2 = 0x13b4;
constexpr int NtUserSetWindowCompositionAttribute_NT6_3 = 0x13e5;
if (IsWindows8_0_Only())
WinSyscall<NtUserSetWindowCompositionAttribute_NT6_2>(hWnd, &data);
else if (IsWindows8_1_Only())
WinSyscall<NtUserSetWindowCompositionAttribute_NT6_3>(hWnd, &data);
else if (IsWindows10_Only() || IsWindows11())
{
((fnSetWindowCompositionAttribute)(PVOID)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute"))
(hWnd, &data);
// Verified this ordinal is the same through Win11 22H2 (5/8/2023)
((fnSetPreferredAppMode)(PVOID)GetProcAddress(GetModuleHandleW(L"uxtheme.dll"), MAKEINTRESOURCEA(135)))
(AppMode_AllowDark);
}
#else
if (IsWindows10_Only())
{
((fnSetWindowCompositionAttribute)(PVOID)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute"))
(hWnd, &data);
// Verified this ordinal is the same through Win11 22H2 (5/8/2023)
((fnSetPreferredAppMode)(PVOID)GetProcAddress(GetModuleHandleW(L"uxtheme.dll"), MAKEINTRESOURCEA(135)))
(AppMode_AllowDark);
}
#endif
} }
} }

View File

@ -6,14 +6,14 @@
namespace WinDarkmode { namespace WinDarkmode {
void setDarkWinTitlebar(WId winid, bool darkmode); void setWindowDarkModeEnabled(HWND hWnd, bool Enabled);
enum PreferredAppMode { enum PreferredAppMode {
Default, AppMode_Default,
AllowDark, AppMode_AllowDark,
ForceDark, AppMode_ForceDark,
ForceLight, AppMode_ForceLight,
Max AppMode_Max
}; };
enum WINDOWCOMPOSITIONATTRIB { enum WINDOWCOMPOSITIONATTRIB {

View File

@ -20,9 +20,10 @@
#include <QtWidgets/QPushButton> #include <QtWidgets/QPushButton>
LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog) LoginDialog::LoginDialog(QWidget *parent, AccountType type) : QDialog(parent), ui(new Ui::LoginDialog), m_accountType{type}
{ {
ui->setupUi(this); ui->setupUi(this);
ui->authlibInjectorBaseTextBox->setVisible(m_accountType == AccountType::AuthlibInjector);
ui->progressBar->setVisible(false); ui->progressBar->setVisible(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
@ -42,7 +43,14 @@ void LoginDialog::accept()
ui->progressBar->setVisible(true); ui->progressBar->setVisible(true);
// Setup the login task and start it // Setup the login task and start it
if (m_accountType == AccountType::Mojang)
{
m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text());
}
else if (m_accountType == AccountType::AuthlibInjector)
{
m_account = MinecraftAccount::createAuthlibInjectorFromUsername(ui->userTextBox->text(), ui->authlibInjectorBaseTextBox->text());
}
m_loginTask = m_account->login(ui->passTextBox->text()); m_loginTask = m_account->login(ui->passTextBox->text());
connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded);
@ -106,10 +114,9 @@ void LoginDialog::onTaskProgress(qint64 current, qint64 total)
ui->progressBar->setValue(current); ui->progressBar->setValue(current);
} }
// Public interface MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg, AccountType type)
MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
{ {
LoginDialog dlg(parent); LoginDialog dlg(parent, type);
dlg.ui->label->setText(msg); dlg.ui->label->setText(msg);
if (dlg.exec() == QDialog::Accepted) if (dlg.exec() == QDialog::Accepted)
{ {

View File

@ -18,6 +18,7 @@
#include <QtWidgets/QDialog> #include <QtWidgets/QDialog>
#include <QtCore/QEventLoop> #include <QtCore/QEventLoop>
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/MinecraftAccount.h" #include "minecraft/auth/MinecraftAccount.h"
#include "tasks/Task.h" #include "tasks/Task.h"
@ -33,10 +34,13 @@ class LoginDialog : public QDialog
public: public:
~LoginDialog(); ~LoginDialog();
static MinecraftAccountPtr newAccount(QWidget *parent, QString message); /*
* @param type: Mojang or Authlib
*/
static MinecraftAccountPtr newAccount(QWidget *parent, QString message, AccountType type = AccountType::Mojang);
private: private:
explicit LoginDialog(QWidget *parent = 0); explicit LoginDialog(QWidget *parent = 0, AccountType type = AccountType::Mojang);
void setUserInputsEnabled(bool enable); void setUserInputsEnabled(bool enable);
@ -56,4 +60,5 @@ private:
Ui::LoginDialog *ui; Ui::LoginDialog *ui;
MinecraftAccountPtr m_account; MinecraftAccountPtr m_account;
Task::Ptr m_loginTask; Task::Ptr m_loginTask;
AccountType m_accountType;
}; };

View File

@ -33,6 +33,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLineEdit" name="authlibInjectorBaseTextBox">
<property name="placeholderText">
<string>AuthlibInjector base URL (e.g. ely.by)</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QLineEdit" name="userTextBox"> <widget class="QLineEdit" name="userTextBox">
<property name="placeholderText"> <property name="placeholderText">

View File

@ -30,7 +30,8 @@ NewsDialog::~NewsDialog()
void NewsDialog::selectedArticleChanged(const QString& new_title) void NewsDialog::selectedArticleChanged(const QString& new_title)
{ {
auto const& article_entry = m_entries.constFind(new_title).value(); auto const& article_entry_ptr = m_entries.constFind(new_title);
auto const& article_entry = article_entry_ptr.value();
ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, new_title)); ui->articleTitleLabel->setText(QString("<a href='%1'>%2</a>").arg(article_entry->link, new_title));
ui->currentArticleContentBrowser->setText(article_entry->content); ui->currentArticleContentBrowser->setText(article_entry->content);

View File

@ -220,10 +220,10 @@ bool AccessibleInstanceView::selectRow(int row)
} }
break; break;
} }
default: { default:
qWarning() << "Unhandled QAbstractItemView selection type!";
break; break;
} }
}
view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
return true; return true;
@ -248,7 +248,7 @@ bool AccessibleInstanceView::selectColumn(int column)
if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) { if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) {
return false; return false;
} }
// fallthrough intentional [[fallthrough]];
} }
case QAbstractItemView::ContiguousSelection: { case QAbstractItemView::ContiguousSelection: {
if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
@ -279,7 +279,7 @@ bool AccessibleInstanceView::unselectRow(int row)
QItemSelection selection(index, index); QItemSelection selection(index, index);
auto selectionModel = view()->selectionModel(); auto selectionModel = view()->selectionModel();
switch (view()->selectionMode()) { switch (const auto selectionType = view()->selectionMode()) {
case QAbstractItemView::SingleSelection: case QAbstractItemView::SingleSelection:
// no unselect // no unselect
if (selectedRowCount() == 1) { if (selectedRowCount() == 1) {
@ -298,8 +298,13 @@ bool AccessibleInstanceView::unselectRow(int row)
//the ones which are down the current row will be deselected //the ones which are down the current row will be deselected
selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex())); selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex()));
} }
break;
} }
case QAbstractItemView::NoSelection:
break;
default: { default: {
// FIXME: See if MultiSelection / ExtendedSelection need to be handled
qWarning() << "Unhandled QAbstractItemView selection type!" << selectionType;
break; break;
} }
} }
@ -342,6 +347,7 @@ bool AccessibleInstanceView::unselectColumn(int column)
//of the current row, the ones which are at the right will be deselected //of the current row, the ones which are at the right will be deselected
selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex())); selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex()));
} }
break;
default: default:
break; break;
} }

View File

@ -40,10 +40,8 @@
#include <QMessageBox> #include <QMessageBox>
#include <QFileDialog> #include <QFileDialog>
#include <QRegularExpression>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTabBar> #include <QTabBar>
#include <QValidator>
#include <QVariant> #include <QVariant>
#include "settings/SettingsObject.h" #include "settings/SettingsObject.h"
@ -82,9 +80,9 @@ APIPage::APIPage(QWidget *parent) :
connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder);
// This function needs to be called even when the ComboBox's index is still in its default state. // This function needs to be called even when the ComboBox's index is still in its default state.
updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex());
ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); ui->baseURLEntry->setValidator(new TrimmedRegExValidator(validUrlRegExp, ui->baseURLEntry));
ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); ui->msaClientID->setValidator(new TrimmedRegExValidator(validMSAClientID, ui->msaClientID));
ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); ui->flameKey->setValidator(new TrimmedRegExValidator(validFlameKey, ui->flameKey));
ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->metaURL->setPlaceholderText(BuildConfig.META_URL);
ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT);

View File

@ -4,6 +4,7 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2022 Lenny McLennington <lenny@sneed.church> * Copyright (c) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (c) 2022 jdp_ (https://github.com/jdpatdiscord)
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -38,14 +39,29 @@
#pragma once #pragma once
#include <QWidget> #include <QWidget>
#include <QValidator>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include "ui/pages/BasePage.h" #include "ui/pages/BasePage.h"
#include <Application.h> #include <Application.h>
namespace Ui { namespace Ui
{
class APIPage; class APIPage;
} }
class TrimmedRegExValidator : public QRegularExpressionValidator
{
using QRegularExpressionValidator::QRegularExpressionValidator;
virtual QValidator::State validate(QString& input, int& npos) const override
{
input = input.trimmed();
return QRegularExpressionValidator::validate(input, npos);
}
};
class APIPage : public QWidget, public BasePage class APIPage : public QWidget, public BasePage
{ {
Q_OBJECT Q_OBJECT

View File

@ -157,6 +157,36 @@ void AccountListPage::on_actionAddMojang_triggered()
} }
} }
void AccountListPage::on_actionAddAuthlibInjector_triggered()
{
if (!m_accounts->drmCheck()) {
QMessageBox::warning(
this,
tr("Error"),
tr(
"You must add a Microsoft or Mojang account that owns Minecraft before you can add an Authlib Injector account."
"<br><br>"
"If you have lost your account you can contact Microsoft for support."
)
);
return;
}
MinecraftAccountPtr account = LoginDialog::newAccount(
this,
tr("Please enter the AuthlibInjector base URL, and enter your account email and password to add your account."),
AccountType::AuthlibInjector
);
if (account)
{
m_accounts->addAccount(account);
if (m_accounts->count() == 1) {
m_accounts->setDefaultAccount(account);
}
}
}
void AccountListPage::on_actionAddMicrosoft_triggered() void AccountListPage::on_actionAddMicrosoft_triggered()
{ {
if(BuildConfig.BUILD_PLATFORM == "osx64") { if(BuildConfig.BUILD_PLATFORM == "osx64") {

View File

@ -83,6 +83,7 @@ public:
public slots: public slots:
void on_actionAddMojang_triggered(); void on_actionAddMojang_triggered();
void on_actionAddAuthlibInjector_triggered();
void on_actionAddMicrosoft_triggered(); void on_actionAddMicrosoft_triggered();
void on_actionAddOffline_triggered(); void on_actionAddOffline_triggered();
void on_actionRemove_triggered(); void on_actionRemove_triggered();

View File

@ -54,6 +54,7 @@
</attribute> </attribute>
<addaction name="actionAddMicrosoft"/> <addaction name="actionAddMicrosoft"/>
<addaction name="actionAddMojang"/> <addaction name="actionAddMojang"/>
<addaction name="actionAddAuthlibInjector"/>
<addaction name="actionAddOffline"/> <addaction name="actionAddOffline"/>
<addaction name="actionRefresh"/> <addaction name="actionRefresh"/>
<addaction name="actionRemove"/> <addaction name="actionRemove"/>
@ -68,6 +69,11 @@
<string>Add &amp;Mojang</string> <string>Add &amp;Mojang</string>
</property> </property>
</action> </action>
<action name="actionAddAuthlibInjector">
<property name="text">
<string>Add Authlib-&amp;Injector</string>
</property>
</action>
<action name="actionRemove"> <action name="actionRemove">
<property name="text"> <property name="text">
<string>Remo&amp;ve</string> <string>Remo&amp;ve</string>

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