diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index c439efe25..6d3845dfc 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -79,6 +79,14 @@ curseforge + + CFBundleURLName + Prismlauncher + CFBundleURLSchemes + + prismlauncher + + diff --git a/flake.lock b/flake.lock index caed1a708..961cb2044 100644 --- a/flake.lock +++ b/flake.lock @@ -23,11 +23,11 @@ ] }, "locked": { - "lastModified": 1717285511, - "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", + "lastModified": 1719994518, + "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", + "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", "type": "github" }, "original": { @@ -75,11 +75,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718276985, - "narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", + "lastModified": 1720031269, + "narHash": "sha256-rwz8NJZV+387rnWpTYcXaRNvzUSnnF9aHONoJIYmiUQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3f84a279f1a6290ce154c5531378acc827836fbb", + "rev": "9f4128e00b0ae8ec65918efeba59db998750ead6", "type": "github" }, "original": { @@ -103,11 +103,11 @@ ] }, "locked": { - "lastModified": 1717664902, - "narHash": "sha256-7XfBuLULizXjXfBYy/VV+SpYMHreNRHk9nKMsm1bgb4=", + "lastModified": 1719259945, + "narHash": "sha256-F1h+XIsGKT9TkGO3omxDLEb/9jOOsI6NnzsXFsZhry4=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "cc4d466cb1254af050ff7bdf47f6d404a7c646d1", + "rev": "0ff4381bbb8f7a52ca4a851660fc7a437a4c6e07", "type": "github" }, "original": { diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 6d30d1101..06fc68575 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -292,12 +292,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QString adjustedBy; QString dataPath; // change folder + QString dataDirEnv; QString dirParam = parser.value("dir"); if (!dirParam.isEmpty()) { // the dir param. it makes multimc data path point to whatever the user specified // on command line adjustedBy = "Command line"; dataPath = dirParam; + } else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); + !dataDirEnv.isEmpty()) { + adjustedBy = "System environment"; + dataPath = dataDirEnv; } else { QDir foo; if (DesktopServices::isSnap()) { @@ -443,7 +448,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // search the dataPath() // seach app data standard path - if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) { + if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { qDebug() << "Found" << logRulesPath << "..."; @@ -554,6 +559,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("NumberOfConcurrentTasks", 10); m_settings->registerSetting("NumberOfConcurrentDownloads", 6); + m_settings->registerSetting("RequestTimeout", 60); QString defaultMonospace; int defaultSize = 11; @@ -656,6 +662,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Minecraft mods m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false); + m_settings->registerSetting("SkipModpackUpdatePrompt", false); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); diff --git a/launcher/Application.h b/launcher/Application.h index 7669e08ec..8303c7475 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -48,7 +48,6 @@ #include #include "minecraft/launch/MinecraftServerTarget.h" -#include "ui/themes/CatPack.h" class LaunchController; class LocalPeer; @@ -193,6 +192,8 @@ class Application : public QApplication { void globalSettingsClosed(); int currentCatChanged(int index); + void oauthReplyRecieved(QVariantMap); + #ifdef Q_OS_MACOS void clickedOnDock(); #endif diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 6379bcb4c..e1fa755dd 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -372,13 +372,13 @@ void InstanceList::undoTrashInstance() auto top = m_trashHistory.pop(); - while (QDir(top.rootPath).exists()) { + while (QDir(top.path).exists()) { top.id += "1"; - top.rootPath += "1"; + top.path += "1"; } - qDebug() << "Moving" << top.trashPath << "back to" << top.rootPath; - QFile(top.trashPath).rename(top.rootPath); + qDebug() << "Moving" << top.trashPath << "back to" << top.path; + QFile(top.trashPath).rename(top.path); m_instanceGroupIndex[top.id] = top.groupName; increaseGroupCount(top.groupName); @@ -635,7 +635,7 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) QString inst_type = instanceSettings->get("InstanceType").toString(); - // NOTE: Some PrismLauncher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a + // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a // OneSix instance if (inst_type == "OneSix" || inst_type.isEmpty()) { inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); @@ -710,6 +710,12 @@ void InstanceList::saveGroupList() groupsArr.insert(name, groupObj); } toplevel.insert("groups", groupsArr); + // empty string represents ungrouped "group" + if (m_collapsedGroups.contains("")) { + QJsonObject ungrouped; + ungrouped.insert("hidden", QJsonValue(true)); + toplevel.insert("ungrouped", ungrouped); + } QJsonDocument doc(toplevel); try { FS::write(groupFileName, doc.toJson()); @@ -805,6 +811,16 @@ void InstanceList::loadGroupList() increaseGroupCount(groupName); } } + + bool ungroupedHidden = false; + if (rootObj.value("ungrouped").isObject()) { + QJsonObject ungrouped = rootObj.value("ungrouped").toObject(); + ungroupedHidden = ungrouped.value("hidden").toBool(false); + } + if (ungroupedHidden) { + // empty string represents ungrouped "group" + m_collapsedGroups.insert(""); + } m_groupsLoaded = true; qDebug() << "Group list loaded."; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index 6c36f6ef8..c85fe55c7 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -58,7 +58,7 @@ enum class GroupsState { NotLoaded, Steady, Dirty }; struct TrashHistoryItem { QString id; - QString rootPath; + QString path; QString trashPath; QString groupName; }; diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index 53476897c..be10bbe07 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -1,5 +1,7 @@ #include "InstanceTask.h" +#include "Application.h" +#include "settings/SettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" #include @@ -22,6 +24,9 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name) { + if (APPLICATION->settings()->get("SkipModpackUpdatePrompt").toBool()) + return ShouldUpdate::SkipUpdating; + auto info = CustomMessageBox::selectable( parent, QObject::tr("Similar modpack was found!"), QObject::tr( diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 4e30f72d1..a20d6c515 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -42,6 +42,20 @@ #include #include +/** + * @brief Collect applicable files for the library. + * + * Depending on whether the library is native or not, it adds paths to the + * appropriate lists for jar files, native libraries for 32-bit, and native + * libraries for 64-bit. + * + * @param runtimeContext The current runtime context. + * @param jar List to store paths for jar files. + * @param native List to store paths for native libraries. + * @param native32 List to store paths for 32-bit native libraries. + * @param native64 List to store paths for 64-bit native libraries. + * @param overridePath Optional path to override the default storage path. + */ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, QStringList& jar, QStringList& native, @@ -50,6 +64,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, const QString& overridePath) const { bool local = isLocal(); + // Lambda function to get the absolute file path auto actualPath = [&](QString relPath) { relPath = FS::RemoveInvalidPathChars(relPath); QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); @@ -59,6 +74,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, } return out.absoluteFilePath(); }; + QString raw_storage = storageSuffix(runtimeContext); if (isNative()) { if (raw_storage.contains("${arch}")) { @@ -76,6 +92,19 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, } } +/** + * @brief Get download requests for the library files. + * + * Depending on whether the library is native or not, and the current runtime context, + * this function prepares download requests for the necessary files. It handles both local + * and remote files, checks for stale cache entries, and adds checksummed downloads. + * + * @param runtimeContext The current runtime context. + * @param cache Pointer to the HTTP meta cache. + * @param failedLocalFiles List to store paths for failed local files. + * @param overridePath Optional path to override the default storage path. + * @return QList List of download requests. + */ QList Library::getDownloads(const RuntimeContext& runtimeContext, class HttpMetaCache* cache, QStringList& failedLocalFiles, @@ -85,6 +114,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC bool stale = isAlwaysStale(); bool local = isLocal(); + // Lambda function to check if a local file exists auto check_local_file = [&](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); @@ -97,6 +127,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC return true; }; + // Lambda function to add a download request auto add_download = [&](QString storage, QString url, QString sha1) { if (local) { return check_local_file(storage); @@ -197,6 +228,15 @@ QList Library::getDownloads(const RuntimeContext& runtimeC return out; } +/** + * @brief Check if the library is active in the given runtime context. + * + * This function evaluates rules to determine if the library should be active, + * considering both general rules and native compatibility. + * + * @param runtimeContext The current runtime context. + * @return bool True if the library is active, false otherwise. + */ bool Library::isActive(const RuntimeContext& runtimeContext) const { bool result = true; @@ -217,16 +257,35 @@ bool Library::isActive(const RuntimeContext& runtimeContext) const return result; } +/** + * @brief Check if the library is considered local. + * + * @return bool True if the library is local, false otherwise. + */ bool Library::isLocal() const { return m_hint == "local"; } +/** + * @brief Check if the library is always considered stale. + * + * @return bool True if the library is always stale, false otherwise. + */ bool Library::isAlwaysStale() const { return m_hint == "always-stale"; } +/** + * @brief Get the compatible native classifier for the current runtime context. + * + * This function attempts to match the current runtime context with the appropriate + * native classifier. + * + * @param runtimeContext The current runtime context. + * @return QString The compatible native classifier, or an empty string if none is found. + */ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const { // try to match precise classifier "[os]-[arch]" @@ -241,16 +300,31 @@ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const return entry.value(); } +/** + * @brief Set the storage prefix for the library. + * + * @param prefix The storage prefix to set. + */ void Library::setStoragePrefix(QString prefix) { m_storagePrefix = prefix; } +/** + * @brief Get the default storage prefix for libraries. + * + * @return QString The default storage prefix. + */ QString Library::defaultStoragePrefix() { return "libraries/"; } +/** + * @brief Get the current storage prefix for the library. + * + * @return QString The current storage prefix. + */ QString Library::storagePrefix() const { if (m_storagePrefix.isEmpty()) { @@ -259,6 +333,15 @@ QString Library::storagePrefix() const return m_storagePrefix; } +/** + * @brief Get the filename for the library in the current runtime context. + * + * This function determines the appropriate filename for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The filename of the library. + */ QString Library::filename(const RuntimeContext& runtimeContext) const { if (!m_filename.isEmpty()) { @@ -280,6 +363,15 @@ QString Library::filename(const RuntimeContext& runtimeContext) const return nativeSpec.getFileName(); } +/** + * @brief Get the display name for the library in the current runtime context. + * + * This function returns the display name for the library, defaulting to the filename + * if no display name is set. + * + * @param runtimeContext The current runtime context. + * @return QString The display name of the library. + */ QString Library::displayName(const RuntimeContext& runtimeContext) const { if (!m_displayname.isEmpty()) @@ -287,6 +379,15 @@ QString Library::displayName(const RuntimeContext& runtimeContext) const return filename(runtimeContext); } +/** + * @brief Get the storage suffix for the library in the current runtime context. + * + * This function determines the appropriate storage suffix for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The storage suffix of the library. + */ QString Library::storageSuffix(const RuntimeContext& runtimeContext) const { // non-native? use only the gradle specifier diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index 5648fe9f6..45926206c 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -59,6 +59,9 @@ void AuthFlow::executeTask() void AuthFlow::nextStep() { + if (!Task::isRunning()) { + return; + } if (m_steps.size() == 0) { // we got to the end without an incident... assume this is all. m_currentStep.reset(); @@ -143,4 +146,11 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason) return false; } } +} +bool AuthFlow::abort() +{ + emitAborted(); + if (m_currentStep) + m_currentStep->abort(); + return true; } \ No newline at end of file diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index d99deec3c..4d18ac845 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -24,6 +24,9 @@ class AuthFlow : public Task { AccountTaskState taskState() { return m_taskState; } + public slots: + bool abort() override; + signals: void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h index cbe157790..aaaec6e7f 100644 --- a/launcher/minecraft/auth/AuthStep.h +++ b/launcher/minecraft/auth/AuthStep.h @@ -34,6 +34,7 @@ class AuthStep : public QObject { public slots: virtual void perform() = 0; + virtual void abort() {} signals: void finished(AccountTaskState resultingState, QString message); diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp index d9785b16a..a54366b50 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.cpp +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -29,5 +29,5 @@ void GetSkinStep::onRequestDone() { if (m_task->error() == QNetworkReply::NoError) m_data->minecraftProfile.skin.data = *m_response; - emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got skin")); } diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index 22f5d4069..7df93d04d 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -46,6 +46,8 @@ MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data) { m_clientId = APPLICATION->getMSAClientID(); + connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort); + connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser); } QString MSADeviceCodeStep::describe() @@ -133,9 +135,7 @@ void MSADeviceCodeStep::deviceAutorizationFinished() m_expiration_timer.setTimerType(Qt::VeryCoarseTimer); m_expiration_timer.setInterval(rsp.expires_in * 1000); m_expiration_timer.setSingleShot(true); - connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort); m_expiration_timer.start(); - m_pool_timer.setTimerType(Qt::VeryCoarseTimer); m_pool_timer.setSingleShot(true); startPoolTimer(); @@ -157,8 +157,12 @@ void MSADeviceCodeStep::startPoolTimer() if (m_is_aborted) { return; } + if (m_expiration_timer.remainingTime() < interval * 1000) { + perform(); + return; + } + m_pool_timer.setInterval(interval * 1000); - connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser); m_pool_timer.start(); } diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index e53eebc62..024927b31 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -51,7 +51,7 @@ class MSADeviceCodeStep : public AuthStep { QString describe() override; public slots: - void abort(); + void abort() override; signals: void authorizeWithBrowser(QString url, QString code, int expiresIn); diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 3f31cdc16..3db04bf2f 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -35,22 +35,74 @@ #include "MSAStep.h" -#include #include #include +#include +#include #include "Application.h" +#include "BuildConfig.h" +#include "FileSystem.h" + +#include +#include +#include + +bool isSchemeHandlerRegistered() +{ +#ifdef Q_OS_LINUX + QProcess process; + process.start("xdg-mime", { "query", "default", "x-scheme-handler/" + BuildConfig.LAUNCHER_APP_BINARY_NAME }); + process.waitForFinished(); + QString output = process.readAllStandardOutput().trimmed(); + + return output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME); + +#elif defined(Q_OS_WIN) + QString regPath = QString("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); + QSettings settings(regPath, QSettings::NativeFormat); + + return settings.contains("shell/open/command/."); +#endif + return true; +} + +class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler { + Q_OBJECT + + public: + explicit CustomOAuthOobReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent) + { + connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); + } + ~CustomOAuthOobReplyHandler() override + { + disconnect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); + } + QString callback() const override { return BuildConfig.LAUNCHER_APP_BINARY_NAME + "://oauth/microsoft"; } +}; MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) { m_clientId = APPLICATION->getMSAClientID(); + if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || + QFile::exists(FS::PathCombine(APPLICATION->root(), "portable.txt")) || !isSchemeHandlerRegistered()) - auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this); - replyHandler->setCallbackText( - "