summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/Application.cpp
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
commitd3261e64152397db2dca4d691a990c6bc2a6f4dd (patch)
treefac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/launcher/Application.cpp
parent31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff)
downloadProject-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.tar.gz
Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.zip
NOISSUE add archived projects
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'archived/projt-launcher/launcher/Application.cpp')
-rw-r--r--archived/projt-launcher/launcher/Application.cpp2450
1 files changed, 2450 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/Application.cpp b/archived/projt-launcher/launcher/Application.cpp
new file mode 100644
index 0000000000..cb878e5bc0
--- /dev/null
+++ b/archived/projt-launcher/launcher/Application.cpp
@@ -0,0 +1,2450 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ === Upstream License Block (Do Not Modify) ==============================
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
+ * Copyright (C) 2022 Tayou <git@tayou.org>
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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.
+ ======================================================================== */
+
+#include "Application.h"
+#include "BuildConfig.h"
+
+#include "DataMigrationTask.h"
+#include "java/services/RuntimeCatalog.hpp"
+#include "minecraft/BackupManager.h"
+#include "net/PasteUpload.h"
+#include "tasks/Task.h"
+#include "tools/GenericProfiler.h"
+#include "ui/InstanceWindow.h"
+#include "ui/MainWindow.h"
+#include "ui/ViewLogWindow.h"
+
+#include "ui/dialogs/ProgressDialog.h"
+#include "ui/instanceview/AccessibleInstanceView.h"
+
+#include <QStatusBar>
+
+#include "ui/pages/BasePageProvider.h"
+#include "ui/pages/global/APIPage.h"
+#include "ui/pages/global/AccountListPage.h"
+#include "ui/pages/global/AppearancePage.h"
+#include "ui/pages/global/ExternalToolsPage.h"
+#include "ui/pages/global/JavaPage.h"
+#include "ui/pages/global/LanguagePage.h"
+#include "ui/pages/global/LauncherPage.h"
+#include "ui/pages/global/MinecraftPage.h"
+#include "ui/pages/global/ProxyPage.h"
+
+#include "ui/setupwizard/AutoJavaWizardPage.h"
+#include "ui/setupwizard/JavaWizardPage.h"
+#include "ui/setupwizard/LanguageWizardPage.h"
+#include "ui/setupwizard/LoginWizardPage.h"
+#include "ui/setupwizard/PasteWizardPage.h"
+#include "ui/setupwizard/SearchWizardPage.h"
+#include "ui/setupwizard/SetupWizard.h"
+#include "ui/setupwizard/ThemeWizardPage.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+#include "ui/pagedialog/PageDialog.h"
+
+#include "ui/themes/ThemeManager.h"
+
+#include "ApplicationMessage.h"
+
+#include <iostream>
+#include <mutex>
+
+#include <QAccessible>
+#include <QCommandLineParser>
+#include <QDebug>
+#include <QDir>
+#include <QFileInfo>
+#include <QFileOpenEvent>
+#include <QIcon>
+#include <QLibraryInfo>
+#include <QList>
+#include <QNetworkAccessManager>
+#include <QProgressDialog>
+#include <QStringList>
+#include <QStringLiteral>
+#include <QStyleFactory>
+#include <QTranslator>
+#include <QWindow>
+
+#include "InstanceList.h"
+#include "MTPixmapCache.h"
+
+#include <minecraft/auth/AccountList.hpp>
+#include "icons/IconList.hpp"
+#include "net/HttpMetaCache.h"
+
+#include "java/services/RuntimeCatalog.hpp"
+
+#include "updater/ExternalUpdater.h"
+
+#include "tools/JProfiler.h"
+#include "tools/JVisualVM.h"
+#include "tools/MCEditTool.h"
+
+#include "settings/INISettingsObject.h"
+#include "settings/Setting.h"
+
+#include "meta/Index.hpp"
+#include "translations/TranslationsModel.h"
+
+#include <DesktopServices.h>
+#include <FileSystem.h>
+#include <LocalPeer.h>
+
+#include <stdlib.h>
+#include <sys.h>
+#include <QStringLiteral>
+#include "SysInfo.h"
+
+#ifdef Q_OS_LINUX
+#include <dlfcn.h>
+#include "MangoHud.h"
+#include "gamemode_client.h"
+#endif
+
+#if defined(Q_OS_LINUX)
+#include <sys/statvfs.h>
+#endif
+
+#if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+#include <sys/mount.h>
+#include <sys/types.h>
+#endif
+
+#if defined(Q_OS_MAC)
+#if defined(SPARKLE_ENABLED)
+#include "updater/MacSparkleUpdater.h"
+#endif
+#else
+#include "updater/ProjTExternalUpdater.h"
+#endif
+
+#if defined Q_OS_WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include <windows.h>
+#include <QStyleHints>
+#include "console/WindowsConsole.hpp"
+#endif
+
+#include "console/Console.hpp"
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+
+static const QLatin1String liveCheckFile("live.check");
+
+PixmapCache* PixmapCache::s_instance = nullptr;
+
+static bool isANSIColorConsole;
+
+static QString defaultLogFormat = QStringLiteral("%{time process}"
+ " "
+ "%{if-debug}Debug:%{endif}"
+ "%{if-info}Info:%{endif}"
+ "%{if-warning}Warning:%{endif}"
+ "%{if-critical}Critical:%{endif}"
+ "%{if-fatal}Fatal:%{endif}"
+ " "
+ "%{if-category}[%{category}] %{endif}"
+ "%{message}"
+ " "
+ "(%{function}:%{line})");
+
+#define ansi_reset "\x1b[0m"
+#define ansi_bold "\x1b[1m"
+#define ansi_reset_bold "\x1b[22m"
+#define ansi_faint "\x1b[2m"
+#define ansi_italic "\x1b[3m"
+#define ansi_red_fg "\x1b[31m"
+#define ansi_green_fg "\x1b[32m"
+#define ansi_yellow_fg "\x1b[33m"
+#define ansi_blue_fg "\x1b[34m"
+#define ansi_purple_fg "\x1b[35m"
+#define ansi_inverse "\x1b[7m"
+
+// clang-format off
+static QString ansiLogFormat = QStringLiteral(
+ ansi_faint "%{time process}" ansi_reset
+ " "
+ "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}"
+ "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}"
+ "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}"
+ "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}"
+ "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}"
+ " "
+ "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}"
+ "%{message}"
+ " "
+ ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset
+);
+// clang-format on
+
+#undef ansi_inverse
+#undef ansi_purple_fg
+#undef ansi_blue_fg
+#undef ansi_yellow_fg
+#undef ansi_green_fg
+#undef ansi_red_fg
+#undef ansi_italic
+#undef ansi_faint
+#undef ansi_bold
+#undef ansi_reset_bold
+#undef ansi_reset
+
+/** This is used so that we can output to the log file in addition to the CLI. */
+void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg)
+{
+ static std::mutex loggerMutex;
+ const std::lock_guard<std::mutex> lock(loggerMutex); // synchronized, QFile logFile is not thread-safe
+
+ if (isANSIColorConsole)
+ {
+ // ensure default is set for log file
+ qSetMessagePattern(defaultLogFormat);
+ }
+
+ QString out = qFormatLogMessage(type, context, msg);
+ if (APPLICATION->logModel)
+ {
+ APPLICATION->logModel->append(MessageLevel::getLevel(type), out);
+ }
+
+ out += QChar::LineFeed;
+ APPLICATION->logFile->write(out.toUtf8());
+ APPLICATION->logFile->flush();
+
+ if (isANSIColorConsole)
+ {
+ // format ansi for console;
+ qSetMessagePattern(ansiLogFormat);
+ out = qFormatLogMessage(type, context, msg);
+ out += QChar::LineFeed;
+ }
+
+ QTextStream(stderr) << out.toLocal8Bit();
+ fflush(stderr);
+}
+
+std::tuple<QDateTime, QString, QString, QString, QString> read_lock_File(const QString& path)
+{
+ auto contents = QString(FS::read(path));
+ auto lines = contents.split('\n');
+
+ QDateTime timestamp;
+ QString from, to, target, data_path;
+ for (auto line : lines)
+ {
+ auto index = line.indexOf("=");
+ if (index < 0)
+ continue;
+ auto left = line.left(index);
+ auto right = line.mid(index + 1);
+ if (left.toLower() == "timestamp")
+ {
+ timestamp = QDateTime::fromString(right, Qt::ISODate);
+ }
+ else if (left.toLower() == "from")
+ {
+ from = right;
+ }
+ else if (left.toLower() == "to")
+ {
+ to = right;
+ }
+ else if (left.toLower() == "target")
+ {
+ target = right;
+ }
+ else if (left.toLower() == "data_path")
+ {
+ data_path = right;
+ }
+ }
+ return std::make_tuple(timestamp, from, to, target, data_path);
+}
+
+Application::Application(int& argc, char** argv) : QApplication(argc, argv)
+{
+#if defined Q_OS_WIN32
+ // attach the parent console if stdout not already captured
+ if (projt::console::AttachWindowsConsole())
+ {
+ consoleAttached = true;
+ if (auto err = projt::console::EnableAnsiSupport(); !err)
+ {
+ isANSIColorConsole = true;
+ }
+ else
+ {
+ std::cout << "Error setting up ansi console" << err.message() << std::endl;
+ }
+ }
+#else
+ if (projt::console::isConsole())
+ {
+ isANSIColorConsole = true;
+ }
+#endif
+
+ setOrganizationName(BuildConfig.LAUNCHER_NAME);
+ setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
+ setApplicationName(BuildConfig.LAUNCHER_NAME);
+ setApplicationDisplayName(
+ QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()));
+ setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
+ setDesktopFileName(BuildConfig.LAUNCHER_APPID);
+ m_startTime = QDateTime::currentDateTime();
+
+ // Don't quit on hiding the last window
+ this->setQuitOnLastWindowClosed(false);
+ this->setQuitLockEnabled(false);
+
+ // Commandline parsing
+ QCommandLineParser parser;
+ parser.setApplicationDescription(BuildConfig.LAUNCHER_DISPLAYNAME);
+
+ parser.addOptions(
+ { { { "d", "dir" }, "Use a custom path as application root (use '.' for current directory)", "directory" },
+ { { "l", "launch" }, "Launch the specified instance (by instance ID)", "instance" },
+ { { "s", "server" },
+ "Join the specified server on launch (only valid in combination with --launch)",
+ "address" },
+ { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" },
+ { { "a", "profile" },
+ "Use the account specified by its profile name (only valid in combination with --launch)",
+ "profile" },
+ { { "o", "offline" },
+ "Launch offline, with given player name (only valid in combination with --launch)",
+ "offline" },
+ { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" },
+ { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" },
+ { "show", "Opens the window for the specified instance (by instance ID)", "show" } });
+ // Has to be positional for some OS to handle that properly
+ parser.addPositionalArgument("URL",
+ "Import the resource(s) at the given URL(s) (same as -I / --import)",
+ "[URL...]");
+
+ parser.addHelpOption();
+ parser.addVersionOption();
+
+ parser.process(arguments());
+
+ m_instanceIdToLaunch = parser.value("launch");
+ m_serverToJoin = parser.value("server");
+ m_worldToJoin = parser.value("world");
+ m_profileToUse = parser.value("profile");
+ if (parser.isSet("offline"))
+ {
+ m_offline = true;
+ m_offlineName = parser.value("offline");
+ }
+ m_liveCheck = parser.isSet("alive");
+
+ m_instanceIdToShowWindowOf = parser.value("show");
+
+ for (auto url : parser.values("import"))
+ {
+ m_urlsToImport.append(normalizeImportUrl(url));
+ }
+
+ // treat unspecified positional arguments as import urls
+ for (auto url : parser.positionalArguments())
+ {
+ m_urlsToImport.append(normalizeImportUrl(url));
+ }
+
+ // error if --launch is missing with --server or --profile
+ if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_offline)
+ && m_instanceIdToLaunch.isEmpty())
+ {
+ std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl;
+ m_status = Application::Failed;
+ return;
+ }
+
+ QString origcwdPath = QDir::currentPath();
+ QString binPath = applicationDirPath();
+
+ {
+ // Root path is used for updates and portable data
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr
+ m_rootPath = foo.absolutePath();
+#elif defined(Q_OS_WIN32)
+ m_rootPath = binPath;
+#elif defined(Q_OS_MAC)
+ QDir foo(FS::PathCombine(binPath, "../.."));
+ m_rootPath = foo.absolutePath();
+ // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues)
+ FS::updateTimestamp(m_rootPath);
+#endif
+ }
+
+ 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())
+ {
+ foo = QDir(qEnvironmentVariable("SNAP_USER_COMMON"));
+ }
+ else
+ {
+ foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), ".."));
+ }
+
+ dataPath = foo.absolutePath();
+ adjustedBy = "Persistent data path";
+
+#ifndef Q_OS_MACOS
+ if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists())
+ {
+ dataPath = portableUserData;
+ adjustedBy = "Portable user data path";
+ m_portable = true;
+ }
+ else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt")))
+ {
+ dataPath = m_rootPath;
+ adjustedBy = "Portable data path";
+ m_portable = true;
+ }
+#endif
+ }
+
+ if (!FS::ensureFolderPathExists(dataPath))
+ {
+ showFatalErrorMessage("The launcher data folder could not be created.",
+ QString("The launcher data folder could not be created.\n"
+ "\n"
+ "Make sure you have the right permissions to the launcher data folder and any "
+ "folder needed to access it.\n"
+ "(%1)\n"
+ "\n"
+ "The launcher cannot continue until you fix this problem.")
+ .arg(dataPath));
+ return;
+ }
+ if (!QDir::setCurrent(dataPath))
+ {
+ showFatalErrorMessage("The launcher data folder could not be opened.",
+ QString("The launcher data folder could not be opened.\n"
+ "\n"
+ "Make sure you have the right permissions to the launcher data folder.\n"
+ "(%1)\n"
+ "\n"
+ "The launcher cannot continue until you fix this problem.")
+ .arg(dataPath));
+ return;
+ }
+ m_dataPath = dataPath;
+
+ /*
+ * Establish the mechanism for communication with an already running ProjTLauncher that uses the same binary.
+ * If there is one, tell it what the user actually wanted to do and exit.
+ * We want to initialize this before logging to avoid messing with the log of a potential already running copy.
+ *
+ * Using binary path (applicationDirPath) instead of data path prevents update conflicts
+ * when multiple instances with different data directories share the same binary.
+ */
+ auto appID =
+ ApplicationId::fromPathAndVersion(QCoreApplication::applicationDirPath(), BuildConfig.printableVersionString());
+ {
+ m_peerInstance = new LocalPeer(this, appID);
+ connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived);
+ if (m_peerInstance->isClient())
+ {
+ bool sentMessage = false;
+ int timeout = 2000;
+
+ if (m_instanceIdToLaunch.isEmpty())
+ {
+ ApplicationMessage activate;
+ activate.command = "activate";
+ sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout);
+
+ if (!m_urlsToImport.isEmpty())
+ {
+ for (auto url : m_urlsToImport)
+ {
+ ApplicationMessage import;
+ import.command = "import";
+ import.args.insert("url", url.toString());
+ sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout);
+ }
+ }
+ }
+ else
+ {
+ ApplicationMessage launch;
+ launch.command = "launch";
+ launch.args["id"] = m_instanceIdToLaunch;
+
+ if (!m_serverToJoin.isEmpty())
+ {
+ launch.args["server"] = m_serverToJoin;
+ }
+ else if (!m_worldToJoin.isEmpty())
+ {
+ launch.args["world"] = m_worldToJoin;
+ }
+ if (!m_profileToUse.isEmpty())
+ {
+ launch.args["profile"] = m_profileToUse;
+ }
+ if (m_offline)
+ {
+ launch.args["offline_enabled"] = "true";
+ launch.args["offline_name"] = m_offlineName;
+ }
+ sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout);
+ }
+ if (sentMessage)
+ {
+ m_status = Application::Succeeded;
+ return;
+ }
+ else
+ {
+ std::cerr << "Unable to redirect command to already running instance\n";
+ // C function not Qt function - event loop not started yet
+ ::exit(1);
+ }
+ }
+ }
+
+ // init the logger
+ {
+ static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
+ static const QString logBase = FS::PathCombine("logs", baseLogFile);
+ if (FS::ensureFolderPathExists("logs"))
+ { // if this did not fail
+ for (auto i = 0; i <= 4; i++)
+ if (auto oldName = baseLogFile.arg(i); QFile::exists(oldName)) // do not pointlessly delete new files if
+ // the old ones are not there
+ FS::move(oldName, logBase.arg(i));
+ }
+
+ for (auto i = 4; i > 0; i--)
+ {
+ const QString source = logBase.arg(i - 1);
+ if (QFile::exists(source))
+ {
+ FS::move(source, logBase.arg(i));
+ }
+ }
+
+ logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
+ if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
+ {
+ showFatalErrorMessage("The launcher data folder is not writable!",
+ QString("The launcher couldn't create a log file - the data folder is not writable.\n"
+ "\n"
+ "Make sure you have write permissions to the data folder.\n"
+ "(%1)\n"
+ "\n"
+ "The launcher cannot continue until you fix this problem.")
+ .arg(dataPath));
+ return;
+ }
+ qInstallMessageHandler(appDebugOutput);
+ qSetMessagePattern(defaultLogFormat);
+
+ logModel.reset(new projt::launch::LaunchLogModel(this));
+
+ bool foundLoggingRules = false;
+
+ auto logRulesFile = QStringLiteral("qtlogging.ini");
+ auto logRulesPath = FS::PathCombine(dataPath, logRulesFile);
+
+ qInfo() << "Testing" << logRulesPath << "...";
+ foundLoggingRules = QFile::exists(logRulesPath);
+
+ // search the dataPath()
+ // seach app data standard path
+ if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty())
+ {
+ logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
+ if (!logRulesPath.isEmpty())
+ {
+ qInfo() << "Found" << logRulesPath << "...";
+ foundLoggingRules = true;
+ }
+ }
+ // seach root path
+ if (!foundLoggingRules)
+ {
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile);
+#else
+ logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
+#endif
+ qInfo() << "Testing" << logRulesPath << "...";
+ foundLoggingRules = QFile::exists(logRulesPath);
+ }
+
+ if (foundLoggingRules)
+ {
+ // load and set logging rules
+ qInfo() << "Loading logging rules from:" << logRulesPath;
+ QSettings loggingRules(logRulesPath, QSettings::IniFormat);
+ loggingRules.beginGroup("Rules");
+ QStringList rule_names = loggingRules.childKeys();
+ QStringList rules;
+ qInfo() << "Setting log rules:";
+ for (auto rule_name : rule_names)
+ {
+ auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString());
+ rules.append(rule);
+ qInfo() << " " << rule;
+ }
+ auto rules_str = rules.join("\n");
+ QLoggingCategory::setFilterRules(rules_str);
+ }
+
+ qInfo() << "<> Log initialized.";
+ }
+
+ {
+ bool migrated = false;
+
+ if (!migrated)
+ migrated = handleDataMigration(
+ dataPath,
+ FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"),
+ "PolyMC",
+ "polymc.cfg");
+ if (!migrated)
+ migrated = handleDataMigration(
+ dataPath,
+ FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"),
+ "MultiMC",
+ "multimc.cfg");
+ }
+
+ {
+ qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", "
+ + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
+ qInfo() << "Version : " << BuildConfig.printableVersionString();
+ qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM;
+ qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT;
+ qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
+ qInfo() << "Compiled for : " << BuildConfig.systemID();
+ qInfo() << "Compiled by : " << BuildConfig.compilerID();
+ qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT;
+ qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No");
+ if (adjustedBy.size())
+ {
+ qInfo() << "Work dir before adjustment : " << origcwdPath;
+ qInfo() << "Work dir after adjustment : " << QDir::currentPath();
+ qInfo() << "Adjusted by : " << adjustedBy;
+ }
+ else
+ {
+ qInfo() << "Work dir : " << QDir::currentPath();
+ }
+ qInfo() << "Binary path : " << binPath;
+ qInfo() << "Application root path : " << m_rootPath;
+ if (!m_instanceIdToLaunch.isEmpty())
+ {
+ qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch;
+ }
+ if (!m_serverToJoin.isEmpty())
+ {
+ qInfo() << "Address of server to join :" << m_serverToJoin;
+ }
+ else if (!m_worldToJoin.isEmpty())
+ {
+ qInfo() << "Name of the world to join :" << m_worldToJoin;
+ }
+ qInfo() << "<> Paths set.";
+ }
+
+ if (m_liveCheck)
+ {
+ QFile check(liveCheckFile);
+ if (check.open(QIODevice::WriteOnly | QIODevice::Truncate))
+ {
+ auto payload = appID.toString().toUtf8();
+ if (check.write(payload) == payload.size())
+ {
+ check.close();
+ }
+ else
+ {
+ qWarning() << "Could not write into" << liveCheckFile << "!";
+ check.remove(); // also closes file!
+ }
+ }
+ else
+ {
+ qWarning() << "Could not open" << liveCheckFile << "for writing!";
+ }
+ }
+
+ // Initialize application settings
+ {
+ // Provide a fallback for migration from PolyMC
+ m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this));
+
+ // Theming
+ m_settings->registerSetting("IconTheme", QString());
+ m_settings->registerSetting("ApplicationTheme", QString());
+ m_settings->registerSetting("BackgroundCat", QString("kitteh"));
+ m_settings->registerSetting("HubSearchEngine", QString());
+
+ // Remembered state
+ m_settings->registerSetting("LastUsedGroupForNewInstance", QString());
+
+ m_settings->registerSetting("MenuBarInsteadOfToolBar", false);
+
+ m_settings->registerSetting("NumberOfConcurrentTasks", 10);
+ m_settings->registerSetting("NumberOfConcurrentDownloads", 6);
+ m_settings->registerSetting("NumberOfManualRetries", 1);
+ m_settings->registerSetting("RequestTimeout", 60);
+
+ QString defaultMonospace;
+ int defaultSize = 11;
+#ifdef Q_OS_WIN32
+ defaultMonospace = "Courier";
+ defaultSize = 10;
+#elif defined(Q_OS_MAC)
+ defaultMonospace = "Menlo";
+#else
+ defaultMonospace = "Monospace";
+#endif
+
+ // resolve the font so the default actually matches
+ QFont consoleFont;
+ consoleFont.setFamily(defaultMonospace);
+ consoleFont.setStyleHint(QFont::Monospace);
+ consoleFont.setFixedPitch(true);
+ QFontInfo consoleFontInfo(consoleFont);
+ QString resolvedDefaultMonospace = consoleFontInfo.family();
+ QFont resolvedFont(resolvedDefaultMonospace);
+ qDebug() << "Detected default console font:" << resolvedDefaultMonospace
+ << ", substitutions:" << resolvedFont.substitutions().join(',');
+
+ m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace);
+ m_settings->registerSetting("ConsoleFontSize", defaultSize);
+ m_settings->registerSetting("ConsoleMaxLines", 100000);
+ m_settings->registerSetting("ConsoleOverflowStop", true);
+
+ logModel->setMaxLines(getConsoleMaxLines(settings()));
+ logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings()));
+ logModel->setOverflowMessage(
+ tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines()));
+
+ // Folders
+ m_settings->registerSetting("InstanceDir", "instances");
+ m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods");
+ m_settings->registerSetting("IconsDir", "icons");
+ m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
+ m_settings->registerSetting("DownloadsDirWatchRecursive", false);
+ m_settings->registerSetting("MoveModsFromDownloadsDir", false);
+ m_settings->registerSetting("SkinsDir", "skins");
+ m_settings->registerSetting("JavaDir", "java");
+
+ // Editors
+ m_settings->registerSetting("JsonEditor", QString());
+
+ // Language
+ m_settings->registerSetting("Language", QString());
+ m_settings->registerSetting("UseSystemLocale", false);
+
+ // Console
+ m_settings->registerSetting("ShowConsole", false);
+ m_settings->registerSetting("AutoCloseConsole", false);
+ m_settings->registerSetting("ShowConsoleOnError", true);
+ m_settings->registerSetting("LogPrePostOutput", true);
+
+ // Window Size
+ m_settings->registerSetting({ "LaunchMaximized", "MCWindowMaximize" }, false);
+ m_settings->registerSetting({ "MinecraftWinWidth", "MCWindowWidth" }, 854);
+ m_settings->registerSetting({ "MinecraftWinHeight", "MCWindowHeight" }, 480);
+
+ // Proxy Settings
+ m_settings->registerSetting("ProxyType", "None");
+ m_settings->registerSetting({ "ProxyAddr", "ProxyHostName" }, "127.0.0.1");
+ m_settings->registerSetting("ProxyPort", 8080);
+ m_settings->registerSetting({ "ProxyUser", "ProxyUsername" }, "");
+ m_settings->registerSetting({ "ProxyPass", "ProxyPassword" }, "");
+
+ // Memory
+ m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512);
+ m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem());
+ m_settings->registerSetting("PermGen", 128);
+ // Java Settings
+ m_settings->registerSetting("JavaPath", "");
+ m_settings->registerSetting("JavaSignature", "");
+ m_settings->registerSetting("JavaArchitecture", "");
+ m_settings->registerSetting("JavaRealArchitecture", "");
+ m_settings->registerSetting("JavaVersion", "");
+ m_settings->registerSetting("JavaVendor", "");
+ m_settings->registerSetting("LastHostname", "");
+ m_settings->registerSetting("JvmArgs", "");
+ m_settings->registerSetting("IgnoreJavaCompatibility", false);
+ m_settings->registerSetting("IgnoreJavaWizard", false);
+ auto defaultEnableAutoJava = m_settings->get("JavaPath").toString().isEmpty();
+ m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava);
+ m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava);
+ m_settings->registerSetting("UserAskedAboutAutomaticJavaDownload", false);
+
+ // Legacy settings
+ m_settings->registerSetting("OnlineFixes", false);
+
+ // Native library workarounds
+ m_settings->registerSetting("UseNativeOpenAL", false);
+ m_settings->registerSetting("CustomOpenALPath", "");
+ m_settings->registerSetting("UseNativeGLFW", false);
+ m_settings->registerSetting("CustomGLFWPath", "");
+
+ // Performance related options
+ m_settings->registerSetting("EnableFeralGamemode", false);
+ m_settings->registerSetting("EnableMangoHud", false);
+ m_settings->registerSetting("UseDiscreteGpu", false);
+ m_settings->registerSetting("UseZink", false);
+
+ // Game time
+ m_settings->registerSetting("ShowGameTime", true);
+ m_settings->registerSetting("ShowGlobalGameTime", true);
+ m_settings->registerSetting("RecordGameTime", true);
+ m_settings->registerSetting("ShowGameTimeWithoutDays", false);
+
+ // Minecraft mods
+ m_settings->registerSetting("ModMetadataDisabled", false);
+ m_settings->registerSetting("ModDependenciesDisabled", false);
+ m_settings->registerSetting("SkipModpackUpdatePrompt", false);
+ m_settings->registerSetting("ShowModIncompat", false);
+
+ // Minecraft offline player name
+ m_settings->registerSetting("LastOfflinePlayerName", "");
+
+ // Backup settings
+ m_settings->registerSetting("AutoBackupBeforeLaunch", false);
+
+ // Wrapper command for launch
+ m_settings->registerSetting("WrapperCommand", "");
+
+ // Custom Commands
+ m_settings->registerSetting({ "PreLaunchCommand", "PreLaunchCmd" }, "");
+ m_settings->registerSetting({ "PostExitCommand", "PostExitCmd" }, "");
+
+ // The cat
+ m_settings->registerSetting("TheCat", false);
+ m_settings->registerSetting("CatOpacity", 100);
+ m_settings->registerSetting("CatFit", "fit");
+
+ m_settings->registerSetting("StatusBarVisible", true);
+
+ m_settings->registerSetting("ToolbarsLocked", false);
+
+ // Instance
+ m_settings->registerSetting("InstSortMode", "Name");
+ m_settings->registerSetting("InstRenamingMode", "AskEverytime");
+ m_settings->registerSetting("SelectedInstance", QString());
+
+ // Window state and geometry
+ m_settings->registerSetting("MainWindowState", "");
+ m_settings->registerSetting("MainWindowGeometry", "");
+
+ m_settings->registerSetting("ConsoleWindowState", "");
+ m_settings->registerSetting("ConsoleWindowGeometry", "");
+
+ m_settings->registerSetting("SettingsGeometry", "");
+
+ m_settings->registerSetting("PagedGeometry", "");
+
+ m_settings->registerSetting("NewInstanceGeometry", "");
+
+ m_settings->registerSetting("UpdateDialogGeometry", "");
+
+ m_settings->registerSetting("ModDownloadGeometry", "");
+ m_settings->registerSetting("RPDownloadGeometry", "");
+ m_settings->registerSetting("TPDownloadGeometry", "");
+ m_settings->registerSetting("ShaderDownloadGeometry", "");
+ m_settings->registerSetting("DataPackDownloadGeometry", "");
+
+ m_settings->registerSetting("NewsGeometry", "");
+
+ // data pack window
+ // in future, more pages may be added - so this name is chosen to avoid needing migration
+ m_settings->registerSetting("WorldManagementGeometry", "");
+
+ // Pastebin settings with automatic migration from legacy format
+ migratePastebinSettings();
+
+ {
+ // Meta URL
+ m_settings->registerSetting("MetaURLOverride", "");
+
+ QUrl metaUrl(m_settings->get("MetaURLOverride").toString());
+
+ // get rid of invalid meta urls
+ if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https"))
+ m_settings->reset("MetaURLOverride");
+
+ // Resource URL
+ m_settings->registerSetting("ResourceURL", "");
+ QString resourceUrlStr = m_settings->get("ResourceURL").toString();
+ if (!resourceUrlStr.isEmpty())
+ {
+ QUrl resourceUrl(resourceUrlStr);
+ if (!resourceUrl.isValid() || (resourceUrl.scheme() != "http" && resourceUrl.scheme() != "https"))
+ m_settings->reset("ResourceURL");
+ }
+ }
+ m_settings->registerSetting("CloseAfterLaunch", false);
+ m_settings->registerSetting("QuitAfterGameStop", false);
+
+ m_settings->registerSetting("Env", "{}");
+
+ // Custom Microsoft Authentication Client ID
+ m_settings->registerSetting("MSAClientIDOverride", "");
+
+ // Custom Flame API Key
+ {
+ m_settings->registerSetting("CFKeyOverride", "");
+ m_settings->registerSetting("FlameKeyOverride", "");
+
+ QString flameKey = m_settings->get("CFKeyOverride").toString();
+
+ if (!flameKey.isEmpty())
+ m_settings->set("FlameKeyOverride", flameKey);
+ m_settings->reset("CFKeyOverride");
+ }
+ m_settings->registerSetting("ModrinthToken", "");
+ m_settings->registerSetting("FallbackMRBlockedMods", true);
+ m_settings->registerSetting("UserAgentOverride", "");
+
+ // FTBApp instances
+ m_settings->registerSetting("FTBAppInstancesPath", "");
+
+ // Custom Technic Client ID
+ m_settings->registerSetting("TechnicClientID", "");
+
+ // Init page provider
+ {
+ m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));
+ m_globalSettingsProvider->addPage<LauncherPage>();
+ m_globalSettingsProvider->addPage<LanguagePage>();
+ m_globalSettingsProvider->addPage<AppearancePage>();
+ m_globalSettingsProvider->addPage<MinecraftPage>();
+ m_globalSettingsProvider->addPage<JavaPage>();
+ m_globalSettingsProvider->addPage<AccountListPage>();
+ m_globalSettingsProvider->addPage<APIPage>();
+ m_globalSettingsProvider->addPage<ExternalToolsPage>();
+ m_globalSettingsProvider->addPage<ProxyPage>();
+ }
+
+ PixmapCache::setInstance(new PixmapCache(this));
+
+ qInfo() << "<> Settings loaded.";
+ }
+
+#ifndef QT_NO_ACCESSIBILITY
+ QAccessible::installFactory(groupViewAccessibleFactory);
+#endif /* !QT_NO_ACCESSIBILITY */
+
+ // initialize network access and proxy setup
+ {
+ m_network.reset(new QNetworkAccessManager());
+ QString proxyTypeStr = settings()->get("ProxyType").toString();
+ QString addr = settings()->get("ProxyAddr").toString();
+ int port = settings()->get("ProxyPort").value<qint16>();
+ QString user = settings()->get("ProxyUser").toString();
+ QString pass = settings()->get("ProxyPass").toString();
+ updateProxySettings(proxyTypeStr, addr, port, user, pass);
+ qInfo() << "<> Network done.";
+ }
+
+ // load translations
+ {
+ m_translations.reset(new TranslationsModel("translations"));
+ auto bcp47Name = m_settings->get("Language").toString();
+ m_translations->selectLanguage(bcp47Name);
+ qInfo() << "Your language is" << bcp47Name;
+ qInfo() << "<> Translations loaded.";
+ }
+
+ // Instance icons
+ {
+ auto setting = APPLICATION->settings()->getSetting("IconsDir");
+ QStringList instFolders = { ":/icons/multimc/32x32/instances/",
+ ":/icons/multimc/50x50/instances/",
+ ":/icons/multimc/128x128/instances/",
+ ":/icons/multimc/scalable/instances/" };
+ m_icons.reset(new projt::icons::IconList(instFolders, setting->get().toString()));
+ connect(setting.get(),
+ &Setting::SettingChanged,
+ [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); });
+ qInfo() << "<> Instance icons initialized.";
+ }
+
+ // Themes
+ m_themeManager = std::make_unique<ThemeManager>();
+
+ // initialize and load all instances
+ {
+ auto InstDirSetting = m_settings->getSetting("InstanceDir");
+ // instance path: check for problems with '!' in instance path and warn the user in the log
+ // and remember that we have to show him a dialog when the gui starts (if it does so)
+ QString instDir = InstDirSetting->get().toString();
+ qInfo() << "Instance path : " << instDir;
+ if (FS::checkProblemticPathJava(QDir(instDir)))
+ {
+ qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";
+ }
+ m_instances.reset(new InstanceList(m_settings, instDir, this));
+ connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged);
+ qInfo() << "Loading Instances...";
+ m_instances->loadList();
+ qInfo() << "<> Instances loaded.";
+ }
+
+ // and accounts
+ {
+ m_accounts.reset(new AccountList(this));
+ qInfo() << "Loading accounts...";
+ m_accounts->setListFilePath("accounts.json", true);
+ m_accounts->loadList();
+ m_accounts->fillQueue();
+ qInfo() << "<> Accounts loaded.";
+ }
+
+ // init the http meta cache
+ {
+ m_metacache.reset(new HttpMetaCache("metacache"));
+ m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath());
+ m_metacache->addBase("libraries", QDir("libraries").absolutePath());
+ m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath());
+ m_metacache->addBase("general", QDir("cache").absolutePath());
+ m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath());
+ m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
+ m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
+ m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
+ m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath());
+ m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
+ m_metacache->addBase("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath());
+ m_metacache->addBase("translations", QDir("translations").absolutePath());
+ m_metacache->addBase("meta", QDir("meta").absolutePath());
+ m_metacache->addBase("java", QDir("cache/java").absolutePath());
+ m_metacache->Load();
+ qInfo() << "<> Cache initialized.";
+ }
+
+ // now we have network, download translation updates
+ m_translations->downloadIndex();
+
+ // Register Java profiler integrations
+ m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));
+ m_profilers.insert("jvisualvm", std::shared_ptr<BaseProfilerFactory>(new JVisualVMFactory()));
+ m_profilers.insert("generic", std::shared_ptr<BaseProfilerFactory>(new GenericProfilerFactory()));
+ for (auto profiler : m_profilers.values())
+ {
+ profiler->registerSettings(m_settings);
+ }
+
+ // Create the MCEdit thing... why is this here?
+ {
+ m_mcedit.reset(new MCEditTool(m_settings));
+ }
+
+#ifdef Q_OS_MACOS
+ connect(this, &Application::clickedOnDock, [this]() { this->showMainWindow(); });
+#endif
+
+ connect(this,
+ &Application::aboutToQuit,
+ [this]()
+ {
+ if (m_instances)
+ {
+ // save any remaining instance state
+ m_instances->saveNow();
+ }
+ if (logFile)
+ {
+ logFile->flush();
+ logFile->close();
+ }
+ });
+
+ updateCapabilities();
+
+ detectLibraries();
+
+ // check update locks
+ {
+ auto update_log_path = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log");
+
+ auto update_lock = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"));
+ if (update_lock.exists())
+ {
+ auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock.absoluteFilePath());
+ auto infoMsg = tr("This installation has a update lock file present at: %1\n"
+ "\n"
+ "Timestamp: %2\n"
+ "Updating from version %3 to %4\n"
+ "Target install path: %5\n"
+ "Data Path: %6"
+ "\n"
+ "This likely means that a update attempt failed. Please ensure your installation is in "
+ "working order before "
+ "proceeding.\n"
+ "Check the Prism Launcher updater log at: \n"
+ "%7\n"
+ "for details on the last update attempt.\n"
+ "\n"
+ "To delete this lock and proceed select \"Ignore\" below.")
+ .arg(update_lock.absoluteFilePath())
+ .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path)
+ .arg(update_log_path);
+ auto msgBox = QMessageBox(QMessageBox::Warning,
+ tr("Update In Progress"),
+ infoMsg,
+ QMessageBox::Ignore | QMessageBox::Abort);
+ msgBox.setDefaultButton(QMessageBox::Abort);
+ msgBox.setModal(true);
+ msgBox.setDetailedText(FS::read(update_log_path));
+ msgBox.setMinimumWidth(460);
+ msgBox.adjustSize();
+ auto res = msgBox.exec();
+ switch (res)
+ {
+ case QMessageBox::Ignore:
+ {
+ FS::deletePath(update_lock.absoluteFilePath());
+ break;
+ }
+ case QMessageBox::Abort: [[fallthrough]];
+ default:
+ {
+ qDebug() << "Exiting because update lockfile is present";
+ QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection);
+ return;
+ }
+ }
+ }
+
+ auto update_fail_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.fail"));
+ if (update_fail_marker.exists())
+ {
+ auto infoMsg = tr("An update attempt failed\n"
+ "\n"
+ "Please ensure your installation is in working order before "
+ "proceeding.\n"
+ "Check the Prism Launcher updater log at: \n"
+ "%1\n"
+ "for details on the last update attempt.")
+ .arg(update_log_path);
+ auto msgBox = QMessageBox(QMessageBox::Warning,
+ tr("Update Failed"),
+ infoMsg,
+ QMessageBox::Ignore | QMessageBox::Abort);
+ msgBox.setDefaultButton(QMessageBox::Abort);
+ msgBox.setModal(true);
+ msgBox.setDetailedText(FS::read(update_log_path));
+ msgBox.setMinimumWidth(460);
+ msgBox.adjustSize();
+ auto res = msgBox.exec();
+ switch (res)
+ {
+ case QMessageBox::Ignore:
+ {
+ FS::deletePath(update_fail_marker.absoluteFilePath());
+ break;
+ }
+ case QMessageBox::Abort: [[fallthrough]];
+ default:
+ {
+ qDebug() << "Exiting because update lockfile is present";
+ QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection);
+ return;
+ }
+ }
+ }
+
+ auto update_success_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.success"));
+ if (update_success_marker.exists())
+ {
+ auto infoMsg = tr("Update succeeded\n"
+ "\n"
+ "You are now running %1 .\n"
+ "Check the Prism Launcher updater log at: \n"
+ "%2\n"
+ "for details.")
+ .arg(BuildConfig.printableVersionString())
+ .arg(update_log_path);
+ auto msgBox = new QMessageBox(QMessageBox::Information, tr("Update Succeeded"), infoMsg, QMessageBox::Ok);
+ msgBox->setDefaultButton(QMessageBox::Ok);
+ msgBox->setDetailedText(FS::read(update_log_path));
+ msgBox->setAttribute(Qt::WA_DeleteOnClose);
+ msgBox->setMinimumWidth(460);
+ msgBox->adjustSize();
+ msgBox->open();
+ FS::deletePath(update_success_marker.absoluteFilePath());
+ }
+ }
+
+ // notify user if /tmp is mounted with `noexec` (#1693)
+ QString jvmArgs = m_settings->get("JvmArgs").toString();
+ if (jvmArgs.indexOf("java.io.tmpdir") == -1)
+ { /* java.io.tmpdir is a valid workaround, so don't annoy */
+ bool is_tmp_noexec = false;
+
+#if defined(Q_OS_LINUX)
+
+ struct statvfs tmp_stat;
+ statvfs("/tmp", &tmp_stat);
+ is_tmp_noexec = tmp_stat.f_flag & ST_NOEXEC;
+
+#elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+
+ struct statfs tmp_stat;
+ statfs("/tmp", &tmp_stat);
+ is_tmp_noexec = tmp_stat.f_flags & MNT_NOEXEC;
+
+#endif
+
+ if (is_tmp_noexec)
+ {
+ auto infoMsg = tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n"
+ "Some versions of Minecraft may not launch.\n"
+ "\n"
+ "You may solve this issue by remounting /tmp as 'exec' or setting "
+ "the java.io.tmpdir JVM argument to a writeable directory in a "
+ "filesystem where the 'exec' flag is set (e.g., /home/user/.local/tmp)\n");
+ auto msgBox = new QMessageBox(QMessageBox::Information,
+ tr("Incompatible system configuration"),
+ infoMsg,
+ QMessageBox::Ok);
+ msgBox->setDefaultButton(QMessageBox::Ok);
+ msgBox->setAttribute(Qt::WA_DeleteOnClose);
+ msgBox->setMinimumWidth(460);
+ msgBox->adjustSize();
+ msgBox->open();
+ }
+ }
+
+ if (createSetupWizard())
+ {
+ return;
+ }
+
+ m_themeManager->applyCurrentlySelectedTheme(true);
+ performMainStartupAction();
+}
+
+bool Application::createSetupWizard()
+{
+ bool javaRequired = [this]()
+ {
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool())
+ {
+ return false;
+ }
+ bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool();
+ if (ignoreJavaWizard)
+ {
+ return false;
+ }
+ QString currentHostName = QHostInfo::localHostName();
+ QString oldHostName = settings()->get("LastHostname").toString();
+ if (currentHostName != oldHostName)
+ {
+ settings()->set("LastHostname", currentHostName);
+ return true;
+ }
+ QString currentJavaPath = settings()->get("JavaPath").toString();
+ QString actualPath = FS::ResolveExecutable(currentJavaPath);
+ return actualPath.isNull();
+ }();
+ bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired
+ && !settings()->get("AutomaticJavaDownload").toBool()
+ && !settings()->get("AutomaticJavaSwitch").toBool()
+ && !settings()->get("UserAskedAboutAutomaticJavaDownload").toBool();
+ bool languageRequired = settings()->get("Language").toString().isEmpty();
+ bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
+ bool searchEngineRequired = settings()->get("HubSearchEngine").toString().isEmpty();
+ bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString());
+ bool validIcons = m_themeManager->isValidIconTheme(settings()->get("IconTheme").toString());
+ bool login = !m_accounts->anyAccountIsValid() && capabilities() & Application::SupportsMSA;
+ bool themeInterventionRequired = !validWidgets || !validIcons;
+ bool wizardRequired =
+ javaRequired || languageRequired || pasteInterventionRequired || searchEngineRequired || themeInterventionRequired
+ || askjava || login;
+ if (wizardRequired)
+ {
+ // set default theme after going into theme wizard
+ if (!validIcons)
+ settings()->set("IconTheme", QString("pe_colored"));
+ if (!validWidgets)
+ {
+#if defined(Q_OS_WIN32) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ const QString style = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark
+ ? QStringLiteral("dark")
+ : QStringLiteral("bright");
+#else
+ const QString style = QStringLiteral("system");
+#endif
+
+ settings()->set("ApplicationTheme", style);
+ }
+
+ m_themeManager->applyCurrentlySelectedTheme(true);
+
+ m_setupWizard = new SetupWizard(nullptr);
+ if (languageRequired)
+ {
+ m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard));
+ }
+
+ if (javaRequired)
+ {
+ m_setupWizard->addPage(new JavaWizardPage(m_setupWizard));
+ }
+ else if (askjava)
+ {
+ m_setupWizard->addPage(new AutoJavaWizardPage(m_setupWizard));
+ }
+
+ if (pasteInterventionRequired)
+ {
+ m_setupWizard->addPage(new PasteWizardPage(m_setupWizard));
+ }
+
+ if (searchEngineRequired)
+ {
+ m_setupWizard->addPage(new SearchWizardPage(m_setupWizard));
+ }
+
+ if (themeInterventionRequired)
+ {
+ m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard));
+ }
+
+ if (login)
+ {
+ m_setupWizard->addPage(new LoginWizardPage(m_setupWizard));
+ }
+ connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished);
+ m_setupWizard->show();
+ }
+
+ return wizardRequired || login;
+}
+
+bool Application::updaterEnabled()
+{
+#if defined(Q_OS_MAC)
+ return BuildConfig.UPDATER_ENABLED;
+#else
+ return BuildConfig.UPDATER_ENABLED && QFileInfo(FS::PathCombine(m_rootPath, updaterBinaryName())).isFile();
+#endif
+}
+
+QString Application::updaterBinaryName()
+{
+ auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
+#if defined Q_OS_WIN32
+ exe_name.append(".exe");
+#else
+ exe_name.prepend("bin/");
+#endif
+ return exe_name;
+}
+
+bool Application::event(QEvent* event)
+{
+#ifdef Q_OS_MACOS
+ if (event->type() == QEvent::ApplicationStateChange)
+ {
+ auto ev = static_cast<QApplicationStateChangeEvent*>(event);
+
+ if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive)
+ {
+ emit clickedOnDock();
+ }
+ m_prevAppState = ev->applicationState();
+ }
+#endif
+
+ if (event->type() == QEvent::FileOpen)
+ {
+ if (!m_mainWindow)
+ {
+ showMainWindow(false);
+ }
+ auto ev = static_cast<QFileOpenEvent*>(event);
+ m_mainWindow->processURLs({ ev->url() });
+ }
+
+ return QApplication::event(event);
+}
+
+void Application::setupWizardFinished(int status)
+{
+ qDebug() << "Wizard result =" << status;
+ performMainStartupAction();
+}
+
+void Application::performMainStartupAction()
+{
+ m_status = Application::Initialized;
+ if (!m_instanceIdToLaunch.isEmpty())
+ {
+ auto inst = instances()->getInstanceById(m_instanceIdToLaunch);
+ if (inst)
+ {
+ MinecraftTarget::Ptr targetToJoin = nullptr;
+ MinecraftAccountPtr accountToUse = nullptr;
+
+ qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching";
+ if (!m_serverToJoin.isEmpty())
+ {
+ auto parsedTarget = MinecraftTarget::parse(m_serverToJoin, false);
+ if (!parsedTarget.isValid())
+ {
+ qWarning() << "Invalid server address:" << m_serverToJoin;
+ }
+ else
+ {
+ targetToJoin.reset(new MinecraftTarget(parsedTarget));
+ qDebug() << " Launching with server" << m_serverToJoin;
+ }
+ }
+ else if (!m_worldToJoin.isEmpty())
+ {
+ targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_worldToJoin, true)));
+ qDebug() << " Launching with world" << m_worldToJoin;
+ }
+
+ if (!m_profileToUse.isEmpty())
+ {
+ accountToUse = accounts()->getAccountByProfileName(m_profileToUse);
+ if (!accountToUse)
+ {
+ return;
+ }
+ qDebug() << " Launching with account" << m_profileToUse;
+ }
+
+ launch(inst, m_offline ? LaunchMode::Offline : LaunchMode::Normal, targetToJoin, accountToUse, m_offlineName);
+ return;
+ }
+ }
+ if (!m_instanceIdToShowWindowOf.isEmpty())
+ {
+ auto inst = instances()->getInstanceById(m_instanceIdToShowWindowOf);
+ if (inst)
+ {
+ qDebug() << "<> Showing window of instance " << m_instanceIdToShowWindowOf;
+ showInstanceWindow(inst);
+ return;
+ }
+ }
+ if (!m_mainWindow)
+ {
+ // normal main window
+ showMainWindow(false);
+ qDebug() << "<> Main window shown.";
+ }
+
+ // initialize the updater
+ if (updaterEnabled())
+ {
+ qDebug() << "Initializing updater";
+#ifdef Q_OS_MAC
+#if defined(SPARKLE_ENABLED)
+ m_updater.reset(new MacSparkleUpdater());
+#endif
+#else
+ m_updater.reset(new ProjTExternalUpdater(m_mainWindow, m_rootPath, m_dataPath));
+#endif
+ qDebug() << "<> Updater started.";
+ }
+
+ { // delete instances tmp dirctory
+ auto instDir = m_settings->get("InstanceDir").toString();
+ const QString tempRoot = FS::PathCombine(instDir, ".tmp");
+ FS::deletePath(tempRoot);
+ }
+
+ if (!m_urlsToImport.isEmpty())
+ {
+ qDebug() << "<> Importing from url:" << m_urlsToImport;
+ m_mainWindow->processURLs(m_urlsToImport);
+ }
+}
+
+void Application::showFatalErrorMessage(const QString& title, const QString& content)
+{
+ m_status = Application::Failed;
+ auto dialog = CustomMessageBox::selectable(nullptr, title, content, QMessageBox::Critical);
+ dialog->exec();
+}
+
+Application::~Application()
+{
+ // Shut down logger by setting the logger function to nothing
+ qInstallMessageHandler(nullptr);
+
+#if defined Q_OS_WIN32
+ // Detach from Windows console
+ if (consoleAttached)
+ {
+ fclose(stdout);
+ fclose(stdin);
+ fclose(stderr);
+ FreeConsole();
+ }
+#endif
+}
+
+void Application::messageReceived(const QByteArray& message)
+{
+ ApplicationMessage received;
+ received.parse(message);
+
+ auto& command = received.command;
+
+ if (status() != Initialized)
+ {
+ bool isLoginAtempt = false;
+ if (command == "import")
+ {
+ QString url = received.args["url"];
+ isLoginAtempt = !url.isEmpty() && normalizeImportUrl(url).scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME;
+ }
+ if (!isLoginAtempt)
+ {
+ qDebug() << "Received message" << message << "while still initializing. It will be ignored.";
+ return;
+ }
+ }
+
+ if (command == "activate")
+ {
+ showMainWindow();
+ }
+ else if (command == "import")
+ {
+ QString url = received.args["url"];
+ if (url.isEmpty())
+ {
+ qWarning() << "Received" << command << "message without a zip path/URL.";
+ return;
+ }
+ if (!m_mainWindow)
+ {
+ showMainWindow(false);
+ }
+ m_mainWindow->processURLs({ normalizeImportUrl(url) });
+ }
+ else if (command == "launch")
+ {
+ QString id = received.args["id"];
+ QString server = received.args["server"];
+ QString world = received.args["world"];
+ QString profile = received.args["profile"];
+ bool offline = received.args["offline_enabled"] == "true";
+ QString offlineName = received.args["offline_name"];
+
+ InstancePtr instance;
+ if (!id.isEmpty())
+ {
+ instance = instances()->getInstanceById(id);
+ if (!instance)
+ {
+ qWarning() << "Launch command requires an valid instance ID. " << id << "resolves to nothing.";
+ return;
+ }
+ }
+ else
+ {
+ qWarning() << "Launch command called without an instance ID...";
+ return;
+ }
+
+ MinecraftTarget::Ptr serverObject = nullptr;
+ if (!server.isEmpty())
+ {
+ serverObject = std::make_shared<MinecraftTarget>(MinecraftTarget::parse(server, false));
+ }
+ else if (!world.isEmpty())
+ {
+ serverObject = std::make_shared<MinecraftTarget>(MinecraftTarget::parse(world, true));
+ }
+ MinecraftAccountPtr accountObject;
+ if (!profile.isEmpty())
+ {
+ accountObject = accounts()->getAccountByProfileName(profile);
+ if (!accountObject)
+ {
+ qWarning() << "Launch command requires the specified profile to be valid. " << profile
+ << "does not resolve to any account.";
+ return;
+ }
+ }
+
+ launch(instance, offline ? LaunchMode::Offline : LaunchMode::Normal, serverObject, accountObject, offlineName);
+ }
+ else
+ {
+ qWarning() << "Received invalid message" << message;
+ }
+}
+
+std::shared_ptr<TranslationsModel> Application::translations()
+{
+ return m_translations;
+}
+
+std::shared_ptr<projt::java::RuntimeCatalog> Application::runtimeCatalog()
+{
+ if (!m_runtimeCatalog)
+ {
+ m_runtimeCatalog.reset(new projt::java::RuntimeCatalog());
+ }
+ return m_runtimeCatalog;
+}
+
+QIcon Application::logo()
+{
+ return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME);
+}
+
+bool Application::openJsonEditor(const QString& filename)
+{
+ const QString file = QDir::current().absoluteFilePath(filename);
+ if (m_settings->get("JsonEditor").toString().isEmpty())
+ {
+ return DesktopServices::openUrl(QUrl::fromLocalFile(file));
+ }
+ else
+ {
+ // return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file);
+ return DesktopServices::run(m_settings->get("JsonEditor").toString(), { file });
+ }
+}
+
+bool Application::launch(InstancePtr instance,
+ LaunchMode mode,
+ MinecraftTarget::Ptr targetToJoin,
+ MinecraftAccountPtr accountToUse,
+ const QString& offlineName)
+{
+ if (m_updateRunning)
+ {
+ qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed.";
+ }
+ else if (instance->canLaunch())
+ {
+ // Auto-backup before launch if enabled
+ if (settings()->get("AutoBackupBeforeLaunch").toBool())
+ {
+ qDebug() << "Creating auto-backup before launch...";
+
+ QProgressDialog* progress =
+ new QProgressDialog("Creating backup before launch...", QString(), 0, 0, m_mainWindow);
+ progress->setWindowModality(Qt::WindowModal);
+ progress->setMinimumDuration(0);
+ progress->setValue(0);
+ progress->setCancelButton(nullptr);
+ progress->show();
+ QApplication::processEvents();
+
+ BackupManager* backupManager = new BackupManager(this);
+ connect(backupManager,
+ &BackupManager::backupCreated,
+ this,
+ [this, instance, mode, offlineName, progress, backupManager](const QString& instanceId,
+ const QString& backupName)
+ {
+ if (instanceId == instance->id())
+ {
+ qDebug() << "Auto-backup before launch completed.";
+ progress->close();
+ progress->deleteLater();
+ backupManager->deleteLater();
+ continueLaunchAfterBackup(instanceId, mode, offlineName);
+ }
+ });
+ connect(backupManager,
+ &BackupManager::backupFailed,
+ this,
+ [progress, backupManager](const QString&, const QString& error)
+ {
+ qWarning() << "Auto-backup before launch failed:" << error;
+ progress->close();
+ progress->deleteLater();
+ backupManager->deleteLater();
+ });
+ BackupOptions options;
+ options.includeSaves = true;
+ options.includeConfig = true;
+ options.includeOptions = true;
+ options.includeMods = false;
+ backupManager->createBackupAsync(instance, "auto-backup-pre-launch", options);
+ // launch işlemini backup tamamlanınca başlatıyoruz, burada return ile çıkıyoruz
+ return true;
+ }
+ continueLaunchAfterBackup(instance->id(), mode, offlineName);
+ return true;
+ }
+ return false;
+}
+
+void Application::continueLaunchAfterBackup(QString instanceId, LaunchMode mode, QString offlineName)
+{
+ InstancePtr instance = instances()->getInstanceById(instanceId);
+ InstanceWindow* window = nullptr;
+
+ if (instance->settings()->get("ShowConsole").toBool())
+ {
+ window = showInstanceWindow(instance);
+ }
+
+ // Get window from instance if not already set
+ if (!window)
+ {
+ window = instance->getInstanceWindow();
+ }
+ if (window)
+ {
+ if (!window->saveAll())
+ {
+ return;
+ }
+ }
+
+ // Create and configure launch controller
+ auto controller = makeShared<LaunchController>();
+ controller->setInstance(instance);
+ controller->setLaunchMode(mode);
+ controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get());
+ controller->setOfflineName(offlineName);
+ if (window)
+ {
+ controller->setParentWidget(window);
+ }
+ else if (m_mainWindow)
+ {
+ controller->setParentWidget(m_mainWindow);
+ }
+ connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded);
+ connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
+ connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); });
+
+ // Store controller in instance
+ instance->setLaunchController(controller);
+ controller->executeTask();
+}
+
+bool Application::kill(InstancePtr instance)
+{
+ if (!instance->isRunning())
+ {
+ qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running.";
+ return false;
+ }
+
+ // Get controller from instance
+ auto controller = instance->getLaunchController();
+ if (controller)
+ {
+ return controller->abort();
+ }
+ return true;
+}
+
+void Application::closeCurrentWindow()
+{
+ if (focusWindow())
+ focusWindow()->close();
+}
+
+void Application::addRunningInstance()
+{
+ m_runningInstances++;
+ if (m_runningInstances == 1)
+ {
+ emit updateAllowedChanged(false);
+ }
+}
+
+void Application::subRunningInstance()
+{
+ if (m_runningInstances == 0)
+ {
+ qCritical() << "Something went really wrong and we now have less than 0 running instances... WTF";
+ return;
+ }
+ m_runningInstances--;
+ if (m_runningInstances == 0)
+ {
+ emit updateAllowedChanged(true);
+ }
+}
+
+bool Application::shouldExitNow() const
+{
+ return m_runningInstances == 0 && m_openWindows == 0;
+}
+
+bool Application::updatesAreAllowed()
+{
+ return m_runningInstances == 0;
+}
+
+void Application::updateIsRunning(bool running)
+{
+ m_updateRunning = running;
+}
+
+void Application::controllerSucceeded()
+{
+ auto controller = qobject_cast<LaunchController*>(sender());
+ if (!controller)
+ return;
+ auto instance = controller->instance();
+
+ // on success, do...
+ if (instance->settings()->get("AutoCloseConsole").toBool())
+ {
+ auto window = instance->getInstanceWindow();
+ if (window)
+ {
+ QMetaObject::invokeMethod(window, &QWidget::close, Qt::QueuedConnection);
+ }
+ }
+
+ // Clear controller from instance
+ instance->setLaunchController(nullptr);
+ subRunningInstance();
+
+ // quit when there are no more windows.
+ if (shouldExitNow())
+ {
+ m_status = Status::Succeeded;
+ exit(0);
+ }
+}
+
+void Application::controllerFailed(const QString& error)
+{
+ Q_UNUSED(error);
+ auto controller = qobject_cast<LaunchController*>(sender());
+ if (!controller)
+ return;
+ auto instance = controller->instance();
+
+ // on failure, do... nothing
+ // Clear controller from instance
+ instance->setLaunchController(nullptr);
+ subRunningInstance();
+
+ // quit when there are no more windows.
+ if (shouldExitNow())
+ {
+ m_status = Status::Failed;
+ exit(1);
+ }
+}
+
+void Application::ShowGlobalSettings(class QWidget* parent, QString open_page)
+{
+ if (!m_globalSettingsProvider)
+ {
+ return;
+ }
+ emit globalSettingsAboutToOpen();
+ {
+ SettingsObject::Lock lock(APPLICATION->settings());
+ PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent);
+ connect(&dlg, &PageDialog::applied, this, &Application::globalSettingsApplied);
+ dlg.exec();
+ }
+}
+
+MainWindow* Application::showMainWindow(bool minimized)
+{
+ if (m_mainWindow)
+ {
+ m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized);
+ m_mainWindow->raise();
+ m_mainWindow->activateWindow();
+ }
+ else
+ {
+ m_mainWindow = new MainWindow();
+ m_mainWindow->restoreState(
+ QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toString().toUtf8()));
+ m_mainWindow->restoreGeometry(
+ QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toString().toUtf8()));
+
+ if (minimized)
+ {
+ m_mainWindow->showMinimized();
+ }
+ else
+ {
+ m_mainWindow->show();
+ }
+
+ m_mainWindow->checkInstancePathForProblems();
+ connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged);
+ connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose);
+ m_openWindows++;
+ }
+ return m_mainWindow;
+}
+
+ViewLogWindow* Application::showLogWindow()
+{
+ if (m_viewLogWindow)
+ {
+ m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized);
+ m_viewLogWindow->raise();
+ m_viewLogWindow->activateWindow();
+ }
+ else
+ {
+ m_viewLogWindow = new ViewLogWindow();
+ connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose);
+ m_openWindows++;
+ }
+ return m_viewLogWindow;
+}
+
+InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page)
+{
+ if (!instance)
+ return nullptr;
+
+ auto window = instance->getInstanceWindow();
+
+ if (window)
+ {
+// If the window is minimized on macOS or Windows, activate and bring it up
+#ifdef Q_OS_MACOS
+ if (window->isMinimized())
+ {
+ window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
+ }
+#elif defined(Q_OS_WIN)
+ if (window->isMinimized())
+ {
+ window->showNormal();
+ }
+#endif
+
+ window->raise();
+ window->activateWindow();
+ }
+ else
+ {
+ window = new InstanceWindow(instance);
+ instance->setInstanceWindow(window);
+ m_openWindows++;
+ connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose);
+ }
+
+ if (!page.isEmpty())
+ {
+ window->selectPage(page);
+ }
+
+ // Update controller parent if exists
+ auto controller = instance->getLaunchController();
+ if (controller)
+ {
+ controller->setParentWidget(window);
+ }
+ return window;
+}
+
+void Application::on_windowClose()
+{
+ m_openWindows--;
+ auto instWindow = qobject_cast<InstanceWindow*>(sender());
+ if (instWindow)
+ {
+ // Get instance and clear window reference
+ auto instance = instances()->getInstanceById(instWindow->instanceId());
+ if (instance)
+ {
+ instance->setInstanceWindow(nullptr);
+
+ // Update controller parent if exists
+ auto controller = instance->getLaunchController();
+ if (controller)
+ {
+ controller->setParentWidget(m_mainWindow);
+ }
+ }
+ }
+ auto mainWindow = qobject_cast<MainWindow*>(sender());
+ if (mainWindow)
+ {
+ m_mainWindow = nullptr;
+ }
+ auto logWindow = qobject_cast<ViewLogWindow*>(sender());
+ if (logWindow)
+ {
+ m_viewLogWindow = nullptr;
+ }
+ // quit when there are no more windows.
+ if (shouldExitNow())
+ {
+ exit(0);
+ }
+}
+
+void Application::updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password)
+{
+ // Set the application proxy settings.
+ if (proxyTypeStr == "SOCKS5")
+ {
+ QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, addr, port, user, password));
+ }
+ else if (proxyTypeStr == "HTTP")
+ {
+ QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, addr, port, user, password));
+ }
+ else if (proxyTypeStr == "None")
+ {
+ // If we have no proxy set, set no proxy and return.
+ QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::NoProxy));
+ }
+ else
+ {
+ // If we have "Default" selected, set Qt to use the system proxy settings.
+ QNetworkProxyFactory::setUseSystemConfiguration(true);
+ }
+
+ qDebug() << "Detecting proxy settings...";
+ QNetworkProxy proxy = QNetworkProxy::applicationProxy();
+ m_network->setProxy(proxy);
+
+ QString proxyDesc;
+ if (proxy.type() == QNetworkProxy::NoProxy)
+ {
+ qDebug() << "Using no proxy is an option!";
+ return;
+ }
+ switch (proxy.type())
+ {
+ case QNetworkProxy::DefaultProxy: proxyDesc = "Default proxy: "; break;
+ case QNetworkProxy::Socks5Proxy: proxyDesc = "Socks5 proxy: "; break;
+ case QNetworkProxy::HttpProxy: proxyDesc = "HTTP proxy: "; break;
+ case QNetworkProxy::HttpCachingProxy: proxyDesc = "HTTP caching: "; break;
+ case QNetworkProxy::FtpCachingProxy: proxyDesc = "FTP caching: "; break;
+ default: proxyDesc = "DERP proxy: "; break;
+ }
+ proxyDesc += QString("%1:%2").arg(proxy.hostName()).arg(proxy.port());
+ qDebug() << proxyDesc;
+}
+
+shared_qobject_ptr<HttpMetaCache> Application::metacache()
+{
+ return m_metacache;
+}
+
+shared_qobject_ptr<QNetworkAccessManager> Application::network()
+{
+ return m_network;
+}
+
+shared_qobject_ptr<projt::meta::MetaIndex> Application::metadataIndex()
+{
+ if (!m_metadataIndex)
+ {
+ m_metadataIndex.reset(new projt::meta::MetaIndex());
+ }
+ return m_metadataIndex;
+}
+
+void Application::updateCapabilities()
+{
+ m_capabilities = None;
+ if (!getMSAClientID().isEmpty())
+ m_capabilities |= SupportsMSA;
+ if (!getFlameAPIKey().isEmpty())
+ m_capabilities |= SupportsFlame;
+
+#ifdef Q_OS_LINUX
+ if (gamemode_query_status() >= 0)
+ m_capabilities |= SupportsGameMode;
+
+ if (!MangoHud::getLibraryString().isEmpty())
+ m_capabilities |= SupportsMangoHud;
+#endif
+}
+
+void Application::detectLibraries()
+{
+#ifdef Q_OS_LINUX
+ m_detectedGLFWPath = MangoHud::findLibrary(BuildConfig.GLFW_LIBRARY_NAME);
+ m_detectedOpenALPath = MangoHud::findLibrary(BuildConfig.OPENAL_LIBRARY_NAME);
+ qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath;
+#endif
+}
+
+QString Application::getJarPath(QString jarFile)
+{
+ QStringList potentialPaths = {
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
+#endif
+ FS::PathCombine(m_rootPath, "jars"),
+ FS::PathCombine(applicationDirPath(), "jars"),
+ FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging
+ };
+ for (QString p : potentialPaths)
+ {
+ QString jarPath = FS::PathCombine(p, jarFile);
+ if (QFileInfo(jarPath).isFile())
+ return jarPath;
+ }
+ return {};
+}
+
+QString Application::getMSAClientID()
+{
+ QString clientIDOverride = m_settings->get("MSAClientIDOverride").toString();
+ if (!clientIDOverride.isEmpty())
+ {
+ return clientIDOverride;
+ }
+
+ return BuildConfig.MSA_CLIENT_ID;
+}
+
+QString Application::getFlameAPIKey()
+{
+ QString keyOverride = m_settings->get("FlameKeyOverride").toString();
+ if (!keyOverride.isEmpty())
+ {
+ return keyOverride;
+ }
+
+ return BuildConfig.FLAME_API_KEY;
+}
+
+QString Application::getModrinthAPIToken()
+{
+ QString tokenOverride = m_settings->get("ModrinthToken").toString();
+ if (!tokenOverride.isEmpty())
+ return tokenOverride;
+
+ return QString();
+}
+
+QString Application::getUserAgent()
+{
+ QString uaOverride = m_settings->get("UserAgentOverride").toString();
+ if (!uaOverride.isEmpty())
+ {
+ return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString());
+ }
+
+ return BuildConfig.USER_AGENT;
+}
+
+bool Application::handleDataMigration(const QString& currentData,
+ const QString& oldData,
+ const QString& name,
+ const QString& configFile) const
+{
+ QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt");
+ QStringList configPaths = { FS::PathCombine(oldData, configFile),
+ FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) };
+
+ QLocale locale;
+
+ // Is there a valid config at the old location?
+ bool configExists = false;
+ for (QString configPath : configPaths)
+ {
+ configExists |= QFileInfo::exists(configPath);
+ }
+
+ if (!configExists || QFileInfo::exists(nomigratePath))
+ {
+ qDebug() << "<> No migration needed from" << name;
+ return false;
+ }
+
+ QString message;
+ bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE));
+
+ if (currentExists)
+ {
+ message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to "
+ "migrate yourself. Do "
+ "you want to be reminded of the pending data migration next time you start %2?")
+ .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
+ }
+ else
+ {
+ message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?")
+ .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
+
+ QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log"));
+ if (logInfo.exists())
+ {
+ QString lastModified = logInfo.lastModified().toString(locale.dateFormat());
+ message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location "
+ "of %3?")
+ .arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME);
+ }
+ }
+
+ QMessageBox::StandardButton askMoveDialogue = QMessageBox::question(nullptr,
+ BuildConfig.LAUNCHER_DISPLAYNAME,
+ message,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes);
+
+ auto setDoNotMigrate = [&nomigratePath]
+ {
+ QFile file(nomigratePath);
+ if (!file.open(QIODevice::WriteOnly))
+ {
+ qWarning() << "setDoNotMigrate failed; Failed to open file '" << file.fileName() << "' for writing!";
+ }
+ };
+
+ // create no-migrate file if user doesn't want to migrate
+ if (askMoveDialogue != QMessageBox::Yes)
+ {
+ qDebug() << "<> Migration declined for" << name;
+ setDoNotMigrate();
+ return currentExists; // cancel further migrations, if we already have a data directory
+ }
+
+ if (!currentExists)
+ {
+ // Migrate!
+ using namespace Filters;
+
+ QList<Filter> filters;
+ filters.append(equals(configFile));
+ filters.append(equals(BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory
+ // before
+ filters.append(startsWith("logs/"));
+ filters.append(equals("accounts.json"));
+ filters.append(startsWith("accounts/"));
+ filters.append(startsWith("assets/"));
+ filters.append(startsWith("icons/"));
+ filters.append(startsWith("instances/"));
+ filters.append(startsWith("libraries/"));
+ filters.append(startsWith("mods/"));
+ filters.append(startsWith("themes/"));
+
+ ProgressDialog diag;
+ DataMigrationTask task(oldData, currentData, any(std::move(filters)));
+ if (diag.execWithTask(task))
+ {
+ qDebug() << "<> Migration succeeded";
+ setDoNotMigrate();
+ }
+ else
+ {
+ QString reason = task.failReason();
+ QMessageBox::critical(nullptr,
+ BuildConfig.LAUNCHER_DISPLAYNAME,
+ tr("Migration failed! Reason: %1").arg(reason));
+ }
+ }
+ else
+ {
+ qWarning() << "<> Migration was skipped, due to existing data";
+ }
+ return true;
+}
+
+void Application::triggerUpdateCheck()
+{
+ if (m_updater)
+ {
+ qDebug() << "Checking for updates.";
+ m_updater->setBetaAllowed(false); // There are no other channels than stable
+ m_updater->checkForUpdates();
+ }
+ else
+ {
+ qDebug() << "Updater not available.";
+ }
+}
+
+QUrl Application::normalizeImportUrl(QString const& url)
+{
+ auto local_file = QFileInfo(url);
+ if (local_file.exists())
+ {
+ return QUrl::fromLocalFile(local_file.absoluteFilePath());
+ }
+ else
+ {
+ return QUrl::fromUserInput(url);
+ }
+}
+
+const QString Application::javaPath()
+{
+ return m_settings->get("JavaDir").toString();
+}
+
+void Application::addQSavePath(QString path)
+{
+ QMutexLocker locker(&m_qsaveResourcesMutex);
+ m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1;
+}
+
+void Application::removeQSavePath(QString path)
+{
+ QMutexLocker locker(&m_qsaveResourcesMutex);
+ auto count = m_qsaveResources.value(path, 0) - 1;
+ if (count <= 0)
+ {
+ m_qsaveResources.remove(path);
+ }
+ else
+ {
+ m_qsaveResources[path] = count;
+ }
+}
+
+bool Application::checkQSavePath(QString path)
+{
+ QMutexLocker locker(&m_qsaveResourcesMutex);
+ for (auto partialPath : m_qsaveResources.keys())
+ {
+ if (path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+void Application::migratePastebinSettings()
+{
+ m_settings->registerSetting("PastebinURL", "");
+ m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs);
+ m_settings->registerSetting("PastebinCustomAPIBase", "");
+ m_settings->registerSetting("PastebinMigrationDone", false);
+
+ // Skip if migration already completed
+ if (m_settings->get("PastebinMigrationDone").toBool())
+ {
+ return;
+ }
+
+ // Check if legacy URL exists
+ QString pastebinURL = m_settings->get("PastebinURL").toString();
+ if (!pastebinURL.isEmpty())
+ {
+ // Migrate from legacy 0x0.st URL to new format
+ bool userHadDefaultPastebin = pastebinURL == "https://0x0.st";
+ if (!userHadDefaultPastebin)
+ {
+ m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer);
+ m_settings->set("PastebinCustomAPIBase", pastebinURL);
+ }
+ m_settings->reset("PastebinURL");
+ }
+
+ // Validate PastebinType
+ bool ok;
+ int pasteType = m_settings->get("PastebinType").toInt(&ok);
+ if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last))
+ {
+ m_settings->reset("PastebinType");
+ m_settings->reset("PastebinCustomAPIBase");
+ }
+
+ // Mark migration as complete
+ m_settings->set("PastebinMigrationDone", true);
+}