/* SPDX-FileCopyrightText: 2026 Project Tick * SPDX-FileContributor: Project Tick * SPDX-License-Identifier: GPL-3.0-or-later * * MeshMC - A Custom Launcher for Minecraft * 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, either version 3 of the License, or * (at your option) any later version. * * 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 . */ #include "Application.h" #include "BuildConfig.h" #include "ui/MainWindow.h" #include "ui/InstanceWindow.h" #include "ui/instanceview/AccessibleInstanceView.h" #include "ui/pages/BasePageProvider.h" #include "ui/pages/global/MeshMCPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" #include "ui/pages/global/ProxyPage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/AccountListPage.h" #include "ui/pages/global/PasteEEPage.h" #include "ui/pages/global/CustomCommandsPage.h" #include "ui/pages/global/AppearancePage.h" #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" #include "ui/setupwizard/AnalyticsWizardPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/pagedialog/PageDialog.h" #include "ApplicationMessage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "InstanceList.h" #include #include "icons/IconList.h" #include "net/HttpMetaCache.h" #include "java/JavaUtils.h" #include "updater/UpdateChecker.h" #include "tools/JProfiler.h" #include "tools/JVisualVM.h" #include "tools/MCEditTool.h" #include #include "settings/INISettingsObject.h" #include "settings/Setting.h" #include "translations/TranslationsModel.h" #include "meta/Index.h" #include #include #include #include #include #include #include "MMCStrings.h" #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #endif #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) static const QLatin1String liveCheckFile("live.check"); using namespace Commandline; #define MACOS_HINT \ "If you are on macOS Sierra, you might have to move the app to your " \ "/Applications or ~/Applications" \ "folder. This usually fixes the problem and you can move the application " \ "elsewhere afterwards.\n" \ "\n" namespace { #if defined(Q_OS_MAC) QString macOSDataPath() { auto dataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (!dataPath.isEmpty()) { return dataPath; } return FS::PathCombine(QDir::homePath(), "Library", "Application Support", BuildConfig.MESHMC_NAME); } QStringList legacyMacDataPatterns() { return {"*.cfg", "*.ini", "*.json", "*.log", "accounts", "assets", "cache", "icons", "instances", "java", "libraries", "meta", "metacache", "mods", "patches", "screenshots", "themes", "translations"}; } QStringList legacyMacDataEntries(const QString& legacyDataPath) { QDir legacyDir(legacyDataPath); QSet entries; for (const auto& pattern : legacyMacDataPatterns()) { auto matches = legacyDir.entryInfoList( QStringList{pattern}, QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System); for (const auto& match : matches) { entries.insert(match.fileName()); } } return entries.values(); } bool migrateLegacyMacEntry(const QString& sourcePath, const QString& targetPath) { QFileInfo sourceInfo(sourcePath); if (!sourceInfo.exists()) { return true; } QFileInfo targetInfo(targetPath); if (sourceInfo.isDir()) { if (!targetInfo.exists() && QDir().rename(sourcePath, targetPath)) { return true; } if (!FS::ensureFolderPathExists(targetPath)) { return false; } if (!FS::copy(sourcePath, targetPath)()) { return false; } return FS::deletePath(sourcePath); } if (targetInfo.exists()) { return QFile::remove(sourcePath); } if (!FS::ensureFilePathExists(targetPath)) { return false; } if (QFile::rename(sourcePath, targetPath)) { return true; } if (!QFile::copy(sourcePath, targetPath)) { return false; } return QFile::remove(sourcePath); } bool migrateLegacyMacData(const QString& legacyDataPath, const QString& dataPath) { bool migrated = true; for (const auto& entry : legacyMacDataEntries(legacyDataPath)) { const auto sourcePath = FS::PathCombine(legacyDataPath, entry); const auto targetPath = FS::PathCombine(dataPath, entry); if (!migrateLegacyMacEntry(sourcePath, targetPath)) { qWarning() << "Failed to migrate legacy macOS data entry" << sourcePath << "to" << targetPath; migrated = false; } } return migrated; } #endif } // namespace namespace { void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { const char* levels = "DWCFIS"; const QString format("%1 %2 %3\n"); qint64 msecstotal = APPLICATION->timeSinceStart(); qint64 seconds = msecstotal / 1000; qint64 msecs = msecstotal % 1000; QString foo; char buf[1025] = {0}; ::snprintf(buf, 1024, "%5lld.%03lld", seconds, msecs); QString out = format.arg(buf).arg(levels[type]).arg(msg); APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); QString coloredOut = QString("%1 %2%3%4%5 %6\n") .arg(buf) .arg(Strings::logColor(type)) .arg(levels[type]) .arg(":") .arg(Strings::logColorReset()) .arg(msg); QTextStream(stderr) << coloredOut.toLocal8Bit(); fflush(stderr); } [[maybe_unused]] QString getIdealPlatform(QString currentPlatform) { auto info = Sys::getKernelInfo(); switch (info.kernelType) { case Sys::KernelType::Darwin: { if (info.kernelMajor >= 17) { // macOS 10.13 or newer return "osx64-5.15.2"; } else { // macOS 10.12 or older return "osx64"; } } case Sys::KernelType::Windows: { // FIXME: 5.15.2 is not stable on Windows, due to a large number // of completely unpredictable and hard to reproduce issues break; } case Sys::KernelType::Undetermined: [[fallthrough]]; case Sys::KernelType::Linux: { break; } } return currentPlatform; } } // namespace Application::Application(int& argc, char** argv) : QApplication(argc, argv) { initPlatform(); if (m_status != StartingUp) return; auto args = parseCommandLine(argc, argv); if (m_status != StartingUp) return; QString origcwdPath, adjustedBy, dataPath; if (!resolveDataPath(args, dataPath, adjustedBy, origcwdPath)) return; if (m_instanceIdToLaunch.isEmpty() && !m_serverToJoin.isEmpty()) { qWarning() << "--server can only be used in combination with --launch!"; m_status = Application::Failed; return; } if (m_instanceIdToLaunch.isEmpty() && !m_profileToUse.isEmpty()) { qWarning() << "--account can only be used in combination with --launch!"; m_status = Application::Failed; return; } if (!initPeerInstance()) return; if (!initLogging(dataPath)) return; QString binPath = applicationDirPath(); setupPaths(binPath, origcwdPath, adjustedBy); initSettings(); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(groupViewAccessibleFactory); #endif /* !QT_NO_ACCESSIBILITY */ initSubsystems(); initAnalytics(); if (createSetupWizard()) { return; } performMainStartupAction(); } void Application::initAnalytics() { const int analyticsVersion = 2; if (BuildConfig.ANALYTICS_ID.isEmpty()) { return; } auto analyticsSetting = m_settings->getSetting("Analytics"); connect(analyticsSetting.get(), &Setting::SettingChanged, this, &Application::analyticsSettingChanged); QString clientID = m_settings->get("AnalyticsClientID").toString(); if (clientID.isEmpty()) { clientID = QUuid::createUuid().toString(); clientID.remove(QLatin1Char('{')); clientID.remove(QLatin1Char('}')); m_settings->set("AnalyticsClientID", clientID); } m_analytics = new GAnalytics(BuildConfig.ANALYTICS_ID, clientID, analyticsVersion, this); m_analytics->setLogLevel(GAnalytics::Debug); m_analytics->setAnonymizeIPs(true); // FIXME: the ganalytics library has no idea about our fancy shared // pointers... m_analytics->setNetworkAccessManager(network().get()); if (m_settings->get("AnalyticsSeen").toInt() < m_analytics->version()) { qDebug() << "Analytics info not seen by user yet (or old version)."; return; } if (!m_settings->get("Analytics").toBool()) { qDebug() << "Analytics disabled by user."; return; } m_analytics->enable(); qDebug() << "<> Initialized analytics with tid" << BuildConfig.ANALYTICS_ID; } void Application::initPlatform() { #if defined Q_OS_WIN32 // attach the parent console if (AttachConsole(ATTACH_PARENT_PROCESS)) { // Reopen and sync all the I/O after attaching to parent console if (freopen("CON", "w", stdout)) { std::ios_base::sync_with_stdio(); } if (freopen("CON", "w", stderr)) { std::ios_base::sync_with_stdio(); } if (freopen("CON", "r", stdin)) { std::cin.sync_with_stdio(); } auto out = GetStdHandle(STD_OUTPUT_HANDLE); DWORD written; const char* endline = "\n"; WriteConsole(out, endline, strlen(endline), &written, nullptr); consoleAttached = true; } #endif setOrganizationName(BuildConfig.MESHMC_NAME); setOrganizationDomain(BuildConfig.MESHMC_DOMAIN); setApplicationName(BuildConfig.MESHMC_NAME); setApplicationDisplayName(BuildConfig.MESHMC_DISPLAYNAME); setApplicationVersion(BuildConfig.printableVersionString()); startTime = QDateTime::currentDateTime(); #ifdef Q_OS_LINUX { QFile osrelease("/proc/sys/kernel/osrelease"); if (osrelease.open(QFile::ReadOnly | QFile::Text)) { QTextStream in(&osrelease); auto contents = in.readAll(); if (contents.contains("WSL", Qt::CaseInsensitive) || contents.contains("Microsoft", Qt::CaseInsensitive)) { showFatalErrorMessage( "Unsupported system detected!", "Linux-on-Windows distributions are not supported.\n\n" "Please use the Windows binary when playing on Windows."); return; } } } #endif // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); } QHash Application::parseCommandLine(int& argc, char** argv) { QHash args; Parser parser(FlagStyle::GNU, ArgumentStyle::SpaceAndEquals); // --help parser.addSwitch("help"); parser.addShortOpt("help", 'h'); parser.addDocumentation("help", "Display this help and exit."); // --version parser.addSwitch("version"); parser.addShortOpt("version", 'V'); parser.addDocumentation("version", "Display program version and exit."); // --dir parser.addOption("dir"); parser.addShortOpt("dir", 'd'); parser.addDocumentation( "dir", "Use the supplied folder as application root " "instead of the binary location (use '.' for current)"); // --launch parser.addOption("launch"); parser.addShortOpt("launch", 'l'); parser.addDocumentation("launch", "Launch the specified instance (by instance ID)"); // --server parser.addOption("server"); parser.addShortOpt("server", 's'); parser.addDocumentation("server", "Join the specified server on launch (only valid " "in combination with --launch)"); // --profile parser.addOption("profile"); parser.addShortOpt("profile", 'a'); parser.addDocumentation("profile", "Use the account specified by its profile name " "(only valid in combination with --launch)"); // --alive parser.addSwitch("alive"); parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after MeshMC starts"); // --import parser.addOption("import"); parser.addShortOpt("import", 'I'); parser.addDocumentation( "import", "Import instance from specified zip (local path or URL)"); // parse the arguments try { args = parser.parse(arguments()); } catch (const ParsingError& e) { qCritical() << "CommandLineError:" << e.what(); if (argc > 0) qCritical() << "Try '" << argv[0] << "' -h' to get help on command line parameters."; m_status = Application::Failed; return args; } // display help and exit if (args["help"].toBool()) { QTextStream(stdout) << parser.compileHelp(arguments()[0]); m_status = Application::Succeeded; return args; } // display version and exit if (args["version"].toBool()) { QTextStream(stdout) << "Version " << BuildConfig.printableVersionString() << "\n"; QTextStream(stdout) << "Git " << BuildConfig.GIT_COMMIT << "\n"; m_status = Application::Succeeded; return args; } m_instanceIdToLaunch = args["launch"].toString(); m_serverToJoin = args["server"].toString(); m_profileToUse = args["profile"].toString(); m_liveCheck = args["alive"].toBool(); m_zipToImport = args["import"].toUrl(); return args; } bool Application::resolveDataPath(const QHash& args, QString& dataPath, QString& adjustedBy, QString& origcwdPath) { origcwdPath = QDir::currentPath(); QString dirParam = args["dir"].toString(); if (!dirParam.isEmpty()) { adjustedBy += "Command line " + dirParam; dataPath = dirParam; } else { #if defined(Q_OS_MAC) dataPath = macOSDataPath(); adjustedBy += "macOS application data location " + dataPath; #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QDir portableDir(applicationDirPath()); portableDir.cdUp(); QString portableRoot = portableDir.absolutePath(); QString portablePath = FS::PathCombine(portableRoot, "portable.txt"); if (QFileInfo::exists(portablePath)) { dataPath = portableRoot; adjustedBy += "Portable mode (portable.txt found), using portable root " + dataPath; } else { QString xdgDataHome = QProcessEnvironment::systemEnvironment().value("XDG_DATA_HOME"); if (xdgDataHome.isEmpty()) { xdgDataHome = QDir::homePath() + "/.local/share"; } dataPath = FS::PathCombine(xdgDataHome, BuildConfig.MESHMC_NAME); adjustedBy += "Non-portable mode, using XDG data location " + dataPath; } #elif defined(Q_OS_WIN32) QString portablePath = FS::PathCombine(applicationDirPath(), "portable.txt"); if (QFileInfo::exists(portablePath)) { dataPath = applicationDirPath(); adjustedBy += "Portable mode (portable.txt found), using binary path " + dataPath; } else { QString appDataPath = QStandardPaths::writableLocation( QStandardPaths::AppLocalDataLocation); dataPath = appDataPath; adjustedBy += "Non-portable mode, using AppData location " + dataPath; } #else dataPath = applicationDirPath(); adjustedBy += "Fallback to binary path " + dataPath; #endif } if (!FS::ensureFolderPathExists(dataPath)) { showFatalErrorMessage( "MeshMC data folder could not be created.", QString("MeshMC data folder could not be created.\n" "\n" #if defined(Q_OS_MAC) MACOS_HINT #endif "Make sure you have the right permissions to MeshMC data " "folder and any folder needed to access it.\n" "(%1)\n" "\n" "MeshMC cannot continue until you fix this problem.") .arg(dataPath)); return false; } if (!QDir::setCurrent(dataPath)) { showFatalErrorMessage( "MeshMC data folder could not be opened.", QString("MeshMC data folder could not be opened.\n" "\n" #if defined(Q_OS_MAC) MACOS_HINT #endif "Make sure you have the right permissions to MeshMC data " "folder.\n" "(%1)\n" "\n" "MeshMC cannot continue until you fix this problem.") .arg(dataPath)); return false; } #if defined(Q_OS_MAC) const auto legacyDataPath = QDir(applicationDirPath()).absolutePath(); if (dataPath != legacyDataPath) { const auto legacyEntries = legacyMacDataEntries(legacyDataPath); if (!legacyEntries.isEmpty()) { qInfo() << "Migrating legacy macOS data from app bundle to" << dataPath << legacyEntries; migrateLegacyMacData(legacyDataPath, dataPath); } } #endif return true; } bool Application::initPeerInstance() { /* * Establish the mechanism for communication with an already running * MeshMC 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()) { int timeout = 2000; if (m_instanceIdToLaunch.isEmpty()) { ApplicationMessage activate; activate.command = "activate"; m_peerInstance->sendMessage(activate.serialize(), timeout); if (!m_zipToImport.isEmpty()) { ApplicationMessage import; import.command = "import"; import.args.insert("path", m_zipToImport.toString()); 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; } if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } m_peerInstance->sendMessage(launch.serialize(), timeout); } m_status = Application::Succeeded; return false; } return true; } bool Application::initLogging(const QString& dataPath) { static const QString logBase = BuildConfig.MESHMC_NAME + "-%0.log"; auto moveFile = [](const QString& oldName, const QString& newName) { static_cast(QFile::remove(newName)); static_cast(QFile::copy(oldName, newName)); static_cast(QFile::remove(oldName)); }; moveFile(logBase.arg(3), logBase.arg(4)); moveFile(logBase.arg(2), logBase.arg(3)); moveFile(logBase.arg(1), logBase.arg(2)); moveFile(logBase.arg(0), logBase.arg(1)); logFile = std::make_unique(logBase.arg(0)); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage( "MeshMC data folder is not writable!", QString("MeshMC couldn't create a log file - the data folder is " "not writable.\n" "\n" #if defined(Q_OS_MAC) MACOS_HINT #endif "Make sure you have write permissions to the data folder.\n" "(%1)\n" "\n" "MeshMC cannot continue until you fix this problem.") .arg(dataPath)); return false; } qInstallMessageHandler(appDebugOutput); qDebug() << "<> Log initialized."; return true; } void Application::setupPaths(const QString& binPath, const QString& origcwdPath, const QString& adjustedBy) { // Root path is used for updates. #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QDir foo(FS::PathCombine(binPath, "..")); 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 qInfo() << BuildConfig.MESHMC_DISPLAYNAME << ", (c) 2026 " << BuildConfig.MESHMC_COPYRIGHT; qInfo() << "Version : " << BuildConfig.printableVersionString(); 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; 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; } qInfo() << "<> Paths set."; if (m_liveCheck) { auto appID = ApplicationId::fromPathAndVersion( QDir::currentPath(), BuildConfig.printableVersionString()); QFile check(liveCheckFile); if (check.open(QIODevice::WriteOnly | QIODevice::Truncate)) { auto payload = appID.toString().toUtf8(); if (check.write(payload) != payload.size()) { qWarning() << "Could not write into" << liveCheckFile << "!"; check.remove(); } else { check.close(); } } else { qWarning() << "Could not open" << liveCheckFile << "for writing!"; } } } void Application::initSettings() { m_settings.reset( new INISettingsObject(BuildConfig.MESHMC_CONFIGFILE, this)); // Updates m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL); m_settings->registerSetting("AutoUpdate", true); // Theming m_settings->registerSetting("IconTheme", QString("pe_colored")); m_settings->registerSetting("ApplicationTheme", QString("system")); // Notifications m_settings->registerSetting("ShownNotifications", QString()); // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); 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); // Folders m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods"); m_settings->registerSetting("IconsDir", "icons"); // Editors m_settings->registerSetting("JsonEditor", QString()); // Language m_settings->registerSetting("Language", QString()); // 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 — compute reasonable defaults based on system RAM int defaultMinMem = 512; int defaultMaxMem = 1024; { uint64_t systemRamMiB = Sys::getSystemRam() / Sys::mebibyte; if (systemRamMiB >= 32768) { // 32+ GB defaultMinMem = 1024; defaultMaxMem = 8192; } else if (systemRamMiB >= 16384) { // 16-32 GB defaultMinMem = 1024; defaultMaxMem = 6144; } else if (systemRamMiB >= 8192) { // 8-16 GB defaultMinMem = 512; defaultMaxMem = 4096; } else if (systemRamMiB >= 4096) { // 4-8 GB defaultMinMem = 512; defaultMaxMem = 2048; } // <4 GB: keep 512/1024 defaults } m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, defaultMinMem); m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, defaultMaxMem); m_settings->registerSetting("PermGen", 128); // Java Settings m_settings->registerSetting("JavaPath", ""); m_settings->registerSetting("JavaTimestamp", 0); m_settings->registerSetting("JavaArchitecture", ""); m_settings->registerSetting("JavaVersion", ""); m_settings->registerSetting("JavaVendor", ""); m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("UseNativeGLFW", false); // Game time m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true); m_settings->registerSetting("RecordGameTime", true); // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "MeshMCPart"); // 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("BackgroundCat", QString("kitteh")); m_settings->registerSetting("CatOpacity", 100); m_settings->registerSetting("InstSortMode", "Name"); 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", ""); // paste.ee API key m_settings->registerSetting("PasteEEAPIKey", "meshmc"); if (!BuildConfig.ANALYTICS_ID.isEmpty()) { // Analytics m_settings->registerSetting("Analytics", true); m_settings->registerSetting("AnalyticsSeen", 0); m_settings->registerSetting("AnalyticsClientID", QString()); } // Init page provider { m_globalSettingsProvider = std::make_shared(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(); m_globalSettingsProvider->addPage(); } qDebug() << "<> Settings loaded."; } void Application::initSubsystems() { // initialize network access and proxy setup { m_network = 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); qDebug() << "<> Network done."; } // load translations { m_translations.reset(new TranslationsModel("translations")); auto bcp47Name = m_settings->get("Language").toString(); m_translations->selectLanguage(bcp47Name); qDebug() << "Your language is" << bcp47Name; qDebug() << "<> Translations loaded."; } // initialize the updater if (BuildConfig.UPDATER_ENABLED && UpdateChecker::isUpdaterSupported()) { m_updateChecker.reset(new UpdateChecker(m_network)); qDebug() << "<> Updater initialized (feed:" << BuildConfig.UPDATER_FEED_URL << "| github:" << BuildConfig.UPDATER_GITHUB_API_URL << ")."; } else if (BuildConfig.UPDATER_ENABLED) { qDebug() << "<> Updater disabled on this platform/mode."; } // 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, [&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); qDebug() << "<> Instance icons intialized."; } // Icon themes { // TODO: icon themes and instance icons do not mesh well together. // Rearrange and fix discrepancies! set icon theme search path! auto searchPaths = QIcon::themeSearchPaths(); searchPaths.append("iconthemes"); QIcon::setThemeSearchPaths(searchPaths); qDebug() << "<> Icon themes initialized."; } // Initialize widget themes { m_themeManager = std::make_unique(); qDebug() << "<> Widget themes initialized."; } // 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(); qDebug() << "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); qDebug() << "Loading Instances..."; m_instances->loadList(); qDebug() << "<> Instances loaded."; } // and accounts { m_accounts.reset(new AccountList(this)); qDebug() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); m_accounts->fillQueue(); qDebug() << "<> 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("asset_objects", QDir("assets/objects").absolutePath()); m_metacache->addBase("versions", QDir("versions").absolutePath()); m_metacache->addBase("libraries", QDir("libraries").absolutePath()); m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath()); m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); m_metacache->addBase("liteloader", QDir("mods/liteloader").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("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->Load(); qDebug() << "<> Cache initialized."; } // now we have network, download translation updates m_translations->downloadIndex(); // FIXME: what to do with these? m_profilers.insert("jprofiler", std::make_shared()); m_profilers.insert("jvisualvm", std::make_shared()); 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)); } connect(this, &Application::aboutToQuit, [this]() { if (m_instances) { // save any remaining instance state m_instances->saveNow(); } if (logFile) { logFile->flush(); logFile->close(); } }); { m_themeManager->applyCurrentlySelectedTheme(true); qDebug() << "<> Theme applied."; } } bool Application::createSetupWizard() { bool javaRequired = [&]() { 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); if (actualPath.isNull()) { return true; } return false; }(); bool analyticsRequired = [&]() { if (BuildConfig.ANALYTICS_ID.isEmpty()) { return false; } if (!settings()->get("Analytics").toBool()) { return false; } if (settings()->get("AnalyticsSeen").toInt() < analytics()->version()) { return true; } return false; }(); bool languageRequired = [&]() { if (settings()->get("Language").toString().isEmpty()) return true; return false; }(); bool wizardRequired = javaRequired || analyticsRequired || languageRequired; if (wizardRequired) { m_setupWizard = new SetupWizard(nullptr); if (languageRequired) { m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); } if (javaRequired) { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); } if (analyticsRequired) { m_setupWizard->addPage(new AnalyticsWizardPage(m_setupWizard)); } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); return true; } return false; } 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) { MinecraftServerTargetPtr serverToJoin = nullptr; MinecraftAccountPtr accountToUse = nullptr; qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; if (!m_serverToJoin.isEmpty()) { // FIXME: validate the server string serverToJoin.reset(new MinecraftServerTarget( MinecraftServerTarget::parse(m_serverToJoin))); qDebug() << " Launching with server" << m_serverToJoin; } if (!m_profileToUse.isEmpty()) { accountToUse = accounts()->getAccountByProfileName(m_profileToUse); if (!accountToUse) { return; } qDebug() << " Launching with account" << m_profileToUse; } launch(inst, true, nullptr, serverToJoin, accountToUse); return; } } if (!m_mainWindow) { // normal main window showMainWindow(false); qDebug() << "<> Main window shown."; } if (!m_zipToImport.isEmpty()) { qDebug() << "<> Importing instance from zip:" << m_zipToImport; m_mainWindow->droppedURLs({m_zipToImport}); } } 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) { if (status() != Initialized) { qDebug() << "Received message" << message << "while still initializing. It will be ignored."; return; } ApplicationMessage received; received.parse(message); auto& command = received.command; if (command == "activate") { showMainWindow(); } else if (command == "import") { QString path = received.args["path"]; if (path.isEmpty()) { qWarning() << "Received" << command << "message without a zip path/URL."; return; } m_mainWindow->droppedURLs({QUrl(path)}); } else if (command == "launch") { QString id = received.args["id"]; QString server = received.args["server"]; QString profile = received.args["profile"]; 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; } MinecraftServerTargetPtr serverObject = nullptr; if (!server.isEmpty()) { serverObject = std::make_shared( MinecraftServerTarget::parse(server)); } 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, true, nullptr, serverObject, accountObject); } else { qWarning() << "Received invalid message" << message; } } void Application::analyticsSettingChanged(const Setting&, QVariant value) { if (!m_analytics) return; bool enabled = value.toBool(); if (enabled) { qDebug() << "Analytics enabled by user."; } else { qDebug() << "Analytics disabled by user."; } m_analytics->enable(enabled); } std::shared_ptr Application::translations() { return m_translations; } std::shared_ptr Application::javalist() { if (!m_javalist) { m_javalist.reset(new JavaInstallList()); } return m_javalist; } std::vector Application::getValidApplicationThemes() { return m_themeManager->allThemes(); } void Application::setApplicationTheme(const QString& name, bool initial) { m_themeManager->setApplicationTheme(name, initial); } void Application::setIconTheme(const QString& name) { m_themeManager->setIconTheme(name); } ThemeManager* Application::themeManager() const { return m_themeManager.get(); } QIcon Application::getThemedIcon(const QString& name) { if (name == "logo") { return QIcon(":/org.projecttick.MeshMC.svg"); } return XdgIcon::fromTheme(name); } 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::run(m_settings->get("JsonEditor").toString(), {file}); } } bool Application::launch(InstancePtr instance, bool online, BaseProfilerFactory* profiler, MinecraftServerTargetPtr serverToJoin, MinecraftAccountPtr accountToUse) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. " "Please try again when updates are completed."; } else if (instance->canLaunch()) { auto& extras = m_instanceExtras[instance->id()]; auto& window = extras.window; if (window) { if (!window->saveAll()) { return false; } } auto& controller = extras.controller; controller.reset(new LaunchController()); controller->setInstance(instance); controller->setOnline(online); controller->setProfiler(profiler); controller->setServerToJoin(serverToJoin); controller->setAccountToUse(accountToUse); 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); addRunningInstance(); controller->start(); return true; } else if (instance->isRunning()) { showInstanceWindow(instance, "console"); return true; } else if (instance->canEdit()) { showInstanceWindow(instance); return true; } return false; } bool Application::kill(InstancePtr instance) { if (!instance->isRunning()) { qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; return false; } auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive auto controller = extras.controller; if (controller) { return controller->abort(); } return true; } 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(QObject::sender()); if (!controller) return; auto id = controller->id(); auto& extras = m_instanceExtras[id]; // on success, do... if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { extras.window->close(); } } extras.controller.reset(); 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(QObject::sender()); if (!controller) return; auto id = controller->id(); auto& extras = m_instanceExtras[id]; // on failure, do... nothing extras.controller.reset(); 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); dlg.exec(); } emit globalSettingsClosed(); } 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").toByteArray())); m_mainWindow->restoreGeometry(QByteArray::fromBase64( APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); 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++; } // FIXME: move this somewhere else... if (m_analytics) { auto windowSize = m_mainWindow->size(); auto sizeString = QString("%1x%2").arg(windowSize.width()).arg(windowSize.height()); qDebug() << "Viewport size" << sizeString; m_analytics->setViewportSize(sizeString); /* * cm1 = java min heap [MB] * cm2 = java max heap [MB] * cm3 = system RAM [MB] * * cd1 = java version * cd2 = java architecture * cd3 = system architecture * cd4 = CPU architecture */ QVariantMap customValues; int min = m_settings->get("MinMemAlloc").toInt(); int max = m_settings->get("MaxMemAlloc").toInt(); if (min < max) { customValues["cm1"] = min; customValues["cm2"] = max; } else { customValues["cm1"] = max; customValues["cm2"] = min; } constexpr uint64_t Mega = 1024ull * 1024ull; int ramSize = int(Sys::getSystemRam() / Mega); qDebug() << "RAM size is" << ramSize << "MB"; customValues["cm3"] = ramSize; customValues["cd1"] = m_settings->get("JavaVersion"); customValues["cd2"] = m_settings->get("JavaArchitecture"); customValues["cd3"] = Sys::isSystem64bit() ? "64" : "32"; customValues["cd4"] = Sys::isCPU64bit() ? "64" : "32"; auto kernelInfo = Sys::getKernelInfo(); customValues["cd5"] = kernelInfo.kernelName; customValues["cd6"] = kernelInfo.kernelVersion; auto distInfo = Sys::getDistributionInfo(); if (!distInfo.distributionName.isEmpty()) { customValues["cd7"] = distInfo.distributionName; } if (!distInfo.distributionVersion.isEmpty()) { customValues["cd8"] = distInfo.distributionVersion; } m_analytics->sendScreenView("Main Window", customValues); } return m_mainWindow; } InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) { if (!instance) return nullptr; auto id = instance->id(); auto& extras = m_instanceExtras[id]; auto& window = extras.window; if (window) { 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(QObject::sender()); if (instWindow) { auto& extras = m_instanceExtras[instWindow->instanceId()]; extras.window = nullptr; if (extras.controller) { extras.controller->setParentWidget(m_mainWindow); } } auto mainWindow = qobject_cast(QObject::sender()); if (mainWindow) { m_mainWindow = nullptr; } // quit when there are no more windows. if (shouldExitNow()) { exit(0); } } QString Application::msaClientId() const { return BuildConfig.MSAClientID; } 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 Application::metacache() { return m_metacache; } shared_qobject_ptr Application::network() { return m_network; } shared_qobject_ptr Application::metadataIndex() { if (!m_metadataIndex) { m_metadataIndex.reset(new Meta::Index()); } return m_metadataIndex; } QString Application::getJarsPath() { if (m_jarsPath.isEmpty()) { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) auto appDir = QCoreApplication::applicationDirPath(); auto installedPath = FS::PathCombine(appDir, "..", "share", BuildConfig.MESHMC_NAME); if (QDir(installedPath).exists()) { return installedPath; } return FS::PathCombine(appDir, "jars"); #else return FS::PathCombine(QCoreApplication::applicationDirPath(), "jars"); #endif } return m_jarsPath; }