Compare commits

...

214 commits

Author SHA1 Message Date
Alexandru Ionut Tripon
d2fa7cf7f7
When auth is down, launch into offline mode (#5647)
Some checks failed
Nix / Build (aarch64-darwin) (push) Has been cancelled
Nix / Build (x86_64-linux) (push) Has been cancelled
Nix / Build (aarch64-linux) (push) Has been cancelled
2026-06-26 13:23:39 +00:00
Alexandru Ionut Tripon
585aa6e674
Add the option to sort instances by total playtime (#5714) 2026-06-26 12:18:44 +00:00
Alexandru Ionut Tripon
475ab8a208
Trim whitespace from environment variables (#5704) 2026-06-26 12:17:49 +00:00
Alexandru Ionut Tripon
62f537dd8d
Sort mod Minecraft versions as version lists (#5705) 2026-06-26 11:55:20 +00:00
DioEgizio
9c2c641531
fix: remove remaining 16x16 new.png (#5721)
Some checks are pending
Nix / Build (aarch64-darwin) (push) Waiting to run
Nix / Build (x86_64-linux) (push) Waiting to run
Nix / Build (aarch64-linux) (push) Waiting to run
2026-06-25 09:41:26 +00:00
DioEgizio
402379a841
Fixed dependencies not enabling/disabling other dependencies (#5717) 2026-06-25 09:31:29 +00:00
DioEgizio
cb56c641d7 fix: remove remaining 16x16 new.png
Signed-off-by: DioEgizio <dioegizio@protonmail.com>
2026-06-25 09:44:31 +02:00
DioEgizio
5b051e7d49
Use native APIs for GPU discovery (#5602) 2026-06-25 07:40:50 +00:00
James Zhou
f181b5d0d7 fix: recursive mod dependencies
Signed-off-by: James Zhou <yunchengzhou@gmail.com>
2026-06-24 10:39:01 -04:00
Anceph
4dc107b1fa
Change "By playtime" to "By total time played"
Co-authored-by: TheKodeToad <TheKodeToad@proton.me>
Signed-off-by: Anceph <41387237+Anceph@users.noreply.github.com>
2026-06-24 13:12:16 +03:00
Anceph
34783c10fe
Remove the comment
Signed-off-by: Anceph <41387237+Anceph@users.noreply.github.com>
2026-06-24 00:49:43 +03:00
Anceph
a607373ced Added the option to sort instances by total playtime (#5701)
Signed-off-by: Anceph <yucehasan31@gmail.com>
2026-06-24 00:23:31 +03:00
Andrey Kurlin
9752f9dfd7 fix: sort mod Minecraft versions as version lists
Signed-off-by: Andrey Kurlin <superkurlin2013@yandex.ru>
2026-06-21 23:58:54 +05:00
Andrey Kurlin
0175653881 fix: trim whitespace from environment variables
Signed-off-by: Andrey Kurlin <superkurlin2013@yandex.ru>
2026-06-21 21:18:54 +05:00
Alexandru Ionut Tripon
f654ce8212
Show process start error string in logs (#5644)
Some checks failed
Nix / Build (aarch64-darwin) (push) Has been cancelled
Nix / Build (x86_64-linux) (push) Has been cancelled
Nix / Build (aarch64-linux) (push) Has been cancelled
2026-06-17 09:57:12 +00:00
Alexandru Ionut Tripon
a8643739f0
Added missing tab stops and/or fixed their order (#5662) 2026-06-17 09:57:06 +00:00
Alexandru Ionut Tripon
d8d2f383e9
Include Flame API key in CDN downloads (#5671) 2026-06-17 09:55:30 +00:00
Tayou
2425d2f8a3
merge dialogs
Signed-off-by: Tayou <git@tayou.org>
2026-06-12 16:33:50 +02:00
Octol1ttle
b387a1f793
change(ApiHeaderProxy): include Flame API key for CDN downloads
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-06-12 19:24:06 +05:00
Tayou
d71cfc33a2
show error code in dialog, change text for no internet
Signed-off-by: Tayou <git@tayou.org>
2026-06-11 19:51:12 +02:00
Tayou
5f59aa5829
Show error message when device code authorization fails (#5645)
Some checks failed
Nix / Build (aarch64-darwin) (push) Has been cancelled
Nix / Build (x86_64-linux) (push) Has been cancelled
Nix / Build (aarch64-linux) (push) Has been cancelled
2026-06-11 13:25:36 +00:00
Tayou
768d12259b
add info & retry dialog
Signed-off-by: Tayou <git@tayou.org>
2026-06-11 14:39:19 +02:00
Tayou
0a3adb7912
on server errors, treat account as offline
Signed-off-by: Tayou <git@tayou.org>
2026-06-11 14:39:19 +02:00
Octol1ttle
a90e3d403d
fix(LauncherPartLaunch): show process start error string
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-06-10 17:19:55 +05:00
Octol1ttle
8de7aa2b17
fix(JavaChecker): show process start error string
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-06-10 17:10:33 +05:00
Dominik Pakuła
022d2fe7cd Added missing tab stops and/or fixed their order
Signed-off-by: Dominik Pakuła <domi@domi.click>
2026-06-09 19:27:54 +02:00
Tayou
803115cfde
add category selector to icon picker dialog (#4397)
Some checks failed
Nix / Build (aarch64-darwin) (push) Has been cancelled
Nix / Build (x86_64-linux) (push) Has been cancelled
Nix / Build (aarch64-linux) (push) Has been cancelled
2026-06-04 17:58:00 +00:00
Octol1ttle
81159fd9d7
fix(MSADeviceCodeStep): show network request error message
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-06-04 22:37:17 +05:00
Octol1ttle
f99a0883af
change(HardwareInfo/Windows): sort GPUs by performance, use smart pointers, log errors better
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-06-04 22:31:15 +05:00
Tayou
502a5175e2
fix category filtering
I have no idea how this worked at all when I made the commit originally, but it works now, just as well as it did on the prior commit.

Further improvements, using subfolders and other metadata will be in another PR.
Signed-off-by: Tayou <git@tayou.org>
2026-06-04 19:07:24 +02:00
Octol1ttle
6c15077731
fix(LoggedProcess): show process start error string
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-06-04 20:27:53 +05:00
Alexandru Ionut Tripon
6f3574f328
Fix memory leak and crash with data packs modal (#5551)
Some checks are pending
Nix / Build (aarch64-darwin) (push) Waiting to run
Nix / Build (x86_64-linux) (push) Waiting to run
Nix / Build (aarch64-linux) (push) Waiting to run
2026-06-04 05:38:22 +00:00
Seth Flynn
dd5261f7ad
Don't remove old Microsoft accounts until they're successfully reauthenticated (#5620) 2026-06-04 03:57:25 +00:00
Seth Flynn
ad85dc3291
AccountList: Skip refresh when !shouldRefresh (#5614) 2026-06-04 03:50:14 +00:00
Seth Flynn
77f5f92634
fix(nix): add jdk25 to wrapper (#5637) 2026-06-04 03:42:25 +00:00
Sefa Eyeoglu
5c1d783293
fix(nix): add jdk25 to wrapper
Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2026-06-03 21:04:01 +02:00
cat
ea2d0d0644
AccountList: Skip refresh when !shouldRefresh
Signed-off-by: cat <cat@plan9.rocks>
2026-06-02 19:06:24 +00:00
Octol1ttle
f6d1b29b04
fix(LaunchController): don't remove account unless we have a new one
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-06-01 13:27:53 +05:00
Seth Flynn
bf8d1ca1f8
chore(deps): update determinatesystems/magic-nix-cache-action action to v14 (#5606) 2026-06-01 04:37:34 +00:00
Alexandru Ionut Tripon
cc466b44c3
chore(deps): update azure/artifact-signing-action action to v2 (#5547) 2026-06-01 04:37:16 +00:00
renovate[bot]
99291186b2
chore(deps): update determinatesystems/magic-nix-cache-action action to v14 2026-05-29 06:04:43 +00:00
renovate[bot]
2bf64efaf2
chore(deps): update azure/artifact-signing-action action to v2 2026-05-28 18:34:22 +00:00
Octol1ttle
ccc23c8bc3
change: use native APIs for GPU discovery
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-28 15:19:31 +05:00
TheKodeToad
fa61e58cd9
Replace exec with open and handle saving geometry in signal listener
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
2026-05-28 09:20:57 +01:00
Seth Flynn
f5d7e76ac4
fix renovate using old label (#5549) 2026-05-27 22:50:58 +00:00
Sefa Eyeoglu
f67a670bcf
chore(deps): update korthout/backport-action action to v4.5 (#5466) 2026-05-23 20:27:34 +00:00
Seth Flynn
a80995ab7e
fix(nix): switch to KF6 ECM (#5545) 2026-05-23 20:17:57 +00:00
Sefa Eyeoglu
64abd37e05
clang-tidy: clang-analyzer-* (#5103) 2026-05-23 20:11:37 +00:00
Sefa Eyeoglu
23c34f495f
Fix offline accounts not being refreshed during launch (#5542) 2026-05-23 20:03:21 +00:00
Sefa Eyeoglu
d7fc7fd855
revert: "change: enable automatically enabling automatic merging on backport PRs" (#5540) 2026-05-23 20:02:08 +00:00
Alexandru Ionut Tripon
22bfe0628f
chore(deps): update cachix/install-nix-action digest to 8aa0397 (#5469) 2026-05-23 19:59:24 +00:00
renovate[bot]
d4d032afea
chore(deps): update korthout/backport-action action to v4.5 2026-05-22 18:59:55 +00:00
renovate[bot]
61cdabd40e
chore(deps): update cachix/install-nix-action digest to 8aa0397 2026-05-22 18:59:51 +00:00
Tayou
43c11a8555
Log components in instance logs (#5560) 2026-05-18 13:11:12 +00:00
Octol1ttle
547b4c0d3a
feat(instance logs): log components
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-18 12:18:44 +05:00
TheKodeToad
28eba8ed43
Fix memory leak and crash with data packs modal
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
2026-05-15 13:01:41 +01:00
Octol1ttle
f4567bcc8c
fix renovate using old label
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-15 14:14:46 +05:00
Sefa Eyeoglu
8ba5444c6b
fix(nix): switch to KF6 ECM
The override can be removed after
https://github.com/NixOS/nixpkgs/pull/518987 reaches nixos-unstable

See https://github.com/NixOS/nixpkgs/pull/513691

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2026-05-14 12:27:25 +02:00
Seth Flynn
de60d804a1
change(bug_report.yml): use input type where short answer is preferred (#5544) 2026-05-14 03:57:29 +00:00
Sefa Eyeoglu
8a7b17f958
flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'https://releases.nixos.org/nixos/unstable/nixos-26.05pre990025.15f4ee454b1d/nixexprs.tar.xz?narHash=sha256-fN6ynMvcdwPDB09LpWJNO5ogu%2BHFydrBWXJywoI/NNg%3D' (2026-04-30)
  → 'https://releases.nixos.org/nixos/unstable/nixos-26.05pre995699.da5ad661ba4e/nixexprs.tar.xz?narHash=sha256-rNDJzV2JTV5SUTwv1cgKZYMdyoUYU9/YfegSaUf3QfY%3D' (2026-05-10)

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2026-05-13 21:14:24 +02:00
mctaylors
558e5bc155
change(bug_report.yml): use input type where short answer is preferred
Signed-off-by: mctaylors <95250141+mctaylors@users.noreply.github.com>
2026-05-13 22:09:55 +03:00
Octol1ttle
bc1f9db653
fix offline accounts not being refreshed during launch
Closes #5435
Closes #5537

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-13 17:59:03 +05:00
Trial97
6699d3eca0
clang-tidy: clang-analyzer-*
This commit aims to fix all clang-analyzer-* warnings from clang-tidy.
Here is the list of the ones found in project:
    "clang-analyzer-core.uninitialized.UndefReturn",
    "clang-analyzer-deadcode.DeadStores",
    "clang-analyzer-optin.core.EnumCastOutOfRange",
Some exceptions:
  clang-analyzer-cplusplus.NewDeleteLeaks -> may need to disable it as
is a false positive
  clang-analyzer-optin.cplusplus.VirtualCall -> may need to disable it
(or refactor a bunch of code to drop the virtual from those functions)

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-13 10:20:26 +03:00
Seth Flynn
40824a187d
revert: "change: enable automatically enabling automatic merging on backport PRs"
Refs: d741b40
Signed-off-by: Seth Flynn <getchoo@tuta.io>
2026-05-13 02:23:28 -04:00
Alexandru Ionut Tripon
323c25d83b
Enable automatically enabling automatic merging on backport PRs (#5534) 2026-05-12 20:25:55 +00:00
Tayou
8d8951475c
Do not open account select dialog if there are no accounts (#5535) 2026-05-12 18:50:38 +00:00
Octol1ttle
d6db750797
fix: do not open account select dialog if there are no valid accounts
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-12 00:19:57 +05:00
Alexandru Ionut Tripon
ecc551b44e
Don't delete base directories when evicting metacache (#5513) 2026-05-11 11:15:43 +00:00
Octol1ttle
d741b403a9
change: enable automatically enabling automatic merging on backport PRs
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-11 15:43:26 +05:00
Tayou
0fee29559f
Fixes to translations (#5512) 2026-05-11 09:49:06 +00:00
Alexandru Ionut Tripon
d146972671
Suppress sfinae-incomplete warning (#5523) 2026-05-09 21:07:58 +00:00
Alexandru Ionut Tripon
c9d07adbbe
add file name column (#5505) 2026-05-09 21:03:47 +00:00
Octol1ttle
993eb40481
fix: suppress sfinae-incomplete warning
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-10 01:46:29 +05:00
Trial97
670f49309c
chore(clang-tidy): fix clang tidy warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-09 23:26:23 +03:00
Trial97
b7381d8088
add size column for datapacks
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-09 23:26:23 +03:00
Trial97
bac959bc6f
chore(clang-tidy): fix clang tidy warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-09 23:26:21 +03:00
Trial97
4c9081a934
add file name column
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-09 23:20:43 +03:00
Alexandru Ionut Tripon
7fcadcd7a2
Add some weird modrinth headers (#5506) 2026-05-09 19:52:55 +00:00
Trial97
97d570b343 chore(clang-tidy): fix clang tidy warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-08 20:41:10 +03:00
Trial97
f9e007ca2b add update reason
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-08 20:41:10 +03:00
Trial97
a63048d7e2 chore(clang-tidy): fix clang tidy warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-08 20:41:10 +03:00
Trial97
ca721f9d67 add special modrinth header
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-08 20:41:10 +03:00
Alexandru Ionut Tripon
18924e43da
fix Atl path traversal (#5511) 2026-05-07 21:16:14 +00:00
Alexandru Ionut Tripon
5d3411f412
fix: ignore non-existent or empty paths in processURLs (#5442) 2026-05-07 18:20:07 +00:00
Octol1ttle
564fca8c9b
fix: don't delete base directories when evicting metacache
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-07 20:48:19 +05:00
Octol1ttle
d59b4b0ad7
fix(translations): do not reset user language if translations index is missing
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-07 20:15:38 +05:00
Octol1ttle
49aef77f3f
fix(translations): redownload index if it is missing after file reloading
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-07 20:11:24 +05:00
Trial97
daa9e07e33 chore(clang-tidy): fix clang tidy warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-07 15:02:50 +03:00
Trial97
4cbfe7fb0e fix atl path traversal
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-07 14:43:44 +03:00
Alexandru Ionut Tripon
5f7aa2fbdb
fix change version (#5504) 2026-05-07 11:28:13 +00:00
Alexandru Ionut Tripon
95a62a5dde
Fixes to 'Use system locale' (#5485) 2026-05-07 10:58:27 +00:00
Alexandru Ionut Tripon
d548cb2e44
INIFile: add file name to error logging (#5502) 2026-05-07 10:54:11 +00:00
Alexandru Ionut Tripon
813cff612c
Fix wrong Xbox failure string (#5492) 2026-05-07 10:53:57 +00:00
Alexandru Ionut Tripon
101127273c
chore(nix): update lockfile (#5454) 2026-05-07 10:53:46 +00:00
Trial97
b174dec0d2 fix change version triggering an intial search
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-07 08:31:49 +03:00
Trial97
4463c21c98 chore(clang-tidy): fix clang tidy warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-05-07 08:31:49 +03:00
Octol1ttle
e7dbdf3489
INIFile: add file name to error logging
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-05 18:57:18 +05:00
TheKodeToad
4f58197edb
changed "Ok" to "OK" (#5331) 2026-05-05 09:21:29 +00:00
Octol1ttle
0a3f7da7e7
XboxAuthorizationStep: clang-tidy
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-05 13:42:45 +05:00
Alexandru Ionut Tripon
ac7c8adea2
Use "Discrete" key from switcheroo if present (#5487) 2026-05-04 20:38:17 +00:00
Alexandru Ionut Tripon
692e0ec00d
Fixes to PrismExternalUpdater (#5486) 2026-05-04 20:37:50 +00:00
Alexandru Ionut Tripon
773285054d
fix: trim whitespaces from ManagedPackURL (#5444) 2026-05-04 20:37:21 +00:00
Alexandru Ionut Tripon
5d8cdb429b
add setting to controll game assets download (#5355) 2026-05-04 20:37:06 +00:00
Alexandru Ionut Tripon
fb745777c3
Improve clang-tidy CI speed by only running autogen & autorcc (#5293) 2026-05-04 20:36:09 +00:00
Alexandru Ionut Tripon
7a94f6b4ae
Fix: Remove trademark and special characters from instance folder names (#5204) 2026-05-04 20:34:56 +00:00
Tayou
c4eb008d58
fix other logs page crash (#5458) 2026-05-04 16:03:09 +00:00
Tayou
882b8e1bf8
Fix Cmd+Q on macOS closing active window instead of quitting (#5427) 2026-05-04 15:50:11 +00:00
Octol1ttle
a021c86871
XboxAuthorizationStep: fix wrong failure string
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-04 20:23:27 +05:00
Tayou
bbd8c1e745
use custom QSortFilterProxyModel impl
Signed-off-by: Tayou <git@tayou.org>
2026-05-04 15:40:38 +02:00
Tayou
0c4c8703a3
rename IconPickerCategory and make public
Signed-off-by: Tayou <git@tayou.org>
2026-05-04 15:40:06 +02:00
Tayou
74308fcaa5
add category selector to icon picker dialog
it uses some regex shenanigans for this, probably not ideal, idk if theres a good way to filter the icons without adding extra metadata or storing them in subfolders

Signed-off-by: Tayou <git@tayou.org>
2026-05-04 15:40:06 +02:00
Octol1ttle
87f7c812c7
TranslationsModel: clang-tidy and code quality
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-04 12:39:23 +05:00
Octol1ttle
2d920da737
change(PrismExternalUpdater): allow unskipping versions by clicking "Remind Me Later"
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 18:47:35 +05:00
Octol1ttle
0f9be64d6c
fix(PrismExternalUpdater): do not show "No updates available" when ignoring skipped version during autocheck
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 18:47:35 +05:00
Octol1ttle
ae33c82268
fix(PrismExternalUpdater): show progress dialog immediately
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 18:47:35 +05:00
Octol1ttle
53dda7cd32
PrismExternalUpdater: clang-tidy
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 18:47:35 +05:00
Octol1ttle
f6096d21db
fix: use "Discrete" key from switcheroo if present
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 18:43:55 +05:00
Octol1ttle
1f291a2d79
change(updater): ignore skipped versions when update check is triggered by user
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 16:49:10 +05:00
Octol1ttle
8b159bacd8
change(LanguageSelectionWidget): 'Use system locale' -> 'Use system regional standards'
Closes #5358

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 16:33:05 +05:00
Octol1ttle
e449aae6c8
fix(TranslationsModel): use current language instead of default when turning off 'Use system locale'
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 16:25:32 +05:00
Octol1ttle
6b5615ece9
fix(TranslationsModel): use proper way to get system locale
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-05-03 16:23:45 +05:00
github-actions[bot]
781e50cdbe chore(nix): update lockfile
Flake lock file updates:

• Updated input 'nixpkgs':
    'https://releases.nixos.org/nixos/unstable/nixos-26.05pre980183.4bd9165a9165/nixexprs.tar.xz?narHash=sha256-Gk2T0tDDDAs319hp/ak%2BbAIUG5bPMvnNEjPV8CS86Fg%3D' (2026-04-14)
  → 'https://releases.nixos.org/nixos/unstable/nixos-26.05pre990025.15f4ee454b1d/nixexprs.tar.xz?narHash=sha256-fN6ynMvcdwPDB09LpWJNO5ogu%2BHFydrBWXJywoI/NNg%3D' (2026-04-30)
2026-05-03 00:54:49 +00:00
so5iso4ka
e9cdef65e6
fix(OtherLogsPage): handle empty log lines
Signed-off-by: so5iso4ka <so5iso4ka@icloud.com>
2026-04-26 18:04:29 +03:00
Octol1ttle
6e0d9b8ca0
change: improve clang-tidy CI speed by only running autogen & autorcc
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-24 12:11:03 +05:00
Alexandru Ionut Tripon
031015b332
chore(deps): update korthout/backport-action action to v4.4 (#5441) 2026-04-22 20:08:16 +00:00
Alexandru Ionut Tripon
b4f34b87d6
chore(deps): update cachix/install-nix-action digest to 6165592 (#5325) 2026-04-22 20:07:58 +00:00
Alexandru Ionut Tripon
de8ad56e60
chore(nix): update lockfile (#5307) 2026-04-22 20:07:36 +00:00
Alexandru Ionut Tripon
b65f25fcfe
chore(deps): update hendrikmuhs/ccache-action action to v1.2.23 (#5440) 2026-04-22 19:03:01 +00:00
captivator
5ad8372e16 fix: trim whitespaces from ManagedPackURL
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-22 15:38:04 +03:00
captivator
92eeeaf14f fix: ignore non-existent or empty paths in processURLs
Assisted-by: Gemini:3-Flash
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-22 12:27:18 +03:00
renovate[bot]
f14701ffb7
chore(deps): update korthout/backport-action action to v4.4 2026-04-21 21:05:28 +00:00
renovate[bot]
672cd4d59c
chore(deps): update cachix/install-nix-action digest to 6165592 2026-04-21 21:05:23 +00:00
renovate[bot]
a7c91796b3
chore(deps): update hendrikmuhs/ccache-action action to v1.2.23 2026-04-21 09:23:37 +00:00
Alexandru Ionut Tripon
5a9fdffd7d
chore(deps): update softprops/action-gh-release action to v3 (#5369) 2026-04-20 10:43:38 +00:00
Alexandru Ionut Tripon
e154413b1d
ci(container): actually use amd64 runner for amd64 (#5436) 2026-04-20 06:35:22 +00:00
Seth Flynn
541e5ca9fe
ci(container): actually use amd64 runner for amd64
Signed-off-by: Seth Flynn <getchoo@tuta.io>
2026-04-20 01:06:38 -04:00
Trial97
418222cd6f
add setting to controll game assets download
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-19 23:51:48 +03:00
Alexandru Ionut Tripon
48f240703f
feat: add Manage Skins menu item to accounts button in MainWindow (#5414) 2026-04-19 20:44:25 +00:00
Alexandru Ionut Tripon
b595488487
NetJob: do not automatically retry on 404 Not Found response (#5416) 2026-04-19 20:44:13 +00:00
Alexandru Ionut Tripon
e7322a4507
fix world size uninitialized memory and UI refresh signal (#5418) 2026-04-19 20:44:06 +00:00
github-actions[bot]
c67de94b3d chore(nix): update lockfile
Flake lock file updates:

• Updated input 'nixpkgs':
    'https://releases.nixos.org/nixos/unstable/nixos-26.05pre971119.8110df5ad7ab/nixexprs.tar.xz?narHash=sha256-D4ely1FsBcvtj/qSrNhSWpq%2BCUZKNiKwJIxpxnfy9o4%3D' (2026-03-28)
  → 'https://releases.nixos.org/nixos/unstable/nixos-26.05pre980183.4bd9165a9165/nixexprs.tar.xz?narHash=sha256-Gk2T0tDDDAs319hp/ak%2BbAIUG5bPMvnNEjPV8CS86Fg%3D' (2026-04-14)
2026-04-19 00:47:57 +00:00
Danny
9621b59573 Change menu role for close action in MainWindow
Fixes issue in Mac OS where pressing Cmd+Q closes only the current window instead of quitting the application.

menuRole is consulted only by Qt's native macOS menu-bar integration, so this change has no effect on Windows, Linux, or BSD.

Fixes #1382

Signed-off-by: Danny <dannydjdk@users.noreply.github.com>
Assisted-by: Claude:claude-opus-4-7
2026-04-17 22:29:57 -05:00
Alexandru Ionut Tripon
e7a03d311c
ProgressDialog: allow finished tasks to be re-displayed once restarted (#5412) 2026-04-17 14:00:07 +00:00
Alexandru Ionut Tripon
af8225e2da
Improve checksum mismatch logging (#5413) 2026-04-17 13:59:58 +00:00
Alexandru Ionut Tripon
49e9f96327
Fixes for task abort logic (#5415) 2026-04-17 13:59:42 +00:00
captivator
cbaf45084e fix world size uninitialized memory and UI refresh signal
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 15:28:12 +03:00
captivator
03799bf258
apply reviewer suggestion: use explicit MSA check again
Co-authored-by: Alexandru Ionut Tripon <alexandru.tripon97@gmail.com>
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 13:47:08 +03:00
captivator
4344f5eef9
apply reviewer suggestion: use explicit MSA check
Co-authored-by: Alexandru Ionut Tripon <alexandru.tripon97@gmail.com>
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 13:46:43 +03:00
Alexandru Ionut Tripon
4872ec634c
chore: bump develop version to 12.0.0 (#5339) 2026-04-17 07:12:35 +00:00
Octol1ttle
85613cfadc
Don't use new Qt method
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-17 11:12:46 +05:00
0x189D7997
4a59e6012d
NetJob: do not automatically retry on 404 response
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 03:45:33 +00:00
0x189D7997
ffded2ccac
Fix(NetJob): do not call emitAborted() when not running
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 02:56:04 +00:00
0x189D7997
15b39af92e
Fix(Task): check if task is still running before calling emitAborted()
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 02:11:06 +00:00
0x189D7997
4ed3aa1f1c
Fix(InstanceCreationTask): propagate abort signal to super
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-17 02:07:09 +00:00
captivator
7d0d9a3827 feat: add Manage Skins menu item to accounts button in MainWindow
Assisted-by: Gemini:3-Flash
Signed-off-by: captivator <84224501+qaptivator@users.noreply.github.com>
2026-04-17 04:15:48 +03:00
Octol1ttle
b9fa4ffc00
fix(ProgressDialog): allow finished tasks to be re-displayed once restarted
Cherry-picked from libcurl (lmao)

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-16 22:04:43 +05:00
Tayou
f40cbf816e
fix text overlap in project item views (#5406) 2026-04-16 09:45:45 +00:00
Octol1ttle
3ee45691ab
change: improve checksum mismatch logging
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-16 11:32:58 +05:00
Alexandru Ionut Tripon
8a68c625fb
Windows installer: Disable skipping files (#5385) 2026-04-15 21:08:25 +00:00
Alexandru Ionut Tripon
8eb9a9971b
Search by project id (#) improvement (#5303) 2026-04-15 20:41:57 +00:00
Alexandru Ionut Tripon
44e3ae59e4
Low RAM warning fixes (#5392) 2026-04-15 20:39:08 +00:00
Alexandru Ionut Tripon
8901da68c7
chore(deps): update actions/cache action to v5.0.5 (#5386) 2026-04-15 20:38:24 +00:00
so5iso4ka
fa54329711
fix text overlap in project item views
Signed-off-by: so5iso4ka <so5iso4ka@icloud.com>
2026-04-15 22:44:17 +03:00
renovate[bot]
cddbb0e970
chore(deps): update softprops/action-gh-release action to v3 2026-04-15 09:43:32 +00:00
renovate[bot]
da50f0e9e3
chore(deps): update actions/cache action to v5.0.5 2026-04-15 09:43:28 +00:00
0x189D7997
28c42d04b6
Limit normal search fallback to 404 respnse
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-14 23:51:34 +00:00
Octol1ttle
5d9622db21
Use newlines more often in macOS dialog for a nicer look
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 23:24:10 +05:00
Alexandru Ionut Tripon
7e8db63882
Fix infinite loop in SkinManageDialog (#5388) 2026-04-14 16:30:03 +00:00
Alexandru Ionut Tripon
519d8f7385
add a option to skip meta refresh on launch (#5267) 2026-04-14 16:29:30 +00:00
Alexandru Ionut Tripon
ece83eb637
fix: force metadata version list refreshes to reload (#5349) 2026-04-14 16:29:05 +00:00
Alexandru Ionut Tripon
06282c0363
fix pessimizing-move warning (#5361) 2026-04-14 16:28:01 +00:00
Alexandru Ionut Tripon
a0c5893a98
Task: Warn when disposing while running (#5371) 2026-04-14 16:27:24 +00:00
Alexandru Ionut Tripon
fbec685eb5
Fix Copy/Upload buttons not working in ScreenshotsPage (#5387) 2026-04-14 16:26:12 +00:00
Octol1ttle
0b578fa767
fix(EnsureAvailableMemory/macOS): warn based on memory pressure rather than available RAM
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 18:58:47 +05:00
Octol1ttle
ae331cfc9a
change(EnsureAvailableMemory): rephrase warning message
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 18:43:52 +05:00
Octol1ttle
575be16d3e
fix(EnsureAvailableMemory): do not warn if available memory could not be read
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 12:47:48 +05:00
Octol1ttle
d5db0c6c1b
fix(SkinList): do not consider non-png files correctly
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-14 00:39:34 +05:00
Octol1ttle
1fec781251
fix(ScreenshotsPage): fix QString::arg in string with no arguments
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 23:44:13 +05:00
Octol1ttle
08de904e21
fix(ScreenshotsPage): disable "Copy Image" when selecting multiple screenshots
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 23:44:13 +05:00
Octol1ttle
7a1d2e41a1
refactor(ScreenshotsPage): clang-tidy
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 23:44:13 +05:00
Octol1ttle
1b650622ea
fix(ScreenshotsPage): use correct selection collection
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 22:57:13 +05:00
Octol1ttle
88035b9815
change(Windows installer): disable skipping files
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-13 21:05:30 +05:00
Octol1ttle
ae7e143537
change(Task): warn when disposing while running
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-12 13:48:15 +05:00
DioEgizio
b230645d53
Updater: Do not reset current task in finished signal (#5370) 2026-04-12 08:22:19 +00:00
Octol1ttle
9b270f783e
fix(updater): do not reset current task in finished signal
The order of signals in case of a success is "succeeded"->"finished"

The "succeeded" signal may launch another download if the updater needs to fetch more pages
But if we reset the task then the newly started download will be disposed and the updater will softlock

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-12 13:02:46 +05:00
Andrei Damian
ac54df366b fix pessimizing-move warning
Signed-off-by: Andrei Damian <andreidaamian@gmail.com>
2026-04-11 12:08:06 +03:00
Alexandru Ionut Tripon
a17a45c748
enable modpack changelog for modrinth page (#5354) 2026-04-11 05:19:09 +00:00
Alexandru Ionut Tripon
a488eb6d5d
fix pack upgrade (#5345) 2026-04-10 17:04:55 +00:00
Trial97
f3ff0a730a enable modpack changelog for modrinth page
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-10 19:29:56 +03:00
Alexandru Ionut Tripon
966ecd00bd
Allow disabling low RAM warning (#5333) 2026-04-10 09:14:47 +00:00
Octol1ttle
4b3aedd5d0
Change LowMemWarning default to always enabled
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-10 11:53:37 +05:00
oosh
f2c5916205 changed "Ok" to "OK"
Signed-off-by: oosh <ovtennakoon@gmail.com>
2026-04-10 10:23:18 +10:00
morsz
2219c37d7f
fix: force metadata version list refreshes to reload
manual refreshes on version selection screens could reuse cached metadata and skip downloading updated manifests, so new versions would not appear until Prism was restarted. pass an explicit forced reload through the shared version list loading path and use it from refresh actions so manual refresh always reloads metadata

Signed-off-by: morsz <morsz@morsz.dev>
2026-04-10 02:19:28 +02:00
Trial97
b7344af313 fix pack upgrade
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-10 00:36:05 +03:00
Trial97
9bccda0a79 chore: bump develop version to 12.0.0
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-09 23:32:02 +03:00
Alexandru Ionut Tripon
013bb5cac3
fix McClient (#5332) 2026-04-09 20:23:53 +00:00
Alexandru Ionut Tripon
e8afd48c67
Don't count JAR mods when checking offline libraries (#5334) 2026-04-09 20:18:41 +00:00
Alexandru Ionut Tripon
2ef22124cd
CI/Nix: Bump macOS (#5335) 2026-04-09 20:16:54 +00:00
Alexandru Ionut Tripon
6b9d2dbb64
fix(PrintInstanceInfo): add break before OS info (#5336) 2026-04-09 20:16:13 +00:00
Octol1ttle
658a1391f8
change(EnsureAvailableMemory): add lenience
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 22:35:32 +05:00
Octol1ttle
4cf8cf7d18
fix(PrintInstanceInfo): add break before OS info
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 20:53:42 +05:00
Octol1ttle
724c9a4a2c
fix(CI/nix): bump macOS
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 20:40:22 +05:00
Octol1ttle
ec4484282c
fix: don't count JAR mods when checking offline libraries
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 20:31:39 +05:00
Octol1ttle
c044ed36af
feat: allow disabling low RAM warning
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 19:34:11 +05:00
Octol1ttle
91616ae9b6
refactor: McClient
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 19:03:35 +05:00
Octol1ttle
2fe0569bd6
fix(McClient): do not use unsigned type for response length
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-09 17:41:25 +05:00
0x189D7997
364968a6b4
Use network_error_code from callbacks
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-07 12:57:09 +00:00
0x189D7997
fdd1a5dde8
oops forgot again
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 13:35:06 +00:00
0x189D7997
4151db6c94
Fallback to normal search on error and apply same changes to ResourceModel
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 13:12:23 +00:00
0x189D7997
4706f894e3
Activate search by project id only for numarical values for CurseForge
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 07:29:19 +00:00
0x189D7997
bf75d50baf
Make search by id fail quietly
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 07:27:29 +00:00
0x189D7997
983bf34807
Allow requesting project info without manual retry on fail
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-04 07:17:58 +00:00
Trial97
66b5bd9618
add more invalid chars for folder name check
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-03 09:57:47 +03:00
Trial97
40b7cab3ed
add a option to skip meta refresh on launch
related to https://github.com/PrismLauncher/PrismLauncher/issues/3785
It doesn't fix it but it should at least allow users to skip the
redownload of the meta files.
So in a previous PR I added an automated way to refresh all the meta
from the original index, to the component index to the actual index.

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-27 00:23:23 +02:00
174 changed files with 3924 additions and 2867 deletions

View file

@ -23,14 +23,14 @@ body:
- macOS - macOS
- Linux - Linux
- Other - Other
- type: textarea - type: input
attributes: attributes:
label: Version of Prism Launcher label: Version of Prism Launcher
description: The version of Prism Launcher used in the bug report. description: The version of Prism Launcher used in the bug report.
placeholder: Prism Launcher 5.0 placeholder: Prism Launcher 5.0
validations: validations:
required: true required: true
- type: textarea - type: input
attributes: attributes:
label: Version of Qt label: Version of Qt
description: The version of Qt used in the bug report. You can find it in Help -> About Prism Launcher -> About Qt. description: The version of Qt used in the bug report. You can find it in Help -> About Prism Launcher -> About Qt.

View file

@ -69,7 +69,7 @@ runs:
- name: Sign executables - name: Sign executables
if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }}
uses: azure/artifact-signing-action@v1 uses: azure/artifact-signing-action@v2
with: with:
endpoint: https://eus.codesigning.azure.net/ endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: PrismLauncher trusted-signing-account-name: PrismLauncher
@ -142,7 +142,7 @@ runs:
- name: Sign installer - name: Sign installer
if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }}
uses: azure/artifact-signing-action@v1 uses: azure/artifact-signing-action@v2
with: with:
endpoint: https://eus.codesigning.azure.net/ endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: PrismLauncher trusted-signing-account-name: PrismLauncher

View file

@ -55,7 +55,7 @@ runs:
# TODO(@getchoo): Get this working on MSYS2! # TODO(@getchoo): Get this working on MSYS2!
- name: Setup ccache - name: Setup ccache
if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }}
uses: hendrikmuhs/ccache-action@v1.2.22 uses: hendrikmuhs/ccache-action@v1.2.23
with: with:
variant: sccache variant: sccache
create-symlink: ${{ runner.os != 'Windows' }} create-symlink: ${{ runner.os != 'Windows' }}

View file

@ -13,7 +13,7 @@ runs:
dpkg-dev \ dpkg-dev \
ninja-build extra-cmake-modules pkg-config scdoc \ ninja-build extra-cmake-modules pkg-config scdoc \
cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \ cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \
libxcb-cursor-dev libtomlplusplus-dev libvulkan-dev libxcb-cursor-dev libtomlplusplus-dev
- name: Setup AppImage tooling - name: Setup AppImage tooling
shell: bash shell: bash

View file

@ -91,7 +91,7 @@ runs:
- name: Retrieve ccache cache (MinGW) - name: Retrieve ccache cache (MinGW)
if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }}
uses: actions/cache@v5.0.4 uses: actions/cache@v5.0.5
with: with:
path: '${{ github.workspace }}\.ccache' path: '${{ github.workspace }}\.ccache'
key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }}

View file

@ -24,7 +24,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs - name: Create backport PRs
uses: korthout/backport-action@v4.3.0 uses: korthout/backport-action@v4.5
with: with:
# Config README: https://github.com/korthout/backport-action#backport-action # Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |- pull_description: |-

View file

@ -28,19 +28,14 @@ jobs:
fetch-depth: 0 # Required for diffing later on fetch-depth: 0 # Required for diffing later on
submodules: "true" submodules: "true"
- name: Setup sccache
uses: hendrikmuhs/ccache-action@v1.2.22
with:
variant: sccache
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
- name: Run build - name: Run source generators
# TODO(@getchoo): Figure out how to make this work with PCH # TODO(@getchoo): Figure out how to make this work with PCH
run: | run: |
nix develop --command bash -c ' nix develop --command bash -c '
cmake -B build -D Launcher_USE_PCH=OFF -D CMAKE_CXX_COMPILER_LAUNCHER=sccache && cmake --build build cmake -B build -D Launcher_USE_PCH=OFF && cmake --build build --target autogen autorcc
' '
# TODO: Use SARIF after https://github.com/psastras/sarif-rs/issues/638 is fixed # TODO: Use SARIF after https://github.com/psastras/sarif-rs/issues/638 is fixed

View file

@ -33,7 +33,7 @@ jobs:
- arch: arm64 - arch: arm64
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
- arch: amd64 - arch: amd64
os: ubuntu-24.04-arm os: ubuntu-24.04
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}

View file

@ -88,7 +88,7 @@ jobs:
- os: ubuntu-22.04-arm - os: ubuntu-22.04-arm
system: aarch64-linux system: aarch64-linux
- os: macos-14 - os: macos-26
system: aarch64-darwin system: aarch64-darwin
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -103,7 +103,7 @@ jobs:
# For PRs # For PRs
- name: Setup Nix Magic Cache - name: Setup Nix Magic Cache
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
uses: DeterminateSystems/magic-nix-cache-action@v13 uses: DeterminateSystems/magic-nix-cache-action@v14
with: with:
diagnostic-endpoint: "" diagnostic-endpoint: ""
use-flakehub: false use-flakehub: false

View file

@ -94,7 +94,7 @@ jobs:
- name: Create release - name: Create release
id: create_release id: create_release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.ref }} tag_name: ${{ github.ref }}

View file

@ -20,7 +20,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: cachix/install-nix-action@96951a368ba55167b55f1c916f7d416bac6505fe # v31 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31
- uses: DeterminateSystems/update-flake-lock@v28 - uses: DeterminateSystems/update-flake-lock@v28
with: with:

View file

@ -13,6 +13,10 @@ endif()
##################################### Set CMake options ##################################### ##################################### Set CMake options #####################################
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOGEN_ORIGIN_DEPENDS OFF)
set(CMAKE_GLOBAL_AUTOGEN_TARGET ON)
set(CMAKE_GLOBAL_AUTORCC_TARGET ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/")
@ -179,7 +183,7 @@ set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CAC
set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.") set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.")
######## Set version numbers ######## ######## Set version numbers ########
set(Launcher_VERSION_MAJOR 11) set(Launcher_VERSION_MAJOR 12)
set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_PATCH 0) set(Launcher_VERSION_PATCH 0)

View file

@ -28,7 +28,7 @@ RUN apt-get --assume-yes --no-install-recommends install \
# Build system # Build system
cmake ninja-build extra-cmake-modules pkg-config \ cmake ninja-build extra-cmake-modules pkg-config \
# Dependencies # Dependencies
cmark gamemode-dev libarchive-dev libcmark-dev libgamemode0 libgl1-mesa-dev libqrencode-dev libtomlplusplus-dev libvulkan-dev scdoc zlib1g-dev \ cmark gamemode-dev libarchive-dev libcmark-dev libgamemode0 libgl1-mesa-dev libqrencode-dev libtomlplusplus-dev scdoc zlib1g-dev \
# Tooling # Tooling
clang-format clang-tidy git clang-format clang-tidy git

View file

@ -194,8 +194,10 @@ class Config {
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" }; QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" };
QString MODRINTH_DOWNLOAD_HOST = "cdn.modrinth.com";
QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; QString FLAME_BASE_URL = "https://api.curseforge.com/v1";
QString FLAME_DOWNLOAD_HOST = "edge.forgecdn.net";
QString versionString() const; QString versionString() const;
/** /**

8
flake.lock generated
View file

@ -18,11 +18,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1774709303, "lastModified": 1778443072,
"narHash": "sha256-D4ely1FsBcvtj/qSrNhSWpq+CUZKNiKwJIxpxnfy9o4=", "narHash": "sha256-rNDJzV2JTV5SUTwv1cgKZYMdyoUYU9/YfegSaUf3QfY=",
"rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685", "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre971119.8110df5ad7ab/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre995699.da5ad661ba4e/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",

View file

@ -578,16 +578,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
} }
{ {
bool migrated = false; auto migrated = handleDataMigration(
if (!migrated)
migrated = handleDataMigration(
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC",
"polymc.cfg"); "polymc.cfg");
if (!migrated) if (!migrated) {
migrated = handleDataMigration( handleDataMigration(dataPath,
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"),
"multimc.cfg"); "MultiMC", "multimc.cfg");
}
} }
{ {
@ -735,6 +733,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512);
m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem()); m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem());
m_settings->registerSetting("PermGen", 128); m_settings->registerSetting("PermGen", 128);
m_settings->registerSetting("LowMemWarning", true);
// Java Settings // Java Settings
m_settings->registerSetting("JavaPath", ""); m_settings->registerSetting("JavaPath", "");
@ -778,6 +777,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false);
m_settings->registerSetting("SkipModpackUpdatePrompt", false); m_settings->registerSetting("SkipModpackUpdatePrompt", false);
m_settings->registerSetting("ShowModIncompat", false); m_settings->registerSetting("ShowModIncompat", false);
m_settings->registerSetting("DownloadGameFilesDuringInstanceCreation", true);
// Minecraft offline player name // Minecraft offline player name
m_settings->registerSetting("LastOfflinePlayerName", ""); m_settings->registerSetting("LastOfflinePlayerName", "");
@ -870,6 +870,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get()); resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get());
} }
m_settings->registerSetting("MetaRefreshOnLaunch", true);
m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false); m_settings->registerSetting("QuitAfterGameStop", false);
@ -934,15 +935,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qInfo() << "<> Network done."; qInfo() << "<> Network done.";
} }
// load translations
{
m_translations.reset(new TranslationsModel("translations"));
auto bcp47Name = m_settings->get("Language").toString();
m_translations->selectLanguage(bcp47Name);
qInfo() << "Your language is" << bcp47Name;
qInfo() << "<> Translations loaded.";
}
// Instance icons // Instance icons
{ {
auto setting = APPLICATION->settings()->getSetting("IconsDir"); auto setting = APPLICATION->settings()->getSetting("IconsDir");
@ -1021,8 +1013,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qInfo() << "<> Cache initialized."; qInfo() << "<> Cache initialized.";
} }
// now we have network, download translation updates // load translations
{
m_translations.reset(new TranslationsModel("translations"));
m_translations->downloadIndex(); m_translations->downloadIndex();
qInfo() << "Your language is" << m_translations->selectedLanguage();
qInfo() << "<> Translations loaded.";
}
// FIXME: what to do with these? // FIXME: what to do with these?
m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory())); m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));

View file

@ -63,7 +63,7 @@ class BaseVersionList : public QAbstractListModel {
* The task returned by this function should reset the model when it's done. * The task returned by this function should reset the model when it's done.
* \return A pointer to a task that reloads the version list. * \return A pointer to a task that reloads the version list.
*/ */
virtual Task::Ptr getLoadTask() = 0; virtual Task::Ptr getLoadTask(bool forceReload = false) = 0;
//! Checks whether or not the list is loaded. If this returns false, the list should be //! Checks whether or not the list is loaded. If this returns false, the list should be
// loaded. // loaded.

View file

@ -1206,78 +1206,6 @@ if(WIN32)
) )
endif() endif()
qt_wrap_ui(LAUNCHER_UI
ui/MainWindow.ui
ui/setupwizard/PasteWizardPage.ui
ui/setupwizard/AutoJavaWizardPage.ui
ui/setupwizard/LoginWizardPage.ui
ui/pages/global/AccountListPage.ui
ui/pages/global/JavaPage.ui
ui/pages/global/LauncherPage.ui
ui/pages/global/APIPage.ui
ui/pages/global/ProxyPage.ui
ui/pages/global/ExternalToolsPage.ui
ui/pages/instance/ExternalResourcesPage.ui
ui/pages/instance/NotesPage.ui
ui/pages/instance/LogPage.ui
ui/pages/instance/ServersPage.ui
ui/pages/instance/OtherLogsPage.ui
ui/pages/instance/VersionPage.ui
ui/pages/instance/ManagedPackPage.ui
ui/pages/instance/WorldListPage.ui
ui/pages/instance/ScreenshotsPage.ui
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/CustomPage.ui
ui/pages/modplatform/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/import_ftb/ImportFTBPage.ui
ui/pages/modplatform/ImportPage.ui
ui/pages/modplatform/OptionalModDialog.ui
ui/pages/modplatform/ftb/FtbPage.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/CustomCommands.ui
ui/widgets/EnvironmentVariables.ui
ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
ui/widgets/SubTaskProgressBar.ui
ui/widgets/AppearanceWidget.ui
ui/widgets/MinecraftSettingsWidget.ui
ui/widgets/JavaSettingsWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/CreateShortcutDialog.ui
ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui
ui/dialogs/NetworkJobFailedDialog.ui
ui/dialogs/NewComponentDialog.ui
ui/dialogs/NewsDialog.ui
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportPackDialog.ui
ui/dialogs/ExportToModListDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/AboutDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui
ui/dialogs/skins/SkinManageDialog.ui
ui/dialogs/ChooseOfflineNameDialog.ui
)
qt_wrap_ui(PRISM_UPDATE_UI
ui/dialogs/UpdateAvailableDialog.ui
)
if (NOT Apple)
set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI})
endif()
qt_add_resources(LAUNCHER_RESOURCES qt_add_resources(LAUNCHER_RESOURCES
resources/backgrounds/backgrounds.qrc resources/backgrounds/backgrounds.qrc
resources/multimc/multimc.qrc resources/multimc/multimc.qrc
@ -1296,12 +1224,6 @@ qt_add_resources(LAUNCHER_RESOURCES
"${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}"
) )
qt_wrap_ui(PRISMUPDATER_UI
updater/prismupdater/SelectReleaseDialog.ui
ui/widgets/SubTaskProgressBar.ui
ui/dialogs/ProgressDialog.ui
)
######## Windows resource files ######## ######## Windows resource files ########
if(WIN32) if(WIN32)
set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
@ -1321,7 +1243,7 @@ endif()
####### Targets ######## ####### Targets ########
# Add executable # Add executable
add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_RESOURCES})
target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION)
@ -1441,7 +1363,7 @@ endif()
if(Launcher_BUILD_UPDATER) if(Launcher_BUILD_UPDATER)
# Updater # Updater
add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES})
target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
if(${Launcher_USE_PCH}) if(${Launcher_USE_PCH})
@ -1569,8 +1491,10 @@ if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
target_compile_options(Launcher_logic PRIVATE /wd4100) # C4100 - unused parameter target_compile_options(Launcher_logic PRIVATE /wd4100) # C4100 - unused parameter
target_compile_options(${Launcher_Name} PRIVATE /wd4100) # C4100 - unused parameter target_compile_options(${Launcher_Name} PRIVATE /wd4100) # C4100 - unused parameter
else() else()
target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers) # sfinae-incomplete is a new GCC warning and triggers in Qt headers
target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers) # no-unknown-warning-option so that compilers that don't have sfinae-incomplete don't error
target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete)
target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete)
endif() endif()
#### The bundle mess! #### #### The bundle mess! ####

View file

@ -36,6 +36,7 @@
*/ */
#include "FileSystem.h" #include "FileSystem.h"
#include <qcontainerfwd.h>
#include <QPair> #include <QPair>
#include "BuildConfig.h" #include "BuildConfig.h"
@ -683,6 +684,32 @@ bool deletePath(QString path)
return err.value() == 0; return err.value() == 0;
} }
bool deleteContents(const QString& path)
{
const QFileInfo info(path);
if (!info.exists()) {
return true;
}
if (!info.isDir()) {
qWarning() << "Attempted to delete contents of non-directory path:" << path;
return false;
}
bool ret = true;
for (const auto& entry : fs::directory_iterator(StringUtils::toStdString(path))) {
std::error_code err;
fs::remove_all(entry.path(), err);
if (err.value() != 0) {
qWarning().nospace() << "Could not delete directory entry " << entry.path() << ": " << QString::fromStdString(err.message());
ret = false;
}
}
return ret;
}
bool trash(QString path, QString* pathInTrash) bool trash(QString path, QString* pathInTrash)
{ {
// FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
@ -796,68 +823,33 @@ QString NormalizePath(QString path)
} }
} }
static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; namespace {
static const QString BAD_NTFS_CHARS = "<>:\"|?*"; const QString g_badChars = "<>:\"|?*\r\n!";
static const QString BAD_HFS_CHARS = ":"; QString removeChars(QString source, QChar replace, const QString& extraChars = "")
{
auto badChars = g_badChars;
if (!extraChars.isEmpty()) {
badChars += extraChars;
}
static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/"; for (auto& c : source) {
if (c.unicode() < 0x20 || !c.isPrint() || badChars.contains(c)) {
c = replace;
}
}
return source;
}
} // namespace
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) return removeChars(std::move(string), replaceWith, "\\/");
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith;
return string;
} }
QString RemoveInvalidPathChars(QString path, QChar replaceWith) QString RemoveInvalidPathChars(QString string, QChar replaceWith)
{ {
QString invalidChars; return removeChars(std::move(string), replaceWith);
#ifdef Q_OS_WIN
invalidChars = BAD_WIN_CHARS;
#endif
// the null character is ignored in this check as it was not a problem until now
switch (statFS(path).fsType) {
case FilesystemType::FAT: // similar to NTFS
/* fallthrough */
case FilesystemType::NTFS:
/* fallthrough */
case FilesystemType::REFS: // similar to NTFS(should be available only on windows)
invalidChars += BAD_NTFS_CHARS;
break;
// case FilesystemType::EXT:
// case FilesystemType::EXT_2_OLD:
// case FilesystemType::EXT_2_3_4:
// case FilesystemType::XFS:
// case FilesystemType::BTRFS:
// case FilesystemType::NFS:
// case FilesystemType::ZFS:
case FilesystemType::APFS:
/* fallthrough */
case FilesystemType::HFS:
/* fallthrough */
case FilesystemType::HFSPLUS:
/* fallthrough */
case FilesystemType::HFSX:
invalidChars += BAD_HFS_CHARS;
break;
// case FilesystemType::FUSEBLK:
// case FilesystemType::F2FS:
// case FilesystemType::UNKNOWN:
default:
break;
}
if (invalidChars.size() != 0) {
for (int i = 0; i < path.length(); i++) {
if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) {
path[i] = replaceWith;
}
}
}
return path;
} }
QString DirNameFromString(QString string, QString inDir) QString DirNameFromString(QString string, QString inDir)

View file

@ -291,6 +291,13 @@ bool move(const QString& source, const QString& dest);
*/ */
bool deletePath(QString path); bool deletePath(QString path);
/**
* Delete a folder's contents recursively but not the folder itself.
* @param path The path to the folder.
* @return Whether the deletion was completely successful.
*/
bool deleteContents(const QString& path);
bool removeFiles(QStringList listFile); bool removeFiles(QStringList listFile);
/** /**

View file

@ -18,84 +18,45 @@
#include "HardwareInfo.h" #include "HardwareInfo.h"
#include <QCoreApplication> #include <QDebug>
#include <QOffscreenSurface> #include <QStringList>
#include <QOpenGLFunctions>
#include <QProcessEnvironment>
#include "BuildConfig.h"
#ifndef Q_OS_MACOS
#include <QVulkanInstance>
#include <QVulkanWindow>
#endif
#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX)
namespace { namespace {
bool vulkanInfo(QStringList& out) QString afterColon(QString str)
{ {
if (!QProcessEnvironment::systemEnvironment() return str.remove(0, str.indexOf(':') + 2).trimmed();
.value(QStringLiteral("%1_DISABLE_GLVULKAN").arg(BuildConfig.LAUNCHER_ENVNAME))
.isEmpty()) {
return false;
}
#ifndef Q_OS_MACOS
QVulkanInstance inst;
if (!inst.create()) {
qWarning() << "Vulkan instance creation failed, VkResult:" << inst.errorCode();
out << "Couldn't get Vulkan device information";
return false;
} }
QVulkanWindow window; template <typename F>
window.setVulkanInstance(&inst); bool readFromOutput(const char* command, F function)
for (auto device : window.availablePhysicalDevices()) {
const auto supportedVulkanVersion = QVersionNumber(VK_API_VERSION_MAJOR(device.apiVersion), VK_API_VERSION_MINOR(device.apiVersion),
VK_API_VERSION_PATCH(device.apiVersion));
out << QString("Found Vulkan device: %1 (API version %2)").arg(device.deviceName).arg(supportedVulkanVersion.toString());
}
#endif
return true;
}
bool openGlInfo(QStringList& out)
{ {
if (!QProcessEnvironment::systemEnvironment() FILE* file = popen(command, "r"); // NOLINT(*-command-processor)
.value(QStringLiteral("%1_DISABLE_GLVULKAN").arg(BuildConfig.LAUNCHER_ENVNAME)) if (!file) {
.isEmpty()) { qWarning().nospace() << "Could not execute command '" << command << "': " << strerror(errno);
return false;
}
QOpenGLContext ctx;
if (!ctx.create()) {
qWarning() << "OpenGL context creation failed";
out << "Couldn't get OpenGL device information";
return false; return false;
} }
QOffscreenSurface surface; constexpr size_t bufferSize = 512;
surface.create(); std::array<char, bufferSize> buffer{};
ctx.makeCurrent(&surface); while (fgets(buffer.data(), bufferSize, file) != nullptr) {
function(buffer.data());
}
auto* f = ctx.functions(); const int exitCode = pclose(file);
f->initializeOpenGLFunctions(); if (exitCode != 0) {
if (exitCode == -1) {
qWarning().nospace() << "Could not close stream for command '" << command << "': " << strerror(errno);
} else {
qWarning().nospace() << "Command '" << command << "' exited with code " << exitCode;
}
auto toQString = [](const GLubyte* str) { return QString(reinterpret_cast<const char*>(str)); }; return false;
out << "OpenGL driver vendor: " + toQString(f->glGetString(GL_VENDOR)); }
out << "OpenGL renderer: " + toQString(f->glGetString(GL_RENDERER));
out << "OpenGL driver version: " + toQString(f->glGetString(GL_VERSION));
return true; return true;
} }
} // namespace } // namespace
#ifndef Q_OS_LINUX
QStringList HardwareInfo::gpuInfo()
{
QStringList info;
vulkanInfo(info);
openGlInfo(info);
return info;
}
#endif #endif
#ifdef Q_OS_WINDOWS #ifdef Q_OS_WINDOWS
@ -104,7 +65,11 @@ QStringList HardwareInfo::gpuInfo()
#endif #endif
#include <QSettings> #include <QSettings>
#include "windows.h" #include <dxgi1_6.h>
#include <windows.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
QString HardwareInfo::cpuInfo() QString HardwareInfo::cpuInfo()
{ {
@ -140,16 +105,42 @@ uint64_t HardwareInfo::availableRamMiB()
return 0; return 0;
} }
QStringList HardwareInfo::gpuInfo()
{
ComPtr<IDXGIFactory6> factory;
HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory));
if (FAILED(hr)) {
qWarning() << "Could not create DXGI factory:" << Qt::hex << hr;
return { "GPU discovery failed: could not create DXGI factory" };
}
UINT i = 0;
ComPtr<IDXGIAdapter> adapter;
QStringList out;
while (factory->EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)) != DXGI_ERROR_NOT_FOUND) {
DXGI_ADAPTER_DESC desc;
hr = adapter->GetDesc(&desc);
if (SUCCEEDED(hr)) {
out << "GPU: " + QString::fromWCharArray(desc.Description); // NOLINT(*-pro-bounds-array-to-pointer-decay, *-no-array-decay)
} else {
qWarning() << "Could not get DXGI adapter description:" << Qt::hex << hr;
}
++i;
}
return out;
}
#elif defined(Q_OS_MACOS) #elif defined(Q_OS_MACOS)
#include "mach/mach.h"
#include "sys/sysctl.h" #include "sys/sysctl.h"
QString HardwareInfo::cpuInfo() QString HardwareInfo::cpuInfo()
{ {
std::array<char, 512> buffer; std::array<char, 512> buffer{};
size_t bufferSize = buffer.size(); size_t bufferSize = buffer.size();
if (sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferSize, nullptr, 0) == 0) { if (sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferSize, nullptr, 0) == 0) {
return QString(buffer.data()); return { buffer.data() };
} }
qWarning() << "Could not get CPU model: sysctlbyname"; qWarning() << "Could not get CPU model: sysctlbyname";
@ -158,7 +149,7 @@ QString HardwareInfo::cpuInfo()
uint64_t HardwareInfo::totalRamMiB() uint64_t HardwareInfo::totalRamMiB()
{ {
uint64_t memsize; uint64_t memsize = 0;
size_t memsizeSize = sizeof memsize; size_t memsizeSize = sizeof memsize;
if (sysctlbyname("hw.memsize", &memsize, &memsizeSize, nullptr, 0) == 0) { if (sysctlbyname("hw.memsize", &memsize, &memsizeSize, nullptr, 0) == 0) {
// transforming bytes -> mib // transforming bytes -> mib
@ -171,36 +162,62 @@ uint64_t HardwareInfo::totalRamMiB()
uint64_t HardwareInfo::availableRamMiB() uint64_t HardwareInfo::availableRamMiB()
{ {
mach_port_t host_port = mach_host_self(); return 0;
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
vm_statistics64_data_t vm_stats;
if (host_statistics64(host_port, HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm_stats), &count) == KERN_SUCCESS) {
// transforming bytes -> mib
return (vm_stats.free_count + vm_stats.inactive_count) * vm_page_size / 1024 / 1024;
} }
qWarning() << "Could not get available RAM: host_statistics64"; MacOSHardwareInfo::MemoryPressureLevel MacOSHardwareInfo::memoryPressureLevel()
return 0; {
uint32_t level = 0;
size_t levelSize = sizeof level;
if (sysctlbyname("kern.memorystatus_vm_pressure_level", &level, &levelSize, nullptr, 0) == 0) {
return static_cast<MemoryPressureLevel>(level);
}
qWarning() << "Could not get memory pressure level: sysctlbyname";
return MemoryPressureLevel::Normal;
}
QString MacOSHardwareInfo::memoryPressureLevelName()
{
// The names are internal, users refer to levels by their graph colors in Activity Monitor
switch (memoryPressureLevel()) {
case MemoryPressureLevel::Normal:
return "Green";
case MemoryPressureLevel::Warning:
return "Yellow";
case MemoryPressureLevel::Critical:
return "Red";
default:
Q_ASSERT(false);
return "";
}
}
QStringList HardwareInfo::gpuInfo()
{
QStringList out;
const bool success = readFromOutput("system_profiler SPDisplaysDataType", [&](const QString& str) {
// Chipset Model: Intel HD Graphics 620
if (str.contains("Chipset Model")) {
out << "GPU: " + afterColon(str);
}
});
if (!success) {
return { "GPU discovery failed: could not read from system_profiler" };
}
return out;
} }
#elif defined(Q_OS_LINUX) #elif defined(Q_OS_LINUX)
#include <fstream> #include <fstream>
namespace {
QString afterColon(QString& str)
{
return str.remove(0, str.indexOf(':') + 2).trimmed();
}
} // namespace
QString HardwareInfo::cpuInfo() QString HardwareInfo::cpuInfo()
{ {
std::ifstream cpuin("/proc/cpuinfo"); std::ifstream cpuin("/proc/cpuinfo");
for (std::string line; std::getline(cpuin, line);) { for (std::string line; std::getline(cpuin, line);) {
// model name : AMD Ryzen 7 5800X 8-Core Processor // model name : AMD Ryzen 7 5800X 8-Core Processor
if (QString str = QString::fromStdString(line); str.startsWith("model name")) { if (const QString str = QString::fromStdString(line); str.startsWith("model name")) {
return afterColon(str); return afterColon(str);
} }
} }
@ -209,12 +226,13 @@ QString HardwareInfo::cpuInfo()
return "unknown"; return "unknown";
} }
uint64_t readMemInfo(QString searchTarget) namespace {
uint64_t readMemInfo(const QString& searchTarget)
{ {
std::ifstream memin("/proc/meminfo"); std::ifstream memin("/proc/meminfo");
for (std::string line; std::getline(memin, line);) { for (std::string line; std::getline(memin, line);) {
// MemTotal: 16287480 kB // MemTotal: 16287480 kB
if (QString str = QString::fromStdString(line); str.startsWith(searchTarget)) { if (const QString str = QString::fromStdString(line); str.startsWith(searchTarget)) {
bool ok = false; bool ok = false;
const uint total = str.simplified().section(' ', 1, 1).toUInt(&ok); const uint total = str.simplified().section(' ', 1, 1).toUInt(&ok);
if (!ok) { if (!ok) {
@ -230,6 +248,7 @@ uint64_t readMemInfo(QString searchTarget)
qWarning() << "Could not read /proc/meminfo: search target not found:" << searchTarget; qWarning() << "Could not read /proc/meminfo: search target not found:" << searchTarget;
return 0; return 0;
} }
} // namespace
uint64_t HardwareInfo::totalRamMiB() uint64_t HardwareInfo::totalRamMiB()
{ {
@ -243,52 +262,50 @@ uint64_t HardwareInfo::availableRamMiB()
QStringList HardwareInfo::gpuInfo() QStringList HardwareInfo::gpuInfo()
{ {
QStringList list;
const bool vulkanSuccess = vulkanInfo(list);
const bool openGlSuccess = openGlInfo(list);
if (vulkanSuccess || openGlSuccess) {
return list;
}
std::array<char, 512> buffer;
FILE* lspci = popen("lspci -k", "r");
if (!lspci) {
return { "Could not detect GPUs: lspci is not present" };
}
bool readingGpuInfo = false; bool readingGpuInfo = false;
QString currentModel = ""; QString gpu;
while (fgets(buffer.data(), 512, lspci) != nullptr) { QString driverInUse = "NONE";
QString str(buffer.data()); QString driversAvailable = "NONE";
QStringList out;
const bool success = readFromOutput("lspci -k", [&](const QString& str) {
// clang-format off // clang-format off
// 04:00.0 VGA compatible controller: Advanced Micro Devices, Inc. [AMD/ATI] Ellesmere [Radeon RX 470/480/570/570X/580/580X/590] (rev e7) // 04:00.0 VGA compatible controller: Advanced Micro Devices, Inc. [AMD/ATI] Ellesmere [Radeon RX 470/480/570/570X/580/580X/590] (rev e7)
// Subsystem: Sapphire Technology Limited Radeon RX 580 Pulse 4GB // Subsystem: Sapphire Technology Limited Radeon RX 580 Pulse 4GB
// Kernel driver in use: amdgpu // Kernel driver in use: amdgpu
// Kernel modules: amdgpu // Kernel modules: amdgpu
// clang-format on // clang-format on
if (str.contains("VGA compatible controller")) { if (str.contains("VGA compatible controller") || str.contains("3D controller")) {
readingGpuInfo = true; readingGpuInfo = true;
} else if (!str.startsWith('\t')) { } else if (!str.startsWith('\t')) {
if (readingGpuInfo) {
out << QString("GPU: %1 (driver in use: %2; drivers available: %3)").arg(gpu, driverInUse, driversAvailable);
driverInUse = "NONE";
driversAvailable = "NONE";
}
readingGpuInfo = false; readingGpuInfo = false;
} }
if (!readingGpuInfo) { if (!readingGpuInfo) {
continue; return;
} }
const QString value = afterColon(str);
if (str.contains("Subsystem")) { if (str.contains("Subsystem")) {
currentModel = "Found GPU: " + afterColon(str); gpu = value;
} }
if (str.contains("Kernel driver in use")) { if (str.contains("Kernel driver in use")) {
currentModel += " (using driver " + afterColon(str); driverInUse = value;
} }
if (str.contains("Kernel modules")) { if (str.contains("Kernel modules")) {
currentModel += ", available drivers: " + afterColon(str) + ")"; driversAvailable = value;
list.append(currentModel);
} }
});
if (!success) {
return { "GPU discovery failed: could not read from lspci" };
} }
pclose(lspci);
return list; return out;
} }
#else #else
@ -303,19 +320,20 @@ QString HardwareInfo::cpuInfo()
uint64_t HardwareInfo::totalRamMiB() uint64_t HardwareInfo::totalRamMiB()
{ {
char buff[512]; uint64_t out = 0;
FILE* fp = popen("sysctl hw.physmem", "r");
if (fp != nullptr) { const bool success = readFromOutput("sysctl hw.physmem", [&](const QString& str) {
if (fgets(buff, 512, fp) != nullptr) { const uint64_t mem = str.mid(12).toULong();
std::string str(buff);
uint64_t mem = std::stoull(str.substr(12, std::string::npos));
// transforming kib -> mib // transforming kib -> mib
return mem / 1024; out = mem / 1024;
} });
if (!success) {
qWarning() << "Could not get total RAM: could not read from sysctl";
return 0;
} }
return 0; return out;
} }
#else #else
@ -330,4 +348,8 @@ uint64_t HardwareInfo::availableRamMiB()
return 0; return 0;
} }
QStringList HardwareInfo::gpuInfo()
{
return { "GPU discovery failed: not implemented for this OS" };
}
#endif #endif

View file

@ -27,3 +27,16 @@ uint64_t totalRamMiB();
uint64_t availableRamMiB(); uint64_t availableRamMiB();
QStringList gpuInfo(); QStringList gpuInfo();
} // namespace HardwareInfo } // namespace HardwareInfo
#ifdef Q_OS_MACOS
namespace MacOSHardwareInfo {
enum class MemoryPressureLevel : uint8_t {
Normal = 1,
Warning = 2,
Critical = 4,
};
MemoryPressureLevel memoryPressureLevel();
QString memoryPressureLevelName();
} // namespace MacOSHardwareInfo
#endif

View file

@ -3,6 +3,7 @@
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
#include "Application.h"
#include "InstanceTask.h" #include "InstanceTask.h"
#include "minecraft/MinecraftLoadAndCheck.h" #include "minecraft/MinecraftLoadAndCheck.h"
#include "tasks/SequentialTask.h" #include "tasks/SequentialTask.h"
@ -18,7 +19,7 @@ bool InstanceCreationTask::abort()
return m_gameFilesTask->abort(); return m_gameFilesTask->abort();
} }
return true; return InstanceTask::abort();
} }
void InstanceCreationTask::executeTask() void InstanceCreationTask::executeTask()
@ -38,8 +39,9 @@ void InstanceCreationTask::executeTask()
m_instance = createInstance(); m_instance = createInstance();
if (!m_instance) { if (!m_instance) {
if (m_abort) if (m_abort) {
return; return;
}
qWarning() << "Instance creation failed!"; qWarning() << "Instance creation failed!";
if (!m_error_message.isEmpty()) { if (!m_error_message.isEmpty()) {
@ -63,8 +65,9 @@ void InstanceCreationTask::executeTask()
qDebug() << "Removing old files"; qDebug() << "Removing old files";
for (const QString& path : m_filesToRemove) { for (const QString& path : m_filesToRemove) {
if (!QFile::exists(path)) if (!QFile::exists(path)) {
continue; continue;
}
qDebug() << "Removing" << path; qDebug() << "Removing" << path;
@ -81,6 +84,10 @@ void InstanceCreationTask::executeTask()
} }
if (!m_abort) { if (!m_abort) {
if (!APPLICATION->settings()->get("DownloadGameFilesDuringInstanceCreation").toBool()) {
emitSucceeded();
return;
}
setAbortable(true); setAbortable(true);
setAbortButtonText(tr("Skip")); setAbortButtonText(tr("Skip"));
qDebug() << "Downloading game files"; qDebug() << "Downloading game files";
@ -110,7 +117,7 @@ void InstanceCreationTask::executeTask()
} }
} }
void InstanceCreationTask::scheduleToDelete(QWidget* parent, QDir dir, QString path, bool checkDisabled) void InstanceCreationTask::scheduleToDelete(QWidget* parent, const QDir& dir, const QString& path, bool checkDisabled)
{ {
if (path.isEmpty()) { if (path.isEmpty()) {
return; return;

View file

@ -38,7 +38,7 @@ class InstanceCreationTask : public InstanceTask {
protected: protected:
void setError(const QString& message) { m_error_message = message; }; void setError(const QString& message) { m_error_message = message; };
void scheduleToDelete(QWidget* parent, QDir dir, QString path, bool checkDisabled = false); void scheduleToDelete(QWidget* parent, const QDir& dir, const QString& path, bool checkDisabled = false);
protected: protected:
bool m_abort = false; bool m_abort = false;

View file

@ -924,23 +924,23 @@ class InstanceStaging : public Task {
connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::progress, this, &InstanceStaging::setProgress);
connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress); connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
m_backoffTimer.setSingleShot(true);
} }
virtual ~InstanceStaging() {} ~InstanceStaging() override = default;
// FIXME/TODO: add ability to abort during instance commit retries // FIXME/TODO: add ability to abort during instance commit retries
bool abort() override bool abort() override
{ {
if (!canAbort()) if (!canAbort()) {
return false; return false;
}
return m_child->abort(); return m_child->abort();
} }
bool canAbort() const override { return (m_child && m_child->canAbort()); } bool canAbort() const override { return (m_child && m_child->canAbort()); }
protected: protected:
virtual void executeTask() override void executeTask() override
{ {
if (m_stagingPath.isNull()) { if (m_stagingPath.isNull()) {
emitFailed(tr("Could not create staging folder")); emitFailed(tr("Could not create staging folder"));
@ -954,10 +954,8 @@ class InstanceStaging : public Task {
private slots: private slots:
void childSucceeded() void childSucceeded()
{ {
if (!isRunning())
return;
unsigned sleepTime = backoff(); unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) { if (m_parent->commitStagedInstance(m_stagingPath, *m_child, m_child->group(), *m_child)) {
m_backoffTimer.stop(); m_backoffTimer.stop();
emitSucceeded(); emitSucceeded();
return; return;

View file

@ -40,6 +40,7 @@
#include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountList.h" #include "minecraft/auth/AccountList.h"
#include "net/NetUtils.h"
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/MSALoginDialog.h"
@ -109,7 +110,7 @@ void LaunchController::decideAccount()
} }
} }
if (!m_accountToUse) { if (!m_accountToUse && accounts->anyAccountIsValid()) {
// 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.
ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox, ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox,
m_parentWidget); m_parentWidget);
@ -133,14 +134,6 @@ LaunchDecision LaunchController::decideLaunchMode()
return LaunchDecision::Continue; return LaunchDecision::Continue;
} }
if (m_wantedLaunchMode == LaunchMode::Normal) {
if (m_accountToUse->shouldRefresh() || m_accountToUse->accountState() == AccountState::Offline) {
// Force account refresh on the account used to launch the instance updating the AccountState
// only on first try and if it is not meant to be offline
m_accountToUse->refresh();
}
}
const auto* accounts = APPLICATION->accounts(); const auto* accounts = APPLICATION->accounts();
MinecraftAccountPtr accountToCheck = nullptr; MinecraftAccountPtr accountToCheck = nullptr;
@ -163,7 +156,9 @@ LaunchDecision LaunchController::decideLaunchMode()
} }
auto state = accountToCheck->accountState(); auto state = accountToCheck->accountState();
if (state == AccountState::Unchecked || state == AccountState::Errored) { const bool needsRefresh =
m_wantedLaunchMode == LaunchMode::Normal && (state == AccountState::Offline || accountToCheck->shouldRefresh());
if (state == AccountState::Unchecked || state == AccountState::Errored || needsRefresh) {
accountToCheck->refresh(); accountToCheck->refresh();
state = AccountState::Working; state = AccountState::Working;
} }
@ -231,13 +226,14 @@ bool LaunchController::askPlayDemo() const
return box.clickedButton() == demoButton; return box.clickedButton() == demoButton;
} }
QString LaunchController::askOfflineName(const QString& playerName, bool* ok) const QString LaunchController::askOfflineName(const QString& playerName, bool* ok)
{ {
if (ok != nullptr) { if (ok != nullptr) {
*ok = false; *ok = false;
} }
QString message; QString title, message;
title = tr("Player name");
switch (m_actualLaunchMode) { switch (m_actualLaunchMode) {
case LaunchMode::Normal: case LaunchMode::Normal:
Q_ASSERT(false); Q_ASSERT(false);
@ -247,7 +243,14 @@ QString LaunchController::askOfflineName(const QString& playerName, bool* ok) co
break; break;
case LaunchMode::Offline: case LaunchMode::Offline:
if (m_wantedLaunchMode == LaunchMode::Normal) { if (m_wantedLaunchMode == LaunchMode::Normal) {
message = tr("You are not connected to the Internet, launching in offline mode\n\n"); auto netErr = m_accountToUse->accountData()->networkError;
if (Net::isServerError(netErr)) {
title = tr("Auth servers offline");
message = tr("The Minecraft authentication servers are currently unavailable, launching in offline mode.\n\n");
} else {
title = tr("No internet connection");
message = tr("You are not connected to the Internet, launching in offline mode.\n\n");
}
} }
message += tr("Choose your offline mode player name"); message += tr("Choose your offline mode player name");
break; break;
@ -257,7 +260,7 @@ QString LaunchController::askOfflineName(const QString& playerName, bool* ok) co
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
ChooseOfflineNameDialog dialog(message, m_parentWidget); ChooseOfflineNameDialog dialog(message, m_parentWidget);
dialog.setWindowTitle(tr("Player name")); dialog.setWindowTitle(title);
dialog.setUsername(usedname); dialog.setUsername(usedname);
if (dialog.exec() != QDialog::Accepted) { if (dialog.exec() != QDialog::Accepted) {
return {}; return {};
@ -339,11 +342,11 @@ bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account,
if (button == QMessageBox::StandardButton::Yes) { if (button == QMessageBox::StandardButton::Yes) {
auto* accounts = APPLICATION->accounts(); auto* accounts = APPLICATION->accounts();
const bool isDefault = accounts->defaultAccount() == account; const bool isDefault = accounts->defaultAccount() == account;
accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId())));
if (account->accountType() == AccountType::MSA) { if (account->accountType() == AccountType::MSA) {
auto newAccount = MSALoginDialog::newAccount(m_parentWidget); auto newAccount = MSALoginDialog::newAccount(m_parentWidget);
if (newAccount != nullptr) { if (newAccount != nullptr) {
accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId())));
accounts->addAccount(newAccount); accounts->addAccount(newAccount);
if (isDefault) { if (isDefault) {

View file

@ -78,7 +78,7 @@ class LaunchController : public Task {
void decideAccount(); void decideAccount();
LaunchDecision decideLaunchMode(); LaunchDecision decideLaunchMode();
bool askPlayDemo() const; bool askPlayDemo() const;
QString askOfflineName(const QString& playerName, bool* ok = nullptr) const; QString askOfflineName(const QString& playerName, bool* ok = nullptr);
bool reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason); bool reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason);
private slots: private slots:

View file

@ -114,7 +114,7 @@ void LoggedProcess::on_error(QProcess::ProcessError error)
{ {
switch (error) { switch (error) {
case QProcess::FailedToStart: { case QProcess::FailedToStart: {
emit log({ tr("The process failed to start.") }, MessageLevel::Fatal); emit log({ tr("The process failed to start: %1").arg(errorString()) }, MessageLevel::Fatal);
changeState(LoggedProcess::FailedToStart); changeState(LoggedProcess::FailedToStart);
break; break;
} }

View file

@ -14,26 +14,26 @@
else \ else \
type = Qt::DirectConnection; type = Qt::DirectConnection;
#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ #define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE, RET_DEF) \
static RET_TYPE NAME() \ static RET_TYPE NAME() \
{ \ { \
RET_TYPE ret; \ RET_TYPE ret = RET_DEF; \
GET_TYPE() \ GET_TYPE() \
QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \
return ret; \ return ret; \
} }
#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ #define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, RET_DEF, PARAM_1_TYPE) \
static RET_TYPE NAME(PARAM_1_TYPE p1) \ static RET_TYPE NAME(PARAM_1_TYPE p1) \
{ \ { \
RET_TYPE ret; \ RET_TYPE ret = RET_DEF; \
GET_TYPE() \ GET_TYPE() \
QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \
return ret; \ return ret; \
} }
#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ #define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, RET_DEF, PARAM_1_TYPE, PARAM_2_TYPE) \
static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \
{ \ { \
RET_TYPE ret; \ RET_TYPE ret = RET_DEF; \
GET_TYPE() \ GET_TYPE() \
QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \
Q_ARG(PARAM_2_TYPE, p2)); \ Q_ARG(PARAM_2_TYPE, p2)); \
@ -53,18 +53,18 @@ class PixmapCache final : public QObject {
static void setInstance(PixmapCache* i) { s_instance = i; } static void setInstance(PixmapCache* i) { s_instance = i; }
public: public:
DEFINE_FUNC_NO_PARAM(cacheLimit, int) DEFINE_FUNC_NO_PARAM(cacheLimit, int, -1)
DEFINE_FUNC_NO_PARAM(clear, bool) DEFINE_FUNC_NO_PARAM(clear, bool, false)
DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) DEFINE_FUNC_TWO_PARAM(find, bool, false, const QString&, QPixmap*)
DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) DEFINE_FUNC_TWO_PARAM(find, bool, false, const QPixmapCache::Key&, QPixmap*)
DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) DEFINE_FUNC_TWO_PARAM(insert, bool, false, const QString&, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, {}, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) DEFINE_FUNC_ONE_PARAM(remove, bool, false, const QString&)
DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) DEFINE_FUNC_ONE_PARAM(remove, bool, false, const QPixmapCache::Key&)
DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) DEFINE_FUNC_TWO_PARAM(replace, bool, false, const QPixmapCache::Key&, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, false, int)
DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool, false)
DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, false, int)
// NOTE: Every function returns something non-void to simplify the macros. // NOTE: Every function returns something non-void to simplify the macros.
private slots: private slots:

View file

@ -19,23 +19,52 @@
#include "ResourceDownloadTask.h" #include "ResourceDownloadTask.h"
#include <utility>
#include "Application.h" #include "Application.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/ResourceFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/helpers/HashUtils.h" #include "modplatform/helpers/HashUtils.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/ChecksumValidator.h" #include "net/ChecksumValidator.h"
namespace {
Net::ModrinthDownloadMeta createModrinthMeta(BaseInstance* instance, QString reason)
{
auto* mcInstance = dynamic_cast<MinecraftInstance*>(instance);
if (!mcInstance) {
return {};
}
auto* profile = mcInstance->getPackProfile();
if (!profile) {
return {};
}
auto loaders = profile->getModLoadersList();
return {
.reason = std::move(reason),
.gameVersion = profile->getComponentVersion("net.minecraft"),
.loader = !loaders.isEmpty() ? ModPlatform::getModLoaderAsString(loaders.first()) : "",
};
}
} // namespace
ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version, ModPlatform::IndexedVersion version,
ResourceFolderModel* packs, ResourceFolderModel* packs,
bool is_indexed) bool isIndexed,
QString downloadReason)
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs)
{ {
if (is_indexed) { if (isIndexed) {
m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version)); m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version));
connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource); connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource);
@ -45,7 +74,9 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network()));
m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl));
auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename())); auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename()),
Net::Download::Option::NoOptions,
createModrinthMeta(m_pack_model->instance(), std::move(downloadReason)));
if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) {
switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { switch (Hashing::algorithmFromString(m_pack_version.hash_type)) {
case Hashing::Algorithm::Md4: case Hashing::Algorithm::Md4:
@ -82,8 +113,9 @@ void ResourceDownloadTask::downloadSucceeded()
auto oldName = std::get<0>(to_delete); auto oldName = std::get<0>(to_delete);
auto oldFilename = std::get<1>(to_delete); auto oldFilename = std::get<1>(to_delete);
if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) {
return; return;
}
m_pack_model->uninstallResource(oldFilename, true); m_pack_model->uninstallResource(oldFilename, true);
@ -95,16 +127,17 @@ void ResourceDownloadTask::downloadSucceeded()
if (oldConfig.exists() && !newConfig.exists()) { if (oldConfig.exists() && !newConfig.exists()) {
bool success = FS::move(oldConfig.filePath(), newConfig.filePath()); bool success = FS::move(oldConfig.filePath(), newConfig.filePath());
if (!success) if (!success) {
emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName())); emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName()));
} }
} }
} }
}
void ResourceDownloadTask::downloadFailed(QString reason) void ResourceDownloadTask::downloadFailed(QString reason)
{ {
m_filesNetJob.reset(); m_filesNetJob.reset();
emitFailed(reason); emitFailed(std::move(reason));
} }
void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
@ -114,7 +147,7 @@ void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
// This indirection is done so that we don't delete a mod before being sure it was // This indirection is done so that we don't delete a mod before being sure it was
// downloaded successfully! // downloaded successfully!
void ResourceDownloadTask::hasOldResource(QString name, QString filename) void ResourceDownloadTask::hasOldResource(const QString& name, const QString& filename)
{ {
to_delete = { name, filename }; to_delete = { name, filename };
} }

View file

@ -33,7 +33,8 @@ class ResourceDownloadTask : public SequentialTask {
explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version, ModPlatform::IndexedVersion version,
ResourceFolderModel* packs, ResourceFolderModel* packs,
bool is_indexed = true); bool isIndexed = true,
QString downloadReason = "standalone");
const QString& getFilename() const { return m_pack_version.fileName; } const QString& getFilename() const { return m_pack_version.fileName; }
const QVariant& getVersionID() const { return m_pack_version.fileId; } const QVariant& getVersionID() const { return m_pack_version.fileId; }
const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; }
@ -56,5 +57,5 @@ class ResourceDownloadTask : public SequentialTask {
std::tuple<QString, QString> to_delete{ "", "" }; std::tuple<QString, QString> to_delete{ "", "" };
private slots: private slots:
void hasOldResource(QString name, QString filename); void hasOldResource(const QString& name, const QString& filename);
}; };

View file

@ -179,13 +179,20 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
void JavaChecker::error(QProcess::ProcessError err) void JavaChecker::error(QProcess::ProcessError err)
{ {
if (err == QProcess::FailedToStart) { if (err == QProcess::FailedToStart) {
qDebug() << "Java checker has failed to start."; qDebug() << "Java checker has failed to start:" << process->errorString();
qDebug() << "Process environment:"; qDebug() << "Process environment:";
qDebug() << process->environment(); qDebug() << process->environment();
qDebug() << "Native environment:"; qDebug() << "Native environment:";
qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); qDebug() << QProcessEnvironment::systemEnvironment().toStringList();
killTimer.stop(); killTimer.stop();
emit checkFinished({ m_path, m_id });
Result result = {
m_path,
m_id,
};
result.errorLog = process->errorString();
result.validity = Result::Validity::Errored;
emit checkFinished(result);
} }
emitSucceeded(); emitSucceeded();
} }

View file

@ -51,8 +51,9 @@ JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions)
: BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions)
{} {}
Task::Ptr JavaInstallList::getLoadTask() Task::Ptr JavaInstallList::getLoadTask(bool forceReload)
{ {
Q_UNUSED(forceReload)
load(); load();
return getCurrentTask(); return getCurrentTask();
} }

View file

@ -35,7 +35,7 @@ class JavaInstallList : public BaseVersionList {
public: public:
explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false);
Task::Ptr getLoadTask() override; Task::Ptr getLoadTask(bool forceReload = false) override;
bool isLoaded() override; bool isLoaded() override;
const BaseVersion::Ptr at(int i) const override; const BaseVersion::Ptr at(int i) const override;
int count() const override; int count() const override;

View file

@ -82,12 +82,12 @@ QUrl BaseEntity::url() const
return QUrl(metaOverride).resolved(localFilename()); return QUrl(metaOverride).resolved(localFilename());
} }
Task::Ptr BaseEntity::loadTask(Net::Mode mode) Task::Ptr BaseEntity::loadTask(Net::Mode mode, bool forceReload)
{ {
if (m_task && m_task->isRunning()) { if (m_task && m_task->isRunning()) {
return m_task; return m_task;
} }
m_task.reset(new BaseEntityLoadTask(this, mode)); m_task.reset(new BaseEntityLoadTask(this, mode, forceReload));
return m_task; return m_task;
} }
@ -107,7 +107,9 @@ BaseEntity::LoadStatus BaseEntity::status() const
return m_load_status; return m_load_status;
} }
BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {} BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload)
: m_entity(parent), m_mode(mode), m_force_reload(forceReload)
{}
void BaseEntityLoadTask::executeTask() void BaseEntityLoadTask::executeTask()
{ {
@ -125,9 +127,11 @@ void BaseEntityLoadTask::executeTask()
} }
// on online the hash needs to match // on online the hash needs to match
hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256; const auto& expected = m_entity->m_sha256;
const auto& actual = m_entity->m_file_sha256;
hashMatches = expected == actual;
if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) {
throw Exception("mismatched checksum"); throw Exception(QString("Checksum mismatch, expected sha256: %1, got: %2").arg(expected, actual));
} }
// load local file // load local file
@ -149,13 +153,18 @@ void BaseEntityLoadTask::executeTask()
auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline; auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline;
// if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match // if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match
auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches; auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches;
if (wasLoadedOffline || wasLoadedRemote) { if (wasLoadedOffline || (wasLoadedRemote && !m_force_reload)) {
emitSucceeded(); emitSucceeded();
return; return;
} }
m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network())); m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network()));
auto url = m_entity->url(); auto url = m_entity->url();
auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename()); auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename());
if (m_force_reload) {
// clear validators so manual refreshes fetch a fresh body
entry->setETag({});
entry->setRemoteChangedTimestamp({});
}
entry->setStale(true); entry->setStale(true);
auto dl = Net::ApiDownload::makeCached(url, entry); auto dl = Net::ApiDownload::makeCached(url, entry);
/* /*

View file

@ -43,7 +43,7 @@ class BaseEntity {
void setSha256(QString sha256); void setSha256(QString sha256);
virtual void parse(const QJsonObject& obj) = 0; virtual void parse(const QJsonObject& obj) = 0;
[[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online); [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online, bool forceReload = false);
protected: protected:
QString m_sha256; // the expected sha256 QString m_sha256; // the expected sha256
@ -58,7 +58,7 @@ class BaseEntityLoadTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode); explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload);
~BaseEntityLoadTask() override = default; ~BaseEntityLoadTask() override = default;
virtual void executeTask() override; virtual void executeTask() override;
@ -68,6 +68,7 @@ class BaseEntityLoadTask : public Task {
private: private:
BaseEntity* m_entity; BaseEntity* m_entity;
Net::Mode m_mode; Net::Mode m_mode;
bool m_force_reload = false;
NetJob::Ptr m_task; NetJob::Ptr m_task;
}; };
} // namespace Meta } // namespace Meta

View file

@ -15,6 +15,7 @@
#include "Index.h" #include "Index.h"
#include "Application.h"
#include "JsonFormat.h" #include "JsonFormat.h"
#include "QObjectPtr.h" #include "QObjectPtr.h"
#include "VersionList.h" #include "VersionList.h"
@ -135,7 +136,7 @@ void Index::connectVersionList(const int row, const VersionList::Ptr& list)
Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force) Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force)
{ {
if (mode == Net::Mode::Offline) { if (mode == Net::Mode::Offline || !APPLICATION->settings()->get("MetaRefreshOnLaunch").toBool()) {
return get(uid, version)->loadTask(mode); return get(uid, version)->loadTask(mode);
} }

View file

@ -32,11 +32,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(
setObjectName("Version list: " + uid); setObjectName("Version list: " + uid);
} }
Task::Ptr VersionList::getLoadTask() Task::Ptr VersionList::getLoadTask(bool forceReload)
{ {
auto loadTask = makeShared<SequentialTask>(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); auto loadTask = makeShared<SequentialTask>(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid));
loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online, forceReload));
loadTask->addTask(this->loadTask(Net::Mode::Online)); loadTask->addTask(this->loadTask(Net::Mode::Online, forceReload));
return loadTask; return loadTask;
} }

View file

@ -37,7 +37,7 @@ class VersionList : public BaseVersionList, public BaseEntity {
enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole };
bool isLoaded() override; bool isLoaded() override;
Task::Ptr getLoadTask() override; Task::Ptr getLoadTask(bool forceReload = false) override;
const BaseVersion::Ptr at(int i) const override; const BaseVersion::Ptr at(int i) const override;
int count() const override; int count() const override;
void sortVersions() override; void sortVersions() override;

View file

@ -349,7 +349,8 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext,
QStringList& jars, QStringList& jars,
QStringList& nativeJars, QStringList& nativeJars,
const QString& overridePath, const QString& overridePath,
const QString& tempPath) const const QString& tempPath,
bool addJarMods) const
{ {
QStringList native32, native64; QStringList native32, native64;
jars.clear(); jars.clear();
@ -360,7 +361,7 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext,
// NOTE: order is important here, add main jar last to the lists // NOTE: order is important here, add main jar last to the lists
if (m_mainJar) { if (m_mainJar) {
// FIXME: HACK!! jar modding is weird and unsystematic! // FIXME: HACK!! jar modding is weird and unsystematic!
if (m_jarMods.size()) { if (m_jarMods.size() && addJarMods) {
QDir tempDir(tempPath); QDir tempDir(tempPath);
jars.append(tempDir.absoluteFilePath("minecraft.jar")); jars.append(tempDir.absoluteFilePath("minecraft.jar"));
} else { } else {

View file

@ -87,7 +87,8 @@ class LaunchProfile : public ProblemProvider {
QStringList& jars, QStringList& jars,
QStringList& nativeJars, QStringList& nativeJars,
const QString& overridePath, const QString& overridePath,
const QString& tempPath) const; const QString& tempPath,
bool addJarMods = true) const;
bool hasTrait(const QString& trait) const; bool hasTrait(const QString& trait) const;
ProblemSeverity getProblemSeverity() const override; ProblemSeverity getProblemSeverity() const override;
const QList<PatchProblem> getProblems() const override; const QList<PatchProblem> getProblems() const override;

View file

@ -149,7 +149,7 @@ QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeC
if (sha1.size()) { if (sha1.size()) {
auto dl = Net::ApiDownload::makeCached(url, entry, options); auto dl = Net::ApiDownload::makeCached(url, entry, options);
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1));
qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url << "expected sha1:" << sha1;
out.append(dl); out.append(dl);
} else { } else {
out.append(Net::ApiDownload::makeCached(url, entry, options)); out.append(Net::ApiDownload::makeCached(url, entry, options));

View file

@ -131,7 +131,8 @@
for (const auto& gpu : gpus) { for (const auto& gpu : gpus) {
QString name = qvariant_cast<QString>(gpu[QStringLiteral("Name")]); QString name = qvariant_cast<QString>(gpu[QStringLiteral("Name")]);
bool defaultGpu = qvariant_cast<bool>(gpu[QStringLiteral("Default")]); bool defaultGpu = qvariant_cast<bool>(gpu[QStringLiteral("Default")]);
if (!defaultGpu) { bool discrete = qvariant_cast<bool>(gpu.value(QStringLiteral("Discrete"), !defaultGpu));
if (discrete) {
QStringList envList = qvariant_cast<QStringList>(gpu[QStringLiteral("Environment")]); QStringList envList = qvariant_cast<QStringList>(gpu[QStringLiteral("Environment")]);
for (int i = 0; i + 1 < envList.size(); i += 2) { for (int i = 0; i + 1 < envList.size(); i += 2) {
env.insert(envList[i], envList[i + 1]); env.insert(envList[i], envList[i + 1]);
@ -209,6 +210,7 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("LowMemWarning"), memorySetting);
// Native library workarounds // Native library workarounds
auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
@ -891,6 +893,14 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
QStringList out; QStringList out;
out << "Components:";
for (int i = 0; i < m_components->rowCount(); ++i) {
const auto& component = m_components->getComponent(i);
out << indent +
QString("%1) %2 (%3) %4").arg(QString::number(i + 1), component->getName(), component->getID(), component->getVersion());
}
out << emptyLine;
out << "Launcher: " + getLauncher(); out << "Launcher: " + getLauncher();
out << "Main class: " + getMainClass() << emptyLine; out << "Main class: " + getMainClass() << emptyLine;

View file

@ -7,30 +7,27 @@
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version) VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loaderVersion)
: InstanceCreationTask() : m_version(std::move(version)), m_using_loader(true), m_loader(std::move(loader)), m_loader_version(std::move(loaderVersion))
, m_version(std::move(version))
, m_using_loader(true)
, m_loader(std::move(loader))
, m_loader_version(std::move(loader_version))
{} {}
std::unique_ptr<MinecraftInstance> VanillaCreationTask::createInstance() std::unique_ptr<MinecraftInstance> VanillaCreationTask::createInstance()
{ {
setStatus(tr("Creating instance from version %1").arg(m_version->name())); setStatus(tr("Creating instance from version %1").arg(m_version->name()));
auto inst = std::make_unique<MinecraftInstance>(m_globalSettings, std::make_unique<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")), auto inst = std::make_unique<MinecraftInstance>(
m_stagingPath); m_globalSettings, std::make_unique<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")), m_stagingPath);
SettingsObject::Lock lock(inst->settings()); SettingsObject::Lock lock(inst->settings());
auto components = inst->getPackProfile(); auto* components = inst->getPackProfile();
components->buildingFromScratch(); components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_version->descriptor(), true); components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
if (m_using_loader) if (m_using_loader) {
components->setComponentVersion(m_loader, m_loader_version->descriptor()); components->setComponentVersion(m_loader, m_loader_version->descriptor());
}
inst->setName(name()); inst->setName(name());
inst->setIconKey(m_instIcon); inst->setIconKey(m_instIcon);
components->saveNow();
return inst; return inst;
} }

View file

@ -7,8 +7,8 @@
class VanillaCreationTask final : public InstanceCreationTask { class VanillaCreationTask final : public InstanceCreationTask {
Q_OBJECT Q_OBJECT
public: public:
VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) {} explicit VanillaCreationTask(BaseVersion::Ptr version) : m_version(std::move(version)) {}
VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loaderVersion);
std::unique_ptr<MinecraftInstance> createInstance() override; std::unique_ptr<MinecraftInstance> createInstance() override;

View file

@ -157,7 +157,8 @@ bool WorldList::resetIcon(int row)
return false; return false;
World& m = m_worlds[row]; World& m = m_worlds[row];
if (m.resetIcon()) { if (m.resetIcon()) {
emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); QModelIndex modelIndex = index(row, NameColumn);
emit dataChanged(modelIndex, modelIndex, { WorldList::IconFileRole });
return true; return true;
} }
return false; return false;
@ -426,7 +427,7 @@ void WorldList::loadWorldsAsync()
m_worlds[row].setSize(size); m_worlds[row].setSize(size);
// Notify views // Notify views
QModelIndex modelIndex = index(row); QModelIndex modelIndex = index(row, SizeColumn);
emit dataChanged(modelIndex, modelIndex, { SizeRole }); emit dataChanged(modelIndex, modelIndex, { SizeRole });
} }
}, },

View file

@ -41,6 +41,7 @@
#include <QDateTime> #include <QDateTime>
#include <QMap> #include <QMap>
#include <QNetworkReply>
#include <QVariantMap> #include <QVariantMap>
enum class Validity { None, Assumed, Certain }; enum class Validity { None, Assumed, Certain };
@ -118,5 +119,6 @@ struct AccountData {
// runtime only information (not saved with the account) // runtime only information (not saved with the account)
QString internalId; QString internalId;
QString errorString; QString errorString;
QNetworkReply::NetworkError networkError = QNetworkReply::NoError;
AccountState accountState = AccountState::Unchecked; AccountState accountState = AccountState::Unchecked;
}; };

View file

@ -648,9 +648,17 @@ void AccountList::tryNext()
while (m_refreshQueue.length()) { while (m_refreshQueue.length()) {
auto accountId = m_refreshQueue.front(); auto accountId = m_refreshQueue.front();
m_refreshQueue.pop_front(); m_refreshQueue.pop_front();
bool found = false;
for (int i = 0; i < count(); i++) { for (int i = 0; i < count(); i++) {
auto account = at(i); auto account = at(i);
if (account->internalId() == accountId) { if (account->internalId() == accountId) {
found = true;
if (!account->shouldRefresh()) {
// Account no longer needs refreshing, skip it.
qDebug() << "RefreshSchedule: Skipping account" << account->profileName() << "with internal ID"
<< accountId << "(no longer needs refresh)";
break;
}
m_currentTask = account->refresh(); m_currentTask = account->refresh();
if (m_currentTask) { if (m_currentTask) {
connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded);
@ -660,10 +668,13 @@ void AccountList::tryNext()
<< accountId; << accountId;
return; return;
} }
break;
} }
} }
if (!found) {
qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found.";
} }
}
// if we get here, no account needed refreshing. Schedule refresh in an hour. // if we get here, no account needed refreshing. Schedule refresh in an hour.
m_refreshTimer->start(1000 * 3600); m_refreshTimer->start(1000 * 3600);
} }

View file

@ -56,10 +56,11 @@ void LauncherLoginStep::onRequestDone(QByteArray* response)
qCDebug(authCredentials()) << *response; qCDebug(authCredentials()) << *response;
if (m_request->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_request->error(); qWarning() << "Reply error:" << m_request->error();
if (Net::isApplicationError(m_request->error())) { if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); tr("Failed to get Minecraft access token: %1").arg(m_request->errorString()));
} else { } else {
m_data->networkError = m_request->error();
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString()));
} }
return; return;

View file

@ -113,6 +113,12 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d
void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response) void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response)
{ {
if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) {
qWarning() << "Device authorization failed:" << m_request->error() << m_request->errorString();
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(m_request->errorString()));
return;
}
auto rsp = parseDeviceAuthorizationResponse(*response); auto rsp = parseDeviceAuthorizationResponse(*response);
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
qWarning() << "Device authorization failed:" << rsp.error; qWarning() << "Device authorization failed:" << rsp.error;
@ -120,12 +126,6 @@ void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response)
tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
return; return;
} }
if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) {
qWarning() << "Device authorization failed:" << *response;
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization"));
return;
}
if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) { if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) {
qWarning() << "Device authorization failed: required fields missing"; qWarning() << "Device authorization failed: required fields missing";
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing")); emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing"));

View file

@ -52,10 +52,11 @@ void MinecraftProfileStep::onRequestDone(QByteArray* response)
qWarning() << " Response:"; qWarning() << " Response:";
qWarning() << QString::fromUtf8(*response); qWarning() << QString::fromUtf8(*response);
if (Net::isApplicationError(m_request->error())) { if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
} else { } else {
m_data->networkError = m_request->error();
emit finished(AccountTaskState::STATE_OFFLINE, emit finished(AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
} }

View file

@ -2,7 +2,7 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonParseError> #include <QJsonParseError>
#include <QNetworkRequest> #include <utility>
#include "Application.h" #include "Application.h"
#include "Logging.h" #include "Logging.h"
@ -12,7 +12,7 @@
#include "net/Upload.h" #include "net/Upload.h"
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind) XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind)
: AuthStep(data), m_token(token), m_relyingParty(relyingParty), m_authorizationKind(authorizationKind) : AuthStep(data), m_token(token), m_relyingParty(std::move(relyingParty)), m_authorizationKind(std::move(authorizationKind))
{} {}
QString XboxAuthorizationStep::describe() QString XboxAuthorizationStep::describe()
@ -22,7 +22,7 @@ QString XboxAuthorizationStep::describe()
void XboxAuthorizationStep::perform() void XboxAuthorizationStep::perform()
{ {
QString xbox_auth_template = R"XXX( const QString xboxAuthTemplate = R"XXX(
{ {
"Properties": { "Properties": {
"SandboxId": "RETAIL", "SandboxId": "RETAIL",
@ -34,15 +34,13 @@ void XboxAuthorizationStep::perform()
"TokenType": "JWT" "TokenType": "JWT"
} }
)XXX"; )XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); const auto xboxAuthData = xboxAuthTemplate.arg(m_data->userToken.token, m_relyingParty);
// http://xboxlive.com // http://xboxlive.com
QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize"); const QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize");
auto headers = QList<Net::HeaderPair>{ auto headers = QList<Net::HeaderPair>{ { .headerName = "Content-Type", .headerValue = "application/json" },
{ "Content-Type", "application/json" }, { .headerName = "Accept", .headerValue = "application/json" },
{ "Accept", "application/json" }, { .headerName = "x-xbl-contract-version", .headerValue = "1" } };
{ "x-xbl-contract-version", "1" } auto [request, response] = Net::Upload::makeByteArray(url, xboxAuthData.toUtf8());
};
auto [request, response] = Net::Upload::makeByteArray(url, xbox_auth_data.toUtf8());
m_request = request; m_request = request;
m_request->addHeaderProxy(std::make_unique<Net::RawHeaderProxy>(headers)); m_request->addHeaderProxy(std::make_unique<Net::RawHeaderProxy>(headers));
m_request->enableAutoRetry(true); m_request->enableAutoRetry(true);
@ -62,15 +60,14 @@ void XboxAuthorizationStep::onRequestDone(QByteArray* response)
qCDebug(authCredentials()) << *response; qCDebug(authCredentials()) << *response;
if (m_request->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_request->error(); qWarning() << "Reply error:" << m_request->error();
if (Net::isApplicationError(m_request->error())) { if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) {
if (!processSTSError(*response)) { if (processSTSError(*response)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, return;
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_request->error())); }
} else {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString()));
}
} else { } else {
m_data->networkError = m_request->error();
emit finished(AccountTaskState::STATE_OFFLINE, emit finished(AccountTaskState::STATE_OFFLINE,
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_request->errorString()));
} }
@ -99,8 +96,8 @@ bool XboxAuthorizationStep::processSTSError(const QByteArray& response)
{ {
if (m_request->error() == QNetworkReply::AuthenticationRequiredError) { if (m_request->error() == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(response, &jsonError); const QJsonDocument doc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error) { if (jsonError.error != QJsonParseError::NoError) {
qWarning() << "Cannot parse error XSTS response as JSON:" << jsonError.errorString(); qWarning() << "Cannot parse error XSTS response as JSON:" << jsonError.errorString();
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString())); tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString()));

View file

@ -56,9 +56,10 @@ void XboxUserStep::onRequestDone(QByteArray* response)
{ {
if (m_request->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_request->error(); qWarning() << "Reply error:" << m_request->error();
if (Net::isApplicationError(m_request->error())) { if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication failed: %1").arg(m_request->errorString()));
} else { } else {
m_data->networkError = m_request->error();
emit finished(AccountTaskState::STATE_OFFLINE, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); emit finished(AccountTaskState::STATE_OFFLINE, tr("Xbox user authentication failed: %1").arg(m_request->errorString()));
} }
return; return;

View file

@ -25,26 +25,81 @@ EnsureAvailableMemory::EnsureAvailableMemory(LaunchTask* parent, MinecraftInstan
void EnsureAvailableMemory::executeTask() void EnsureAvailableMemory::executeTask()
{ {
const uint64_t available = HardwareInfo::availableRamMiB(); #ifdef Q_OS_MACOS
const uint64_t min = m_instance->settings()->get("MinMemAlloc").toUInt(); QString text;
const uint64_t max = m_instance->settings()->get("MaxMemAlloc").toUInt(); switch (MacOSHardwareInfo::memoryPressureLevel()) {
const uint64_t required = std::max(min, max); case MacOSHardwareInfo::MemoryPressureLevel::Normal:
emitSucceeded();
return;
case MacOSHardwareInfo::MemoryPressureLevel::Warning:
text =
tr("The system is under increased memory pressure.\n"
"This may lead to lag or slowdowns.\n"
"If possible, close other applications before continuing.\n\n"
"Launch anyway?");
break;
case MacOSHardwareInfo::MemoryPressureLevel::Critical:
text =
tr("Your system is under critical memory pressure.\n"
"This may lead to severe slowdowns, crashes or system instability.\n"
"It is recommended to close other applications or restart your system.\n\n"
"Launch anyway?");
break;
}
if (required > available) { bool shouldAbort = false;
if (m_instance->settings()->get("LowMemWarning").toBool()) {
auto* dialog = CustomMessageBox::selectable(nullptr, tr("High memory pressure"), text, QMessageBox::Icon::Warning,
QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
QMessageBox::StandardButton::No);
shouldAbort = dialog->exec() == QMessageBox::No;
dialog->deleteLater();
}
const auto message = tr("The system is under high memory pressure");
if (shouldAbort) {
emit logLine(message, MessageLevel::Fatal);
emitFailed(message);
return;
}
emit logLine(message, MessageLevel::Warning);
emitSucceeded();
#else
const uint64_t available = HardwareInfo::availableRamMiB();
if (available == 0) {
// could not read
emitSucceeded();
return;
}
const uint64_t settingMin = m_instance->settings()->get("MinMemAlloc").toUInt();
const uint64_t settingMax = m_instance->settings()->get("MaxMemAlloc").toUInt();
const uint64_t max = std::max(settingMin, settingMax);
if (static_cast<double>(max) * 0.9 > static_cast<double>(available)) {
bool shouldAbort = false;
if (m_instance->settings()->get("LowMemWarning").toBool()) {
auto* dialog = CustomMessageBox::selectable( auto* dialog = CustomMessageBox::selectable(
nullptr, tr("Not enough RAM"), nullptr, tr("Low free memory"),
tr("There is not enough RAM available to launch this instance with the current memory settings.\n\n" tr("There might not be enough free RAM to launch this instance with the current memory settings.\n\n"
"Required: %1 MiB\nAvailable: %2 MiB\n\n" "Maximum allocated: %1 MiB\nFree: %2 MiB (out of %3 MiB total)\n\n"
"Continue anyway? This may cause slowdowns in the game and your system.") "Launch anyway? This may cause slowdowns in the game and your system.")
.arg(required) .arg(max)
.arg(available), .arg(available)
.arg(HardwareInfo::totalRamMiB()),
QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
QMessageBox::StandardButton::No); QMessageBox::StandardButton::No);
const auto response = dialog->exec();
shouldAbort = dialog->exec() == QMessageBox::No;
dialog->deleteLater(); dialog->deleteLater();
}
const auto message = tr("Not enough RAM available to launch this instance"); const auto message = tr("Not enough RAM available to launch this instance");
if (response == QMessageBox::No) { if (shouldAbort) {
emit logLine(message, MessageLevel::Fatal); emit logLine(message, MessageLevel::Fatal);
emitFailed(message); emitFailed(message);
return; return;
@ -54,4 +109,5 @@ void EnsureAvailableMemory::executeTask()
} }
emitSucceeded(); emitSucceeded();
#endif
} }

View file

@ -27,16 +27,27 @@ void EnsureOfflineLibraries::executeTask()
{ {
const auto profile = m_instance->getPackProfile()->getProfile(); const auto profile = m_instance->getPackProfile()->getProfile();
QStringList allJars; QStringList allJars;
profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot()); profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot(),
false);
QStringList missing;
for (const auto& jar : allJars) { for (const auto& jar : allJars) {
if (!QFileInfo::exists(jar)) { if (!QFileInfo::exists(jar)) {
emit logLine(tr("This instance cannot be launched because some libraries are missing or have not been downloaded yet. Please " missing.append(jar);
"try again in online mode with a working Internet connection"),
MessageLevel::Fatal);
emitFailed("Required libraries are missing");
return;
} }
} }
if (missing.isEmpty()) {
emitSucceeded(); emitSucceeded();
return;
}
emit logLine("Missing libraries:", MessageLevel::Error);
for (const auto& jar : missing) {
emit logLine(" " + jar, MessageLevel::Error);
}
emit logLine(tr("\nThis instance cannot be launched because some libraries are missing or have not been downloaded yet. Please "
"try again in online mode with a working Internet connection"),
MessageLevel::Fatal);
emitFailed("Required libraries are missing");
} }

View file

@ -164,9 +164,9 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state)
switch (state) { switch (state) {
case LoggedProcess::FailedToStart: { case LoggedProcess::FailedToStart: {
//: Error message displayed if instace can't start //: Error message displayed if instace can't start
const char* reason = QT_TR_NOOP("Could not launch Minecraft!"); const char* reason = QT_TR_NOOP("Could not launch Minecraft: %1");
emit logLine(reason, MessageLevel::Fatal); emit logLine(QString(reason).arg(m_process.errorString()), MessageLevel::Fatal);
emitFailed(tr(reason)); emitFailed(tr(reason).arg(m_process.errorString()));
return; return;
} }
case LoggedProcess::Aborted: case LoggedProcess::Aborted:

View file

@ -61,13 +61,19 @@ void PrintInstanceInfo::executeTask()
auto instance = m_parent->instance(); auto instance = m_parent->instance();
QStringList log; QStringList log;
log << "";
log << "OS: " + QString("%1 | %2 | %3").arg(QSysInfo::prettyProductName(), QSysInfo::kernelType(), QSysInfo::kernelVersion()); log << "OS: " + QString("%1 | %2 | %3").arg(QSysInfo::prettyProductName(), QSysInfo::kernelType(), QSysInfo::kernelVersion());
#ifdef Q_OS_FREEBSD #ifdef Q_OS_FREEBSD
::runSysctlHwModel(log); ::runSysctlHwModel(log);
::runPciconf(log); ::runPciconf(log);
#else #else
log << "CPU: " + HardwareInfo::cpuInfo(); log << "CPU: " + HardwareInfo::cpuInfo();
#ifdef Q_OS_MACOS
log << "Memory pressure level: " + MacOSHardwareInfo::memoryPressureLevelName();
#else
log << QString("RAM: %1 MiB (available: %2 MiB)").arg(HardwareInfo::totalRamMiB()).arg(HardwareInfo::availableRamMiB()); log << QString("RAM: %1 MiB (available: %2 MiB)").arg(HardwareInfo::totalRamMiB()).arg(HardwareInfo::availableRamMiB());
#endif
#endif #endif
log.append(HardwareInfo::gpuInfo()); log.append(HardwareInfo::gpuInfo());
log << ""; log << "";

View file

@ -216,7 +216,7 @@ static std::pair<Version, Version> map(int format, const QMap<std::pair<int, int
int DataPack::compare(const Resource& other, SortType type) const int DataPack::compare(const Resource& other, SortType type) const
{ {
const auto& cast_other = static_cast<const DataPack&>(other); const auto& cast_other = static_cast<const DataPack&>(other);
if (type == SortType::PACK_FORMAT) { if (type == SortType::PackFormat) {
auto this_ver = packFormat(); auto this_ver = packFormat();
auto other_ver = cast_other.packFormat(); auto other_ver = cast_other.packFormat();

View file

@ -40,25 +40,26 @@
#include <QIcon> #include <QIcon>
#include <QStyle> #include <QStyle>
#include "Version.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h"
DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent)
: ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) : ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent)
{ {
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); m_columnNames = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Size", "File Name" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); m_columnNamesTranslated =
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size"), tr("File Name") });
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::PackFormat,
QHeaderView::Interactive }; SortType::Date, SortType::Size, SortType::Filename };
m_columnsHideable = { false, true, false, true, true }; m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true, true };
} }
QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
{ {
if (!validateIndex(index)) if (!validateIndex(index)) {
return {}; return {};
}
int row = index.row(); int row = index.row();
int column = index.column(); int column = index.column();
@ -67,11 +68,13 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
case Qt::BackgroundRole: case Qt::BackgroundRole:
return rowBackground(row); return rowBackground(row);
case Qt::DisplayRole: case Qt::DisplayRole:
switch (column) { if (column == PackFormatColumn) {
case PackFormatColumn: {
const auto& resource = at(row); const auto& resource = at(row);
return resource.packFormatStr(); return resource.packFormatStr();
} }
if (column == SizeColumn) {
const auto& resource = at(row);
return resource.sizeStr();
} }
break; break;
case Qt::DecorationRole: { case Qt::DecorationRole: {
@ -92,6 +95,8 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
return QSize(32, 32); return QSize(32, 32);
} }
break; break;
default:
break;
} }
// map the columns to the base equivilents // map the columns to the base equivilents
@ -109,7 +114,14 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
case ProviderColumn: case ProviderColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn);
break; break;
// FIXME: there is no size column due to an oversight case FileNameColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn);
break;
case SizeColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn);
break;
default:
break;
} }
if (mappedIndex.isValid()) { if (mappedIndex.isValid()) {
@ -129,6 +141,8 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
case PackFormatColumn: case PackFormatColumn:
case DateColumn: case DateColumn:
case ImageColumn: case ImageColumn:
case SizeColumn:
case FileNameColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return {}; return {};
@ -145,6 +159,10 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); return tr("The data pack format ID, as well as the Minecraft versions it was designed for.");
case DateColumn: case DateColumn:
return tr("The date and time this data pack was last changed (or added)."); return tr("The date and time this data pack was last changed (or added).");
case SizeColumn:
return tr("The size of the data pack.");
case FileNameColumn:
return tr("The file name of the data pack.");
default: default:
return {}; return {};
} }
@ -160,7 +178,7 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
int DataPackFolderModel::columnCount(const QModelIndex& parent) const int DataPackFolderModel::columnCount(const QModelIndex& parent) const
{ {
return parent.isValid() ? 0 : NUM_COLUMNS; return parent.isValid() ? 0 : NumColumns;
} }
Resource* DataPackFolderModel::createResource(const QFileInfo& file) Resource* DataPackFolderModel::createResource(const QFileInfo& file)
@ -170,5 +188,5 @@ Resource* DataPackFolderModel::createResource(const QFileInfo& file)
Task* DataPackFolderModel::createParseTask(Resource& resource) Task* DataPackFolderModel::createParseTask(Resource& resource)
{ {
return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast<DataPack*>(&resource)); return new LocalDataPackParseTask(m_nextResolutionTicket, static_cast<DataPack*>(&resource));
} }

View file

@ -39,16 +39,24 @@
#include "ResourceFolderModel.h" #include "ResourceFolderModel.h"
#include "DataPack.h" #include "DataPack.h"
#include "ResourcePack.h"
class DataPackFolderModel : public ResourceFolderModel { class DataPackFolderModel : public ResourceFolderModel {
Q_OBJECT Q_OBJECT
public: public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; enum Columns : std::uint8_t {
ActiveColumn = 0,
ImageColumn,
NameColumn,
PackFormatColumn,
DateColumn,
SizeColumn,
FileNameColumn,
NumColumns
};
explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr);
virtual QString id() const override { return "datapacks"; } QString id() const override { return "datapacks"; }
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
@ -56,7 +64,7 @@ class DataPackFolderModel : public ResourceFolderModel {
int columnCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override;
[[nodiscard]] Resource* createResource(const QFileInfo& file) override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override;
[[nodiscard]] Task* createParseTask(Resource&) override; [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override;
RESOURCE_HELPERS(DataPack) RESOURCE_HELPERS(DataPack)
}; };

View file

@ -40,6 +40,7 @@
#include <QDir> #include <QDir>
#include <QRegularExpression> #include <QRegularExpression>
#include <QString> #include <QString>
#include <algorithm>
#include "MTPixmapCache.h" #include "MTPixmapCache.h"
#include "MetadataHandler.h" #include "MetadataHandler.h"
@ -49,6 +50,34 @@
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
namespace {
int compareVersionLists(const QStringList& leftVersions, const QStringList& rightVersions)
{
const qsizetype commonSize = std::min(leftVersions.size(), rightVersions.size());
for (qsizetype i = 0; i < commonSize; i++) {
const auto leftVersion = Version(leftVersions.at(i).trimmed());
const auto rightVersion = Version(rightVersions.at(i).trimmed());
if (leftVersion > rightVersion)
return 1;
if (leftVersion < rightVersion)
return -1;
}
if (leftVersions.size() > rightVersions.size())
return 1;
if (leftVersions.size() < rightVersions.size())
return -1;
return 0;
}
} // namespace
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{ {
m_enabled = (file.suffix() != "disabled"); m_enabled = (file.suffix() != "disabled");
@ -61,18 +90,18 @@ void Mod::setDetails(const ModDetails& details)
int Mod::compare(const Resource& other, SortType type) const int Mod::compare(const Resource& other, SortType type) const
{ {
auto cast_other = dynamic_cast<Mod const*>(&other); auto cast_other = dynamic_cast<const Mod*>(&other);
if (!cast_other) if (!cast_other)
return Resource::compare(other, type); return Resource::compare(other, type);
switch (type) { switch (type) {
default: default:
case SortType::ENABLED: case SortType::Enabled:
case SortType::NAME: case SortType::Name:
case SortType::DATE: case SortType::Date:
case SortType::SIZE: case SortType::Size:
return Resource::compare(other, type); return Resource::compare(other, type);
case SortType::VERSION: { case SortType::Version: {
auto this_ver = Version(version()); auto this_ver = Version(version());
auto other_ver = Version(cast_other->version()); auto other_ver = Version(cast_other->version());
if (this_ver > other_ver) if (this_ver > other_ver)
@ -81,38 +110,38 @@ int Mod::compare(const Resource& other, SortType type) const
return -1; return -1;
break; break;
} }
case SortType::SIDE: { case SortType::Side: {
auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return compare_result; return compare_result;
break; break;
} }
case SortType::MC_VERSIONS: { case SortType::McVersions: {
auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); auto compare_result = compareVersionLists(mcVersions(), cast_other->mcVersions());
if (compare_result != 0) if (compare_result != 0)
return compare_result; return compare_result;
break; break;
} }
case SortType::LOADERS: { case SortType::Loaders: {
auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return compare_result; return compare_result;
break; break;
} }
case SortType::RELEASE_TYPE: { case SortType::ReleaseType: {
auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive);
if (compare_result != 0) if (compare_result != 0)
return compare_result; return compare_result;
break; break;
} }
case SortType::REQUIRED_BY: { case SortType::RequiredBy: {
if (requiredByCount() > cast_other->requiredByCount()) if (requiredByCount() > cast_other->requiredByCount())
return 1; return 1;
if (requiredByCount() < cast_other->requiredByCount()) if (requiredByCount() < cast_other->requiredByCount())
return -1; return -1;
break; break;
} }
case SortType::REQUIRES: { case SortType::Requires: {
if (requiresCount() > cast_other->requiresCount()) if (requiresCount() > cast_other->requiresCount())
return 1; return 1;
if (requiresCount() < cast_other->requiresCount()) if (requiresCount() < cast_other->requiresCount())
@ -197,14 +226,19 @@ auto Mod::side() const -> QString
return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide);
} }
auto Mod::mcVersions() const -> QString auto Mod::mcVersions() const -> QStringList
{ {
if (metadata()) if (metadata())
return metadata()->mcVersions.join(", "); return metadata()->mcVersions;
return {}; return {};
} }
auto Mod::mcVersionsString() const -> QString
{
return mcVersions().join(", ");
}
auto Mod::releaseType() const -> QString auto Mod::releaseType() const -> QString
{ {
if (metadata()) if (metadata())

View file

@ -68,7 +68,8 @@ class Mod : public Resource {
auto issueTracker() const -> QString; auto issueTracker() const -> QString;
auto side() const -> QString; auto side() const -> QString;
auto loaders() const -> QString; auto loaders() const -> QString;
auto mcVersions() const -> QString; auto mcVersions() const -> QStringList;
auto mcVersionsString() const -> QString;
auto releaseType() const -> QString; auto releaseType() const -> QString;
QStringList dependencies() const; QStringList dependencies() const;

View file

@ -51,38 +51,39 @@
#include <QUrl> #include <QUrl>
#include <QUuid> #include <QUuid>
#include <algorithm> #include <algorithm>
#include <set>
#include "minecraft/Component.h"
#include "minecraft/mod/Resource.h" #include "minecraft/mod/Resource.h"
#include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/ResourceFolderModel.h"
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent)
: ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) : ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent)
{ {
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", m_columnNames = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders",
"Minecraft Versions", "Release Type", "Requires", "Required By" }); "Minecraft Versions", "Release Type", "Requires", "Required By", "File Name" });
m_column_names_translated = m_columnNamesTranslated =
QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"), QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"),
tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires"), tr("Required By") }); tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires"), tr("Required By"), tr("File Name") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::Version, SortType::Date,
SortType::PROVIDER, SortType::SIZE, SortType::SIDE, SortType::LOADERS, SortType::MC_VERSIONS, SortType::Provider, SortType::Size, SortType::Side, SortType::Loaders, SortType::McVersions,
SortType::RELEASE_TYPE, SortType::REQUIRES, SortType::REQUIRED_BY }; SortType::ReleaseType, SortType::Requires, SortType::RequiredBy, SortType::Filename };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive,
QHeaderView::Interactive }; QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true, true, true }; m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true, true, true, true };
connect(this, &ModFolderModel::parseFinished, this, &ModFolderModel::onParseFinished); connect(this, &ModFolderModel::parseFinished, this, &ModFolderModel::onParseFinished);
} }
QVariant ModFolderModel::data(const QModelIndex& index, int role) const QVariant ModFolderModel::data(const QModelIndex& index, int role) const
{ {
if (!validateIndex(index)) if (!validateIndex(index)) {
return {}; return {};
}
int row = index.row(); int row = index.row();
int column = index.column(); int column = index.column();
@ -109,7 +110,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
return at(row).loaders(); return at(row).loaders();
} }
case McVersionsColumn: { case McVersionsColumn: {
return at(row).mcVersions(); return at(row).mcVersionsString();
} }
case ReleaseTypeColumn: { case ReleaseTypeColumn: {
return at(row).releaseType(); return at(row).releaseType();
@ -120,6 +121,8 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
case RequiresColumn: { case RequiresColumn: {
return at(row).requiresCount(); return at(row).requiresCount();
} }
default:
break;
} }
break; break;
case Qt::DecorationRole: { case Qt::DecorationRole: {
@ -155,6 +158,11 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
case SizeColumn: case SizeColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn);
break; break;
case FileNameColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn);
break;
default:
break;
} }
if (mappedIndex.isValid()) { if (mappedIndex.isValid()) {
@ -182,6 +190,7 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
case SizeColumn: case SizeColumn:
case RequiredByColumn: case RequiredByColumn:
case RequiresColumn: case RequiresColumn:
case FileNameColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return QVariant(); return QVariant();
@ -213,6 +222,8 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
return tr("For each mod, the number of other mods which depend on it."); return tr("For each mod, the number of other mods which depend on it.");
case RequiresColumn: case RequiresColumn:
return tr("For each mod, the number of other mods it depends on."); return tr("For each mod, the number of other mods it depends on.");
case FileNameColumn:
return tr("The file name of the mod.");
default: default:
return QVariant(); return QVariant();
} }
@ -223,12 +234,12 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
int ModFolderModel::columnCount(const QModelIndex& parent) const int ModFolderModel::columnCount(const QModelIndex& parent) const
{ {
return parent.isValid() ? 0 : NUM_COLUMNS; return parent.isValid() ? 0 : NumColumns;
} }
Task* ModFolderModel::createParseTask(Resource& resource) Task* ModFolderModel::createParseTask(Resource& resource)
{ {
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); return new LocalModParseTask(m_nextResolutionTicket, resource.type(), resource.fileinfo());
} }
bool ModFolderModel::isValid() bool ModFolderModel::isValid()
@ -236,35 +247,37 @@ bool ModFolderModel::isValid()
return m_dir.exists() && m_dir.isReadable(); return m_dir.exists() && m_dir.isReadable();
} }
void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) void ModFolderModel::onParseSucceeded(int ticket, const QString& resourceId)
{ {
auto iter = m_active_parse_tasks.constFind(ticket); auto iter = m_activeParseTasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd()) if (iter == m_activeParseTasks.constEnd()) {
return; return;
}
int row = m_resources_index[mod_id]; int row = m_resourcesIndex[resourceId];
auto parse_task = *iter; const auto& parseTask = *iter;
auto cast_task = static_cast<LocalModParseTask*>(parse_task.get()); auto* castTask = static_cast<LocalModParseTask*>(parseTask.get());
Q_ASSERT(cast_task->token() == ticket); Q_ASSERT(castTask->token() == ticket);
auto resource = find(mod_id); auto resource = find(resourceId);
auto result = cast_task->result(); auto result = castTask->result();
if (result && resource) { if (result && resource) {
auto* mod = static_cast<Mod*>(resource.get()); auto* mod = static_cast<Mod*>(resource.get());
mod->finishResolvingWithDetails(std::move(result->details)); mod->finishResolvingWithDetails(std::move(result->details));
} }
emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn));
} }
Mod* findById(QSet<Mod*> mods, QString modId) namespace {
Mod* findById(QSet<Mod*> mods, const QString& resourceId)
{ {
auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; }); auto found = std::ranges::find_if(mods, [resourceId](Mod* m) { return m->mod_id() == resourceId; });
return found != mods.end() ? *found : nullptr; return found != mods.end() ? *found : nullptr;
} }
} // namespace
void ModFolderModel::onParseFinished() void ModFolderModel::onParseFinished()
{ {
@ -277,25 +290,25 @@ void ModFolderModel::onParseFinished()
m_requires.clear(); m_requires.clear();
m_requiredBy.clear(); m_requiredBy.clear();
auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* { auto findByProjectID = [mods](const QVariant& modId, ModPlatform::ResourceProvider provider) -> Mod* {
auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) { auto found = std::ranges::find_if(mods, [modId, provider](Mod* m) {
return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId; return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId;
}); });
return found != mods.end() ? *found : nullptr; return found != mods.end() ? *found : nullptr;
}; };
for (auto mod : mods) { for (auto* mod : mods) {
auto id = mod->mod_id(); auto id = mod->mod_id();
for (auto dep : mod->dependencies()) { for (const auto& dep : mod->dependencies()) {
auto d = findById(mods, dep); auto* d = findById(mods, dep);
if (d) { if (d) {
m_requires[id] << d; m_requires[id] << d;
m_requiredBy[d->mod_id()] << mod; m_requiredBy[d->mod_id()] << mod;
} }
} }
if (mod->metadata()) { if (mod->metadata()) {
for (auto dep : mod->metadata()->dependencies) { for (const auto& dep : mod->metadata()->dependencies) {
if (dep.type == ModPlatform::DependencyType::REQUIRED) { if (dep.type == ModPlatform::DependencyType::REQUIRED) {
auto d = findByProjectID(dep.addonId, mod->metadata()->provider); auto* d = findByProjectID(dep.addonId, mod->metadata()->provider);
if (d) { if (d) {
m_requires[id] << d; m_requires[id] << d;
m_requiredBy[d->mod_id()] << mod; m_requiredBy[d->mod_id()] << mod;
@ -304,30 +317,31 @@ void ModFolderModel::onParseFinished()
} }
} }
} }
for (auto mod : mods) { for (auto* mod : mods) {
auto id = mod->mod_id(); auto id = mod->mod_id();
if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) {
mod->setRequiredByCount(m_requiredBy[id].count()); mod->setRequiredByCount(m_requiredBy[id].count());
mod->setRequiresCount(m_requires[id].count()); mod->setRequiresCount(m_requires[id].count());
int row = m_resources_index[mod->internal_id()]; int row = m_resourcesIndex[mod->internalId()];
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
} }
} }
} }
QSet<Mod*> collectMods(QSet<Mod*> mods, QHash<QString, QSet<Mod*>> relation, std::set<QString>& seen, bool shouldBeEnabled) namespace {
QSet<Mod*> collectMods(const QSet<Mod*>& mods, QHash<QString, QSet<Mod*>> relation, std::set<QString>& seen, bool shouldBeEnabled)
{ {
QSet<Mod*> affectedList = {}; QSet<Mod*> affectedList = {};
QSet<Mod*> needToCheck = {}; QSet<Mod*> needToCheck = {};
for (auto mod : mods) { for (auto* mod : mods) {
auto id = mod->mod_id(); auto id = mod->mod_id();
if (seen.count(id) == 0) { if (!seen.contains(id)) {
seen.insert(id); seen.insert(id);
for (auto affected : relation[id]) { for (auto* affected : relation[id]) {
auto affectedId = affected->mod_id(); auto affectedId = affected->mod_id();
if (findById(mods, affectedId) == nullptr && seen.count(affectedId) == 0) { if (findById(mods, affectedId) == nullptr && !seen.contains(affectedId)) {
seen.insert(affectedId);
if (shouldBeEnabled != affected->enabled()) { if (shouldBeEnabled != affected->enabled()) {
affectedList << affected; affectedList << affected;
} }
@ -342,11 +356,13 @@ QSet<Mod*> collectMods(QSet<Mod*> mods, QHash<QString, QSet<Mod*>> relation, std
} }
return affectedList; return affectedList;
} }
} // namespace
QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action)
{ {
if (indexes.isEmpty()) if (indexes.isEmpty()) {
return {}; return {};
}
QModelIndexList affectedList = {}; QModelIndexList affectedList = {};
auto affectedModsList = selectedMods(indexes); auto affectedModsList = selectedMods(indexes);
@ -366,9 +382,9 @@ QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes,
return {}; // this function should not be called with TOGGLE return {}; // this function should not be called with TOGGLE
} }
} }
for (auto affected : affectedMods) { for (auto* affected : affectedMods) {
auto affectedId = affected->mod_id(); auto affectedId = affected->mod_id();
auto row = m_resources_index[affected->internal_id()]; auto row = m_resourcesIndex[affected->internalId()];
affectedList << index(row, 0); affectedList << index(row, 0);
} }
return affectedList; return affectedList;
@ -376,8 +392,9 @@ QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes,
bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action)
{ {
if (indexes.isEmpty()) if (indexes.isEmpty()) {
return {}; return {};
}
auto indexedModsList = selectedMods(indexes); auto indexedModsList = selectedMods(indexes);
auto indexedMods = QSet(indexedModsList.begin(), indexedModsList.end()); auto indexedMods = QSet(indexedModsList.begin(), indexedModsList.end());
@ -396,7 +413,7 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc
break; break;
} }
case EnableAction::TOGGLE: { case EnableAction::TOGGLE: {
for (auto mod : indexedMods) { for (auto* mod : indexedMods) {
if (mod->enabled()) { if (mod->enabled()) {
toDisable << mod; toDisable << mod;
} else { } else {
@ -411,10 +428,10 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc
auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen, false); auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen, false);
toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); });
auto toList = [this](QSet<Mod*> mods) { auto toList = [this](const QSet<Mod*>& mods) {
QModelIndexList list; QModelIndexList list;
for (auto mod : mods) { for (auto* mod : mods) {
auto row = m_resources_index[mod->internal_id()]; auto row = m_resourcesIndex[mod->internalId()];
list << index(row, 0); list << index(row, 0);
} }
return list; return list;
@ -447,7 +464,7 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc
yesButton = tr("Disable Required"); yesButton = tr("Disable Required");
} }
auto box = CustomMessageBox::selectable(nullptr, title, message, QMessageBox::Warning, auto* box = CustomMessageBox::selectable(nullptr, title, message, QMessageBox::Warning,
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No);
box->button(QMessageBox::No)->setText(noButton); box->button(QMessageBox::No)->setText(noButton);
box->button(QMessageBox::Yes)->setText(yesButton); box->button(QMessageBox::Yes)->setText(yesButton);
@ -466,21 +483,23 @@ bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAc
return disableStatus && enableStatus; return disableStatus && enableStatus;
} }
QStringList reqToList(QSet<Mod*> l) namespace {
QStringList reqToList(const QSet<Mod*>& l)
{ {
QStringList req; QStringList req;
for (auto m : l) { for (auto* m : l) {
req << m->name(); req << m->name();
} }
return req; return req;
} }
} // namespace
QStringList ModFolderModel::requiresList(QString id) QStringList ModFolderModel::requiresList(const QString& id)
{ {
return reqToList(m_requires[id]); return reqToList(m_requires[id]);
} }
QStringList ModFolderModel::requiredByList(QString id) QStringList ModFolderModel::requiredByList(const QString& id)
{ {
return reqToList(m_requiredBy[id]); return reqToList(m_requiredBy[id]);
} }
@ -489,7 +508,7 @@ bool ModFolderModel::deleteResources(const QModelIndexList& indexes)
{ {
auto deleteInvalid = [](QSet<Mod*>& mods) { auto deleteInvalid = [](QSet<Mod*>& mods) {
for (auto it = mods.begin(); it != mods.end();) { for (auto it = mods.begin(); it != mods.end();) {
auto mod = *it; auto* mod = *it;
// the QFileInfo::exists is used instead of mod->fileinfo().exists // the QFileInfo::exists is used instead of mod->fileinfo().exists
// because the later somehow caches that the file exists // because the later somehow caches that the file exists
if (!mod || !QFileInfo::exists(mod->fileinfo().absoluteFilePath())) { if (!mod || !QFileInfo::exists(mod->fileinfo().absoluteFilePath())) {
@ -500,14 +519,14 @@ bool ModFolderModel::deleteResources(const QModelIndexList& indexes)
} }
}; };
auto rsp = ResourceFolderModel::deleteResources(indexes); auto rsp = ResourceFolderModel::deleteResources(indexes);
for (auto mod : allMods()) { for (auto* mod : allMods()) {
auto id = mod->mod_id(); auto id = mod->mod_id();
deleteInvalid(m_requiredBy[id]); deleteInvalid(m_requiredBy[id]);
deleteInvalid(m_requires[id]); deleteInvalid(m_requires[id]);
if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) {
mod->setRequiredByCount(m_requiredBy[id].count()); mod->setRequiredByCount(m_requiredBy[id].count());
mod->setRequiresCount(m_requires[id].count()); mod->setRequiresCount(m_requires[id].count());
int row = m_resources_index[mod->internal_id()]; int row = m_resourcesIndex[mod->internalId()];
emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn));
} }
} }

View file

@ -46,7 +46,6 @@
#include "Mod.h" #include "Mod.h"
#include "ResourceFolderModel.h" #include "ResourceFolderModel.h"
#include "minecraft/Component.h"
#include "minecraft/mod/Resource.h" #include "minecraft/mod/Resource.h"
class BaseInstance; class BaseInstance;
@ -59,7 +58,7 @@ class QFileSystemWatcher;
class ModFolderModel : public ResourceFolderModel { class ModFolderModel : public ResourceFolderModel {
Q_OBJECT Q_OBJECT
public: public:
enum Columns { enum Columns : std::uint8_t {
ActiveColumn = 0, ActiveColumn = 0,
ImageColumn, ImageColumn,
NameColumn, NameColumn,
@ -73,11 +72,12 @@ class ModFolderModel : public ResourceFolderModel {
ReleaseTypeColumn, ReleaseTypeColumn,
RequiresColumn, RequiresColumn,
RequiredByColumn, RequiredByColumn,
NUM_COLUMNS FileNameColumn,
NumColumns
}; };
ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); ModFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr);
virtual QString id() const override { return "mods"; } QString id() const override { return "mods"; }
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
@ -85,7 +85,7 @@ class ModFolderModel : public ResourceFolderModel {
int columnCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override;
[[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); } [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); }
[[nodiscard]] Task* createParseTask(Resource&) override; [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override;
bool isValid(); bool isValid();
@ -97,11 +97,11 @@ class ModFolderModel : public ResourceFolderModel {
RESOURCE_HELPERS(Mod) RESOURCE_HELPERS(Mod)
public: public:
QStringList requiresList(QString id); QStringList requiresList(const QString& id);
QStringList requiredByList(QString id); QStringList requiredByList(const QString& id);
private slots: private slots:
void onParseSucceeded(int ticket, QString resource_id) override; void onParseSucceeded(int ticket, const QString& resourceId) override;
void onParseFinished(); void onParseFinished();
private: private:

View file

@ -4,71 +4,75 @@
#include <QFileInfo> #include <QFileInfo>
#include <QRegularExpression> #include <QRegularExpression>
#include <tuple> #include <tuple>
#include <utility>
#include "FileSystem.h" #include "FileSystem.h"
#include "StringUtils.h" #include "StringUtils.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
Resource::Resource(QObject* parent) : QObject(parent) {} Resource::Resource(QObject* parent) : QObject(parent), m_size_info(0) {}
Resource::Resource(QFileInfo file_info) : QObject() Resource::Resource(QFileInfo fileInfo) : m_size_info(0)
{ {
setFile(file_info); setFile(fileInfo);
} }
void Resource::setFile(QFileInfo file_info) void Resource::setFile(QFileInfo fileInfo)
{ {
m_file_info = file_info; m_file_info = std::move(fileInfo);
parseFile(); parseFile();
} }
static std::tuple<QString, qint64> calculateFileSize(const QFileInfo& file) namespace {
std::tuple<QString, qint64> calculateFileSize(const QFileInfo& file)
{ {
if (file.isDir()) { if (file.isDir()) {
auto dir = QDir(file.absoluteFilePath()); auto dir = QDir(file.absoluteFilePath());
dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
auto count = dir.count(); auto count = dir.count();
auto str = QObject::tr("item"); auto str = QObject::tr("item");
if (count != 1) if (count != 1) {
str = QObject::tr("items"); str = QObject::tr("items");
}
return { QString("%1 %2").arg(QString::number(count), str), count }; return { QString("%1 %2").arg(QString::number(count), str), count };
} }
return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; return { StringUtils::humanReadableFileSize(file.size(), true), file.size() };
} }
} // namespace
void Resource::parseFile() void Resource::parseFile()
{ {
QString file_name{ m_file_info.fileName() }; QString fileName{ m_file_info.fileName() };
m_type = ResourceType::UNKNOWN; m_type = ResourceType::UNKNOWN;
m_internal_id = file_name; m_internal_id = fileName;
std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info);
if (m_file_info.isDir()) { if (m_file_info.isDir()) {
m_type = ResourceType::FOLDER; m_type = ResourceType::FOLDER;
m_name = file_name; m_name = fileName;
} else if (m_file_info.isFile()) { } else if (m_file_info.isFile()) {
if (file_name.endsWith(".disabled")) { if (fileName.endsWith(".disabled")) {
file_name.chop(9); fileName.chop(9);
m_enabled = false; m_enabled = false;
} }
if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
m_type = ResourceType::ZIPFILE; m_type = ResourceType::ZIPFILE;
file_name.chop(4); fileName.chop(4);
} else if (file_name.endsWith(".nilmod")) { } else if (fileName.endsWith(".nilmod")) {
m_type = ResourceType::ZIPFILE; m_type = ResourceType::ZIPFILE;
file_name.chop(7); fileName.chop(7);
} else if (file_name.endsWith(".litemod")) { } else if (fileName.endsWith(".litemod")) {
m_type = ResourceType::LITEMOD; m_type = ResourceType::LITEMOD;
file_name.chop(8); fileName.chop(8);
} else { } else {
m_type = ResourceType::SINGLEFILE; m_type = ResourceType::SINGLEFILE;
} }
m_name = file_name; m_name = fileName;
} }
m_changed_date_time = m_file_info.lastModified(); m_changed_date_time = m_file_info.lastModified();
@ -76,39 +80,45 @@ void Resource::parseFile()
auto Resource::name() const -> QString auto Resource::name() const -> QString
{ {
if (metadata()) if (metadata()) {
return metadata()->name; return metadata()->name;
}
return m_name; return m_name;
} }
static void removeThePrefix(QString& string) namespace {
void removeThePrefix(QString& string)
{ {
static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
string.remove(s_regex); string.remove(s_regex);
string = string.trimmed(); string = string.trimmed();
} }
} // namespace
auto Resource::provider() const -> QString auto Resource::provider() const -> QString
{ {
if (metadata()) if (metadata()) {
return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); return ModPlatform::ProviderCapabilities::readableName(metadata()->provider);
}
return tr("Unknown"); return tr("Unknown");
} }
auto Resource::homepage() const -> QString auto Resource::homepage() const -> QString
{ {
if (metadata()) if (metadata()) {
return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id);
}
return {}; return {};
} }
void Resource::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata) void Resource::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
{ {
if (status() == ResourceStatus::NO_METADATA) if (status() == ResourceStatus::NoMetadata) {
setStatus(ResourceStatus::INSTALLED); setStatus(ResourceStatus::Installed);
}
m_metadata = metadata; m_metadata = metadata;
} }
@ -133,12 +143,12 @@ void Resource::updateIssues(const BaseInstance* inst)
return; return;
} }
auto mcInst = dynamic_cast<const MinecraftInstance*>(inst); const auto* mcInst = dynamic_cast<const MinecraftInstance*>(inst);
if (mcInst == nullptr) { if (mcInst == nullptr) {
return; return;
} }
auto profile = mcInst->getPackProfile(); auto* profile = mcInst->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft"); QString mcVersion = profile->getComponentVersion("net.minecraft");
if (!m_metadata->mcVersions.empty() && !m_metadata->mcVersions.contains(mcVersion)) { if (!m_metadata->mcVersions.empty() && !m_metadata->mcVersions.contains(mcVersion)) {
@ -151,46 +161,59 @@ int Resource::compare(const Resource& other, SortType type) const
{ {
switch (type) { switch (type) {
default: default:
case SortType::ENABLED: case SortType::Enabled:
if (enabled() && !other.enabled()) if (enabled() && !other.enabled()) {
return 1; return 1;
if (!enabled() && other.enabled()) }
if (!enabled() && other.enabled()) {
return -1; return -1;
}
break; break;
case SortType::NAME: { case SortType::Name: {
QString this_name{ name() }; QString thisName{ name() };
QString other_name{ other.name() }; QString otherName{ other.name() };
// TODO do we need this? it could result in 0 being returned // TODO do we need this? it could result in 0 being returned
removeThePrefix(this_name); removeThePrefix(thisName);
removeThePrefix(other_name); removeThePrefix(otherName);
return QString::compare(this_name, other_name, Qt::CaseInsensitive); return QString::compare(thisName, otherName, Qt::CaseInsensitive);
} }
case SortType::DATE: case SortType::Date:
if (dateTimeChanged() > other.dateTimeChanged()) if (dateTimeChanged() > other.dateTimeChanged()) {
return 1; return 1;
if (dateTimeChanged() < other.dateTimeChanged()) }
if (dateTimeChanged() < other.dateTimeChanged()) {
return -1; return -1;
}
break; break;
case SortType::SIZE: { case SortType::Filename:
return fileinfo().fileName().localeAwareCompare(other.fileinfo().fileName());
case SortType::Size: {
if (this->type() != other.type()) { if (this->type() != other.type()) {
if (this->type() == ResourceType::FOLDER) if (this->type() == ResourceType::FOLDER) {
return -1; return -1;
if (other.type() == ResourceType::FOLDER) }
if (other.type() == ResourceType::FOLDER) {
return 1; return 1;
} }
}
if (sizeInfo() > other.sizeInfo()) if (sizeInfo() > other.sizeInfo()) {
return 1; return 1;
if (sizeInfo() < other.sizeInfo()) }
if (sizeInfo() < other.sizeInfo()) {
return -1; return -1;
}
break; break;
} }
case SortType::PROVIDER: {
auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); case SortType::Provider: {
if (compare_result != 0) auto compareResult = QString::compare(provider(), other.provider(), Qt::CaseInsensitive);
return compare_result; if (compareResult != 0) {
return compareResult;
}
break; break;
} }
} }
@ -200,13 +223,20 @@ int Resource::compare(const Resource& other, SortType type) const
bool Resource::applyFilter(QRegularExpression filter) const bool Resource::applyFilter(QRegularExpression filter) const
{ {
return filter.match(name()).hasMatch(); if (filter.match(name()).hasMatch()) {
return true;
}
if (filter.match(fileinfo().fileName()).hasMatch()) {
return true;
}
return false;
} }
bool Resource::enable(EnableAction action) bool Resource::enable(EnableAction action)
{ {
if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) {
return false; return false;
}
QString path = m_file_info.absoluteFilePath(); QString path = m_file_info.absoluteFilePath();
QFile file(path); QFile file(path);
@ -225,14 +255,16 @@ bool Resource::enable(EnableAction action)
break; break;
} }
if (m_enabled == enable) if (m_enabled == enable) {
return false; return false;
}
if (enable) { if (enable) {
// m_enabled is false, but there's no '.disabled' suffix. // m_enabled is false, but there's no '.disabled' suffix.
// TODO: Report error? // TODO: Report error?
if (!path.endsWith(".disabled")) if (!path.endsWith(".disabled")) {
return false; return false;
}
path.chop(9); path.chop(9);
} else { } else {
path += ".disabled"; path += ".disabled";
@ -240,8 +272,9 @@ bool Resource::enable(EnableAction action)
path = FS::getUniqueResourceName(path); path = FS::getUniqueResourceName(path);
} }
} }
if (!file.rename(path)) if (!file.rename(path)) {
return false; return false;
}
setFile(QFileInfo(path)); setFile(QFileInfo(path));
@ -249,33 +282,34 @@ bool Resource::enable(EnableAction action)
return true; return true;
} }
auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool auto Resource::destroy(const QDir& indexDir, bool preserveMetadata, bool attemptTrash) -> bool
{ {
m_type = ResourceType::UNKNOWN; m_type = ResourceType::UNKNOWN;
if (!preserve_metadata) { if (!preserveMetadata) {
qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
destroyMetadata(index_dir); destroyMetadata(indexDir);
} }
return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath());
} }
auto Resource::destroyMetadata(const QDir& index_dir) -> void auto Resource::destroyMetadata(const QDir& indexDir) -> void
{ {
if (metadata()) { if (metadata()) {
Metadata::remove(index_dir, metadata()->slug); Metadata::remove(indexDir, metadata()->slug);
} else { } else {
auto n = name(); auto n = name();
Metadata::remove(index_dir, n); Metadata::remove(indexDir, n);
} }
m_metadata = nullptr; m_metadata = nullptr;
} }
bool Resource::isSymLinkUnder(const QString& instPath) const bool Resource::isSymLinkUnder(const QString& instPath) const
{ {
if (isSymLink()) if (isSymLink()) {
return true; return true;
}
auto instDir = QDir(instPath); auto instDir = QDir(instPath);
@ -293,8 +327,9 @@ bool Resource::isMoreThanOneHardLink() const
auto Resource::getOriginalFileName() const -> QString auto Resource::getOriginalFileName() const -> QString
{ {
auto fileName = m_file_info.fileName(); auto fileName = m_file_info.fileName();
if (!m_enabled) if (!m_enabled) {
fileName.chop(9); fileName.chop(9);
}
return fileName; return fileName;
} }
@ -324,16 +359,16 @@ QDebug operator<<(QDebug debug, ResourceType type)
QDebug operator<<(QDebug debug, ResourceStatus status) QDebug operator<<(QDebug debug, ResourceStatus status)
{ {
switch (status) { switch (status) {
case ResourceStatus::INSTALLED: case ResourceStatus::Installed:
debug << "INSTALLED"; debug << "INSTALLED";
break; break;
case ResourceStatus::NOT_INSTALLED: case ResourceStatus::NotInstalled:
debug << "NOT_INSTALLED"; debug << "NOT_INSTALLED";
break; break;
case ResourceStatus::NO_METADATA: case ResourceStatus::NoMetadata:
debug << "NO_METADATA"; debug << "NO_METADATA";
break; break;
case ResourceStatus::UNKNOWN: case ResourceStatus::Unknown:
default: default:
debug << "UNKNOWN"; debug << "UNKNOWN";
break; break;

View file

@ -45,7 +45,7 @@
class BaseInstance; class BaseInstance;
enum class ResourceType { enum class ResourceType : std::uint8_t {
UNKNOWN, //!< Indicates an unspecified resource type. UNKNOWN, //!< Indicates an unspecified resource type.
ZIPFILE, //!< The resource is a zip file containing the resource's class files. ZIPFILE, //!< The resource is a zip file containing the resource's class files.
SINGLEFILE, //!< The resource is a single file (not a zip file). SINGLEFILE, //!< The resource is a single file (not a zip file).
@ -55,32 +55,33 @@ enum class ResourceType {
QDebug operator<<(QDebug debug, ResourceType type); QDebug operator<<(QDebug debug, ResourceType type);
enum class ResourceStatus { enum class ResourceStatus : std::uint8_t {
INSTALLED, // Both JAR and Metadata are present Installed, // Both JAR and Metadata are present
NOT_INSTALLED, // Only the Metadata is present NotInstalled, // Only the Metadata is present
NO_METADATA, // Only the JAR is present NoMetadata, // Only the JAR is present
UNKNOWN, // Default status Unknown, // Default status
}; };
QDebug operator<<(QDebug debug, ResourceStatus status); QDebug operator<<(QDebug debug, ResourceStatus status);
enum class SortType { enum class SortType : std::uint8_t {
NAME, Name,
DATE, Date,
VERSION, Version,
ENABLED, Enabled,
PACK_FORMAT, PackFormat,
PROVIDER, Provider,
SIZE, Size,
SIDE, Side,
MC_VERSIONS, McVersions,
LOADERS, Loaders,
RELEASE_TYPE, ReleaseType,
REQUIRES, Requires,
REQUIRED_BY, RequiredBy,
Filename,
}; };
enum class EnableAction { ENABLE, DISABLE, TOGGLE }; enum class EnableAction : std::uint8_t { ENABLE, DISABLE, TOGGLE };
/** General class for managed resources. It mirrors a file in disk, with some more info /** General class for managed resources. It mirrors a file in disk, with some more info
* for display and house-keeping purposes. * for display and house-keeping purposes.
@ -92,20 +93,19 @@ class Resource : public QObject {
Q_DISABLE_COPY(Resource) Q_DISABLE_COPY(Resource)
public: public:
using Ptr = shared_qobject_ptr<Resource>; using Ptr = shared_qobject_ptr<Resource>;
using WeakPtr = QPointer<Resource>;
Resource(QObject* parent = nullptr); Resource(QObject* parent = nullptr);
Resource(QFileInfo file_info); Resource(QFileInfo fileInfo);
Resource(QString file_path) : Resource(QFileInfo(file_path)) {} Resource(const QString& filePath) : Resource(QFileInfo(filePath)) {}
~Resource() override = default; ~Resource() override = default;
void setFile(QFileInfo file_info); void setFile(QFileInfo fileInfo);
void parseFile(); void parseFile();
auto fileinfo() const -> QFileInfo { return m_file_info; } auto fileinfo() const -> QFileInfo { return m_file_info; }
auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; }
auto internal_id() const -> QString { return m_internal_id; } auto internalId() const -> QString { return m_internal_id; }
auto type() const -> ResourceType { return m_type; } auto type() const -> ResourceType { return m_type; }
bool enabled() const { return m_enabled; } bool enabled() const { return m_enabled; }
auto getOriginalFileName() const -> QString; auto getOriginalFileName() const -> QString;
@ -138,7 +138,7 @@ class Resource : public QObject {
* = 0: 'this' is equal to 'other' * = 0: 'this' is equal to 'other'
* < 0: 'this' comes before 'other' * < 0: 'this' comes before 'other'
*/ */
virtual int compare(const Resource& other, SortType type = SortType::NAME) const; virtual int compare(const Resource& other, SortType type = SortType::Name) const;
/** Returns whether the given filter should filter out 'this' (false), /** Returns whether the given filter should filter out 'this' (false),
* or if such filter includes the Resource (true). * or if such filter includes the Resource (true).
@ -163,9 +163,9 @@ class Resource : public QObject {
} }
// Delete all files of this resource. // Delete all files of this resource.
auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; auto destroy(const QDir& indexDir, bool preserveMetadata = false, bool attemptTrash = true) -> bool;
// Delete the metadata only. // Delete the metadata only.
auto destroyMetadata(const QDir& index_dir) -> void; auto destroyMetadata(const QDir& indexDir) -> void;
auto isSymLink() const -> bool { return m_file_info.isSymLink(); } auto isSymLink() const -> bool { return m_file_info.isSymLink(); }
@ -195,7 +195,7 @@ class Resource : public QObject {
ResourceType m_type = ResourceType::UNKNOWN; ResourceType m_type = ResourceType::UNKNOWN;
/* Installation status of the resource. */ /* Installation status of the resource. */
ResourceStatus m_status = ResourceStatus::UNKNOWN; ResourceStatus m_status = ResourceStatus::Unknown;
std::shared_ptr<Metadata::ModStruct> m_metadata = nullptr; std::shared_ptr<Metadata::ModStruct> m_metadata = nullptr;

View file

@ -11,6 +11,7 @@
#include <QStyle> #include <QStyle>
#include <QThreadPool> #include <QThreadPool>
#include <QUrl> #include <QUrl>
#include <algorithm>
#include <utility> #include <utility>
#include "Application.h" #include "Application.h"
@ -27,10 +28,10 @@
#include "tasks/Task.h" #include "tasks/Task.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent)
: QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_is_indexed(is_indexed) : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_isIndexed(isIndexed)
{ {
if (create_dir) { if (createDir) {
FS::ensureFolderPathExists(m_dir.absolutePath()); FS::ensureFolderPathExists(m_dir.absolutePath());
} }
@ -49,70 +50,75 @@ ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance
ResourceFolderModel::~ResourceFolderModel() ResourceFolderModel::~ResourceFolderModel()
{ {
while (!QThreadPool::globalInstance()->waitForDone(100)) while (!QThreadPool::globalInstance()->waitForDone(100)) {
QCoreApplication::processEvents(); QCoreApplication::processEvents();
} }
}
bool ResourceFolderModel::startWatching(const QStringList& paths) bool ResourceFolderModel::startWatching(const QStringList& paths)
{ {
// Remove orphaned metadata next time // Remove orphaned metadata next time
m_first_folder_load = true; m_firstFolderLoad = true;
if (m_is_watching) if (m_isWatching) {
return false; return false;
}
auto couldnt_be_watched = m_watcher.addPaths(paths); auto couldntBeWatched = m_watcher.addPaths(paths);
for (auto path : paths) { for (const auto& path : paths) {
if (couldnt_be_watched.contains(path)) if (couldntBeWatched.contains(path)) {
qDebug() << "Failed to start watching" << path; qDebug() << "Failed to start watching" << path;
else } else {
qDebug() << "Started watching" << path; qDebug() << "Started watching" << path;
} }
}
update(); update();
m_is_watching = !m_is_watching; m_isWatching = !m_isWatching;
return m_is_watching; return m_isWatching;
} }
bool ResourceFolderModel::stopWatching(const QStringList& paths) bool ResourceFolderModel::stopWatching(const QStringList& paths)
{ {
if (!m_is_watching) if (!m_isWatching) {
return false; return false;
}
auto couldnt_be_stopped = m_watcher.removePaths(paths); auto couldntBeStopped = m_watcher.removePaths(paths);
for (auto path : paths) { for (const auto& path : paths) {
if (couldnt_be_stopped.contains(path)) if (couldntBeStopped.contains(path)) {
qDebug() << "Failed to stop watching" << path; qDebug() << "Failed to stop watching" << path;
else } else {
qDebug() << "Stopped watching" << path; qDebug() << "Stopped watching" << path;
} }
m_is_watching = !m_is_watching;
return !m_is_watching;
} }
bool ResourceFolderModel::installResource(QString original_path) m_isWatching = !m_isWatching;
return !m_isWatching;
}
bool ResourceFolderModel::installResource(QString originalPath)
{ {
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
original_path = FS::NormalizePath(original_path); originalPath = FS::NormalizePath(originalPath);
QFileInfo file_info(original_path); QFileInfo fileInfo(originalPath);
if (!file_info.exists() || !file_info.isReadable()) { if (!fileInfo.exists() || !fileInfo.isReadable()) {
qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
return false; return false;
} }
qDebug() << "Installing:" << file_info.absoluteFilePath(); qDebug() << "Installing:" << fileInfo.absoluteFilePath();
Resource resource(file_info); Resource resource(fileInfo);
if (!resource.valid()) { if (!resource.valid()) {
qWarning() << original_path << "is not a valid resource. Ignoring it."; qWarning() << originalPath << "is not a valid resource. Ignoring it.";
return false; return false;
} }
auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); auto newPath = FS::NormalizePath(m_dir.filePath(fileInfo.fileName()));
if (original_path == new_path) { if (originalPath == newPath) {
qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; qWarning() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
return false; return false;
} }
@ -120,45 +126,47 @@ bool ResourceFolderModel::installResource(QString original_path)
case ResourceType::SINGLEFILE: case ResourceType::SINGLEFILE:
case ResourceType::ZIPFILE: case ResourceType::ZIPFILE:
case ResourceType::LITEMOD: { case ResourceType::LITEMOD: {
if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { if (QFile::exists(newPath) || QFile::exists(newPath + QString(".disabled"))) {
if (!FS::deletePath(new_path)) { if (!FS::deletePath(newPath)) {
qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; qCritical() << "Cleaning up new location (" << newPath << ") was unsuccessful!";
return false; return false;
} }
qDebug() << new_path << "has been deleted."; qDebug() << newPath << "has been deleted.";
} }
if (!QFile::copy(original_path, new_path)) { if (!QFile::copy(originalPath, newPath)) {
qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; qCritical() << "Copy from" << originalPath << "to" << newPath << "has failed.";
return false; return false;
} }
FS::updateTimestamp(new_path); FS::updateTimestamp(newPath);
QFileInfo new_path_file_info(new_path); QFileInfo newPathFileInfo(newPath);
resource.setFile(new_path_file_info); resource.setFile(newPathFileInfo);
if (!m_is_watching) if (!m_isWatching) {
return update(); return update();
}
return true; return true;
} }
case ResourceType::FOLDER: { case ResourceType::FOLDER: {
if (QFile::exists(new_path)) { if (QFile::exists(newPath)) {
qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; qDebug() << "Ignoring folder '" << originalPath << "', it would merge with" << newPath;
return false; return false;
} }
if (!FS::copy(original_path, new_path)()) { if (!FS::copy(originalPath, newPath)()) {
qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed."; qWarning() << "Copy of folder from" << originalPath << "to" << newPath << "has (potentially partially) failed.";
return false; return false;
} }
QFileInfo newpathInfo(new_path); QFileInfo newpathInfo(newPath);
resource.setFile(newpathInfo); resource.setFile(newpathInfo);
if (!m_is_watching) if (!m_isWatching) {
return update(); return update();
}
return true; return true;
} }
@ -168,24 +176,24 @@ bool ResourceFolderModel::installResource(QString original_path)
return false; return false;
} }
void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) void ResourceFolderModel::installResourceWithFlameMetadata(const QString& path, ModPlatform::IndexedVersion& vers)
{ {
auto install = [this, path] { installResource(std::move(path)); }; auto install = [this, path] { installResource(path); };
if (vers.addonId.isValid()) { if (vers.addonId.isValid()) {
ModPlatform::IndexedPack pack{ ModPlatform::IndexedPack pack{
vers.addonId, .addonId = vers.addonId,
ModPlatform::ResourceProvider::FLAME, .provider = ModPlatform::ResourceProvider::FLAME,
}; };
auto [job, response] = FlameAPI().getProject(vers.addonId.toString()); auto [job, response] = FlameAPI().getProject(vers.addonId.toString());
connect(job.get(), &Task::failed, this, install); connect(job.get(), &Task::failed, this, install);
connect(job.get(), &Task::aborted, this, install); connect(job.get(), &Task::aborted, this, install);
connect(job.get(), &Task::succeeded, [response, this, &vers, install, &pack] { connect(job.get(), &Task::succeeded, [response, this, &vers, install, &pack] {
QJsonParseError parse_error{}; QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parse_error.error != QJsonParseError::NoError) { if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset qWarning() << "Error while parsing JSON response for mod info at" << parseError.offset
<< "reason:" << parse_error.errorString(); << "reason:" << parseError.errorString();
qDebug() << *response; qDebug() << *response;
return; return;
} }
@ -196,9 +204,9 @@ void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlat
qDebug() << doc; qDebug() << doc;
qWarning() << "Error while reading mod info:" << e.cause(); qWarning() << "Error while reading mod info:" << e.cause();
} }
LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); LocalResourceUpdateTask updateMetadata(indexDir(), pack, vers);
connect(&update_metadata, &Task::finished, this, install); connect(&updateMetadata, &Task::finished, this, install);
update_metadata.start(); updateMetadata.start();
}); });
job->start(); job->start();
@ -207,7 +215,7 @@ void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlat
} }
} }
bool ResourceFolderModel::uninstallResource(const QString& file_name, bool preserve_metadata) bool ResourceFolderModel::uninstallResource(const QString& fileName, bool preserveMetadata)
{ {
for (auto& resource : m_resources) { for (auto& resource : m_resources) {
auto resourceFileInfo = resource->fileinfo(); auto resourceFileInfo = resource->fileinfo();
@ -216,8 +224,8 @@ bool ResourceFolderModel::uninstallResource(const QString& file_name, bool prese
resourceFileName.chop(9); resourceFileName.chop(9);
} }
if (resourceFileName == file_name) { if (resourceFileName == fileName) {
auto res = resource->destroy(indexDir(), preserve_metadata, false); auto res = resource->destroy(indexDir(), preserveMetadata, false);
update(); update();
@ -229,14 +237,16 @@ bool ResourceFolderModel::uninstallResource(const QString& file_name, bool prese
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
{ {
if (indexes.isEmpty()) if (indexes.isEmpty()) {
return true; return true;
}
for (auto i : indexes) { for (auto i : indexes) {
if (i.column() != 0) if (i.column() != 0) {
continue; continue;
}
auto& resource = m_resources.at(i.row()); const auto& resource = m_resources.at(i.row());
resource->destroy(indexDir()); resource->destroy(indexDir());
} }
@ -247,14 +257,16 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes)
{ {
if (indexes.isEmpty()) if (indexes.isEmpty()) {
return; return;
}
for (auto i : indexes) { for (auto i : indexes) {
if (i.column() != 0) if (i.column() != 0) {
continue; continue;
}
auto& resource = m_resources.at(i.row()); const auto& resource = m_resources.at(i.row());
resource->destroyMetadata(indexDir()); resource->destroyMetadata(indexDir());
} }
@ -271,33 +283,36 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec(); ->exec();
if (response != QMessageBox::Yes) if (response != QMessageBox::Yes) {
return false; return false;
} }
}
if (indexes.isEmpty()) if (indexes.isEmpty()) {
return true; return true;
}
bool succeeded = true; bool succeeded = true;
for (auto const& idx : indexes) { for (const auto& idx : indexes) {
if (!validateIndex(idx) || idx.column() != 0) if (!validateIndex(idx) || idx.column() != 0) {
continue; continue;
}
int row = idx.row(); int row = idx.row();
auto& resource = m_resources[row]; auto& resource = m_resources[row];
// Preserve the row, but change its ID // Preserve the row, but change its ID
auto old_id = resource->internal_id(); auto oldId = resource->internalId();
if (!resource->enable(action)) { if (!resource->enable(action)) {
succeeded = false; succeeded = false;
continue; continue;
} }
auto new_id = resource->internal_id(); auto newId = resource->internalId();
m_resources_index.remove(old_id); m_resourcesIndex.remove(oldId);
m_resources_index[new_id] = row; m_resourcesIndex[newId] = row;
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
} }
@ -312,24 +327,25 @@ bool ResourceFolderModel::update()
QMutexLocker lock(&s_update_task_mutex); QMutexLocker lock(&s_update_task_mutex);
// Already updating, so we schedule a future update and return. // Already updating, so we schedule a future update and return.
if (m_current_update_task) { if (m_currentUpdateTask) {
m_scheduled_update = true; m_scheduledUpdate = true;
return false; return false;
} }
m_current_update_task.reset(createUpdateTask()); m_currentUpdateTask.reset(createUpdateTask());
if (!m_current_update_task) if (!m_currentUpdateTask) {
return false; return false;
}
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, connect(m_currentUpdateTask.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
Qt::ConnectionType::QueuedConnection); Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect(m_currentUpdateTask.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
connect( connect(
m_current_update_task.get(), &Task::finished, this, m_currentUpdateTask.get(), &Task::finished, this,
[this] { [this] {
m_current_update_task.reset(); m_currentUpdateTask.reset();
if (m_scheduled_update) { if (m_scheduledUpdate) {
m_scheduled_update = false; m_scheduledUpdate = false;
update(); update();
} else { } else {
emit updateFinished(); emit updateFinished();
@ -340,16 +356,16 @@ bool ResourceFolderModel::update()
Task::Ptr preUpdate{ createPreUpdateTask() }; Task::Ptr preUpdate{ createPreUpdateTask() };
if (preUpdate != nullptr) { if (preUpdate != nullptr) {
auto task = new SequentialTask("ResourceFolderModel::update"); auto* task = new SequentialTask("ResourceFolderModel::update");
task->addTask(preUpdate); task->addTask(preUpdate);
task->addTask(m_current_update_task); task->addTask(m_currentUpdateTask);
connect(task, &Task::finished, [task] { task->deleteLater(); }); connect(task, &Task::finished, [task] { task->deleteLater(); });
QThreadPool::globalInstance()->start(task); QThreadPool::globalInstance()->start(task);
} else { } else {
QThreadPool::globalInstance()->start(m_current_update_task.get()); QThreadPool::globalInstance()->start(m_currentUpdateTask.get());
} }
return true; return true;
@ -362,24 +378,25 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res)
} }
Task::Ptr task{ createParseTask(*res) }; Task::Ptr task{ createParseTask(*res) };
if (!task) if (!task) {
return; return;
}
int ticket = m_next_resolution_ticket.fetch_add(1); int ticket = m_nextResolutionTicket.fetch_add(1);
res->setResolving(true, ticket); res->setResolving(true, ticket);
m_active_parse_tasks.insert(ticket, task); m_activeParseTasks.insert(ticket, task);
connect( connect(
task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internalId()); },
Qt::ConnectionType::QueuedConnection); Qt::ConnectionType::QueuedConnection);
connect( connect(
task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internalId()); },
Qt::ConnectionType::QueuedConnection); Qt::ConnectionType::QueuedConnection);
connect( connect(
task.get(), &Task::finished, this, task.get(), &Task::finished, this,
[this, ticket] { [this, ticket] {
m_active_parse_tasks.remove(ticket); m_activeParseTasks.remove(ticket);
emit parseFinished(); emit parseFinished();
}, },
Qt::ConnectionType::QueuedConnection); Qt::ConnectionType::QueuedConnection);
@ -394,44 +411,45 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res)
void ResourceFolderModel::onUpdateSucceeded() void ResourceFolderModel::onUpdateSucceeded()
{ {
auto update_results = static_cast<ResourceFolderLoadTask*>(m_current_update_task.get())->result(); auto updateResults = static_cast<ResourceFolderLoadTask*>(m_currentUpdateTask.get())->result();
auto& new_resources = update_results->resources; auto& newResources = updateResults->resources;
auto current_list = m_resources_index.keys(); auto currentList = m_resourcesIndex.keys();
QSet<QString> current_set(current_list.begin(), current_list.end()); QSet<QString> currentSet(currentList.begin(), currentList.end());
auto new_list = new_resources.keys(); auto newList = newResources.keys();
QSet<QString> new_set(new_list.begin(), new_list.end()); QSet<QString> newSet(newList.begin(), newList.end());
applyUpdates(current_set, new_set, new_resources); applyUpdates(currentSet, newSet, newResources);
} }
void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) void ResourceFolderModel::onParseSucceeded(int ticket, const QString& resourceId)
{ {
auto iter = m_active_parse_tasks.constFind(ticket); auto iter = m_activeParseTasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) if (iter == m_activeParseTasks.constEnd() || !m_resourcesIndex.contains(resourceId)) {
return; return;
}
int row = m_resources_index[resource_id]; int row = m_resourcesIndex[resourceId];
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
} }
Task* ResourceFolderModel::createUpdateTask() Task* ResourceFolderModel::createUpdateTask()
{ {
auto index_dir = indexDir(); auto indexDir2 = indexDir();
auto task = new ResourceFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, auto* task = new ResourceFolderLoadTask(dir(), indexDir2, m_isIndexed, m_firstFolderLoad,
[this](const QFileInfo& file) { return createResource(file); }); [this](const QFileInfo& file) { return createResource(file); });
m_first_folder_load = false; m_firstFolderLoad = false;
return task; return task;
} }
bool ResourceFolderModel::hasPendingParseTasks() const bool ResourceFolderModel::hasPendingParseTasks() const
{ {
return !m_active_parse_tasks.isEmpty(); return !m_activeParseTasks.isEmpty();
} }
void ResourceFolderModel::directoryChanged(QString path) void ResourceFolderModel::directoryChanged(const QString& /*path*/)
{ {
update(); update();
} }
@ -446,8 +464,9 @@ Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
{ {
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags | Qt::ItemIsDropEnabled; auto flags = defaultFlags | Qt::ItemIsDropEnabled;
if (index.isValid()) if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable; flags |= Qt::ItemIsUserCheckable;
}
return flags; return flags;
} }
@ -458,21 +477,25 @@ QStringList ResourceFolderModel::mimeTypes() const
return types; return types;
} }
bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) bool ResourceFolderModel::dropMimeData(const QMimeData* data,
Qt::DropAction action,
int /*row*/,
int /*column*/,
const QModelIndex& /*parent*/)
{ {
if (action == Qt::IgnoreAction) { if (action == Qt::IgnoreAction) {
return true; return true;
} }
// check if the action is supported // check if the action is supported
if (!data || !(action & supportedDropActions())) { if ((data == nullptr) || !(action & supportedDropActions())) {
return false; return false;
} }
// files dropped from outside? // files dropped from outside?
if (data->hasUrls()) { if (data->hasUrls()) {
auto urls = data->urls(); auto urls = data->urls();
for (auto url : urls) { for (const auto& url : urls) {
// only local files may be dropped... // only local files may be dropped...
if (!url.isLocalFile()) { if (!url.isLocalFile()) {
continue; continue;
@ -488,14 +511,12 @@ bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction act
bool ResourceFolderModel::validateIndex(const QModelIndex& index) const bool ResourceFolderModel::validateIndex(const QModelIndex& index) const
{ {
if (!index.isValid()) if (!index.isValid()) {
return false; return false;
}
int row = index.row(); int row = index.row();
if (row < 0 || row >= m_resources.size()) return row >= 0 && row < m_resources.size();
return false;
return true;
} }
// HACK: all subclasses need to call this to have the whole row painted // HACK: all subclasses need to call this to have the whole row painted
@ -504,15 +525,15 @@ QBrush ResourceFolderModel::rowBackground(int row) const
{ {
if (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) { if (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) {
return { QColor(255, 0, 0, 40) }; return { QColor(255, 0, 0, 40) };
} else {
return {};
} }
return {};
} }
QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
{ {
if (!validateIndex(index)) if (!validateIndex(index)) {
return {}; return {};
}
int row = index.row(); int row = index.row();
int column = index.column(); int column = index.column();
@ -530,11 +551,13 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return m_resources[row]->provider(); return m_resources[row]->provider();
case SizeColumn: case SizeColumn:
return m_resources[row]->sizeStr(); return m_resources[row]->sizeStr();
case FileNameColumn:
return m_resources[row]->fileinfo().fileName();
default: default:
return {}; return {};
} }
case Qt::ToolTipRole: { case Qt::ToolTipRole: {
QString tooltip = m_resources[row]->internal_id(); QString tooltip = m_resources[row]->internalId();
if (column == NameColumn) { if (column == NameColumn) {
if (APPLICATION->settings()->get("ShowModIncompat").toBool()) { if (APPLICATION->settings()->get("ShowModIncompat").toBool()) {
@ -545,7 +568,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
if (at(row).isSymLinkUnder(instDirPath())) { if (at(row).isSymLinkUnder(instDirPath())) {
tooltip += tooltip +=
m_resources[row]->internal_id() + m_resources[row]->internalId() +
tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original."
"\nCanonical Path: %1") "\nCanonical Path: %1")
.arg(at(row).fileinfo().canonicalFilePath()); .arg(at(row).fileinfo().canonicalFilePath());
@ -562,7 +585,8 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
if (column == NameColumn) { if (column == NameColumn) {
if (APPLICATION->settings()->get("ShowModIncompat").toBool() && at(row).hasIssues()) { if (APPLICATION->settings()->get("ShowModIncompat").toBool() && at(row).hasIssues()) {
return QIcon::fromTheme("status-bad"); return QIcon::fromTheme("status-bad");
} else if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) { }
if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) {
return QIcon::fromTheme("status-yellow"); return QIcon::fromTheme("status-yellow");
} }
} }
@ -570,8 +594,9 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return {}; return {};
} }
case Qt::CheckStateRole: case Qt::CheckStateRole:
if (column == ActiveColumn) if (column == ActiveColumn) {
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
}
return {}; return {};
default: default:
return {}; return {};
@ -581,8 +606,9 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role)
{ {
int row = index.row(); int row = index.row();
if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) {
return false; return false;
}
if (role == Qt::CheckStateRole) { if (role == Qt::CheckStateRole) {
return setResourceEnabled({ index }, EnableAction::TOGGLE); return setResourceEnabled({ index }, EnableAction::TOGGLE);
@ -601,6 +627,7 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
case DateColumn: case DateColumn:
case ProviderColumn: case ProviderColumn:
case SizeColumn: case SizeColumn:
case FileNameColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return {}; return {};
@ -618,6 +645,8 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
return tr("The source provider of the resource."); return tr("The source provider of the resource.");
case SizeColumn: case SizeColumn:
return tr("The size of the resource."); return tr("The size of the resource.");
case FileNameColumn:
return tr("The file name of the resource.");
default: default:
return {}; return {};
} }
@ -638,22 +667,22 @@ void ResourceFolderModel::setupHeaderAction(QAction* act, int column)
void ResourceFolderModel::saveColumns(QTreeView* tree) void ResourceFolderModel::saveColumns(QTreeView* tree)
{ {
auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); const auto stateSettingName = QString("UI/%1_Page/Columns").arg(id());
auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); const auto overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id());
auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); const auto visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id());
auto stateSetting = m_instance->settings()->getSetting(stateSettingName); auto stateSetting = m_instance->settings()->getSetting(stateSettingName);
stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64()));
// neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false
auto settings = m_instance->settings(); auto* settings = m_instance->settings();
if (!settings->get(overrideSettingName).toBool()) { if (!settings->get(overrideSettingName).toBool()) {
settings = APPLICATION->settings(); settings = APPLICATION->settings();
} }
auto visibility = Json::toMap(settings->get(visibilitySettingName).toString()); auto visibility = Json::toMap(settings->get(visibilitySettingName).toString());
for (auto i = 0; i < m_column_names.size(); ++i) { for (auto i = 0; i < m_columnNames.size(); ++i) {
if (m_columnsHideable[i]) { if (m_columnsHideable[i]) {
auto name = m_column_names[i]; auto name = m_columnNames[i];
visibility[name] = !tree->isColumnHidden(i); visibility[name] = !tree->isColumnHidden(i);
} }
} }
@ -662,24 +691,24 @@ void ResourceFolderModel::saveColumns(QTreeView* tree)
void ResourceFolderModel::loadColumns(QTreeView* tree) void ResourceFolderModel::loadColumns(QTreeView* tree)
{ {
auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); const auto stateSettingName = QString("UI/%1_Page/Columns").arg(id());
auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); const auto overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id());
auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); const auto visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id());
auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, "");
tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8()));
auto setVisible = [this, tree](QVariant value) { auto setVisible = [this, tree](const QVariant& value) {
auto visibility = Json::toMap(value.toString()); auto visibility = Json::toMap(value.toString());
for (auto i = 0; i < m_column_names.size(); ++i) { for (auto i = 0; i < m_columnNames.size(); ++i) {
if (m_columnsHideable[i]) { if (m_columnsHideable[i]) {
auto name = m_column_names[i]; auto name = m_columnNames[i];
tree->setColumnHidden(i, !visibility.value(name, false).toBool()); tree->setColumnHidden(i, !visibility.value(name, false).toBool());
} }
} }
}; };
auto const defaultValue = Json::fromMap({ const auto defaultValue = Json::fromMap({
{ "Image", true }, { "Image", true },
{ "Version", true }, { "Version", true },
{ "Last Modified", true }, { "Last Modified", true },
@ -687,7 +716,7 @@ void ResourceFolderModel::loadColumns(QTreeView* tree)
{ "Pack Format", true }, { "Pack Format", true },
}); });
// neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false
auto settings = m_instance->settings(); auto* settings = m_instance->settings();
if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) { if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) {
settings = APPLICATION->settings(); settings = APPLICATION->settings();
} }
@ -696,7 +725,7 @@ void ResourceFolderModel::loadColumns(QTreeView* tree)
// allways connect the signal in case the setting is toggled on and off // allways connect the signal in case the setting is toggled on and off
auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue); auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue);
connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible, overrideSettingName](const Setting&, QVariant value) { connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible, overrideSettingName](const Setting&, const QVariant& value) {
if (!m_instance->settings()->get(overrideSettingName).toBool()) { if (!m_instance->settings()->get(overrideSettingName).toBool()) {
setVisible(value); setVisible(value);
} }
@ -705,11 +734,11 @@ void ResourceFolderModel::loadColumns(QTreeView* tree)
QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree)
{ {
auto menu = new QMenu(tree); auto* menu = new QMenu(tree);
{ // action to decide if the visibility is per instance or not { // action to decide if the visibility is per instance or not
auto act = new QAction(tr("Override Columns Visibility"), menu); auto* act = new QAction(tr("Override Columns Visibility"), menu);
auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); const auto overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id());
act->setCheckable(true); act->setCheckable(true);
act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool());
@ -725,9 +754,10 @@ QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree)
for (int col = 0; col < columnCount(); ++col) { for (int col = 0; col < columnCount(); ++col) {
// Skip creating actions for columns that should not be hidden // Skip creating actions for columns that should not be hidden
if (!m_columnsHideable.at(col)) if (!m_columnsHideable.at(col)) {
continue; continue;
auto act = new QAction(menu); }
auto* act = new QAction(menu);
setupHeaderAction(act, col); setupHeaderAction(act, col);
act->setCheckable(true); act->setCheckable(true);
@ -736,9 +766,10 @@ QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree)
connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled) { connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled) {
tree->setColumnHidden(col, !toggled); tree->setColumnHidden(col, !toggled);
for (int c = 0; c < columnCount(); ++c) { for (int c = 0; c < columnCount(); ++c) {
if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) if (m_columnResizeModes.at(c) == QHeaderView::ResizeToContents) {
tree->resizeColumnToContents(c); tree->resizeColumnToContents(c);
} }
}
saveColumns(tree); saveColumns(tree);
}); });
@ -755,41 +786,43 @@ QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* pare
SortType ResourceFolderModel::columnToSortKey(size_t column) const SortType ResourceFolderModel::columnToSortKey(size_t column) const
{ {
Q_ASSERT(m_column_sort_keys.size() == columnCount()); Q_ASSERT(m_columnSortKeys.size() == columnCount());
return m_column_sort_keys.at(column); return m_columnSortKeys.at(column);
} }
/* Standard Proxy Model for createFilterProxyModel */ /* Standard Proxy Model for createFilterProxyModel */
bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, [[maybe_unused]] const QModelIndex& source_parent) const bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const
{ {
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel()); auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
if (!model) if (!model) {
return true; return true;
}
const auto& resource = model->at(source_row); const auto& resource = model->at(sourceRow);
return resource.applyFilter(filterRegularExpression()); return resource.applyFilter(filterRegularExpression());
} }
bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight) const
{ {
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel()); auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { if (!model || !sourceLeft.isValid() || !sourceRight.isValid() || sourceLeft.column() != sourceRight.column()) {
return QSortFilterProxyModel::lessThan(source_left, source_right); return QSortFilterProxyModel::lessThan(sourceLeft, sourceRight);
} }
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
// proceed. // proceed.
auto column_sort_key = model->columnToSortKey(source_left.column()); auto columnSortKey = model->columnToSortKey(sourceLeft.column());
auto const& resource_left = model->at(source_left.row()); const auto& resourceLeft = model->at(sourceLeft.row());
auto const& resource_right = model->at(source_right.row()); const auto& resourceRight = model->at(sourceRight.row());
auto compare_result = resource_left.compare(resource_right, column_sort_key); auto compareResult = resourceLeft.compare(resourceRight, columnSortKey);
if (compare_result == 0) if (compareResult == 0) {
return QSortFilterProxyModel::lessThan(source_left, source_right); return QSortFilterProxyModel::lessThan(sourceLeft, sourceRight);
}
return compare_result < 0; return compareResult < 0;
} }
QString ResourceFolderModel::instDirPath() const QString ResourceFolderModel::instDirPath() const
@ -797,50 +830,51 @@ QString ResourceFolderModel::instDirPath() const
return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
} }
void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) void ResourceFolderModel::onParseFailed(int ticket, const QString& resourceId)
{ {
auto iter = m_active_parse_tasks.constFind(ticket); auto iter = m_activeParseTasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) if (iter == m_activeParseTasks.constEnd() || !m_resourcesIndex.contains(resourceId)) {
return; return;
}
auto removed_index = m_resources_index[resource_id]; auto removedIndex = m_resourcesIndex[resourceId];
auto removed_it = m_resources.begin() + removed_index; auto removedIt = m_resources.begin() + removedIndex;
Q_ASSERT(removed_it != m_resources.end()); Q_ASSERT(removedIt != m_resources.end());
beginRemoveRows(QModelIndex(), removed_index, removed_index); beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
m_resources.erase(removed_it); m_resources.erase(removedIt);
// update index // update index
m_resources_index.clear(); m_resourcesIndex.clear();
int idx = 0; int idx = 0;
for (auto const& mod : qAsConst(m_resources)) { for (const auto& mod : qAsConst(m_resources)) {
m_resources_index[mod->internal_id()] = idx; m_resourcesIndex[mod->internalId()] = idx;
idx++; idx++;
} }
endRemoveRows(); endRemoveRows();
} }
void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, Resource::Ptr>& new_resources) void ResourceFolderModel::applyUpdates(QSet<QString>& currentSet, QSet<QString>& newSet, QMap<QString, Resource::Ptr>& newResources)
{ {
// see if the kept resources changed in some way // see if the kept resources changed in some way
{ {
QSet<QString> kept_set = current_set; QSet<QString> keptSet = currentSet;
kept_set.intersect(new_set); keptSet.intersect(newSet);
for (auto const& kept : kept_set) { for (const auto& kept : keptSet) {
auto row_it = m_resources_index.constFind(kept); auto rowIt = m_resourcesIndex.constFind(kept);
Q_ASSERT(row_it != m_resources_index.constEnd()); Q_ASSERT(rowIt != m_resourcesIndex.constEnd());
auto row = row_it.value(); auto row = rowIt.value();
auto& new_resource = new_resources[kept]; auto& newResource = newResources[kept];
auto const& current_resource = m_resources.at(row); const auto& currentResource = m_resources.at(row);
if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { if (newResource->dateTimeChanged() == currentResource->dateTimeChanged()) {
// no significant change // no significant change
bool hadIssues = !current_resource->hasIssues(); bool hadIssues = !currentResource->hasIssues();
current_resource->updateIssues(m_instance); currentResource->updateIssues(m_instance);
if (hadIssues != current_resource->hasIssues()) { if (hadIssues != currentResource->hasIssues()) {
emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); emit dataChanged(index(row, 0), index(row, columnCount({}) - 1));
} }
continue; continue;
@ -848,16 +882,16 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
// If the resource is resolving, but something about it changed, we don't want to // If the resource is resolving, but something about it changed, we don't want to
// continue the resolving. // continue the resolving.
if (current_resource->isResolving()) { if (currentResource->isResolving()) {
auto ticket = current_resource->resolutionTicket(); auto ticket = currentResource->resolutionTicket();
if (m_active_parse_tasks.contains(ticket)) { if (m_activeParseTasks.contains(ticket)) {
auto task = (*m_active_parse_tasks.find(ticket)).get(); auto* task = (*m_activeParseTasks.find(ticket)).get();
task->abort(); task->abort();
} }
} }
m_resources[row].reset(new_resource); m_resources[row].reset(newResource);
new_resource->updateIssues(m_instance); newResource->updateIssues(m_instance);
resolveResource(m_resources.at(row)); resolveResource(m_resources.at(row));
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
@ -866,46 +900,47 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
// remove resources no longer present // remove resources no longer present
{ {
QSet<QString> removed_set = current_set; QSet<QString> removedSet = currentSet;
removed_set.subtract(new_set); removedSet.subtract(newSet);
QList<int> removed_rows; QList<int> removedRows;
for (auto& removed : removed_set) for (const auto& removed : removedSet) {
removed_rows.append(m_resources_index[removed]); removedRows.append(m_resourcesIndex[removed]);
}
std::sort(removed_rows.begin(), removed_rows.end(), std::greater<int>()); std::ranges::sort(removedRows, std::greater());
for (auto& removed_index : removed_rows) { for (auto& removedIndex : removedRows) {
auto removed_it = m_resources.begin() + removed_index; auto removedIt = m_resources.begin() + removedIndex;
Q_ASSERT(removed_it != m_resources.end()); Q_ASSERT(removedIt != m_resources.end());
if ((*removed_it)->isResolving()) { if ((*removedIt)->isResolving()) {
auto ticket = (*removed_it)->resolutionTicket(); auto ticket = (*removedIt)->resolutionTicket();
if (m_active_parse_tasks.contains(ticket)) { if (m_activeParseTasks.contains(ticket)) {
auto task = (*m_active_parse_tasks.find(ticket)).get(); auto* task = (*m_activeParseTasks.find(ticket)).get();
task->abort(); task->abort();
} }
} }
beginRemoveRows(QModelIndex(), removed_index, removed_index); beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
m_resources.erase(removed_it); m_resources.erase(removedIt);
endRemoveRows(); endRemoveRows();
} }
} }
// add new resources to the end // add new resources to the end
{ {
QSet<QString> added_set = new_set; QSet<QString> addedSet = newSet;
added_set.subtract(current_set); addedSet.subtract(currentSet);
// When you have a Qt build with assertions turned on, proceeding here will abort the application // When you have a Qt build with assertions turned on, proceeding here will abort the application
if (added_set.size() > 0) { if (addedSet.size() > 0) {
beginInsertRows(QModelIndex(), static_cast<int>(m_resources.size()), beginInsertRows(QModelIndex(), static_cast<int>(m_resources.size()),
static_cast<int>(m_resources.size() + added_set.size() - 1)); static_cast<int>(m_resources.size() + addedSet.size() - 1));
for (auto& added : added_set) { for (const auto& added : addedSet) {
auto res = new_resources[added]; auto res = newResources[added];
res->updateIssues(m_instance); res->updateIssues(m_instance);
m_resources.append(res); m_resources.append(res);
resolveResource(m_resources.last()); resolveResource(m_resources.last());
@ -917,10 +952,10 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
// update index // update index
{ {
m_resources_index.clear(); m_resourcesIndex.clear();
int idx = 0; int idx = 0;
for (auto const& mod : qAsConst(m_resources)) { for (const auto& mod : qAsConst(m_resources)) {
m_resources_index[mod->internal_id()] = idx; m_resourcesIndex[mod->internalId()] = idx;
idx++; idx++;
} }
} }
@ -928,17 +963,19 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
Resource::Ptr ResourceFolderModel::find(QString id) Resource::Ptr ResourceFolderModel::find(QString id)
{ {
auto iter = auto iter =
std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](Resource::Ptr const& r) { return r->internal_id() == id; }); std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](const Resource::Ptr& r) { return r->internalId() == id; });
if (iter == m_resources.constEnd()) if (iter == m_resources.constEnd()) {
return nullptr; return nullptr;
}
return *iter; return *iter;
} }
QList<Resource*> ResourceFolderModel::allResources() QList<Resource*> ResourceFolderModel::allResources()
{ {
QList<Resource*> result; QList<Resource*> result;
result.reserve(m_resources.size()); result.reserve(m_resources.size());
for (const Resource ::Ptr& resource : m_resources) for (const Resource ::Ptr& resource : m_resources) {
result.append((resource.get())); result.append((resource.get()));
}
return result; return result;
} }
@ -946,8 +983,9 @@ QList<Resource*> ResourceFolderModel::selectedResources(const QModelIndexList& i
{ {
QList<Resource*> result; QList<Resource*> result;
for (const QModelIndex& index : indexes) { for (const QModelIndex& index : indexes) {
if (index.column() != 0) if (index.column() != 0) {
continue; continue;
}
result.append(&at(index.row())); result.append(&at(index.row()));
} }
return result; return result;

View file

@ -61,7 +61,7 @@ class QSortFilterProxyModel;
class ResourceFolderModel : public QAbstractListModel { class ResourceFolderModel : public QAbstractListModel {
Q_OBJECT Q_OBJECT
public: public:
ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr);
~ResourceFolderModel() override; ~ResourceFolderModel() override;
virtual QString id() const { return "resource"; } virtual QString id() const { return "resource"; }
@ -93,13 +93,13 @@ class ResourceFolderModel : public QAbstractListModel {
*/ */
virtual bool installResource(QString path); virtual bool installResource(QString path);
virtual void installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); virtual void installResourceWithFlameMetadata(const QString& path, ModPlatform::IndexedVersion& vers);
/** Uninstall (i.e. remove all data about it) a resource, given its file name. /** Uninstall (i.e. remove all data about it) a resource, given its file name.
* *
* Returns whether the removal was successful. * Returns whether the removal was successful.
*/ */
virtual bool uninstallResource(const QString& file_name, bool preserve_metadata = false); virtual bool uninstallResource(const QString& fileName, bool preserveMetadata = false);
virtual bool deleteResources(const QModelIndexList&); virtual bool deleteResources(const QModelIndexList&);
virtual void deleteMetadata(const QModelIndexList&); virtual void deleteMetadata(const QModelIndexList&);
@ -125,7 +125,7 @@ class ResourceFolderModel : public QAbstractListModel {
Resource::Ptr find(QString id); Resource::Ptr find(QString id);
QDir const& dir() const { return m_dir; } const QDir& dir() const { return m_dir; }
/** Checks whether there's any parse tasks being done. /** Checks whether there's any parse tasks being done.
* *
@ -137,12 +137,12 @@ class ResourceFolderModel : public QAbstractListModel {
/* Qt behavior */ /* Qt behavior */
/* Basic columns */ /* Basic columns */
enum Columns { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; enum Columns : std::uint8_t { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, FileNameColumn, NumColumns };
QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } QStringList columnNames(bool translated = true) const { return translated ? m_columnNamesTranslated : m_columnNames; }
int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); } int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast<int>(size()); }
int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NumColumns; }
Qt::DropActions supportedDropActions() const override; Qt::DropActions supportedDropActions() const override;
@ -171,18 +171,19 @@ class ResourceFolderModel : public QAbstractListModel {
QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
SortType columnToSortKey(size_t column) const; SortType columnToSortKey(size_t column) const;
QList<QHeaderView::ResizeMode> columnResizeModes() const { return m_column_resize_modes; } QList<QHeaderView::ResizeMode> columnResizeModes() const { return m_columnResizeModes; }
class ProxyModel : public QSortFilterProxyModel { class ProxyModel : public QSortFilterProxyModel {
public: public:
explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
protected: protected:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight) const override;
}; };
QString instDirPath() const; QString instDirPath() const;
BaseInstance* instance() const { return m_instance; }
signals: signals:
void updateFinished(); void updateFinished();
@ -206,7 +207,7 @@ class ResourceFolderModel : public QAbstractListModel {
* This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed
* in the background, so it slowly updates the UI as tasks get done. * in the background, so it slowly updates the UI as tasks get done.
*/ */
[[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; } [[nodiscard]] virtual Task* createParseTask(Resource& /*unused*/) { return nullptr; }
/** Standard implementation of the model update logic. /** Standard implementation of the model update logic.
* *
@ -214,10 +215,10 @@ class ResourceFolderModel : public QAbstractListModel {
* to act only on those disparities. * to act only on those disparities.
* *
*/ */
void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, Resource::Ptr>& new_resources); void applyUpdates(QSet<QString>& currentSet, QSet<QString>& newSet, QMap<QString, Resource::Ptr>& newResources);
protected slots: protected slots:
void directoryChanged(QString); void directoryChanged(const QString&);
/** Called when the update task is successful. /** Called when the update task is successful.
* *
@ -233,39 +234,40 @@ class ResourceFolderModel : public QAbstractListModel {
* This is just a simple reference implementation. You probably want to override it with your own logic in a subclass * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass
* if the resource is complex and has more stuff to parse. * if the resource is complex and has more stuff to parse.
*/ */
virtual void onParseSucceeded(int ticket, QString resource_id); virtual void onParseSucceeded(int ticket, const QString& resourceId);
virtual void onParseFailed(int ticket, QString resource_id); virtual void onParseFailed(int ticket, const QString& resourceId);
protected: protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key. // Represents the relationship between a column's index (represented by the list index), and it's sorting key.
// As such, the order in with they appear is very important! // As such, the order in with they appear is very important!
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; QList<SortType> m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Date,
QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; SortType::Provider, SortType::Size, SortType::Filename };
QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }; QStringList m_columnNames = { "Enable", "Name", "Last Modified", "Provider", "Size", "File Name" };
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QStringList m_columnNamesTranslated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") };
QHeaderView::Interactive, QHeaderView::Interactive }; QList<QHeaderView::ResizeMode> m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QList<bool> m_columnsHideable = { false, false, true, true, true }; QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
QList<bool> m_columnsHideable = { false, false, true, true, true, true };
QDir m_dir; QDir m_dir;
BaseInstance* m_instance; BaseInstance* m_instance;
QFileSystemWatcher m_watcher; QFileSystemWatcher m_watcher;
bool m_is_watching = false; bool m_isWatching = false;
bool m_is_indexed; bool m_isIndexed;
bool m_first_folder_load = true; bool m_firstFolderLoad = true;
Task::Ptr m_current_update_task = nullptr; Task::Ptr m_currentUpdateTask = nullptr;
bool m_scheduled_update = false; bool m_scheduledUpdate = false;
QList<Resource::Ptr> m_resources; QList<Resource::Ptr> m_resources;
// Represents the relationship between a resource's internal ID and it's row position on the model. // Represents the relationship between a resource's internal ID and it's row position on the model.
QMap<QString, int> m_resources_index; QMap<QString, int> m_resourcesIndex;
// Runs off-thread // Runs off-thread
ConcurrentTask m_resourceResolver; ConcurrentTask m_resourceResolver;
bool m_resourceResolverRunning = false; bool m_resourceResolverRunning = false;
QMap<int, Task::Ptr> m_active_parse_tasks; QMap<int, Task::Ptr> m_activeParseTasks;
std::atomic<int> m_next_resolution_ticket = 0; std::atomic<int> m_nextResolutionTicket = 0;
}; };

View file

@ -39,27 +39,26 @@
#include <QIcon> #include <QIcon>
#include <QStyle> #include <QStyle>
#include "Version.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h"
ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent)
: ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) : ResourceFolderModel(dir, instance, isIndexed, createDir, parent)
{ {
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); m_columnNames = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size", "File Name" });
m_column_names_translated = m_columnNamesTranslated = QStringList(
QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); { tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::PackFormat,
SortType::DATE, SortType::PROVIDER, SortType::SIZE }; SortType::Date, SortType::Provider, SortType::Size, SortType::Filename };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true, true }; m_columnsHideable = { false, true, false, true, true, true, true, true };
} }
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
{ {
if (!validateIndex(index)) if (!validateIndex(index)) {
return {}; return {};
}
int row = index.row(); int row = index.row();
int column = index.column(); int column = index.column();
@ -92,6 +91,8 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
return QSize(32, 32); return QSize(32, 32);
} }
break; break;
default:
break;
} }
// map the columns to the base equivilents // map the columns to the base equivilents
@ -112,6 +113,11 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
case SizeColumn: case SizeColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn);
break; break;
case FileNameColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn);
break;
default:
break;
} }
if (mappedIndex.isValid()) { if (mappedIndex.isValid()) {
@ -133,6 +139,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
case ImageColumn: case ImageColumn:
case ProviderColumn: case ProviderColumn:
case SizeColumn: case SizeColumn:
case FileNameColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return {}; return {};
@ -153,6 +160,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
return tr("The source provider of the resource pack."); return tr("The source provider of the resource pack.");
case SizeColumn: case SizeColumn:
return tr("The size of the resource pack."); return tr("The size of the resource pack.");
case FileNameColumn:
return tr("The file name of the resource pack.");
default: default:
return {}; return {};
} }
@ -168,10 +177,10 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O
int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
{ {
return parent.isValid() ? 0 : NUM_COLUMNS; return parent.isValid() ? 0 : NumColumns;
} }
Task* ResourcePackFolderModel::createParseTask(Resource& resource) Task* ResourcePackFolderModel::createParseTask(Resource& resource)
{ {
return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast<ResourcePack*>(&resource)); return new LocalDataPackParseTask(m_nextResolutionTicket, dynamic_cast<ResourcePack*>(&resource));
} }

View file

@ -7,9 +7,19 @@
class ResourcePackFolderModel : public ResourceFolderModel { class ResourcePackFolderModel : public ResourceFolderModel {
Q_OBJECT Q_OBJECT
public: public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; enum Columns : std::uint8_t {
ActiveColumn = 0,
ImageColumn,
NameColumn,
PackFormatColumn,
DateColumn,
ProviderColumn,
SizeColumn,
FileNameColumn,
NumColumns
};
explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr);
QString id() const override { return "resourcepacks"; } QString id() const override { return "resourcepacks"; }
@ -19,7 +29,7 @@ class ResourcePackFolderModel : public ResourceFolderModel {
int columnCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override;
[[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); } [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); }
[[nodiscard]] Task* createParseTask(Resource&) override; [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override;
RESOURCE_HELPERS(ResourcePack) RESOURCE_HELPERS(ResourcePack)
}; };

View file

@ -18,7 +18,7 @@ class ShaderPackFolderModel : public ResourceFolderModel {
[[nodiscard]] Task* createParseTask(Resource& resource) override [[nodiscard]] Task* createParseTask(Resource& resource) override
{ {
return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast<ShaderPack&>(resource)); return new LocalShaderPackParseTask(m_nextResolutionTicket, static_cast<ShaderPack&>(resource));
} }
QDir indexDir() const override { return m_dir; } QDir indexDir() const override { return m_dir; }

View file

@ -36,28 +36,30 @@
#include "TexturePackFolderModel.h" #include "TexturePackFolderModel.h"
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
#include "minecraft/mod/tasks/ResourceFolderLoadTask.h"
TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent)
: ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) : ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent)
{ {
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); m_columnNames = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size", "File Name" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); m_columnNamesTranslated =
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") });
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::Date,
SortType::Provider, SortType::Size, SortType::Filename };
m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true }; m_columnsHideable = { false, true, false, true, true, true, true };
} }
Task* TexturePackFolderModel::createParseTask(Resource& resource) Task* TexturePackFolderModel::createParseTask(Resource& resource)
{ {
return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource)); return new LocalTexturePackParseTask(m_nextResolutionTicket, static_cast<TexturePack&>(resource));
} }
QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
{ {
if (!validateIndex(index)) if (!validateIndex(index)) {
return {}; return {};
}
int row = index.row(); int row = index.row();
int column = index.column(); int column = index.column();
@ -76,6 +78,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
return QSize(32, 32); return QSize(32, 32);
} }
break; break;
default:
break;
} }
// map the columns to the base equivilents // map the columns to the base equivilents
@ -96,6 +100,11 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
case SizeColumn: case SizeColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn);
break; break;
case FileNameColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn);
break;
default:
break;
} }
if (mappedIndex.isValid()) { if (mappedIndex.isValid()) {
@ -116,6 +125,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
case ImageColumn: case ImageColumn:
case ProviderColumn: case ProviderColumn:
case SizeColumn: case SizeColumn:
case FileNameColumn:
return columnNames().at(section); return columnNames().at(section);
default: default:
return {}; return {};
@ -132,6 +142,8 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
return tr("The source provider of the texture pack."); return tr("The source provider of the texture pack.");
case SizeColumn: case SizeColumn:
return tr("The size of the texture pack."); return tr("The size of the texture pack.");
case FileNameColumn:
return tr("The file name of the texture pack.");
default: default:
return {}; return {};
} }
@ -145,5 +157,5 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
int TexturePackFolderModel::columnCount(const QModelIndex& parent) const int TexturePackFolderModel::columnCount(const QModelIndex& parent) const
{ {
return parent.isValid() ? 0 : NUM_COLUMNS; return parent.isValid() ? 0 : NumColumns;
} }

View file

@ -44,11 +44,20 @@ class TexturePackFolderModel : public ResourceFolderModel {
Q_OBJECT Q_OBJECT
public: public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; enum Columns : std::uint8_t {
ActiveColumn = 0,
ImageColumn,
NameColumn,
DateColumn,
ProviderColumn,
SizeColumn,
FileNameColumn,
NumColumns
};
explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr);
virtual QString id() const override { return "texturepacks"; } QString id() const override { return "texturepacks"; }
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
@ -56,7 +65,7 @@ class TexturePackFolderModel : public ResourceFolderModel {
int columnCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override;
[[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); } [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); }
[[nodiscard]] Task* createParseTask(Resource&) override; [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override;
RESOURCE_HELPERS(TexturePack) RESOURCE_HELPERS(TexturePack)
}; };

View file

@ -41,26 +41,28 @@
#include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/MetadataHandler.h"
#include <QThread> #include <QThread>
#include <utility>
ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resourceDir,
const QDir& index_dir, const QDir& indexDir,
bool is_indexed, bool isIndexed,
bool clean_orphan, bool cleanOrphan,
std::function<Resource*(const QFileInfo&)> create_function) std::function<Resource*(const QFileInfo&)> createFunction)
: Task(false) : Task(false)
, m_resource_dir(resource_dir) , m_resource_dir(resourceDir)
, m_index_dir(index_dir) , m_index_dir(indexDir)
, m_is_indexed(is_indexed) , m_is_indexed(isIndexed)
, m_clean_orphan(clean_orphan) , m_clean_orphan(cleanOrphan)
, m_create_func(create_function) , m_create_func(std::move(createFunction))
, m_result(new Result()) , m_result(new Result())
, m_thread_to_spawn_into(thread()) , m_thread_to_spawn_into(thread())
{} {}
void ResourceFolderLoadTask::executeTask() void ResourceFolderLoadTask::executeTask()
{ {
if (thread() != m_thread_to_spawn_into) if (thread() != m_thread_to_spawn_into) {
connect(this, &Task::finished, this->thread(), &QThread::quit); connect(this, &Task::finished, this->thread(), &QThread::quit);
}
if (m_is_indexed) { if (m_is_indexed) {
// Read metadata first // Read metadata first
@ -71,7 +73,7 @@ void ResourceFolderLoadTask::executeTask()
m_resource_dir.refresh(); m_resource_dir.refresh();
for (auto entry : m_resource_dir.entryInfoList()) { for (auto entry : m_resource_dir.entryInfoList()) {
auto filePath = entry.absoluteFilePath(); auto filePath = entry.absoluteFilePath();
if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { if (auto* app = APPLICATION_DYN; (app != nullptr) && app->checkQSavePath(filePath)) {
continue; continue;
} }
auto newFilePath = FS::getUniqueResourceName(filePath); auto newFilePath = FS::getUniqueResourceName(filePath);
@ -83,29 +85,29 @@ void ResourceFolderLoadTask::executeTask()
Resource* resource = m_create_func(entry); Resource* resource = m_create_func(entry);
if (resource->enabled()) { if (resource->enabled()) {
if (m_result->resources.contains(resource->internal_id())) { if (m_result->resources.contains(resource->internalId())) {
m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); m_result->resources[resource->internalId()]->setStatus(ResourceStatus::Installed);
// Delete the object we just created, since a valid one is already in the mods list. // Delete the object we just created, since a valid one is already in the mods list.
delete resource; delete resource;
} else { } else {
m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internalId()].reset(resource);
m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); m_result->resources[resource->internalId()]->setStatus(ResourceStatus::NoMetadata);
} }
} else { } else {
QString chopped_id = resource->internal_id().chopped(9); QString choppedId = resource->internalId().chopped(9);
if (m_result->resources.contains(chopped_id)) { if (m_result->resources.contains(choppedId)) {
m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internalId()].reset(resource);
auto metadata = m_result->resources[chopped_id]->metadata(); auto metadata = m_result->resources[choppedId]->metadata();
if (metadata) { if (metadata) {
resource->setMetadata(*metadata); resource->setMetadata(*metadata);
m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); m_result->resources[resource->internalId()]->setStatus(ResourceStatus::Installed);
m_result->resources.remove(chopped_id); m_result->resources.remove(choppedId);
} }
} else { } else {
m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internalId()].reset(resource);
m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); m_result->resources[resource->internalId()]->setStatus(ResourceStatus::NoMetadata);
} }
} }
} }
@ -116,38 +118,41 @@ void ResourceFolderLoadTask::executeTask()
QMutableMapIterator iter(m_result->resources); QMutableMapIterator iter(m_result->resources);
while (iter.hasNext()) { while (iter.hasNext()) {
auto resource = iter.next().value(); auto resource = iter.next().value();
if (resource->status() == ResourceStatus::NOT_INSTALLED) { if (resource->status() == ResourceStatus::NotInstalled) {
resource->destroy(m_index_dir, false, false); resource->destroy(m_index_dir, false, false);
iter.remove(); iter.remove();
} }
} }
} }
for (auto mod : m_result->resources) for (const auto& mod : m_result->resources) {
mod->moveToThread(m_thread_to_spawn_into); mod->moveToThread(m_thread_to_spawn_into);
}
if (m_aborted) if (m_aborted) {
emit finished(); emit finished();
else } else {
emitSucceeded(); emitSucceeded();
} }
}
void ResourceFolderLoadTask::getFromMetadata() void ResourceFolderLoadTask::getFromMetadata()
{ {
m_index_dir.refresh(); m_index_dir.refresh();
for (auto entry : m_index_dir.entryList(QDir::Files)) { for (const auto& entry : m_index_dir.entryList(QDir::Files)) {
if (!entry.endsWith(".pw.toml")) { if (!entry.endsWith(".pw.toml")) {
continue; continue;
} }
auto metadata = Metadata::get(m_index_dir, entry); auto metadata = Metadata::get(m_index_dir, entry);
if (!metadata.isValid()) if (!metadata.isValid()) {
continue; continue;
}
auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename)));
resource->setMetadata(metadata); resource->setMetadata(metadata);
resource->setStatus(ResourceStatus::NOT_INSTALLED); resource->setStatus(ResourceStatus::NotInstalled);
m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internalId()].reset(resource);
} }
} }

View file

@ -41,7 +41,7 @@
#include <QObject> #include <QObject>
#include <QRunnable> #include <QRunnable>
#include <memory> #include <memory>
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Resource.h"
#include "tasks/Task.h" #include "tasks/Task.h"
class ResourceFolderLoadTask : public Task { class ResourceFolderLoadTask : public Task {
@ -54,11 +54,11 @@ class ResourceFolderLoadTask : public Task {
ResultPtr result() const { return m_result; } ResultPtr result() const { return m_result; }
public: public:
ResourceFolderLoadTask(const QDir& resource_dir, ResourceFolderLoadTask(const QDir& resourceDir,
const QDir& index_dir, const QDir& indexDir,
bool is_indexed, bool isIndexed,
bool clean_orphan, bool cleanOrphan,
std::function<Resource*(const QFileInfo&)> create_function); std::function<Resource*(const QFileInfo&)> createFunction);
bool canAbort() const override { return true; } bool canAbort() const override { return true; }
bool abort() override bool abort() override
@ -76,7 +76,7 @@ class ResourceFolderLoadTask : public Task {
QDir m_resource_dir, m_index_dir; QDir m_resource_dir, m_index_dir;
bool m_is_indexed; bool m_is_indexed;
bool m_clean_orphan; bool m_clean_orphan;
std::function<Resource*(QFileInfo const&)> m_create_func; std::function<Resource*(const QFileInfo&)> m_create_func;
ResultPtr m_result; ResultPtr m_result;
std::atomic<bool> m_aborted = false; std::atomic<bool> m_aborted = false;

View file

@ -121,7 +121,7 @@ bool SkinList::update()
auto folderContents = m_dir.entryInfoList(); auto folderContents = m_dir.entryInfoList();
// if there are any untracked files... // if there are any untracked files...
for (QFileInfo entry : folderContents) { for (QFileInfo entry : folderContents) {
if (!entry.isFile() && entry.suffix() != "png") if (!entry.isFile() || entry.suffix() != "png")
continue; continue;
SkinModel w(entry.absoluteFilePath()); SkinModel w(entry.absoluteFilePath());

View file

@ -15,15 +15,15 @@ class CheckUpdateTask : public Task {
std::vector<Version>& mcVersions, std::vector<Version>& mcVersions,
QList<ModPlatform::ModLoaderType> loadersList, QList<ModPlatform::ModLoaderType> loadersList,
ResourceFolderModel* resourceModel) ResourceFolderModel* resourceModel)
: Task(), m_resources(resources), m_gameVersions(mcVersions), m_loadersList(std::move(loadersList)), m_resourceModel(resourceModel) : m_resources(resources), m_gameVersions(mcVersions), m_loadersList(std::move(loadersList)), m_resourceModel(resourceModel)
{} {}
struct Update { struct Update {
QString name; QString name;
QString old_hash; QString oldHash;
QString old_version; QString oldVersion;
QString new_version; QString newVersion;
std::optional<ModPlatform::IndexedVersionType> new_version_type; std::optional<ModPlatform::IndexedVersionType> newVersionType;
QString changelog; QString changelog;
ModPlatform::ResourceProvider provider; ModPlatform::ResourceProvider provider;
shared_qobject_ptr<ResourceDownloadTask> download; shared_qobject_ptr<ResourceDownloadTask> download;
@ -31,19 +31,19 @@ class CheckUpdateTask : public Task {
public: public:
Update(QString name, Update(QString name,
QString old_h, QString oldH,
QString old_v, QString oldV,
QString new_v, QString newV,
std::optional<ModPlatform::IndexedVersionType> new_v_type, std::optional<ModPlatform::IndexedVersionType> newVType,
QString changelog, QString changelog,
ModPlatform::ResourceProvider p, ModPlatform::ResourceProvider p,
shared_qobject_ptr<ResourceDownloadTask> t, shared_qobject_ptr<ResourceDownloadTask> t,
bool enabled = true) bool enabled = true)
: name(std::move(name)) : name(std::move(name))
, old_hash(std::move(old_h)) , oldHash(std::move(oldH))
, old_version(std::move(old_v)) , oldVersion(std::move(oldV))
, new_version(std::move(new_v)) , newVersion(std::move(newV))
, new_version_type(std::move(new_v_type)) , newVersionType(newVType)
, changelog(std::move(changelog)) , changelog(std::move(changelog))
, provider(p) , provider(p)
, download(std::move(t)) , download(std::move(t))
@ -54,14 +54,11 @@ class CheckUpdateTask : public Task {
auto getUpdates() -> std::vector<Update>&& { return std::move(m_updates); } auto getUpdates() -> std::vector<Update>&& { return std::move(m_updates); }
auto getDependencies() -> QList<std::shared_ptr<GetModDependenciesTask::PackDependency>>&& { return std::move(m_deps); } auto getDependencies() -> QList<std::shared_ptr<GetModDependenciesTask::PackDependency>>&& { return std::move(m_deps); }
public slots:
bool abort() override = 0;
protected slots: protected slots:
void executeTask() override = 0; void executeTask() override = 0;
signals: signals:
void checkFailed(Resource* failed, QString reason, QUrl recover_url = {}); void checkFailed(Resource* failed, QString reason, QUrl recoverUrl = {});
protected: protected:
QList<Resource*>& m_resources; QList<Resource*>& m_resources;

View file

@ -99,7 +99,7 @@ void EnsureMetadataTask::executeTask()
} }
// They already have the right metadata :o // They already have the right metadata :o
if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata() && resource->metadata()->provider == m_provider) { if (resource->status() != ResourceStatus::NoMetadata && resource->metadata() && resource->metadata()->provider == m_provider) {
qDebug() << "Resource" << resource->name() << "already has metadata!"; qDebug() << "Resource" << resource->name() << "already has metadata!";
emitReady(resource); emitReady(resource);
continue; continue;
@ -263,7 +263,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
Task::Ptr EnsureMetadataTask::modrinthProjectsTask() Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
{ {
QHash<QString, QString> addonIds; QHash<QString, QString> addonIds;
for (auto const& data : m_tempVersions) for (const auto& data : m_tempVersions)
addonIds.insert(data.addonId.toString(), data.hash); addonIds.insert(data.addonId.toString(), data.hash);
Task::Ptr proj_task; Task::Ptr proj_task;
@ -404,7 +404,7 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask()
Task::Ptr EnsureMetadataTask::flameProjectsTask() Task::Ptr EnsureMetadataTask::flameProjectsTask()
{ {
QHash<QString, QString> addonIds; QHash<QString, QString> addonIds;
for (auto const& hash : m_resources.keys()) { for (const auto& hash : m_resources.keys()) {
if (m_tempVersions.contains(hash)) { if (m_tempVersions.contains(hash)) {
auto data = m_tempVersions.find(hash).value(); auto data = m_tempVersions.find(hash).value();

View file

@ -32,6 +32,7 @@ class QIODevice;
namespace ModPlatform { namespace ModPlatform {
enum class ModLoaderType : std::uint16_t { enum class ModLoaderType : std::uint16_t {
None = 0U,
NeoForge = 1U << 0U, NeoForge = 1U << 0U,
Forge = 1U << 1U, Forge = 1U << 1U,
Cauldron = 1U << 2U, Cauldron = 1U << 2U,

View file

@ -148,9 +148,9 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
return netJob; return netJob;
} }
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks) const Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks, bool askRetry) const
{ {
auto [job, response] = getProject(args.pack->addonId.toString()); auto [job, response] = getProject(args.pack->addonId.toString(), askRetry);
QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] {
auto pack = args.pack; auto pack = args.pack;
@ -284,7 +284,7 @@ QString ResourceAPI::mapMCVersionToModrinth(Version v) const
return verStr; return verStr;
} }
std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId) const std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId, bool askRetry) const
{ {
auto project_url_optional = getInfoURL(addonId); auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value()) if (!project_url_optional.has_value())
@ -293,6 +293,7 @@ std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId) const
auto project_url = project_url_optional.value(); auto project_url = project_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network()); auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network());
netJob->setAskRetry(askRetry);
auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url)); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url));
netJob->addNetAction(action); netJob->addNetAction(action);

View file

@ -115,10 +115,10 @@ class ResourceAPI {
public slots: public slots:
virtual Task::Ptr searchProjects(SearchArgs&&, Callback<QList<ModPlatform::IndexedPack::Ptr>>&&) const; virtual Task::Ptr searchProjects(SearchArgs&&, Callback<QList<ModPlatform::IndexedPack::Ptr>>&&) const;
virtual std::pair<Task::Ptr, QByteArray*> getProject(QString addonId) const; virtual std::pair<Task::Ptr, QByteArray*> getProject(QString addonId, bool askRetry = true) const;
virtual std::pair<Task::Ptr, QByteArray*> getProjects(QStringList addonIds) const = 0; virtual std::pair<Task::Ptr, QByteArray*> getProjects(QStringList addonIds) const = 0;
virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack::Ptr>&&) const; virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack::Ptr>&&, bool askRetry = true) const;
Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const; Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const; virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;

View file

@ -38,6 +38,7 @@
#include <QtConcurrent> #include <QtConcurrent>
#include <algorithm> #include <algorithm>
#include <utility>
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h" #include "Json.h"
@ -59,18 +60,28 @@
#include "BuildConfig.h" #include "BuildConfig.h"
#include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/BlockedModsDialog.h"
namespace {
bool isPathTraversal(const QString& basePath, const QString& entryName)
{
auto safeName = FS::RemoveInvalidPathChars(entryName);
auto fullPath = FS::PathCombine(basePath, safeName);
auto baseUrl = QUrl::fromLocalFile(basePath);
return !baseUrl.isParentOf(QUrl::fromLocalFile(fullPath));
}
Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version)
{
return APPLICATION->metadataIndex()->getLoadedVersion(uid, version);
}
} // namespace
namespace ATLauncher { namespace ATLauncher {
static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version);
PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packName, QString version, InstallMode installMode) PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packName, QString version, InstallMode installMode)
: m_support(support), m_install_mode(installMode), m_pack_name(packName), m_version_name(std::move(version))
{ {
m_support = support;
m_pack_name = packName;
static const QRegularExpression s_regex("[^A-Za-z0-9]"); static const QRegularExpression s_regex("[^A-Za-z0-9]");
m_pack_safe_name = packName.replace(s_regex, ""); m_pack_safe_name = packName.replace(s_regex, "");
m_version_name = version;
m_install_mode = installMode;
} }
bool PackInstallTask::abort() bool PackInstallTask::abort()
@ -107,11 +118,10 @@ void PackInstallTask::onDownloadSucceeded(QByteArray* responsePtr)
QByteArray response = std::move(*responsePtr); QByteArray response = std::move(*responsePtr);
jobPtr.reset(); jobPtr.reset();
QJsonParseError parse_error{}; QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(response, &parseError);
if (parse_error.error != QJsonParseError::NoError) { if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ATLauncher at" << parse_error.offset qWarning() << "Error while parsing JSON response from ATLauncher at" << parseError.offset << "reason:" << parseError.errorString();
<< "reason:" << parse_error.errorString();
qWarning() << response; qWarning() << response;
return; return;
} }
@ -128,7 +138,7 @@ void PackInstallTask::onDownloadSucceeded(QByteArray* responsePtr)
// Derived from the installation mode // Derived from the installation mode
QString message; QString message;
bool resetDirectory; bool resetDirectory = false;
switch (m_install_mode) { switch (m_install_mode) {
case InstallMode::Reinstall: case InstallMode::Reinstall:
@ -148,8 +158,9 @@ void PackInstallTask::onDownloadSucceeded(QByteArray* responsePtr)
} }
// Display message if one exists // Display message if one exists
if (!message.isEmpty()) if (!message.isEmpty()) {
m_support->displayMessage(message); m_support->displayMessage(message);
}
auto ver = getComponentVersion("net.minecraft", m_version.minecraft); auto ver = getComponentVersion("net.minecraft", m_version.minecraft);
if (!ver) { if (!ver) {
@ -173,7 +184,7 @@ void PackInstallTask::onDownloadFailed(QString reason)
{ {
qDebug() << "PackInstallTask::onDownloadFailed:" << QThread::currentThreadId(); qDebug() << "PackInstallTask::onDownloadFailed:" << QThread::currentThreadId();
jobPtr.reset(); jobPtr.reset();
emitFailed(reason); emitFailed(std::move(reason));
} }
void PackInstallTask::onDownloadAborted() void PackInstallTask::onDownloadAborted()
@ -202,26 +213,30 @@ void PackInstallTask::deleteExistingFiles()
keeps.files.append(VersionKeep{ "root", "servers.dat" }); keeps.files.append(VersionKeep{ "root", "servers.dat" });
// Merge with version deletes and keeps // Merge with version deletes and keeps
for (const auto& item : m_version.deletes.files) for (const auto& item : m_version.deletes.files) {
deletes.files.append(item); deletes.files.append(item);
for (const auto& item : m_version.deletes.folders) }
for (const auto& item : m_version.deletes.folders) {
deletes.folders.append(item); deletes.folders.append(item);
for (const auto& item : m_version.keeps.files) }
for (const auto& item : m_version.keeps.files) {
keeps.files.append(item); keeps.files.append(item);
for (const auto& item : m_version.keeps.folders) }
for (const auto& item : m_version.keeps.folders) {
keeps.folders.append(item); keeps.folders.append(item);
}
auto getPathForBase = [this](const QString& base) { auto getPathForBase = [this](const QString& base) {
auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft"); auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft");
if (base == "root") { if (base == "root") {
return minecraftPath; return minecraftPath;
} else if (base == "config") { }
if (base == "config") {
return FS::PathCombine(minecraftPath, "config"); return FS::PathCombine(minecraftPath, "config");
} else { }
qWarning() << "Unrecognised base path" << base; qWarning() << "Unrecognised base path" << base;
return minecraftPath; return minecraftPath;
}
}; };
auto convertToSystemPath = [](const QString& path) { auto convertToSystemPath = [](const QString& path) {
@ -231,25 +246,23 @@ void PackInstallTask::deleteExistingFiles()
}; };
auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) { auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) {
for (const auto& item : keeps.files) { if (std::ranges::any_of(keeps.files, [&fullPath, &getPathForBase, &convertToSystemPath](const auto& item) {
auto basePath = getPathForBase(item.base); auto basePath = getPathForBase(item.base);
auto targetPath = convertToSystemPath(item.target); auto targetPath = convertToSystemPath(item.target);
auto path = FS::PathCombine(basePath, targetPath); auto path = FS::PathCombine(basePath, targetPath);
return fullPath == path;
if (fullPath == path) { })) {
return true; return true;
} }
}
for (const auto& item : keeps.folders) { if (std::ranges::any_of(keeps.folders, [&fullPath, &getPathForBase, &convertToSystemPath](const auto& item) {
auto basePath = getPathForBase(item.base); auto basePath = getPathForBase(item.base);
auto targetPath = convertToSystemPath(item.target); auto targetPath = convertToSystemPath(item.target);
auto path = FS::PathCombine(basePath, targetPath); auto path = FS::PathCombine(basePath, targetPath);
return fullPath.startsWith(path);
if (fullPath.startsWith(path)) { })) {
return true; return true;
} }
}
return false; return false;
}; };
@ -262,8 +275,9 @@ void PackInstallTask::deleteExistingFiles()
auto targetPath = convertToSystemPath(item.target); auto targetPath = convertToSystemPath(item.target);
auto fullPath = FS::PathCombine(basePath, targetPath); auto fullPath = FS::PathCombine(basePath, targetPath);
if (shouldKeep(fullPath)) if (shouldKeep(fullPath)) {
continue; continue;
}
filesToDelete.insert(fullPath); filesToDelete.insert(fullPath);
} }
@ -277,8 +291,9 @@ void PackInstallTask::deleteExistingFiles()
while (it.hasNext()) { while (it.hasNext()) {
auto path = it.next(); auto path = it.next();
if (shouldKeep(path)) if (shouldKeep(path)) {
continue; continue;
}
filesToDelete.insert(path); filesToDelete.insert(path);
} }
@ -290,7 +305,7 @@ void PackInstallTask::deleteExistingFiles()
} }
} }
QString PackInstallTask::getDirForModType(ModType type, QString raw) QString PackInstallTask::getDirForModType(ModType type, const QString& raw)
{ {
switch (type) { switch (type) {
// Mod types that can either be ignored at this stage, or ignored // Mod types that can either be ignored at this stage, or ignored
@ -338,7 +353,7 @@ QString PackInstallTask::getDirForModType(ModType type, QString raw)
return Q_NULLPTR; return Q_NULLPTR;
} }
QString PackInstallTask::getVersionForLoader(QString uid) QString PackInstallTask::getVersionForLoader(const QString& uid)
{ {
if (m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) { if (m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) {
auto vlist = APPLICATION->metadataIndex()->get(uid); auto vlist = APPLICATION->metadataIndex()->get(uid);
@ -359,24 +374,28 @@ QString PackInstallTask::getVersionForLoader(QString uid)
// filtering for those loaders. // filtering for those loaders.
if (m_version.loader.type != "fabric") { if (m_version.loader.type != "fabric") {
auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) { return req.uid == "net.minecraft"; }); auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) { return req.uid == "net.minecraft"; });
if (iter == reqs.end()) if (iter == reqs.end()) {
continue; continue;
if (iter->equalsVersion != m_version.minecraft) }
if (iter->equalsVersion != m_version.minecraft) {
continue; continue;
} }
}
if (m_version.loader.recommended) { if (m_version.loader.recommended) {
// first recommended build we find, we use. // first recommended build we find, we use.
if (!version->isRecommended()) if (!version->isRecommended()) {
continue; continue;
} }
}
return version->descriptor(); return version->descriptor();
} }
emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type));
return Q_NULLPTR; return Q_NULLPTR;
} else if (m_version.loader.choose) { }
if (m_version.loader.choose) {
// Fabric Loader doesn't depend on a given Minecraft version. // Fabric Loader doesn't depend on a given Minecraft version.
if (m_version.loader.type == "fabric") { if (m_version.loader.type == "fabric") {
return m_support->chooseVersion(vlist, Q_NULLPTR); return m_support->chooseVersion(vlist, Q_NULLPTR);
@ -420,7 +439,8 @@ QString PackInstallTask::detectLibrary(const VersionLibrary& library)
if (name == QString("guava")) { if (name == QString("guava")) {
return "com.google.guava:guava:" + version; return "com.google.guava:guava:" + version;
} else if (name == QString("commons-lang3")) { }
if (name == QString("commons-lang3")) {
return "org.apache.commons:commons-lang3:" + version; return "org.apache.commons:commons-lang3:" + version;
} }
} }
@ -428,7 +448,7 @@ QString PackInstallTask::detectLibrary(const VersionLibrary& library)
return "org.multimc.atlauncher:" + library.md5 + ":1"; return "org.multimc.atlauncher:" + library.md5 + ":1";
} }
bool PackInstallTask::createLibrariesComponent(QString instanceRoot, PackProfile* profile) bool PackInstallTask::createLibrariesComponent(const QString& instanceRoot, PackProfile* profile)
{ {
if (m_version.libraries.isEmpty()) { if (m_version.libraries.isEmpty()) {
return true; return true;
@ -453,18 +473,18 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, PackProfile
} }
auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); auto id = QUuid::createUuid().toString(QUuid::WithoutBraces);
auto target_id = "org.multimc.atlauncher." + id; auto targetId = "org.multimc.atlauncher." + id;
auto patchDir = FS::PathCombine(instanceRoot, "patches"); auto patchDir = FS::PathCombine(instanceRoot, "patches");
if (!FS::ensureFolderPathExists(patchDir)) { if (!FS::ensureFolderPathExists(patchDir)) {
return false; return false;
} }
auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); auto patchFileName = FS::PathCombine(patchDir, targetId + ".json");
auto f = std::make_shared<VersionFile>(); auto f = std::make_shared<VersionFile>();
f->name = m_pack_name + " " + m_version_name + " (libraries)"; f->name = m_pack_name + " " + m_version_name + " (libraries)";
const static QMap<QString, QString> liteLoaderMap = { const static QMap<QString, QString> s_liteLoaderMap = {
{ "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" },
{ "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" },
{ "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" },
@ -484,8 +504,8 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, PackProfile
for (const auto& lib : m_version.libraries) { for (const auto& lib : m_version.libraries) {
// If the library is LiteLoader, we need to ignore it and handle it separately. // If the library is LiteLoader, we need to ignore it and handle it separately.
if (liteLoaderMap.contains(lib.md5)) { if (s_liteLoaderMap.contains(lib.md5)) {
auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5)); auto ver = getComponentVersion("com.mumfrey.liteloader", s_liteLoaderMap.value(lib.md5));
if (ver) { if (ver) {
componentsToInstall.insert("com.mumfrey.liteloader", ver); componentsToInstall.insert("com.mumfrey.liteloader", ver);
continue; continue;
@ -502,8 +522,9 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, PackProfile
libExempt = Version(libSpecifier.version()) >= Version(existingLib.version()); libExempt = Version(libSpecifier.version()) >= Version(existingLib.version());
} }
} }
if (libExempt) if (libExempt) {
continue; continue;
}
auto library = std::make_shared<Library>(); auto library = std::make_shared<Library>();
library->setRawName(libName); library->setRawName(libName);
@ -536,11 +557,11 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, PackProfile
file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close(); file.close();
profile->appendComponent(ComponentPtr{ new Component(profile, target_id, f) }); profile->appendComponent(ComponentPtr{ new Component(profile, targetId, f) });
return true; return true;
} }
bool PackInstallTask::createPackComponent(QString instanceRoot, PackProfile* profile) bool PackInstallTask::createPackComponent(const QString& instanceRoot, PackProfile* profile)
{ {
if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) {
return true; return true;
@ -571,13 +592,13 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, PackProfile* pro
} }
auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); auto id = QUuid::createUuid().toString(QUuid::WithoutBraces);
auto target_id = "org.multimc.atlauncher." + id; auto targetId = "org.multimc.atlauncher." + id;
auto patchDir = FS::PathCombine(instanceRoot, "patches"); auto patchDir = FS::PathCombine(instanceRoot, "patches");
if (!FS::ensureFolderPathExists(patchDir)) { if (!FS::ensureFolderPathExists(patchDir)) {
return false; return false;
} }
auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); auto patchFileName = FS::PathCombine(patchDir, targetId + ".json");
QStringList mainClasses; QStringList mainClasses;
QStringList tweakers; QStringList tweakers;
@ -604,8 +625,9 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, PackProfile* pro
for (auto arg : args) { for (auto arg : args) {
if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") { if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") {
auto tweakClass = arg.remove("--tweakClass="); auto tweakClass = arg.remove("--tweakClass=");
if (tweakers.contains(tweakClass)) if (tweakers.contains(tweakClass)) {
continue; continue;
}
f->addTweakers.append(tweakClass); f->addTweakers.append(tweakClass);
} }
@ -624,7 +646,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, PackProfile* pro
file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close(); file.close();
profile->appendComponent(ComponentPtr{ new Component(profile, target_id, f) }); profile->appendComponent(ComponentPtr{ new Component(profile, targetId, f) });
return true; return true;
} }
@ -654,7 +676,7 @@ void PackInstallTask::installConfigs()
connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { connect(jobPtr.get(), &NetJob::failed, [this](QString reason) {
abortable = false; abortable = false;
jobPtr.reset(); jobPtr.reset();
emitFailed(reason); emitFailed(std::move(reason));
}); });
connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) {
abortable = true; abortable = true;
@ -711,15 +733,17 @@ void PackInstallTask::downloadMods()
jarmods.clear(); jarmods.clear();
jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network())); jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
QList<VersionMod> blocked_mods; QList<VersionMod> blockedMods;
for (const auto& mod : m_version.mods) { for (const auto& mod : m_version.mods) {
// skip non-client mods // skip non-client mods
if (!mod.client) if (!mod.client) {
continue; continue;
}
// skip optional mods that were not selected // skip optional mods that were not selected
if (mod.optional && !selectedMods.contains(mod.name)) if (mod.optional && !selectedMods.contains(mod.name)) {
continue; continue;
}
QString url; QString url;
switch (mod.download) { switch (mod.download) {
@ -727,7 +751,7 @@ void PackInstallTask::downloadMods()
url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url;
break; break;
case DownloadType::Browser: { case DownloadType::Browser: {
blocked_mods.append(mod); blockedMods.append(mod);
continue; continue;
} }
case DownloadType::Direct: case DownloadType::Direct:
@ -763,8 +787,9 @@ void PackInstallTask::downloadMods()
jobPtr->addNetAction(dl); jobPtr->addNetAction(dl);
} else { } else {
auto relpath = getDirForModType(mod.type, mod.type_raw); auto relpath = getDirForModType(mod.type, mod.type_raw);
if (relpath == Q_NULLPTR) if (relpath == Q_NULLPTR) {
continue; continue;
}
auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName);
entry->setStale(true); entry->setStale(true);
@ -798,49 +823,51 @@ void PackInstallTask::downloadMods()
modsToCopy[entry->getFullPath()] = path; modsToCopy[entry->getFullPath()] = path;
} }
} }
if (!blocked_mods.isEmpty()) { if (!blockedMods.isEmpty()) {
QList<BlockedMod> mods; QList<BlockedMod> mods;
for (auto mod : blocked_mods) { for (const auto& mod : blockedMods) {
BlockedMod blocked_mod; BlockedMod blockedMod;
blocked_mod.name = mod.file; blockedMod.name = mod.file;
blocked_mod.websiteUrl = mod.url; blockedMod.websiteUrl = mod.url;
blocked_mod.hash = mod.md5; blockedMod.hash = mod.md5;
blocked_mod.matched = false; blockedMod.matched = false;
blocked_mod.localPath = ""; blockedMod.localPath = "";
mods.append(blocked_mod); mods.append(blockedMod);
} }
qWarning() << "Blocked mods found, displaying mod list"; qWarning() << "Blocked mods found, displaying mod list";
BlockedModsDialog message_dialog(nullptr, tr("Blocked mods found"), BlockedModsDialog messageDialog(nullptr, tr("Blocked mods found"),
tr("The following files are not available for download in third party launchers.<br/>" tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."), "You will need to manually download them and add them to the instance."),
mods, "md5"); mods, "md5");
message_dialog.setModal(true); messageDialog.setModal(true);
if (message_dialog.exec()) { if (messageDialog.exec() != 0) {
qDebug() << "Post dialog blocked mods list:" << mods; qDebug() << "Post dialog blocked mods list:" << mods;
for (auto blocked : mods) { for (const auto& blocked : mods) {
if (!blocked.matched) { if (!blocked.matched) {
qDebug() << blocked.name << "was not matched to a local file, skipping copy"; qDebug() << blocked.name << "was not matched to a local file, skipping copy";
continue; continue;
} }
auto modIter = std::find_if(blocked_mods.begin(), blocked_mods.end(), auto modIter =
[blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; }); std::ranges::find_if(blockedMods, [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; });
if (modIter == blocked_mods.end()) if (modIter == blockedMods.end()) {
continue; continue;
auto mod = *modIter; }
const auto& mod = *modIter;
if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) {
modsToExtract.insert(blocked.localPath, mod); modsToExtract.insert(blocked.localPath, mod);
} else if (mod.type == ModType::Decomp) { } else if (mod.type == ModType::Decomp) {
modsToDecomp.insert(blocked.localPath, mod); modsToDecomp.insert(blocked.localPath, mod);
} else { } else {
auto relpath = getDirForModType(mod.type, mod.type_raw); auto relpath = getDirForModType(mod.type, mod.type_raw);
if (relpath == Q_NULLPTR) if (relpath == Q_NULLPTR) {
continue; continue;
}
auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file);
@ -918,8 +945,8 @@ bool PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
setStatus(tr("Extracting mods...")); setStatus(tr("Extracting mods..."));
for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) { for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) {
auto& modPath = iter.key(); const auto& modPath = iter.key();
auto& mod = iter.value(); const auto& mod = iter.value();
QString extractToDir; QString extractToDir;
if (mod.type == ModType::Extract) { if (mod.type == ModType::Extract) {
@ -938,6 +965,10 @@ bool PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
folderToExtract = mod.extractFolder; folderToExtract = mod.extractFolder;
static const QRegularExpression s_regex("^/"); static const QRegularExpression s_regex("^/");
folderToExtract.remove(s_regex); folderToExtract.remove(s_regex);
if (isPathTraversal(extractToPath, folderToExtract)) {
qWarning() << "Blocked path traversal in" << mod.extractFolder;
return false;
}
} }
qDebug() << "Extracting " + mod.file + " to " + extractToDir; qDebug() << "Extracting " + mod.file + " to " + extractToDir;
@ -948,13 +979,18 @@ bool PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
} }
for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) {
auto& modPath = iter.key(); const auto& modPath = iter.key();
auto& mod = iter.value(); const auto& mod = iter.value();
auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw);
QDir extractDir(m_stagingPath); QDir extractDir(m_stagingPath);
auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile);
if (isPathTraversal(extractToPath, mod.decompFile)) {
qWarning() << "Blocked path traversal in decompFile" << mod.decompFile;
return false;
}
qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir;
if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) {
qWarning() << "Failed to extract" << mod.decompFile; qWarning() << "Failed to extract" << mod.decompFile;
@ -963,8 +999,8 @@ bool PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
} }
for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) {
auto& from = iter.key(); const auto& from = iter.key();
auto& to = iter.value(); const auto& to = iter.value();
// If the file already exists, assume the mod is the correct copy - and remove // If the file already exists, assume the mod is the correct copy - and remove
// the copy from the Configs.zip // the copy from the Configs.zip
@ -994,7 +1030,7 @@ void PackInstallTask::install()
MinecraftInstance instance(m_globalSettings, std::make_unique<INISettingsObject>(instanceConfigPath), m_stagingPath); MinecraftInstance instance(m_globalSettings, std::make_unique<INISettingsObject>(instanceConfigPath), m_stagingPath);
{ {
SettingsObject::Lock lock(instance.settings()); SettingsObject::Lock lock(instance.settings());
auto components = instance.getPackProfile(); auto* components = instance.getPackProfile();
components->buildingFromScratch(); components->buildingFromScratch();
// Use a component to add libraries BEFORE Minecraft // Use a component to add libraries BEFORE Minecraft
@ -1009,20 +1045,23 @@ void PackInstallTask::install()
// Loader // Loader
if (m_version.loader.type == QString("forge")) { if (m_version.loader.type == QString("forge")) {
auto version = getVersionForLoader("net.minecraftforge"); auto version = getVersionForLoader("net.minecraftforge");
if (version == Q_NULLPTR) if (version == Q_NULLPTR) {
return; return;
}
components->setComponentVersion("net.minecraftforge", version); components->setComponentVersion("net.minecraftforge", version);
} else if (m_version.loader.type == QString("neoforge")) { } else if (m_version.loader.type == QString("neoforge")) {
auto version = getVersionForLoader("net.neoforged"); auto version = getVersionForLoader("net.neoforged");
if (version == Q_NULLPTR) if (version == Q_NULLPTR) {
return; return;
}
components->setComponentVersion("net.neoforged", version); components->setComponentVersion("net.neoforged", version);
} else if (m_version.loader.type == QString("fabric")) { } else if (m_version.loader.type == QString("fabric")) {
auto version = getVersionForLoader("net.fabricmc.fabric-loader"); auto version = getVersionForLoader("net.fabricmc.fabric-loader");
if (version == Q_NULLPTR) if (version == Q_NULLPTR) {
return; return;
}
components->setComponentVersion("net.fabricmc.fabric-loader", version); components->setComponentVersion("net.fabricmc.fabric-loader", version);
} else if (m_version.loader.type != QString()) { } else if (m_version.loader.type != QString()) {
@ -1055,9 +1094,4 @@ void PackInstallTask::install()
emitSucceeded(); emitSucceeded();
} }
static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version)
{
return APPLICATION->metadataIndex()->getLoadedVersion(uid, version);
}
} // namespace ATLauncher } // namespace ATLauncher

View file

@ -44,14 +44,13 @@
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "net/NetJob.h" #include "net/NetJob.h"
#include "settings/INISettingsObject.h"
#include <memory> #include <cstdint>
#include <optional> #include <optional>
namespace ATLauncher { namespace ATLauncher {
enum class InstallMode { enum class InstallMode : std::uint8_t {
Install, Install,
Reinstall, Reinstall,
Update, Update,
@ -86,13 +85,13 @@ class PackInstallTask : public InstanceTask {
QString packName, QString packName,
QString version, QString version,
InstallMode installMode = InstallMode::Install); InstallMode installMode = InstallMode::Install);
virtual ~PackInstallTask() { delete m_support; } ~PackInstallTask() override { delete m_support; }
bool canAbort() const override { return true; } bool canAbort() const override { return true; }
bool abort() override; bool abort() override;
protected: protected:
virtual void executeTask() override; void executeTask() override;
private slots: private slots:
void onDownloadSucceeded(QByteArray* responsePtr); void onDownloadSucceeded(QByteArray* responsePtr);
@ -103,12 +102,12 @@ class PackInstallTask : public InstanceTask {
void onModsExtracted(); void onModsExtracted();
private: private:
QString getDirForModType(ModType type, QString raw); QString getDirForModType(ModType type, const QString& raw);
QString getVersionForLoader(QString uid); QString getVersionForLoader(const QString& uid);
QString detectLibrary(const VersionLibrary& library); static QString detectLibrary(const VersionLibrary& library);
bool createLibrariesComponent(QString instanceRoot, PackProfile* profile); bool createLibrariesComponent(const QString& instanceRoot, PackProfile* profile);
bool createPackComponent(QString instanceRoot, PackProfile* profile); bool createPackComponent(const QString& instanceRoot, PackProfile* profile);
void deleteExistingFiles(); void deleteExistingFiles();
void installConfigs(); void installConfigs();

View file

@ -79,6 +79,7 @@ class FlameAPI : public ResourceAPI {
case ModPlatform::LegacyFabric: case ModPlatform::LegacyFabric:
case ModPlatform::Ornithe: case ModPlatform::Ornithe:
case ModPlatform::Rift: case ModPlatform::Rift:
case ModPlatform::None:
break; // not supported break; // not supported
} }
return 0; return 0;

View file

@ -18,8 +18,6 @@
#include "net/NetJob.h" #include "net/NetJob.h"
#include "tasks/Task.h" #include "tasks/Task.h"
static FlameAPI api;
bool FlameCheckUpdate::abort() bool FlameCheckUpdate::abort()
{ {
bool result = false; bool result = false;
@ -39,7 +37,7 @@ void FlameCheckUpdate::executeTask()
{ {
setStatus(tr("Preparing resources for CurseForge...")); setStatus(tr("Preparing resources for CurseForge..."));
auto netJob = new NetJob("Get latest versions", APPLICATION->network()); auto* netJob = new NetJob("Get latest versions", APPLICATION->network());
connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods); connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods);
connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress); connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress);
@ -48,9 +46,10 @@ void FlameCheckUpdate::executeTask()
for (auto* resource : m_resources) { for (auto* resource : m_resources) {
auto project = std::make_shared<ModPlatform::IndexedPack>(); auto project = std::make_shared<ModPlatform::IndexedPack>();
project->addonId = resource->metadata()->project_id.toString(); project->addonId = resource->metadata()->project_id.toString();
auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions }); auto versionsUrlOptional = FlameAPI().getVersionsURL({ .pack = project, .mcVersions = m_gameVersions });
if (!versionsUrlOptional.has_value()) if (!versionsUrlOptional.has_value()) {
continue; continue;
}
auto [task, response] = Net::ApiDownload::makeByteArray(versionsUrlOptional.value()); auto [task, response] = Net::ApiDownload::makeByteArray(versionsUrlOptional.value());
@ -63,11 +62,11 @@ void FlameCheckUpdate::executeTask()
void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, QByteArray* response) void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, QByteArray* response)
{ {
QJsonParseError parse_error{}; QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parse_error.error != QJsonParseError::NoError) { if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from latest mod version at" << parse_error.offset qWarning() << "Error while parsing JSON response from latest mod version at" << parseError.offset
<< "reason:" << parse_error.errorString(); << "reason:" << parseError.errorString();
qWarning() << *response; qWarning() << *response;
return; return;
} }
@ -88,100 +87,104 @@ void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, QByteArray*
qCritical() << e.what(); qCritical() << e.what();
qDebug() << doc; qDebug() << doc;
} }
auto latest_ver = api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); auto latestVer = FlameAPI().getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty());
setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name()));
if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) { if (!latestVer.has_value() || !latestVer->addonId.isValid()) {
QString reason; QString reason;
if (dynamic_cast<Mod*>(resource) != nullptr) if (dynamic_cast<Mod*>(resource) != nullptr) {
reason = reason =
tr("No valid version found for this resource. It's probably unavailable for the current game " tr("No valid version found for this resource. It's probably unavailable for the current game "
"version / mod loader."); "version / mod loader.");
else } else {
reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); reason = tr("No valid version found for this resource. It's probably unavailable for the current game version.");
}
emit checkFailed(resource, reason); emit checkFailed(resource, reason);
return; return;
} }
if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) { if (latestVer->downloadUrl.isEmpty() && latestVer->fileId != resource->metadata()->file_id) {
m_blocked[resource] = latest_ver->fileId.toString(); m_blocked[resource] = latestVer->fileId.toString();
return; return;
} }
if (!latest_ver->hash.isEmpty() && if (!latestVer->hash.isEmpty() &&
(resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) { (resource->metadata()->hash != latestVer->hash || resource->status() == ResourceStatus::NotInstalled)) {
auto old_version = resource->metadata()->version_number; auto oldVersion = resource->metadata()->version_number;
if (old_version.isEmpty()) { if (oldVersion.isEmpty()) {
if (resource->status() == ResourceStatus::NOT_INSTALLED) if (resource->status() == ResourceStatus::NotInstalled) {
old_version = tr("Not installed"); oldVersion = tr("Not installed");
else } else {
old_version = tr("Unknown"); oldVersion = tr("Unknown");
}
} }
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver.value(), m_resourceModel); auto downloadTask = makeShared<ResourceDownloadTask>(pack, latestVer.value(), m_resourceModel, true, "update");
m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type, m_updates.emplace_back(pack->name, resource->metadata()->hash, oldVersion, latestVer->version, latestVer->version_type,
api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()), FlameAPI().getModFileChangelog(latestVer->addonId.toInt(), latestVer->fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled()); ModPlatform::ResourceProvider::FLAME, downloadTask, resource->enabled());
} }
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value())); m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latestVer.value()));
} }
void FlameCheckUpdate::collectBlockedMods() void FlameCheckUpdate::collectBlockedMods()
{ {
QStringList addonIds; QStringList addonIds;
QHash<QString, Resource*> quickSearch; QHash<QString, Resource*> quickSearch;
for (auto const& resource : m_blocked.keys()) { for (const auto& resource : m_blocked.keys()) {
auto addonId = resource->metadata()->project_id.toString(); auto addonId = resource->metadata()->project_id.toString();
addonIds.append(addonId); addonIds.append(addonId);
quickSearch[addonId] = resource; quickSearch[addonId] = resource;
} }
Task::Ptr projTask; Task::Ptr projTask;
QByteArray* response; QByteArray* response = nullptr;
if (addonIds.isEmpty()) { if (addonIds.isEmpty()) {
emitSucceeded(); emitSucceeded();
return; return;
} else if (addonIds.size() == 1) { }
std::tie(projTask, response) = api.getProject(*addonIds.begin()); if (addonIds.size() == 1) {
std::tie(projTask, response) = FlameAPI().getProject(*addonIds.begin());
} else { } else {
std::tie(projTask, response) = api.getProjects(addonIds); std::tie(projTask, response) = FlameAPI().getProjects(addonIds);
} }
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] { connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] {
QJsonParseError parse_error{}; QJsonParseError parseError{};
auto doc = QJsonDocument::fromJson(*response, &parse_error); auto doc = QJsonDocument::fromJson(*response, &parseError);
if (parse_error.error != QJsonParseError::NoError) { if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame projects task at" << parse_error.offset qWarning() << "Error while parsing JSON response from Flame projects task at" << parseError.offset
<< "reason:" << parse_error.errorString(); << "reason:" << parseError.errorString();
qWarning() << *response; qWarning() << *response;
return; return;
} }
try { try {
QJsonArray entries; QJsonArray entries;
if (addonIds.size() == 1) if (addonIds.size() == 1) {
entries = { Json::requireObject(Json::requireObject(doc), "data") }; entries = { Json::requireObject(Json::requireObject(doc), "data") };
else } else {
entries = Json::requireArray(Json::requireObject(doc), "data"); entries = Json::requireArray(Json::requireObject(doc), "data");
}
for (auto entry : entries) { for (auto entry : entries) {
auto entry_obj = Json::requireObject(entry); auto entryObj = Json::requireObject(entry);
auto id = QString::number(Json::requireInteger(entry_obj, "id")); auto id = QString::number(Json::requireInteger(entryObj, "id"));
auto resource = quickSearch.find(id).value(); auto* resource = quickSearch.find(id).value();
ModPlatform::IndexedPack pack; ModPlatform::IndexedPack pack;
try { try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name()));
FlameMod::loadIndexedPack(pack, entry_obj); FlameMod::loadIndexedPack(pack, entryObj);
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); auto recoverUrl = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]);
emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."),
recover_url); recoverUrl);
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
qDebug() << e.cause(); qDebug() << e.cause();
qDebug() << entries; qDebug() << entries;

View file

@ -23,7 +23,9 @@
namespace ExportToModList { namespace ExportToModList {
enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM };
enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 }; enum OptionalDataValue { None = 0, Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 };
Q_DECLARE_FLAGS(OptionalData, OptionalDataValue)
QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData); QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
QString exportToModList(QList<Mod*> mods, QString lineTemplate); QString exportToModList(QList<Mod*> mods, QString lineTemplate);
} // namespace ExportToModList } // namespace ExportToModList

View file

@ -63,12 +63,12 @@ void PackInstallTask::copySettings()
instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString()); instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString());
} }
auto components = instance.getPackProfile(); auto* components = instance.getPackProfile();
components->buildingFromScratch(); components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); components->setComponentVersion("net.minecraft", m_pack.mcVersion, true);
auto modloader = m_pack.loaderType; auto modloader = m_pack.loaderType;
if (modloader.has_value()) if (modloader.has_value()) {
switch (modloader.value()) { switch (modloader.value()) {
case ModPlatform::NeoForge: { case ModPlatform::NeoForge: {
components->setComponentVersion("net.neoforged", m_pack.loaderVersion, true); components->setComponentVersion("net.neoforged", m_pack.loaderVersion, true);
@ -86,28 +86,16 @@ void PackInstallTask::copySettings()
components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.loaderVersion, true); components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.loaderVersion, true);
break; break;
} }
case ModPlatform::Cauldron: default:
break;
case ModPlatform::LiteLoader:
break;
case ModPlatform::DataPack:
break;
case ModPlatform::Babric:
break;
case ModPlatform::BTA:
break;
case ModPlatform::LegacyFabric:
break;
case ModPlatform::Ornithe:
break;
case ModPlatform::Rift:
break; break;
} }
}
components->saveNow(); components->saveNow();
instance.setName(name()); instance.setName(name());
if (m_instIcon == "default") if (m_instIcon == "default") {
m_instIcon = "ftb_logo"; m_instIcon = "ftb_logo";
}
instance.setIconKey(m_instIcon); instance.setIconKey(m_instIcon);
} }
emitSucceeded(); emitSucceeded();

View file

@ -53,7 +53,7 @@ void ModrinthCheckUpdate::executeTask()
setStatus(tr("Preparing resources for Modrinth...")); setStatus(tr("Preparing resources for Modrinth..."));
setProgress(0, ((m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2) + 1); setProgress(0, ((m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2) + 1);
auto hashing_task = auto hashingTask =
makeShared<ConcurrentTask>("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); makeShared<ConcurrentTask>("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
bool startHasing = false; bool startHasing = false;
for (auto* resource : m_resources) { for (auto* resource : m_resources) {
@ -63,10 +63,11 @@ void ModrinthCheckUpdate::executeTask()
// need to generate a new hash if the current one is innadequate // need to generate a new hash if the current one is innadequate
// (though it will rarely happen, if at all) // (though it will rarely happen, if at all)
if (resource->metadata()->hash_format != m_hashType) { if (resource->metadata()->hash_format != m_hashType) {
auto hash_task = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); auto hashTask = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH);
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_mappings.insert(hash, resource); }); connect(hashTask.get(), &Hashing::Hasher::resultsReady,
connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); [this, resource](const QString& hash) { m_mappings.insert(hash, resource); });
hashing_task->addTask(hash_task); connect(hashTask.get(), &Task::failed, [this] { failed("Failed to generate hash"); });
hashingTask->addTask(hashTask);
startHasing = true; startHasing = true;
} else { } else {
m_mappings.insert(hash, resource); m_mappings.insert(hash, resource);
@ -74,9 +75,9 @@ void ModrinthCheckUpdate::executeTask()
} }
if (startHasing) { if (startHasing) {
connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); connect(hashingTask.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader);
m_job = hashing_task; m_job = hashingTask;
hashing_task->start(); hashingTask->start();
} else { } else {
checkNextLoader(); checkNextLoader();
} }
@ -120,14 +121,14 @@ void ModrinthCheckUpdate::checkVersionsResponse(QByteArray* response, std::optio
setStatus(tr("Parsing the API response from Modrinth...")); setStatus(tr("Parsing the API response from Modrinth..."));
setProgress(m_progress + 1, m_progressTotal); setProgress(m_progress + 1, m_progressTotal);
QJsonParseError parse_error{}; QJsonParseError parseError{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
if (parse_error.error != QJsonParseError::NoError) { if (parseError.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at" << parse_error.offset qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at" << parseError.offset
<< "reason:" << parse_error.errorString(); << "reason:" << parseError.errorString();
qWarning() << *response; qWarning() << *response;
emitFailed(parse_error.errorString()); emitFailed(parseError.errorString());
return; return;
} }
@ -138,11 +139,11 @@ void ModrinthCheckUpdate::checkVersionsResponse(QByteArray* response, std::optio
const QString hash = iter.key(); const QString hash = iter.key();
Resource* resource = iter.value(); Resource* resource = iter.value();
auto project_obj = doc[hash].toObject(); auto projectObj = doc[hash].toObject();
// If the returned project is empty, but we have Modrinth metadata, // If the returned project is empty, but we have Modrinth metadata,
// it means this specific version is not available // it means this specific version is not available
if (project_obj.isEmpty()) { if (projectObj.isEmpty()) {
qDebug() << "Mod" << m_mappings.find(hash).value()->name() << "got an empty response. Hash:" << hash; qDebug() << "Mod" << m_mappings.find(hash).value()->name() << "got an empty response. Hash:" << hash;
++iter; ++iter;
continue; continue;
@ -150,11 +151,11 @@ void ModrinthCheckUpdate::checkVersionsResponse(QByteArray* response, std::optio
// Sometimes a version may have multiple files, one with "forge" and one with "fabric", // Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it // so we may want to filter it
QString loader_filter; QString loaderFilter;
if (loader.has_value() && loader != 0) { if (loader.has_value() && loader != 0) {
auto modLoaders = ModPlatform::modLoaderTypesToList(*loader); auto modLoaders = ModPlatform::modLoaderTypesToList(*loader);
if (!modLoaders.isEmpty()) { if (!modLoaders.isEmpty()) {
loader_filter = ModPlatform::getModLoaderAsString(modLoaders.first()); loaderFilter = ModPlatform::getModLoaderAsString(modLoaders.first());
} }
} }
@ -164,9 +165,9 @@ void ModrinthCheckUpdate::checkVersionsResponse(QByteArray* response, std::optio
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
// Such is the pain of having arbitrary files for a given version .-. // Such is the pain of having arbitrary files for a given version .-.
auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hashType, loader_filter); auto projectVer = Modrinth::loadIndexedPackVersion(projectObj, m_hashType, loaderFilter);
if (project_ver.downloadUrl.isEmpty()) { if (projectVer.downloadUrl.isEmpty()) {
qCritical() << "Modrinth mod without download url!" << project_ver.fileName; qCritical() << "Modrinth mod without download url!" << projectVer.fileName;
++iter; ++iter;
continue; continue;
} }
@ -177,21 +178,22 @@ void ModrinthCheckUpdate::checkVersionsResponse(QByteArray* response, std::optio
pack->slug = resource->metadata()->slug; pack->slug = resource->metadata()->slug;
pack->addonId = resource->metadata()->project_id; pack->addonId = resource->metadata()->project_id;
pack->provider = ModPlatform::ResourceProvider::MODRINTH; pack->provider = ModPlatform::ResourceProvider::MODRINTH;
if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { if ((projectVer.hash != hash && projectVer.is_preferred) || (resource->status() == ResourceStatus::NotInstalled)) {
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_resourceModel); auto downloadTask = makeShared<ResourceDownloadTask>(pack, projectVer, m_resourceModel, true, "update");
QString old_version = resource->metadata()->version_number; QString oldVersion = resource->metadata()->version_number;
if (old_version.isEmpty()) { if (oldVersion.isEmpty()) {
if (resource->status() == ResourceStatus::NOT_INSTALLED) if (resource->status() == ResourceStatus::NotInstalled) {
old_version = tr("Not installed"); oldVersion = tr("Not installed");
else } else {
old_version = tr("Unknown"); oldVersion = tr("Unknown");
}
} }
m_updates.emplace_back(pack->name, hash, old_version, project_ver.version_number, project_ver.version_type, m_updates.emplace_back(pack->name, hash, oldVersion, projectVer.version_number, projectVer.version_type,
project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, resource->enabled()); projectVer.changelog, ModPlatform::ResourceProvider::MODRINTH, downloadTask, resource->enabled());
} }
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver)); m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, projectVer));
iter = m_mappings.erase(iter); iter = m_mappings.erase(iter);
} }
@ -211,20 +213,22 @@ void ModrinthCheckUpdate::checkNextLoader()
if (m_loaderIdx < m_loadersList.size()) { // this are mods so check with loades if (m_loaderIdx < m_loadersList.size()) { // this are mods so check with loades
getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize); getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize);
return; return;
} else if (m_loadersList.isEmpty() && m_loaderIdx == 0) { // this are other resources no need to check more than once with empty loader }
if (m_loadersList.isEmpty() && m_loaderIdx == 0) { // this are other resources no need to check more than once with empty loader
getUpdateModsForLoader(); getUpdateModsForLoader();
return; return;
} }
for (auto resource : m_mappings) { for (auto* resource : m_mappings) {
QString reason; QString reason;
if (dynamic_cast<Mod*>(resource) != nullptr) if (dynamic_cast<Mod*>(resource) != nullptr) {
reason = reason =
tr("No valid version found for this resource. It's probably unavailable for the current game " tr("No valid version found for this resource. It's probably unavailable for the current game "
"version / mod loader."); "version / mod loader.");
else } else {
reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); reason = tr("No valid version found for this resource. It's probably unavailable for the current game version.");
}
emit checkFailed(resource, reason); emit checkFailed(resource, reason);
} }

View file

@ -16,7 +16,10 @@
#include "net/ChecksumValidator.h" #include "net/ChecksumValidator.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/ApiHeaderProxy.h"
#include "net/NetJob.h" #include "net/NetJob.h"
#include "modplatform/ModIndex.h"
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
@ -29,111 +32,116 @@
bool ModrinthCreationTask::abort() bool ModrinthCreationTask::abort()
{ {
if (!canAbort()) if (!canAbort()) {
return false; return false;
}
if (m_task) if (m_task) {
m_task->abort(); m_task->abort();
}
return InstanceCreationTask::abort(); return InstanceCreationTask::abort();
} }
bool ModrinthCreationTask::updateInstance() bool ModrinthCreationTask::updateInstance()
{ {
auto instance_list = APPLICATION->instances(); auto* instanceList = APPLICATION->instances();
// FIXME: How to handle situations when there's more than one install already for a given modpack? // FIXME: How to handle situations when there's more than one install already for a given modpack?
BaseInstance* inst; BaseInstance* inst = nullptr;
if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { if (auto originalId = originalInstanceID(); !originalId.isEmpty()) {
inst = instance_list->getInstanceById(original_id); inst = instanceList->getInstanceById(originalId);
Q_ASSERT(inst); Q_ASSERT(inst);
} else { } else {
inst = instance_list->getInstanceByManagedName(originalName()); inst = instanceList->getInstanceByManagedName(originalName());
if (!inst) { if (!inst) {
inst = instance_list->getInstanceById(originalName()); inst = instanceList->getInstanceById(originalName());
if (!inst) if (!inst) {
return false; return false;
} }
} }
}
QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
if (!parseManifest(index_path, m_files, true, false)) if (!parseManifest(indexPath, m_files, true, false)) {
return false; return false;
}
auto version_name = inst->getManagedPackVersionName(); auto versionName = inst->getManagedPackVersionName();
m_root_path = QFileInfo(inst->gameRoot()).fileName(); m_root_path = QFileInfo(inst->gameRoot()).fileName();
auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; auto versionStr = !versionName.isEmpty() ? tr(" (version %1)").arg(versionName) : "";
if (shouldConfirmUpdate()) { if (shouldConfirmUpdate()) {
auto should_update = askIfShouldUpdate(m_parent, version_str); auto shouldUpdate = askIfShouldUpdate(m_parent, versionStr);
if (should_update == ShouldUpdate::SkipUpdating) if (shouldUpdate == ShouldUpdate::SkipUpdating) {
return false; return false;
if (should_update == ShouldUpdate::Cancel) { }
if (shouldUpdate == ShouldUpdate::Cancel) {
m_abort = true; m_abort = true;
return false; return false;
} }
} }
// Remove repeated files, we don't need to download them! // Remove repeated files, we don't need to download them!
QDir old_inst_dir(inst->instanceRoot()); QDir oldInstDir(inst->instanceRoot());
QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack")); QString oldIndexFolder(FS::PathCombine(oldInstDir.absolutePath(), "mrpack"));
QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json")); QString oldIndexPath(FS::PathCombine(oldIndexFolder, "modrinth.index.json"));
QFileInfo old_index_file(old_index_path); QFileInfo oldIndexFile(oldIndexPath);
if (old_index_file.exists()) { if (oldIndexFile.exists()) {
std::vector<File> old_files; std::vector<File> oldFiles;
parseManifest(old_index_path, old_files, false, false); parseManifest(oldIndexPath, oldFiles, false, false);
// Let's remove all duplicated, identical resources! // Let's remove all duplicated, identical resources!
auto files_iterator = m_files.begin(); auto filesIterator = m_files.begin();
begin: begin:
while (files_iterator != m_files.end()) { while (filesIterator != m_files.end()) {
auto const& file = *files_iterator; const auto& file = *filesIterator;
auto old_files_iterator = old_files.begin(); auto oldFilesIterator = oldFiles.begin();
while (old_files_iterator != old_files.end()) { while (oldFilesIterator != oldFiles.end()) {
auto const& old_file = *old_files_iterator; const auto& oldFile = *oldFilesIterator;
if (old_file.hash == file.hash) { if (oldFile.hash == file.hash) {
qDebug() << "Removed file at" << file.path << "from list of downloads"; qDebug() << "Removed file at" << file.path << "from list of downloads";
files_iterator = m_files.erase(files_iterator); filesIterator = m_files.erase(filesIterator);
old_files_iterator = old_files.erase(old_files_iterator); oldFilesIterator = oldFiles.erase(oldFilesIterator);
goto begin; // Sorry :c goto begin; // Sorry :c
} }
old_files_iterator++; oldFilesIterator++;
} }
files_iterator++; filesIterator++;
} }
QDir old_minecraft_dir(inst->gameRoot()); QDir oldMinecraftDir(inst->gameRoot());
// Some files were removed from the old version, and some will be downloaded in an updated version, // Some files were removed from the old version, and some will be downloaded in an updated version,
// so we're fine removing them! // so we're fine removing them!
if (!old_files.empty()) { if (!oldFiles.empty()) {
for (auto const& file : old_files) { for (const auto& file : oldFiles) {
scheduleToDelete(m_parent, old_minecraft_dir, file.path, true); scheduleToDelete(m_parent, oldMinecraftDir, file.path, true);
} }
} }
// We will remove all the previous overrides, to prevent duplicate files! // We will remove all the previous overrides, to prevent duplicate files!
// TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides?
// FIXME: We may want to do something about disabled mods. // FIXME: We may want to do something about disabled mods.
auto old_overrides = Override::readOverrides("overrides", old_index_folder); auto oldOverrides = Override::readOverrides("overrides", oldIndexFolder);
for (const auto& entry : old_overrides) { for (const auto& entry : oldOverrides) {
scheduleToDelete(m_parent, old_minecraft_dir, entry); scheduleToDelete(m_parent, oldMinecraftDir, entry);
} }
auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); auto oldClientOverrides = Override::readOverrides("client-overrides", oldIndexFolder);
for (const auto& entry : old_client_overrides) { for (const auto& entry : oldClientOverrides) {
scheduleToDelete(m_parent, old_minecraft_dir, entry); scheduleToDelete(m_parent, oldMinecraftDir, entry);
} }
} else { } else {
// We don't have an old index file, so we may duplicate stuff! // We don't have an old index file, so we may duplicate stuff!
auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), auto* dialog = CustomMessageBox::selectable(m_parent, tr("No index file."),
tr("We couldn't find a suitable index file for the older version. This may cause some " tr("We couldn't find a suitable index file for the older version. This may cause some "
"of the files to be duplicated. Do you want to continue?"), "of the files to be duplicated. Do you want to continue?"),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel);
@ -158,39 +166,40 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
{ {
QEventLoop loop; QEventLoop loop;
QString parent_folder(FS::PathCombine(m_stagingPath, "mrpack")); QString parentFolder(FS::PathCombine(m_stagingPath, "mrpack"));
QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
if (m_files.empty() && !parseManifest(index_path, m_files, true, true)) if (m_files.empty() && !parseManifest(indexPath, m_files, true, true)) {
return nullptr; return nullptr;
}
// Keep index file in case we need it some other time (like when changing versions) // Keep index file in case we need it some other time (like when changing versions)
QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); QString newIndexPlace(FS::PathCombine(parentFolder, "modrinth.index.json"));
FS::ensureFilePathExists(new_index_place); FS::ensureFilePathExists(newIndexPlace);
FS::move(index_path, new_index_place); FS::move(indexPath, newIndexPlace);
auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
auto override_path = FS::PathCombine(m_stagingPath, "overrides"); auto overridePath = FS::PathCombine(m_stagingPath, "overrides");
if (QFile::exists(override_path)) { if (QFile::exists(overridePath)) {
// Create a list of overrides in "overrides.txt" inside mrpack/ // Create a list of overrides in "overrides.txt" inside mrpack/
Override::createOverrides("overrides", parent_folder, override_path); Override::createOverrides("overrides", parentFolder, overridePath);
// Apply the overrides // Apply the overrides
if (!FS::move(override_path, mcPath)) { if (!FS::move(overridePath, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + "overrides"); setError(tr("Could not rename the overrides folder:\n") + "overrides");
return nullptr; return nullptr;
} }
} }
// Do client overrides // Do client overrides
auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); auto clientOverridePath = FS::PathCombine(m_stagingPath, "client-overrides");
if (QFile::exists(client_override_path)) { if (QFile::exists(clientOverridePath)) {
// Create a list of overrides in "client-overrides.txt" inside mrpack/ // Create a list of overrides in "client-overrides.txt" inside mrpack/
Override::createOverrides("client-overrides", parent_folder, client_override_path); Override::createOverrides("client-overrides", parentFolder, clientOverridePath);
// Apply the overrides // Apply the overrides
if (!FS::overrideFolder(mcPath, client_override_path)) { if (!FS::overrideFolder(mcPath, clientOverridePath)) {
setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); setError(tr("Could not rename the client overrides folder:\n") + "client overrides");
return nullptr; return nullptr;
} }
@ -200,18 +209,27 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
auto instanceSettings = std::make_unique<INISettingsObject>(configPath); auto instanceSettings = std::make_unique<INISettingsObject>(configPath);
auto instance = std::make_unique<MinecraftInstance>(m_globalSettings, std::move(instanceSettings), m_stagingPath); auto instance = std::make_unique<MinecraftInstance>(m_globalSettings, std::move(instanceSettings), m_stagingPath);
auto components = instance->getPackProfile(); auto* components = instance->getPackProfile();
components->buildingFromScratch(); components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_minecraft_version, true); components->setComponentVersion("net.minecraft", m_minecraft_version, true);
if (!m_fabric_version.isEmpty()) QString loader;
if (!m_fabric_version.isEmpty()) {
components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version); components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version);
if (!m_quilt_version.isEmpty()) loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Fabric);
}
if (!m_quilt_version.isEmpty()) {
components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version);
if (!m_forge_version.isEmpty()) loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Quilt);
}
if (!m_forge_version.isEmpty()) {
components->setComponentVersion("net.minecraftforge", m_forge_version); components->setComponentVersion("net.minecraftforge", m_forge_version);
if (!m_neoForge_version.isEmpty()) loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Forge);
}
if (!m_neoForge_version.isEmpty()) {
components->setComponentVersion("net.neoforged", m_neoForge_version); components->setComponentVersion("net.neoforged", m_neoForge_version);
loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::NeoForge);
}
if (m_instIcon != "default") { if (m_instIcon != "default") {
instance->setIconKey(m_instIcon); instance->setIconKey(m_instIcon);
@ -220,34 +238,35 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
} }
// Don't add managed info to packs without an ID (most likely imported from ZIP) // Don't add managed info to packs without an ID (most likely imported from ZIP)
if (!m_managed_id.isEmpty()) if (!m_managed_id.isEmpty()) {
instance->setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); instance->setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version());
else } else {
instance->setManagedPack("modrinth", "", name(), "", ""); instance->setManagedPack("modrinth", "", name(), "", "");
}
instance->setName(name()); instance->setName(name());
instance->saveNow(); instance->saveNow();
auto downloadMods = makeShared<NetJob>(tr("Mod Download Modrinth"), APPLICATION->network()); auto downloadMods = makeShared<NetJob>(tr("Mod Download Modrinth"), APPLICATION->network());
auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); auto rootModpackPath = FS::PathCombine(m_stagingPath, m_root_path);
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); auto rootModpackUrl = QUrl::fromLocalFile(rootModpackPath);
// TODO make this work with other sorts of resource // TODO make this work with other sorts of resource
QHash<QString, Resource*> resources; QHash<QString, Resource*> resources;
for (auto& file : m_files) { for (auto& file : m_files) {
auto fileName = file.path; auto fileName = file.path;
fileName = FS::RemoveInvalidPathChars(fileName); fileName = FS::RemoveInvalidPathChars(fileName);
auto file_path = FS::PathCombine(root_modpack_path, fileName); auto filePath = FS::PathCombine(rootModpackPath, fileName);
if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) { if (!rootModpackUrl.isParentOf(QUrl::fromLocalFile(filePath))) {
// This means we somehow got out of the root folder, so abort here to prevent exploits // This means we somehow got out of the root folder, so abort here to prevent exploits
setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.") setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.")
.arg(fileName)); .arg(fileName));
return nullptr; return nullptr;
} }
if (fileName.startsWith("mods/")) { if (fileName.startsWith("mods/")) {
auto mod = new Mod(file_path); auto* mod = new Mod(filePath);
ModDetails d; ModDetails d;
d.mod_id = file_path; d.mod_id = filePath;
mod->setDetails(d); mod->setDetails(d);
resources[file.hash.toHex()] = mod; resources[file.hash.toHex()] = mod;
} }
@ -255,29 +274,39 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName));
return nullptr; return nullptr;
} }
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; qDebug() << "Will try to download" << file.downloads.front() << "to" << filePath;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
Net::ModrinthDownloadMeta meta{
.reason = m_instance.has_value() ? "update" : "modpack",
.gameVersion = m_minecraft_version,
.loader = loader,
};
QUrl downloadUrl = file.downloads.dequeue();
auto dl = Net::ApiDownload::makeFile(downloadUrl, filePath, Net::Download::Option::NoOptions, meta);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
downloadMods->addNetAction(dl); downloadMods->addNetAction(dl);
if (!file.downloads.empty()) { if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of // FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :) // MultipleOptionsTask's , once those exist :)
auto param = dl.toWeakRef(); auto param = dl.toWeakRef();
connect(dl.get(), &Task::failed, [&file, file_path, param, downloadMods] { connect(dl.get(), &Task::failed, [&file, filePath, param, downloadMods, meta] {
auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); QUrl fallbackUrl = file.downloads.dequeue();
auto ndl = Net::ApiDownload::makeFile(fallbackUrl, filePath, Net::Download::Option::NoOptions, meta);
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
downloadMods->addNetAction(ndl); downloadMods->addNetAction(ndl);
if (auto shared = param.lock()) if (auto shared = param.lock()) {
shared->succeeded(); shared->succeeded();
}
}); });
} }
} }
bool ended_well = false; bool endedWell = false;
connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; }); connect(downloadMods.get(), &NetJob::succeeded, this, [&endedWell]() { endedWell = true; });
connect(downloadMods.get(), &NetJob::failed, [this, &ended_well](const QString& reason) { connect(downloadMods.get(), &NetJob::failed, [this, &endedWell](const QString& reason) {
ended_well = false; endedWell = false;
setError(reason); setError(reason);
}); });
connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit);
@ -293,8 +322,8 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
loop.exec(); loop.exec();
if (!ended_well) { if (!endedWell) {
for (auto resource : resources) { for (auto* resource : resources) {
delete resource; delete resource;
} }
return nullptr; return nullptr;
@ -303,7 +332,7 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
QEventLoop ensureMetaLoop; QEventLoop ensureMetaLoop;
QDir folder = FS::PathCombine(instance->modsRoot(), ".index"); QDir folder = FS::PathCombine(instance->modsRoot(), ".index");
auto ensureMetadataTask = makeShared<EnsureMetadataTask>(resources, folder, ModPlatform::ResourceProvider::MODRINTH); auto ensureMetadataTask = makeShared<EnsureMetadataTask>(resources, folder, ModPlatform::ResourceProvider::MODRINTH);
connect(ensureMetadataTask.get(), &Task::succeeded, this, [&ended_well]() { ended_well = true; }); connect(ensureMetadataTask.get(), &Task::succeeded, this, [&endedWell]() { endedWell = true; });
connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit);
connect(ensureMetadataTask.get(), &Task::progress, [this](qint64 current, qint64 total) { connect(ensureMetadataTask.get(), &Task::progress, [this](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
@ -315,40 +344,38 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
m_task = ensureMetadataTask; m_task = ensureMetadataTask;
ensureMetaLoop.exec(); ensureMetaLoop.exec();
for (auto resource : resources) { for (auto* resource : resources) {
delete resource; delete resource;
} }
resources.clear(); resources.clear();
// Update information of the already installed instance, if any. // Update information of the already installed instance, if any.
if (m_instance && ended_well) { if (m_instance && endedWell) {
setAbortable(false); setAbortable(false);
auto inst = m_instance.value(); auto* inst = m_instance.value();
// Only change the name if it didn't use a custom name, so that the previous custom name // Only change the name if it didn't use a custom name, so that the previous custom name
// is preserved, but if we're using the original one, we update the version string. // is preserved, but if we're using the original one, we update the version string.
// NOTE: This needs to come before the copyManagedPack call! // NOTE: This needs to come before the copyManagedPack call!
if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance->name()) { if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance->name()) {
if (askForChangingInstanceName(m_parent, inst->name(), instance->name()) == InstanceNameChange::ShouldChange) if (askForChangingInstanceName(m_parent, inst->name(), instance->name()) == InstanceNameChange::ShouldChange) {
inst->setName(instance->name()); inst->setName(instance->name());
} }
}
inst->copyManagedPack(*instance); inst->copyManagedPack(*instance);
} }
if (ended_well) { if (endedWell) {
return instance; return instance;
} }
return nullptr; return nullptr;
} }
bool ModrinthCreationTask::parseManifest(const QString& index_path, bool ModrinthCreationTask::parseManifest(const QString& indexPath, std::vector<File>& files, bool setInternalData, bool showOptionalDialog)
std::vector<File>& files,
bool set_internal_data,
bool show_optional_dialog)
{ {
try { try {
auto doc = Json::requireDocument(index_path); auto doc = Json::requireDocument(indexPath);
auto obj = Json::requireObject(doc, "modrinth.index.json"); auto obj = Json::requireObject(doc, "modrinth.index.json");
int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json");
if (formatVersion == 1) { if (formatVersion == 1) {
@ -357,9 +384,10 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
throw JSONValidationError("Unknown game: " + game); throw JSONValidationError("Unknown game: " + game);
} }
if (set_internal_data) { if (setInternalData) {
if (m_managed_version_id.isEmpty()) if (m_managed_version_id.isEmpty()) {
m_managed_version_id = obj["versionId"].toString(); m_managed_version_id = obj["versionId"].toString();
}
m_managed_name = obj["name"].toString(); m_managed_name = obj["name"].toString();
} }
@ -375,7 +403,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
QString support = env["client"].toString("unsupported"); QString support = env["client"].toString("unsupported");
if (support == "unsupported") { if (support == "unsupported") {
continue; continue;
} else if (support == "optional") { }
if (support == "optional") {
file.required = false; file.required = false;
} }
} }
@ -387,20 +416,21 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces) // (as Modrinth seems to incorrectly handle spaces)
auto download_arr = modInfo["downloads"].toArray(); auto downloadArr = modInfo["downloads"].toArray();
for (auto download : download_arr) { for (auto download : downloadArr) {
qWarning() << download.toString(); qWarning() << download.toString();
bool is_last = download.toString() == download_arr.last().toString(); bool isLast = download.toString() == downloadArr.last().toString();
auto download_url = QUrl(download.toString()); auto downloadUrl = QUrl(download.toString());
if (!download_url.isValid()) { if (!downloadUrl.isValid()) {
qDebug() qDebug()
<< QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(download_url.toString(), file.path); << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(downloadUrl.toString(), file.path);
if (is_last && file.downloads.isEmpty()) if (isLast && file.downloads.isEmpty()) {
throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
}
} else { } else {
file.downloads.push_back(download_url); file.downloads.push_back(downloadUrl);
} }
} }
@ -408,10 +438,11 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
} }
if (!optionalFiles.empty()) { if (!optionalFiles.empty()) {
if (show_optional_dialog) { if (showOptionalDialog) {
QStringList oFiles; QStringList oFiles;
for (auto file : optionalFiles) for (const auto& file : optionalFiles) {
oFiles.push_back(file.path); oFiles.push_back(file.path);
}
OptionalModDialog optionalModDialog(m_parent, oFiles); OptionalModDialog optionalModDialog(m_parent, oFiles);
if (optionalModDialog.exec() == QDialog::Rejected) { if (optionalModDialog.exec() == QDialog::Rejected) {
emitAborted(); emitAborted();
@ -434,7 +465,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
} }
} }
} }
if (set_internal_data) { if (setInternalData) {
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
QString name = it.key(); QString name = it.key();

View file

@ -24,18 +24,18 @@ class ModrinthCreationTask final : public InstanceCreationTask {
}; };
public: public:
ModrinthCreationTask(QString staging_path, ModrinthCreationTask(const QString& stagingPath,
SettingsObject* global_settings, SettingsObject* globalSettings,
QWidget* parent, QWidget* parent,
QString id, QString id,
QString version_id = {}, QString versionId = {},
QString original_instance_id = {}) QString originalInstanceId = {})
: InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id)) : m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(versionId))
{ {
setStagingPath(staging_path); setStagingPath(stagingPath);
setParentSettings(global_settings); setParentSettings(globalSettings);
m_original_instance_id = std::move(original_instance_id); m_original_instance_id = std::move(originalInstanceId);
} }
bool abort() override; bool abort() override;
@ -44,7 +44,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
std::unique_ptr<MinecraftInstance> createInstance() override; std::unique_ptr<MinecraftInstance> createInstance() override;
private: private:
bool parseManifest(const QString&, std::vector<File>&, bool set_internal_data = true, bool show_optional_dialog = true); bool parseManifest(const QString&, std::vector<File>&, bool setInternalData = true, bool showOptionalDialog = true);
private: private:
QWidget* m_parent = nullptr; QWidget* m_parent = nullptr;

View file

@ -18,28 +18,30 @@
*/ */
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include <utility>
#include "net/ApiHeaderProxy.h" #include "net/ApiHeaderProxy.h"
namespace Net { namespace Net {
Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options) Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options)
{ {
auto dl = Download::makeCached(url, entry, options); auto dl = Download::makeCached(std::move(url), std::move(entry), options);
dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>()); dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>());
return dl; return dl;
} }
std::pair<Download::Ptr, QByteArray*> ApiDownload::makeByteArray(QUrl url, Download::Options options) std::pair<Download::Ptr, QByteArray*> ApiDownload::makeByteArray(QUrl url, Download::Options options)
{ {
auto [dl, response] = Download::makeByteArray(url, options); auto [dl, response] = Download::makeByteArray(std::move(url), options);
dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>()); dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>());
return { dl, response }; return { dl, response };
} }
Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options) Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options, ModrinthDownloadMeta meta)
{ {
auto dl = Download::makeFile(url, path, options); auto dl = Download::makeFile(std::move(url), std::move(path), options);
dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>()); dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>(std::move(meta)));
return dl; return dl;
} }

View file

@ -20,13 +20,17 @@
#pragma once #pragma once
#include "Download.h" #include "Download.h"
#include "net/ApiHeaderProxy.h"
namespace Net { namespace Net {
namespace ApiDownload { namespace ApiDownload {
Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Download::Options options = Download::Option::NoOptions); Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Download::Options options = Download::Option::NoOptions);
std::pair<Download::Ptr, QByteArray*> makeByteArray(QUrl url, Download::Options options = Download::Option::NoOptions); std::pair<Download::Ptr, QByteArray*> makeByteArray(QUrl url, Download::Options options = Download::Option::NoOptions);
Download::Ptr makeFile(QUrl url, QString path, Download::Options options = Download::Option::NoOptions); Download::Ptr makeFile(QUrl url,
QString path,
Download::Options options = Download::Option::NoOptions,
ModrinthDownloadMeta meta = ModrinthDownloadMeta());
}; // namespace ApiDownload }; // namespace ApiDownload
} // namespace Net } // namespace Net

View file

@ -23,27 +23,64 @@
#include "BuildConfig.h" #include "BuildConfig.h"
#include "net/HeaderProxy.h" #include "net/HeaderProxy.h"
#include <QJsonDocument>
#include <QJsonObject>
namespace Net { namespace Net {
struct ModrinthDownloadMeta {
QString reason;
QString gameVersion;
QString loader;
bool isEmpty() const { return reason.isEmpty(); }
QByteArray toJson() const
{
QJsonObject obj;
if (!reason.isEmpty()) {
obj["reason"] = reason;
}
if (!gameVersion.isEmpty()) {
obj["game_version"] = gameVersion;
}
if (!loader.isEmpty()) {
obj["loader"] = loader;
}
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
}
};
class ApiHeaderProxy : public HeaderProxy { class ApiHeaderProxy : public HeaderProxy {
public: public:
ApiHeaderProxy() : HeaderProxy() {} ApiHeaderProxy() = default;
virtual ~ApiHeaderProxy() = default; explicit ApiHeaderProxy(ModrinthDownloadMeta meta) : m_meta(std::move(meta)) {}
~ApiHeaderProxy() override = default;
public: public:
virtual QList<HeaderPair> headers(const QNetworkRequest& request) const override QList<HeaderPair> headers(const QNetworkRequest& request) const override
{ {
QList<HeaderPair> hdrs; QList<HeaderPair> hdrs;
if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { const auto host = request.url().host();
hdrs.append({ "x-api-key", APPLICATION->getFlameAPIKey().toUtf8() });
} else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || if (APPLICATION->capabilities() & Application::SupportsFlame &&
request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { (host == QUrl(BuildConfig.FLAME_BASE_URL).host() || host == BuildConfig.FLAME_DOWNLOAD_HOST)) {
hdrs.append({ .headerName = "x-api-key", .headerValue = APPLICATION->getFlameAPIKey().toUtf8() });
} else if (host == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || host == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) {
QString token = APPLICATION->getModrinthAPIToken(); QString token = APPLICATION->getModrinthAPIToken();
if (!token.isNull()) if (!token.isNull()) {
hdrs.append({ "Authorization", token.toUtf8() }); hdrs.append({ .headerName = "Authorization", .headerValue = token.toUtf8() });
}
}
if (host == BuildConfig.MODRINTH_DOWNLOAD_HOST && !m_meta.isEmpty()) {
hdrs.append({ .headerName = "modrinth-download-meta", .headerValue = m_meta.toJson() });
} }
return hdrs; return hdrs;
}; };
private:
ModrinthDownloadMeta m_meta;
}; };
} // namespace Net } // namespace Net

View file

@ -38,7 +38,6 @@
#include "Validator.h" #include "Validator.h"
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QFile>
namespace Net { namespace Net {
class ChecksumValidator : public Validator { class ChecksumValidator : public Validator {
@ -69,10 +68,10 @@ class ChecksumValidator : public Validator {
return true; return true;
} }
auto validate(QNetworkReply&) -> bool override auto validate(QNetworkReply& reply) -> bool override
{ {
if (m_expected.size() && m_expected != hash()) { if (!m_expected.isEmpty() && m_expected != hash()) {
qWarning() << "Checksum mismatch, download is bad."; qWarning() << "Checksum mismatch for URL:" << reply.url().toString() << "expected:" << m_expected << "got:" << hash();
return false; return false;
} }
return true; return true;

View file

@ -182,7 +182,7 @@ auto HttpMetaCache::evictAll() -> bool
} }
map.entry_list.clear(); map.entry_list.clear();
// AND all return codes together so the result is true iff all runs of deletePath() are true // AND all return codes together so the result is true iff all runs of deletePath() are true
ret &= FS::deletePath(map.base_path); ret &= FS::deleteContents(map.base_path);
} }
return ret; return ret;
} }

View file

@ -69,11 +69,15 @@ void NetJob::executeNextSubTask()
// We're finished, check for failures and retry if we can (up to 3 times) // We're finished, check for failures and retry if we can (up to 3 times)
if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) { if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) {
m_try += 1; m_try += 1;
while (!m_failed.isEmpty()) { m_failed.removeIf([this](QHash<Task*, Task::Ptr>::iterator task) {
auto task = m_failed.take(*m_failed.keyBegin()); // there is no point in retying on 404 Not Found
m_done.remove(task.get()); if (static_cast<Net::NetRequest*>(task->get())->replyStatusCode() == 404) {
m_queue.enqueue(task); return false;
} }
m_done.remove(task->get());
m_queue.enqueue(*task);
return true;
});
} }
ConcurrentTask::executeNextSubTask(); ConcurrentTask::executeNextSubTask();
} }
@ -100,13 +104,18 @@ auto NetJob::canAbort() const -> bool
auto NetJob::abort() -> bool auto NetJob::abort() -> bool
{ {
bool fullyAborted = true;
// fail all downloads on the queue // fail all downloads on the queue
for (auto task : m_queue) for (auto task : m_queue)
m_failed.insert(task.get(), task); m_failed.insert(task.get(), task);
m_queue.clear(); m_queue.clear();
if (m_doing.isEmpty()) {
// no downloads to abort, NetJob is not running
return true;
}
bool fullyAborted = true;
// abort active downloads // abort active downloads
auto toKill = m_doing.values(); auto toKill = m_doing.values();
for (auto part : toKill) { for (auto part : toKill) {

View file

@ -40,4 +40,18 @@ inline bool isApplicationError(QNetworkReply::NetworkError x)
QNetworkReply::UnknownContentError }; QNetworkReply::UnknownContentError };
return errors.contains(x); return errors.contains(x);
} }
// 500 class errors, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/500
// microsoft may send these error codes when services (auth) are down.
// We treat this as a reason to launch in offline mode.
inline bool isServerError(QNetworkReply::NetworkError x)
{
static QSet<QNetworkReply::NetworkError> errors = { QNetworkReply::InternalServerError,
QNetworkReply::OperationNotImplementedError,
QNetworkReply::ServiceUnavailableError, // 503 | seen in logs in 2026
//QNetworkReply::GatewayTimeoutError, // 504 | seen in logs in 2024
// Qt doesn't have it mapped. Unknown covers it
QNetworkReply::UnknownServerError };
return errors.contains(x);
}
} // namespace Net } // namespace Net

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 B

View file

@ -118,7 +118,7 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State
Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot) Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot)
{ {
auto up = makeShared<ImgurUpload>(m_shot->m_file); auto up = makeShared<ImgurUpload>(m_shot->m_file);
up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "image"); up->m_url = BuildConfig.IMGUR_BASE_URL + "image";
up->m_sink.reset(new Sink(m_shot)); up->m_sink.reset(new Sink(m_shot));
up->addHeaderProxy(std::make_unique<Net::RawHeaderProxy>(QList<Net::HeaderPair>{ up->addHeaderProxy(std::make_unique<Net::RawHeaderProxy>(QList<Net::HeaderPair>{
{ "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } }));

View file

@ -35,6 +35,8 @@
*/ */
#include "settings/INIFile.h" #include "settings/INIFile.h"
#include <AssertHelpers.h>
#include <FileSystem.h> #include <FileSystem.h>
#include <QDebug> #include <QDebug>
@ -62,11 +64,10 @@ bool INIFile::saveFile(QString fileName)
_settings_obj.sync(); _settings_obj.sync();
if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) {
// Shouldn't be possible!
Q_ASSERT(status != QSettings::Status::FormatError);
if (status == QSettings::Status::AccessError) if (status == QSettings::Status::AccessError)
qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; qCritical() << "An access error occurred while saving INI file" << fileName << "(is the file read-only?)";
if (ASSERT_NEVER(status == QSettings::Status::FormatError))
qCritical() << "A format error occurred while saving INI file" << fileName << "(this shouldn't be possible!)";
return false; return false;
} }
@ -178,9 +179,9 @@ bool INIFile::loadFile(QString fileName)
if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) {
if (status == QSettings::Status::AccessError) if (status == QSettings::Status::AccessError)
qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; qCritical() << "An access error occurred while loading INI file" << fileName;
if (status == QSettings::Status::FormatError) if (status == QSettings::Status::FormatError)
qCritical() << "A format error occurred (e.g. loading a malformed INI file)."; qCritical() << "A format error occurred while loading INI file" << fileName << "(is the file malformed or corrupted?)";
return false; return false;
} }
if (!_settings_obj.value("ConfigVersion").isValid()) { if (!_settings_obj.value("ConfigVersion").isValid()) {

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