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
167 changed files with 3606 additions and 2601 deletions

View file

@ -23,14 +23,14 @@ body:
- macOS
- Linux
- Other
- type: textarea
- type: input
attributes:
label: Version of Prism Launcher
description: The version of Prism Launcher used in the bug report.
placeholder: Prism Launcher 5.0
validations:
required: true
- type: textarea
- type: input
attributes:
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.

View file

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

View file

@ -55,7 +55,7 @@ runs:
# TODO(@getchoo): Get this working on MSYS2!
- name: Setup ccache
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:
variant: sccache
create-symlink: ${{ runner.os != 'Windows' }}

View file

@ -13,7 +13,7 @@ runs:
dpkg-dev \
ninja-build extra-cmake-modules pkg-config scdoc \
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
shell: bash

View file

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

View file

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

View file

@ -28,19 +28,14 @@ jobs:
fetch-depth: 0 # Required for diffing later on
submodules: "true"
- name: Setup sccache
uses: hendrikmuhs/ccache-action@v1.2.22
with:
variant: sccache
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Run build
- name: Run source generators
# TODO(@getchoo): Figure out how to make this work with PCH
run: |
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

View file

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

View file

@ -103,7 +103,7 @@ jobs:
# For PRs
- name: Setup Nix Magic Cache
if: ${{ github.event_name == 'pull_request' }}
uses: DeterminateSystems/magic-nix-cache-action@v13
uses: DeterminateSystems/magic-nix-cache-action@v14
with:
diagnostic-endpoint: ""
use-flakehub: false

View file

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

View file

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

View file

@ -13,6 +13,10 @@ endif()
##################################### Set CMake options #####################################
set(CMAKE_AUTOMOC 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_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/")
@ -179,9 +183,9 @@ 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 version numbers ########
set(Launcher_VERSION_MAJOR 11)
set(Launcher_VERSION_MAJOR 12)
set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_PATCH 1)
set(Launcher_VERSION_PATCH 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}")
set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0")

View file

@ -28,7 +28,7 @@ RUN apt-get --assume-yes --no-install-recommends install \
# Build system
cmake ninja-build extra-cmake-modules pkg-config \
# 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
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_PROD_URL = "https://api.modrinth.com/v2";
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_DOWNLOAD_HOST = "edge.forgecdn.net";
QString versionString() const;
/**

8
flake.lock generated
View file

@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1774709303,
"narHash": "sha256-D4ely1FsBcvtj/qSrNhSWpq+CUZKNiKwJIxpxnfy9o4=",
"rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685",
"lastModified": 1778443072,
"narHash": "sha256-rNDJzV2JTV5SUTwv1cgKZYMdyoUYU9/YfegSaUf3QfY=",
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"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": {
"type": "tarball",

View file

@ -578,16 +578,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
{
bool migrated = false;
if (!migrated)
migrated = handleDataMigration(
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC",
"polymc.cfg");
if (!migrated)
migrated = handleDataMigration(
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC",
"multimc.cfg");
auto migrated = handleDataMigration(
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC",
"polymc.cfg");
if (!migrated) {
handleDataMigration(dataPath,
FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"),
"MultiMC", "multimc.cfg");
}
}
{
@ -779,6 +777,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModDependenciesDisabled", false);
m_settings->registerSetting("SkipModpackUpdatePrompt", false);
m_settings->registerSetting("ShowModIncompat", false);
m_settings->registerSetting("DownloadGameFilesDuringInstanceCreation", true);
// Minecraft offline player name
m_settings->registerSetting("LastOfflinePlayerName", "");
@ -871,6 +870,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get());
}
m_settings->registerSetting("MetaRefreshOnLaunch", true);
m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false);
@ -935,15 +935,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
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
{
auto setting = APPLICATION->settings()->getSetting("IconsDir");
@ -1022,8 +1013,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qInfo() << "<> Cache initialized.";
}
// now we have network, download translation updates
m_translations->downloadIndex();
// load translations
{
m_translations.reset(new TranslationsModel("translations"));
m_translations->downloadIndex();
qInfo() << "Your language is" << m_translations->selectedLanguage();
qInfo() << "<> Translations loaded.";
}
// FIXME: what to do with these?
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.
* \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
// loaded.

View file

@ -1206,78 +1206,6 @@ if(WIN32)
)
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
resources/backgrounds/backgrounds.qrc
resources/multimc/multimc.qrc
@ -1296,12 +1224,6 @@ qt_add_resources(LAUNCHER_RESOURCES
"${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 ########
if(WIN32)
set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
@ -1321,7 +1243,7 @@ endif()
####### Targets ########
# 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_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION)
@ -1441,7 +1363,7 @@ endif()
if(Launcher_BUILD_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})
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_Name} PRIVATE /wd4100) # C4100 - unused parameter
else()
target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers)
target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers)
# sfinae-incomplete is a new GCC warning and triggers in Qt headers
# 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()
#### The bundle mess! ####

View file

@ -36,6 +36,7 @@
*/
#include "FileSystem.h"
#include <qcontainerfwd.h>
#include <QPair>
#include "BuildConfig.h"
@ -683,6 +684,32 @@ bool deletePath(QString path)
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)
{
// 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";
static const QString BAD_NTFS_CHARS = "<>:\"|?*";
static const QString BAD_HFS_CHARS = ":";
static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
namespace {
const QString g_badChars = "<>:\"|?*\r\n!";
QString removeChars(QString source, QChar replace, const QString& extraChars = "")
{
for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith;
return string;
}
QString RemoveInvalidPathChars(QString path, QChar replaceWith)
{
QString invalidChars;
#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;
auto badChars = g_badChars;
if (!extraChars.isEmpty()) {
badChars += extraChars;
}
if (invalidChars.size() != 0) {
for (int i = 0; i < path.length(); i++) {
if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) {
path[i] = replaceWith;
}
for (auto& c : source) {
if (c.unicode() < 0x20 || !c.isPrint() || badChars.contains(c)) {
c = replace;
}
}
return path;
return source;
}
} // namespace
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{
return removeChars(std::move(string), replaceWith, "\\/");
}
QString RemoveInvalidPathChars(QString string, QChar replaceWith)
{
return removeChars(std::move(string), replaceWith);
}
QString DirNameFromString(QString string, QString inDir)

View file

@ -291,6 +291,13 @@ bool move(const QString& source, const QString& dest);
*/
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);
/**

View file

@ -18,84 +18,45 @@
#include "HardwareInfo.h"
#include <QCoreApplication>
#include <QOffscreenSurface>
#include <QOpenGLFunctions>
#include <QProcessEnvironment>
#include "BuildConfig.h"
#ifndef Q_OS_MACOS
#include <QVulkanInstance>
#include <QVulkanWindow>
#endif
#include <QDebug>
#include <QStringList>
#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX)
namespace {
bool vulkanInfo(QStringList& out)
QString afterColon(QString str)
{
if (!QProcessEnvironment::systemEnvironment()
.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;
window.setVulkanInstance(&inst);
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;
return str.remove(0, str.indexOf(':') + 2).trimmed();
}
bool openGlInfo(QStringList& out)
template <typename F>
bool readFromOutput(const char* command, F function)
{
if (!QProcessEnvironment::systemEnvironment()
.value(QStringLiteral("%1_DISABLE_GLVULKAN").arg(BuildConfig.LAUNCHER_ENVNAME))
.isEmpty()) {
return false;
}
QOpenGLContext ctx;
if (!ctx.create()) {
qWarning() << "OpenGL context creation failed";
out << "Couldn't get OpenGL device information";
FILE* file = popen(command, "r"); // NOLINT(*-command-processor)
if (!file) {
qWarning().nospace() << "Could not execute command '" << command << "': " << strerror(errno);
return false;
}
QOffscreenSurface surface;
surface.create();
ctx.makeCurrent(&surface);
constexpr size_t bufferSize = 512;
std::array<char, bufferSize> buffer{};
while (fgets(buffer.data(), bufferSize, file) != nullptr) {
function(buffer.data());
}
auto* f = ctx.functions();
f->initializeOpenGLFunctions();
const int exitCode = pclose(file);
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)); };
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 false;
}
return true;
}
} // namespace
#ifndef Q_OS_LINUX
QStringList HardwareInfo::gpuInfo()
{
QStringList info;
vulkanInfo(info);
openGlInfo(info);
return info;
}
#endif
#ifdef Q_OS_WINDOWS
@ -104,7 +65,11 @@ QStringList HardwareInfo::gpuInfo()
#endif
#include <QSettings>
#include "windows.h"
#include <dxgi1_6.h>
#include <windows.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
QString HardwareInfo::cpuInfo()
{
@ -140,16 +105,42 @@ uint64_t HardwareInfo::availableRamMiB()
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)
#include "mach/mach.h"
#include "sys/sysctl.h"
QString HardwareInfo::cpuInfo()
{
std::array<char, 512> buffer;
std::array<char, 512> buffer{};
size_t bufferSize = buffer.size();
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";
@ -158,7 +149,7 @@ QString HardwareInfo::cpuInfo()
uint64_t HardwareInfo::totalRamMiB()
{
uint64_t memsize;
uint64_t memsize = 0;
size_t memsizeSize = sizeof memsize;
if (sysctlbyname("hw.memsize", &memsize, &memsizeSize, nullptr, 0) == 0) {
// transforming bytes -> mib
@ -171,36 +162,62 @@ uint64_t HardwareInfo::totalRamMiB()
uint64_t HardwareInfo::availableRamMiB()
{
mach_port_t host_port = mach_host_self();
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
return 0;
}
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;
MacOSHardwareInfo::MemoryPressureLevel MacOSHardwareInfo::memoryPressureLevel()
{
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 available RAM: host_statistics64";
return 0;
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)
#include <fstream>
namespace {
QString afterColon(QString& str)
{
return str.remove(0, str.indexOf(':') + 2).trimmed();
}
} // namespace
QString HardwareInfo::cpuInfo()
{
std::ifstream cpuin("/proc/cpuinfo");
for (std::string line; std::getline(cpuin, line);) {
// 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);
}
}
@ -209,12 +226,13 @@ QString HardwareInfo::cpuInfo()
return "unknown";
}
uint64_t readMemInfo(QString searchTarget)
namespace {
uint64_t readMemInfo(const QString& searchTarget)
{
std::ifstream memin("/proc/meminfo");
for (std::string line; std::getline(memin, line);) {
// 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;
const uint total = str.simplified().section(' ', 1, 1).toUInt(&ok);
if (!ok) {
@ -230,6 +248,7 @@ uint64_t readMemInfo(QString searchTarget)
qWarning() << "Could not read /proc/meminfo: search target not found:" << searchTarget;
return 0;
}
} // namespace
uint64_t HardwareInfo::totalRamMiB()
{
@ -243,52 +262,50 @@ uint64_t HardwareInfo::availableRamMiB()
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;
QString currentModel = "";
while (fgets(buffer.data(), 512, lspci) != nullptr) {
QString str(buffer.data());
QString gpu;
QString driverInUse = "NONE";
QString driversAvailable = "NONE";
QStringList out;
const bool success = readFromOutput("lspci -k", [&](const QString& str) {
// 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)
// Subsystem: Sapphire Technology Limited Radeon RX 580 Pulse 4GB
// Kernel driver in use: amdgpu
// Kernel modules: amdgpu
// clang-format on
if (str.contains("VGA compatible controller")) {
if (str.contains("VGA compatible controller") || str.contains("3D controller")) {
readingGpuInfo = true;
} 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;
}
if (!readingGpuInfo) {
continue;
return;
}
const QString value = afterColon(str);
if (str.contains("Subsystem")) {
currentModel = "Found GPU: " + afterColon(str);
gpu = value;
}
if (str.contains("Kernel driver in use")) {
currentModel += " (using driver " + afterColon(str);
driverInUse = value;
}
if (str.contains("Kernel modules")) {
currentModel += ", available drivers: " + afterColon(str) + ")";
list.append(currentModel);
driversAvailable = value;
}
});
if (!success) {
return { "GPU discovery failed: could not read from lspci" };
}
pclose(lspci);
return list;
return out;
}
#else
@ -303,19 +320,20 @@ QString HardwareInfo::cpuInfo()
uint64_t HardwareInfo::totalRamMiB()
{
char buff[512];
FILE* fp = popen("sysctl hw.physmem", "r");
if (fp != nullptr) {
if (fgets(buff, 512, fp) != nullptr) {
std::string str(buff);
uint64_t mem = std::stoull(str.substr(12, std::string::npos));
uint64_t out = 0;
// transforming kib -> mib
return mem / 1024;
}
const bool success = readFromOutput("sysctl hw.physmem", [&](const QString& str) {
const uint64_t mem = str.mid(12).toULong();
// transforming kib -> mib
out = mem / 1024;
});
if (!success) {
qWarning() << "Could not get total RAM: could not read from sysctl";
return 0;
}
return 0;
return out;
}
#else
@ -330,4 +348,8 @@ uint64_t HardwareInfo::availableRamMiB()
return 0;
}
QStringList HardwareInfo::gpuInfo()
{
return { "GPU discovery failed: not implemented for this OS" };
}
#endif

View file

@ -27,3 +27,16 @@ uint64_t totalRamMiB();
uint64_t availableRamMiB();
QStringList gpuInfo();
} // 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 <QFile>
#include "Application.h"
#include "InstanceTask.h"
#include "minecraft/MinecraftLoadAndCheck.h"
#include "tasks/SequentialTask.h"
@ -18,7 +19,7 @@ bool InstanceCreationTask::abort()
return m_gameFilesTask->abort();
}
return true;
return InstanceTask::abort();
}
void InstanceCreationTask::executeTask()
@ -38,8 +39,9 @@ void InstanceCreationTask::executeTask()
m_instance = createInstance();
if (!m_instance) {
if (m_abort)
if (m_abort) {
return;
}
qWarning() << "Instance creation failed!";
if (!m_error_message.isEmpty()) {
@ -63,8 +65,9 @@ void InstanceCreationTask::executeTask()
qDebug() << "Removing old files";
for (const QString& path : m_filesToRemove) {
if (!QFile::exists(path))
if (!QFile::exists(path)) {
continue;
}
qDebug() << "Removing" << path;
@ -81,6 +84,10 @@ void InstanceCreationTask::executeTask()
}
if (!m_abort) {
if (!APPLICATION->settings()->get("DownloadGameFilesDuringInstanceCreation").toBool()) {
emitSucceeded();
return;
}
setAbortable(true);
setAbortButtonText(tr("Skip"));
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()) {
return;

View file

@ -38,7 +38,7 @@ class InstanceCreationTask : public InstanceTask {
protected:
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:
bool m_abort = false;

View file

@ -40,6 +40,7 @@
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountList.h"
#include "net/NetUtils.h"
#include "ui/InstanceWindow.h"
#include "ui/dialogs/CustomMessageBox.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.
ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox,
m_parentWidget);
@ -133,14 +134,6 @@ LaunchDecision LaunchController::decideLaunchMode()
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();
MinecraftAccountPtr accountToCheck = nullptr;
@ -163,7 +156,9 @@ LaunchDecision LaunchController::decideLaunchMode()
}
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();
state = AccountState::Working;
}
@ -231,13 +226,14 @@ bool LaunchController::askPlayDemo() const
return box.clickedButton() == demoButton;
}
QString LaunchController::askOfflineName(const QString& playerName, bool* ok) const
QString LaunchController::askOfflineName(const QString& playerName, bool* ok)
{
if (ok != nullptr) {
*ok = false;
}
QString message;
QString title, message;
title = tr("Player name");
switch (m_actualLaunchMode) {
case LaunchMode::Normal:
Q_ASSERT(false);
@ -247,7 +243,14 @@ QString LaunchController::askOfflineName(const QString& playerName, bool* ok) co
break;
case LaunchMode::Offline:
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");
break;
@ -257,7 +260,7 @@ QString LaunchController::askOfflineName(const QString& playerName, bool* ok) co
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
ChooseOfflineNameDialog dialog(message, m_parentWidget);
dialog.setWindowTitle(tr("Player name"));
dialog.setWindowTitle(title);
dialog.setUsername(usedname);
if (dialog.exec() != QDialog::Accepted) {
return {};
@ -339,11 +342,11 @@ bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account,
if (button == QMessageBox::StandardButton::Yes) {
auto* accounts = APPLICATION->accounts();
const bool isDefault = accounts->defaultAccount() == account;
accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId())));
if (account->accountType() == AccountType::MSA) {
auto newAccount = MSALoginDialog::newAccount(m_parentWidget);
if (newAccount != nullptr) {
accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId())));
accounts->addAccount(newAccount);
if (isDefault) {

View file

@ -78,7 +78,7 @@ class LaunchController : public Task {
void decideAccount();
LaunchDecision decideLaunchMode();
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);
private slots:

View file

@ -114,7 +114,7 @@ void LoggedProcess::on_error(QProcess::ProcessError error)
{
switch (error) {
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);
break;
}

View file

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

View file

@ -19,23 +19,52 @@
#include "ResourceDownloadTask.h"
#include <utility>
#include "Application.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ResourceFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "net/ApiDownload.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,
ModPlatform::IndexedVersion version,
ResourceFolderModel* packs,
bool is_indexed)
bool isIndexed,
QString downloadReason)
: 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));
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->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()) {
switch (Hashing::algorithmFromString(m_pack_version.hash_type)) {
case Hashing::Algorithm::Md4:
@ -82,8 +113,9 @@ void ResourceDownloadTask::downloadSucceeded()
auto oldName = std::get<0>(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;
}
m_pack_model->uninstallResource(oldFilename, true);
@ -95,8 +127,9 @@ void ResourceDownloadTask::downloadSucceeded()
if (oldConfig.exists() && !newConfig.exists()) {
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()));
}
}
}
}
@ -104,7 +137,7 @@ void ResourceDownloadTask::downloadSucceeded()
void ResourceDownloadTask::downloadFailed(QString reason)
{
m_filesNetJob.reset();
emitFailed(reason);
emitFailed(std::move(reason));
}
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
// downloaded successfully!
void ResourceDownloadTask::hasOldResource(QString name, QString filename)
void ResourceDownloadTask::hasOldResource(const QString& name, const QString& filename)
{
to_delete = { name, filename };
}

View file

@ -33,7 +33,8 @@ class ResourceDownloadTask : public SequentialTask {
explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version,
ResourceFolderModel* packs,
bool is_indexed = true);
bool isIndexed = true,
QString downloadReason = "standalone");
const QString& getFilename() const { return m_pack_version.fileName; }
const QVariant& getVersionID() const { return m_pack_version.fileId; }
const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; }
@ -56,5 +57,5 @@ class ResourceDownloadTask : public SequentialTask {
std::tuple<QString, QString> to_delete{ "", "" };
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)
{
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() << "Native environment:";
qDebug() << QProcessEnvironment::systemEnvironment().toStringList();
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();
}

View file

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

View file

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

View file

@ -82,12 +82,12 @@ QUrl BaseEntity::url() const
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()) {
return m_task;
}
m_task.reset(new BaseEntityLoadTask(this, mode));
m_task.reset(new BaseEntityLoadTask(this, mode, forceReload));
return m_task;
}
@ -107,7 +107,9 @@ BaseEntity::LoadStatus BaseEntity::status() const
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()
{
@ -125,9 +127,11 @@ void BaseEntityLoadTask::executeTask()
}
// 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) {
throw Exception("mismatched checksum");
throw Exception(QString("Checksum mismatch, expected sha256: %1, got: %2").arg(expected, actual));
}
// 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;
// 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;
if (wasLoadedOffline || wasLoadedRemote) {
if (wasLoadedOffline || (wasLoadedRemote && !m_force_reload)) {
emitSucceeded();
return;
}
m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network()));
auto url = m_entity->url();
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);
auto dl = Net::ApiDownload::makeCached(url, entry);
/*

View file

@ -43,7 +43,7 @@ class BaseEntity {
void setSha256(QString sha256);
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:
QString m_sha256; // the expected sha256
@ -58,7 +58,7 @@ class BaseEntityLoadTask : public Task {
Q_OBJECT
public:
explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode);
explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload);
~BaseEntityLoadTask() override = default;
virtual void executeTask() override;
@ -68,6 +68,7 @@ class BaseEntityLoadTask : public Task {
private:
BaseEntity* m_entity;
Net::Mode m_mode;
bool m_force_reload = false;
NetJob::Ptr m_task;
};
} // namespace Meta

View file

@ -15,6 +15,7 @@
#include "Index.h"
#include "Application.h"
#include "JsonFormat.h"
#include "QObjectPtr.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)
{
if (mode == Net::Mode::Offline) {
if (mode == Net::Mode::Offline || !APPLICATION->settings()->get("MetaRefreshOnLaunch").toBool()) {
return get(uid, version)->loadTask(mode);
}

View file

@ -32,11 +32,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(
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));
loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online));
loadTask->addTask(this->loadTask(Net::Mode::Online));
loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online, forceReload));
loadTask->addTask(this->loadTask(Net::Mode::Online, forceReload));
return loadTask;
}

View file

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

View file

@ -149,7 +149,7 @@ QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeC
if (sha1.size()) {
auto dl = Net::ApiDownload::makeCached(url, entry, options);
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);
} else {
out.append(Net::ApiDownload::makeCached(url, entry, options));

View file

@ -131,7 +131,8 @@
for (const auto& gpu : gpus) {
QString name = qvariant_cast<QString>(gpu[QStringLiteral("Name")]);
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")]);
for (int i = 0; i + 1 < envList.size(); i += 2) {
env.insert(envList[i], envList[i + 1]);
@ -892,6 +893,14 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
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 << "Main class: " + getMainClass() << emptyLine;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -113,6 +113,12 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d
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);
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
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));
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) {
qWarning() << "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() << 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,
tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
} else {
m_data->networkError = m_request->error();
emit finished(AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
}

View file

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

View file

@ -25,22 +25,72 @@ EnsureAvailableMemory::EnsureAvailableMemory(LaunchTask* parent, MinecraftInstan
void EnsureAvailableMemory::executeTask()
{
const uint64_t available = HardwareInfo::availableRamMiB();
const uint64_t min = m_instance->settings()->get("MinMemAlloc").toUInt();
const uint64_t max = m_instance->settings()->get("MaxMemAlloc").toUInt();
const uint64_t required = std::max(min, max);
#ifdef Q_OS_MACOS
QString text;
switch (MacOSHardwareInfo::memoryPressureLevel()) {
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 (static_cast<double>(required) * 0.9 > static_cast<double>(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(
nullptr, tr("Not enough RAM"),
tr("There is not enough RAM available to launch this instance with the current memory settings.\n\n"
"Required: %1 MiB\nAvailable: %2 MiB\n\n"
"Continue anyway? This may cause slowdowns in the game and your system.")
.arg(required)
.arg(available),
nullptr, tr("Low free memory"),
tr("There might not be enough free RAM to launch this instance with the current memory settings.\n\n"
"Maximum allocated: %1 MiB\nFree: %2 MiB (out of %3 MiB total)\n\n"
"Launch anyway? This may cause slowdowns in the game and your system.")
.arg(max)
.arg(available)
.arg(HardwareInfo::totalRamMiB()),
QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
QMessageBox::StandardButton::No);
@ -59,4 +109,5 @@ void EnsureAvailableMemory::executeTask()
}
emitSucceeded();
#endif
}

View file

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

View file

@ -68,7 +68,12 @@ void PrintInstanceInfo::executeTask()
::runPciconf(log);
#else
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());
#endif
#endif
log.append(HardwareInfo::gpuInfo());
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
{
const auto& cast_other = static_cast<const DataPack&>(other);
if (type == SortType::PACK_FORMAT) {
if (type == SortType::PackFormat) {
auto this_ver = packFormat();
auto other_ver = cast_other.packFormat();

View file

@ -40,25 +40,26 @@
#include <QIcon>
#include <QStyle>
#include "Version.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h"
DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent)
: ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent)
DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent)
: ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent)
{
m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true };
m_columnNames = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Size", "File Name" });
m_columnNamesTranslated =
QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size"), tr("File Name") });
m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::PackFormat,
SortType::Date, SortType::Size, SortType::Filename };
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
{
if (!validateIndex(index))
if (!validateIndex(index)) {
return {};
}
int row = index.row();
int column = index.column();
@ -67,11 +68,13 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
case Qt::BackgroundRole:
return rowBackground(row);
case Qt::DisplayRole:
switch (column) {
case PackFormatColumn: {
const auto& resource = at(row);
return resource.packFormatStr();
}
if (column == PackFormatColumn) {
const auto& resource = at(row);
return resource.packFormatStr();
}
if (column == SizeColumn) {
const auto& resource = at(row);
return resource.sizeStr();
}
break;
case Qt::DecorationRole: {
@ -92,6 +95,8 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
return QSize(32, 32);
}
break;
default:
break;
}
// map the columns to the base equivilents
@ -109,7 +114,14 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const
case ProviderColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn);
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()) {
@ -129,6 +141,8 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
case PackFormatColumn:
case DateColumn:
case ImageColumn:
case SizeColumn:
case FileNameColumn:
return columnNames().at(section);
default:
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.");
case DateColumn:
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:
return {};
}
@ -160,7 +178,7 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien
int DataPackFolderModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : NUM_COLUMNS;
return parent.isValid() ? 0 : NumColumns;
}
Resource* DataPackFolderModel::createResource(const QFileInfo& file)
@ -170,5 +188,5 @@ Resource* DataPackFolderModel::createResource(const QFileInfo& file)
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 "DataPack.h"
#include "ResourcePack.h"
class DataPackFolderModel : public ResourceFolderModel {
Q_OBJECT
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;
@ -56,7 +64,7 @@ class DataPackFolderModel : public ResourceFolderModel {
int columnCount(const QModelIndex& parent) const override;
[[nodiscard]] Resource* createResource(const QFileInfo& file) override;
[[nodiscard]] Task* createParseTask(Resource&) override;
[[nodiscard]] Task* createParseTask(Resource& /*unused*/) override;
RESOURCE_HELPERS(DataPack)
};

View file

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

View file

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

View file

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

View file

@ -46,7 +46,6 @@
#include "Mod.h"
#include "ResourceFolderModel.h"
#include "minecraft/Component.h"
#include "minecraft/mod/Resource.h"
class BaseInstance;
@ -59,7 +58,7 @@ class QFileSystemWatcher;
class ModFolderModel : public ResourceFolderModel {
Q_OBJECT
public:
enum Columns {
enum Columns : std::uint8_t {
ActiveColumn = 0,
ImageColumn,
NameColumn,
@ -73,11 +72,12 @@ class ModFolderModel : public ResourceFolderModel {
ReleaseTypeColumn,
RequiresColumn,
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;
@ -85,7 +85,7 @@ class ModFolderModel : public ResourceFolderModel {
int columnCount(const QModelIndex& parent) const override;
[[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); }
[[nodiscard]] Task* createParseTask(Resource&) override;
[[nodiscard]] Task* createParseTask(Resource& /*unused*/) override;
bool isValid();
@ -97,11 +97,11 @@ class ModFolderModel : public ResourceFolderModel {
RESOURCE_HELPERS(Mod)
public:
QStringList requiresList(QString id);
QStringList requiredByList(QString id);
QStringList requiresList(const QString& id);
QStringList requiredByList(const QString& id);
private slots:
void onParseSucceeded(int ticket, QString resource_id) override;
void onParseSucceeded(int ticket, const QString& resourceId) override;
void onParseFinished();
private:

View file

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

View file

@ -45,7 +45,7 @@
class BaseInstance;
enum class ResourceType {
enum class ResourceType : std::uint8_t {
UNKNOWN, //!< Indicates an unspecified resource type.
ZIPFILE, //!< The resource is a zip file containing the resource's class files.
SINGLEFILE, //!< The resource is a single file (not a zip file).
@ -55,32 +55,33 @@ enum class ResourceType {
QDebug operator<<(QDebug debug, ResourceType type);
enum class ResourceStatus {
INSTALLED, // Both JAR and Metadata are present
NOT_INSTALLED, // Only the Metadata is present
NO_METADATA, // Only the JAR is present
UNKNOWN, // Default status
enum class ResourceStatus : std::uint8_t {
Installed, // Both JAR and Metadata are present
NotInstalled, // Only the Metadata is present
NoMetadata, // Only the JAR is present
Unknown, // Default status
};
QDebug operator<<(QDebug debug, ResourceStatus status);
enum class SortType {
NAME,
DATE,
VERSION,
ENABLED,
PACK_FORMAT,
PROVIDER,
SIZE,
SIDE,
MC_VERSIONS,
LOADERS,
RELEASE_TYPE,
REQUIRES,
REQUIRED_BY,
enum class SortType : std::uint8_t {
Name,
Date,
Version,
Enabled,
PackFormat,
Provider,
Size,
Side,
McVersions,
Loaders,
ReleaseType,
Requires,
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
* for display and house-keeping purposes.
@ -92,20 +93,19 @@ class Resource : public QObject {
Q_DISABLE_COPY(Resource)
public:
using Ptr = shared_qobject_ptr<Resource>;
using WeakPtr = QPointer<Resource>;
Resource(QObject* parent = nullptr);
Resource(QFileInfo file_info);
Resource(QString file_path) : Resource(QFileInfo(file_path)) {}
Resource(QFileInfo fileInfo);
Resource(const QString& filePath) : Resource(QFileInfo(filePath)) {}
~Resource() override = default;
void setFile(QFileInfo file_info);
void setFile(QFileInfo fileInfo);
void parseFile();
auto fileinfo() const -> QFileInfo { return m_file_info; }
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; }
bool enabled() const { return m_enabled; }
auto getOriginalFileName() const -> QString;
@ -138,7 +138,7 @@ class Resource : public QObject {
* = 0: 'this' is equal to '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),
* or if such filter includes the Resource (true).
@ -163,9 +163,9 @@ class Resource : public QObject {
}
// 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.
auto destroyMetadata(const QDir& index_dir) -> void;
auto destroyMetadata(const QDir& indexDir) -> void;
auto isSymLink() const -> bool { return m_file_info.isSymLink(); }
@ -195,7 +195,7 @@ class Resource : public QObject {
ResourceType m_type = ResourceType::UNKNOWN;
/* Installation status of the resource. */
ResourceStatus m_status = ResourceStatus::UNKNOWN;
ResourceStatus m_status = ResourceStatus::Unknown;
std::shared_ptr<Metadata::ModStruct> m_metadata = nullptr;

View file

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

View file

@ -61,7 +61,7 @@ class QSortFilterProxyModel;
class ResourceFolderModel : public QAbstractListModel {
Q_OBJECT
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;
virtual QString id() const { return "resource"; }
@ -93,13 +93,13 @@ class ResourceFolderModel : public QAbstractListModel {
*/
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.
*
* 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 void deleteMetadata(const QModelIndexList&);
@ -125,7 +125,7 @@ class ResourceFolderModel : public QAbstractListModel {
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.
*
@ -137,12 +137,12 @@ class ResourceFolderModel : public QAbstractListModel {
/* Qt behavior */
/* 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 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;
@ -171,18 +171,19 @@ class ResourceFolderModel : public QAbstractListModel {
QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
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 {
public:
explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
protected:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight) const override;
};
QString instDirPath() const;
BaseInstance* instance() const { return m_instance; }
signals:
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
* 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.
*
@ -214,10 +215,10 @@ class ResourceFolderModel : public QAbstractListModel {
* 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:
void directoryChanged(QString);
void directoryChanged(const QString&);
/** 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
* if the resource is complex and has more stuff to parse.
*/
virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id);
virtual void onParseSucceeded(int ticket, const QString& resourceId);
virtual void onParseFailed(int ticket, const QString& resourceId);
protected:
// 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!
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE };
QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" };
QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") };
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive };
QList<bool> m_columnsHideable = { false, false, true, true, true };
QList<SortType> m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Date,
SortType::Provider, SortType::Size, SortType::Filename };
QStringList m_columnNames = { "Enable", "Name", "Last Modified", "Provider", "Size", "File Name" };
QStringList m_columnNamesTranslated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") };
QList<QHeaderView::ResizeMode> m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
QList<bool> m_columnsHideable = { false, false, true, true, true, true };
QDir m_dir;
BaseInstance* m_instance;
QFileSystemWatcher m_watcher;
bool m_is_watching = false;
bool m_isWatching = false;
bool m_is_indexed;
bool m_first_folder_load = true;
bool m_isIndexed;
bool m_firstFolderLoad = true;
Task::Ptr m_current_update_task = nullptr;
bool m_scheduled_update = false;
Task::Ptr m_currentUpdateTask = nullptr;
bool m_scheduledUpdate = false;
QList<Resource::Ptr> m_resources;
// 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
ConcurrentTask m_resourceResolver;
bool m_resourceResolverRunning = false;
QMap<int, Task::Ptr> m_active_parse_tasks;
std::atomic<int> m_next_resolution_ticket = 0;
QMap<int, Task::Ptr> m_activeParseTasks;
std::atomic<int> m_nextResolutionTicket = 0;
};

View file

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

View file

@ -18,7 +18,7 @@ class ShaderPackFolderModel : public ResourceFolderModel {
[[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; }

View file

@ -36,28 +36,30 @@
#include "TexturePackFolderModel.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)
: ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent)
TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent)
: ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent)
{
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true };
m_columnNames = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size", "File Name" });
m_columnNamesTranslated =
QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") });
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 };
m_columnsHideable = { false, true, false, true, true, true, true };
}
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
{
if (!validateIndex(index))
if (!validateIndex(index)) {
return {};
}
int row = index.row();
int column = index.column();
@ -76,6 +78,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
return QSize(32, 32);
}
break;
default:
break;
}
// map the columns to the base equivilents
@ -96,6 +100,11 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const
case SizeColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn);
break;
case FileNameColumn:
mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn);
break;
default:
break;
}
if (mappedIndex.isValid()) {
@ -116,6 +125,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
case ImageColumn:
case ProviderColumn:
case SizeColumn:
case FileNameColumn:
return columnNames().at(section);
default:
return {};
@ -132,6 +142,8 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
return tr("The source provider of the texture pack.");
case SizeColumn:
return tr("The size of the texture pack.");
case FileNameColumn:
return tr("The file name of the texture pack.");
default:
return {};
}
@ -145,5 +157,5 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or
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
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;
@ -56,7 +65,7 @@ class TexturePackFolderModel : public ResourceFolderModel {
int columnCount(const QModelIndex& parent) const override;
[[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)
};

View file

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

View file

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

View file

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

View file

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

View file

@ -99,7 +99,7 @@ void EnsureMetadataTask::executeTask()
}
// 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!";
emitReady(resource);
continue;
@ -263,7 +263,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
{
QHash<QString, QString> addonIds;
for (auto const& data : m_tempVersions)
for (const auto& data : m_tempVersions)
addonIds.insert(data.addonId.toString(), data.hash);
Task::Ptr proj_task;
@ -404,7 +404,7 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask()
Task::Ptr EnsureMetadataTask::flameProjectsTask()
{
QHash<QString, QString> addonIds;
for (auto const& hash : m_resources.keys()) {
for (const auto& hash : m_resources.keys()) {
if (m_tempVersions.contains(hash)) {
auto data = m_tempVersions.find(hash).value();

View file

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

View file

@ -148,9 +148,9 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
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] {
auto pack = args.pack;
@ -284,7 +284,7 @@ QString ResourceAPI::mapMCVersionToModrinth(Version v) const
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);
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 netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network());
netJob->setAskRetry(askRetry);
auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url));
netJob->addNetAction(action);

View file

@ -115,10 +115,10 @@ class ResourceAPI {
public slots:
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 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;
virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,9 @@
namespace ExportToModList {
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, QString lineTemplate);
} // namespace ExportToModList

View file

@ -63,12 +63,12 @@ void PackInstallTask::copySettings()
instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString());
}
auto components = instance.getPackProfile();
auto* components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_pack.mcVersion, true);
auto modloader = m_pack.loaderType;
if (modloader.has_value())
if (modloader.has_value()) {
switch (modloader.value()) {
case ModPlatform::NeoForge: {
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);
break;
}
case ModPlatform::Cauldron:
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:
default:
break;
}
}
components->saveNow();
instance.setName(name());
if (m_instIcon == "default")
if (m_instIcon == "default") {
m_instIcon = "ftb_logo";
}
instance.setIconKey(m_instIcon);
}
emitSucceeded();

View file

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

View file

@ -16,7 +16,10 @@
#include "net/ChecksumValidator.h"
#include "net/ApiDownload.h"
#include "net/ApiHeaderProxy.h"
#include "net/NetJob.h"
#include "modplatform/ModIndex.h"
#include "settings/INISettingsObject.h"
#include "ui/dialogs/CustomMessageBox.h"
@ -29,114 +32,119 @@
bool ModrinthCreationTask::abort()
{
if (!canAbort())
if (!canAbort()) {
return false;
}
if (m_task)
if (m_task) {
m_task->abort();
}
return InstanceCreationTask::abort();
}
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?
BaseInstance* inst;
if (auto original_id = originalInstanceID(); !original_id.isEmpty()) {
inst = instance_list->getInstanceById(original_id);
BaseInstance* inst = nullptr;
if (auto originalId = originalInstanceID(); !originalId.isEmpty()) {
inst = instanceList->getInstanceById(originalId);
Q_ASSERT(inst);
} else {
inst = instance_list->getInstanceByManagedName(originalName());
inst = instanceList->getInstanceByManagedName(originalName());
if (!inst) {
inst = instance_list->getInstanceById(originalName());
inst = instanceList->getInstanceById(originalName());
if (!inst)
if (!inst) {
return false;
}
}
}
QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json");
if (!parseManifest(index_path, m_files, true, false))
QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
if (!parseManifest(indexPath, m_files, true, false)) {
return false;
}
auto version_name = inst->getManagedPackVersionName();
auto versionName = inst->getManagedPackVersionName();
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()) {
auto should_update = askIfShouldUpdate(m_parent, version_str);
if (should_update == ShouldUpdate::SkipUpdating)
auto shouldUpdate = askIfShouldUpdate(m_parent, versionStr);
if (shouldUpdate == ShouldUpdate::SkipUpdating) {
return false;
if (should_update == ShouldUpdate::Cancel) {
}
if (shouldUpdate == ShouldUpdate::Cancel) {
m_abort = true;
return false;
}
}
// 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"));
QFileInfo old_index_file(old_index_path);
if (old_index_file.exists()) {
std::vector<File> old_files;
parseManifest(old_index_path, old_files, false, false);
QString oldIndexPath(FS::PathCombine(oldIndexFolder, "modrinth.index.json"));
QFileInfo oldIndexFile(oldIndexPath);
if (oldIndexFile.exists()) {
std::vector<File> oldFiles;
parseManifest(oldIndexPath, oldFiles, false, false);
// Let's remove all duplicated, identical resources!
auto files_iterator = m_files.begin();
auto filesIterator = m_files.begin();
begin:
while (files_iterator != m_files.end()) {
auto const& file = *files_iterator;
while (filesIterator != m_files.end()) {
const auto& file = *filesIterator;
auto old_files_iterator = old_files.begin();
while (old_files_iterator != old_files.end()) {
auto const& old_file = *old_files_iterator;
auto oldFilesIterator = oldFiles.begin();
while (oldFilesIterator != oldFiles.end()) {
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";
files_iterator = m_files.erase(files_iterator);
old_files_iterator = old_files.erase(old_files_iterator);
filesIterator = m_files.erase(filesIterator);
oldFilesIterator = oldFiles.erase(oldFilesIterator);
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,
// so we're fine removing them!
if (!old_files.empty()) {
for (auto const& file : old_files) {
scheduleToDelete(m_parent, old_minecraft_dir, file.path, true);
if (!oldFiles.empty()) {
for (const auto& file : oldFiles) {
scheduleToDelete(m_parent, oldMinecraftDir, file.path, true);
}
}
// 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?
// FIXME: We may want to do something about disabled mods.
auto old_overrides = Override::readOverrides("overrides", old_index_folder);
for (const auto& entry : old_overrides) {
scheduleToDelete(m_parent, old_minecraft_dir, entry);
auto oldOverrides = Override::readOverrides("overrides", oldIndexFolder);
for (const auto& entry : oldOverrides) {
scheduleToDelete(m_parent, oldMinecraftDir, entry);
}
auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder);
for (const auto& entry : old_client_overrides) {
scheduleToDelete(m_parent, old_minecraft_dir, entry);
auto oldClientOverrides = Override::readOverrides("client-overrides", oldIndexFolder);
for (const auto& entry : oldClientOverrides) {
scheduleToDelete(m_parent, oldMinecraftDir, entry);
}
} else {
// We don't have an old index file, so we may duplicate stuff!
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 "
"of the files to be duplicated. Do you want to continue?"),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel);
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 "
"of the files to be duplicated. Do you want to continue?"),
QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel);
if (dialog->exec() == QDialog::DialogCode::Rejected) {
m_abort = true;
@ -158,39 +166,40 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
{
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");
if (m_files.empty() && !parseManifest(index_path, m_files, true, true))
QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
if (m_files.empty() && !parseManifest(indexPath, m_files, true, true)) {
return nullptr;
}
// 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"));
FS::ensureFilePathExists(new_index_place);
FS::move(index_path, new_index_place);
QString newIndexPlace(FS::PathCombine(parentFolder, "modrinth.index.json"));
FS::ensureFilePathExists(newIndexPlace);
FS::move(indexPath, newIndexPlace);
auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
auto override_path = FS::PathCombine(m_stagingPath, "overrides");
if (QFile::exists(override_path)) {
auto overridePath = FS::PathCombine(m_stagingPath, "overrides");
if (QFile::exists(overridePath)) {
// 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
if (!FS::move(override_path, mcPath)) {
if (!FS::move(overridePath, mcPath)) {
setError(tr("Could not rename the overrides folder:\n") + "overrides");
return nullptr;
}
}
// Do client overrides
auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides");
if (QFile::exists(client_override_path)) {
auto clientOverridePath = FS::PathCombine(m_stagingPath, "client-overrides");
if (QFile::exists(clientOverridePath)) {
// 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
if (!FS::overrideFolder(mcPath, client_override_path)) {
if (!FS::overrideFolder(mcPath, clientOverridePath)) {
setError(tr("Could not rename the client overrides folder:\n") + "client overrides");
return nullptr;
}
@ -200,18 +209,27 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
auto instanceSettings = std::make_unique<INISettingsObject>(configPath);
auto instance = std::make_unique<MinecraftInstance>(m_globalSettings, std::move(instanceSettings), m_stagingPath);
auto components = instance->getPackProfile();
auto* components = instance->getPackProfile();
components->buildingFromScratch();
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);
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);
if (!m_forge_version.isEmpty())
loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Quilt);
}
if (!m_forge_version.isEmpty()) {
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);
loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::NeoForge);
}
if (m_instIcon != "default") {
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)
if (!m_managed_id.isEmpty())
if (!m_managed_id.isEmpty()) {
instance->setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version());
else
} else {
instance->setManagedPack("modrinth", "", name(), "", "");
}
instance->setName(name());
instance->saveNow();
auto downloadMods = makeShared<NetJob>(tr("Mod Download Modrinth"), APPLICATION->network());
auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path);
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
auto rootModpackPath = FS::PathCombine(m_stagingPath, m_root_path);
auto rootModpackUrl = QUrl::fromLocalFile(rootModpackPath);
// TODO make this work with other sorts of resource
QHash<QString, Resource*> resources;
for (auto& file : m_files) {
auto fileName = file.path;
fileName = FS::RemoveInvalidPathChars(fileName);
auto file_path = FS::PathCombine(root_modpack_path, fileName);
if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) {
auto filePath = FS::PathCombine(rootModpackPath, fileName);
if (!rootModpackUrl.isParentOf(QUrl::fromLocalFile(filePath))) {
// 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.")
.arg(fileName));
return nullptr;
}
if (fileName.startsWith("mods/")) {
auto mod = new Mod(file_path);
auto* mod = new Mod(filePath);
ModDetails d;
d.mod_id = file_path;
d.mod_id = filePath;
mod->setDetails(d);
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));
return nullptr;
}
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
qDebug() << "Will try to download" << file.downloads.front() << "to" << filePath;
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));
downloadMods->addNetAction(dl);
if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
auto param = dl.toWeakRef();
connect(dl.get(), &Task::failed, [&file, file_path, param, downloadMods] {
auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
connect(dl.get(), &Task::failed, [&file, filePath, param, downloadMods, meta] {
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));
downloadMods->addNetAction(ndl);
if (auto shared = param.lock())
if (auto shared = param.lock()) {
shared->succeeded();
}
});
}
}
bool ended_well = false;
bool endedWell = false;
connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; });
connect(downloadMods.get(), &NetJob::failed, [this, &ended_well](const QString& reason) {
ended_well = false;
connect(downloadMods.get(), &NetJob::succeeded, this, [&endedWell]() { endedWell = true; });
connect(downloadMods.get(), &NetJob::failed, [this, &endedWell](const QString& reason) {
endedWell = false;
setError(reason);
});
connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit);
@ -293,8 +322,8 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
loop.exec();
if (!ended_well) {
for (auto resource : resources) {
if (!endedWell) {
for (auto* resource : resources) {
delete resource;
}
return nullptr;
@ -303,7 +332,7 @@ std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
QEventLoop ensureMetaLoop;
QDir folder = FS::PathCombine(instance->modsRoot(), ".index");
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::progress, [this](qint64 current, qint64 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;
ensureMetaLoop.exec();
for (auto resource : resources) {
for (auto* resource : resources) {
delete resource;
}
resources.clear();
// Update information of the already installed instance, if any.
if (m_instance && ended_well) {
if (m_instance && endedWell) {
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
// is preserved, but if we're using the original one, we update the version string.
// NOTE: This needs to come before the copyManagedPack call!
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->copyManagedPack(*instance);
}
if (ended_well) {
if (endedWell) {
return instance;
}
return nullptr;
}
bool ModrinthCreationTask::parseManifest(const QString& index_path,
std::vector<File>& files,
bool set_internal_data,
bool show_optional_dialog)
bool ModrinthCreationTask::parseManifest(const QString& indexPath, std::vector<File>& files, bool setInternalData, bool showOptionalDialog)
{
try {
auto doc = Json::requireDocument(index_path);
auto doc = Json::requireDocument(indexPath);
auto obj = Json::requireObject(doc, "modrinth.index.json");
int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json");
if (formatVersion == 1) {
@ -357,9 +384,10 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
throw JSONValidationError("Unknown game: " + game);
}
if (set_internal_data) {
if (m_managed_version_id.isEmpty())
if (setInternalData) {
if (m_managed_version_id.isEmpty()) {
m_managed_version_id = obj["versionId"].toString();
}
m_managed_name = obj["name"].toString();
}
@ -375,7 +403,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path,
QString support = env["client"].toString("unsupported");
if (support == "unsupported") {
continue;
} else if (support == "optional") {
}
if (support == "optional") {
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
// (as Modrinth seems to incorrectly handle spaces)
auto download_arr = modInfo["downloads"].toArray();
for (auto download : download_arr) {
auto downloadArr = modInfo["downloads"].toArray();
for (auto download : downloadArr) {
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()
<< QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(download_url.toString(), file.path);
if (is_last && file.downloads.isEmpty())
<< QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(downloadUrl.toString(), file.path);
if (isLast && file.downloads.isEmpty()) {
throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
}
} 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 (show_optional_dialog) {
if (showOptionalDialog) {
QStringList oFiles;
for (auto file : optionalFiles)
for (const auto& file : optionalFiles) {
oFiles.push_back(file.path);
}
OptionalModDialog optionalModDialog(m_parent, oFiles);
if (optionalModDialog.exec() == QDialog::Rejected) {
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");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
QString name = it.key();

View file

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

View file

@ -18,28 +18,30 @@
*/
#include "net/ApiDownload.h"
#include <utility>
#include "net/ApiHeaderProxy.h"
namespace Net {
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>());
return dl;
}
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>());
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);
dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>());
auto dl = Download::makeFile(std::move(url), std::move(path), options);
dl->addHeaderProxy(std::make_unique<ApiHeaderProxy>(std::move(meta)));
return dl;
}

View file

@ -20,13 +20,17 @@
#pragma once
#include "Download.h"
#include "net/ApiHeaderProxy.h"
namespace Net {
namespace ApiDownload {
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);
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 Net

View file

@ -23,27 +23,64 @@
#include "BuildConfig.h"
#include "net/HeaderProxy.h"
#include <QJsonDocument>
#include <QJsonObject>
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 {
public:
ApiHeaderProxy() : HeaderProxy() {}
virtual ~ApiHeaderProxy() = default;
ApiHeaderProxy() = default;
explicit ApiHeaderProxy(ModrinthDownloadMeta meta) : m_meta(std::move(meta)) {}
~ApiHeaderProxy() override = default;
public:
virtual QList<HeaderPair> headers(const QNetworkRequest& request) const override
QList<HeaderPair> headers(const QNetworkRequest& request) const override
{
QList<HeaderPair> hdrs;
if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) {
hdrs.append({ "x-api-key", APPLICATION->getFlameAPIKey().toUtf8() });
} else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() ||
request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) {
const auto host = request.url().host();
if (APPLICATION->capabilities() & Application::SupportsFlame &&
(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();
if (!token.isNull())
hdrs.append({ "Authorization", token.toUtf8() });
if (!token.isNull()) {
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;
};
private:
ModrinthDownloadMeta m_meta;
};
} // namespace Net

View file

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

View file

@ -182,7 +182,7 @@ auto HttpMetaCache::evictAll() -> bool
}
map.entry_list.clear();
// 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;
}

View file

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

View file

@ -40,4 +40,18 @@ inline bool isApplicationError(QNetworkReply::NetworkError x)
QNetworkReply::UnknownContentError };
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

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)
{
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->addHeaderProxy(std::make_unique<Net::RawHeaderProxy>(QList<Net::HeaderPair>{
{ "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 <AssertHelpers.h>
#include <FileSystem.h>
#include <QDebug>
@ -62,11 +64,10 @@ bool INIFile::saveFile(QString fileName)
_settings_obj.sync();
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)
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;
}
@ -178,9 +179,9 @@ bool INIFile::loadFile(QString fileName)
if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) {
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)
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;
}
if (!_settings_obj.value("ConfigVersion").isValid()) {

View file

@ -48,6 +48,13 @@ Task::Task(bool show_debug) : m_show_debug(show_debug)
setAutoDelete(false);
}
Task::~Task()
{
if (isRunning()) {
qCWarning(taskLogC) << "Task" << describe() << "disposed while running!";
}
}
void Task::setStatus(const QString& new_status)
{
if (m_status != new_status) {

View file

@ -94,7 +94,7 @@ class Task : public QObject, public QRunnable {
public:
explicit Task(bool show_debug_log = true);
virtual ~Task() = default;
~Task() override;
bool isRunning() const;
bool isFinished() const;
@ -165,7 +165,7 @@ class Task : public QObject, public QRunnable {
//! used by external code to ask the task to abort
virtual bool abort()
{
if (canAbort())
if (canAbort() && isRunning())
emitAborted();
return canAbort();
}

View file

@ -183,8 +183,7 @@ void POTranslatorPrivate::reload()
nextFuzzy = true;
}
} else if (line.startsWith('"')) {
QByteArray temp;
QByteArray* out = &temp;
QByteArray* out = nullptr;
switch (mode) {
case Mode::First:

View file

@ -36,13 +36,9 @@
#include "TranslationsModel.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QLibraryInfo>
#include <QLocale>
#include <QTranslator>
#include <locale>
#include <memory>
#include <utility>
#include "BuildConfig.h"
#include "FileSystem.h"
@ -55,18 +51,26 @@
#include "Application.h"
#include "settings/SettingsObject.h"
const static QLatin1String defaultLangCode("en_US");
static constexpr QLatin1String g_defaultLangCode("en_US");
enum class FileType { NONE, QM, PO };
namespace {
enum class FileType : std::uint8_t { None, Qm, Po };
QString getSystemLocaleName()
{
return QLocale::system().name();
}
QString getSystemLanguage()
{
return getSystemLocaleName().split('_').front();
}
} // namespace
struct Language {
Language() { updated = true; }
Language(const QString& _key)
{
key = _key;
locale = QLocale(key);
updated = (key == defaultLangCode);
}
Language() : updated(true) {}
explicit Language(QString _key) : key(std::move(_key)), updated(key == g_defaultLangCode) { locale = QLocale(key); }
QString languageName() const
{
@ -98,12 +102,12 @@ struct Language {
float percentTranslated() const
{
if (total == 0) {
return 100.0f;
return 100.0F;
}
return 100.0f * float(translated) / float(total);
return 100.0F * static_cast<float>(translated) / static_cast<float>(total);
}
void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy)
void setTranslationStats(const unsigned _translated, const unsigned _untranslated, const unsigned _fuzzy)
{
translated = _translated;
untranslated = _untranslated;
@ -115,18 +119,18 @@ struct Language {
bool isIdenticalTo(const Language& other) const
{
return (key == other.key && file_name == other.file_name && file_size == other.file_size && file_sha1 == other.file_sha1 &&
return (key == other.key && fileName == other.fileName && fileSize == other.fileSize && fileSha1 == other.fileSha1 &&
translated == other.translated && fuzzy == other.fuzzy && total == other.fuzzy && localFileType == other.localFileType);
}
Language& apply(Language& other)
Language& apply(const Language& other)
{
if (!isOfSameNameAs(other)) {
return *this;
}
file_name = other.file_name;
file_size = other.file_size;
file_sha1 = other.file_sha1;
fileName = other.fileName;
fileSize = other.fileSize;
fileSha1 = other.fileSha1;
translated = other.translated;
fuzzy = other.fuzzy;
total = other.total;
@ -138,47 +142,44 @@ struct Language {
QLocale locale;
bool updated;
QString file_name = QString();
std::size_t file_size = 0;
QString file_sha1 = QString();
QString fileName = QString();
std::size_t fileSize = 0;
QString fileSha1 = QString();
unsigned translated = 0;
unsigned untranslated = 0;
unsigned fuzzy = 0;
unsigned total = 0;
FileType localFileType = FileType::NONE;
FileType localFileType = FileType::None;
};
struct TranslationsModel::Private {
QDir m_dir;
// initial state is just english
QList<Language> m_languages = { Language(defaultLangCode) };
QList<Language> m_languages = { Language(g_defaultLangCode) };
QString m_selectedLanguage = defaultLangCode;
std::unique_ptr<QTranslator> m_qt_translator;
std::unique_ptr<QTranslator> m_app_translator;
QString m_selectedLanguage = g_defaultLangCode;
std::unique_ptr<QTranslator> m_qtTranslator;
std::unique_ptr<QTranslator> m_appTranslator;
Net::Download* m_index_task;
Net::Download* m_indexTask = nullptr;
QString m_downloadingTranslation;
NetJob::Ptr m_dl_job;
NetJob::Ptr m_index_job;
NetJob::Ptr m_downloadJob;
NetJob::Ptr m_indexJob;
QString m_nextDownload;
std::unique_ptr<POTranslator> m_po_translator;
QFileSystemWatcher* watcher;
QFileSystemWatcher* watcher = nullptr;
const QString m_system_locale = QLocale::system().name();
const QString m_system_language = m_system_locale.split('_').front();
bool no_language_set = false;
bool m_noLanguageSet = false;
};
TranslationsModel::TranslationsModel(QString path, QObject* parent) : QAbstractListModel(parent)
TranslationsModel::TranslationsModel(const QString& path, QObject* parent) : QAbstractListModel(parent)
{
d.reset(new Private);
d = std::make_unique<Private>();
d->m_dir.setPath(path);
d->m_selectedLanguage = APPLICATION->settings()->get("Language").toString();
FS::ensureFolderPathExists(path);
reloadLocalFiles();
@ -187,12 +188,12 @@ TranslationsModel::TranslationsModel(QString path, QObject* parent) : QAbstractL
d->watcher->addPath(d->m_dir.canonicalPath());
}
TranslationsModel::~TranslationsModel() {}
TranslationsModel::~TranslationsModel() = default;
void TranslationsModel::translationDirChanged(const QString& path)
{
qDebug() << "Dir changed:" << path;
if (!d->no_language_set) {
if (!d->m_noLanguageSet) {
reloadLocalFiles();
}
selectLanguage(selectedLanguage());
@ -201,25 +202,21 @@ void TranslationsModel::translationDirChanged(const QString& path)
void TranslationsModel::indexReceived()
{
qDebug() << "Got translations index!";
d->m_index_job.reset();
d->m_indexJob.reset();
reloadLocalFiles();
if (d->no_language_set) {
reloadLocalFiles();
auto language = d->m_system_locale;
if (d->m_noLanguageSet) {
auto language = getSystemLocaleName();
if (!findLanguageAsOptional(language).has_value()) {
language = d->m_system_language;
language = getSystemLanguage();
}
selectLanguage(language);
if (selectedLanguage() != defaultLangCode) {
updateLanguage(selectedLanguage());
}
APPLICATION->settings()->set("Language", selectedLanguage());
d->no_language_set = false;
d->m_noLanguageSet = false;
}
else if (d->m_selectedLanguage != defaultLangCode) {
downloadTranslation(d->m_selectedLanguage);
if (selectedLanguage() != g_defaultLangCode) {
updateLanguage(selectedLanguage());
}
}
@ -235,27 +232,27 @@ void readIndex(const QString& path, QMap<QString, Language>& languages)
}
try {
auto toplevel_doc = Json::requireDocument(data);
auto doc = Json::requireObject(toplevel_doc);
auto file_type = Json::requireString(doc, "file_type");
if (file_type != "MMC-TRANSLATION-INDEX") {
qCritical() << "Translations Download Failed: index file is of unknown file type" << file_type;
auto toplevelDoc = Json::requireDocument(data);
auto doc = Json::requireObject(toplevelDoc);
auto fileType = Json::requireString(doc, "file_type");
if (fileType != "MMC-TRANSLATION-INDEX") {
qCritical() << "Translations Download Failed: index file is of unknown file type" << fileType;
return;
}
auto version = Json::requireInteger(doc, "version");
if (version > 2) {
qCritical() << "Translations Download Failed: index file is of unknown format version" << file_type;
qCritical() << "Translations Download Failed: index file is of unknown format version" << fileType;
return;
}
auto langObjs = Json::requireObject(doc, "languages");
for (auto iter = langObjs.begin(); iter != langObjs.end(); iter++) {
for (auto iter = langObjs.begin(); iter != langObjs.end(); ++iter) {
Language lang(iter.key());
auto langObj = Json::requireObject(iter.value());
lang.setTranslationStats(langObj["translated"].toInt(), langObj["untranslated"].toInt(), langObj["fuzzy"].toInt());
lang.file_name = Json::requireString(langObj, "file");
lang.file_sha1 = Json::requireString(langObj, "sha1");
lang.file_size = Json::requireInteger(langObj, "size");
lang.fileName = Json::requireString(langObj, "file");
lang.fileSha1 = Json::requireString(langObj, "sha1");
lang.fileSize = Json::requireInteger(langObj, "size");
languages.insert(lang.key, lang);
}
@ -267,20 +264,25 @@ void readIndex(const QString& path, QMap<QString, Language>& languages)
void TranslationsModel::reloadLocalFiles()
{
QMap<QString, Language> languages = { { defaultLangCode, Language(defaultLangCode) } };
QMap<QString, Language> languages = { { g_defaultLangCode, Language(g_defaultLangCode) } };
readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages);
const auto indexPath = d->m_dir.absoluteFilePath("index_v2.json");
if (!QFileInfo::exists(indexPath)) {
downloadIndex();
return;
}
readIndex(indexPath, languages);
auto entries = d->m_dir.entryInfoList({ "mmc_*.qm", "*.po" }, QDir::Files | QDir::NoDotAndDotDot);
for (auto& entry : entries) {
auto completeSuffix = entry.completeSuffix();
QString langCode;
FileType fileType = FileType::NONE;
FileType fileType = FileType::None;
if (completeSuffix == "qm") {
langCode = entry.baseName().remove(0, 4);
fileType = FileType::QM;
fileType = FileType::Qm;
} else if (completeSuffix == "po") {
langCode = entry.baseName();
fileType = FileType::PO;
fileType = FileType::Po;
} else {
continue;
}
@ -288,13 +290,14 @@ void TranslationsModel::reloadLocalFiles()
auto langIter = languages.find(langCode);
if (langIter != languages.end()) {
auto& language = *langIter;
if (int(fileType) > int(language.localFileType)) {
// TODO: use std::to_underlying in C++23
if (static_cast<int>(fileType) > static_cast<int>(language.localFileType)) {
language.localFileType = fileType;
}
} else {
if (fileType == FileType::PO) {
if (fileType == FileType::Po) {
Language localFound(langCode);
localFound.localFileType = FileType::PO;
localFound.localFileType = FileType::Po;
languages.insert(langCode, localFound);
}
}
@ -314,7 +317,7 @@ void TranslationsModel::reloadLocalFiles()
emit dataChanged(index(row), index(row));
languages.remove(language.key);
}
iter++;
++iter;
} else {
beginRemoveRows(QModelIndex(), row, row);
iter = d->m_languages.erase(iter);
@ -329,34 +332,38 @@ void TranslationsModel::reloadLocalFiles()
for (auto& language : languages) {
d->m_languages.append(language);
}
std::sort(d->m_languages.begin(), d->m_languages.end(), [this](const Language& a, const Language& b) {
const auto comp = [systemLocale = getSystemLocaleName(), systemLanguage = getSystemLanguage()](const Language& a, const Language& b) {
if (a.key != b.key) {
if (a.key == d->m_system_locale || a.key == d->m_system_language) {
if (a.key == systemLocale || a.key == systemLanguage) {
return true;
}
if (b.key == d->m_system_locale || b.key == d->m_system_language) {
if (b.key == systemLocale || b.key == systemLanguage) {
return false;
}
}
return a.languageName().toLower() < b.languageName().toLower();
});
};
std::ranges::sort(d->m_languages, comp);
endInsertRows();
}
namespace {
enum class Column { Language, Completeness };
enum class Column : std::uint8_t { Language, Completeness };
}
QVariant TranslationsModel::data(const QModelIndex& index, int role) const
QVariant TranslationsModel::data(const QModelIndex& index, const int role) const
{
if (!index.isValid())
return QVariant();
if (!index.isValid()) {
return {};
}
int row = index.row();
auto column = static_cast<Column>(index.column());
const int row = index.row();
const auto column = static_cast<Column>(index.column());
if (row < 0 || row >= d->m_languages.size())
return QVariant();
if (row < 0 || row >= d->m_languages.size()) {
return {};
}
auto& lang = d->m_languages[row];
switch (role) {
@ -378,11 +385,11 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const
case Qt::UserRole:
return lang.key;
default:
return QVariant();
return {};
}
}
QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, int role) const
QVariant TranslationsModel::headerData(int section, const Qt::Orientation orientation, const int role) const
{
auto column = static_cast<Column>(section);
if (role == Qt::DisplayRole) {
@ -417,49 +424,50 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c
return 2;
}
QList<Language>::Iterator TranslationsModel::findLanguage(const QString& key)
QList<Language>::Iterator TranslationsModel::findLanguage(const QString& key) const
{
return std::find_if(d->m_languages.begin(), d->m_languages.end(), [key](Language& lang) { return lang.key == key; });
return std::ranges::find_if(d->m_languages, [key](const Language& lang) { return lang.key == key; });
}
std::optional<Language> TranslationsModel::findLanguageAsOptional(const QString& key)
std::optional<Language> TranslationsModel::findLanguageAsOptional(const QString& key) const
{
auto found = findLanguage(key);
if (found != d->m_languages.end())
if (found != d->m_languages.end()) {
return *found;
}
return {};
}
void TranslationsModel::setUseSystemLocale(bool useSystemLocale)
void TranslationsModel::setUseSystemLocale(const bool useSystemLocale) const
{
APPLICATION->settings()->set("UseSystemLocale", useSystemLocale);
QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode));
QLocale::setDefault(useSystemLocale ? QLocale::system() : QLocale(selectedLanguage()));
}
bool TranslationsModel::selectLanguage(QString key)
bool TranslationsModel::selectLanguage(QString key) const
{
QString& langCode = key;
auto langPtr = findLanguageAsOptional(key);
if (langCode.isEmpty()) {
d->no_language_set = true;
d->m_noLanguageSet = true;
}
if (!langPtr.has_value()) {
qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
langCode = defaultLangCode;
qWarning() << "Selected invalid language" << key << ", defaulting to" << g_defaultLangCode;
langCode = g_defaultLangCode;
} else {
langCode = langPtr->key;
}
// uninstall existing translators if there are any
if (d->m_app_translator) {
QCoreApplication::removeTranslator(d->m_app_translator.get());
d->m_app_translator.reset();
if (d->m_appTranslator) {
QCoreApplication::removeTranslator(d->m_appTranslator.get());
d->m_appTranslator.reset();
}
if (d->m_qt_translator) {
QCoreApplication::removeTranslator(d->m_qt_translator.get());
d->m_qt_translator.reset();
if (d->m_qtTranslator) {
QCoreApplication::removeTranslator(d->m_qtTranslator.get());
d->m_qtTranslator.reset();
}
/*
@ -467,11 +475,11 @@ bool TranslationsModel::selectLanguage(QString key)
* In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
* This function is not reentrant.
*/
QLocale::setDefault(
QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode));
const bool useSystemLocale = APPLICATION->settings()->get("UseSystemLocale").toBool();
QLocale::setDefault(useSystemLocale ? QLocale::system() : QLocale(langCode));
// if it's the default UI language, finish
if (langCode == defaultLangCode) {
if (langCode == g_defaultLangCode) {
d->m_selectedLanguage = langCode;
return true;
}
@ -479,89 +487,88 @@ bool TranslationsModel::selectLanguage(QString key)
// otherwise install new translations
bool successful = false;
// FIXME: this is likely never present. FIX IT.
d->m_qt_translator.reset(new QTranslator());
if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) {
d->m_qtTranslator = std::make_unique<QTranslator>();
if (d->m_qtTranslator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) {
qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "...";
if (!QCoreApplication::installTranslator(d->m_qt_translator.get())) {
if (!QCoreApplication::installTranslator(d->m_qtTranslator.get())) {
qCritical() << "Loading Qt Language File failed.";
d->m_qt_translator.reset();
d->m_qtTranslator.reset();
} else {
successful = true;
}
} else {
d->m_qt_translator.reset();
d->m_qtTranslator.reset();
}
if (langPtr->localFileType == FileType::PO) {
if (langPtr->localFileType == FileType::Po) {
qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po"));
if (!poTranslator->isEmpty()) {
if (!QCoreApplication::installTranslator(poTranslator)) {
delete poTranslator;
d->m_appTranslator = std::make_unique<POTranslator>(FS::PathCombine(d->m_dir.path(), langCode + ".po"));
if (!d->m_appTranslator->isEmpty()) {
if (!QCoreApplication::installTranslator(d->m_appTranslator.get())) {
qCritical() << "Installing Application Language File failed.";
d->m_appTranslator.reset();
} else {
d->m_app_translator.reset(poTranslator);
successful = true;
}
} else {
qCritical() << "Loading Application Language File failed.";
d->m_app_translator.reset();
d->m_appTranslator.reset();
}
} else if (langPtr->localFileType == FileType::QM) {
d->m_app_translator.reset(new QTranslator());
if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) {
} else if (langPtr->localFileType == FileType::Qm) {
d->m_appTranslator = std::make_unique<QTranslator>();
if (d->m_appTranslator->load("mmc_" + langCode, d->m_dir.path())) {
qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
if (!QCoreApplication::installTranslator(d->m_app_translator.get())) {
if (!QCoreApplication::installTranslator(d->m_appTranslator.get())) {
qCritical() << "Installing Application Language File failed.";
d->m_app_translator.reset();
d->m_appTranslator.reset();
} else {
successful = true;
}
} else {
d->m_app_translator.reset();
d->m_appTranslator.reset();
}
} else {
d->m_app_translator.reset();
d->m_appTranslator.reset();
}
d->m_selectedLanguage = langCode;
return successful;
}
QModelIndex TranslationsModel::selectedIndex()
QModelIndex TranslationsModel::selectedIndex() const
{
auto found = findLanguage(d->m_selectedLanguage);
if (found != d->m_languages.end()) {
return index(std::distance(d->m_languages.begin(), found), 0, QModelIndex());
}
return QModelIndex();
return {};
}
QString TranslationsModel::selectedLanguage()
QString TranslationsModel::selectedLanguage() const
{
return d->m_selectedLanguage;
}
void TranslationsModel::downloadIndex()
{
if (d->m_index_job || d->m_dl_job) {
if (d->m_indexJob || d->m_downloadJob) {
return;
}
qDebug() << "Downloading Translations Index...";
d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network()));
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json");
d->m_indexJob.reset(new NetJob("Translations Index", APPLICATION->network()));
const MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json");
entry->setStale(true);
auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + "index_v2.json"), entry);
d->m_index_task = task.get();
d->m_index_job->addNetAction(task);
d->m_index_job->setAskRetry(false);
connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
d->m_index_job->start();
d->m_indexTask = task.get();
d->m_indexJob->addNetAction(task);
d->m_indexJob->setAskRetry(false);
connect(d->m_indexJob.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
connect(d->m_indexJob.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
d->m_indexJob->start();
}
void TranslationsModel::updateLanguage(QString key)
void TranslationsModel::updateLanguage(const QString& key)
{
if (key == defaultLangCode) {
if (key == g_defaultLangCode) {
qWarning() << "Cannot update builtin language" << key;
return;
}
@ -575,9 +582,9 @@ void TranslationsModel::updateLanguage(QString key)
}
}
void TranslationsModel::downloadTranslation(QString key)
void TranslationsModel::downloadTranslation(const QString& key)
{
if (d->m_dl_job) {
if (d->m_downloadJob) {
d->m_nextDownload = key;
return;
}
@ -588,21 +595,21 @@ void TranslationsModel::downloadTranslation(QString key)
}
d->m_downloadingTranslation = key;
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm");
const MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm");
entry->setStale(true);
auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->file_name), entry);
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1));
dl->setProgress(dl->getProgress(), lang->file_size);
auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->fileName), entry);
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->fileSha1));
dl->setProgress(dl->getProgress(), lang->fileSize);
d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network()));
d->m_dl_job->addNetAction(dl);
d->m_dl_job->setAskRetry(false);
d->m_downloadJob.reset(new NetJob("Translation for " + key, APPLICATION->network()));
d->m_downloadJob->addNetAction(dl);
d->m_downloadJob->setAskRetry(false);
connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood);
connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed);
connect(d->m_downloadJob.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood);
connect(d->m_downloadJob.get(), &NetJob::failed, this, &TranslationsModel::dlFailed);
d->m_dl_job->start();
d->m_downloadJob->start();
}
void TranslationsModel::downloadNext()
@ -613,10 +620,10 @@ void TranslationsModel::downloadNext()
}
}
void TranslationsModel::dlFailed(QString reason)
void TranslationsModel::dlFailed(const QString& reason)
{
qCritical() << "Translations Download Failed:" << reason;
d->m_dl_job.reset();
d->m_downloadJob.reset();
downloadNext();
}
@ -627,12 +634,12 @@ void TranslationsModel::dlGood()
if (d->m_downloadingTranslation == d->m_selectedLanguage) {
selectLanguage(d->m_selectedLanguage);
}
d->m_dl_job.reset();
d->m_downloadJob.reset();
downloadNext();
}
void TranslationsModel::indexFailed(QString reason)
void TranslationsModel::indexFailed(const QString& reason) const
{
qCritical() << "Translations Index Download Failed:" << reason;
d->m_index_job.reset();
d->m_indexJob.reset();
}

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