PrismLauncher/application/MultiMC.cpp
Hamish Arblaster e883cf2359 GH-2529 Move data path on macOS to MultiMC.app/Data
macOS seems to dislike changing files in the APPBUNDLE.app/Contents directory because it has to re-scan the directory every launch. As a result, large amounts of data there seems to cause freezes of MultiMC. Moving the default location outside of this directory, and thus the data, stops these freezes.
There is also a dialogue when the user first opens the app that asks them if they'd like to migrate their data folder, if they select yes it will move it, and if they select no it will not move it and allow them to move it later with an option in settings.
2021-07-11 09:36:21 +10:00

1449 lines
45 KiB
C++

#include "MultiMC.h"
#include "BuildConfig.h"
#include "MainWindow.h"
#include "InstanceWindow.h"
#include "groupview/AccessibleGroupView.h"
#include <QAccessible>
#include "pages/BasePageProvider.h"
#include "pages/global/MultiMCPage.h"
#include "pages/global/MinecraftPage.h"
#include "pages/global/JavaPage.h"
#include "pages/global/LanguagePage.h"
#include "pages/global/ProxyPage.h"
#include "pages/global/ExternalToolsPage.h"
#include "pages/global/AccountListPage.h"
#include "pages/global/PasteEEPage.h"
#include "pages/global/CustomCommandsPage.h"
#include "themes/ITheme.h"
#include "themes/SystemTheme.h"
#include "themes/DarkTheme.h"
#include "themes/BrightTheme.h"
#include "themes/CustomTheme.h"
#include "setupwizard/SetupWizard.h"
#include "setupwizard/LanguageWizardPage.h"
#include "setupwizard/JavaWizardPage.h"
#include "setupwizard/AnalyticsWizardPage.h"
#include <iostream>
#include <QDir>
#include <QFileInfo>
#include <QNetworkAccessManager>
#include <QTranslator>
#include <QLibraryInfo>
#include <QList>
#include <QStringList>
#include <QDebug>
#include <QStyleFactory>
#include "dialogs/CustomMessageBox.h"
#include "InstanceList.h"
#include <minecraft/auth/MojangAccountList.h>
#include "icons/IconList.h"
#include "net/HttpMetaCache.h"
#include "Env.h"
#include "java/JavaUtils.h"
#include "updater/UpdateChecker.h"
#include "tools/JProfiler.h"
#include "tools/JVisualVM.h"
#include "tools/MCEditTool.h"
#include <xdgicon.h>
#include "settings/INISettingsObject.h"
#include "settings/Setting.h"
#include "translations/TranslationsModel.h"
#include <Commandline.h>
#include <FileSystem.h>
#include <DesktopServices.h>
#include <LocalPeer.h>
#include <ganalytics.h>
#include <sys.h>
#include "pagedialog/PageDialog.h"
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <stdio.h>
#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 MultiMC.app to your /Applications or ~/Applications folder. "\
"This usually fixes the problem and you can move the application elsewhere afterwards.\n"\
"\n"
static void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
const char *levels = "DWCFIS";
const QString format("%1 %2 %3\n");
qint64 msecstotal = MMC->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);
MMC->logFile->write(out.toUtf8());
MMC->logFile->flush();
QTextStream(stderr) << out.toLocal8Bit();
fflush(stderr);
}
MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
{
#if defined Q_OS_WIN32
// attach the parent console
if(AttachConsole(ATTACH_PARENT_PROCESS))
{
// if attach succeeds, reopen and sync all the i/o
if(freopen("CON", "w", stdout))
{
std::cout.sync_with_stdio();
}
if(freopen("CON", "w", stderr))
{
std::cerr.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, NULL);
consoleAttached = true;
}
#endif
setOrganizationName("MultiMC");
setOrganizationDomain("multimc.org");
setApplicationName("MultiMC5");
setApplicationDisplayName("MultiMC 5");
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 MultiMC binary when playing on Windows."
);
return;
}
}
}
#endif
// Don't quit on hiding the last window
this->setQuitOnLastWindowClosed(false);
// Commandline parsing
QHash<QString, QVariant> 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 MultiMC 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)");
// --alive
parser.addSwitch("alive");
parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after MultiMC 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)
{
std::cerr << "CommandLineError: " << e.what() << std::endl;
if(argc > 0)
std::cerr << "Try '" << argv[0] << " -h' to get help on MultiMC's command line parameters."
<< std::endl;
m_status = MultiMC::Failed;
return;
}
// display help and exit
if (args["help"].toBool())
{
std::cout << qPrintable(parser.compileHelp(arguments()[0]));
m_status = MultiMC::Succeeded;
return;
}
// display version and exit
if (args["version"].toBool())
{
std::cout << "Version " << BuildConfig.printableVersionString().toStdString() << std::endl;
std::cout << "Git " << BuildConfig.GIT_COMMIT.toStdString() << std::endl;
m_status = MultiMC::Succeeded;
return;
}
}
m_instanceIdToLaunch = args["launch"].toString();
m_serverToJoin = args["server"].toString();
m_liveCheck = args["alive"].toBool();
m_zipToImport = args["import"].toUrl();
QString origcwdPath = QDir::currentPath();
QString binPath = applicationDirPath();
QString adjustedBy;
QString dataPath;
// change folder
QString dirParam = args["dir"].toString();
if (!dirParam.isEmpty())
{
// the dir param. it makes multimc data path point to whatever the user specified
// on command line
adjustedBy += "Command line " + dirParam;
dataPath = dirParam;
}
else
{
#ifdef MULTIMC_LINUX_DATADIR
QString xdgDataHome = QFile::decodeName(qgetenv("XDG_DATA_HOME"));
if (xdgDataHome.isEmpty())
xdgDataHome = QDir::homePath() + QLatin1String("/.local/share");
dataPath = xdgDataHome + "/multimc";
adjustedBy += "XDG standard " + dataPath;
#elif defined(Q_OS_MAC)
QDir foo(FS::PathCombine(applicationDirPath(), "../../Data"));
dataPath = foo.absolutePath();
adjustedBy += "Fallback to special Mac location " + dataPath;
#else
dataPath = applicationDirPath();
adjustedBy += "Fallback to binary path " + dataPath;
#endif
}
if (!FS::ensureFolderPathExists(dataPath))
{
showFatalErrorMessage(
"MultiMC data folder could not be created.",
"MultiMC data folder could not be created.\n"
"\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have the right permissions to the MultiMC data folder and any folder needed to access it.\n"
"\n"
"MultiMC cannot continue until you fix this problem."
);
return;
}
if (!QDir::setCurrent(dataPath))
{
showFatalErrorMessage(
"MultiMC data folder could not be opened.",
"MultiMC data folder could not be opened.\n"
"\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have the right permissions to the MultiMC data folder.\n"
"\n"
"MultiMC cannot continue until you fix this problem."
);
return;
}
if(m_instanceIdToLaunch.isEmpty() && !m_serverToJoin.isEmpty())
{
std::cerr << "--server can only be used in combination with --launch!" << std::endl;
m_status = MultiMC::Failed;
return;
}
#if defined(Q_OS_MAC)
// move user data to new location if on macOS and it still exists in Contents/MacOS
QDir fi(applicationDirPath());
QString originalData = fi.absolutePath();
// if the config file exists in Contents/MacOS, then user data is still there and needs to moved
if (QFileInfo::exists(FS::PathCombine(originalData, "multimc.cfg")))
{
if (!QFileInfo::exists(FS::PathCombine(originalData, "dontmovemacdata")))
{
QMessageBox::StandardButton askMoveDialogue;
askMoveDialogue = QMessageBox::question(nullptr, "MultiMC 5", "Would you like to move application data to a new data location? It will improve MultiMC's performance, but if you switch to older versions it will look like instances have disappeared. If you select no, you can migrate later in settings. You should select yes unless you're commonly switching between different versions of MultiMC (eg. develop and stable).", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (askMoveDialogue == QMessageBox::Yes)
{
qDebug() << "On macOS and found config file in old location, moving user data...";
QDir dir;
QStringList dataFiles {
"*.log", // MultiMC-@.log
"accounts.json",
"accounts",
"assets",
"cache",
"icons",
"instances",
"libraries",
"meta",
"metacache",
"mods",
"multimc.cfg",
"themes",
"translations"
};
QDirIterator files(originalData, dataFiles);
while (files.hasNext()) {
QString filePath(files.next());
QString fileName(files.fileName());
if (!dir.rename(filePath, FS::PathCombine(dataPath, fileName)))
{
qWarning() << "Failed to move " << fileName;
}
}
}
else
{
dataPath = originalData;
QDir::setCurrent(dataPath);
QFile file(originalData + "/dontmovemacdata");
file.open(QIODevice::WriteOnly);
}
}
else
{
dataPath = originalData;
QDir::setCurrent(dataPath);
}
}
#endif
/*
* Establish the mechanism for communication with an already running MultiMC 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, &MultiMC::messageReceived);
if(m_peerInstance->isClient())
{
int timeout = 2000;
if(m_instanceIdToLaunch.isEmpty())
{
m_peerInstance->sendMessage("activate", timeout);
if(!m_zipToImport.isEmpty())
{
m_peerInstance->sendMessage("import " + m_zipToImport.toString(), timeout);
}
}
else
{
if(!m_serverToJoin.isEmpty())
{
m_peerInstance->sendMessage(
"launch-with-server " + m_instanceIdToLaunch + " " + m_serverToJoin, timeout);
}
else
{
m_peerInstance->sendMessage("launch " + m_instanceIdToLaunch, timeout);
}
}
m_status = MultiMC::Succeeded;
return;
}
}
// init the logger
{
static const QString logBase = "MultiMC-%0.log";
auto moveFile = [](const QString &oldName, const QString &newName)
{
QFile::remove(newName);
QFile::copy(oldName, newName);
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::unique_ptr<QFile>(new QFile(logBase.arg(0)));
if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
{
showFatalErrorMessage(
"MultiMC data folder is not writable!",
"MultiMC couldn't create a log file - the MultiMC data folder is not writable.\n"
"\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have write permissions to the MultiMC data folder.\n"
"\n"
"MultiMC cannot continue until you fix this problem."
);
return;
}
qInstallMessageHandler(appDebugOutput);
qDebug() << "<> Log initialized.";
}
// Set up paths
{
// Root path is used for updates.
#ifdef Q_OS_LINUX
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
#ifdef MULTIMC_JARS_LOCATION
ENV.setJarsPath( TOSTRING(MULTIMC_JARS_LOCATION) );
#endif
qDebug() << "MultiMC 5, (c) 2013-2021 MultiMC Contributors";
qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
if (adjustedBy.size())
{
qDebug() << "Work dir before adjustment : " << origcwdPath;
qDebug() << "Work dir after adjustment : " << QDir::currentPath();
qDebug() << "Adjusted by : " << adjustedBy;
}
else
{
qDebug() << "Work dir : " << QDir::currentPath();
}
qDebug() << "Binary path : " << binPath;
qDebug() << "Application root path : " << m_rootPath;
if(!m_instanceIdToLaunch.isEmpty())
{
qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch;
}
if(!m_serverToJoin.isEmpty())
{
qDebug() << "Address of server to join :" << m_serverToJoin;
}
qDebug() << "<> Paths set.";
}
do // once
{
if(m_liveCheck)
{
QFile check(liveCheckFile);
if(!check.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
qWarning() << "Could not open" << liveCheckFile << "for writing!";
break;
}
auto payload = appID.toString().toUtf8();
if(check.write(payload) != payload.size())
{
qWarning() << "Could not write into" << liveCheckFile << "!";
check.remove();
break;
}
check.close();
}
} while(false);
// Initialize application settings
{
m_settings.reset(new INISettingsObject("multimc.cfg", this));
// Updates
m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
m_settings->registerSetting("AutoUpdate", true);
// Theming
m_settings->registerSetting("IconTheme", QString("multimc"));
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
m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512);
m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024);
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("RecordGameTime", true);
// Minecraft launch method
m_settings->registerSetting("MCLaunchMethod", "LauncherPart");
// 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("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", "multimc");
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<GenericPageProvider>(tr("Settings"));
m_globalSettingsProvider->addPage<MultiMCPage>();
m_globalSettingsProvider->addPage<MinecraftPage>();
m_globalSettingsProvider->addPage<JavaPage>();
m_globalSettingsProvider->addPage<LanguagePage>();
m_globalSettingsProvider->addPage<CustomCommandsPage>();
m_globalSettingsProvider->addPage<ProxyPage>();
m_globalSettingsProvider->addPage<ExternalToolsPage>();
m_globalSettingsProvider->addPage<AccountListPage>();
m_globalSettingsProvider->addPage<PasteEEPage>();
}
qDebug() << "<> Settings loaded.";
}
#ifndef QT_NO_ACCESSIBILITY
QAccessible::installFactory(groupViewAccessibleFactory);
#endif /* !QT_NO_ACCESSIBILITY */
// 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)
{
m_updateChecker.reset(new UpdateChecker(BuildConfig.CHANLIST_URL, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD));
qDebug() << "<> Updater started.";
}
// Instance icons
{
auto setting = MMC->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());
});
ENV.registerIconList(m_icons);
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
{
auto insertTheme = [this](ITheme * theme)
{
m_themes.insert(std::make_pair(theme->id(), std::unique_ptr<ITheme>(theme)));
};
auto darkTheme = new DarkTheme();
insertTheme(new SystemTheme());
insertTheme(darkTheme);
insertTheme(new BrightTheme());
insertTheme(new CustomTheme(darkTheme, "custom"));
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 MojangAccountList(this));
qDebug() << "Loading accounts...";
m_accounts->setListFilePath("accounts.json", true);
m_accounts->loadList();
qDebug() << "<> Accounts loaded.";
}
// init the http meta cache
{
ENV.initHttpMetaCache();
qDebug() << "<> Cache initialized.";
}
// init proxy settings
{
QString proxyTypeStr = settings()->get("ProxyType").toString();
QString addr = settings()->get("ProxyAddr").toString();
int port = settings()->get("ProxyPort").value<qint16>();
QString user = settings()->get("ProxyUser").toString();
QString pass = settings()->get("ProxyPass").toString();
ENV.updateProxySettings(proxyTypeStr, addr, port, user, pass);
qDebug() << "<> Proxy settings done.";
}
// now we have network, download translation updates
m_translations->downloadIndex();
//FIXME: what to do with these?
m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));
m_profilers.insert("jvisualvm", std::shared_ptr<BaseProfilerFactory>(new JVisualVMFactory()));
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, &MultiMC::aboutToQuit, [this](){
if(m_instances)
{
// save any remaining instance state
m_instances->saveNow();
}
if(logFile)
{
logFile->flush();
logFile->close();
}
});
{
setIconTheme(settings()->get("IconTheme").toString());
qDebug() << "<> Icon theme set.";
setApplicationTheme(settings()->get("ApplicationTheme").toString(), true);
qDebug() << "<> Application theme set.";
}
// Initialize analytics
[this]()
{
const int analyticsVersion = 2;
if(BuildConfig.ANALYTICS_ID.isEmpty())
{
return;
}
auto analyticsSetting = m_settings->getSetting("Analytics");
connect(analyticsSetting.get(), &Setting::SettingChanged, this, &MultiMC::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);
m_analytics->setNetworkAccessManager(&ENV.qnam());
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;
}();
if(createSetupWizard())
{
return;
}
performMainStartupAction();
}
bool MultiMC::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, &MultiMC::setupWizardFinished);
m_setupWizard->show();
return true;
}
return false;
}
void MultiMC::setupWizardFinished(int status)
{
qDebug() << "Wizard result =" << status;
performMainStartupAction();
}
void MultiMC::performMainStartupAction()
{
m_status = MultiMC::Initialized;
if(!m_instanceIdToLaunch.isEmpty())
{
auto inst = instances()->getInstanceById(m_instanceIdToLaunch);
if(inst)
{
MinecraftServerTargetPtr serverToJoin = nullptr;
if(!m_serverToJoin.isEmpty())
{
serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin)));
qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching with server" << m_serverToJoin;
}
else
{
qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching";
}
launch(inst, true, nullptr, serverToJoin);
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 MultiMC::showFatalErrorMessage(const QString& title, const QString& content)
{
m_status = MultiMC::Failed;
auto dialog = CustomMessageBox::selectable(nullptr, title, content, QMessageBox::Critical);
dialog->exec();
}
MultiMC::~MultiMC()
{
// kill the other globals.
Env::dispose();
// 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 MultiMC::messageReceived(const QString& message)
{
if(status() != Initialized)
{
qDebug() << "Received message" << message << "while still initializing. It will be ignored.";
return;
}
QString command = message.section(' ', 0, 0);
if(command == "activate")
{
showMainWindow();
}
else if(command == "import")
{
QString arg = message.section(' ', 1);
if(arg.isEmpty())
{
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
m_mainWindow->droppedURLs({ QUrl(arg) });
}
else if(command == "launch")
{
QString arg = message.section(' ', 1);
if(arg.isEmpty())
{
qWarning() << "Received" << command << "message without an instance ID.";
return;
}
auto inst = instances()->getInstanceById(arg);
if(inst)
{
launch(inst, true, nullptr);
}
}
else if(command == "launch-with-server")
{
QString instanceID = message.section(' ', 1, 1);
QString serverToJoin = message.section(' ', 2, 2);
if(instanceID.isEmpty())
{
qWarning() << "Received" << command << "message without an instance ID.";
return;
}
if(serverToJoin.isEmpty())
{
qWarning() << "Received" << command << "message without a server to join.";
return;
}
auto inst = instances()->getInstanceById(instanceID);
if(inst)
{
launch(
inst,
true,
nullptr,
std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(serverToJoin))
);
}
}
else
{
qWarning() << "Received invalid message" << message;
}
}
void MultiMC::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<TranslationsModel> MultiMC::translations()
{
return m_translations;
}
std::shared_ptr<JavaInstallList> MultiMC::javalist()
{
if (!m_javalist)
{
m_javalist.reset(new JavaInstallList());
}
return m_javalist;
}
std::vector<ITheme *> MultiMC::getValidApplicationThemes()
{
std::vector<ITheme *> ret;
auto iter = m_themes.cbegin();
while (iter != m_themes.cend())
{
ret.push_back((*iter).second.get());
iter++;
}
return ret;
}
void MultiMC::setApplicationTheme(const QString& name, bool initial)
{
auto systemPalette = qApp->palette();
auto themeIter = m_themes.find(name);
if(themeIter != m_themes.end())
{
auto & theme = (*themeIter).second;
theme->apply(initial);
}
else
{
qWarning() << "Tried to set invalid theme:" << name;
}
}
void MultiMC::setIconTheme(const QString& name)
{
XdgIcon::setThemeName(name);
}
QIcon MultiMC::getThemedIcon(const QString& name)
{
return XdgIcon::fromTheme(name);
}
bool MultiMC::openJsonEditor(const QString &filename)
{
const QString file = QDir::current().absoluteFilePath(filename);
if (m_settings->get("JsonEditor").toString().isEmpty())
{
return DesktopServices::openUrl(QUrl::fromLocalFile(file));
}
else
{
//return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file);
return DesktopServices::run(m_settings->get("JsonEditor").toString(), {file});
}
}
bool MultiMC::launch(
InstancePtr instance,
bool online,
BaseProfilerFactory *profiler,
MinecraftServerTargetPtr serverToJoin
) {
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);
if(window)
{
controller->setParentWidget(window);
}
else if(m_mainWindow)
{
controller->setParentWidget(m_mainWindow);
}
connect(controller.get(), &LaunchController::succeeded, this, &MultiMC::controllerSucceeded);
connect(controller.get(), &LaunchController::failed, this, &MultiMC::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 MultiMC::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 MultiMC::addRunningInstance()
{
m_runningInstances ++;
if(m_runningInstances == 1)
{
emit updateAllowedChanged(false);
}
}
void MultiMC::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 MultiMC::shouldExitNow() const
{
return m_runningInstances == 0 && m_openWindows == 0;
}
bool MultiMC::updatesAreAllowed()
{
return m_runningInstances == 0;
}
void MultiMC::updateIsRunning(bool running)
{
m_updateRunning = running;
}
void MultiMC::controllerSucceeded()
{
auto controller = qobject_cast<LaunchController *>(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 MultiMC::controllerFailed(const QString& error)
{
Q_UNUSED(error);
auto controller = qobject_cast<LaunchController *>(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 MultiMC::ShowGlobalSettings(class QWidget* parent, QString open_page)
{
if(!m_globalSettingsProvider) {
return;
}
emit globalSettingsAboutToOpen();
{
SettingsObject::Lock lock(MMC->settings());
PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent);
dlg.exec();
}
emit globalSettingsClosed();
}
MainWindow* MultiMC::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(MMC->settings()->get("MainWindowState").toByteArray()));
m_mainWindow->restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("MainWindowGeometry").toByteArray()));
if(minimized)
{
m_mainWindow->showMinimized();
}
else
{
m_mainWindow->show();
}
m_mainWindow->checkInstancePathForProblems();
connect(this, &MultiMC::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged);
connect(m_mainWindow, &MainWindow::isClosing, this, &MultiMC::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 *MultiMC::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, &MultiMC::on_windowClose);
}
if(!page.isEmpty())
{
window->selectPage(page);
}
if(extras.controller)
{
extras.controller->setParentWidget(window);
}
return window;
}
void MultiMC::on_windowClose()
{
m_openWindows--;
auto instWindow = qobject_cast<InstanceWindow *>(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<MainWindow *>(QObject::sender());
if(mainWindow)
{
m_mainWindow = nullptr;
}
// quit when there are no more windows.
if(shouldExitNow())
{
exit(0);
}
}