aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt25
-rw-r--r--README.md7
-rw-r--r--default.nix2
-rw-r--r--flake.lock31
-rw-r--r--flake.nix9
-rw-r--r--launcher/CMakeLists.txt7
-rw-r--r--launcher/InstanceImportTask.cpp217
-rw-r--r--launcher/InstanceImportTask.h6
-rw-r--r--launcher/icons/IconList.cpp4
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp124
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h8
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp35
-rw-r--r--launcher/modplatform/flame/PackManifest.h10
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackManifest.cpp22
-rw-r--r--launcher/net/Upload.cpp199
-rw-r--r--launcher/net/Upload.h31
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.cpp15
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.h20
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.ui84
-rw-r--r--launcher/ui/pages/global/APIPage.ui12
-rw-r--r--launcher/ui/pages/global/JavaPage.cpp6
-rw-r--r--launcher/ui/pages/global/JavaPage.ui69
-rw-r--r--launcher/ui/pages/modplatform/ImportPage.cpp4
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.cpp2
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.cpp2
-rw-r--r--nix/NIX.md (renamed from packages/nix/NIX.md)0
-rw-r--r--nix/default.nix (renamed from packages/nix/polymc/default.nix)38
-rw-r--r--nix/flake-compat.nix (renamed from packages/nix/flake-compat.nix)4
-rw-r--r--program_info/CMakeLists.txt3
-rw-r--r--program_info/polymc.manifest.in (renamed from program_info/polymc.manifest)6
-rw-r--r--program_info/polymc.rc.in (renamed from program_info/polymc.rc)6
31 files changed, 789 insertions, 219 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 320984df..11d58213 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -91,13 +91,6 @@ set(Launcher_META_URL "https://meta.polymc.org/v1/" CACHE STRING "URL to fetch L
# Imgur API Client ID
set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application")
-# MSA Client ID
-set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application")
-
-# CurseForge API Key
-# CHANGE THIS IF YOU FORK THIS PROJECT!
-set(Launcher_CURSEFORGE_API_KEY "$2a$10$1Oqr2MX3O4n/ilhFGc597u8tfI3L2Hyr9/rtWDAMRjghSQV2QUuxq" CACHE STRING "CurseForge API Key")
-
# Bug tracker URL
set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.")
@@ -119,6 +112,22 @@ set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PolyMCLauncher/" CACHE STRI
set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules")
set(Launcher_QT_VERSION_MAJOR "5" CACHE STRING "Major Qt version to build against")
+# API Keys
+# NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service
+# of these platforms, please change these API keys beforehand.
+# Be aware that if you were to use these API keys for malicious purposes they might get revoked, which might cause
+# breakage to thousands of users.
+# If you don't plan to use these features of this software, you can just remove these values.
+
+# By using this key in your builds you accept the terms of use laid down in
+# https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use
+set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application")
+
+# By using this key in your builds you accept the terms and conditions laid down in
+# https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions
+# NOTE: CurseForge requires you to change this if you make any kind of derivative work.
+set(Launcher_CURSEFORGE_API_KEY "$2a$10$1Oqr2MX3O4n/ilhFGc597u8tfI3L2Hyr9/rtWDAMRjghSQV2QUuxq" CACHE STRING "CurseForge API Key")
+
#### Check the current Git commit and branch
include(GetGitRevisionDescription)
@@ -128,6 +137,8 @@ message(STATUS "Git commit: ${Launcher_GIT_COMMIT}")
message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}")
set(Launcher_RELEASE_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}")
+set(Launcher_RELEASE_VERSION_NAME4 "${Launcher_RELEASE_VERSION_NAME}.0")
+set(Launcher_RELEASE_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_HOTFIX},0")
string(TIMESTAMP TODAY "%Y-%m-%d")
set(Launcher_RELEASE_TIMESTAMP "${TODAY}")
diff --git a/README.md b/README.md
index c493293d..a08d5dc0 100644
--- a/README.md
+++ b/README.md
@@ -82,8 +82,11 @@ To modify download information or change packaging information send a pull reque
Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue.
+Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions:
+ - [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use)
+ - [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions)
+If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file.
+
All launcher code is available under the GPL-3.0-only license.
-[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3.0-or-later License.
-
The logo and related assets are under the CC BY-SA 4.0 license.
diff --git a/default.nix b/default.nix
index 5abfc1bd..146942d5 100644
--- a/default.nix
+++ b/default.nix
@@ -1 +1 @@
-(import packages/nix/flake-compat.nix).defaultNix
+(import nix/flake-compat.nix).defaultNix
diff --git a/flake.lock b/flake.lock
index e3c490fd..ccdd51da 100644
--- a/flake.lock
+++ b/flake.lock
@@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1648199409,
- "narHash": "sha256-JwPKdC2PoVBkG6E+eWw3j6BMR6sL3COpYWfif7RVb8Y=",
+ "lastModified": 1650374568,
+ "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "64a525ee38886ab9028e6f61790de0832aa3ef03",
+ "rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"type": "github"
},
"original": {
@@ -34,11 +34,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1648219316,
- "narHash": "sha256-Ctij+dOi0ZZIfX5eMhgwugfvB+WZSrvVNAyAuANOsnQ=",
+ "lastModified": 1653326962,
+ "narHash": "sha256-W8feCYqKTsMre4nAEpv5Kx1PVFC+hao/LwqtB2Wci/8=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "30d3d79b7d3607d56546dd2a6b49e156ba0ec634",
+ "rev": "41cc1d5d9584103be4108c1815c350e07c807036",
"type": "github"
},
"original": {
@@ -48,28 +48,11 @@
"type": "github"
}
},
- "quazip": {
- "flake": false,
- "locked": {
- "lastModified": 1643049383,
- "narHash": "sha256-LcJY6yd6GyeL7X5MP4L94diceM1TYespWByliBsjK98=",
- "owner": "stachenov",
- "repo": "quazip",
- "rev": "09ec1d10c6d627f895109b21728dda000cbfa7d1",
- "type": "github"
- },
- "original": {
- "owner": "stachenov",
- "repo": "quazip",
- "type": "github"
- }
- },
"root": {
"inputs": {
"flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus",
- "nixpkgs": "nixpkgs",
- "quazip": "quazip"
+ "nixpkgs": "nixpkgs"
}
}
},
diff --git a/flake.nix b/flake.nix
index e59d6be8..f2247bed 100644
--- a/flake.nix
+++ b/flake.nix
@@ -5,10 +5,9 @@
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
libnbtplusplus = { url = "github:multimc/libnbtplusplus"; flake = false; };
- quazip = { url = "github:stachenov/quazip"; flake = false; };
};
- outputs = { self, nixpkgs, libnbtplusplus, quazip, ... }:
+ outputs = { self, nixpkgs, libnbtplusplus, ... }:
let
# Generate a user-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate;
@@ -23,7 +22,11 @@
pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
in
{
- packages = forAllSystems (system: { polymc = pkgs.${system}.libsForQt5.callPackage ./packages/nix/polymc { inherit version self quazip libnbtplusplus; }; });
+ packages = forAllSystems (system: {
+ polymc = pkgs.${system}.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; };
+ polymc-qt6 = pkgs.${system}.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; };
+ });
+
defaultPackage = forAllSystems (system: self.packages.${system}.polymc);
apps = forAllSystems (system: { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; });
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 15534c71..bbf80185 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -128,6 +128,8 @@ set(NET_SOURCES
net/PasteUpload.h
net/Sink.h
net/Validator.h
+ net/Upload.cpp
+ net/Upload.h
)
# Game launch logic
@@ -837,6 +839,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/SkinUploadDialog.h
ui/dialogs/ModDownloadDialog.cpp
ui/dialogs/ModDownloadDialog.h
+ ui/dialogs/ScrollMessageBox.cpp
+ ui/dialogs/ScrollMessageBox.h
# GUI - widgets
ui/widgets/Common.cpp
@@ -940,6 +944,7 @@ qt5_wrap_ui(LAUNCHER_UI
ui/dialogs/LoginDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
+ ui/dialogs/ScrollMessageBox.ui
)
qt5_add_resources(LAUNCHER_RESOURCES
@@ -958,7 +963,7 @@ qt5_add_resources(LAUNCHER_RESOURCES
######## Windows resource files ########
if(WIN32)
- set(LAUNCHER_RCS ../${Launcher_Branding_WindowsRC})
+ set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC})
endif()
# Add executable
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 4acde16d..09c2a333 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -60,9 +60,9 @@
#include "net/ChecksumValidator.h"
#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/ScrollMessageBox.h"
#include <algorithm>
-#include <iterator>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{
@@ -164,14 +164,14 @@ void InstanceImportTask::processZipPack()
QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
- if (!mmcRoot.isEmpty())
+ if (!mmcRoot.isNull())
{
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << mmcRoot;
root = mmcRoot;
m_modpackType = ModpackType::MultiMC;
}
- else if(!flameRoot.isEmpty())
+ else if(!flameRoot.isNull())
{
// process as Flame pack
qDebug() << "Flame:" << flameRoot;
@@ -394,61 +394,136 @@ void InstanceImportTask::processFlame()
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
{
auto results = m_modIdResolver->getResults();
- m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
- for(const auto& result: results.files)
- {
- QString filename = result.fileName;
- if(!result.required)
- {
- filename += ".disabled";
+ //first check for blocked mods
+ QString text;
+ auto anyBlocked = false;
+ for(const auto& result: results.files.values()) {
+ if (!result.resolved || result.url.isEmpty()) {
+ text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
+ anyBlocked = true;
}
+ }
+ if(anyBlocked) {
+ qWarning() << "Blocked mods found, displaying mod list";
+
+ auto message_dialog = new ScrollMessageBox(m_parent,
+ tr("Blocked mods found"),
+ tr("The following mods were blocked on third party launchers.<br/>"
+ "You will need to manually download them and add them to the modpack"),
+ text);
+ message_dialog->setModal(true);
+ message_dialog->show();
+ connect(message_dialog, &QDialog::rejected, [&]() {
+ m_modIdResolver.reset();
+ emitFailed("Canceled");
+ });
+ connect(message_dialog, &QDialog::accepted, [&]() {
+ m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
+ for (const auto &result: m_modIdResolver->getResults().files) {
+ QString filename = result.fileName;
+ if (!result.required) {
+ filename += ".disabled";
+ }
- auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
- auto path = FS::PathCombine(m_stagingPath , relpath);
+ auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
+ auto path = FS::PathCombine(m_stagingPath, relpath);
- switch(result.type)
- {
- case Flame::File::Type::Folder:
- {
- logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
- // fall-through intentional, we treat these as plain old mods and dump them wherever.
+ switch (result.type) {
+ case Flame::File::Type::Folder: {
+ logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
+ // fall-through intentional, we treat these as plain old mods and dump them wherever.
+ }
+ case Flame::File::Type::SingleFile:
+ case Flame::File::Type::Mod: {
+ if (!result.url.isEmpty()) {
+ qDebug() << "Will download" << result.url << "to" << path;
+ auto dl = Net::Download::makeFile(result.url, path);
+ m_filesNetJob->addNetAction(dl);
+ }
+ break;
+ }
+ case Flame::File::Type::Modpack:
+ logWarning(
+ tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
+ relpath));
+ break;
+ case Flame::File::Type::Cmod2:
+ case Flame::File::Type::Ctoc:
+ case Flame::File::Type::Unknown:
+ logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
+ break;
+ }
}
- case Flame::File::Type::SingleFile:
- case Flame::File::Type::Mod:
- {
- qDebug() << "Will download" << result.url << "to" << path;
- auto dl = Net::Download::makeFile(result.url, path);
- m_filesNetJob->addNetAction(dl);
- break;
+ m_modIdResolver.reset();
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
+ m_filesNetJob.reset();
+ emitSucceeded();
+ }
+ );
+ connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
+ m_filesNetJob.reset();
+ emitFailed(reason);
+ });
+ connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
+ setProgress(current, total);
+ });
+ setStatus(tr("Downloading mods..."));
+ m_filesNetJob->start();
+ });
+ }else{
+ //TODO extract to function ?
+ m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
+ for (const auto &result: m_modIdResolver->getResults().files) {
+ QString filename = result.fileName;
+ if (!result.required) {
+ filename += ".disabled";
+ }
+
+ auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
+ auto path = FS::PathCombine(m_stagingPath, relpath);
+
+ switch (result.type) {
+ case Flame::File::Type::Folder: {
+ logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
+ // fall-through intentional, we treat these as plain old mods and dump them wherever.
+ }
+ case Flame::File::Type::SingleFile:
+ case Flame::File::Type::Mod: {
+ if (!result.url.isEmpty()) {
+ qDebug() << "Will download" << result.url << "to" << path;
+ auto dl = Net::Download::makeFile(result.url, path);
+ m_filesNetJob->addNetAction(dl);
+ }
+ break;
+ }
+ case Flame::File::Type::Modpack:
+ logWarning(
+ tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
+ relpath));
+ break;
+ case Flame::File::Type::Cmod2:
+ case Flame::File::Type::Ctoc:
+ case Flame::File::Type::Unknown:
+ logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
+ break;
}
- case Flame::File::Type::Modpack:
- logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath));
- break;
- case Flame::File::Type::Cmod2:
- case Flame::File::Type::Ctoc:
- case Flame::File::Type::Unknown:
- logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
- break;
}
+ m_modIdResolver.reset();
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
+ m_filesNetJob.reset();
+ emitSucceeded();
+ }
+ );
+ connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
+ m_filesNetJob.reset();
+ emitFailed(reason);
+ });
+ connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
+ setProgress(current, total);
+ });
+ setStatus(tr("Downloading mods..."));
+ m_filesNetJob->start();
}
- m_modIdResolver.reset();
- connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
- {
- m_filesNetJob.reset();
- emitSucceeded();
- }
- );
- connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason)
- {
- m_filesNetJob.reset();
- emitFailed(reason);
- });
- connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
- {
- setProgress(current, total);
- });
- setStatus(tr("Downloading mods..."));
- m_filesNetJob->start();
}
);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
@@ -510,6 +585,7 @@ void InstanceImportTask::processMultiMC()
void InstanceImportTask::processModrinth()
{
std::vector<Modrinth::File> files;
+ std::vector<Modrinth::File> non_whitelisted_files;
QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion;
try {
QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
@@ -524,11 +600,11 @@ void InstanceImportTask::processModrinth()
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
bool had_optional = false;
- for (auto& obj : jsonFiles) {
+ for (auto& modInfo : jsonFiles) {
Modrinth::File file;
- file.path = Json::requireString(obj, "path");
+ file.path = Json::requireString(modInfo, "path");
- auto env = Json::ensureObject(obj, "env");
+ auto env = Json::ensureObject(modInfo, "env");
QString support = Json::ensureString(env, "client", "unsupported");
if (support == "unsupported") {
continue;
@@ -546,7 +622,7 @@ void InstanceImportTask::processModrinth()
file.path += ".disabled";
}
- QJsonObject hashes = Json::requireObject(obj, "hashes");
+ QJsonObject hashes = Json::requireObject(modInfo, "hashes");
QString hash;
QCryptographicHash::Algorithm hashAlgorithm;
hash = Json::ensureString(hashes, "sha1");
@@ -566,13 +642,38 @@ void InstanceImportTask::processModrinth()
file.hashAlgorithm = hashAlgorithm;
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces)
- file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path);
- if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) {
- throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
+
+ file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path);
+
+ if (!file.download.isValid()) {
+ qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(file.download.toString(), file.path);
+ throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
+ }
+ else if (!Modrinth::validateDownloadUrl(file.download)) {
+ qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(file.download.toString(), file.path);
+ non_whitelisted_files.push_back(file);
}
+
files.push_back(file);
}
+ if (!non_whitelisted_files.empty()) {
+ QString text;
+ for (const auto& file : non_whitelisted_files) {
+ text += tr("Filepath: %1<br>URL: <a href='%2'>%2</a><br>").arg(file.path, file.download.toString());
+ }
+
+ auto message_dialog = new ScrollMessageBox(m_parent, tr("Non-whitelisted mods found"),
+ tr("The following mods have URLs that are not whitelisted by Modrinth.\n"
+ "Proceed with caution!"),
+ text);
+ message_dialog->setModal(true);
+ if (message_dialog->exec() == QDialog::Rejected) {
+ emitFailed("Aborted");
+ return;
+ }
+ }
+
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
QString name = it.key();
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
index 5e4d3235..b67d48f3 100644
--- a/launcher/InstanceImportTask.h
+++ b/launcher/InstanceImportTask.h
@@ -42,6 +42,7 @@
#include <QFutureWatcher>
#include "settings/SettingsObject.h"
#include "QObjectPtr.h"
+#include "modplatform/flame/PackManifest.h"
#include <nonstd/optional>
@@ -59,6 +60,10 @@ public:
bool canAbort() const override { return true; }
bool abort() override;
+ const QVector<Flame::File> &getBlockedFiles() const
+ {
+ return m_blockedMods;
+ }
protected:
//! Entry point for tasks.
@@ -87,6 +92,7 @@ private: /* data */
std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QVector<Flame::File> m_blockedMods;
enum class ModpackType{
Unknown,
MultiMC,
diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp
index c269d10a..0ddfae55 100644
--- a/launcher/icons/IconList.cpp
+++ b/launcher/icons/IconList.cpp
@@ -273,7 +273,7 @@ void IconList::installIcons(const QStringList &iconFiles)
QFileInfo fileinfo(file);
if (!fileinfo.isReadable() || !fileinfo.isFile())
continue;
- QString target = FS::PathCombine(m_dir.dirName(), fileinfo.fileName());
+ QString target = FS::PathCombine(getDirectory(), fileinfo.fileName());
QString suffix = fileinfo.suffix();
if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif")
@@ -290,7 +290,7 @@ void IconList::installIcon(const QString &file, const QString &name)
if(!fileinfo.isReadable() || !fileinfo.isFile())
return;
- QString target = FS::PathCombine(m_dir.dirName(), name);
+ QString target = FS::PathCombine(getDirectory(), name);
QFile::copy(file, target);
}
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index 95924a68..a790ab9c 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -1,7 +1,9 @@
#include "FileResolvingTask.h"
+
#include "Json.h"
+#include "net/Upload.h"
-Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
+Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
{}
@@ -10,40 +12,116 @@ void Flame::FileResolvingTask::executeTask()
setStatus(tr("Resolving mod IDs..."));
setProgress(0, m_toProcess.files.size());
m_dljob = new NetJob("Mod id resolver", m_network);
- results.resize(m_toProcess.files.size());
- int index = 0;
- for (auto& file : m_toProcess.files) {
- auto projectIdStr = QString::number(file.projectId);
- auto fileIdStr = QString::number(file.fileId);
- QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
- auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
- m_dljob->addNetAction(dl);
- index++;
- }
+ result.reset(new QByteArray());
+ //build json data to send
+ QJsonObject object;
+
+ object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) {
+ l.push_back(s.fileId);
+ return l;
+ }));
+ QByteArray data = Json::toText(object);
+ auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data);
+ m_dljob->addNetAction(dl);
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
m_dljob->start();
}
void Flame::FileResolvingTask::netJobFinished()
{
- bool failed = false;
int index = 0;
- for (auto& bytes : results) {
- auto& out = m_toProcess.files[index];
+ // job to check modrinth for blocked projects
+ auto job = new NetJob("Modrinth check", m_network);
+ blockedProjects = QMap<File *,QByteArray *>();
+ auto doc = Json::requireDocument(*result);
+ auto array = Json::requireArray(doc.object()["data"]);
+ for (QJsonValueRef file : array) {
+ auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
+ auto& out = m_toProcess.files[fileid];
try {
- failed &= (!out.parseFromBytes(bytes));
+ out.parseFromObject(Json::requireObject(file));
} catch (const JSONValidationError& e) {
- qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
- qCritical() << e.cause();
- qCritical() << "JSON:";
- qCritical() << bytes;
- failed = true;
+ qDebug() << "Blocked mod on curseforge" << out.fileName;
+ auto hash = out.hash;
+ if(!hash.isEmpty()) {
+ auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash);
+ auto output = new QByteArray();
+ auto dl = Net::Download::makeByteArray(QUrl(url), output);
+ QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() {
+ out.resolved = true;
+ });
+
+ job->addNetAction(dl);
+ blockedProjects.insert(&out, output);
+ }
}
index++;
}
- if (!failed) {
- emitSucceeded();
+ connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);
+
+ job->start();
+}
+
+void Flame::FileResolvingTask::modrinthCheckFinished() {
+ qDebug() << "Finished with blocked mods : " << blockedProjects.size();
+
+ for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) {
+ auto &out = *it;
+ auto bytes = blockedProjects[out];
+ if (!out->resolved) {
+ delete bytes;
+ continue;
+ }
+ QJsonDocument doc = QJsonDocument::fromJson(*bytes);
+ auto obj = doc.object();
+ auto array = Json::requireArray(obj,"files");
+ for (auto file: array) {
+ auto fileObj = Json::requireObject(file);
+ auto primary = Json::requireBoolean(fileObj,"primary");
+ if (primary) {
+ out->url = Json::requireUrl(fileObj,"url");
+ qDebug() << "Found alternative on modrinth " << out->fileName;
+ break;
+ }
+ }
+ delete bytes;
+ }
+ //copy to an output list and filter out projects found on modrinth
+ auto block = new QList<File *>();
+ auto it = blockedProjects.keys();
+ std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) {
+ return !f->resolved;
+ });
+ //Display not found mods early
+ if (!block->empty()) {
+ //blocked mods found, we need the slug for displaying.... we need another job :D !
+ auto slugJob = new NetJob("Slug Job", m_network);
+ auto slugs = QVector<QByteArray>(block->size());
+ auto index = 0;
+ for (auto fileInfo: *block) {
+ auto projectId = fileInfo->projectId;
+ slugs[index] = QByteArray();
+ auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId);
+ auto dl = Net::Download::makeByteArray(url, &slugs[index]);
+ slugJob->addNetAction(dl);
+ index++;
+ }
+ connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() {
+ slugJob->deleteLater();
+ auto index = 0;
+ for (const auto &slugResult: slugs) {
+ auto json = QJsonDocument::fromJson(slugResult);
+ auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"),
+ "websiteUrl");
+ auto mod = block->at(index);
+ auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId));
+ mod->websiteUrl = link;
+ index++;
+ }
+ emitSucceeded();
+ });
+ slugJob->start();
} else {
- emitFailed(tr("Some mod ID resolving tasks failed."));
+ emitSucceeded();
}
}
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
index 5e5adcd7..87981f0a 100644
--- a/launcher/modplatform/flame/FileResolvingTask.h
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -10,7 +10,7 @@ class FileResolvingTask : public Task
{
Q_OBJECT
public:
- explicit FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest &toProcess);
+ explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
virtual ~FileResolvingTask() {};
const Flame::Manifest &getResults() const
@@ -27,7 +27,11 @@ protected slots:
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess;
- QVector<QByteArray> results;
+ std::shared_ptr<QByteArray> result;
NetJob::Ptr m_dljob;
+
+ void modrinthCheckFinished();
+
+ QMap<File *, QByteArray *> blockedProjects;
};
}
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index e4f90c1a..12a4b990 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -41,7 +41,7 @@ static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
auto obj = Json::requireObject(item);
Flame::File file;
loadFileV1(file, obj);
- m.files.append(file);
+ m.files.insert(file.fileId,file);
}
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
}
@@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
loadManifestV1(m, obj);
}
-bool Flame::File::parseFromBytes(const QByteArray& bytes)
+bool Flame::File::parseFromObject(const QJsonObject& obj)
{
- auto doc = Json::requireDocument(bytes);
- if (!doc.isObject()) {
- throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
- }
- auto obj = Json::ensureObject(doc.object(), "data");
-
fileName = Json::requireString(obj, "fileName");
-
- QString rawUrl = Json::requireString(obj, "downloadUrl");
- url = QUrl(rawUrl, QUrl::TolerantMode);
- if (!url.isValid()) {
- throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
- }
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
// It is also optional
type = File::Type::SingleFile;
@@ -87,6 +75,25 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes)
// this is probably a mod, dunno what else could modpacks download
targetFolder = "mods";
}
+ // get the hash
+ hash = QString();
+ auto hashes = Json::ensureArray(obj, "hashes");
+ for(QJsonValueRef item : hashes) {
+ auto hobj = Json::requireObject(item);
+ auto algo = Json::requireInteger(hobj, "algo");
+ auto value = Json::requireString(hobj, "value");
+ if (algo == 1) {
+ hash = value;
+ }
+ }
+
+
+ // may throw, if the project is blocked
+ QString rawUrl = Json::ensureString(obj, "downloadUrl");
+ url = QUrl(rawUrl, QUrl::TolerantMode);
+ if (!url.isValid()) {
+ throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
+ }
resolved = true;
return true;
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
index 02f39f0e..26a48d1c 100644
--- a/launcher/modplatform/flame/PackManifest.h
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -2,19 +2,24 @@
#include <QString>
#include <QVector>
+#include <QMap>
#include <QUrl>
+#include <QJsonObject>
namespace Flame
{
struct File
{
// NOTE: throws JSONValidationError
- bool parseFromBytes(const QByteArray &bytes);
+ bool parseFromObject(const QJsonObject& object);
int projectId = 0;
int fileId = 0;
// NOTE: the opposite to 'optional'. This is at the time of writing unused.
bool required = true;
+ QString hash;
+ // NOTE: only set on blocked files ! Empty otherwise.
+ QString websiteUrl;
// our
bool resolved = false;
@@ -54,7 +59,8 @@ struct Manifest
QString name;
QString version;
QString author;
- QVector<Flame::File> files;
+ //File id -> File
+ QMap<int,Flame::File> files;
QString overrides;
};
diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
index f1ad39ce..33116231 100644
--- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
@@ -42,6 +42,8 @@
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include <QSet>
+
static ModrinthAPI api;
namespace Modrinth {
@@ -95,19 +97,15 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc)
auto validateDownloadUrl(QUrl url) -> bool
{
+ static QSet<QString> domainWhitelist{
+ "cdn.modrinth.com",
+ "github.com",
+ "raw.githubusercontent.com",
+ "gitlab.com"
+ };
+
auto domain = url.host();
- if(domain == "cdn.modrinth.com")
- return true;
- if(domain == "edge.forgecdn.net")
- return true;
- if(domain == "media.forgecdn.net")
- return true;
- if(domain == "github.com")
- return true;
- if(domain == "raw.githubusercontent.com")
- return true;
-
- return false;
+ return domainWhitelist.contains(domain);
}
auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
new file mode 100644
index 00000000..bbd27390
--- /dev/null
+++ b/launcher/net/Upload.cpp
@@ -0,0 +1,199 @@
+//
+// Created by timoreo on 20/05/22.
+//
+
+#include "Upload.h"
+
+#include <utility>
+#include "ByteArraySink.h"
+#include "BuildConfig.h"
+#include "Application.h"
+
+namespace Net {
+
+ void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
+ setProgress(bytesReceived, bytesTotal);
+ }
+
+ void Upload::downloadError(QNetworkReply::NetworkError error) {
+ if (error == QNetworkReply::OperationCanceledError) {
+ qCritical() << "Aborted " << m_url.toString();
+ m_state = State::AbortedByUser;
+ } else {
+ // error happened during download.
+ qCritical() << "Failed " << m_url.toString() << " with reason " << error;
+ m_state = State::Failed;
+ }
+ }
+
+ void Upload::sslErrors(const QList<QSslError> &errors) {
+ int i = 1;
+ for (const auto& error : errors) {
+ qCritical() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+ }
+
+ bool Upload::handleRedirect()
+ {
+ QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
+ if (!redirect.isValid()) {
+ if (!m_reply->hasRawHeader("Location")) {
+ // no redirect -> it's fine to continue
+ return false;
+ }
+ // there is a Location header, but it's not correct. we need to apply some workarounds...
+ QByteArray redirectBA = m_reply->rawHeader("Location");
+ if (redirectBA.size() == 0) {
+ // empty, yet present redirect header? WTF?
+ return false;
+ }
+ QString redirectStr = QString::fromUtf8(redirectBA);
+
+ if (redirectStr.startsWith("//")) {
+ /*
+ * IF the URL begins with //, we need to insert the URL scheme.
+ * See: https://bugreports.qt.io/browse/QTBUG-41061
+ * See: http://tools.ietf.org/html/rfc3986#section-4.2
+ */
+ redirectStr = m_reply->url().scheme() + ":" + redirectStr;
+ } else if (redirectStr.startsWith("/")) {
+ /*
+ * IF the URL begins with /, we need to process it as a relative URL
+ */
+ auto url = m_reply->url();
+ url.setPath(redirectStr, QUrl::TolerantMode);
+ redirectStr = url.toString();
+ }
+
+ /*
+ * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues.
+ * FIXME: report Qt bug for this
+ */
+ redirect = QUrl(redirectStr, QUrl::TolerantMode);
+ if (!redirect.isValid()) {
+ qWarning() << "Failed to parse redirect URL:" << redirectStr;
+ downloadError(QNetworkReply::ProtocolFailure);
+ return false;
+ }
+ qDebug() << "Fixed location header:" << redirect;
+ } else {
+ qDebug() << "Location header:" << redirect;
+ }
+
+ m_url = QUrl(redirect.toString());
+ qDebug() << "Following redirect to " << m_url.toString();
+ startAction(m_network);
+ return true;
+ }
+
+ void Upload::downloadFinished() {
+ // handle HTTP redirection first
+ // very unlikely for post requests, still can happen
+ if (handleRedirect()) {
+ qDebug() << "Upload redirected:" << m_url.toString();
+ return;
+ }
+
+ // if the download failed before this point ...
+ if (m_state == State::Succeeded) {
+ qDebug() << "Upload failed but we are allowed to proceed:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit succeeded();
+ return;
+ } else if (m_state == State::Failed) {
+ qDebug() << "Upload failed in previous step:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit failed("");
+ return;
+ } else if (m_state == State::AbortedByUser) {
+ qDebug() << "Upload aborted in previous step:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit aborted();
+ return;
+ }
+
+ // make sure we got all the remaining data, if any
+ auto data = m_reply->readAll();
+ if (data.size()) {
+ qDebug() << "Writing extra" << data.size() << "bytes";
+ m_state = m_sink->write(data);
+ }
+
+ // otherwise, finalize the whole graph
+ m_state = m_sink->finalize(*m_reply.get());
+ if (m_state != State::Succeeded) {
+ qDebug() << "Upload failed to finalize:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit failed("");
+ return;
+ }
+ m_reply.reset();
+ qDebug() << "Upload succeeded:" << m_url.toString();
+ emit succeeded();
+ }
+
+ void Upload::downloadReadyRead() {
+ if (m_state == State::Running) {
+ auto data = m_reply->readAll();
+ m_state = m_sink->write(data);
+ }
+ }
+
+ void Upload::executeTask() {
+ setStatus(tr("Uploading %1").arg(m_url.toString()));
+
+ if (m_state == State::AbortedByUser) {
+ qWarning() << "Attempt to start an aborted Upload:" << m_url.toString();
+ emit aborted();
+ return;
+ }
+ QNetworkRequest request(m_url);
+ m_state = m_sink->init(request);
+ switch (m_state) {
+ case State::Succeeded:
+ emitSucceeded();
+ qDebug() << "Upload cache hit " << m_url.toString();
+ return;
+ case State::Running:
+ qDebug() << "Uploading " << m_url.toString();
+ break;
+ case State::Inactive:
+ case State::Failed:
+ emitFailed("");
+ return;
+ case State::AbortedByUser:
+ emitAborted();
+ return;
+ }
+
+ request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT);
+ if (request.url().host().contains("api.curseforge.com")) {
+ request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8());
+ }
+ //TODO other types of post requests ?
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ QNetworkReply* rep = m_network->post(request, m_post_data);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors);
+ connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead);
+ }
+
+ Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) {
+ auto* up = new Upload();
+ up->m_url = std::move(url);
+ up->m_sink.reset(new ByteArraySink(output));
+ up->m_post_data = std::move(m_post_data);
+ return up;
+ }
+} // Net
diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h
new file mode 100644
index 00000000..ee784c6e
--- /dev/null
+++ b/launcher/net/Upload.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "NetAction.h"
+#include "Sink.h"
+
+namespace Net {
+
+ class Upload : public NetAction {
+ Q_OBJECT
+
+ public:
+ static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data);
+
+ protected slots:
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
+ void downloadError(QNetworkReply::NetworkError error) override;
+ void sslErrors(const QList<QSslError> & errors);
+ void downloadFinished() override;
+ void downloadReadyRead() override;
+
+ public slots:
+ void executeTask() override;
+ private:
+ std::unique_ptr<Sink> m_sink;
+ QByteArray m_post_data;
+
+ bool handleRedirect();
+ };
+
+} // Net
+
diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp
new file mode 100644
index 00000000..afdc4bae
--- /dev/null
+++ b/launcher/ui/dialogs/ScrollMessageBox.cpp
@@ -0,0 +1,15 @@
+#include "ScrollMessageBox.h"
+#include "ui_ScrollMessageBox.h"
+
+
+ScrollMessageBox::ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body) :
+ QDialog(parent), ui(new Ui::ScrollMessageBox) {
+ ui->setupUi(this);
+ this->setWindowTitle(title);
+ ui->label->setText(text);
+ ui->textBrowser->setText(body);
+}
+
+ScrollMessageBox::~ScrollMessageBox() {
+ delete ui;
+}
diff --git a/launcher/ui/dialogs/ScrollMessageBox.h b/launcher/ui/dialogs/ScrollMessageBox.h
new file mode 100644
index 00000000..84aa253a
--- /dev/null
+++ b/launcher/ui/dialogs/ScrollMessageBox.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <QDialog>
+
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class ScrollMessageBox; }
+QT_END_NAMESPACE
+
+class ScrollMessageBox : public QDialog {
+Q_OBJECT
+
+public:
+ ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body);
+
+ ~ScrollMessageBox() override;
+
+private:
+ Ui::ScrollMessageBox *ui;
+};
diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui
new file mode 100644
index 00000000..299d2ecc
--- /dev/null
+++ b/launcher/ui/dialogs/ScrollMessageBox.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ScrollMessageBox</class>
+ <widget class="QDialog" name="ScrollMessageBox">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>455</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">ScrollMessageBox</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QTextBrowser" name="textBrowser">
+ <property name="acceptRichText">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ScrollMessageBox</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ScrollMessageBox</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui
index cf15065b..5c927391 100644
--- a/launcher/ui/pages/global/APIPage.ui
+++ b/launcher/ui/pages/global/APIPage.ui
@@ -36,13 +36,16 @@
<item>
<widget class="QGroupBox" name="groupBox_paste">
<property name="title">
- <string>Pastebin Service</string>
+ <string>&amp;Pastebin Service</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="pasteServiceTypeLabel">
<property name="text">
- <string>Paste Service Type</string>
+ <string>Paste Service &amp;Type</string>
+ </property>
+ <property name="buddy">
+ <cstring>pasteTypeComboBox</cstring>
</property>
</widget>
</item>
@@ -52,7 +55,10 @@
<item>
<widget class="QLabel" name="baseURLLabel">
<property name="text">
- <string>Base URL</string>
+ <string>Base &amp;URL</string>
+ </property>
+ <property name="buddy">
+ <cstring>baseURLEntry</cstring>
</property>
</widget>
</item>
diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp
index b5e8de6c..025771e8 100644
--- a/launcher/ui/pages/global/JavaPage.cpp
+++ b/launcher/ui/pages/global/JavaPage.cpp
@@ -95,7 +95,7 @@ void JavaPage::applySettings()
// Java Settings
s->set("JavaPath", ui->javaPathTextBox->text());
- s->set("JvmArgs", ui->jvmArgsTextBox->text());
+ s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " "));
s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked());
s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked());
JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget());
@@ -120,7 +120,7 @@ void JavaPage::loadSettings()
// Java Settings
ui->javaPathTextBox->setText(s->get("JavaPath").toString());
- ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString());
+ ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString());
ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool());
ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool());
}
@@ -166,7 +166,7 @@ void JavaPage::on_javaTestBtn_clicked()
return;
}
checker.reset(new JavaCommon::TestCheck(
- this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(),
+ this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "),
ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value()));
connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished()));
checker->run();
diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui
index 3e4b12a1..6ccffed4 100644
--- a/launcher/ui/pages/global/JavaPage.ui
+++ b/launcher/ui/pages/global/JavaPage.ui
@@ -150,19 +150,16 @@
<string>Java Runtime</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
- <widget class="QLabel" name="labelJavaPath">
+ <item row="3" column="1">
+ <widget class="QPushButton" name="javaDetectBtn">
<property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
- <string>&amp;Java path:</string>
- </property>
- <property name="buddy">
- <cstring>javaPathTextBox</cstring>
+ <string>&amp;Auto-detect...</string>
</property>
</widget>
</item>
@@ -175,31 +172,31 @@
</sizepolicy>
</property>
<property name="text">
- <string>J&amp;VM arguments:</string>
+ <string>JVM arguments:</string>
</property>
- <property name="buddy">
- <cstring>jvmArgsTextBox</cstring>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="4" column="1">
- <widget class="QCheckBox" name="skipCompatibilityCheckbox">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelJavaPath">
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
- <property name="toolTip">
- <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string>
- </property>
<property name="text">
- <string>&amp;Skip Java compatibility checks</string>
+ <string>&amp;Java path:</string>
+ </property>
+ <property name="buddy">
+ <cstring>javaPathTextBox</cstring>
</property>
</widget>
</item>
- <item row="3" column="1">
- <widget class="QPushButton" name="javaDetectBtn">
+ <item row="3" column="2">
+ <widget class="QPushButton" name="javaTestBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
@@ -207,7 +204,7 @@
</sizepolicy>
</property>
<property name="text">
- <string>&amp;Auto-detect...</string>
+ <string>&amp;Test</string>
</property>
</widget>
</item>
@@ -237,22 +234,22 @@
</item>
</layout>
</item>
- <item row="3" column="2">
- <widget class="QPushButton" name="javaTestBtn">
+ <item row="4" column="1">
+ <widget class="QCheckBox" name="skipCompatibilityCheckbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
+ <property name="toolTip">
+ <string>If enabled, the launcher will not check if an instance is compatible with the selected Java version.</string>
+ </property>
<property name="text">
- <string>&amp;Test</string>
+ <string>&amp;Skip Java compatibility checks</string>
</property>
</widget>
</item>
- <item row="2" column="1" colspan="2">
- <widget class="QLineEdit" name="jvmArgsTextBox"/>
- </item>
<item row="5" column="1">
<widget class="QCheckBox" name="skipJavaWizardCheckbox">
<property name="toolTip">
@@ -263,6 +260,25 @@
</property>
</widget>
</item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QPlainTextEdit" name="jvmArgsTextBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>100</height>
+ </size>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
@@ -291,7 +307,6 @@
<tabstop>permGenSpinBox</tabstop>
<tabstop>javaBrowseBtn</tabstop>
<tabstop>javaPathTextBox</tabstop>
- <tabstop>jvmArgsTextBox</tabstop>
<tabstop>javaDetectBtn</tabstop>
<tabstop>javaTestBtn</tabstop>
<tabstop>tabWidget</tabstop>
diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp
index c7bc13d8..b3ed1b73 100644
--- a/launcher/ui/pages/modplatform/ImportPage.cpp
+++ b/launcher/ui/pages/modplatform/ImportPage.cpp
@@ -117,7 +117,7 @@ void ImportPage::updateState()
if(fi.exists() && (zip || fi.suffix() == "mrpack"))
{
QFileInfo fi(url.fileName());
- dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
dialog->setSuggestedIcon("default");
}
}
@@ -130,7 +130,7 @@ void ImportPage::updateState()
}
// hook, line and sinker.
QFileInfo fi(url.fileName());
- dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this));
dialog->setSuggestedIcon("default");
}
}
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
index ec774621..7e90af47 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -201,7 +201,7 @@ void FlamePage::suggestCurrent()
return;
}
- dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this));
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl,
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
index 27a12cda..7667d169 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp
@@ -175,7 +175,7 @@ void Page::suggestCurrent()
return;
}
- dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion));
QString editedLogoName;
if(selected.logo.toLower().startsWith("ftb"))
{
diff --git a/packages/nix/NIX.md b/nix/NIX.md
index 1ceba9a3..1ceba9a3 100644
--- a/packages/nix/NIX.md
+++ b/nix/NIX.md
diff --git a/packages/nix/polymc/default.nix b/nix/default.nix
index e352209a..969b455e 100644
--- a/packages/nix/polymc/default.nix
+++ b/nix/default.nix
@@ -1,5 +1,5 @@
-{ lib
-, mkDerivation
+{ stdenv
+, lib
, fetchFromGitHub
, cmake
, ninja
@@ -7,10 +7,11 @@
, jdk
, zlib
, file
-, makeWrapper
+, wrapQtAppsHook
, xorg
, libpulseaudio
, qtbase
+, quazip
, libGL
, msaClientID ? ""
@@ -18,7 +19,7 @@
, self
, version
, libnbtplusplus
-, quazip
+, enableLTO ? false
}:
let
@@ -37,41 +38,34 @@ let
gameLibraryPath = libpath + ":/run/opengl-driver/lib";
in
-mkDerivation rec {
+stdenv.mkDerivation rec {
pname = "polymc";
inherit version;
src = lib.cleanSource self;
- nativeBuildInputs = [ cmake ninja file makeWrapper ];
- buildInputs = [ qtbase jdk zlib ];
+ nativeBuildInputs = [ cmake ninja jdk file wrapQtAppsHook ];
+ buildInputs = [ qtbase quazip zlib ];
dontWrapQtApps = true;
- postPatch = lib.optionalString (msaClientID != "") ''
- # add client ID
- substituteInPlace CMakeLists.txt \
- --replace '17b47edd-c884-4997-926d-9e7f9a6b4647' '${msaClientID}'
- '';
-
postUnpack = ''
- # Copy submodules inputs
- rm -rf source/libraries/{libnbtplusplus,quazip}
- mkdir source/libraries/{libnbtplusplus,quazip}
+ # Copy libnbtplusplus
+ rm -rf source/libraries/libnbtplusplus
+ mkdir source/libraries/libnbtplusplus
cp -a ${libnbtplusplus}/* source/libraries/libnbtplusplus
- cp -a ${quazip}/* source/libraries/quazip
- chmod a+r+w source/libraries/{libnbtplusplus,quazip}/*
+ chmod a+r+w source/libraries/libnbtplusplus/*
'';
cmakeFlags = [
"-GNinja"
- "-DLauncher_PORTABLE=OFF"
- ];
+ "-DLauncher_QT_VERSION_MAJOR=${lib.versions.major qtbase.version}"
+ ] ++ lib.optionals enableLTO [ "-DENABLE_LTO=on" ]
+ ++ lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ];
postInstall = ''
# xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128
- wrapProgram $out/bin/polymc \
- "''${qtWrapperArgs[@]}" \
+ wrapQtApp $out/bin/polymc \
--set GAME_LIBRARY_PATH ${gameLibraryPath} \
--prefix POLYMC_JAVA_PATHS : ${jdk}/lib/openjdk/bin/java:${jdk8}/lib/openjdk/bin/java \
--prefix PATH : ${lib.makeBinPath [ xorg.xrandr ]}
diff --git a/packages/nix/flake-compat.nix b/nix/flake-compat.nix
index bb7ee13e..8b6cb99c 100644
--- a/packages/nix/flake-compat.nix
+++ b/nix/flake-compat.nix
@@ -1,9 +1,9 @@
let
- lock = builtins.fromJSON (builtins.readFile ../../flake.lock);
+ lock = builtins.fromJSON (builtins.readFile ../flake.lock);
inherit (lock.nodes.flake-compat.locked) rev narHash;
flake-compat = fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${rev}.tar.gz";
sha256 = narHash;
};
in
-import flake-compat { src = ../..; }
+import flake-compat { src = ../.; }
diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt
index 60549d8d..2cbef1b6 100644
--- a/program_info/CMakeLists.txt
+++ b/program_info/CMakeLists.txt
@@ -21,3 +21,6 @@ set(Launcher_Portable_File "program_info/portable.txt" PARENT_SCOPE)
configure_file(org.polymc.PolyMC.desktop.in org.polymc.PolyMC.desktop)
configure_file(org.polymc.PolyMC.metainfo.xml.in org.polymc.PolyMC.metainfo.xml)
+configure_file(polymc.rc.in polymc.rc @ONLY)
+configure_file(polymc.manifest.in polymc.manifest @ONLY)
+configure_file(polymc.ico polymc.ico COPYONLY)
diff --git a/program_info/polymc.manifest b/program_info/polymc.manifest.in
index 2d9eb165..0eefacac 100644
--- a/program_info/polymc.manifest
+++ b/program_info/polymc.manifest.in
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
- <assemblyIdentity name="PolyMC.Application.1" type="win32" version="1.0.0.0" />
+ <assemblyIdentity name="PolyMC.Application.1" type="win32" version="@Launcher_RELEASE_VERSION_NAME4@" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
@@ -16,15 +16,13 @@
<description>Custom Minecraft launcher for managing multiple installs.</description>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
- <!--The ID below indicates app support for Windows Vista -->
- <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!--The ID below indicates app support for Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!--The ID below indicates app support for Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!--The ID below indicates app support for Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
- <!--The ID below indicates app support for Windows 10 -->
+ <!--The ID below indicates app support for Windows 10/11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
diff --git a/program_info/polymc.rc b/program_info/polymc.rc.in
index 011e944b..0ea9b73a 100644
--- a/program_info/polymc.rc
+++ b/program_info/polymc.rc.in
@@ -7,7 +7,7 @@ IDI_ICON1 ICON DISCARDABLE "polymc.ico"
1 RT_MANIFEST "polymc.manifest"
VS_VERSION_INFO VERSIONINFO
-FILEVERSION 1,0,0,0
+FILEVERSION @Launcher_RELEASE_VERSION_NAME4_COMMA@
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -17,9 +17,9 @@ BEGIN
BEGIN
VALUE "CompanyName", "MultiMC & PolyMC Contributors"
VALUE "FileDescription", "PolyMC"
- VALUE "FileVersion", "1.0.0.0"
+ VALUE "FileVersion", "@Launcher_RELEASE_VERSION_NAME4@"
VALUE "ProductName", "PolyMC"
- VALUE "ProductVersion", "1"
+ VALUE "ProductVersion", "@Launcher_RELEASE_VERSION_NAME4@"
END
END
BLOCK "VarFileInfo"