// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * 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, see . * * 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/JavaInstallList.h" #include "net/PasteUpload.h" #include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" #include "ui/ToolTipFilter.h" #include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" #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/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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "InstanceList.h" #include "MTPixmapCache.h" #include #include "icons/IconList.h" #include "net/HttpMetaCache.h" #include "updater/ExternalUpdater.h" #include "tools/JProfiler.h" #include "tools/JVisualVM.h" #include "settings/INISettingsObject.h" #include "settings/Setting.h" #include "meta/Index.h" #include "translations/TranslationsModel.h" #include #include #include #include #include "SysInfo.h" #ifdef Q_OS_LINUX #include #include "LibraryUtils.h" #include "gamemode_client.h" #endif #if defined(Q_OS_LINUX) #include #endif #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #include #include #endif #if defined(Q_OS_MAC) #if defined(SPARKLE_ENABLED) #include "updater/MacSparkleUpdater.h" #endif #else #include "updater/PrismExternalUpdater.h" #endif #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #endif #include "console/Console.h" #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) static const QLatin1String g_liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; static bool isANSIColorConsole; static const QString g_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 const QString g_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 namespace { /** 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 s_loggerMutex; const std::lock_guard lock(s_loggerMutex); // synchronized, QFile logFile is not thread-safe if (isANSIColorConsole) { // ensure default is set for log file qSetMessagePattern(g_defaultLogFormat); } QString out = qFormatLogMessage(type, context, msg); if (APPLICATION->logModel) { APPLICATION->logModel->append(MessageLevel::fromQtMsgType(type), out); } out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); if (isANSIColorConsole) { // format ansi for console; qSetMessagePattern(g_ansiLogFormat); out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; } QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } std::tuple readLockFile(const QString& path) { auto contents = QString(FS::read(path)); auto lines = contents.split('\n'); QDateTime timestamp; QString from; QString to; QString target; QString dataPath; for (const 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") { dataPath = right; } } return std::make_tuple(timestamp, from, to, target, dataPath); } } // namespace Application::Application(int& argc, char** argv) : QApplication(argc, argv) { if (console::isConsole()) { isANSIColorConsole = true; } 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 Application::setQuitOnLastWindowClosed(false); Application::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 '" + g_liveCheckFile + "' file after the launcher starts" }, { "show-window", "Show the main launcher window (useful in combination with --launch)" }, { { "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_launchOffline = true; m_offlineName = parser.value("offline"); } m_liveCheck = parser.isSet("alive"); m_instanceIdToShowWindowOf = parser.value("show"); m_showMainWindow = parser.isSet("show-window"); for (const auto& url : parser.values("import")) { m_urlsToImport.append(normalizeImportUrl(url)); } // treat unspecified positional arguments as import urls for (const 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_launchOffline) && 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 PrismLauncher that uses the same data path. * 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. */ auto appID = ApplicationId::fromPathAndVersion(QDir::currentPath(), BuildConfig.printableVersionString()); { // FIXME: you can run the same binaries with multiple data dirs and they won't clash. This could cause issues for updates. 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 (const 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_launchOffline) { 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; } 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 s_baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; static const QString s_logBase = FS::PathCombine("logs", s_baseLogFile); if (FS::ensureFolderPathExists("logs")) { // if this did not fail for (auto i = 0; i <= 4; i++) { if (auto oldName = s_baseLogFile.arg(i); QFile::exists(oldName)) { // do not pointlessly delete new files if the old ones are not there FS::move(oldName, s_logBase.arg(i)); } } } for (auto i = 4; i > 0; i--) { FS::move(s_logBase.arg(i - 1), s_logBase.arg(i)); } logFile = std::make_unique(s_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 - %1.\n" "\n" "Make sure you have write permissions to the data folder.\n" "(%2)\n" "\n" "The launcher cannot continue until you fix this problem.") .arg(logFile->errorString()) .arg(dataPath)); return; } qInstallMessageHandler(appDebugOutput); qSetMessagePattern(g_defaultLogFormat); logModel = std::make_unique(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 ruleNames = loggingRules.childKeys(); QStringList rules; qInfo() << "Setting log rules:"; for (const auto& ruleName : ruleNames) { auto rule = QString("%1=%2").arg(ruleName).arg(loggingRules.value(ruleName).toString()); rules.append(rule); qInfo() << " " << rule; } auto rulesStr = rules.join("\n"); QLoggingCategory::setFilterRules(rulesStr); } qInfo() << "<> Log initialized."; } { auto migrated = handleDataMigration( dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", "polymc.cfg"); if (!migrated) { handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", "multimc.cfg"); } } { 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.isEmpty()) { 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(g_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" << g_liveCheckFile << "error:" << check.errorString(); check.remove(); // also closes file! } } else { qWarning() << "Could not open" << g_liveCheckFile << "for writing:" << check.errorString(); } } // 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")); // 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().nospace() << "Detected default console font: " << resolvedDefaultMonospace << ", substitutions: " << QFont::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("AdditionalInstanceDirs", QVariant(QStringList())); m_settings->registerSetting("LastUsedInstDirForNewInstance", ""); 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"); #ifdef Q_OS_MACOS // Folder security-scoped bookmarks m_settings->registerSetting("InstanceDirBookmark", ""); m_settings->registerSetting("CentralModsDirBookmark", ""); m_settings->registerSetting("IconsDirBookmark", ""); m_settings->registerSetting("DownloadsDirBookmark", ""); m_settings->registerSetting("SkinsDirBookmark", ""); m_settings->registerSetting("JavaDirBookmark", ""); #endif // 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); m_settings->registerSetting("LowMemWarning", true); // 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", ""); m_settings->registerSetting("UseNativeSDL", false); m_settings->registerSetting("CustomSDLPath", ""); // 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); m_settings->registerSetting("DownloadGameFilesDuringInstanceCreation", true); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); // Wrapper command for launch m_settings->registerSetting("WrapperCommand", ""); // Custom Commands m_settings->registerSetting({ "PreLoadCommand", "PreLoadCmd" }, ""); m_settings->registerSetting({ "PreLaunchCommand", "PreLaunchCmd" }, ""); m_settings->registerSetting({ "PostExitCommand", "PostExitCmd" }, ""); // The cat m_settings->registerSetting("EnableCat", true); 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("EditInstanceOnDoubleClick", false); 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("NewsGeometry", ""); m_settings->registerSetting("ModDownloadGeometry", ""); m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); m_settings->registerSetting("DataPackDownloadGeometry", ""); // data pack window // in future, more pages may be added - so this name is chosen to avoid needing migration m_settings->registerSetting("WorldManagementGeometry", ""); // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); m_settings->registerSetting("PastebinCustomAPIBase", ""); QString pastebinURL = m_settings->get("PastebinURL").toString(); bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; if (!pastebinURL.isEmpty() && !userHadDefaultPastebin) { m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); m_settings->set("PastebinCustomAPIBase", pastebinURL); m_settings->reset("PastebinURL"); } bool ok = false; int pasteType = m_settings->get("PastebinType").toInt(&ok); // If PastebinType is invalid then reset the related settings. if (!ok || pasteType < PasteUpload::PasteType::First || pasteType > PasteUpload::PasteType::Last) { m_settings->reset("PastebinType"); m_settings->reset("PastebinCustomAPIBase"); } } { auto resetIfInvalid = [this](const Setting* setting) { if (const auto value = setting->get().toString(); !value.isEmpty()) { if (const QUrl url(value); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https")) { m_settings->reset(setting->id()); } } }; // Meta URL resetIfInvalid(m_settings->registerSetting("MetaURLOverride", "").get()); // Resource URL resetIfInvalid(m_settings->registerSetting({ "ResourceURLOverride", "ResourceURL" }, "").get()); // Legacy FML libs URL resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get()); } m_settings->registerSetting("MetaRefreshOnLaunch", true); 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({ "FlameKeyOverride", "CFKeyOverride" }, ""); m_settings->registerSetting("FallbackMRBlockedMods", true); m_settings->registerSetting("ModrinthToken", ""); 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_unique(tr("Settings")); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); qInfo() << "<> Settings loaded."; } // Initialize playtime settings, stored separately so this data can be synced // independently of machine-specific configuration { m_playtimeSettings.reset(new INISettingsObject(QString("playtime.cfg"), this)); m_playtimeSettings->registerSetting("TotalPlayTime", 0); m_playtimeSettings->registerSetting("TotalPlayTimeMigrated", false); } #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(); QString user = settings()->get("ProxyUser").toString(); QString pass = settings()->get("ProxyPass").toString(); updateProxySettings(proxyTypeStr, addr, port, user, pass); qInfo() << "<> Network done."; } // 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 IconList(instFolders, setting->get().toString())); connect(setting.get(), &Setting::SettingChanged, this, [this](const Setting&, const QVariant& value) { m_icons->directoryChanged(value.toString()); }); qInfo() << "<> Instance icons initialized."; } // Themes m_themeManager = std::make_unique(); #ifdef Q_OS_MACOS // for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present // this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory, // and a sandboxed version that can't access the directory without a bookmark // this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore { m_settings->get("InstanceDir"); m_settings->get("CentralModsDir"); m_settings->get("IconsDir"); m_settings->get("DownloadsDir"); m_settings->get("SkinsDir"); m_settings->get("JavaDir"); } #endif // initialize and load all instances { auto instDirSetting = m_settings->getSetting("InstanceDir"); auto additionalInstanceDirsSetting = m_settings->getSetting("AdditionalInstanceDirs"); // 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 = m_settings->get("InstanceDir").toString(); qInfo() << "Instance path :" << instDir; if (FS::checkProblemticPathJava(QDir(instDir))) { qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; } QStringList additionalDirs = m_settings->get("AdditionalInstanceDirs").toStringList(); QStringList allInstDirs; allInstDirs << instDir; for (const auto& dir : additionalDirs) { if (!dir.isEmpty() && !allInstDirs.contains(dir)) { allInstDirs << dir; } } m_instances.reset(new InstanceList(m_settings.get(), allInstDirs, this)); connect(instDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); connect(additionalInstanceDirsSetting.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->addBase("feed", QDir("cache/feed").absolutePath()); m_metacache->Load(); qInfo() << "<> Cache initialized."; } // load translations { m_translations.reset(new TranslationsModel("translations")); m_translations->downloadIndex(); qInfo() << "Your language is" << m_translations->selectedLanguage(); qInfo() << "<> Translations loaded."; } // FIXME: what to do with these? m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory())); for (const auto& profiler : m_profilers.values()) { profiler->registerSettings(m_settings.get()); } #ifdef Q_OS_MACOS connect(this, &Application::clickedOnDock, this, [this]() { this->showMainWindow(); }); #endif connect(this, &Application::aboutToQuit, this, [this]() { if (m_instances) { // save any remaining instance state m_instances->saveNow(); } if (logFile) { logFile->flush(); logFile->close(); } }); updateCapabilities(); detectLibraries(); // check update locks { auto updateLogPath = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); auto updateLock = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.lock")); if (updateLock.exists()) { auto [timestamp, from, to, target, lockDataPath] = readLockFile(updateLock.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(updateLock.absoluteFilePath()) .arg(timestamp.toString(Qt::ISODate), from, to, target, lockDataPath) .arg(updateLogPath); 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(updateLogPath)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); auto res = msgBox.exec(); switch (res) { case QMessageBox::Ignore: { FS::deletePath(updateLock.absoluteFilePath()); break; } case QMessageBox::Abort: [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } } auto updateFailMarker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.fail")); if (updateFailMarker.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(updateLogPath); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Failed"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort); msgBox.setDefaultButton(QMessageBox::Abort); msgBox.setModal(true); msgBox.setDetailedText(FS::read(updateLogPath)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); auto res = msgBox.exec(); switch (res) { case QMessageBox::Ignore: { FS::deletePath(updateFailMarker.absoluteFilePath()); break; } case QMessageBox::Abort: [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } } auto updateSuccessMarker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.success")); if (updateSuccessMarker.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(updateLogPath); auto* msgBox = new QMessageBox(QMessageBox::Information, tr("Update Succeeded"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setDetailedText(FS::read(updateLogPath)); msgBox->setAttribute(Qt::WA_DeleteOnClose); msgBox->setMinimumWidth(460); msgBox->adjustSize(); msgBox->open(); FS::deletePath(updateSuccessMarker.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 isTmpNoexec = false; #if defined(Q_OS_LINUX) struct statvfs tmpStat{}; statvfs("/tmp", &tmpStat); isTmpNoexec = ((tmpStat.f_flag & ST_NOEXEC) != 0U); #elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) struct statfs tmp_stat; statfs("/tmp", &tmp_stat); isTmpNoexec = ((tmp_stat.f_flags & MNT_NOEXEC) != 0U); #endif if (isTmpNoexec) { 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 (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { installEventFilter(new ToolTipFilter); } 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 validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); bool validIcons = m_themeManager->isValidIconTheme(settings()->get("IconTheme").toString()); bool login = !m_accounts->anyAccountIsValid() && (capabilities().testAnyFlags(Application::SupportsMSA)); bool themeInterventionRequired = !validWidgets || !validIcons; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || 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) 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 (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 exeName = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exeName.append(".exe"); #else exeName.prepend("bin/"); #endif return exeName; } bool Application::event(QEvent* event) { #ifdef Q_OS_MACOS if (event->type() == QEvent::ApplicationStateChange) { auto ev = static_cast(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(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()) { // FIXME: validate the server string targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_serverToJoin, false))); 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_launchOffline ? LaunchMode::Offline : LaunchMode::Normal, targetToJoin, accountToUse, m_offlineName); if (!m_showMainWindow) { 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 PrismExternalUpdater(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); } 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"]; MinecraftInstance* instance = nullptr; 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::parse(server, false)); } else if (!world.isEmpty()) { serverObject = std::make_shared(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; } } TranslationsModel* Application::translations() { return m_translations.get(); } JavaInstallList* Application::javalist() { if (!m_javalist) { m_javalist.reset(new JavaInstallList()); } return m_javalist.get(); } 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)); } // return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file); return DesktopServices::run(m_settings->get("JsonEditor").toString(), { file }); } bool Application::launch(MinecraftInstance* 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()) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; auto* window = extras.window; if (window) { if (!window->saveAll()) { return false; } } auto& controller = extras.controller; controller = std::make_unique(); controller->setInstance(instance); controller->setLaunchMode(mode); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(std::move(targetToJoin)); controller->setAccountToUse(std::move(accountToUse)); controller->setOfflineName(offlineName); if (window) { controller->setParentWidget(window); } else if (m_mainWindow) { controller->setParentWidget(m_mainWindow); } connect(controller.get(), &LaunchController::finished, this, &Application::controllerFinished); addRunningInstance(); QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; } else if (instance->isRunning()) { showInstanceWindow(instance, "console"); return true; } else if (instance->canEdit()) { showInstanceWindow(instance); return true; } return false; } bool Application::kill(BaseInstance* instance) { if (!instance->isRunning()) { qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; return false; } QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive auto& controller = extras.controller; locker.unlock(); 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() const { return m_runningInstances == 0; } void Application::updateIsRunning(bool running) { m_updateRunning = running; } void Application::controllerFinished() { auto* controller = qobject_cast(sender()); if (!controller) { return; } auto id = controller->id(); QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras.at(id); const bool wasSuccessful = controller->wasSuccessful(); // on success, do... if (wasSuccessful && controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } } extras.controller.reset(); subRunningInstance(); // quit when there are no more windows. if (shouldExitNow()) { m_status = wasSuccessful ? Succeeded : Failed; exit(wasSuccessful ? 0 : 1); } } void Application::ShowGlobalSettings(class QWidget* parent, QString openPage) { if (!m_globalSettingsProvider) { return; } emit globalSettingsAboutToOpen(); { SettingsObject::Lock lock(APPLICATION->settings()); PageDialog dlg(m_globalSettingsProvider.get(), std::move(openPage), 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(MinecraftInstance* instance, const QString& page) { if (!instance) { return nullptr; } auto id = instance->id(); QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; auto& window = extras.window; 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); m_openWindows++; connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose); } if (!page.isEmpty()) { window->selectPage(page); } if (extras.controller) { extras.controller->setParentWidget(window); } return window; } void Application::on_windowClose() { m_openWindows--; auto* instWindow = qobject_cast(sender()); if (instWindow) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; extras.window = nullptr; if (extras.controller) { extras.controller->setParentWidget(m_mainWindow); } } auto* mainWindow = qobject_cast(sender()); if (mainWindow) { m_mainWindow = nullptr; } auto* logWindow = qobject_cast(sender()); if (logWindow) { m_viewLogWindow = nullptr; } // quit when there are no more windows. if (shouldExitNow()) { exit(0); } } void Application::updateProxySettings(const QString& proxyTypeStr, const QString& addr, int port, const QString& user, const 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; } HttpMetaCache* Application::metacache() { return m_metacache.get(); } QNetworkAccessManager* Application::network() { return m_network.get(); } Meta::Index* Application::metadataIndex() { if (!m_metadataIndex) { m_metadataIndex.reset(new Meta::Index()); } return m_metadataIndex.get(); } 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 (!LibraryUtils::findMangoHud().isEmpty()) { m_capabilities |= SupportsMangoHud; } #endif } void Application::detectLibraries() { #ifdef Q_OS_LINUX m_detectedGLFWPath = LibraryUtils::find(BuildConfig.GLFW_LIBRARY_NAME); m_detectedOpenALPath = LibraryUtils::find(BuildConfig.OPENAL_LIBRARY_NAME); m_detectedSDLPath = LibraryUtils::find(BuildConfig.SDL_LIBRARY_NAME); qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath << m_detectedSDLPath; #endif } QString Application::getJarPath(const 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 (const auto& 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 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) { 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 (const auto& 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:" << file.errorString(); } }; // 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 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) != 0) { 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(const QString& url) { auto localFile = QFileInfo(url); if (localFile.exists()) { return QUrl::fromLocalFile(localFile.absoluteFilePath()); } return QUrl::fromUserInput(url); } QString Application::javaPath() { return m_settings->get("JavaDir").toString(); } void Application::addQSavePath(const QString& path) { QMutexLocker locker(&m_qsaveResourcesMutex); m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1; } void Application::removeQSavePath(const 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(const QString& path) { QMutexLocker locker(&m_qsaveResourcesMutex); return std::ranges::any_of(m_qsaveResources.keys(), [&path, this](const auto& partialPath) { return path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0; }); }