Compare commits

...

286 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
Alexandru Ionut Tripon
194b72f180
Fix CurseForge recommended RAM check (#5310) 2026-04-06 16:36:59 +00:00
Felix Schnabel
68efc9b9df
Fix Flame recommended RAM check
Signed-off-by: Felix Schnabel <f.schnabel@tum.de>
2026-04-06 17:59:27 +02: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
DioEgizio
b0f7ae1223
Add back drag and dropping to screenshots page (#5300) 2026-04-03 21:10:02 +00:00
Alexandru Ionut Tripon
447333c3f9
LaunchController: fix double task finish (#5301) 2026-04-03 20:37:39 +00:00
Octol1ttle
29c4f2f0e8
LaunchController: replace Q_ASSERT_X with regular Q_ASSERT
the info specified in the where/what arguments isn't more helpful compared to the default output of Q_ASSERT

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-03 21:22:24 +05:00
Octol1ttle
ad325960e7
LaunchController: clang-tidy
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-03 21:21:44 +05:00
Octol1ttle
c367cc1c59
LaunchController: fix double finish
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-03 21:15:37 +05:00
leia uwu
35526b53f9
Add back drag and dropping to screenshots page
Fixes #4548

And #1503 was technically fixed when drag and drop was disabled but can also be closed as this does not reintroduce the issue

Signed-off-by: leia uwu <leia@tutamail.com>
2026-04-03 12:24:05 -03:00
Alexandru Ionut Tripon
3656335666
CI: verify clang-tidy config before running (#5291) 2026-04-03 12:08:05 +00:00
Alexandru Ionut Tripon
16bd9c2743
fix heap overflow with unstable version comparation (#5252) 2026-04-03 11:54:09 +00:00
Octol1ttle
a79cb5a9fc
change(CI): run clang-tidy quietly and only for files in compilation database
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-03 16:42:46 +05:00
Octol1ttle
d2eae3b072
change(CI): verify clang-tidy config before running
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-04-03 16:42:45 +05:00
Trial97
087ffb26ba
clang-tidy: fix warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-03 14:39:27 +03:00
Trial97
8427626e56
add modrinth pre-release support to flexVer implementation
extended the flexVer implementation to consider any space that is after
a numeric section as a pre-release.

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-03 13:59:06 +03:00
Trial97
5a0931d3cf
fix heap overflow with unstable version comparation
fixes #5210
fixes #5251 (the removeDuplicates line)

The issue was mostly with the Version parsing and compring
implementation.
Refactored that based on the https://git.sleeping.town/exa/FlexVer
examples.

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-03 13:59:06 +03:00
Alexandru Ionut Tripon
156b7f365e
fix: clang-tidy action (#5292) 2026-04-03 10:34:43 +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
Alexandru Ionut Tripon
67a1aee306
Allow selecting multiple items in Network Error dialog (#5296) 2026-04-03 06:37:28 +00:00
0x189D7997
c58562a304
Allow selecting multiple items in Network Error dialog
Signed-off-by: 0x189D7997 <199489335+0x189D7997@users.noreply.github.com>
2026-04-02 13:20:03 +00:00
Alexandru Ionut Tripon
a3c5f1f6f2
Fix weird utf archive (#5186) 2026-04-02 09:55:40 +00:00
Trial97
9c81e74061
nix: update to llvmPackages_22
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-02 00:41:31 +03:00
Trial97
1f3403677c
update clang-tidy config
feat from @Octol1ttle

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-02 00:40:51 +03:00
Alexandru Ionut Tripon
a4c9e294da
fixes crash on servers with invalid packet (#5289) 2026-04-01 10:24:39 +00:00
Trial97
0689e58ca2 fixes crash on servers with invalid packet
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-04-01 11:28:10 +03:00
DioEgizio
1450ffca18
fix incorrect mod side beeing saved (#5283) 2026-03-31 18:46:02 +00:00
DioEgizio
2b390a4ca3
fix world import (#5282) 2026-03-31 18:45:25 +00:00
DioEgizio
3d1f495bd5
Correctly append PRISMLAUNCHER_DISABLE_GLVULKAN to AppImage env (#5286) 2026-03-31 18:23:36 +00:00
Octol1ttle
9b06c0699c
fix(CI): correctly append PRISMLAUNCHER_DISABLE_GLVULKAN to AppImage environment
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-03-31 23:03:01 +05:00
Trial97
fbc45699c1
fix incorrect mod side beeing saved
fixes #5262

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-31 00:46:46 +03:00
Trial97
83d82c2519
fix world import
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-30 23:52:48 +03:00
Alexandru Ionut Tripon
31be615f7b
chore(nix): update lockfile (#5278) 2026-03-29 16:50:56 +00:00
Seth Flynn
17048a586a
Use Launcher_ENVName for launcher environment variable names (#5226) 2026-03-29 06:56:04 +00:00
github-actions[bot]
071be5f700 chore(nix): update lockfile
Flake lock file updates:

• Updated input 'nixpkgs':
    'https://releases.nixos.org/nixos/25.11/nixos-25.11.7849.812b3986fd15/nixexprs.tar.xz?narHash=sha256-d2Q5VNbc91GloTZNByC4u3JS8Tj5BjfuOF19/vuJ/iM%3D' (2026-03-20)
  → 'https://releases.nixos.org/nixos/25.11/nixos-25.11.8107.1073dad219cb/nixexprs.tar.xz?narHash=sha256-cUgsPWt0NJz21K4i/5191mWaizw4XtT20WFqyxzSuQI%3D' (2026-03-24)
2026-03-29 00:43:39 +00:00
Seth Flynn
01a4a6a528
build(devcontainer): explicitly include vulkan headers (#5276) 2026-03-28 20:19:03 +00:00
Seth Flynn
2b9620b6a6
build(devcontainer): explicitly include vulkan headers
Signed-off-by: Seth Flynn <getchoo@tuta.io>
2026-03-28 15:46:20 -04:00
Alexandru Ionut Tripon
7ffec104dc
chore(deps): update cachix/install-nix-action digest to 96951a3 (#5242) 2026-03-28 10:10:59 +00:00
renovate[bot]
f191947ad5
chore(deps): update cachix/install-nix-action digest to 96951a3 2026-03-28 00:50:01 +00:00
DioEgizio
63a8b43119 chore: clang-format
Signed-off-by: DioEgizio <dioegizio@protonmail.com>
2026-03-27 07:24:15 +01: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
DioEgizio
eb44bdc3b3 fix: fix PRISMLAUNCHER_JAVA_PATHS
also set Launcher_ENVName as parent scope directly

Signed-off-by: DioEgizio <dioegizio@protonmail.com>
2026-03-26 22:07:02 +01:00
Alexandru Ionut Tripon
75f951fec9
Add Renovate labels automatically (#5265) 2026-03-26 20:09:37 +00:00
Alexandru Ionut Tripon
39bc1a72dc
chore(deps): update hendrikmuhs/ccache-action action to v1.2.22 (#5263) 2026-03-26 19:46:20 +00:00
Alexandru Ionut Tripon
43c4223413
chore(deps): update korthout/backport-action action to v4.3.0 (#5264) 2026-03-26 19:45:50 +00:00
Alexandru Ionut Tripon
c7fd66cf97
Revert "Sort modpack entries by version, rather than publishing date" (#5243) 2026-03-26 18:05:19 +00:00
Alexandru Ionut Tripon
1b05e33202
do not delete mod on cancel (#5238) 2026-03-26 18:04:58 +00:00
DioEgizio
1ea0c7570f fix: dehardcode PRISMLAUNCHER_JAVA_PATHS too
Signed-off-by: DioEgizio <dioegizio@protonmail.com>
2026-03-26 18:33:44 +01:00
DioEgizio
bf42cfdcf2 fix: rename LAUNCHER_DISABLE_GLVULKAN to PRISMLAUNCHER_DISABLE_GLVULKAN
for consistency with other env vars

this also introduces LAUNCHER_ENVNAME in BuildConfig/program_info for rebranded configurations

Signed-off-by: DioEgizio <dioegizio@protonmail.com>
2026-03-26 18:33:44 +01:00
Alexandru Ionut Tripon
5ad45a4098
Warn user on launch if there is not enough available RAM (#5249) 2026-03-26 17:00:48 +00:00
Octol1ttle
eed2facb66
change(renovate.json): add labels automatically
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-03-26 21:41:53 +05:00
renovate[bot]
47ad0703b2
chore(deps): update korthout/backport-action action to v4.3.0 2026-03-26 16:28:19 +00:00
renovate[bot]
d0ac15a275
chore(deps): update hendrikmuhs/ccache-action action to v1.2.22 2026-03-26 16:28:16 +00:00
Alexandru Ionut Tripon
9f5f1bcf10
Enable automatic update checking by default (#5259) 2026-03-26 16:09:48 +00:00
Alexandru Ionut Tripon
64c78fadc1
Improve NetJob failure dialog (#5260) 2026-03-26 16:09:40 +00:00
Octol1ttle
3a48d13c07
feat(NetworkJobFailedDialog): implement URL copying
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-03-26 10:46:37 +05:00
TheKodeToad
7bb746dfab
Safer dialog
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
2026-03-26 10:46:37 +05:00
TheKodeToad
4bc72ccca4
My tweaks
Use a tree view instead of table view, remove toggle button (janky)

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
2026-03-26 10:46:37 +05:00
TheKodeToad
e6d7e5cdae
Backport new NetJob failure dialog from Octol1ttle's libcurl PR
Co-authored-by: Octol1ttle <l1ttleofficial@outlook.com>
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
2026-03-26 10:46:37 +05:00
Octol1ttle
b3fa99dd2f
change: enable automatic update checking by default
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-03-26 09:29:02 +05:00
Octol1ttle
69fe3e3b1a
feat: warn user on launch if there is not enough available RAM
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-03-24 12:09:11 +05:00
Trial97
6674f1e803
Revert "Sort modpack entries by version, rather than publishing date"
This reverts commit 9e3893fd62.

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-23 19:57:07 +02:00
timoreo
c16a25faef
make OpenJ9 logo square (#5240) 2026-03-23 16:04:44 +00:00
Tayou
19eba5c6bc
make OpenJ9 logo square
Signed-off-by: Tayou <git@tayou.org>
2026-03-23 14:27:54 +01:00
Alexandru Ionut Tripon
f91accdce8
Log error if file open/commit fails (#5235) 2026-03-23 13:11:39 +00:00
Alexandru Ionut Tripon
6c6dc55dc0
Update update-flake.yml with new labels (#5231) 2026-03-23 12:58:09 +00:00
Trial97
3a65ed4c25
do not delete mod on cancel
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-23 13:26:57 +02:00
DioEgizio
e90ecdaeea
rate limit the FTB mod downloads (#5237) 2026-03-23 08:48:27 +00:00
Trial97
5136c15833
rate limit the FTB mod downloads
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-23 10:14:23 +02:00
Rachel Powers
f0f26bbfaf
add robot type label
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
2026-03-22 21:58:39 -07:00
Octol1ttle
838687fb2e
fix: log error if file open/commit fails
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-03-23 01:07:05 +05:00
Rachel Powers
0daf4669ad
add priority label
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
2026-03-21 19:51:52 -07:00
Rachel Powers
988ec79bc7
Update update-flake.yml with new labels
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
2026-03-21 19:48:38 -07:00
Trial97
441fb4a891
load the path name with local8Bit if not utf
extended to the symlinks stuff so I geneeralized the function:
- if I can get the utf8(the best outcome) I will use fromUtf8
- if not I will fall back to normal funciton and decode it with
fromLocal8Bit

This convention applies to:
- archive_entry_pathname
- archive_entry_symlink
- archive_entry_hardlink

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-17 09:12:27 +02:00
Trial97
3f97d65224
codeql: fix some warnings
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-15 23:32:10 +02:00
Trial97
cf024e228f
fix utf8 archive that doesn't mark the file as utf8
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-15 23:28:54 +02:00
211 changed files with 4751 additions and 3297 deletions

View file

@ -1,32 +1,32 @@
FormatStyle: file FormatStyle: file
Checks: Checks:
'bugprone-*,clang-analyzer-*,cppcoreguidelines-*,hicpp-*,misc-*,modernize-*,performance-*,portability-*,readability-*, "bugprone-*,clang-analyzer-*,cppcoreguidelines-*,hicpp-*,misc-*,modernize-*,performance-*,portability-*,readability-*,
-*-magic-numbers, -*-magic-numbers,
-*-non-private-member-variables-in-classes, -*-non-private-member-variables-in-classes,
-*-special-member-functions, -*-special-member-functions,
-bugprone-easily-swappable-parameters, -bugprone-easily-swappable-parameters,
-cppcoreguidelines-owning-memory, -cppcoreguidelines-owning-memory,
-cppcoreguidelines-pro-type-static-cast-downcast, -cppcoreguidelines-pro-type-static-cast-downcast,
-modernize-use-nodiscard, -modernize-use-nodiscard,
-modernize-use-trailing-return-type, -modernize-use-trailing-return-type,
-portability-avoid-pragma-once, -portability-avoid-pragma-once,
-readability-avoid-unconditional-preprocessor-if, -readability-avoid-unconditional-preprocessor-if,
-readability-function-cognitive-complexity, -readability-function-cognitive-complexity,
-readability-identifier-length, -readability-identifier-length,
-readability-redundant-access-specifiers' -readability-redundant-access-specifiers"
CheckOptions: CheckOptions:
- { key: misc-include-cleaner.MissingIncludes, value: false } misc-include-cleaner.MissingIncludes: false
- { key: readability-identifier-naming.DefaultCase, value: camelBack } readability-identifier-naming.DefaultCase: "camelBack"
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase } readability-identifier-naming.NamespaceCase: "CamelCase"
- { key: readability-identifier-naming.ClassCase, value: CamelCase } readability-identifier-naming.ClassCase: "CamelCase"
- { key: readability-identifier-naming.ClassConstantCase, value: CamelCase } readability-identifier-naming.ClassConstantCase: "CamelCase"
- { key: readability-identifier-naming.EnumCase, value: CamelCase } readability-identifier-naming.EnumCase: "CamelCase"
- { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } readability-identifier-naming.EnumConstantCase: "CamelCase"
- { key: readability-identifier-naming.MacroDefinitionCase, value: UPPER_CASE } readability-identifier-naming.MacroDefinitionCase: "UPPER_CASE"
- { key: readability-identifier-naming.ClassMemberPrefix, value: m_ } readability-identifier-naming.ClassMemberPrefix: "m_"
- { key: readability-identifier-naming.StaticConstantPrefix, value: s_ } readability-identifier-naming.StaticConstantPrefix: "s_"
- { key: readability-identifier-naming.StaticVariablePrefix, value: s_ } readability-identifier-naming.StaticVariablePrefix: "s_"
- { key: readability-identifier-naming.GlobalConstantPrefix, value: g_ } readability-identifier-naming.GlobalConstantPrefix: "g_"
- { key: readability-implicit-bool-conversion.AllowPointerConditions, value: true } readability-implicit-bool-conversion.AllowPointerConditions: true

View file

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

View file

@ -91,9 +91,9 @@ runs:
find "$INSTALL_APPIMAGE_DIR" -name '*gamemode*' -exec rm {} + find "$INSTALL_APPIMAGE_DIR" -name '*gamemode*' -exec rm {} +
#disable OpenGL and Vulkan launcher features until https://github.com/VHSgunzo/sharun/issues/35 #disable OpenGL and Vulkan launcher features until https://github.com/VHSgunzo/sharun/issues/35
echo "LAUNCHER_DISABLE_GLVULKAN=1" > "$INSTALL_APPIMAGE_DIR"/.env echo "PRISMLAUNCHER_DISABLE_GLVULKAN=1" >> "$INSTALL_APPIMAGE_DIR"/.env
#makes the launcher use portals for file picking #makes the launcher use portals for file picking
echo "QT_QPA_PLATFORMTHEME=xdgdesktopportal" > "$INSTALL_APPIMAGE_DIR"/.env echo "QT_QPA_PLATFORMTHEME=xdgdesktopportal" >> "$INSTALL_APPIMAGE_DIR"/.env
ln -s org.prismlauncher.PrismLauncher.metainfo.xml "$INSTALL_APPIMAGE_DIR"/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml ln -s org.prismlauncher.PrismLauncher.metainfo.xml "$INSTALL_APPIMAGE_DIR"/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
ln -s share/applications/org.prismlauncher.PrismLauncher.desktop "$INSTALL_APPIMAGE_DIR" ln -s share/applications/org.prismlauncher.PrismLauncher.desktop "$INSTALL_APPIMAGE_DIR"
ln -s share/icons/hicolor/256x256/apps/org.prismlauncher.PrismLauncher.png "$INSTALL_APPIMAGE_DIR" ln -s share/icons/hicolor/256x256/apps/org.prismlauncher.PrismLauncher.png "$INSTALL_APPIMAGE_DIR"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,19 +28,14 @@ jobs:
fetch-depth: 0 # Required for diffing later on fetch-depth: 0 # Required for diffing later on
submodules: "true" submodules: "true"
- name: Setup sccache
uses: hendrikmuhs/ccache-action@v1.2.21
with:
variant: sccache
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
- name: Run build - name: Run source generators
# TODO(@getchoo): Figure out how to make this work with PCH # TODO(@getchoo): Figure out how to make this work with PCH
run: | run: |
nix develop --command bash -c ' nix develop --command bash -c '
cmake -B build -D Launcher_USE_PCH=OFF -D CMAKE_CXX_COMPILER_LAUNCHER=sccache && cmake --build build cmake -B build -D Launcher_USE_PCH=OFF && cmake --build build --target autogen autorcc
' '
# TODO: Use SARIF after https://github.com/psastras/sarif-rs/issues/638 is fixed # TODO: Use SARIF after https://github.com/psastras/sarif-rs/issues/638 is fixed
@ -49,5 +44,5 @@ jobs:
BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
run: | run: |
nix develop --command bash -c ' nix develop --command bash -c '
git diff -U0 --no-color "$BASE_REF" | clang-tidy-diff.py -p1 clang-tidy -verify-config && git diff -U0 --no-color "$BASE_REF" | clang-tidy-diff.py -p1 -quiet -only-check-in-db
' '

View file

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

View file

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

View file

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

View file

@ -20,14 +20,16 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31 - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31
- uses: DeterminateSystems/update-flake-lock@v28 - uses: DeterminateSystems/update-flake-lock@v28
with: with:
commit-msg: "chore(nix): update lockfile" commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile"
pr-labels: | pr-labels: |
Linux platform: Linux
packaging area: packaging
simple change complexity: low
priority: low
type: robot
changelog:omit changelog:omit

View file

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

View file

@ -50,6 +50,7 @@ Config::Config()
LAUNCHER_GIT = "@Launcher_Git@"; LAUNCHER_GIT = "@Launcher_Git@";
LAUNCHER_APPID = "@Launcher_AppID@"; LAUNCHER_APPID = "@Launcher_AppID@";
LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@";
LAUNCHER_ENVNAME = "@Launcher_ENVName@";
USER_AGENT = "@Launcher_UserAgent@"; USER_AGENT = "@Launcher_UserAgent@";

View file

@ -54,6 +54,7 @@ class Config {
QString LAUNCHER_GIT; QString LAUNCHER_GIT;
QString LAUNCHER_APPID; QString LAUNCHER_APPID;
QString LAUNCHER_SVGFILENAME; QString LAUNCHER_SVGFILENAME;
QString LAUNCHER_ENVNAME;
/// The major version number. /// The major version number.
int VERSION_MAJOR; int VERSION_MAJOR;
@ -193,8 +194,10 @@ class Config {
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" }; QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" };
QString MODRINTH_DOWNLOAD_HOST = "cdn.modrinth.com";
QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; QString FLAME_BASE_URL = "https://api.curseforge.com/v1";
QString FLAME_DOWNLOAD_HOST = "edge.forgecdn.net";
QString versionString() const; QString versionString() const;
/** /**

10
flake.lock generated
View file

@ -18,15 +18,15 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1773964973, "lastModified": 1778443072,
"narHash": "sha256-d2Q5VNbc91GloTZNByC4u3JS8Tj5BjfuOF19/vuJ/iM=", "narHash": "sha256-rNDJzV2JTV5SUTwv1cgKZYMdyoUYU9/YfegSaUf3QfY=",
"rev": "812b3986fd1568f7a858f97fcf425ad996ba7d25", "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.7849.812b3986fd15/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre995699.da5ad661ba4e/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
"url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
} }
}, },
"root": { "root": {

View file

@ -9,7 +9,7 @@
}; };
inputs = { inputs = {
nixpkgs.url = "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz"; nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
libnbtplusplus = { libnbtplusplus = {
url = "github:PrismLauncher/libnbtplusplus"; url = "github:PrismLauncher/libnbtplusplus";
@ -42,7 +42,7 @@
let let
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
llvm = pkgs.llvmPackages_19; llvm = pkgs.llvmPackages_22;
in in
{ {
@ -85,7 +85,7 @@
let let
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
llvm = pkgs.llvmPackages_19; llvm = pkgs.llvmPackages_22;
python = pkgs.python3; python = pkgs.python3;
mkShell = pkgs.mkShell.override { inherit (llvm) stdenv; }; mkShell = pkgs.mkShell.override { inherit (llvm) stdenv; };
@ -189,7 +189,7 @@
final: prev: final: prev:
let let
llvm = final.llvmPackages_19 or prev.llvmPackages_19; llvm = final.llvmPackages_22 or prev.llvmPackages_22;
in in
{ {

View file

@ -514,12 +514,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0))); logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
showFatalErrorMessage("The launcher data folder is not writable!", showFatalErrorMessage("The launcher data folder is not writable!",
QString("The launcher couldn't create a log file - the data folder is not writable.\n" QString("The launcher couldn't create a log file - %1.\n"
"\n" "\n"
"Make sure you have write permissions to the data folder.\n" "Make sure you have write permissions to the data folder.\n"
"(%1)\n" "(%2)\n"
"\n" "\n"
"The launcher cannot continue until you fix this problem.") "The launcher cannot continue until you fix this problem.")
.arg(logFile->errorString())
.arg(dataPath)); .arg(dataPath));
return; return;
} }
@ -577,16 +578,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
} }
{ {
bool migrated = false; auto migrated = handleDataMigration(
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC",
if (!migrated) "polymc.cfg");
migrated = handleDataMigration( if (!migrated) {
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", handleDataMigration(dataPath,
"polymc.cfg"); FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"),
if (!migrated) "MultiMC", "multimc.cfg");
migrated = handleDataMigration( }
dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC",
"multimc.cfg");
} }
{ {
@ -626,11 +625,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
if (check.write(payload) == payload.size()) { if (check.write(payload) == payload.size()) {
check.close(); check.close();
} else { } else {
qWarning() << "Could not write into" << liveCheckFile << "!"; qWarning() << "Could not write into" << liveCheckFile << "error:" << check.errorString();
check.remove(); // also closes file! check.remove(); // also closes file!
} }
} else { } else {
qWarning() << "Could not open" << liveCheckFile << "for writing!"; qWarning() << "Could not open" << liveCheckFile << "for writing:" << check.errorString();
} }
} }
@ -734,6 +733,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512);
m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem()); m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem());
m_settings->registerSetting("PermGen", 128); m_settings->registerSetting("PermGen", 128);
m_settings->registerSetting("LowMemWarning", true);
// Java Settings // Java Settings
m_settings->registerSetting("JavaPath", ""); m_settings->registerSetting("JavaPath", "");
@ -777,6 +777,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false);
m_settings->registerSetting("SkipModpackUpdatePrompt", false); m_settings->registerSetting("SkipModpackUpdatePrompt", false);
m_settings->registerSetting("ShowModIncompat", false); m_settings->registerSetting("ShowModIncompat", false);
m_settings->registerSetting("DownloadGameFilesDuringInstanceCreation", true);
// Minecraft offline player name // Minecraft offline player name
m_settings->registerSetting("LastOfflinePlayerName", ""); m_settings->registerSetting("LastOfflinePlayerName", "");
@ -869,6 +870,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get()); resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get());
} }
m_settings->registerSetting("MetaRefreshOnLaunch", true);
m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false); m_settings->registerSetting("QuitAfterGameStop", false);
@ -933,15 +935,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qInfo() << "<> Network done."; qInfo() << "<> Network done.";
} }
// load translations
{
m_translations.reset(new TranslationsModel("translations"));
auto bcp47Name = m_settings->get("Language").toString();
m_translations->selectLanguage(bcp47Name);
qInfo() << "Your language is" << bcp47Name;
qInfo() << "<> Translations loaded.";
}
// Instance icons // Instance icons
{ {
auto setting = APPLICATION->settings()->getSetting("IconsDir"); auto setting = APPLICATION->settings()->getSetting("IconsDir");
@ -1020,8 +1013,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
qInfo() << "<> Cache initialized."; qInfo() << "<> Cache initialized.";
} }
// now we have network, download translation updates // load translations
m_translations->downloadIndex(); {
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? // FIXME: what to do with these?
m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory())); m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));
@ -1955,7 +1953,7 @@ bool Application::handleDataMigration(const QString& currentData,
auto setDoNotMigrate = [&nomigratePath] { auto setDoNotMigrate = [&nomigratePath] {
QFile file(nomigratePath); QFile file(nomigratePath);
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "setDoNotMigrate failed; Failed to open file '" << file.fileName() << "' for writing!"; qWarning() << "setDoNotMigrate failed; Failed to open file" << file.fileName() << "for writing:" << file.errorString();
} }
}; };

View file

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

View file

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

View file

@ -36,6 +36,7 @@
*/ */
#include "FileSystem.h" #include "FileSystem.h"
#include <qcontainerfwd.h>
#include <QPair> #include <QPair>
#include "BuildConfig.h" #include "BuildConfig.h"
@ -683,6 +684,32 @@ bool deletePath(QString path)
return err.value() == 0; return err.value() == 0;
} }
bool deleteContents(const QString& path)
{
const QFileInfo info(path);
if (!info.exists()) {
return true;
}
if (!info.isDir()) {
qWarning() << "Attempted to delete contents of non-directory path:" << path;
return false;
}
bool ret = true;
for (const auto& entry : fs::directory_iterator(StringUtils::toStdString(path))) {
std::error_code err;
fs::remove_all(entry.path(), err);
if (err.value() != 0) {
qWarning().nospace() << "Could not delete directory entry " << entry.path() << ": " << QString::fromStdString(err.message());
ret = false;
}
}
return ret;
}
bool trash(QString path, QString* pathInTrash) bool trash(QString path, QString* pathInTrash)
{ {
// FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
@ -796,68 +823,33 @@ QString NormalizePath(QString path)
} }
} }
static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; namespace {
static const QString BAD_NTFS_CHARS = "<>:\"|?*"; const QString g_badChars = "<>:\"|?*\r\n!";
static const QString BAD_HFS_CHARS = ":"; QString removeChars(QString source, QChar replace, const QString& extraChars = "")
static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) auto badChars = g_badChars;
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) if (!extraChars.isEmpty()) {
string[i] = replaceWith; badChars += extraChars;
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;
} }
if (invalidChars.size() != 0) { for (auto& c : source) {
for (int i = 0; i < path.length(); i++) { if (c.unicode() < 0x20 || !c.isPrint() || badChars.contains(c)) {
if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) { c = replace;
path[i] = replaceWith;
}
} }
} }
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) QString DirNameFromString(QString string, QString inDir)
@ -954,7 +946,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS
return QString(); return QString();
} }
if (!info.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!info.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "Failed to open file" << info.fileName() << "for writing!"; qWarning() << "Failed to open file" << info.fileName() << "for writing:" << info.errorString();
return QString(); return QString();
} }
@ -965,7 +957,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS
QFile f(exec); QFile f(exec);
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "Failed to open file" << f.fileName() << "for writing!"; qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString();
return QString(); return QString();
} }
QTextStream stream(&f); QTextStream stream(&f);
@ -1010,7 +1002,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS
destination += ".desktop"; destination += ".desktop";
QFile f(destination); QFile f(destination);
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "Failed to open file '" << f.fileName() << "' for writing!"; qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString();
return QString(); return QString();
} }
QTextStream stream(&f); QTextStream stream(&f);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,7 @@
#include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountList.h" #include "minecraft/auth/AccountList.h"
#include "net/NetUtils.h"
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/MSALoginDialog.h"
@ -50,7 +51,7 @@
#include <QInputDialog> #include <QInputDialog>
#include <QList> #include <QList>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression> #include <utility>
#include "BuildConfig.h" #include "BuildConfig.h"
#include "JavaCommon.h" #include "JavaCommon.h"
@ -82,9 +83,9 @@ void LaunchController::decideAccount()
} }
// Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used
auto accounts = APPLICATION->accounts(); auto* accounts = APPLICATION->accounts();
auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); const auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString();
auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); const auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId);
if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) { if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) {
m_accountToUse = accounts->defaultAccount(); m_accountToUse = accounts->defaultAccount();
} else { } else {
@ -109,7 +110,7 @@ void LaunchController::decideAccount()
} }
} }
if (!m_accountToUse) { if (!m_accountToUse && accounts->anyAccountIsValid()) {
// If no default account is set, ask the user which one to use. // If no default account is set, ask the user which one to use.
ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox, ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox,
m_parentWidget); m_parentWidget);
@ -133,15 +134,7 @@ LaunchDecision LaunchController::decideLaunchMode()
return LaunchDecision::Continue; return LaunchDecision::Continue;
} }
if (m_wantedLaunchMode == LaunchMode::Normal) { const auto* accounts = APPLICATION->accounts();
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; MinecraftAccountPtr accountToCheck = nullptr;
if (m_accountToUse->accountType() != AccountType::Offline) { if (m_accountToUse->accountType() != AccountType::Offline) {
@ -163,7 +156,9 @@ LaunchDecision LaunchController::decideLaunchMode()
} }
auto state = accountToCheck->accountState(); auto state = accountToCheck->accountState();
if (state == AccountState::Unchecked || state == AccountState::Errored) { const bool needsRefresh =
m_wantedLaunchMode == LaunchMode::Normal && (state == AccountState::Offline || accountToCheck->shouldRefresh());
if (state == AccountState::Unchecked || state == AccountState::Errored || needsRefresh) {
accountToCheck->refresh(); accountToCheck->refresh();
state = AccountState::Working; state = AccountState::Working;
} }
@ -213,7 +208,7 @@ LaunchDecision LaunchController::decideLaunchMode()
return LaunchDecision::Abort; return LaunchDecision::Abort;
} }
bool LaunchController::askPlayDemo() bool LaunchController::askPlayDemo() const
{ {
QMessageBox box(m_parentWidget); QMessageBox box(m_parentWidget);
box.setWindowTitle(tr("Play demo?")); box.setWindowTitle(tr("Play demo?"));
@ -223,21 +218,22 @@ bool LaunchController::askPlayDemo()
text += tr("\n\nDo you want to play the demo?"); text += tr("\n\nDo you want to play the demo?");
box.setText(text); box.setText(text);
box.setIcon(QMessageBox::Warning); box.setIcon(QMessageBox::Warning);
auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); const auto* demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole);
auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); auto* cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole);
box.setDefaultButton(cancelButton); box.setDefaultButton(cancelButton);
box.exec(); box.exec();
return box.clickedButton() == demoButton; return box.clickedButton() == demoButton;
} }
QString LaunchController::askOfflineName(QString playerName, bool* ok) QString LaunchController::askOfflineName(const QString& playerName, bool* ok)
{ {
if (ok != nullptr) { if (ok != nullptr) {
*ok = false; *ok = false;
} }
QString message; QString title, message;
title = tr("Player name");
switch (m_actualLaunchMode) { switch (m_actualLaunchMode) {
case LaunchMode::Normal: case LaunchMode::Normal:
Q_ASSERT(false); Q_ASSERT(false);
@ -247,17 +243,24 @@ QString LaunchController::askOfflineName(QString playerName, bool* ok)
break; break;
case LaunchMode::Offline: case LaunchMode::Offline:
if (m_wantedLaunchMode == LaunchMode::Normal) { if (m_wantedLaunchMode == LaunchMode::Normal) {
message = tr("You are not connected to the Internet, launching in offline mode\n\n"); auto netErr = m_accountToUse->accountData()->networkError;
if (Net::isServerError(netErr)) {
title = tr("Auth servers offline");
message = tr("The Minecraft authentication servers are currently unavailable, launching in offline mode.\n\n");
} else {
title = tr("No internet connection");
message = tr("You are not connected to the Internet, launching in offline mode.\n\n");
}
} }
message += tr("Choose your offline mode player name"); message += tr("Choose your offline mode player name");
break; break;
} }
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); const QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
ChooseOfflineNameDialog dialog(message, m_parentWidget); ChooseOfflineNameDialog dialog(message, m_parentWidget);
dialog.setWindowTitle(tr("Player name")); dialog.setWindowTitle(title);
dialog.setUsername(usedname); dialog.setUsername(usedname);
if (dialog.exec() != QDialog::Accepted) { if (dialog.exec() != QDialog::Accepted) {
return {}; return {};
@ -331,23 +334,24 @@ void LaunchController::login()
launchInstance(); launchInstance();
} }
bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account, QString reason) bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason)
{ {
auto button = QMessageBox::warning( auto button = QMessageBox::warning(
m_parentWidget, tr("Account refresh failed"), tr("%1. Do you want to reauthenticate this account?").arg(reason), m_parentWidget, tr("Account refresh failed"), tr("%1. Do you want to reauthenticate this account?").arg(reason),
QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes); QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes);
if (button == QMessageBox::StandardButton::Yes) { if (button == QMessageBox::StandardButton::Yes) {
auto accounts = APPLICATION->accounts(); auto* accounts = APPLICATION->accounts();
bool isDefault = accounts->defaultAccount() == account; const bool isDefault = accounts->defaultAccount() == account;
accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId())));
if (account->accountType() == AccountType::MSA) { if (account->accountType() == AccountType::MSA) {
auto newAccount = MSALoginDialog::newAccount(m_parentWidget); auto newAccount = MSALoginDialog::newAccount(m_parentWidget);
if (newAccount != nullptr) { if (newAccount != nullptr) {
accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId())));
accounts->addAccount(newAccount); accounts->addAccount(newAccount);
if (isDefault) if (isDefault) {
accounts->setDefaultAccount(newAccount); accounts->setDefaultAccount(newAccount);
}
if (m_accountToUse == account) { if (m_accountToUse == account) {
m_accountToUse = nullptr; m_accountToUse = nullptr;
@ -358,14 +362,13 @@ bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account, QStrin
} }
} }
emitFailed(reason);
return false; return false;
} }
void LaunchController::launchInstance() void LaunchController::launchInstance()
{ {
Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); Q_ASSERT(m_instance != nullptr);
Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); Q_ASSERT(m_session.get() != nullptr);
if (!m_instance->reloadSettings()) { if (!m_instance->reloadSettings()) {
QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile."));
@ -379,8 +382,8 @@ void LaunchController::launchInstance()
return; return;
} }
auto console = qobject_cast<InstanceWindow*>(m_parentWidget); const auto* console = qobject_cast<InstanceWindow*>(m_parentWidget);
auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); const auto showConsole = m_instance->settings()->get("ShowConsole").toBool();
if (!console && showConsole) { if (!console && showConsole) {
APPLICATION->showInstanceWindow(m_instance); APPLICATION->showInstanceWindow(m_instance);
} }
@ -395,7 +398,7 @@ void LaunchController::launchInstance()
online_mode = "online"; online_mode = "online";
// Prepend Server Status // Prepend Server Status
QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; const QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" };
m_launcher->prependStep(makeShared<PrintServers>(m_launcher, servers)); m_launcher->prependStep(makeShared<PrintServers>(m_launcher, servers));
} else { } else {
@ -465,10 +468,10 @@ void LaunchController::onFailed(QString reason)
if (m_instance->settings()->get("ShowConsoleOnError").toBool()) { if (m_instance->settings()->get("ShowConsoleOnError").toBool()) {
APPLICATION->showInstanceWindow(m_instance, "console"); APPLICATION->showInstanceWindow(m_instance, "console");
} }
emitFailed(reason); emitFailed(std::move(reason));
} }
void LaunchController::onProgressRequested(Task* task) void LaunchController::onProgressRequested(Task* task) const
{ {
ProgressDialog progDialog(m_parentWidget); ProgressDialog progDialog(m_parentWidget);
progDialog.setSkipButton(true, tr("Abort")); progDialog.setSkipButton(true, tr("Abort"));

View file

@ -50,11 +50,11 @@ class LaunchController : public Task {
void executeTask() override; void executeTask() override;
LaunchController(); LaunchController();
virtual ~LaunchController() = default; ~LaunchController() override = default;
void setInstance(BaseInstance* instance) { m_instance = instance; } void setInstance(BaseInstance* instance) { m_instance = instance; }
BaseInstance* instance() { return m_instance; } BaseInstance* instance() const { return m_instance; }
void setLaunchMode(const LaunchMode mode) { m_wantedLaunchMode = mode; } void setLaunchMode(const LaunchMode mode) { m_wantedLaunchMode = mode; }
@ -68,7 +68,7 @@ class LaunchController : public Task {
void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); } void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); }
QString id() { return m_instance->id(); } QString id() const { return m_instance->id(); }
bool abort() override; bool abort() override;
@ -77,27 +77,27 @@ class LaunchController : public Task {
void launchInstance(); void launchInstance();
void decideAccount(); void decideAccount();
LaunchDecision decideLaunchMode(); LaunchDecision decideLaunchMode();
bool askPlayDemo(); bool askPlayDemo() const;
QString askOfflineName(QString playerName, bool* ok = nullptr); QString askOfflineName(const QString& playerName, bool* ok = nullptr);
bool reauthenticateAccount(MinecraftAccountPtr account, QString reason); bool reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason);
private slots: private slots:
void readyForLaunch(); void readyForLaunch();
void onSucceeded(); void onSucceeded();
void onFailed(QString reason); void onFailed(QString reason);
void onProgressRequested(Task* task); void onProgressRequested(Task* task) const;
private: private:
LaunchMode m_wantedLaunchMode = LaunchMode::Normal; LaunchMode m_wantedLaunchMode = LaunchMode::Normal;
LaunchMode m_actualLaunchMode = LaunchMode::Normal; LaunchMode m_actualLaunchMode = LaunchMode::Normal;
BaseProfilerFactory* m_profiler = nullptr; BaseProfilerFactory* m_profiler = nullptr;
QString m_offlineName; QString m_offlineName;
BaseInstance* m_instance; BaseInstance* m_instance = nullptr;
QWidget* m_parentWidget = nullptr; QWidget* m_parentWidget = nullptr;
InstanceWindow* m_console = nullptr; InstanceWindow* m_console = nullptr;
MinecraftAccountPtr m_accountToUse = nullptr; MinecraftAccountPtr m_accountToUse = nullptr;
AuthSessionPtr m_session; AuthSessionPtr m_session = nullptr;
LaunchTask* m_launcher; LaunchTask* m_launcher = nullptr;
MinecraftTarget::Ptr m_targetToJoin; MinecraftTarget::Ptr m_targetToJoin = nullptr;
}; };

View file

@ -15,6 +15,7 @@ fi
LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@
LAUNCHER_ENVNAME=@Launcher_ENVName@
LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")"
echo "Launcher Dir: ${LAUNCHER_DIR}" echo "Launcher Dir: ${LAUNCHER_DIR}"
@ -23,7 +24,7 @@ export QT_QPA_PLATFORMTHEME=xdgdesktopportal
# disable OpenGL and Vulkan launcher features on sharun until https://github.com/VHSgunzo/sharun/issues/35 # disable OpenGL and Vulkan launcher features on sharun until https://github.com/VHSgunzo/sharun/issues/35
if [[ -f "${LAUNCHER_DIR}/sharun" ]]; then if [[ -f "${LAUNCHER_DIR}/sharun" ]]; then
export LAUNCHER_DISABLE_GLVULKAN=1 export ${LAUNCHER_ENVNAME}_DISABLE_GLVULKAN=1
fi fi
# Just to be sure... # Just to be sure...

View file

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

View file

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

View file

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

View file

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

View file

@ -1,124 +1,42 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2026 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Version.h" #include "Version.h"
#include <QDebug> #include <QDebug>
#include <QRegularExpressionMatch> #include <QRegularExpressionMatch>
#include <QUrl> #include <QUrl>
#include <compare>
Version::Version(QString str) : m_string(std::move(str))
{
parse();
}
#define VERSION_OPERATOR(return_on_different) \
bool exclude_our_sections = false; \
bool exclude_their_sections = false; \
\
const auto size = qMax(m_sections.size(), other.m_sections.size()); \
for (int i = 0; i < size; ++i) { \
Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \
Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \
\
{ /* Don't include appendixes in the comparison */ \
if (sec1.isAppendix()) \
exclude_our_sections = true; \
if (sec2.isAppendix()) \
exclude_their_sections = true; \
\
if (exclude_our_sections) { \
sec1 = Section(); \
if (sec2.m_isNull) \
break; \
} \
\
if (exclude_their_sections) { \
sec2 = Section(); \
if (sec1.m_isNull) \
break; \
} \
} \
\
if (sec1 != sec2) \
return return_on_different; \
}
bool Version::operator<(const Version& other) const
{
VERSION_OPERATOR(sec1 < sec2)
return false;
}
bool Version::operator==(const Version& other) const
{
VERSION_OPERATOR(false)
return true;
}
bool Version::operator!=(const Version& other) const
{
return !operator==(other);
}
bool Version::operator<=(const Version& other) const
{
return *this < other || *this == other;
}
bool Version::operator>(const Version& other) const
{
return !(*this <= other);
}
bool Version::operator>=(const Version& other) const
{
return !(*this < other);
}
void Version::parse()
{
m_sections.clear();
QString currentSection;
if (m_string.isEmpty())
return;
auto classChange = [&currentSection](QChar lastChar, QChar currentChar) {
if (lastChar.isNull())
return false;
if (lastChar.isDigit() != currentChar.isDigit())
return true;
const QList<QChar> s_separators{ '.', '-', '+' };
if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar)
return true;
return false;
};
currentSection += m_string.at(0);
for (int i = 1; i < m_string.size(); ++i) {
const auto& current_char = m_string.at(i);
if (classChange(m_string.at(i - 1), current_char)) {
if (!currentSection.isEmpty())
m_sections.append(Section(currentSection));
currentSection = "";
}
currentSection += current_char;
}
if (!currentSection.isEmpty())
m_sections.append(Section(currentSection));
}
/// qDebug print support for the Version class /// qDebug print support for the Version class
QDebug operator<<(QDebug debug, const Version& v) QDebug operator<<(QDebug debug, const Version& v)
{ {
QDebugStateSaver saver(debug); const QDebugStateSaver saver(debug);
debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ ";
bool first = true; bool first = true;
for (auto s : v.m_sections) { for (const auto& s : v.m_sections) {
if (!first) if (!first) {
debug.nospace() << ", "; debug.nospace() << ", ";
debug.nospace() << s.m_fullString; }
debug.nospace() << s.value;
first = false; first = false;
} }
@ -126,3 +44,114 @@ QDebug operator<<(QDebug debug, const Version& v)
return debug; return debug;
} }
std::strong_ordering Version::Section::operator<=>(const Section& other) const
{
// If both components are numeric, compare numerically (codepoint-wise)
if (this->t == Type::Numeric && other.t == Type::Numeric) {
auto aLen = this->value.size();
if (aLen != other.value.size()) {
// Lengths differ; compare by length
return aLen <=> other.value.size();
}
// Compare by digits
auto cmp = QString::compare(this->value, other.value);
if (cmp < 0) {
return std::strong_ordering::less;
}
if (cmp > 0) {
return std::strong_ordering::greater;
}
return std::strong_ordering::equal;
}
// One or both are null
if (this->t == Type::Null) {
if (other.t == Type::PreRelease) {
return std::strong_ordering::greater;
}
return std::strong_ordering::less;
}
if (other.t == Type::Null) {
if (this->t == Type::PreRelease) {
return std::strong_ordering::less;
}
return std::strong_ordering::greater;
}
// Textual comparison (differing type, or both textual/pre-release)
auto minLen = qMin(this->value.size(), other.value.size());
for (int i = 0; i < minLen; i++) {
auto a = this->value.at(i);
auto b = other.value.at(i);
if (a != b) {
// Compare by rune
return a.unicode() <=> b.unicode();
}
}
// Compare by length
return this->value.size() <=> other.value.size();
}
namespace {
void removeLeadingZeros(QString& s)
{
s.remove(0, std::distance(s.begin(), std::ranges::find_if_not(s, [](QChar c) { return c == '0'; })));
}
} // namespace
void Version::parse()
{
auto len = m_string.size();
for (int i = 0; i < len;) {
Section cur(Section::Type::Textual);
auto c = m_string.at(i);
if (c == '+') {
break; // Ignore appendices
}
// custom: the space is special to handle the strings like "1.20 Pre-Release 1"
// this is needed to support Modrinth versions
if (c == '-' || c == ' ') {
// Add dash to component
cur.value += c;
i++;
// If the next rune is non-digit, mark as pre-release (requires >= 1 non-digit after dash so the component has length > 1)
if (i < len && !m_string.at(i).isDigit()) {
cur.t = Section::Type::PreRelease;
}
} else if (c.isDigit()) {
// Mark as numeric
cur.t = Section::Type::Numeric;
}
for (; i < len; i++) {
auto r = m_string.at(i);
if ((r.isDigit() != (cur.t == Section::Type::Numeric)) // starts a new section
|| (r == ' ' && cur.t == Section::Type::Numeric) // custom: numeric section then a space is a pre-release
|| (r == '-' && cur.t != Section::Type::PreRelease) // "---" is a valid pre-release component
|| r == '+') {
// Run completed (do not consume this rune)
break;
}
// Add rune to current run
cur.value += r;
}
if (!cur.value.isEmpty()) {
if (cur.t == Section::Type::Numeric) {
removeLeadingZeros(cur.value);
}
m_sections.append(cur);
}
}
}
std::strong_ordering Version::operator<=>(const Version& other) const
{
const auto size = qMax(m_sections.size(), other.m_sections.size());
for (int i = 0; i < size; ++i) {
auto sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i);
auto sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i);
if (auto cmp = sec1 <=> sec2; cmp != std::strong_ordering::equal) {
return cmp;
}
}
return std::strong_ordering::equal;
}

View file

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com> * Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2026 Trial97 <alexandru.tripon97@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,23 +16,6 @@
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
#pragma once #pragma once
@ -41,115 +25,36 @@
#include <QString> #include <QString>
#include <QStringView> #include <QStringView>
class QUrl; // this implements the FlexVer
// https://git.sleeping.town/exa/FlexVer
class Version { class Version {
public: public:
Version(QString str); Version(QString str) : m_string(std::move(str)) { parse(); } // NOLINT(hicpp-explicit-conversions)
Version() = default; Version() = default;
bool operator<(const Version& other) const; private:
bool operator<=(const Version& other) const; struct Section {
bool operator>(const Version& other) const; enum class Type : std::uint8_t { Null, Textual, Numeric, PreRelease };
bool operator>=(const Version& other) const; explicit Section(Type t = Type::Null, QString value = "") : t(t), value(std::move(value)) {}
bool operator==(const Version& other) const; Type t;
bool operator!=(const Version& other) const; QString value;
bool operator==(const Section& other) const = default;
std::strong_ordering operator<=>(const Section& other) const;
};
private:
void parse();
public:
QString toString() const { return m_string; } QString toString() const { return m_string; }
bool isEmpty() const { return m_string.isEmpty(); } bool isEmpty() const { return m_string.isEmpty(); }
friend QDebug operator<<(QDebug debug, const Version& v); friend QDebug operator<<(QDebug debug, const Version& v);
private: bool operator==(const Version& other) const { return (*this <=> other) == std::strong_ordering::equal; }
struct Section { std::strong_ordering operator<=>(const Version& other) const;
explicit Section(QString fullString) : m_fullString(std::move(fullString))
{
qsizetype cutoff = m_fullString.size();
for (int i = 0; i < m_fullString.size(); i++) {
if (!m_fullString[i].isDigit()) {
cutoff = i;
break;
}
}
auto numPart = QStringView{ m_fullString }.left(cutoff);
if (!numPart.isEmpty()) {
m_isNull = false;
m_numPart = numPart.toInt();
}
auto stringPart = QStringView{ m_fullString }.mid(cutoff);
if (!stringPart.isEmpty()) {
m_isNull = false;
m_stringPart = stringPart.toString();
}
}
explicit Section() = default;
bool m_isNull = true;
int m_numPart = 0;
QString m_stringPart;
QString m_fullString;
inline bool isAppendix() const { return m_stringPart.startsWith('+'); }
inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; }
inline bool operator==(const Section& other) const
{
if (m_isNull && !other.m_isNull)
return false;
if (!m_isNull && other.m_isNull)
return false;
if (!m_isNull && !other.m_isNull) {
return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart);
}
return true;
}
inline bool operator<(const Section& other) const
{
static auto unequal_is_less = [](const Section& non_null) -> bool {
if (non_null.m_stringPart.isEmpty())
return non_null.m_numPart == 0;
return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease();
};
if (!m_isNull && other.m_isNull)
return unequal_is_less(*this);
if (m_isNull && !other.m_isNull)
return !unequal_is_less(other);
if (!m_isNull && !other.m_isNull) {
if (m_numPart < other.m_numPart)
return true;
if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart)
return true;
if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty())
return false;
if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty())
return true;
return false;
}
return m_fullString < other.m_fullString;
}
inline bool operator!=(const Section& other) const { return !(*this == other); }
inline bool operator>(const Section& other) const { return !(*this < other || *this == other); }
};
private: private:
QString m_string; QString m_string;
QList<Section> m_sections; QList<Section> m_sections;
void parse();
}; };

View file

@ -24,6 +24,7 @@
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QUrl> #include <QUrl>
#include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
@ -36,25 +37,36 @@ QStringList ArchiveReader::getFiles()
bool ArchiveReader::collectFiles(bool onlyFiles) bool ArchiveReader::collectFiles(bool onlyFiles)
{ {
return parse([this, onlyFiles](File* f) { return parse([this, onlyFiles](File* f) {
if (!onlyFiles || f->isFile()) if (!onlyFiles || f->isFile()) {
m_fileNames << f->filename(); m_fileNames << f->filename();
}
return f->skip(); return f->skip();
}); });
} }
using getPathFunc = std::function<const char*(archive_entry*)>;
static QString decodeLibArchivePath(archive_entry* entry, const getPathFunc& getUtf8Path, const getPathFunc& getPath)
{
auto fileName = QString::fromUtf8(getUtf8Path(entry));
if (fileName.isEmpty()) {
fileName = QString::fromLocal8Bit(getPath(entry));
}
return fileName;
}
QString ArchiveReader::File::filename() QString ArchiveReader::File::filename()
{ {
return QString::fromUtf8(archive_entry_pathname_utf8(m_entry)); return decodeLibArchivePath(m_entry, archive_entry_pathname_utf8, archive_entry_pathname);
} }
QByteArray ArchiveReader::File::readAll(int* outStatus) QByteArray ArchiveReader::File::readAll(int* outStatus)
{ {
QByteArray data; QByteArray data;
const void* buff; const void* buff = nullptr;
size_t size; size_t size = 0;
la_int64_t offset; la_int64_t offset = 0;
int status; int status = 0;
while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) { while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) {
data.append(static_cast<const char*>(buff), static_cast<qsizetype>(size)); data.append(static_cast<const char*>(buff), static_cast<qsizetype>(size));
} }
@ -80,10 +92,10 @@ int ArchiveReader::File::readNextHeader()
return archive_read_next_header(m_archive.get(), &m_entry); return archive_read_next_header(m_archive.get(), &m_entry);
} }
auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr<File> auto ArchiveReader::goToFile(const QString& filename) -> std::unique_ptr<File>
{ {
auto f = std::make_unique<File>(); auto f = std::make_unique<File>();
auto a = f->m_archive.get(); auto* a = f->m_archive.get();
archive_read_support_format_all(a); archive_read_support_format_all(a);
archive_read_support_filter_all(a); archive_read_support_filter_all(a);
auto fileName = m_archivePath.toStdWString(); auto fileName = m_archivePath.toStdWString();
@ -105,15 +117,16 @@ auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr<File>
static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false)
{ {
int r; int r = 0;
const void* buff; const void* buff = nullptr;
size_t size; size_t size = 0;
la_int64_t offset; la_int64_t offset = 0;
for (;;) { for (;;) {
r = archive_read_data_block(ar, &buff, &size, &offset); r = archive_read_data_block(ar, &buff, &size, &offset);
if (r == ARCHIVE_EOF) if (r == ARCHIVE_EOF) {
return (ARCHIVE_OK); return ARCHIVE_OK;
}
if (r < ARCHIVE_OK) { if (r < ARCHIVE_OK) {
qCritical() << "Failed reading data block:" << archive_error_string(ar); qCritical() << "Failed reading data block:" << archive_error_string(ar);
return (r); return (r);
@ -130,39 +143,43 @@ static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = fal
} }
} }
bool willEscapeRoot(const QDir& root, archive_entry* entry) static bool willEscapeRoot(const QDir& root, archive_entry* entry)
{ {
const char* entryPathC = archive_entry_pathname(entry); auto entryPath = decodeLibArchivePath(entry, archive_entry_pathname_utf8, archive_entry_pathname);
const char* linkTargetC = archive_entry_symlink(entry); auto linkTarget = decodeLibArchivePath(entry, archive_entry_symlink_utf8, archive_entry_symlink);
const char* hardlinkC = archive_entry_hardlink(entry); auto hardLink = decodeLibArchivePath(entry, archive_entry_hardlink_utf8, archive_entry_hardlink);
if (!entryPathC || (!linkTargetC && !hardlinkC)) if (entryPath.isEmpty() || (linkTarget.isEmpty() && hardLink.isEmpty())) {
return false; return false;
}
QString entryPath = QString::fromUtf8(entryPathC); bool isHardLink = false;
QString linkTarget = linkTargetC ? QString::fromUtf8(linkTargetC) : QString::fromUtf8(hardlinkC); if (isHardLink = linkTarget.isEmpty(); isHardLink) {
linkTarget = hardLink;
}
QString linkFullPath = root.filePath(entryPath); QString linkFullPath = root.filePath(entryPath);
auto rootDir = QUrl::fromLocalFile(root.absolutePath()); auto rootDir = QUrl::fromLocalFile(root.absolutePath());
if (!rootDir.isParentOf(QUrl::fromLocalFile(linkFullPath))) if (!rootDir.isParentOf(QUrl::fromLocalFile(linkFullPath))) {
return true; return true;
}
QDir linkDir = QFileInfo(linkFullPath).dir(); QDir linkDir = QFileInfo(linkFullPath).dir();
if (!QDir::isAbsolutePath(linkTarget)) { if (!QDir::isAbsolutePath(linkTarget)) {
linkTarget = (linkTargetC ? linkDir : root).filePath(linkTarget); linkTarget = (!isHardLink ? linkDir : root).filePath(linkTarget);
} }
return !rootDir.isParentOf(QUrl::fromLocalFile(QDir::cleanPath(linkTarget))); return !rootDir.isParentOf(QUrl::fromLocalFile(QDir::cleanPath(linkTarget)));
} }
bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock) bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, bool notBlock)
{ {
return writeFile(out, targetFileName, {}, notBlock); return writeFile(out, targetFileName, {}, notBlock);
}; };
bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, std::optional<QDir> root, bool notBlock) bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, std::optional<QDir> root, bool notBlock)
{ {
auto entry = m_entry; auto* entry = m_entry;
std::unique_ptr<archive_entry, decltype(&archive_entry_free)> entryClone(nullptr, &archive_entry_free); std::unique_ptr<archive_entry, decltype(&archive_entry_free)> entryClone(nullptr, &archive_entry_free);
if (!targetFileName.isEmpty()) { if (!targetFileName.isEmpty()) {
entryClone.reset(archive_entry_clone(m_entry)); entryClone.reset(archive_entry_clone(m_entry));
@ -175,25 +192,29 @@ bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, std::o
return false; return false;
} }
if (archive_write_header(out, entry) < ARCHIVE_OK) { if (archive_write_header(out, entry) < ARCHIVE_OK) {
qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out); qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out) << targetFileName;
return false; return false;
} else if (archive_entry_size(m_entry) > 0) { }
if (archive_entry_size(m_entry) > 0) {
auto r = copy_data(m_archive.get(), out, notBlock); auto r = copy_data(m_archive.get(), out, notBlock);
if (r < ARCHIVE_OK) if (r < ARCHIVE_OK) {
qCritical() << "Failed reading data block:" << archive_error_string(out); qCritical() << "Failed reading data block:" << archive_error_string(out);
if (r < ARCHIVE_WARN) }
if (r < ARCHIVE_WARN) {
return false; return false;
}
} }
auto r = archive_write_finish_entry(out); auto r = archive_write_finish_entry(out);
if (r < ARCHIVE_OK) if (r < ARCHIVE_OK) {
qCritical() << "Failed to finish writing entry:" << archive_error_string(out); qCritical() << "Failed to finish writing entry:" << archive_error_string(out);
}
return (r >= ARCHIVE_WARN); return (r >= ARCHIVE_WARN);
} }
bool ArchiveReader::parse(std::function<bool(File*, bool&)> doStuff) bool ArchiveReader::parse(const std::function<bool(File*, bool&)>& doStuff)
{ {
auto f = std::make_unique<File>(); auto f = std::make_unique<File>();
auto a = f->m_archive.get(); auto* a = f->m_archive.get();
archive_read_support_format_all(a); archive_read_support_format_all(a);
archive_read_support_filter_all(a); archive_read_support_filter_all(a);
auto fileName = m_archivePath.toStdWString(); auto fileName = m_archivePath.toStdWString();
@ -217,7 +238,7 @@ bool ArchiveReader::parse(std::function<bool(File*, bool&)> doStuff)
return true; return true;
} }
bool ArchiveReader::parse(std::function<bool(File*)> doStuff) bool ArchiveReader::parse(const std::function<bool(File*)>& doStuff)
{ {
return parse([doStuff](File* f, bool&) { return doStuff(f); }); return parse([doStuff](File* f, bool&) { return doStuff(f); });
} }
@ -241,26 +262,32 @@ QString ArchiveReader::getZipName()
bool ArchiveReader::exists(const QString& filePath) const bool ArchiveReader::exists(const QString& filePath) const
{ {
if (filePath == QLatin1String("/") || filePath.isEmpty()) if (filePath == QLatin1String("/") || filePath.isEmpty()) {
return true; return true;
}
// Normalize input path (remove trailing slash, if any) // Normalize input path (remove trailing slash, if any)
QString normalizedPath = QDir::cleanPath(filePath); QString normalizedPath = QDir::cleanPath(filePath);
if (normalizedPath.startsWith('/')) if (normalizedPath.startsWith('/')) {
normalizedPath.remove(0, 1); normalizedPath.remove(0, 1);
if (normalizedPath == QLatin1String(".")) }
if (normalizedPath == QLatin1String(".")) {
return true; return true;
if (normalizedPath == QLatin1String("..")) }
if (normalizedPath == QLatin1String("..")) {
return false; // root only return false; // root only
}
// Check for exact file match // Check for exact file match
if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) {
return true; return true;
}
// Check for directory existence by seeing if any file starts with that path // Check for directory existence by seeing if any file starts with that path
QString dirPath = normalizedPath + QLatin1Char('/'); QString dirPath = normalizedPath + QLatin1Char('/');
for (const QString& f : m_fileNames) { for (const QString& f : m_fileNames) {
if (f.startsWith(dirPath, Qt::CaseInsensitive)) if (f.startsWith(dirPath, Qt::CaseInsensitive)) {
return true; return true;
}
} }
return false; return false;

View file

@ -23,6 +23,7 @@
#include <QStringList> #include <QStringList>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <utility>
struct archive; struct archive;
struct archive_entry; struct archive_entry;
@ -30,7 +31,7 @@ namespace MMCZip {
class ArchiveReader { class ArchiveReader {
public: public:
using ArchivePtr = std::unique_ptr<struct archive, int (*)(struct archive*)>; using ArchivePtr = std::unique_ptr<struct archive, int (*)(struct archive*)>;
ArchiveReader(QString fileName) : m_archivePath(fileName) {} explicit ArchiveReader(QString fileName) : m_archivePath(std::move(fileName)) {}
virtual ~ArchiveReader() = default; virtual ~ArchiveReader() = default;
QStringList getFiles(); QStringList getFiles();
@ -50,8 +51,8 @@ class ArchiveReader {
QByteArray readAll(int* outStatus = nullptr); QByteArray readAll(int* outStatus = nullptr);
bool skip(); bool skip();
bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false); bool writeFile(archive* out, const QString& targetFileName = "", bool notBlock = false);
bool writeFile(archive* out, QString targetFileName, std::optional<QDir> root, bool notBlock = false); bool writeFile(archive* out, const QString& targetFileName, std::optional<QDir> root, bool notBlock = false);
private: private:
int readNextHeader(); int readNextHeader();
@ -62,14 +63,14 @@ class ArchiveReader {
archive_entry* m_entry; archive_entry* m_entry;
}; };
std::unique_ptr<File> goToFile(QString filename); std::unique_ptr<File> goToFile(const QString& filename);
bool parse(std::function<bool(File*)>); bool parse(const std::function<bool(File*)>&);
bool parse(std::function<bool(File*, bool&)>); bool parse(const std::function<bool(File*, bool&)>&);
private: private:
QString m_archivePath; QString m_archivePath;
size_t m_blockSize = 10240; size_t m_blockSize = 10240;
QStringList m_fileNames = {}; QStringList m_fileNames;
}; };
} // namespace MMCZip } // namespace MMCZip

View file

@ -174,7 +174,7 @@ bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest)
if (fileInfo.isFile() && !fileInfo.isSymLink()) { if (fileInfo.isFile() && !fileInfo.isSymLink()) {
QFile file(fileInfo.absoluteFilePath()); QFile file(fileInfo.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file:" << fileInfo.filePath(); qCritical() << "Failed to open file:" << fileInfo.filePath() << "error:" << file.errorString();
return false; return false;
} }

View file

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

View file

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

View file

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

View file

@ -42,6 +42,7 @@
#include <QDebug> #include <QDebug>
#include "Application.h" #include "Application.h"
#include "BuildConfig.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "java/JavaInstallList.h" #include "java/JavaInstallList.h"
#include "java/JavaUtils.h" #include "java/JavaUtils.h"
@ -155,7 +156,7 @@ JavaInstallPtr JavaUtils::GetDefaultJava()
QStringList addJavasFromEnv(QList<QString> javas) QStringList addJavasFromEnv(QList<QString> javas)
{ {
auto env = qEnvironmentVariable("PRISMLAUNCHER_JAVA_PATHS"); // FIXME: use launcher name from buildconfig auto env = QProcessEnvironment::systemEnvironment().value(QStringLiteral("%1_JAVA_PATHS").arg(BuildConfig.LAUNCHER_ENVNAME));
#if defined(Q_OS_WIN32) #if defined(Q_OS_WIN32)
QList<QString> javaPaths = env.replace("\\", "/").split(QLatin1String(";")); QList<QString> javaPaths = env.replace("\\", "/").split(QLatin1String(";"));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -104,7 +104,7 @@ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsInd
// Try to open the file and fail if we can't. // Try to open the file and fail if we can't.
// TODO: We should probably report this error to the user. // TODO: We should probably report this error to the user.
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to read assets index file" << path; qCritical() << "Failed to read assets index file" << path << "error:" << file.errorString();
return false; return false;
} }
index.id = assetsId; index.id = assetsId;

View file

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

View file

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

View file

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

View file

@ -59,6 +59,7 @@
#include "minecraft/launch/AutoInstallJava.h" #include "minecraft/launch/AutoInstallJava.h"
#include "minecraft/launch/ClaimAccount.h" #include "minecraft/launch/ClaimAccount.h"
#include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/CreateGameFolders.h"
#include "minecraft/launch/EnsureAvailableMemory.h"
#include "minecraft/launch/EnsureOfflineLibraries.h" #include "minecraft/launch/EnsureOfflineLibraries.h"
#include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/ExtractNatives.h"
#include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/LauncherPartLaunch.h"
@ -130,7 +131,8 @@
for (const auto& gpu : gpus) { for (const auto& gpu : gpus) {
QString name = qvariant_cast<QString>(gpu[QStringLiteral("Name")]); QString name = qvariant_cast<QString>(gpu[QStringLiteral("Name")]);
bool defaultGpu = qvariant_cast<bool>(gpu[QStringLiteral("Default")]); bool defaultGpu = qvariant_cast<bool>(gpu[QStringLiteral("Default")]);
if (!defaultGpu) { bool discrete = qvariant_cast<bool>(gpu.value(QStringLiteral("Discrete"), !defaultGpu));
if (discrete) {
QStringList envList = qvariant_cast<QStringList>(gpu[QStringLiteral("Environment")]); QStringList envList = qvariant_cast<QStringList>(gpu[QStringLiteral("Environment")]);
for (int i = 0; i + 1 < envList.size(); i += 2) { for (int i = 0; i + 1 < envList.size(); i += 2) {
env.insert(envList[i], envList[i + 1]); env.insert(envList[i], envList[i + 1]);
@ -208,6 +210,7 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("LowMemWarning"), memorySetting);
// Native library workarounds // Native library workarounds
auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
@ -890,6 +893,14 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
QStringList out; QStringList out;
out << "Components:";
for (int i = 0; i < m_components->rowCount(); ++i) {
const auto& component = m_components->getComponent(i);
out << indent +
QString("%1) %2 (%3) %4").arg(QString::number(i + 1), component->getName(), component->getID(), component->getVersion());
}
out << emptyLine;
out << "Launcher: " + getLauncher(); out << "Launcher: " + getLauncher();
out << "Main class: " + getMainClass() << emptyLine; out << "Main class: " + getMainClass() << emptyLine;
@ -1191,6 +1202,11 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf
process->appendStep(makeShared<ScanModFolders>(pptr)); process->appendStep(makeShared<ScanModFolders>(pptr));
} }
// make sure we have enough RAM, warn the user if we don't
{
process->appendStep(makeShared<EnsureAvailableMemory>(pptr, this));
}
// print some instance info here... // print some instance info here...
{ {
process->appendStep(makeShared<PrintInstanceInfo>(pptr, session, targetToJoin)); process->appendStep(makeShared<PrintInstanceInfo>(pptr, session, targetToJoin));

View file

@ -144,13 +144,13 @@ bool saveJsonFile(const QJsonDocument& doc, const QString& filename)
auto data = doc.toJson(); auto data = doc.toJson();
QSaveFile jsonFile(filename); QSaveFile jsonFile(filename);
if (!jsonFile.open(QIODevice::WriteOnly)) { if (!jsonFile.open(QIODevice::WriteOnly)) {
qWarning() << "Couldn't open" << filename << "for writing:" << jsonFile.errorString();
jsonFile.cancelWriting(); jsonFile.cancelWriting();
qWarning() << "Couldn't open" << filename << "for writing";
return false; return false;
} }
jsonFile.write(data); jsonFile.write(data);
if (!jsonFile.commit()) { if (!jsonFile.commit()) {
qWarning() << "Couldn't save" << filename; qWarning() << "Couldn't save" << filename << "error:" << jsonFile.errorString();
return false; return false;
} }
return true; return true;

View file

@ -71,7 +71,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath)
QFile iconFile(iconPath); QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) { if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application: %1").arg(iconFile.errorString()));
return false; return false;
} }
@ -101,7 +101,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath)
QFile iconFile(iconPath); QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) { if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString()));
return false; return false;
} }
bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG");
@ -127,7 +127,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath)
QFile iconFile(iconPath); QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) { if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString()));
return false; return false;
} }
bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO");

View file

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

View file

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

View file

@ -274,9 +274,6 @@ void World::readFromZip(const QFileInfo& file)
QFileInfo fi(filePath); QFileInfo fi(filePath);
if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) {
m_containerOffsetPath = filePath.chopped(levelDat.length()); m_containerOffsetPath = filePath.chopped(levelDat.length());
if (!m_containerOffsetPath.isEmpty()) {
return false;
}
m_levelDatTime = file->dateTime(); m_levelDatTime = file->dateTime();
loadFromLevelDat(file->readAll()); loadFromLevelDat(file->readAll());
m_isValid = true; m_isValid = true;

View file

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

View file

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

View file

@ -450,7 +450,7 @@ bool AccountList::loadList()
// Try to open the file and fail if we can't. // Try to open the file and fail if we can't.
// TODO: We should probably report this error to the user. // TODO: We should probably report this error to the user.
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); qCritical() << QString("Failed to read the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8();
return false; return false;
} }
@ -567,7 +567,7 @@ bool AccountList::saveList()
// Try to open the file and fail if we can't. // Try to open the file and fail if we can't.
// TODO: We should probably report this error to the user. // TODO: We should probably report this error to the user.
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly)) {
qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); qCritical() << QString("Failed to save the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8();
return false; return false;
} }
@ -578,7 +578,7 @@ bool AccountList::saveList()
qDebug() << "Saved account list to" << m_listFilePath; qDebug() << "Saved account list to" << m_listFilePath;
return true; return true;
} else { } else {
qDebug() << "Failed to save accounts to" << m_listFilePath; qDebug() << "Failed to save accounts to" << m_listFilePath << "error:" << file.errorString();
return false; return false;
} }
} }
@ -648,9 +648,17 @@ void AccountList::tryNext()
while (m_refreshQueue.length()) { while (m_refreshQueue.length()) {
auto accountId = m_refreshQueue.front(); auto accountId = m_refreshQueue.front();
m_refreshQueue.pop_front(); m_refreshQueue.pop_front();
bool found = false;
for (int i = 0; i < count(); i++) { for (int i = 0; i < count(); i++) {
auto account = at(i); auto account = at(i);
if (account->internalId() == accountId) { if (account->internalId() == accountId) {
found = true;
if (!account->shouldRefresh()) {
// Account no longer needs refreshing, skip it.
qDebug() << "RefreshSchedule: Skipping account" << account->profileName() << "with internal ID"
<< accountId << "(no longer needs refresh)";
break;
}
m_currentTask = account->refresh(); m_currentTask = account->refresh();
if (m_currentTask) { if (m_currentTask) {
connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded);
@ -660,9 +668,12 @@ void AccountList::tryNext()
<< accountId; << accountId;
return; 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. // if we get here, no account needed refreshing. Schedule refresh in an hour.
m_refreshTimer->start(1000 * 3600); m_refreshTimer->start(1000 * 3600);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,113 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2026 Octol1ttle <l1ttleofficial@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "EnsureAvailableMemory.h"
#include "HardwareInfo.h"
#include "ui/dialogs/CustomMessageBox.h"
EnsureAvailableMemory::EnsureAvailableMemory(LaunchTask* parent, MinecraftInstance* instance) : LaunchStep(parent), m_instance(instance) {}
void EnsureAvailableMemory::executeTask()
{
#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;
}
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("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);
shouldAbort = dialog->exec() == QMessageBox::No;
dialog->deleteLater();
}
const auto message = tr("Not enough RAM available to launch this instance");
if (shouldAbort) {
emit logLine(message, MessageLevel::Fatal);
emitFailed(message);
return;
}
emit logLine(message, MessageLevel::Warning);
}
emitSucceeded();
#endif
}

View file

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2026 Octol1ttle <l1ttleofficial@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "launch/LaunchStep.h"
#include "minecraft/MinecraftInstance.h"
class EnsureAvailableMemory : public LaunchStep {
Q_OBJECT
public:
explicit EnsureAvailableMemory(LaunchTask* parent, MinecraftInstance* instance);
~EnsureAvailableMemory() override = default;
void executeTask() override;
bool canAbort() const override { return false; }
private:
MinecraftInstance* m_instance;
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -751,7 +751,7 @@ bool loadIconFile(const Mod& mod, QPixmap* pixmap)
if (icon_info.exists() && icon_info.isFile()) { if (icon_info.exists() && icon_info.isFile()) {
QFile icon(icon_info.filePath()); QFile icon(icon_info.filePath());
if (!icon.open(QIODevice::ReadOnly)) { if (!icon.open(QIODevice::ReadOnly)) {
return png_invalid("failed to open file " + icon_info.filePath()); return png_invalid("failed to open file " + icon_info.filePath() + " " + icon.errorString());
} }
auto data = icon.readAll(); auto data = icon.readAll();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,6 +32,7 @@ class QIODevice;
namespace ModPlatform { namespace ModPlatform {
enum class ModLoaderType : std::uint16_t { enum class ModLoaderType : std::uint16_t {
None = 0U,
NeoForge = 1U << 0U, NeoForge = 1U << 0U,
Forge = 1U << 1U, Forge = 1U << 1U,
Cauldron = 1U << 2U, Cauldron = 1U << 2U,
@ -124,7 +125,7 @@ struct IndexedVersion {
bool is_preferred = true; bool is_preferred = true;
QString changelog; QString changelog;
QList<Dependency> dependencies; QList<Dependency> dependencies;
Side side; // this is for flame API Side side = Side::NoSide; // this is for flame API
// For internal use, not provided by APIs // For internal use, not provided by APIs
bool is_currently_selected = false; bool is_currently_selected = false;
@ -172,7 +173,7 @@ struct IndexedPack {
QString logoName; QString logoName;
QString logoUrl; QString logoUrl;
QString websiteUrl; QString websiteUrl;
Side side; Side side = Side::NoSide;
bool versionsLoaded = false; bool versionsLoaded = false;
QList<IndexedVersion> versions; QList<IndexedVersion> versions;

View file

@ -2,7 +2,6 @@
#include "Application.h" #include "Application.h"
#include "Json.h" #include "Json.h"
#include "Version.h"
#include "net/NetJob.h" #include "net/NetJob.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
@ -117,7 +116,8 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
} }
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
return Version(a.version) > Version(b.version); // dates are in RFC 3339 format
return a.date > b.date;
}; };
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
} catch (const JSONValidationError& e) { } catch (const JSONValidationError& e) {
@ -148,9 +148,9 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback<QVe
return netJob; return netJob;
} }
Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks) const Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks, bool askRetry) const
{ {
auto [job, response] = getProject(args.pack->addonId.toString()); auto [job, response] = getProject(args.pack->addonId.toString(), askRetry);
QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] {
auto pack = args.pack; auto pack = args.pack;
@ -284,7 +284,7 @@ QString ResourceAPI::mapMCVersionToModrinth(Version v) const
return verStr; return verStr;
} }
std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId) const std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId, bool askRetry) const
{ {
auto project_url_optional = getInfoURL(addonId); auto project_url_optional = getInfoURL(addonId);
if (!project_url_optional.has_value()) if (!project_url_optional.has_value())
@ -293,6 +293,7 @@ std::pair<Task::Ptr, QByteArray*> ResourceAPI::getProject(QString addonId) const
auto project_url = project_url_optional.value(); auto project_url = project_url_optional.value();
auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network()); auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network());
netJob->setAskRetry(askRetry);
auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url)); auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url));
netJob->addNetAction(action); netJob->addNetAction(action);

View file

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

View file

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

View file

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

View file

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

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